From 38eb00a643daba06a2959e2978e1555e8a8df663 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Thu, 29 Aug 2024 17:56:50 +0300 Subject: [PATCH 001/259] feat(editor): Update new canvas node handle label rendering mechanism and design (no-changelog) (#10611) --- .../design-system/src/css/_tokens.dark.scss | 6 +++ packages/design-system/src/css/_tokens.scss | 6 +++ .../render-types/CanvasHandleMainInput.vue | 9 ++-- .../render-types/CanvasHandleMainOutput.vue | 25 +++++++-- .../render-types/CanvasHandleNonMainInput.vue | 3 +- .../CanvasHandleNonMainOutput.vue | 3 +- .../render-types/parts/CanvasHandlePlus.vue | 46 ++++++++--------- .../CanvasHandleDiamond.spec.ts.snap | 2 +- .../CanvasHandlePlus.spec.ts.snap | 2 +- .../src/composables/useCanvasMapping.spec.ts | 12 +++++ .../src/composables/useCanvasMapping.ts | 51 ++++++++++++++++++- packages/editor-ui/src/types/canvas.ts | 8 +++ 12 files changed, 135 insertions(+), 38 deletions(-) diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index 8865fb2e3e..398a00e538 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -72,6 +72,12 @@ --color-canvas-read-only-line: var(--prim-gray-800); --color-canvas-selected: var(--prim-gray-0-alpha-025); --color-canvas-selected-transparent: var(--color-canvas-selected); + --color-canvas-label-background: hsla( + var(--color-canvas-background-h), + var(--color-canvas-background-s), + var(--color-canvas-background-l), + 0.85 + ); // Nodes --color-node-background: var(--prim-gray-740); diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index 7b1adf0d36..3176b4e398 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -80,6 +80,12 @@ --color-canvas-read-only-line: var(--prim-gray-30); --color-canvas-selected: var(--prim-gray-70); --color-canvas-selected-transparent: hsla(var(--prim-gray-h), 47%, 30%, 0.1); + --color-canvas-label-background: hsla( + var(--color-canvas-background-h), + var(--color-canvas-background-s), + var(--color-canvas-background-l), + 0.85 + ); // Nodes --color-node-background: var(--color-background-xlight); diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue index f34cc07789..27abce02a8 100644 --- a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue @@ -22,13 +22,14 @@ const handleClasses = 'target'; .label { position: absolute; - top: 20px; - left: 50%; - transform: translate(-50%, 0); + top: 50%; + left: calc(var(--spacing-xs) * -1); + transform: translate(-100%, -50%); font-size: var(--font-size-2xs); color: var(--color-foreground-xdark); - background: var(--color-background-light); + background: var(--color-canvas-label-background); z-index: 1; text-align: center; + white-space: nowrap; } diff --git a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue index 9a37a3aff7..8f8ca9c6ea 100644 --- a/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue +++ b/packages/editor-ui/src/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue @@ -1,17 +1,31 @@ diff --git a/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue index 658f2da9d5..01c7b4071d 100644 --- a/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue +++ b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue @@ -1,191 +1,190 @@ - - +
{{ $locale.baseText('resourceLocator.mode.list.searchRequired') }} @@ -225,14 +224,14 @@ export default defineComponent({
- +
diff --git a/packages/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue b/packages/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue index 41ba8d1a1b..5051e095d8 100644 --- a/packages/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue +++ b/packages/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue @@ -108,7 +108,9 @@ function setWidth() { } } -function onInputChange(value: string): void { +function onInputChange(value: NodeParameterValue): void { + if (typeof value !== 'string') return; + const params: INodeParameterResourceLocator = { __rl: true, value, mode: selectedMode.value }; if (isListMode.value) { const resource = workflowsStore.getWorkflowById(value); @@ -119,7 +121,7 @@ function onInputChange(value: string): void { emit('update:modelValue', params); } -function onListItemSelected(value: string) { +function onListItemSelected(value: NodeParameterValue) { onInputChange(value); hideDropdown(); } From 6c1f23777515ef89cb93b0fe6ee5545fb256a34b Mon Sep 17 00:00:00 2001 From: Marc Littlemore Date: Thu, 29 Aug 2024 17:21:58 +0100 Subject: [PATCH 003/259] docs: Add missing changelog entry (#10609) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ff1d8de1..ed7b53506e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ * **core:** Make execution queries faster ([#9817](https://github.com/n8n-io/n8n/issues/9817)) ([dc7dc99](https://github.com/n8n-io/n8n/commit/dc7dc995d5e2ea8fbd0dcb54cfa8aa93ecb437c9)) +### Other +* **Add user journey link to [n8n.io](https://n8n.io)** ([#10331](https://github.com/n8n-io/n8n/pull/10331)) # [1.56.0](https://github.com/n8n-io/n8n/compare/n8n@1.55.0...n8n@1.56.0) (2024-08-21) From eb074c04c92255727a443777921ada2ee000fd23 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:40:48 +0300 Subject: [PATCH 004/259] fix: Reduce variability in benchmarks (no-changelog) (#10606) --- packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf | 4 ++-- packages/@n8n/benchmark/src/testExecution/k6Executor.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf b/packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf index d671253006..b7a3f18d77 100644 --- a/packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf +++ b/packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf @@ -21,8 +21,8 @@ variable "ssh_public_key" { variable "vm_size" { description = "VM Size" - # 4 vCPUs, 16 GiB memory - default = "Standard_DC4s_v2" + # 8 vCPUs, 32 GiB memory + default = "Standard_DC8_v2" } variable "tags" { diff --git a/packages/@n8n/benchmark/src/testExecution/k6Executor.ts b/packages/@n8n/benchmark/src/testExecution/k6Executor.ts index 07c0e4787e..a491f84a32 100644 --- a/packages/@n8n/benchmark/src/testExecution/k6Executor.ts +++ b/packages/@n8n/benchmark/src/testExecution/k6Executor.ts @@ -45,7 +45,7 @@ export function handleSummary(data) { const augmentedTestScriptPath = this.augmentSummaryScript(scenario, scenarioRunName); const runDirPath = path.dirname(augmentedTestScriptPath); - const flags: K6CliFlag[] = [['--quiet'], ['--duration', '1m'], ['--vus', '5']]; + const flags: K6CliFlag[] = [['--quiet'], ['--duration', '3m'], ['--vus', '5']]; if (this.opts.k6ApiToken) { flags.push(['--out', 'cloud']); From 47eb28d767eca561b82aa124d74d986922f36e65 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:41:09 +0300 Subject: [PATCH 005/259] ci: Run nightly benchmark against nightly n8n image (no-changelog) (#10588) --- .github/workflows/benchmark-nightly.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark-nightly.yml b/.github/workflows/benchmark-nightly.yml index 0100d12a5d..3f22676358 100644 --- a/.github/workflows/benchmark-nightly.yml +++ b/.github/workflows/benchmark-nightly.yml @@ -1,5 +1,5 @@ name: Run Nightly Benchmark -run-name: Benchmark ${{ inputs.n8n_tag }} +run-name: Benchmark ${{ inputs.n8n_tag || 'nightly' }} on: schedule: @@ -60,10 +60,10 @@ jobs: - name: Run the benchmark with debug logging if: github.event.inputs.debug == 'true' - run: pnpm run-in-cloud --debug + run: pnpm run-in-cloud --n8nTag ${{ inputs.n8n_tag || 'nightly' }} --benchmarkTag ${{ inputs.benchmark_tag || 'latest' }} --debug working-directory: packages/@n8n/benchmark - name: Run the benchmark if: github.event.inputs.debug != 'true' - run: pnpm run-in-cloud + run: pnpm run-in-cloud --n8nTag ${{ inputs.n8n_tag || 'nightly' }} --benchmarkTag ${{ inputs.benchmark_tag || 'latest' }} working-directory: packages/@n8n/benchmark From 1c5164c786f2ca64becf41d8448a19988bcc7bda Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:46:55 +0300 Subject: [PATCH 006/259] feat: Add local orchestration of benchmarks (no-changelog) (#10589) --- packages/@n8n/benchmark/README.md | 55 ++++--- packages/@n8n/benchmark/package.json | 4 +- .../scripts/{runOnVm => }/bootstrap.sh | 0 .../scripts/clients/dockerComposeClient.mjs | 45 ++++++ .../scripts/{ => clients}/sshClient.mjs | 0 .../scripts/{ => clients}/terraformClient.mjs | 3 +- .../sqlite-legacy/docker-compose.yml | 2 +- .../n8nSetups/sqlite/docker-compose.yml | 2 +- packages/@n8n/benchmark/scripts/run.mjs | 151 ++++++++++++++++++ .../@n8n/benchmark/scripts/runForN8nSetup.mjs | 97 +++++++++++ .../@n8n/benchmark/scripts/runInCloud.mjs | 134 +++------------- .../@n8n/benchmark/scripts/runLocally.mjs | 61 +++++++ .../benchmark/scripts/runOnVm/runOnVm.mjs | 75 --------- 13 files changed, 415 insertions(+), 214 deletions(-) rename packages/@n8n/benchmark/scripts/{runOnVm => }/bootstrap.sh (100%) create mode 100644 packages/@n8n/benchmark/scripts/clients/dockerComposeClient.mjs rename packages/@n8n/benchmark/scripts/{ => clients}/sshClient.mjs (100%) rename packages/@n8n/benchmark/scripts/{ => clients}/terraformClient.mjs (92%) rename packages/@n8n/benchmark/scripts/{runOnVm => }/n8nSetups/sqlite-legacy/docker-compose.yml (94%) rename packages/@n8n/benchmark/scripts/{runOnVm => }/n8nSetups/sqlite/docker-compose.yml (95%) create mode 100755 packages/@n8n/benchmark/scripts/run.mjs create mode 100755 packages/@n8n/benchmark/scripts/runForN8nSetup.mjs create mode 100755 packages/@n8n/benchmark/scripts/runLocally.mjs delete mode 100755 packages/@n8n/benchmark/scripts/runOnVm/runOnVm.mjs diff --git a/packages/@n8n/benchmark/README.md b/packages/@n8n/benchmark/README.md index a16e03572a..d2b7784fa6 100644 --- a/packages/@n8n/benchmark/README.md +++ b/packages/@n8n/benchmark/README.md @@ -1,8 +1,38 @@ # n8n benchmarking tool -Tool for executing benchmarks against an n8n instance. +Tool for executing benchmarks against an n8n instance. The tool consists of these components: -## Running locally with Docker +## Directory structure + +```text +packages/@n8n/benchmark +├── scenarios Benchmark scenarios +├── src Source code for the n8n-benchmark cli +├── Dockerfile Dockerfile for the n8n-benchmark cli +├── scripts Orchestration scripts +``` + +## Running the entire benchmark suite + +The benchmark suite consists of [benchmark scenarios](#benchmark-scenarios) and different [n8n setups](#n8n-setups). + +### locally + +```sh +pnpm run-locally +``` + +### In the cloud + +```sh +pnpm run-in-cloud +``` + +## Running the `n8n-benchmark` cli + +The `n8n-benchmark` cli is a node.js program that runs one or more scenarios against a single n8n instance. + +### Locally with Docker Build the Docker image: @@ -23,7 +53,7 @@ docker run \ n8n-benchmark ``` -## Running locally without Docker +### Locally without Docker Requirements: @@ -35,23 +65,8 @@ pnpm build # Run tests against http://localhost:5678 with specified email and password N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run - -# If you installed k6 using brew, you might have to specify it explicitly -K6_PATH=/opt/homebrew/bin/k6 N8N_USER_EMAIL=user@n8n.io N8N_USER_PASSWORD=password ./bin/n8n-benchmark run ``` -## Running in the cloud - -There's a script to run the performance tests in a cloud environment. The script provisions a cloud environment, sets up n8n in the environment, runs the tests and destroys the environment. - -```sh -pnpm run-in-cloud -``` - -## Configuration - -The configuration options the cli accepts can be seen from [config.ts](./src/config/config.ts) - ## Benchmark scenarios A benchmark scenario defines one or multiple steps to execute and measure. It consists of: @@ -61,3 +76,7 @@ A benchmark scenario defines one or multiple steps to execute and measure. It co - A [`k6`](https://grafana.com/docs/k6/latest/using-k6/http-requests/) script which executes the steps and receives `API_BASE_URL` environment variable in runtime. Available scenarios are located in [`./scenarios`](./scenarios/). + +## n8n setups + +A n8n setup defines a single n8n runtime configuration using Docker compose. Different n8n setups are located in [`./scripts/n8nSetups`](./scripts/n8nSetups). diff --git a/packages/@n8n/benchmark/package.json b/packages/@n8n/benchmark/package.json index 1e73381427..2d9affef5d 100644 --- a/packages/@n8n/benchmark/package.json +++ b/packages/@n8n/benchmark/package.json @@ -10,7 +10,9 @@ "start": "./bin/n8n-benchmark", "test": "echo \"Error: no test specified\" && exit 1", "typecheck": "tsc --noEmit", - "run-in-cloud": "zx scripts/runInCloud.mjs", + "benchmark": "zx scripts/run.mjs", + "benchmark-in-cloud": "pnpm benchmark --env cloud", + "benchmark-locally": "pnpm benchmark --env local", "destroy-cloud-env": "zx scripts/destroyCloudEnv.mjs", "watch": "concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\"" }, diff --git a/packages/@n8n/benchmark/scripts/runOnVm/bootstrap.sh b/packages/@n8n/benchmark/scripts/bootstrap.sh similarity index 100% rename from packages/@n8n/benchmark/scripts/runOnVm/bootstrap.sh rename to packages/@n8n/benchmark/scripts/bootstrap.sh diff --git a/packages/@n8n/benchmark/scripts/clients/dockerComposeClient.mjs b/packages/@n8n/benchmark/scripts/clients/dockerComposeClient.mjs new file mode 100644 index 0000000000..064893a572 --- /dev/null +++ b/packages/@n8n/benchmark/scripts/clients/dockerComposeClient.mjs @@ -0,0 +1,45 @@ +import { which } from 'zx'; + +export class DockerComposeClient { + /** + * + * @param {{ $: Shell; verbose?: boolean }} opts + */ + constructor({ $ }) { + this.$$ = $; + } + + async $(...args) { + await this.resolveExecutableIfNeeded(); + + if (this.isCompose) { + return await this.$$`docker-compose ${args}`; + } else { + return await this.$$`docker compose ${args}`; + } + } + + async resolveExecutableIfNeeded() { + if (this.isResolved) { + return; + } + + // The VM deployment doesn't have `docker compose` available, + // so try to resolve the `docker-compose` first + const compose = await which('docker-compose', { nothrow: true }); + if (compose) { + this.isResolved = true; + this.isCompose = true; + return; + } + + const docker = await which('docker', { nothrow: true }); + if (docker) { + this.isResolved = true; + this.isCompose = false; + return; + } + + throw new Error('Could not resolve docker-compose or docker'); + } +} diff --git a/packages/@n8n/benchmark/scripts/sshClient.mjs b/packages/@n8n/benchmark/scripts/clients/sshClient.mjs similarity index 100% rename from packages/@n8n/benchmark/scripts/sshClient.mjs rename to packages/@n8n/benchmark/scripts/clients/sshClient.mjs diff --git a/packages/@n8n/benchmark/scripts/terraformClient.mjs b/packages/@n8n/benchmark/scripts/clients/terraformClient.mjs similarity index 92% rename from packages/@n8n/benchmark/scripts/terraformClient.mjs rename to packages/@n8n/benchmark/scripts/clients/terraformClient.mjs index 1ba4fcedb4..bfbf914faa 100644 --- a/packages/@n8n/benchmark/scripts/terraformClient.mjs +++ b/packages/@n8n/benchmark/scripts/clients/terraformClient.mjs @@ -9,8 +9,7 @@ const paths = { }; export class TerraformClient { - constructor({ privateKeyPath, isVerbose = false }) { - this.privateKeyPath = privateKeyPath; + constructor({ isVerbose = false }) { this.isVerbose = isVerbose; this.$$ = $({ cwd: paths.infraCodeDir, diff --git a/packages/@n8n/benchmark/scripts/runOnVm/n8nSetups/sqlite-legacy/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8nSetups/sqlite-legacy/docker-compose.yml similarity index 94% rename from packages/@n8n/benchmark/scripts/runOnVm/n8nSetups/sqlite-legacy/docker-compose.yml rename to packages/@n8n/benchmark/scripts/n8nSetups/sqlite-legacy/docker-compose.yml index 02b61961f1..4210339f8e 100644 --- a/packages/@n8n/benchmark/scripts/runOnVm/n8nSetups/sqlite-legacy/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8nSetups/sqlite-legacy/docker-compose.yml @@ -7,7 +7,7 @@ services: ports: - 5678:5678 volumes: - - /n8n:/n8n + - ${RUN_DIR}:/n8n benchmark: image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest} depends_on: diff --git a/packages/@n8n/benchmark/scripts/runOnVm/n8nSetups/sqlite/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8nSetups/sqlite/docker-compose.yml similarity index 95% rename from packages/@n8n/benchmark/scripts/runOnVm/n8nSetups/sqlite/docker-compose.yml rename to packages/@n8n/benchmark/scripts/n8nSetups/sqlite/docker-compose.yml index 3d953b04d2..36ad256fd2 100644 --- a/packages/@n8n/benchmark/scripts/runOnVm/n8nSetups/sqlite/docker-compose.yml +++ b/packages/@n8n/benchmark/scripts/n8nSetups/sqlite/docker-compose.yml @@ -9,7 +9,7 @@ services: ports: - 5678:5678 volumes: - - /n8n:/n8n + - ${RUN_DIR}:/n8n benchmark: image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest} depends_on: diff --git a/packages/@n8n/benchmark/scripts/run.mjs b/packages/@n8n/benchmark/scripts/run.mjs new file mode 100755 index 0000000000..fe268843e7 --- /dev/null +++ b/packages/@n8n/benchmark/scripts/run.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env zx +/** + * Script to run benchmarks either on the cloud benchmark environment or locally. + * + * NOTE: Must be run in the root of the package. + * + * Usage: + * zx scripts/run.mjs + * + */ +// @ts-check +import fs from 'fs'; +import minimist from 'minimist'; +import path from 'path'; +import { runInCloud } from './runInCloud.mjs'; +import { runLocally } from './runLocally.mjs'; + +const paths = { + n8nSetupsDir: path.join(path.resolve('scripts'), 'n8nSetups'), +}; + +async function main() { + const config = await parseAndValidateConfig(); + + const n8nSetupsToUse = + config.n8nSetupToUse === 'all' ? readAvailableN8nSetups() : [config.n8nSetupToUse]; + + console.log('Using n8n tag', config.n8nTag); + console.log('Using benchmark cli tag', config.benchmarkTag); + console.log('Using environment', config.env); + console.log('Using n8n setups', n8nSetupsToUse.join(', ')); + console.log(''); + + if (config.env === 'cloud') { + await runInCloud({ + benchmarkTag: config.benchmarkTag, + isVerbose: config.isVerbose, + k6ApiToken: config.k6ApiToken, + n8nTag: config.n8nTag, + n8nSetupsToUse, + }); + } else { + await runLocally({ + benchmarkTag: config.benchmarkTag, + isVerbose: config.isVerbose, + k6ApiToken: config.k6ApiToken, + n8nTag: config.n8nTag, + runDir: config.runDir, + n8nSetupsToUse, + }); + } +} + +function readAvailableN8nSetups() { + const setups = fs.readdirSync(paths.n8nSetupsDir); + + return setups; +} + +/** + * @typedef {Object} Config + * @property {boolean} isVerbose + * @property {'cloud' | 'local'} env + * @property {string} n8nSetupToUse + * @property {string} n8nTag + * @property {string} benchmarkTag + * @property {string} [k6ApiToken] + * @property {string} [runDir] + * + * @returns {Promise} + */ +async function parseAndValidateConfig() { + const args = minimist(process.argv.slice(3), { + boolean: ['debug', 'help'], + }); + + if (args.help) { + printUsage(); + process.exit(0); + } + + const n8nSetupToUse = await getAndValidateN8nSetup(args); + const isVerbose = args.debug || false; + const n8nTag = args.n8nTag || process.env.N8N_DOCKER_TAG || 'latest'; + const benchmarkTag = args.benchmarkTag || process.env.BENCHMARK_DOCKER_TAG || 'latest'; + const k6ApiToken = args.k6ApiToken || process.env.K6_API_TOKEN || undefined; + const runDir = args.runDir || undefined; + const env = args.env || 'local'; + + if (!env) { + printUsage(); + process.exit(1); + } + + return { + isVerbose, + env, + n8nSetupToUse, + n8nTag, + benchmarkTag, + k6ApiToken, + runDir, + }; +} + +/** + * @param {minimist.ParsedArgs} args + */ +async function getAndValidateN8nSetup(args) { + // Last parameter is the n8n setup to use + const n8nSetupToUse = args._[args._.length - 1]; + if (!n8nSetupToUse || n8nSetupToUse === 'all') { + return 'all'; + } + + const availableSetups = readAvailableN8nSetups(); + + if (!availableSetups.includes(n8nSetupToUse)) { + printUsage(); + process.exit(1); + } + + return n8nSetupToUse; +} + +function printUsage() { + const availableSetups = readAvailableN8nSetups(); + + console.log(`Usage: zx scripts/${path.basename(__filename)} [n8n setup name]`); + console.log(` eg: zx scripts/${path.basename(__filename)}`); + console.log(''); + console.log('Options:'); + console.log( + ` [n8n setup name] Against which n8n setup to run the benchmarks. One of: ${['all', ...availableSetups].join(', ')}. Default is all`, + ); + console.log( + ' --env Env where to run the benchmarks. Either cloud or local. Default is local.', + ); + console.log(' --debug Enable verbose output'); + console.log(' --n8nTag Docker tag for n8n image. Default is latest'); + console.log(' --benchmarkTag Docker tag for benchmark cli image. Default is latest'); + console.log( + ' --k6ApiToken API token for k6 cloud. Default is read from K6_API_TOKEN env var. If omitted, k6 cloud will not be used', + ); + console.log( + ' --runDir Directory to share with the n8n container for storing data. Needed only for local runs.', + ); + console.log(''); +} + +main().catch(console.error); diff --git a/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs b/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs new file mode 100755 index 0000000000..1bd31a1b5e --- /dev/null +++ b/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env zx +/** + * This script runs the benchmarks for the given n8n setup. + */ +// @ts-check +import path from 'path'; +import { $, argv, fs } from 'zx'; +import { DockerComposeClient } from './clients/dockerComposeClient.mjs'; + +const paths = { + n8nSetupsDir: path.join(__dirname, 'n8nSetups'), +}; + +async function main() { + const [n8nSetupToUse] = argv._; + validateN8nSetup(n8nSetupToUse); + + const composeFilePath = path.join(paths.n8nSetupsDir, n8nSetupToUse); + const n8nTag = argv.n8nDockerTag || process.env.N8N_DOCKER_TAG || 'latest'; + const benchmarkTag = argv.benchmarkDockerTag || process.env.BENCHMARK_DOCKER_TAG || 'latest'; + const k6ApiToken = argv.k6ApiToken || process.env.K6_API_TOKEN || undefined; + const runDir = argv.runDir || process.env.RUN_DIR || '/n8n'; + + if (!fs.existsSync(runDir)) { + console.error( + `The run directory "${runDir}" does not exist. Please specify a valid directory using --runDir`, + ); + process.exit(1); + } + + const dockerComposeClient = new DockerComposeClient({ + $: $({ + cwd: composeFilePath, + verbose: true, + env: { + N8N_VERSION: n8nTag, + BENCHMARK_VERSION: benchmarkTag, + K6_API_TOKEN: k6ApiToken, + RUN_DIR: runDir, + }, + }), + }); + + try { + await dockerComposeClient.$('up', '-d', 'n8n'); + + await dockerComposeClient.$('run', 'benchmark', 'run', `--scenarioNamePrefix=${n8nSetupToUse}`); + } catch (error) { + console.error('An error occurred while running the benchmarks:'); + console.error(error); + console.error(''); + await dumpN8nInstanceLogs(dockerComposeClient); + } finally { + await dockerComposeClient.$('down'); + } +} + +async function dumpN8nInstanceLogs(dockerComposeClient) { + console.error('n8n instance logs:'); + await dockerComposeClient.$('logs', 'n8n'); +} + +function printUsage() { + const availableSetups = getAllN8nSetups(); + console.log('Usage: zx runForN8nSetup.mjs --runDir /path/for/n8n/data '); + console.log(` eg: zx runForN8nSetup.mjs --runDir /path/for/n8n/data ${availableSetups[0]}`); + console.log(''); + console.log('Flags:'); + console.log( + ' --runDir Directory to share with the n8n container for storing data. Default is /n8n', + ); + console.log(' --n8nDockerTag Docker tag for n8n image. Default is latest'); + console.log( + ' --benchmarkDockerTag Docker tag for benchmark cli image. Default is latest', + ); + console.log(' --k6ApiToken K6 API token to upload the results'); + console.log(''); + console.log('Available setups:'); + console.log(availableSetups.join(', ')); +} + +/** + * @returns {string[]} + */ +function getAllN8nSetups() { + return fs.readdirSync(paths.n8nSetupsDir); +} + +function validateN8nSetup(givenSetup) { + const availableSetups = getAllN8nSetups(); + if (!availableSetups.includes(givenSetup)) { + printUsage(); + process.exit(1); + } +} + +main(); diff --git a/packages/@n8n/benchmark/scripts/runInCloud.mjs b/packages/@n8n/benchmark/scripts/runInCloud.mjs index e1c71d37d2..dee3a30f06 100755 --- a/packages/@n8n/benchmark/scripts/runInCloud.mjs +++ b/packages/@n8n/benchmark/scripts/runInCloud.mjs @@ -7,18 +7,12 @@ * 3. Destroy the cloud environment. * * NOTE: Must be run in the root of the package. - * - * Usage: - * zx scripts/runBenchmarksOnCloud.mjs [--debug] - * */ // @ts-check -import fs from 'fs'; -import minimist from 'minimist'; import { sleep, which } from 'zx'; import path from 'path'; -import { SshClient } from './sshClient.mjs'; -import { TerraformClient } from './terraformClient.mjs'; +import { SshClient } from './clients/sshClient.mjs'; +import { TerraformClient } from './clients/terraformClient.mjs'; /** * @typedef {Object} BenchmarkEnv @@ -27,19 +21,20 @@ import { TerraformClient } from './terraformClient.mjs'; const RESOURCE_GROUP_NAME = 'n8n-benchmarking'; -const paths = { - n8nSetupsDir: path.join(path.resolve('scripts'), 'runOnVm', 'n8nSetups'), -}; - -async function main() { - const config = await parseAndValidateConfig(); +/** + * @typedef {Object} Config + * @property {boolean} isVerbose + * @property {string[]} n8nSetupsToUse + * @property {string} n8nTag + * @property {string} benchmarkTag + * @property {string} [k6ApiToken] + * + * @param {Config} config + */ +export async function runInCloud(config) { await ensureDependencies(); - console.log('Using n8n tag', config.n8nTag); - console.log('Using benchmark cli tag', config.benchmarkTag); - const terraformClient = new TerraformClient({ - privateKeyPath: paths.privateKeyPath, isVerbose: config.isVerbose, }); @@ -65,7 +60,7 @@ async function ensureDependencies() { * @param {BenchmarkEnv} benchmarkEnv */ async function runBenchmarksOnVm(config, benchmarkEnv) { - console.log(`Setting up the environment for ${config.n8nSetupToUse}...`); + console.log(`Setting up the environment...`); const sshClient = new SshClient({ vmName: benchmarkEnv.vmName, @@ -85,23 +80,12 @@ async function runBenchmarksOnVm(config, benchmarkEnv) { // Give some time for the VM to be ready await sleep(1000); - if (config.n8nSetupToUse === 'all') { - const availableSetups = readAvailableN8nSetups(); - - for (const n8nSetup of availableSetups) { - await runBenchmarkForN8nSetup({ - config, - sshClient, - scriptsDir, - n8nSetup, - }); - } - } else { + for (const n8nSetup of config.n8nSetupsToUse) { await runBenchmarkForN8nSetup({ config, sshClient, scriptsDir, - n8nSetup: config.n8nSetupToUse, + n8nSetup, }); } } @@ -111,7 +95,7 @@ async function runBenchmarksOnVm(config, benchmarkEnv) { */ async function runBenchmarkForN8nSetup({ config, sshClient, scriptsDir, n8nSetup }) { console.log(`Running benchmarks for ${n8nSetup}...`); - const runScriptPath = path.join(scriptsDir, 'runOnVm.mjs'); + const runScriptPath = path.join(scriptsDir, 'runForN8nSetup.mjs'); const flags = { n8nDockerTag: config.n8nTag, @@ -142,87 +126,5 @@ async function transferScriptsToVm(sshClient) { await sshClient.ssh('git clone --depth=1 https://github.com/n8n-io/n8n.git'); - return '~/n8n/packages/@n8n/benchmark/scripts/runOnVm'; + return '~/n8n/packages/@n8n/benchmark/scripts'; } - -function readAvailableN8nSetups() { - const setups = fs.readdirSync(paths.n8nSetupsDir); - - return setups; -} - -/** - * @typedef {Object} Config - * @property {boolean} isVerbose - * @property {string} n8nSetupToUse - * @property {string} n8nTag - * @property {string} benchmarkTag - * @property {string} [k6ApiToken] - * - * @returns {Promise} - */ -async function parseAndValidateConfig() { - const args = minimist(process.argv.slice(3), { - boolean: ['debug', 'help'], - }); - - if (args.help) { - printUsage(); - process.exit(0); - } - - const n8nSetupToUse = await getAndValidateN8nSetup(args); - const isVerbose = args.debug || false; - const n8nTag = args.n8nTag || process.env.N8N_DOCKER_TAG || 'latest'; - const benchmarkTag = args.benchmarkTag || process.env.BENCHMARK_DOCKER_TAG || 'latest'; - const k6ApiToken = args.k6ApiToken || process.env.K6_API_TOKEN || undefined; - - return { - isVerbose, - n8nSetupToUse, - n8nTag, - benchmarkTag, - k6ApiToken, - }; -} - -/** - * @param {minimist.ParsedArgs} args - */ -async function getAndValidateN8nSetup(args) { - // Last parameter is the n8n setup to use - const n8nSetupToUse = args._[args._.length - 1]; - if (!n8nSetupToUse || n8nSetupToUse === 'all') { - return 'all'; - } - - const availableSetups = readAvailableN8nSetups(); - - if (!availableSetups.includes(n8nSetupToUse)) { - printUsage(); - process.exit(1); - } - - return n8nSetupToUse; -} - -function printUsage() { - const availableSetups = readAvailableN8nSetups(); - - console.log('Usage: zx scripts/runInCloud.mjs [n8n setup name]'); - console.log(' eg: zx scripts/runInCloud.mjs'); - console.log(''); - console.log('Options:'); - console.log( - ` [n8n setup name] Against which n8n setup to run the benchmarks. One of: ${['all', ...availableSetups].join(', ')}. Default is all`, - ); - console.log(' --debug Enable verbose output'); - console.log(' --n8nTag Docker tag for n8n image. Default is latest'); - console.log(' --benchmarkTag Docker tag for benchmark cli image. Default is latest'); - console.log( - ' --k6ApiToken API token for k6 cloud. Default is read from K6_API_TOKEN env var. If omitted, k6 cloud will not be used', - ); - console.log(''); -} - -main().catch(console.error); diff --git a/packages/@n8n/benchmark/scripts/runLocally.mjs b/packages/@n8n/benchmark/scripts/runLocally.mjs new file mode 100755 index 0000000000..8fcdcacfb7 --- /dev/null +++ b/packages/@n8n/benchmark/scripts/runLocally.mjs @@ -0,0 +1,61 @@ +#!/usr/bin/env zx +/** + * Script to run benchmarks on the cloud benchmark environment. + * This script will: + * 1. Provision a benchmark environment using Terraform. + * 2. Run the benchmarks on the VM. + * 3. Destroy the cloud environment. + * + * NOTE: Must be run in the root of the package. + */ +// @ts-check +import { $ } from 'zx'; +import path from 'path'; + +/** + * @typedef {Object} BenchmarkEnv + * @property {string} vmName + */ + +const paths = { + scriptsDir: path.join(path.resolve('scripts')), +}; + +/** + * @typedef {Object} Config + * @property {boolean} isVerbose + * @property {string[]} n8nSetupsToUse + * @property {string} n8nTag + * @property {string} benchmarkTag + * @property {string} [runDir] + * @property {string} [k6ApiToken] + * + * @param {Config} config + */ +export async function runLocally(config) { + const runScriptPath = path.join(paths.scriptsDir, 'runForN8nSetup.mjs'); + + const flags = Object.entries({ + n8nDockerTag: config.n8nTag, + benchmarkDockerTag: config.benchmarkTag, + runDir: config.runDir, + }) + .filter(([, value]) => value !== undefined) + .map(([key, value]) => `--${key}=${value}`); + + try { + for (const n8nSetup of config.n8nSetupsToUse) { + console.log(`Running benchmarks for n8n setup: ${n8nSetup}`); + + await $({ + env: { + ...process.env, + K6_API_TOKEN: config.k6ApiToken, + }, + })`npx ${runScriptPath} ${flags} ${n8nSetup}`; + } + } catch (error) { + console.error('An error occurred while running the benchmarks:'); + console.error(error); + } +} diff --git a/packages/@n8n/benchmark/scripts/runOnVm/runOnVm.mjs b/packages/@n8n/benchmark/scripts/runOnVm/runOnVm.mjs deleted file mode 100755 index fd853203ac..0000000000 --- a/packages/@n8n/benchmark/scripts/runOnVm/runOnVm.mjs +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env zx -/** - * This script runs the benchmarks using a given docker compose setup - */ -// @ts-check -import path from 'path'; -import { $, argv, fs } from 'zx'; - -const paths = { - n8nSetupsDir: path.join(__dirname, 'n8nSetups'), -}; - -async function main() { - const [n8nSetupToUse] = argv._; - validateN8nSetup(n8nSetupToUse); - - const composeFilePath = path.join(paths.n8nSetupsDir, n8nSetupToUse); - const n8nTag = argv.n8nDockerTag || process.env.N8N_DOCKER_TAG || 'latest'; - const benchmarkTag = argv.benchmarkDockerTag || process.env.BENCHMARK_DOCKER_TAG || 'latest'; - const k6ApiToken = argv.k6ApiToken || process.env.K6_API_TOKEN || undefined; - - const $$ = $({ - cwd: composeFilePath, - verbose: true, - env: { - N8N_VERSION: n8nTag, - BENCHMARK_VERSION: benchmarkTag, - K6_API_TOKEN: k6ApiToken, - }, - }); - - try { - await $$`docker-compose up -d n8n`; - - await $$`docker-compose run benchmark run --scenarioNamePrefix=${n8nSetupToUse} `; - } catch (error) { - console.error('An error occurred while running the benchmarks:'); - console.error(error); - console.error(''); - await dumpN8nInstanceLogs($$); - } finally { - await $$`docker-compose down`; - } -} - -async function dumpN8nInstanceLogs($$) { - console.error('n8n instance logs:'); - await $$`docker-compose logs n8n`; -} - -function printUsage() { - const availableSetups = getAllN8nSetups(); - console.log('Usage: zx runOnVm.mjs '); - console.log(` eg: zx runOnVm.mjs ${availableSetups[0]}`); - console.log(''); - console.log('Available setups:'); - console.log(availableSetups.join(', ')); -} - -/** - * @returns {string[]} - */ -function getAllN8nSetups() { - return fs.readdirSync(paths.n8nSetupsDir); -} - -function validateN8nSetup(givenSetup) { - const availableSetups = getAllN8nSetups(); - if (!availableSetups.includes(givenSetup)) { - printUsage(); - process.exit(1); - } -} - -main(); From 1b409b4e3e3968110dbe0055a947ac9ce20271ca Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 08:47:35 +0300 Subject: [PATCH 007/259] fix: Fix edge case in log in (no-changelog) (#10610) --- .../n8nApiClient/authenticatedN8nApiClient.ts | 23 ++++++++++++++++--- .../src/n8nApiClient/n8nApiClient.ts | 8 +++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/@n8n/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts b/packages/@n8n/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts index b7ebc6800d..f734cb2ac6 100644 --- a/packages/@n8n/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts +++ b/packages/@n8n/benchmark/src/n8nApiClient/authenticatedN8nApiClient.ts @@ -1,4 +1,3 @@ -import { strict as assert } from 'node:assert'; import type { AxiosRequestConfig } from 'axios'; import { N8nApiClient } from './n8nApiClient'; @@ -16,15 +15,33 @@ export class AuthenticatedN8nApiClient extends N8nApiClient { email: string; password: string; }, - ) { + ): Promise { const response = await apiClient.restApiRequest('/login', { method: 'POST', data: loginDetails, }); + if (response.data === 'n8n is starting up. Please wait') { + await apiClient.delay(1000); + return await this.createUsingUsernameAndPassword(apiClient, loginDetails); + } + const cookieHeader = response.headers['set-cookie']; const authCookie = Array.isArray(cookieHeader) ? cookieHeader.join('; ') : cookieHeader; - assert(authCookie); + if (!authCookie) { + throw new Error( + 'Did not receive authentication cookie even tho login succeeded: ' + + JSON.stringify( + { + status: response.status, + headers: response.headers, + data: response.data, + }, + null, + 2, + ), + ); + } return new AuthenticatedN8nApiClient(apiClient.apiBaseUrl, authCookie); } diff --git a/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts index a188c4eefe..dd81fa9cfb 100644 --- a/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts +++ b/packages/@n8n/benchmark/src/n8nApiClient/n8nApiClient.ts @@ -75,11 +75,11 @@ export class N8nApiClient { } } + async delay(ms: number): Promise { + return await new Promise((resolve) => setTimeout(resolve, ms)); + } + protected getRestEndpointUrl(endpoint: string) { return `${this.apiBaseUrl}/rest${endpoint}`; } - - private async delay(ms: number): Promise { - return await new Promise((resolve) => setTimeout(resolve, ms)); - } } From 1dcb814ced7cfbc80eddbb4bc03108341a9f27f5 Mon Sep 17 00:00:00 2001 From: Cornelius Suermann Date: Fri, 30 Aug 2024 08:04:36 +0200 Subject: [PATCH 008/259] fix(API): Update express-openapi-validator to resolve AIKIDO-2024-10229 (#10612) --- packages/cli/package.json | 2 +- pnpm-lock.yaml | 274 ++++++++++++++++++++++++++------------ 2 files changed, 188 insertions(+), 88 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 93be64e63c..0de30e5ac5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -115,7 +115,7 @@ "express": "4.19.2", "express-async-errors": "3.1.1", "express-handlebars": "7.1.2", - "express-openapi-validator": "5.3.1", + "express-openapi-validator": "5.3.3", "express-prom-bundle": "6.6.0", "express-rate-limit": "7.2.0", "fast-glob": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91d6bbb24d..b769d8a6a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -559,7 +559,7 @@ importers: version: 8.1.4(@types/react@18.0.27)(encoding@0.1.13)(prettier@3.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/addon-interactions': specifier: ^8.1.4 - version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) + version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) '@storybook/addon-links': specifier: ^8.1.4 version: 8.1.4(react@18.2.0) @@ -571,7 +571,7 @@ importers: version: 8.1.4(@types/react@18.0.27)(encoding@0.1.13)(prettier@3.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/test': specifier: ^8.1.4 - version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) + version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) '@storybook/vue3': specifier: ^8.1.4 version: 8.1.4(encoding@0.1.13)(prettier@3.2.5)(vue@3.4.21(typescript@5.5.2)) @@ -741,8 +741,8 @@ importers: specifier: 7.1.2 version: 7.1.2 express-openapi-validator: - specifier: 5.3.1 - version: 5.3.1(express@4.19.2) + specifier: 5.3.3 + version: 5.3.3(express@4.19.2) express-prom-bundle: specifier: 6.6.0 version: 6.6.0(prom-client@13.2.0) @@ -1820,7 +1820,7 @@ importers: devDependencies: '@langchain/core': specifier: ^0.2.18 - version: 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) + version: 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -7973,8 +7973,8 @@ packages: resolution: {integrity: sha512-ss9d3mBChOLTEtyfzXCsxlItUxpgS3i4cb/F70G6Q5ohQzmD12XB4x/Y9U6YboeeYBJZt7WQ5yUNu7ZSQ/EGyQ==} engines: {node: '>=v16'} - express-openapi-validator@5.3.1: - resolution: {integrity: sha512-Mlo3N1yvaZJlIs/nX0ig4xSu4g1CmLK/InRuqrXPmiqijfHa5qx/5ng92kq2dfTKd77XE7e9sPJqkI79asqNlQ==} + express-openapi-validator@5.3.3: + resolution: {integrity: sha512-WolpeQTubIlPOjKZiMmFL+pvztAq4bpL/ylvKjs2v7JN/8qC2J2k21MuYcz0o5vQBt4cdIg2cZ2I1Ik2R3FG6w==} peerDependencies: express: '*' @@ -13130,8 +13130,8 @@ packages: vue-component-type-helpers@2.0.19: resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==} - vue-component-type-helpers@2.0.29: - resolution: {integrity: sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==} + vue-component-type-helpers@2.1.2: + resolution: {integrity: sha512-URuxnrOhO9lUG4LOAapGWBaa/WOLDzzyAbL+uKZqT7RS+PFy0cdXI2mUSh7GaMts6vtHaeVbGk7trd0FPJi65Q==} vue-demi@0.14.5: resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} @@ -16045,8 +16045,8 @@ snapshots: url-join: 4.0.1 zod: 3.23.8 optionalDependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) - langchain: 0.2.11(m7otbkpxyspoz5trt2pa3dcs6u) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + langchain: 0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1) transitivePeerDependencies: - encoding @@ -16469,7 +16469,7 @@ snapshots: '@langchain/anthropic@0.2.9(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: '@anthropic-ai/sdk': 0.22.0(encoding@0.1.13) - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) fast-xml-parser: 4.3.5 zod: 3.23.8 zod-to-json-schema: 3.23.0(zod@3.23.8) @@ -16481,7 +16481,7 @@ snapshots: '@langchain/cohere@0.0.10(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) cohere-ai: 7.10.1(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -16559,23 +16559,78 @@ snapshots: - pyodide - supports-color - '@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0)': + '@langchain/community@0.2.20(yatsnfdsa55wls5pvl4axjr4ti)': dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.1.39(@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0))(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) - ml-distance: 4.0.1 - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/openai': 0.2.5(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1)) + binary-extensions: 2.2.0 + expr-eval: 2.0.2 + flat: 5.0.2 + js-yaml: 4.1.0 + langchain: 0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1) + langsmith: 0.1.34(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)) uuid: 10.0.0 zod: 3.23.8 zod-to-json-schema: 3.23.0(zod@3.23.8) + optionalDependencies: + '@aws-sdk/client-bedrock-runtime': 3.535.0 + '@aws-sdk/client-s3': 3.478.0 + '@aws-sdk/credential-provider-node': 3.535.0 + '@azure/storage-blob': 12.18.0(encoding@0.1.13) + '@getzep/zep-cloud': 1.0.11(@langchain/core@0.2.18)(encoding@0.1.13)(langchain@0.2.11) + '@getzep/zep-js': 0.9.0 + '@google-ai/generativelanguage': 2.5.0(encoding@0.1.13) + '@google-cloud/storage': 7.12.1(encoding@0.1.13) + '@huggingface/inference': 2.7.0 + '@mozilla/readability': 0.5.0 + '@pinecone-database/pinecone': 3.0.0 + '@qdrant/js-client-rest': 1.9.0(typescript@5.5.2) + '@smithy/eventstream-codec': 2.2.0 + '@smithy/protocol-http': 3.3.0 + '@smithy/signature-v4': 2.2.1 + '@smithy/util-utf8': 2.3.0 + '@supabase/postgrest-js': 1.15.2 + '@supabase/supabase-js': 2.43.4 + '@xata.io/client': 0.28.4(typescript@5.5.2) + cheerio: 1.0.0-rc.12 + cohere-ai: 7.10.1(encoding@0.1.13) + crypto-js: 4.2.0 + d3-dsv: 2.0.0 + epub2: 3.0.2(ts-toolbelt@9.6.0) + google-auth-library: 9.10.0(encoding@0.1.13) + html-to-text: 9.0.5 + ignore: 5.2.4 + ioredis: 5.3.2 + jsdom: 23.0.1 + jsonwebtoken: 9.0.2 + lodash: 4.17.21 + mammoth: 1.7.2 + mongodb: 6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1) + mysql2: 3.11.0 + pdf-parse: 1.1.1 + pg: 8.12.0 + redis: 4.6.14 + ws: 8.17.1 transitivePeerDependencies: - - langchain + - '@gomomento/sdk-web' + - '@langchain/anthropic' + - '@langchain/aws' + - '@langchain/cohere' + - '@langchain/google-genai' + - '@langchain/google-vertexai' + - '@langchain/google-vertexai-web' + - '@langchain/groq' + - '@langchain/mistralai' + - '@langchain/ollama' + - axios + - encoding + - fast-xml-parser + - handlebars - openai + - peggy + - pyodide + - supports-color + optional: true '@langchain/core@0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))': dependencies: @@ -16595,9 +16650,27 @@ snapshots: - langchain - openai - '@langchain/google-common@0.0.22(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': + '@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.12 + langsmith: 0.1.39(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)) + ml-distance: 4.0.1 + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.23.8 + zod-to-json-schema: 3.23.0(zod@3.23.8) + transitivePeerDependencies: + - langchain + - openai + + '@langchain/google-common@0.0.22(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': + dependencies: + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) uuid: 10.0.0 zod-to-json-schema: 3.23.0(zod@3.23.8) transitivePeerDependencies: @@ -16605,10 +16678,10 @@ snapshots: - openai - zod - '@langchain/google-gauth@0.0.21(encoding@0.1.13)(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': + '@langchain/google-gauth@0.0.21(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) - '@langchain/google-common': 0.0.22(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/google-common': 0.0.22(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) google-auth-library: 8.9.0(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -16620,7 +16693,7 @@ snapshots: '@langchain/google-genai@0.0.23(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': dependencies: '@google/generative-ai': 0.7.1 - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) zod-to-json-schema: 3.23.0(zod@3.23.8) transitivePeerDependencies: - langchain @@ -16629,8 +16702,8 @@ snapshots: '@langchain/google-vertexai@0.0.21(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) - '@langchain/google-gauth': 0.0.21(encoding@0.1.13)(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/google-gauth': 0.0.21(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) transitivePeerDependencies: - encoding - langchain @@ -16640,8 +16713,8 @@ snapshots: '@langchain/groq@0.0.15(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) - '@langchain/openai': 0.2.5(encoding@0.1.13)(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u)) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/openai': 0.2.5(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1)) groq-sdk: 0.3.2(encoding@0.1.13) zod: 3.23.8 zod-to-json-schema: 3.23.0(zod@3.23.8) @@ -16653,7 +16726,7 @@ snapshots: '@langchain/mistralai@0.0.27(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) '@mistralai/mistralai': 0.4.0(encoding@0.1.13) uuid: 10.0.0 zod: 3.23.8 @@ -16665,13 +16738,25 @@ snapshots: '@langchain/ollama@0.0.2(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) ollama: 0.5.6 uuid: 10.0.0 transitivePeerDependencies: - langchain - openai + '@langchain/openai@0.2.5(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))': + dependencies: + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + js-tiktoken: 1.0.12 + openai: 4.53.0(encoding@0.1.13) + zod: 3.23.8 + zod-to-json-schema: 3.23.0(zod@3.23.8) + transitivePeerDependencies: + - encoding + - langchain + - supports-color + '@langchain/openai@0.2.5(encoding@0.1.13)(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))': dependencies: '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) @@ -16684,19 +16769,6 @@ snapshots: - langchain - supports-color - '@langchain/openai@0.2.5(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))': - dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) - js-tiktoken: 1.0.12 - openai: 4.53.0(encoding@0.1.13) - zod: 3.23.8 - zod-to-json-schema: 3.23.0(zod@3.23.8) - transitivePeerDependencies: - - encoding - - langchain - - supports-color - optional: true - '@langchain/pinecone@0.0.8(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))': dependencies: '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) @@ -16725,9 +16797,9 @@ snapshots: - langchain - openai - '@langchain/textsplitters@0.0.3(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0)': + '@langchain/textsplitters@0.0.3(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) js-tiktoken: 1.0.12 transitivePeerDependencies: - langchain @@ -18040,11 +18112,11 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/addon-interactions@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': + '@storybook/addon-interactions@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))': dependencies: '@storybook/global': 5.0.0 '@storybook/instrumenter': 8.1.4 - '@storybook/test': 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) + '@storybook/test': 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) '@storybook/types': 8.1.4 polished: 4.2.2 ts-dedent: 2.2.0 @@ -18492,14 +18564,14 @@ snapshots: - prettier - supports-color - '@storybook/test@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': + '@storybook/test@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))': dependencies: '@storybook/client-logger': 8.1.4 '@storybook/core-events': 8.1.4 '@storybook/instrumenter': 8.1.4 '@storybook/preview-api': 8.1.4 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) + '@testing-library/jest-dom': 6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) '@vitest/expect': 1.3.1 '@vitest/spy': 1.3.1 @@ -18562,7 +18634,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.5.2) - vue-component-type-helpers: 2.0.29 + vue-component-type-helpers: 2.1.2 transitivePeerDependencies: - encoding - prettier @@ -18662,7 +18734,7 @@ snapshots: jest: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) vitest: 1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1) - '@testing-library/jest-dom@6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': + '@testing-library/jest-dom@6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))': dependencies: '@adobe/css-tools': 4.3.2 '@babel/runtime': 7.23.6 @@ -21613,7 +21685,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -21638,7 +21710,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.2) eslint: 8.57.0 @@ -21658,7 +21730,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -21937,7 +22009,7 @@ snapshots: graceful-fs: 4.2.11 handlebars: 4.7.8 - express-openapi-validator@5.3.1(express@4.19.2): + express-openapi-validator@5.3.3(express@4.19.2): dependencies: '@apidevtools/json-schema-ref-parser': 11.7.0 '@types/multer': 1.4.11 @@ -22528,7 +22600,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -23879,17 +23951,17 @@ snapshots: kuler@2.0.0: {} - langchain@0.2.11(axios@1.7.4)(openai@4.53.0): + langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1): dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) - '@langchain/openai': 0.2.5(langchain@0.2.11(axios@1.7.4)(openai@4.53.0)) - '@langchain/textsplitters': 0.0.3(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/openai': 0.2.5(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1)) + '@langchain/textsplitters': 0.0.3(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)) binary-extensions: 2.2.0 js-tiktoken: 1.0.12 js-yaml: 4.1.0 jsonpointer: 5.0.1 langchainhub: 0.0.8 - langsmith: 0.1.34(@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0))(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) + langsmith: 0.1.34(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)) ml-distance: 4.0.1 openapi-types: 12.1.3 p-retry: 4.6.2 @@ -23898,7 +23970,35 @@ snapshots: zod: 3.23.8 zod-to-json-schema: 3.23.0(zod@3.23.8) optionalDependencies: + '@aws-sdk/client-s3': 3.478.0 + '@aws-sdk/credential-provider-node': 3.535.0 + '@azure/storage-blob': 12.18.0(encoding@0.1.13) + '@langchain/anthropic': 0.2.9(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/cohere': 0.0.10(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/community': 0.2.20(yatsnfdsa55wls5pvl4axjr4ti) + '@langchain/google-genai': 0.0.23(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) + '@langchain/google-vertexai': 0.0.21(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) + '@langchain/groq': 0.0.15(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/mistralai': 0.0.27(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/ollama': 0.0.2(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@pinecone-database/pinecone': 3.0.0 + '@supabase/supabase-js': 2.43.4 + '@xata.io/client': 0.28.4(typescript@5.5.2) axios: 1.7.4(debug@4.3.6) + cheerio: 1.0.0-rc.12 + d3-dsv: 2.0.0 + epub2: 3.0.2(ts-toolbelt@9.6.0) + fast-xml-parser: 4.4.1 + handlebars: 4.7.8 + html-to-text: 9.0.5 + ignore: 5.2.4 + ioredis: 5.3.2 + jsdom: 23.0.1 + mammoth: 1.7.2 + mongodb: 6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1) + pdf-parse: 1.1.1 + redis: 4.6.14 + ws: 8.17.1 transitivePeerDependencies: - encoding - openai @@ -23959,20 +24059,6 @@ snapshots: langchainhub@0.0.8: {} - langsmith@0.1.34(@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0))(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0): - dependencies: - '@types/uuid': 9.0.7 - commander: 10.0.1 - lodash.set: 4.3.2 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 9.0.1 - optionalDependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) - langchain: 0.2.11(axios@1.7.4)(openai@4.53.0) - openai: 4.53.0(encoding@0.1.13) - optional: true - langsmith@0.1.34(@langchain/core@0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)): dependencies: '@types/uuid': 9.0.7 @@ -23986,17 +24072,19 @@ snapshots: langchain: 0.2.11(m7otbkpxyspoz5trt2pa3dcs6u) openai: 4.53.0(encoding@0.1.13) - langsmith@0.1.39(@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0))(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0): + langsmith@0.1.34(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)): dependencies: '@types/uuid': 9.0.7 commander: 10.0.1 + lodash.set: 4.3.2 p-queue: 6.6.2 p-retry: 4.6.2 uuid: 9.0.1 optionalDependencies: - '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) - langchain: 0.2.11(axios@1.7.4)(openai@4.53.0) + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + langchain: 0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1) openai: 4.53.0(encoding@0.1.13) + optional: true langsmith@0.1.39(@langchain/core@0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)): dependencies: @@ -24010,6 +24098,18 @@ snapshots: langchain: 0.2.11(m7otbkpxyspoz5trt2pa3dcs6u) openai: 4.53.0(encoding@0.1.13) + langsmith@0.1.39(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)): + dependencies: + '@types/uuid': 9.0.7 + commander: 10.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 9.0.1 + optionalDependencies: + '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + langchain: 0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1) + openai: 4.53.0(encoding@0.1.13) + lazy-ass@1.6.0: {} lazy-universal-dotenv@4.0.0: @@ -25549,7 +25649,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -26435,7 +26535,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -28046,7 +28146,7 @@ snapshots: vue-component-type-helpers@2.0.19: {} - vue-component-type-helpers@2.0.29: {} + vue-component-type-helpers@2.1.2: {} vue-demi@0.14.5(vue@3.4.21(typescript@5.5.2)): dependencies: From 4e899ea55fad938751ec40bcf4478aef12f05159 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 09:41:50 +0300 Subject: [PATCH 009/259] feat: Add n8n postgres setup to benchmarks (no-changelog) (#10604) --- .../n8nSetups/postgres/docker-compose.yml | 29 +++++++++++++++++++ .../@n8n/benchmark/scripts/runForN8nSetup.mjs | 11 ++++--- 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 packages/@n8n/benchmark/scripts/n8nSetups/postgres/docker-compose.yml diff --git a/packages/@n8n/benchmark/scripts/n8nSetups/postgres/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8nSetups/postgres/docker-compose.yml new file mode 100644 index 0000000000..7e9c48e01b --- /dev/null +++ b/packages/@n8n/benchmark/scripts/n8nSetups/postgres/docker-compose.yml @@ -0,0 +1,29 @@ +services: + postgres: + image: postgres:16 + restart: always + environment: + - POSTGRES_DB=n8n + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + n8n: + image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest} + environment: + - N8N_DIAGNOSTICS_ENABLED=false + - N8N_USER_FOLDER=/n8n + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_PASSWORD=password + ports: + - 5678:5678 + volumes: + - ${RUN_DIR}:/n8n + depends_on: + - postgres + benchmark: + image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest} + depends_on: + - n8n + environment: + - N8N_BASE_URL=http://n8n:5678 + - K6_API_TOKEN=${K6_API_TOKEN} diff --git a/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs b/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs index 1bd31a1b5e..c7f1e88904 100755 --- a/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs +++ b/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs @@ -19,15 +19,18 @@ async function main() { const n8nTag = argv.n8nDockerTag || process.env.N8N_DOCKER_TAG || 'latest'; const benchmarkTag = argv.benchmarkDockerTag || process.env.BENCHMARK_DOCKER_TAG || 'latest'; const k6ApiToken = argv.k6ApiToken || process.env.K6_API_TOKEN || undefined; - const runDir = argv.runDir || process.env.RUN_DIR || '/n8n'; + const baseRunDir = argv.runDir || process.env.RUN_DIR || '/n8n'; - if (!fs.existsSync(runDir)) { + if (!fs.existsSync(baseRunDir)) { console.error( - `The run directory "${runDir}" does not exist. Please specify a valid directory using --runDir`, + `The run directory "${baseRunDir}" does not exist. Please specify a valid directory using --runDir`, ); process.exit(1); } + const runDir = path.join(baseRunDir, n8nSetupToUse); + fs.emptyDirSync(runDir); + const dockerComposeClient = new DockerComposeClient({ $: $({ cwd: composeFilePath, @@ -42,7 +45,7 @@ async function main() { }); try { - await dockerComposeClient.$('up', '-d', 'n8n'); + await dockerComposeClient.$('up', '-d', '--remove-orphans', 'n8n'); await dockerComposeClient.$('run', 'benchmark', 'run', `--scenarioNamePrefix=${n8nSetupToUse}`); } catch (error) { From 7e9d186496f8391fbbf7e1cd833ed2beb5fada56 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 09:42:01 +0300 Subject: [PATCH 010/259] feat: Add queue mode setup to benchmarks (no-changelog) (#10608) --- .../n8nSetups/queue/docker-compose.yml | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 packages/@n8n/benchmark/scripts/n8nSetups/queue/docker-compose.yml diff --git a/packages/@n8n/benchmark/scripts/n8nSetups/queue/docker-compose.yml b/packages/@n8n/benchmark/scripts/n8nSetups/queue/docker-compose.yml new file mode 100644 index 0000000000..d8e35885e8 --- /dev/null +++ b/packages/@n8n/benchmark/scripts/n8nSetups/queue/docker-compose.yml @@ -0,0 +1,73 @@ +services: + redis: + image: redis:6-alpine + ports: + - 6379:6379 + postgres: + image: postgres:16 + restart: always + environment: + - POSTGRES_DB=n8n + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + n8n_worker1: + image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest} + environment: + - N8N_DIAGNOSTICS_ENABLED=false + - N8N_USER_FOLDER=/n8n/worker1 + - N8N_ENCRYPTION_KEY=very-secret-encryption-key + - EXECUTIONS_MODE=queue + - QUEUE_BULL_REDIS_HOST=redis + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_PASSWORD=password + command: worker + volumes: + - ${RUN_DIR}:/n8n + depends_on: + - postgres + - redis + n8n_worker2: + image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest} + environment: + - N8N_DIAGNOSTICS_ENABLED=false + - N8N_USER_FOLDER=/n8n/worker2 + - N8N_ENCRYPTION_KEY=very-secret-encryption-key + - EXECUTIONS_MODE=queue + - QUEUE_BULL_REDIS_HOST=redis + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_PASSWORD=password + command: worker + volumes: + - ${RUN_DIR}:/n8n + depends_on: + - postgres + - redis + n8n: + image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest} + environment: + - N8N_DIAGNOSTICS_ENABLED=false + - N8N_USER_FOLDER=/n8n/main + - N8N_ENCRYPTION_KEY=very-secret-encryption-key + - EXECUTIONS_MODE=queue + - QUEUE_BULL_REDIS_HOST=redis + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_PASSWORD=password + ports: + - 5678:5678 + volumes: + - ${RUN_DIR}:/n8n + depends_on: + - postgres + - redis + - n8n_worker1 + - n8n_worker2 + benchmark: + image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest} + depends_on: + - n8n + environment: + - N8N_BASE_URL=http://n8n:5678 + - K6_API_TOKEN=${K6_API_TOKEN} From c5c903ba8b25594802529d2b6565617bece125cb Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:48:26 +0300 Subject: [PATCH 011/259] ci: Fix nightly image not being pushed to ghcr (#10620) --- .github/workflows/docker-images-nightly.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-images-nightly.yml b/.github/workflows/docker-images-nightly.yml index 189b690cbd..b6831b6399 100644 --- a/.github/workflows/docker-images-nightly.yml +++ b/.github/workflows/docker-images-nightly.yml @@ -32,6 +32,9 @@ on: required: false default: '' +env: + N8N_TAG: ${{ inputs.tag || 'nightly' }} + jobs: build: runs-on: ubuntu-latest @@ -76,10 +79,10 @@ jobs: push: true cache-from: type=gha cache-to: type=gha,mode=max - tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.tag || 'nightly' }} + tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ env.N8N_TAG }} - name: Login to GitHub Container Registry - if: github.event.inputs.tag == 'nightly' + if: env.N8N_TAG == 'nightly' uses: docker/login-action@v3.0.0 with: registry: ghcr.io @@ -87,7 +90,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Push image to GHCR - if: github.event.inputs.tag == 'nightly' + if: env.N8N_TAG == 'nightly' run: | docker buildx imagetools create \ --tag ghcr.io/${{ github.repository_owner }}/n8n:nightly \ From 1e08f444c942129ca4594f3960f2b5984524a6ef Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:48:34 +0300 Subject: [PATCH 012/259] ci: Fix script name in gh workflow (#10619) --- .github/workflows/benchmark-nightly.yml | 4 ++-- packages/@n8n/benchmark/README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/benchmark-nightly.yml b/.github/workflows/benchmark-nightly.yml index 3f22676358..f413a4aab7 100644 --- a/.github/workflows/benchmark-nightly.yml +++ b/.github/workflows/benchmark-nightly.yml @@ -60,10 +60,10 @@ jobs: - name: Run the benchmark with debug logging if: github.event.inputs.debug == 'true' - run: pnpm run-in-cloud --n8nTag ${{ inputs.n8n_tag || 'nightly' }} --benchmarkTag ${{ inputs.benchmark_tag || 'latest' }} --debug + run: pnpm benchmark-in-cloud --n8nTag ${{ inputs.n8n_tag || 'nightly' }} --benchmarkTag ${{ inputs.benchmark_tag || 'latest' }} --debug working-directory: packages/@n8n/benchmark - name: Run the benchmark if: github.event.inputs.debug != 'true' - run: pnpm run-in-cloud --n8nTag ${{ inputs.n8n_tag || 'nightly' }} --benchmarkTag ${{ inputs.benchmark_tag || 'latest' }} + run: pnpm benchmark-in-cloud --n8nTag ${{ inputs.n8n_tag || 'nightly' }} --benchmarkTag ${{ inputs.benchmark_tag || 'latest' }} working-directory: packages/@n8n/benchmark diff --git a/packages/@n8n/benchmark/README.md b/packages/@n8n/benchmark/README.md index d2b7784fa6..b3a0a4d548 100644 --- a/packages/@n8n/benchmark/README.md +++ b/packages/@n8n/benchmark/README.md @@ -19,13 +19,13 @@ The benchmark suite consists of [benchmark scenarios](#benchmark-scenarios) and ### locally ```sh -pnpm run-locally +pnpm benchmark-locally ``` ### In the cloud ```sh -pnpm run-in-cloud +pnpm benchmark-in-cloud ``` ## Running the `n8n-benchmark` cli From 9fa1a9aa99de93682966dbeeac8a28b5ff0f77a9 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:59:30 +0300 Subject: [PATCH 013/259] fix: Disable errors obfuscation (no-changelog) (#10617) --- .../agents/ConversationalAgent/execute.ts | 2 +- .../agents/OpenAiFunctionsAgent/execute.ts | 2 +- .../agents/PlanAndExecuteAgent/execute.ts | 2 +- .../agents/Agent/agents/ReActAgent/execute.ts | 2 +- .../agents/Agent/agents/SqlAgent/execute.ts | 2 +- .../agents/Agent/agents/ToolsAgent/execute.ts | 2 +- .../OpenAiAssistant/OpenAiAssistant.node.ts | 2 +- .../nodes/chains/ChainLLM/ChainLlm.node.ts | 2 +- .../ChainRetrievalQA/ChainRetrievalQa.node.ts | 2 +- .../V2/ChainSummarizationV2.node.ts | 2 +- .../InformationExtractor.node.ts | 2 +- .../SentimentAnalysis.node.ts | 2 +- .../TextClassifier/TextClassifier.node.ts | 2 +- .../nodes-langchain/nodes/code/Code.node.ts | 2 +- .../nodes/vendors/OpenAi/actions/router.ts | 2 +- packages/core/src/NodeExecuteFunctions.ts | 9 +--- packages/core/src/WorkflowExecute.ts | 2 - .../src/components/Error/NodeErrorView.vue | 4 -- .../src/plugins/i18n/locales/en.json | 1 - .../nodes/ActionNetwork/ActionNetwork.node.ts | 2 +- .../ActiveCampaign/ActiveCampaign.node.ts | 2 +- .../nodes/Affinity/Affinity.node.ts | 2 +- .../nodes/AiTransform/AiTransform.node.ts | 2 +- .../nodes/Airtable/v1/AirtableV1.node.ts | 10 ++-- .../v2/actions/base/getSchema.operation.ts | 2 +- .../v2/actions/record/create.operation.ts | 2 +- .../actions/record/deleteRecord.operation.ts | 2 +- .../v2/actions/record/get.operation.ts | 2 +- .../v2/actions/record/search.operation.ts | 2 +- .../v2/actions/record/update.operation.ts | 2 +- .../v2/actions/record/upsert.operation.ts | 2 +- packages/nodes-base/nodes/Amqp/Amqp.node.ts | 2 +- .../nodes/ApiTemplateIo/ApiTemplateIo.node.ts | 6 +-- packages/nodes-base/nodes/Asana/Asana.node.ts | 2 +- .../nodes/Autopilot/Autopilot.node.ts | 2 +- .../nodes-base/nodes/Aws/AwsLambda.node.ts | 2 +- packages/nodes-base/nodes/Aws/AwsSns.node.ts | 2 +- .../AwsCertificateManager.node.ts | 2 +- .../Aws/Comprehend/AwsComprehend.node.ts | 2 +- .../nodes/Aws/DynamoDB/AwsDynamoDB.node.ts | 2 +- .../nodes-base/nodes/Aws/ELB/AwsElb.node.ts | 2 +- .../Aws/Rekognition/AwsRekognition.node.ts | 2 +- .../nodes/Aws/S3/V1/AwsS3V1.node.ts | 2 +- .../nodes/Aws/S3/V2/AwsS3V2.node.ts | 2 +- .../nodes-base/nodes/Aws/SES/AwsSes.node.ts | 2 +- .../nodes-base/nodes/Aws/SQS/AwsSqs.node.ts | 2 +- .../nodes/Aws/Textract/AwsTextract.node.ts | 2 +- .../Aws/Transcribe/AwsTranscribe.node.ts | 2 +- .../nodes/BambooHr/v1/actions/router.ts | 2 +- .../nodes-base/nodes/Baserow/Baserow.node.ts | 2 +- .../nodes/Beeminder/Beeminder.node.ts | 2 +- packages/nodes-base/nodes/Bitly/Bitly.node.ts | 2 +- packages/nodes-base/nodes/Box/Box.node.ts | 2 +- .../nodes/Brandfetch/Brandfetch.node.ts | 2 +- .../nodes/Chargebee/Chargebee.node.ts | 2 +- .../nodes/CircleCi/CircleCi.node.ts | 2 +- .../nodes/Cisco/Webex/CiscoWebex.node.ts | 4 +- .../nodes/Clearbit/Clearbit.node.ts | 2 +- .../nodes-base/nodes/ClickUp/ClickUp.node.ts | 2 +- .../nodes/Clockify/Clockify.node.ts | 2 +- .../nodes/Cloudflare/Cloudflare.node.ts | 2 +- .../nodes-base/nodes/Cockpit/Cockpit.node.ts | 2 +- packages/nodes-base/nodes/Coda/Coda.node.ts | 34 +++++++------- packages/nodes-base/nodes/Code/Code.node.ts | 4 +- .../nodes/CoinGecko/CoinGecko.node.ts | 2 +- .../nodes/Compression/Compression.node.ts | 2 +- .../nodes/Contentful/Contentful.node.ts | 2 +- .../nodes/ConvertKit/ConvertKit.node.ts | 2 +- .../nodes-base/nodes/Copper/Copper.node.ts | 2 +- .../nodes-base/nodes/Cortex/Cortex.node.ts | 2 +- .../nodes-base/nodes/Crypto/Crypto.node.ts | 2 +- .../nodes/CustomerIo/CustomerIo.node.ts | 2 +- .../nodes/DateTime/V1/DateTimeV1.node.ts | 2 +- .../nodes/DateTime/V2/DateTimeV2.node.ts | 2 +- .../nodes/DebugHelper/DebugHelper.node.ts | 2 +- packages/nodes-base/nodes/DeepL/DeepL.node.ts | 2 +- packages/nodes-base/nodes/Demio/Demio.node.ts | 2 +- packages/nodes-base/nodes/Dhl/Dhl.node.ts | 2 +- .../v2/actions/channel/create.operation.ts | 2 +- .../channel/deleteChannel.operation.ts | 2 +- .../v2/actions/channel/get.operation.ts | 2 +- .../v2/actions/channel/getAll.operation.ts | 2 +- .../v2/actions/channel/update.operation.ts | 2 +- .../v2/actions/member/getAll.operation.ts | 2 +- .../v2/actions/member/roleAdd.operation.ts | 2 +- .../v2/actions/member/roleRemove.operation.ts | 2 +- .../message/deleteMessage.operation.ts | 2 +- .../v2/actions/message/get.operation.ts | 2 +- .../v2/actions/message/getAll.operation.ts | 2 +- .../v2/actions/message/react.operation.ts | 2 +- .../v2/actions/message/send.operation.ts | 2 +- .../actions/webhook/sendLegacy.operation.ts | 2 +- .../nodes/Discourse/Discourse.node.ts | 2 +- .../nodes-base/nodes/Disqus/Disqus.node.ts | 2 +- packages/nodes-base/nodes/Drift/Drift.node.ts | 2 +- .../nodes-base/nodes/Dropbox/Dropbox.node.ts | 2 +- .../nodes/EditImage/EditImage.node.ts | 2 +- packages/nodes-base/nodes/Egoi/Egoi.node.ts | 2 +- .../ElasticSecurity/ElasticSecurity.node.ts | 2 +- .../nodes/EmailSend/v1/EmailSendV1.node.ts | 2 +- .../nodes/EmailSend/v2/send.operation.ts | 2 +- .../nodes-base/nodes/Emelia/Emelia.node.ts | 2 +- .../ExecuteCommand/ExecuteCommand.node.ts | 2 +- .../ExecuteWorkflow/ExecuteWorkflow.node.ts | 4 +- .../nodes/Facebook/FacebookGraphApi.node.ts | 2 +- .../nodes/FileMaker/FileMaker.node.ts | 2 +- .../actions/spreadsheet.operation.ts | 2 +- .../actions/toBinary.operation.ts | 2 +- .../ConvertToFile/actions/toJson.operation.ts | 4 +- .../ConvertToFile/actions/toText.operation.ts | 2 +- .../actions/moveTo.operation.ts | 2 +- .../ExtractFromFile/actions/pdf.operation.ts | 2 +- .../ReadWriteFile/actions/read.operation.ts | 2 +- .../ReadWriteFile/actions/write.operation.ts | 2 +- .../nodes/Filter/V2/FilterV2.node.ts | 2 +- .../nodes/Freshdesk/Freshdesk.node.ts | 2 +- .../nodes/Freshservice/Freshservice.node.ts | 2 +- .../nodes/FreshworksCrm/FreshworksCrm.node.ts | 2 +- packages/nodes-base/nodes/Ftp/Ftp.node.ts | 4 +- .../nodes/Function/Function.node.ts | 2 +- .../nodes/FunctionItem/FunctionItem.node.ts | 4 +- .../nodes/GetResponse/GetResponse.node.ts | 2 +- packages/nodes-base/nodes/Ghost/Ghost.node.ts | 2 +- packages/nodes-base/nodes/Git/Git.node.ts | 2 +- .../nodes-base/nodes/Github/Github.node.ts | 2 +- .../nodes-base/nodes/Gitlab/Gitlab.node.ts | 4 +- .../nodes/GoToWebinar/GoToWebinar.node.ts | 2 +- .../Analytics/v1/GoogleAnalyticsV1.node.ts | 2 +- .../Google/Analytics/v2/actions/router.ts | 2 +- .../BigQuery/v1/GoogleBigQueryV1.node.ts | 4 +- .../database/executeQuery.operation.ts | 4 +- .../v2/actions/database/insert.operation.ts | 2 +- .../nodes/Google/Books/GoogleBooks.node.ts | 2 +- .../Google/Calendar/GoogleCalendar.node.ts | 2 +- .../nodes/Google/Chat/GoogleChat.node.ts | 2 +- .../Google/Contacts/GoogleContacts.node.ts | 2 +- .../nodes/Google/Docs/GoogleDocs.node.ts | 2 +- .../Google/Drive/v1/GoogleDriveV1.node.ts | 2 +- .../nodes/Google/Drive/v2/actions/router.ts | 2 +- .../GoogleFirebaseCloudFirestore.node.ts | 4 +- .../GoogleFirebaseRealtimeDatabase.node.ts | 2 +- .../Google/GSuiteAdmin/GSuiteAdmin.node.ts | 2 +- .../nodes/Google/Gmail/v1/GmailV1.node.ts | 2 +- .../nodes/Google/Gmail/v2/GmailV2.node.ts | 2 +- .../Perspective/GooglePerspective.node.ts | 2 +- .../Google/Sheet/v1/GoogleSheetsV1.node.ts | 18 +++---- .../nodes/Google/Sheet/v2/actions/router.ts | 2 +- .../nodes/Google/Slides/GoogleSlides.node.ts | 2 +- .../nodes/Google/Task/GoogleTasks.node.ts | 2 +- .../nodes/Google/YouTube/YouTube.node.ts | 2 +- .../nodes-base/nodes/Gotify/Gotify.node.ts | 2 +- .../nodes-base/nodes/Grafana/Grafana.node.ts | 2 +- .../nodes-base/nodes/GraphQL/GraphQL.node.ts | 2 +- packages/nodes-base/nodes/Grist/Grist.node.ts | 2 +- .../nodes/HackerNews/HackerNews.node.ts | 2 +- .../nodes-base/nodes/HaloPSA/HaloPSA.node.ts | 2 +- .../nodes-base/nodes/Harvest/Harvest.node.ts | 2 +- .../nodes/HelpScout/HelpScout.node.ts | 2 +- .../nodes/HomeAssistant/HomeAssistant.node.ts | 2 +- packages/nodes-base/nodes/Html/Html.node.ts | 2 +- .../nodes/HtmlExtract/HtmlExtract.node.ts | 2 +- .../nodes/Hubspot/V1/HubspotV1.node.ts | 4 +- .../nodes/Hubspot/V2/HubspotV2.node.ts | 4 +- .../nodes-base/nodes/Hunter/Hunter.node.ts | 2 +- .../nodes/ICalendar/createEvent.operation.ts | 2 +- packages/nodes-base/nodes/If/V2/IfV2.node.ts | 2 +- .../nodes/Intercom/Intercom.node.ts | 2 +- .../nodes/InvoiceNinja/InvoiceNinja.node.ts | 2 +- .../nodes-base/nodes/Jenkins/Jenkins.node.ts | 2 +- packages/nodes-base/nodes/Jwt/Jwt.node.ts | 2 +- packages/nodes-base/nodes/Kafka/Kafka.node.ts | 2 +- packages/nodes-base/nodes/Ldap/Ldap.node.ts | 4 +- .../nodes-base/nodes/Lemlist/Lemlist.node.ts | 2 +- packages/nodes-base/nodes/Line/Line.node.ts | 2 +- .../nodes-base/nodes/Linear/Linear.node.ts | 2 +- .../nodes/LinkedIn/LinkedIn.node.ts | 2 +- .../nodes/LoneScale/LoneScale.node.ts | 2 +- .../nodes-base/nodes/Magento/Magento2.node.ts | 2 +- .../nodes/Mailcheck/Mailcheck.node.ts | 2 +- .../nodes/Mailchimp/Mailchimp.node.ts | 2 +- .../nodes/MailerLite/MailerLite.node.ts | 2 +- .../nodes-base/nodes/Mailgun/Mailgun.node.ts | 2 +- .../nodes-base/nodes/Mailjet/Mailjet.node.ts | 2 +- .../nodes/Mandrill/Mandrill.node.ts | 2 +- .../nodes/Markdown/Markdown.node.ts | 2 +- .../nodes/Marketstack/Marketstack.node.ts | 2 +- .../nodes-base/nodes/Matrix/Matrix.node.ts | 2 +- .../nodes/Mattermost/v1/actions/router.ts | 2 +- .../nodes-base/nodes/Mautic/Mautic.node.ts | 2 +- .../nodes-base/nodes/Medium/Medium.node.ts | 2 +- .../nodes/MessageBird/MessageBird.node.ts | 2 +- .../Dynamics/MicrosoftDynamicsCrm.node.ts | 2 +- .../Excel/v1/MicrosoftExcelV1.node.ts | 12 ++--- .../v2/actions/table/addTable.operation.ts | 2 +- .../v2/actions/table/append.operation.ts | 2 +- .../actions/table/convertToRange.operation.ts | 2 +- .../v2/actions/table/deleteTable.operation.ts | 2 +- .../v2/actions/table/getColumns.operation.ts | 2 +- .../v2/actions/table/getRows.operation.ts | 2 +- .../v2/actions/table/lookup.operation.ts | 2 +- .../workbook/addWorksheet.operation.ts | 2 +- .../workbook/deleteWorkbook.operation.ts | 2 +- .../v2/actions/workbook/getAll.operation.ts | 2 +- .../v2/actions/worksheet/clear.operation.ts | 2 +- .../worksheet/deleteWorksheet.operation.ts | 2 +- .../v2/actions/worksheet/getAll.operation.ts | 2 +- .../actions/worksheet/readRows.operation.ts | 2 +- .../v2/actions/worksheet/update.operation.ts | 2 +- .../v2/actions/worksheet/upsert.operation.ts | 2 +- .../MicrosoftGraphSecurity.node.ts | 2 +- .../OneDrive/MicrosoftOneDrive.node.ts | 2 +- .../Outlook/v1/MicrosoftOutlookV1.node.ts | 42 ++++++++--------- .../Microsoft/Outlook/v2/actions/router.ts | 2 +- .../nodes/Microsoft/Sql/MicrosoftSql.node.ts | 4 +- .../Teams/v1/MicrosoftTeamsV1.node.ts | 2 +- .../Microsoft/Teams/v2/actions/router.ts | 2 +- .../Microsoft/ToDo/MicrosoftToDo.node.ts | 2 +- .../nodes-base/nodes/Mindee/Mindee.node.ts | 2 +- packages/nodes-base/nodes/Misp/Misp.node.ts | 2 +- .../nodes-base/nodes/Mocean/Mocean.node.ts | 2 +- .../nodes/MondayCom/MondayCom.node.ts | 2 +- .../nodes-base/nodes/MongoDb/MongoDb.node.ts | 14 +++--- .../nodes/MonicaCrm/MonicaCrm.node.ts | 2 +- .../nodes-base/nodes/MySql/v1/MySqlV1.node.ts | 6 +-- .../nodes/MySql/v2/helpers/utils.ts | 6 +-- packages/nodes-base/nodes/Nasa/Nasa.node.ts | 2 +- .../nodes-base/nodes/Netlify/Netlify.node.ts | 2 +- .../nodes/Netscaler/ADC/NetscalerAdc.node.ts | 2 +- .../nodes/NextCloud/NextCloud.node.ts | 4 +- .../nodes-base/nodes/NocoDB/NocoDB.node.ts | 10 ++-- .../nodes/Notion/v2/NotionV2.node.ts | 28 +++++------ packages/nodes-base/nodes/Odoo/Odoo.node.ts | 2 +- .../nodes/OneSimpleApi/OneSimpleApi.node.ts | 2 +- packages/nodes-base/nodes/Onfleet/Onfleet.ts | 20 ++++---- .../nodes/OpenThesaurus/OpenThesaurus.node.ts | 2 +- .../OpenWeatherMap/OpenWeatherMap.node.ts | 2 +- .../nodes-base/nodes/Paddle/Paddle.node.ts | 2 +- .../nodes/PagerDuty/PagerDuty.node.ts | 2 +- .../nodes-base/nodes/PayPal/PayPal.node.ts | 2 +- .../nodes/Phantombuster/Phantombuster.node.ts | 2 +- .../nodes/Pipedrive/Pipedrive.node.ts | 2 +- .../nodes-base/nodes/PostHog/PostHog.node.ts | 8 ++-- .../nodes/ProfitWell/ProfitWell.node.ts | 2 +- .../nodes/Pushbullet/Pushbullet.node.ts | 2 +- .../nodes/Pushover/Pushover.node.ts | 2 +- .../nodes/QuickBooks/QuickBooks.node.ts | 2 +- .../nodes/Raindrop/Raindrop.node.ts | 2 +- .../ReadBinaryFile/ReadBinaryFile.node.ts | 2 +- .../nodes-base/nodes/ReadPdf/ReadPDF.node.ts | 2 +- .../nodes-base/nodes/Reddit/Reddit.node.ts | 2 +- .../RespondToWebhook/RespondToWebhook.node.ts | 2 +- .../nodes/Rocketchat/Rocketchat.node.ts | 2 +- .../nodes/RssFeedRead/RssFeedRead.node.ts | 2 +- packages/nodes-base/nodes/S3/S3.node.ts | 2 +- .../nodes/Salesforce/Salesforce.node.ts | 2 +- .../nodes/SeaTable/SeaTable.node.ts | 10 ++-- .../nodes-base/nodes/Segment/Segment.node.ts | 2 +- .../nodes/SendGrid/SendGrid.node.ts | 20 ++++---- .../nodes/SentryIo/SentryIo.node.ts | 2 +- .../nodes/ServiceNow/ServiceNow.node.ts | 2 +- .../nodes-base/nodes/Set/v2/manual.mode.ts | 2 +- packages/nodes-base/nodes/Set/v2/raw.mode.ts | 2 +- .../nodes-base/nodes/Shopify/Shopify.node.ts | 2 +- .../nodes-base/nodes/Signl4/Signl4.node.ts | 2 +- .../nodes-base/nodes/Slack/V1/SlackV1.node.ts | 2 +- .../nodes-base/nodes/Slack/V2/SlackV2.node.ts | 2 +- packages/nodes-base/nodes/Sms77/Sms77.node.ts | 2 +- .../nodes-base/nodes/Spontit/Spontit.node.ts | 2 +- .../nodes-base/nodes/Spotify/Spotify.node.ts | 2 +- .../v1/SpreadsheetFileV1.node.ts | 4 +- .../SpreadsheetFile/v2/fromFile.operation.ts | 2 +- .../SpreadsheetFile/v2/toFile.operation.ts | 2 +- packages/nodes-base/nodes/Ssh/Ssh.node.ts | 2 +- .../nodes-base/nodes/Stackby/Stackby.node.ts | 8 ++-- .../nodes/Storyblok/Storyblok.node.ts | 2 +- .../nodes-base/nodes/Strapi/Strapi.node.ts | 2 +- .../nodes-base/nodes/Strava/Strava.node.ts | 2 +- .../nodes-base/nodes/Stripe/Stripe.node.ts | 2 +- .../nodes/Supabase/Supabase.node.ts | 10 ++-- .../nodes/Switch/V1/SwitchV1.node.ts | 2 +- .../nodes/Switch/V2/SwitchV2.node.ts | 2 +- .../nodes/Switch/V3/SwitchV3.node.ts | 2 +- .../nodes/SyncroMSP/v1/actions/router.ts | 2 +- packages/nodes-base/nodes/Taiga/Taiga.node.ts | 2 +- .../nodes/Tapfiliate/Tapfiliate.node.ts | 2 +- .../nodes/Telegram/Telegram.node.ts | 2 +- .../nodes-base/nodes/TheHive/TheHive.node.ts | 2 +- .../nodes/TheHiveProject/actions/router.ts | 2 +- .../nodes/Todoist/v1/TodoistV1.node.ts | 2 +- .../nodes/Todoist/v2/TodoistV2.node.ts | 2 +- .../nodes/TravisCi/TravisCi.node.ts | 2 +- .../nodes-base/nodes/Trello/Trello.node.ts | 2 +- .../nodes-base/nodes/Twilio/Twilio.node.ts | 2 +- packages/nodes-base/nodes/Twist/Twist.node.ts | 2 +- .../nodes/Twitter/V1/TwitterV1.node.ts | 2 +- .../nodes/Twitter/V2/TwitterV2.node.ts | 2 +- packages/nodes-base/nodes/UProc/UProc.node.ts | 2 +- .../nodes-base/nodes/Uplead/Uplead.node.ts | 2 +- .../nodes/UptimeRobot/UptimeRobot.node.ts | 2 +- .../nodes/UrlScanIo/UrlScanIo.node.ts | 2 +- .../VenafiTlsProtectDatacenter.node.ts | 2 +- .../VenafiTlsProtectCloud.node.ts | 2 +- packages/nodes-base/nodes/Vero/Vero.node.ts | 2 +- .../nodes-base/nodes/Vonage/Vonage.node.ts | 2 +- packages/nodes-base/nodes/Wekan/Wekan.node.ts | 2 +- packages/nodes-base/nodes/Wise/Wise.node.ts | 2 +- .../nodes/Wordpress/Wordpress.node.ts | 2 +- .../WriteBinaryFile/WriteBinaryFile.node.ts | 2 +- packages/nodes-base/nodes/Xero/Xero.node.ts | 2 +- packages/nodes-base/nodes/Xml/Xml.node.ts | 2 +- .../nodes-base/nodes/Yourls/Yourls.node.ts | 2 +- .../nodes-base/nodes/Zammad/Zammad.node.ts | 2 +- .../nodes-base/nodes/Zendesk/Zendesk.node.ts | 2 +- .../nodes-base/nodes/Zoho/ZohoCrm.node.ts | 2 +- packages/nodes-base/nodes/Zoom/Zoom.node.ts | 2 +- packages/nodes-base/nodes/Zulip/Zulip.node.ts | 2 +- packages/workflow/src/Constants.ts | 2 - packages/workflow/src/Interfaces.ts | 2 +- .../src/errors/node-operation.error.ts | 12 ----- .../workflow/test/errors/node.error.test.ts | 47 ------------------- 320 files changed, 445 insertions(+), 518 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts index 899fe57a74..887017ccaf 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ConversationalAgent/execute.ts @@ -114,7 +114,7 @@ export async function conversationalAgentExecute( } catch (error) { throwIfToolSchema(this, error); - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } }); continue; } diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts index 072039259f..12e1dbda4e 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/OpenAiFunctionsAgent/execute.ts @@ -116,7 +116,7 @@ export async function openAiFunctionsAgentExecute( returnData.push({ json: response }); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } }); continue; } diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts index 4c77a6a0aa..a4ae1a0f1c 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/PlanAndExecuteAgent/execute.ts @@ -93,7 +93,7 @@ export async function planAndExecuteAgentExecute( returnData.push({ json: response }); } catch (error) { throwIfToolSchema(this, error); - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } }); continue; } diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts index 1fdaafc290..11a5acb040 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ReActAgent/execute.ts @@ -114,7 +114,7 @@ export async function reActAgentAgentExecute( returnData.push({ json: response }); } catch (error) { throwIfToolSchema(this, error); - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } }); continue; } diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts index bd8868ad32..4b31491120 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/SqlAgent/execute.ts @@ -142,7 +142,7 @@ export async function sqlAgentAgentExecute( returnData.push({ json: response }); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i } }); continue; } diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts index fc760cb928..295b0a5d1d 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts @@ -225,7 +225,7 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise executeData, - continueOnFail: (error?: Error) => { - const shouldContinue = continueOnFail(node); - if (error && shouldContinue && !(error instanceof ApplicationError)) { - error.message = OBFUSCATED_ERROR_MESSAGE; - } - return shouldContinue; + continueOnFail: () => { + return continueOnFail(node); }, evaluateExpression: (expression: string, itemIndex: number) => { return workflow.expression.resolveSimpleParameterValue( diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 3e563e04b8..a7c282e434 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -1333,8 +1333,6 @@ export class WorkflowExecute { } else { // Report any unhandled and non-wrapped errors to Sentry toReport = error; - // Set obfuscate to true so that the error would be obfuscated in th UI - error.obfuscate = true; } if (toReport) { ErrorReporterProxy.error(toReport, { diff --git a/packages/editor-ui/src/components/Error/NodeErrorView.vue b/packages/editor-ui/src/components/Error/NodeErrorView.vue index 7ca1b7d2d9..d1bcb87bee 100644 --- a/packages/editor-ui/src/components/Error/NodeErrorView.vue +++ b/packages/editor-ui/src/components/Error/NodeErrorView.vue @@ -226,10 +226,6 @@ function addItemIndexSuffix(message: string): string { } function getErrorMessage(): string { - if ('obfuscate' in props.error && props.error.obfuscate === true) { - return i18n.baseText('nodeErrorView.showMessage.obfuscate'); - } - let message = ''; const isSubNodeError = diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 89d317d64e..3fd9714b8e 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1135,7 +1135,6 @@ "nodeErrorView.itemIndex": "Item Index", "nodeErrorView.runIndex": "Run Index", "nodeErrorView.showMessage.title": "Copied to clipboard", - "nodeErrorView.showMessage.obfuscate": "Internal error", "nodeErrorView.stack": "Stack", "nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed": "The error cause is too large to be displayed", "nodeErrorView.time": "Time", diff --git a/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.ts b/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.ts index 20a9cbce0e..068cf0b902 100644 --- a/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.ts +++ b/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.ts @@ -487,7 +487,7 @@ export class ActionNetwork implements INodeType { ? returnData.push(...(response as IDataObject[])) : returnData.push(response as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/ActiveCampaign/ActiveCampaign.node.ts b/packages/nodes-base/nodes/ActiveCampaign/ActiveCampaign.node.ts index 33ebbd9261..ca7ea31c3d 100644 --- a/packages/nodes-base/nodes/ActiveCampaign/ActiveCampaign.node.ts +++ b/packages/nodes-base/nodes/ActiveCampaign/ActiveCampaign.node.ts @@ -1189,7 +1189,7 @@ export class ActiveCampaign implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Affinity/Affinity.node.ts b/packages/nodes-base/nodes/Affinity/Affinity.node.ts index d526f4f5b2..d70d908704 100644 --- a/packages/nodes-base/nodes/Affinity/Affinity.node.ts +++ b/packages/nodes-base/nodes/Affinity/Affinity.node.ts @@ -420,7 +420,7 @@ export class Affinity implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/AiTransform/AiTransform.node.ts b/packages/nodes-base/nodes/AiTransform/AiTransform.node.ts index 9d21d82888..573c34ad62 100644 --- a/packages/nodes-base/nodes/AiTransform/AiTransform.node.ts +++ b/packages/nodes-base/nodes/AiTransform/AiTransform.node.ts @@ -133,7 +133,7 @@ export class AiTransform implements INodeType { try { items = (await sandbox.runCodeAllItems()) as INodeExecutionData[]; } catch (error) { - if (!this.continueOnFail(error)) { + if (!this.continueOnFail()) { set(error, 'node', node); throw error; } diff --git a/packages/nodes-base/nodes/Airtable/v1/AirtableV1.node.ts b/packages/nodes-base/nodes/Airtable/v1/AirtableV1.node.ts index b143394e92..84ddd66e06 100644 --- a/packages/nodes-base/nodes/Airtable/v1/AirtableV1.node.ts +++ b/packages/nodes-base/nodes/Airtable/v1/AirtableV1.node.ts @@ -653,7 +653,7 @@ export class AirtableV1 implements INodeType { rows.length = 0; } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } @@ -696,7 +696,7 @@ export class AirtableV1 implements INodeType { rows.length = 0; } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } @@ -757,7 +757,7 @@ export class AirtableV1 implements INodeType { }), ]; } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); } else { throw error; @@ -792,7 +792,7 @@ export class AirtableV1 implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } @@ -880,7 +880,7 @@ export class AirtableV1 implements INodeType { rows.length = 0; } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/base/getSchema.operation.ts b/packages/nodes-base/nodes/Airtable/v2/actions/base/getSchema.operation.ts index fae52ba66e..aa791f7b64 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/base/getSchema.operation.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/base/getSchema.operation.ts @@ -48,7 +48,7 @@ export async function execute( returnData.push(...executionData); } catch (error) { error = processAirtableError(error as NodeApiError, undefined, i); - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/record/create.operation.ts b/packages/nodes-base/nodes/Airtable/v2/actions/record/create.operation.ts index 3686e33bff..758c3b7a2f 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/record/create.operation.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/record/create.operation.ts @@ -87,7 +87,7 @@ export async function execute( returnData.push(...executionData); } catch (error) { error = processAirtableError(error as NodeApiError, undefined, i); - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { message: error.message, error } }); continue; } diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/record/deleteRecord.operation.ts b/packages/nodes-base/nodes/Airtable/v2/actions/record/deleteRecord.operation.ts index 100590fc5a..cbbf12ba3c 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/record/deleteRecord.operation.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/record/deleteRecord.operation.ts @@ -55,7 +55,7 @@ export async function execute( returnData.push(...executionData); } catch (error) { error = processAirtableError(error as NodeApiError, id, i); - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/record/get.operation.ts b/packages/nodes-base/nodes/Airtable/v2/actions/record/get.operation.ts index 4dcfaeda4e..73a9933076 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/record/get.operation.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/record/get.operation.ts @@ -91,7 +91,7 @@ export async function execute( returnData.push(...executionData); } catch (error) { error = processAirtableError(error as NodeApiError, id, i); - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/record/search.operation.ts b/packages/nodes-base/nodes/Airtable/v2/actions/record/search.operation.ts index beb11dc921..4d3fc8e9df 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/record/search.operation.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/record/search.operation.ts @@ -227,7 +227,7 @@ export async function execute( returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { message: error.message, error }, pairedItem: { item: i } }); continue; } else { diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/record/update.operation.ts b/packages/nodes-base/nodes/Airtable/v2/actions/record/update.operation.ts index cc9d785cfc..d2c4172dd5 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/record/update.operation.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/record/update.operation.ts @@ -138,7 +138,7 @@ export async function execute( returnData.push(...executionData); } catch (error) { error = processAirtableError(error as NodeApiError, recordId, i); - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { message: error.message, error } }); continue; } diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/record/upsert.operation.ts b/packages/nodes-base/nodes/Airtable/v2/actions/record/upsert.operation.ts index 27f9314cbc..e6266480ce 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/record/upsert.operation.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/record/upsert.operation.ts @@ -148,7 +148,7 @@ export async function execute( returnData.push(...executionData); } catch (error) { error = processAirtableError(error as NodeApiError, undefined, i); - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { message: error.message, error } }); continue; } diff --git a/packages/nodes-base/nodes/Amqp/Amqp.node.ts b/packages/nodes-base/nodes/Amqp/Amqp.node.ts index 47f1833a38..ce711d9695 100644 --- a/packages/nodes-base/nodes/Amqp/Amqp.node.ts +++ b/packages/nodes-base/nodes/Amqp/Amqp.node.ts @@ -261,7 +261,7 @@ export class Amqp implements INodeType { return [responseData]; } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { return [[{ json: { error: error.message }, pairedItems: { item: 0 } }]]; } else { throw error; diff --git a/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts b/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts index 0ffbc8d7c1..28f1a52c6a 100644 --- a/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts +++ b/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts @@ -385,7 +385,7 @@ export class ApiTemplateIo implements INodeType { returnData.push(responseData as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } @@ -471,7 +471,7 @@ export class ApiTemplateIo implements INodeType { } returnData.push(responseData as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } @@ -561,7 +561,7 @@ export class ApiTemplateIo implements INodeType { } returnData.push(responseData as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/Asana/Asana.node.ts b/packages/nodes-base/nodes/Asana/Asana.node.ts index dc980a96e0..550c386273 100644 --- a/packages/nodes-base/nodes/Asana/Asana.node.ts +++ b/packages/nodes-base/nodes/Asana/Asana.node.ts @@ -2467,7 +2467,7 @@ export class Asana implements INodeType { ), ); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Autopilot/Autopilot.node.ts b/packages/nodes-base/nodes/Autopilot/Autopilot.node.ts index 876dfdbc34..069e2dd03f 100644 --- a/packages/nodes-base/nodes/Autopilot/Autopilot.node.ts +++ b/packages/nodes-base/nodes/Autopilot/Autopilot.node.ts @@ -306,7 +306,7 @@ export class Autopilot implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const exectionErrorWithMetaData = this.helpers.constructExecutionMetaData( [{ json: { error: error.message } }], { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Aws/AwsLambda.node.ts b/packages/nodes-base/nodes/Aws/AwsLambda.node.ts index ddd95c4a22..1dc35d651b 100644 --- a/packages/nodes-base/nodes/Aws/AwsLambda.node.ts +++ b/packages/nodes-base/nodes/Aws/AwsLambda.node.ts @@ -204,7 +204,7 @@ export class AwsLambda implements INodeType { returnData.push(...executionData); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: (error as JsonObject).message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Aws/AwsSns.node.ts b/packages/nodes-base/nodes/Aws/AwsSns.node.ts index fe898d4276..858f93774a 100644 --- a/packages/nodes-base/nodes/Aws/AwsSns.node.ts +++ b/packages/nodes-base/nodes/Aws/AwsSns.node.ts @@ -318,7 +318,7 @@ export class AwsSns implements INodeType { } as IDataObject); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Aws/CertificateManager/AwsCertificateManager.node.ts b/packages/nodes-base/nodes/Aws/CertificateManager/AwsCertificateManager.node.ts index 5cdb0cfa76..25abdb7669 100644 --- a/packages/nodes-base/nodes/Aws/CertificateManager/AwsCertificateManager.node.ts +++ b/packages/nodes-base/nodes/Aws/CertificateManager/AwsCertificateManager.node.ts @@ -223,7 +223,7 @@ export class AwsCertificateManager implements INodeType { returnData.push(...executionData); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/Aws/Comprehend/AwsComprehend.node.ts b/packages/nodes-base/nodes/Aws/Comprehend/AwsComprehend.node.ts index 3d995fd01f..8a416bcd50 100644 --- a/packages/nodes-base/nodes/Aws/Comprehend/AwsComprehend.node.ts +++ b/packages/nodes-base/nodes/Aws/Comprehend/AwsComprehend.node.ts @@ -277,7 +277,7 @@ export class AwsComprehend implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Aws/DynamoDB/AwsDynamoDB.node.ts b/packages/nodes-base/nodes/Aws/DynamoDB/AwsDynamoDB.node.ts index efc7130007..cecc3aca46 100644 --- a/packages/nodes-base/nodes/Aws/DynamoDB/AwsDynamoDB.node.ts +++ b/packages/nodes-base/nodes/Aws/DynamoDB/AwsDynamoDB.node.ts @@ -399,7 +399,7 @@ export class AwsDynamoDB implements INodeType { returnData.push(...executionData); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Aws/ELB/AwsElb.node.ts b/packages/nodes-base/nodes/Aws/ELB/AwsElb.node.ts index 470b5b3546..fbc781fb6f 100644 --- a/packages/nodes-base/nodes/Aws/ELB/AwsElb.node.ts +++ b/packages/nodes-base/nodes/Aws/ELB/AwsElb.node.ts @@ -451,7 +451,7 @@ export class AwsElb implements INodeType { ), ); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: (error as JsonObject).toString() }); continue; } diff --git a/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts b/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts index 0f9be63d17..a1e24ea4b9 100644 --- a/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts +++ b/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts @@ -455,7 +455,7 @@ export class AwsRekognition implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Aws/S3/V1/AwsS3V1.node.ts b/packages/nodes-base/nodes/Aws/S3/V1/AwsS3V1.node.ts index 82de951fef..8456f4a8cf 100644 --- a/packages/nodes-base/nodes/Aws/S3/V1/AwsS3V1.node.ts +++ b/packages/nodes-base/nodes/Aws/S3/V1/AwsS3V1.node.ts @@ -893,7 +893,7 @@ export class AwsS3V1 implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Aws/S3/V2/AwsS3V2.node.ts b/packages/nodes-base/nodes/Aws/S3/V2/AwsS3V2.node.ts index 7892374659..fe97fa0761 100644 --- a/packages/nodes-base/nodes/Aws/S3/V2/AwsS3V2.node.ts +++ b/packages/nodes-base/nodes/Aws/S3/V2/AwsS3V2.node.ts @@ -1057,7 +1057,7 @@ export class AwsS3V2 implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts b/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts index 5cf0bd4dc8..4589f8a212 100644 --- a/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts +++ b/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts @@ -1285,7 +1285,7 @@ export class AwsSes implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts b/packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts index 5b4654a08b..474a928bb2 100644 --- a/packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts +++ b/packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts @@ -378,7 +378,7 @@ export class AwsSqs implements INodeType { const result = responseData.SendMessageResponse.SendMessageResult; returnData.push(result as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.description }); continue; } diff --git a/packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts b/packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts index a5173d0d14..6d032b3d89 100644 --- a/packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts +++ b/packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts @@ -144,7 +144,7 @@ export class AwsTextract implements INodeType { returnData.push(responseData as unknown as IDataObject); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Aws/Transcribe/AwsTranscribe.node.ts b/packages/nodes-base/nodes/Aws/Transcribe/AwsTranscribe.node.ts index d44f1ca0c7..013279db5d 100644 --- a/packages/nodes-base/nodes/Aws/Transcribe/AwsTranscribe.node.ts +++ b/packages/nodes-base/nodes/Aws/Transcribe/AwsTranscribe.node.ts @@ -540,7 +540,7 @@ export class AwsTranscribe implements INodeType { returnData.push(responseData as IDataObject); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/BambooHr/v1/actions/router.ts b/packages/nodes-base/nodes/BambooHr/v1/actions/router.ts index 4d3f434a2a..9e8cb85a9e 100644 --- a/packages/nodes-base/nodes/BambooHr/v1/actions/router.ts +++ b/packages/nodes-base/nodes/BambooHr/v1/actions/router.ts @@ -39,7 +39,7 @@ export async function router(this: IExecuteFunctions): Promise { x.json.error = error.reason || 'LDAP connection error occurred'; @@ -418,7 +418,7 @@ export class Ldap implements INodeType { ); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnItems.push({ json: items[itemIndex].json, error, pairedItem: itemIndex }); } else { await client.unbind(); diff --git a/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts b/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts index 63c21d7c88..9e55d83759 100644 --- a/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts +++ b/packages/nodes-base/nodes/Lemlist/Lemlist.node.ts @@ -289,7 +289,7 @@ export class Lemlist implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Line/Line.node.ts b/packages/nodes-base/nodes/Line/Line.node.ts index 7cdc4b03de..349e598722 100644 --- a/packages/nodes-base/nodes/Line/Line.node.ts +++ b/packages/nodes-base/nodes/Line/Line.node.ts @@ -129,7 +129,7 @@ export class Line implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Linear/Linear.node.ts b/packages/nodes-base/nodes/Linear/Linear.node.ts index 0657cf7d05..7ea6f2aca5 100644 --- a/packages/nodes-base/nodes/Linear/Linear.node.ts +++ b/packages/nodes-base/nodes/Linear/Linear.node.ts @@ -299,7 +299,7 @@ export class Linear implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts b/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts index b3be6c4eaa..9732176da9 100644 --- a/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts +++ b/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts @@ -282,7 +282,7 @@ export class LinkedIn implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/LoneScale/LoneScale.node.ts b/packages/nodes-base/nodes/LoneScale/LoneScale.node.ts index 521a5f9876..46efa2af38 100644 --- a/packages/nodes-base/nodes/LoneScale/LoneScale.node.ts +++ b/packages/nodes-base/nodes/LoneScale/LoneScale.node.ts @@ -466,7 +466,7 @@ export class LoneScale implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Magento/Magento2.node.ts b/packages/nodes-base/nodes/Magento/Magento2.node.ts index 835fb81441..0d7196eb75 100644 --- a/packages/nodes-base/nodes/Magento/Magento2.node.ts +++ b/packages/nodes-base/nodes/Magento/Magento2.node.ts @@ -800,7 +800,7 @@ export class Magento2 implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Mailcheck/Mailcheck.node.ts b/packages/nodes-base/nodes/Mailcheck/Mailcheck.node.ts index 5a9a30aca4..2c5562dbc4 100644 --- a/packages/nodes-base/nodes/Mailcheck/Mailcheck.node.ts +++ b/packages/nodes-base/nodes/Mailcheck/Mailcheck.node.ts @@ -98,7 +98,7 @@ export class Mailcheck implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts index 8b452a1eee..c2e63b6f93 100644 --- a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts @@ -2190,7 +2190,7 @@ export class Mailchimp implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts b/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts index 266aebcd27..6fe7c621b1 100644 --- a/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts +++ b/packages/nodes-base/nodes/MailerLite/MailerLite.node.ts @@ -182,7 +182,7 @@ export class MailerLite implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts b/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts index a36b235d45..324118a46e 100644 --- a/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts +++ b/packages/nodes-base/nodes/Mailgun/Mailgun.node.ts @@ -194,7 +194,7 @@ export class Mailgun implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: itemIndex } }, diff --git a/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts b/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts index a1d282deac..0b02a4590c 100644 --- a/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts +++ b/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts @@ -310,7 +310,7 @@ export class Mailjet implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Mandrill/Mandrill.node.ts b/packages/nodes-base/nodes/Mandrill/Mandrill.node.ts index 7371d7acfa..b673d3eba6 100644 --- a/packages/nodes-base/nodes/Mandrill/Mandrill.node.ts +++ b/packages/nodes-base/nodes/Mandrill/Mandrill.node.ts @@ -891,7 +891,7 @@ export class Mandrill implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Markdown/Markdown.node.ts b/packages/nodes-base/nodes/Markdown/Markdown.node.ts index dc38ee2567..bcd270797d 100644 --- a/packages/nodes-base/nodes/Markdown/Markdown.node.ts +++ b/packages/nodes-base/nodes/Markdown/Markdown.node.ts @@ -608,7 +608,7 @@ export class Markdown implements INodeType { returnData.push(newItem); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: (error as JsonObject).message }); continue; } diff --git a/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts b/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts index 2d7aba98e2..62617ee6fa 100644 --- a/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts +++ b/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts @@ -161,7 +161,7 @@ export class Marketstack implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Matrix/Matrix.node.ts b/packages/nodes-base/nodes/Matrix/Matrix.node.ts index eb24fb3c31..141a6c5181 100644 --- a/packages/nodes-base/nodes/Matrix/Matrix.node.ts +++ b/packages/nodes-base/nodes/Matrix/Matrix.node.ts @@ -151,7 +151,7 @@ export class Matrix implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Mattermost/v1/actions/router.ts b/packages/nodes-base/nodes/Mattermost/v1/actions/router.ts index 17ce9945fa..bd1af59af2 100644 --- a/packages/nodes-base/nodes/Mattermost/v1/actions/router.ts +++ b/packages/nodes-base/nodes/Mattermost/v1/actions/router.ts @@ -42,7 +42,7 @@ export async function router(this: IExecuteFunctions): Promise result[0]) as unknown as IDataObject[], ); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnItems = this.helpers.returnJsonArray({ error: error.message }); } else { await connection.end(); diff --git a/packages/nodes-base/nodes/MySql/v2/helpers/utils.ts b/packages/nodes-base/nodes/MySql/v2/helpers/utils.ts index daff2a131e..1e15ce71bb 100644 --- a/packages/nodes-base/nodes/MySql/v2/helpers/utils.ts +++ b/packages/nodes-base/nodes/MySql/v2/helpers/utils.ts @@ -264,7 +264,7 @@ export function configureQueryRunner( } catch (err) { const error = parseMySqlError.call(this, err, 0, formatedQueries); - if (!this.continueOnFail(err)) throw error; + if (!this.continueOnFail()) throw error; returnData.push({ json: { message: error.message, error: { ...error } } }); } } else { @@ -302,7 +302,7 @@ export function configureQueryRunner( } catch (err) { const error = parseMySqlError.call(this, err, index, [formatedQuery]); - if (!this.continueOnFail(err)) { + if (!this.continueOnFail()) { connection.release(); throw error; } @@ -352,7 +352,7 @@ export function configureQueryRunner( connection.release(); } - if (!this.continueOnFail(err)) throw error; + if (!this.continueOnFail()) throw error; returnData.push(prepareErrorItem(queries[index], error as Error, index)); // Return here because we already rolled back the transaction diff --git a/packages/nodes-base/nodes/Nasa/Nasa.node.ts b/packages/nodes-base/nodes/Nasa/Nasa.node.ts index b192c21451..b291680c59 100644 --- a/packages/nodes-base/nodes/Nasa/Nasa.node.ts +++ b/packages/nodes-base/nodes/Nasa/Nasa.node.ts @@ -1110,7 +1110,7 @@ export class Nasa implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { if (resource === 'earthImagery' && operation === 'get') { items[i].json = { error: error.message }; } else if (resource === 'astronomyPictureOfTheDay' && operation === 'get' && download) { diff --git a/packages/nodes-base/nodes/Netlify/Netlify.node.ts b/packages/nodes-base/nodes/Netlify/Netlify.node.ts index 11cd3c1fed..c2f6f93320 100644 --- a/packages/nodes-base/nodes/Netlify/Netlify.node.ts +++ b/packages/nodes-base/nodes/Netlify/Netlify.node.ts @@ -195,7 +195,7 @@ export class Netlify implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Netscaler/ADC/NetscalerAdc.node.ts b/packages/nodes-base/nodes/Netscaler/ADC/NetscalerAdc.node.ts index 5dd09f9808..2f7fff154d 100644 --- a/packages/nodes-base/nodes/Netscaler/ADC/NetscalerAdc.node.ts +++ b/packages/nodes-base/nodes/Netscaler/ADC/NetscalerAdc.node.ts @@ -240,7 +240,7 @@ export class NetscalerAdc implements INodeType { }), ); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: (error as JsonObject).toString() }); continue; } diff --git a/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts b/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts index e889bf572c..9a710c2a9c 100644 --- a/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts +++ b/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts @@ -1099,7 +1099,7 @@ export class NextCloud implements INodeType { qs, ); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { if (resource === 'file' && operation === 'download') { items[i].json = { error: error.message }; } else { @@ -1288,7 +1288,7 @@ export class NextCloud implements INodeType { returnData.push(...executionData); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { if (resource === 'file' && operation === 'download') { items[i].json = { error: error.message }; } else { diff --git a/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts b/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts index 00b46ebd02..390c3e2791 100644 --- a/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts +++ b/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts @@ -405,7 +405,7 @@ export class NocoDB implements INodeType { returnData.push(...body); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.toString() }); } throw new NodeApiError(this.getNode(), error as JsonObject); @@ -468,7 +468,7 @@ export class NocoDB implements INodeType { returnData.push(...responseData); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.toString() }); } throw new NodeApiError(this.getNode(), error as JsonObject); @@ -541,7 +541,7 @@ export class NocoDB implements INodeType { return [data]; } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.toString() } }); } else { throw error; @@ -617,7 +617,7 @@ export class NocoDB implements INodeType { newItems.push(...executionData); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.toString() }), { itemData: { item: i } }, @@ -759,7 +759,7 @@ export class NocoDB implements INodeType { returnData.push(...body); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.toString() }); } throw new NodeApiError(this.getNode(), error as JsonObject); diff --git a/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts index c69bb8ee7b..6c5205f9d0 100644 --- a/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts +++ b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts @@ -86,7 +86,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -161,7 +161,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -193,7 +193,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -236,7 +236,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -299,7 +299,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -386,7 +386,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -416,7 +416,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -511,7 +511,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -563,7 +563,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -589,7 +589,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -618,7 +618,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -652,7 +652,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -704,7 +704,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, @@ -766,7 +766,7 @@ export class NotionV2 implements INodeType { ); returnData = returnData.concat(executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: i }, diff --git a/packages/nodes-base/nodes/Odoo/Odoo.node.ts b/packages/nodes-base/nodes/Odoo/Odoo.node.ts index 0df41f47bc..58472d9340 100644 --- a/packages/nodes-base/nodes/Odoo/Odoo.node.ts +++ b/packages/nodes-base/nodes/Odoo/Odoo.node.ts @@ -751,7 +751,7 @@ export class Odoo implements INodeType { returnData.push(...executionData); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.ts b/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.ts index be8e294ce6..e04dbaaedd 100644 --- a/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.ts +++ b/packages/nodes-base/nodes/OneSimpleApi/OneSimpleApi.node.ts @@ -865,7 +865,7 @@ export class OneSimpleApi implements INodeType { returnData.push(responseData as IDataObject); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Onfleet/Onfleet.ts b/packages/nodes-base/nodes/Onfleet/Onfleet.ts index 1c49d8908c..b5c17186c5 100644 --- a/packages/nodes-base/nodes/Onfleet/Onfleet.ts +++ b/packages/nodes-base/nodes/Onfleet/Onfleet.ts @@ -879,7 +879,7 @@ export class Onfleet { } } catch (error) { //@ts-ignore - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } @@ -923,7 +923,7 @@ export class Onfleet { responseData.push(await onfleetApiRequest.call(this, 'GET', path)); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } @@ -965,7 +965,7 @@ export class Onfleet { responseData.push(await onfleetApiRequest.call(this, 'GET', path)); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } @@ -1022,7 +1022,7 @@ export class Onfleet { responseData.push(await onfleetApiRequest.call(this, 'GET', path)); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } @@ -1085,7 +1085,7 @@ export class Onfleet { responseData.push({ success: true }); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } @@ -1140,7 +1140,7 @@ export class Onfleet { responseData.push(await onfleetApiRequest.call(this, 'PUT', path, hubData)); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } @@ -1259,7 +1259,7 @@ export class Onfleet { responseData.push(await onfleetApiRequest.call(this, 'POST', path, workerSchedule)); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } @@ -1308,7 +1308,7 @@ export class Onfleet { responseData.push({ success: true }); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } @@ -1366,7 +1366,7 @@ export class Onfleet { responseData.push(await onfleetApiRequest.call(this, 'PUT', path, { tasks, ...options })); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } @@ -1462,7 +1462,7 @@ export class Onfleet { responseData.push(await onfleetApiRequest.call(this, 'POST', path, teamAutoDispatch)); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { responseData.push({ error: (error as IDataObject).toString() }); continue; } diff --git a/packages/nodes-base/nodes/OpenThesaurus/OpenThesaurus.node.ts b/packages/nodes-base/nodes/OpenThesaurus/OpenThesaurus.node.ts index b85e296296..0e2481f625 100644 --- a/packages/nodes-base/nodes/OpenThesaurus/OpenThesaurus.node.ts +++ b/packages/nodes-base/nodes/OpenThesaurus/OpenThesaurus.node.ts @@ -173,7 +173,7 @@ export class OpenThesaurus implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/OpenWeatherMap/OpenWeatherMap.node.ts b/packages/nodes-base/nodes/OpenWeatherMap/OpenWeatherMap.node.ts index 9cb874c68e..c5766b5df8 100644 --- a/packages/nodes-base/nodes/OpenWeatherMap/OpenWeatherMap.node.ts +++ b/packages/nodes-base/nodes/OpenWeatherMap/OpenWeatherMap.node.ts @@ -278,7 +278,7 @@ export class OpenWeatherMap implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/Paddle/Paddle.node.ts b/packages/nodes-base/nodes/Paddle/Paddle.node.ts index d472b428e3..cb9ce4b76a 100644 --- a/packages/nodes-base/nodes/Paddle/Paddle.node.ts +++ b/packages/nodes-base/nodes/Paddle/Paddle.node.ts @@ -520,7 +520,7 @@ export class Paddle implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/PagerDuty/PagerDuty.node.ts b/packages/nodes-base/nodes/PagerDuty/PagerDuty.node.ts index d55a9db026..4a1cfbbe67 100644 --- a/packages/nodes-base/nodes/PagerDuty/PagerDuty.node.ts +++ b/packages/nodes-base/nodes/PagerDuty/PagerDuty.node.ts @@ -460,7 +460,7 @@ export class PagerDuty implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/PayPal/PayPal.node.ts b/packages/nodes-base/nodes/PayPal/PayPal.node.ts index bbd46890b5..c046226a3e 100644 --- a/packages/nodes-base/nodes/PayPal/PayPal.node.ts +++ b/packages/nodes-base/nodes/PayPal/PayPal.node.ts @@ -242,7 +242,7 @@ export class PayPal implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts b/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts index 0bb38ba27a..33a323830d 100644 --- a/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts +++ b/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts @@ -253,7 +253,7 @@ export class Phantombuster implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts index 15cb8358b9..7fb02e9fae 100644 --- a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts +++ b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts @@ -4962,7 +4962,7 @@ export class Pipedrive implements INodeType { returnData.push(...executionData); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { if (resource === 'file' && operation === 'download') { items[i].json = { error: error.message }; } else { diff --git a/packages/nodes-base/nodes/PostHog/PostHog.node.ts b/packages/nodes-base/nodes/PostHog/PostHog.node.ts index 7b9a1b737d..603bddcc02 100644 --- a/packages/nodes-base/nodes/PostHog/PostHog.node.ts +++ b/packages/nodes-base/nodes/PostHog/PostHog.node.ts @@ -122,7 +122,7 @@ export class PostHog implements INodeType { returnData.push(responseData as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } @@ -174,7 +174,7 @@ export class PostHog implements INodeType { returnData.push(responseData as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); } else { throw error; @@ -218,7 +218,7 @@ export class PostHog implements INodeType { returnData.push(responseData as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } @@ -274,7 +274,7 @@ export class PostHog implements INodeType { returnData.push(responseData as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/ProfitWell/ProfitWell.node.ts b/packages/nodes-base/nodes/ProfitWell/ProfitWell.node.ts index 727d32ac5b..8298e68340 100644 --- a/packages/nodes-base/nodes/ProfitWell/ProfitWell.node.ts +++ b/packages/nodes-base/nodes/ProfitWell/ProfitWell.node.ts @@ -139,7 +139,7 @@ export class ProfitWell implements INodeType { returnData.push(responseData as IDataObject); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts b/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts index c1a9209dac..6af7b5f236 100644 --- a/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts +++ b/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts @@ -493,7 +493,7 @@ export class Pushbullet implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Pushover/Pushover.node.ts b/packages/nodes-base/nodes/Pushover/Pushover.node.ts index b1262175de..2ac87ecb8a 100644 --- a/packages/nodes-base/nodes/Pushover/Pushover.node.ts +++ b/packages/nodes-base/nodes/Pushover/Pushover.node.ts @@ -359,7 +359,7 @@ export class Pushover implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts index 8e81df8aa7..17e1e01e9d 100644 --- a/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts +++ b/packages/nodes-base/nodes/QuickBooks/QuickBooks.node.ts @@ -1119,7 +1119,7 @@ export class QuickBooks implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const download = this.getNodeParameter('download', 0, false); if ( ['invoice', 'estimate', 'payment'].includes(resource) && diff --git a/packages/nodes-base/nodes/Raindrop/Raindrop.node.ts b/packages/nodes-base/nodes/Raindrop/Raindrop.node.ts index 2a4661b35b..9a14e77854 100644 --- a/packages/nodes-base/nodes/Raindrop/Raindrop.node.ts +++ b/packages/nodes-base/nodes/Raindrop/Raindrop.node.ts @@ -417,7 +417,7 @@ export class Raindrop implements INodeType { ? returnData.push(...(responseData as IDataObject[])) : returnData.push(responseData as IDataObject); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/ReadBinaryFile/ReadBinaryFile.node.ts b/packages/nodes-base/nodes/ReadBinaryFile/ReadBinaryFile.node.ts index a41dadbe7a..dedc00d37c 100644 --- a/packages/nodes-base/nodes/ReadBinaryFile/ReadBinaryFile.node.ts +++ b/packages/nodes-base/nodes/ReadBinaryFile/ReadBinaryFile.node.ts @@ -75,7 +75,7 @@ export class ReadBinaryFile implements INodeType { newItem.binary![dataPropertyName] = await this.helpers.prepareBinaryData(stream, filePath); returnData.push(newItem); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: (error as Error).message, diff --git a/packages/nodes-base/nodes/ReadPdf/ReadPDF.node.ts b/packages/nodes-base/nodes/ReadPdf/ReadPDF.node.ts index 7eed004d57..c7a1485cb1 100644 --- a/packages/nodes-base/nodes/ReadPdf/ReadPDF.node.ts +++ b/packages/nodes-base/nodes/ReadPdf/ReadPDF.node.ts @@ -85,7 +85,7 @@ export class ReadPDF implements INodeType { json, }); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message, diff --git a/packages/nodes-base/nodes/Reddit/Reddit.node.ts b/packages/nodes-base/nodes/Reddit/Reddit.node.ts index 38d35b1326..d04e6406aa 100644 --- a/packages/nodes-base/nodes/Reddit/Reddit.node.ts +++ b/packages/nodes-base/nodes/Reddit/Reddit.node.ts @@ -428,7 +428,7 @@ export class Reddit implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts index ea378f97e2..68bd19e3f1 100644 --- a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts +++ b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts @@ -433,7 +433,7 @@ export class RespondToWebhook implements INodeType { this.sendResponse(response); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const itemData = generatePairedItemData(items.length); const returnData = this.helpers.constructExecutionMetaData( [{ json: { error: error.message } }], diff --git a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts index de8ddb4924..274aae41ab 100644 --- a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts +++ b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts @@ -484,7 +484,7 @@ export class Rocketchat implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/RssFeedRead/RssFeedRead.node.ts b/packages/nodes-base/nodes/RssFeedRead/RssFeedRead.node.ts index 4b63a43d74..de9f602b81 100644 --- a/packages/nodes-base/nodes/RssFeedRead/RssFeedRead.node.ts +++ b/packages/nodes-base/nodes/RssFeedRead/RssFeedRead.node.ts @@ -137,7 +137,7 @@ export class RssFeedRead implements INodeType { returnData.push(...executionData); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: fallbackPairedItems || [{ item: i }], diff --git a/packages/nodes-base/nodes/S3/S3.node.ts b/packages/nodes-base/nodes/S3/S3.node.ts index 777788d52d..bfb581447c 100644 --- a/packages/nodes-base/nodes/S3/S3.node.ts +++ b/packages/nodes-base/nodes/S3/S3.node.ts @@ -890,7 +890,7 @@ export class S3 implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { if (resource === 'file' && operation === 'download') { items[i].json = { error: error.message }; } else { diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts index 73fc54fce1..df0fd29515 100644 --- a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -3084,7 +3084,7 @@ export class Salesforce implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts index 9010e0d3c9..0b62dae691 100644 --- a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts +++ b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts @@ -234,7 +234,7 @@ export class SeaTable implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -266,7 +266,7 @@ export class SeaTable implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -329,7 +329,7 @@ export class SeaTable implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -364,7 +364,7 @@ export class SeaTable implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -431,7 +431,7 @@ export class SeaTable implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Segment/Segment.node.ts b/packages/nodes-base/nodes/Segment/Segment.node.ts index d54612d114..bfef2985a5 100644 --- a/packages/nodes-base/nodes/Segment/Segment.node.ts +++ b/packages/nodes-base/nodes/Segment/Segment.node.ts @@ -617,7 +617,7 @@ export class Segment implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/SendGrid/SendGrid.node.ts b/packages/nodes-base/nodes/SendGrid/SendGrid.node.ts index eedd6f7e6a..c803521206 100644 --- a/packages/nodes-base/nodes/SendGrid/SendGrid.node.ts +++ b/packages/nodes-base/nodes/SendGrid/SendGrid.node.ts @@ -173,7 +173,7 @@ export class SendGrid implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -214,7 +214,7 @@ export class SendGrid implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -306,7 +306,7 @@ export class SendGrid implements INodeType { ); returnData.push(responseData as INodeExecutionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message } }); } else { throw error; @@ -335,7 +335,7 @@ export class SendGrid implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -372,7 +372,7 @@ export class SendGrid implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -403,7 +403,7 @@ export class SendGrid implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -433,7 +433,7 @@ export class SendGrid implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -464,7 +464,7 @@ export class SendGrid implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -494,7 +494,7 @@ export class SendGrid implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -642,7 +642,7 @@ export class SendGrid implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/SentryIo/SentryIo.node.ts b/packages/nodes-base/nodes/SentryIo/SentryIo.node.ts index bd76abd86c..ad83f771b6 100644 --- a/packages/nodes-base/nodes/SentryIo/SentryIo.node.ts +++ b/packages/nodes-base/nodes/SentryIo/SentryIo.node.ts @@ -733,7 +733,7 @@ export class SentryIo implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.ts b/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.ts index 0e866e1357..50d78a31f5 100644 --- a/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.ts +++ b/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.ts @@ -1138,7 +1138,7 @@ export class ServiceNow implements INodeType { }); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Set/v2/manual.mode.ts b/packages/nodes-base/nodes/Set/v2/manual.mode.ts index 724a303336..282730bbf0 100644 --- a/packages/nodes-base/nodes/Set/v2/manual.mode.ts +++ b/packages/nodes-base/nodes/Set/v2/manual.mode.ts @@ -249,7 +249,7 @@ export async function execute( ); return composeReturnItem.call(this, i, item, newData, options, node.typeVersion); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { return { json: { error: (error as Error).message, pairedItem: { item: i } } }; } throw new NodeOperationError(this.getNode(), error as Error, { diff --git a/packages/nodes-base/nodes/Set/v2/raw.mode.ts b/packages/nodes-base/nodes/Set/v2/raw.mode.ts index 86aa11c750..b24a5e2dd5 100644 --- a/packages/nodes-base/nodes/Set/v2/raw.mode.ts +++ b/packages/nodes-base/nodes/Set/v2/raw.mode.ts @@ -56,7 +56,7 @@ export async function execute( return composeReturnItem.call(this, i, item, newData, options, node.typeVersion); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { return { json: { error: (error as Error).message }, pairedItem: { item: i } }; } throw new NodeOperationError(node, error as Error, { diff --git a/packages/nodes-base/nodes/Shopify/Shopify.node.ts b/packages/nodes-base/nodes/Shopify/Shopify.node.ts index 0c3f49078f..f2c70184e6 100644 --- a/packages/nodes-base/nodes/Shopify/Shopify.node.ts +++ b/packages/nodes-base/nodes/Shopify/Shopify.node.ts @@ -466,7 +466,7 @@ export class Shopify implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Signl4/Signl4.node.ts b/packages/nodes-base/nodes/Signl4/Signl4.node.ts index b86170ea6f..6aa599b120 100644 --- a/packages/nodes-base/nodes/Signl4/Signl4.node.ts +++ b/packages/nodes-base/nodes/Signl4/Signl4.node.ts @@ -342,7 +342,7 @@ export class Signl4 implements INodeType { returnData.push(responseData as IDataObject); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts b/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts index 70cd852469..585bde58e4 100644 --- a/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts +++ b/packages/nodes-base/nodes/Slack/V1/SlackV1.node.ts @@ -1372,7 +1372,7 @@ export class SlackV1 implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: (error as JsonObject).message } }); continue; } diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts index 19a5efc286..14d62c6971 100644 --- a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -1359,7 +1359,7 @@ export class SlackV2 implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: (error as JsonObject).message } }); continue; } diff --git a/packages/nodes-base/nodes/Sms77/Sms77.node.ts b/packages/nodes-base/nodes/Sms77/Sms77.node.ts index 1fd9a9a12e..a59f1ea217 100644 --- a/packages/nodes-base/nodes/Sms77/Sms77.node.ts +++ b/packages/nodes-base/nodes/Sms77/Sms77.node.ts @@ -298,7 +298,7 @@ export class Sms77 implements INodeType { returnData.push(responseData as IDataObject); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Spontit/Spontit.node.ts b/packages/nodes-base/nodes/Spontit/Spontit.node.ts index c91fb3eced..9e20cd87cd 100644 --- a/packages/nodes-base/nodes/Spontit/Spontit.node.ts +++ b/packages/nodes-base/nodes/Spontit/Spontit.node.ts @@ -103,7 +103,7 @@ export class Spontit implements INodeType { returnData.push(responseData as IDataObject); } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } diff --git a/packages/nodes-base/nodes/Spotify/Spotify.node.ts b/packages/nodes-base/nodes/Spotify/Spotify.node.ts index 5a7db76a6c..b951831c3b 100644 --- a/packages/nodes-base/nodes/Spotify/Spotify.node.ts +++ b/packages/nodes-base/nodes/Spotify/Spotify.node.ts @@ -1314,7 +1314,7 @@ export class Spotify implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/SpreadsheetFile/v1/SpreadsheetFileV1.node.ts b/packages/nodes-base/nodes/SpreadsheetFile/v1/SpreadsheetFileV1.node.ts index b954aa1691..99c7cc80bb 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile/v1/SpreadsheetFileV1.node.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile/v1/SpreadsheetFileV1.node.ts @@ -155,7 +155,7 @@ export class SpreadsheetFileV1 implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { newItems.push({ json: { error: error.message, @@ -242,7 +242,7 @@ export class SpreadsheetFileV1 implements INodeType { newItems.push(newItem); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { newItems.push({ json: { error: error.message, diff --git a/packages/nodes-base/nodes/SpreadsheetFile/v2/fromFile.operation.ts b/packages/nodes-base/nodes/SpreadsheetFile/v2/fromFile.operation.ts index de0e41ba61..719faeb478 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile/v2/fromFile.operation.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile/v2/fromFile.operation.ts @@ -209,7 +209,7 @@ export async function execute( error.message = `The file selected in 'Input Binary Field' is not in ${fileFormat} format`; errorDescription = `Try to change the operation or select a ${fileFormat} file in 'Input Binary Field'`; } - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message, diff --git a/packages/nodes-base/nodes/SpreadsheetFile/v2/toFile.operation.ts b/packages/nodes-base/nodes/SpreadsheetFile/v2/toFile.operation.ts index 85948b9915..61f8f8509e 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile/v2/toFile.operation.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile/v2/toFile.operation.ts @@ -29,7 +29,7 @@ export async function execute(this: IExecuteFunctions, items: INodeExecutionData returnData.push(newItem); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData.push({ json: { error: error.message, diff --git a/packages/nodes-base/nodes/Ssh/Ssh.node.ts b/packages/nodes-base/nodes/Ssh/Ssh.node.ts index ad17713ff6..c344fcf7ae 100644 --- a/packages/nodes-base/nodes/Ssh/Ssh.node.ts +++ b/packages/nodes-base/nodes/Ssh/Ssh.node.ts @@ -466,7 +466,7 @@ export class Ssh implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { if (resource === 'file' && operation === 'download') { items[i] = { json: { diff --git a/packages/nodes-base/nodes/Stackby/Stackby.node.ts b/packages/nodes-base/nodes/Stackby/Stackby.node.ts index c269849a70..72c9ef4a18 100644 --- a/packages/nodes-base/nodes/Stackby/Stackby.node.ts +++ b/packages/nodes-base/nodes/Stackby/Stackby.node.ts @@ -192,7 +192,7 @@ export class Stackby implements INodeType { responseData.map((data: any) => data.field) as INodeExecutionData[], ); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -228,7 +228,7 @@ export class Stackby implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -282,7 +282,7 @@ export class Stackby implements INodeType { responseData.map((data: any) => data.field) as INodeExecutionData[], ); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const itemData = generatePairedItemData(items.length); const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), @@ -332,7 +332,7 @@ export class Stackby implements INodeType { responseData.map((data: any) => data.field) as INodeExecutionData[], ); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Storyblok/Storyblok.node.ts b/packages/nodes-base/nodes/Storyblok/Storyblok.node.ts index 02045aa29a..c921e6a26d 100644 --- a/packages/nodes-base/nodes/Storyblok/Storyblok.node.ts +++ b/packages/nodes-base/nodes/Storyblok/Storyblok.node.ts @@ -349,7 +349,7 @@ export class Storyblok implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Strapi/Strapi.node.ts b/packages/nodes-base/nodes/Strapi/Strapi.node.ts index 8c991e7397..c3ff781b6f 100644 --- a/packages/nodes-base/nodes/Strapi/Strapi.node.ts +++ b/packages/nodes-base/nodes/Strapi/Strapi.node.ts @@ -389,7 +389,7 @@ export class Strapi implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Strava/Strava.node.ts b/packages/nodes-base/nodes/Strava/Strava.node.ts index 32f989051c..48f01c78f5 100644 --- a/packages/nodes-base/nodes/Strava/Strava.node.ts +++ b/packages/nodes-base/nodes/Strava/Strava.node.ts @@ -187,7 +187,7 @@ export class Strava implements INodeType { returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Stripe/Stripe.node.ts b/packages/nodes-base/nodes/Stripe/Stripe.node.ts index 79d829d61f..2c8b3042a1 100644 --- a/packages/nodes-base/nodes/Stripe/Stripe.node.ts +++ b/packages/nodes-base/nodes/Stripe/Stripe.node.ts @@ -464,7 +464,7 @@ export class Stripe implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionErrorData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Supabase/Supabase.node.ts b/packages/nodes-base/nodes/Supabase/Supabase.node.ts index 6323747b7b..d6151b2dca 100644 --- a/packages/nodes-base/nodes/Supabase/Supabase.node.ts +++ b/packages/nodes-base/nodes/Supabase/Supabase.node.ts @@ -171,7 +171,7 @@ export class Supabase implements INodeType { returnData.push(...executionData); }); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.description }), { itemData: mapPairedItemsFrom(records) }, @@ -220,7 +220,7 @@ export class Supabase implements INodeType { try { rows = await supabaseApiRequest.call(this, 'DELETE', endpoint, {}, qs); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.description }), { itemData: { item: i } }, @@ -260,7 +260,7 @@ export class Supabase implements INodeType { try { rows = await supabaseApiRequest.call(this, 'GET', endpoint, {}, qs); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.message }), { itemData: { item: i } }, @@ -326,7 +326,7 @@ export class Supabase implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.description }), { itemData: { item: i } }, @@ -402,7 +402,7 @@ export class Supabase implements INodeType { ); returnData.push(...executionData); } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray({ error: error.description }), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Switch/V1/SwitchV1.node.ts b/packages/nodes-base/nodes/Switch/V1/SwitchV1.node.ts index 4782da7c8e..b3c2ca30bb 100644 --- a/packages/nodes-base/nodes/Switch/V1/SwitchV1.node.ts +++ b/packages/nodes-base/nodes/Switch/V1/SwitchV1.node.ts @@ -678,7 +678,7 @@ export class SwitchV1 implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData[0].push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/Switch/V2/SwitchV2.node.ts b/packages/nodes-base/nodes/Switch/V2/SwitchV2.node.ts index c940658c60..19f56fd5d0 100644 --- a/packages/nodes-base/nodes/Switch/V2/SwitchV2.node.ts +++ b/packages/nodes-base/nodes/Switch/V2/SwitchV2.node.ts @@ -699,7 +699,7 @@ export class SwitchV2 implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData[0].push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts b/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts index 5c9c212b47..6892bc2d21 100644 --- a/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts +++ b/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts @@ -396,7 +396,7 @@ export class SwitchV3 implements INodeType { } } } catch (error) { - if (this.continueOnFail(error)) { + if (this.continueOnFail()) { returnData[0].push({ json: { error: error.message } }); continue; } diff --git a/packages/nodes-base/nodes/SyncroMSP/v1/actions/router.ts b/packages/nodes-base/nodes/SyncroMSP/v1/actions/router.ts index c57f20f530..3b5a5b3210 100644 --- a/packages/nodes-base/nodes/SyncroMSP/v1/actions/router.ts +++ b/packages/nodes-base/nodes/SyncroMSP/v1/actions/router.ts @@ -42,7 +42,7 @@ export async function router(this: IExecuteFunctions): Promise = Functions export type ContextType = 'flow' | 'node'; type BaseExecutionFunctions = FunctionsBaseWithRequiredKeys<'getMode'> & { - continueOnFail(error?: Error): boolean; + continueOnFail(): boolean; evaluateExpression(expression: string, itemIndex: number): NodeParameterValueType; getContext(type: ContextType): IContextObject; getExecuteData(): IExecuteData; diff --git a/packages/workflow/src/errors/node-operation.error.ts b/packages/workflow/src/errors/node-operation.error.ts index 1990714681..c55cd003cc 100644 --- a/packages/workflow/src/errors/node-operation.error.ts +++ b/packages/workflow/src/errors/node-operation.error.ts @@ -9,8 +9,6 @@ import { ApplicationError } from './application.error'; export class NodeOperationError extends NodeError { type: string | undefined; - obfuscate: boolean = false; - constructor( node: INode, error: Error | string | JsonObject, @@ -20,13 +18,8 @@ export class NodeOperationError extends NodeError { return error; } - let obfuscateErrorMessage = false; - if (typeof error === 'string') { error = new ApplicationError(error); - } else if (!(error instanceof ApplicationError)) { - // this error was no processed by n8n, obfuscate error message - obfuscateErrorMessage = true; } super(node, error); @@ -35,11 +28,6 @@ export class NodeOperationError extends NodeError { error.messages.forEach((message) => this.addToMessages(message)); } - if (obfuscateErrorMessage && !options.description) { - const originalMessage = typeof error === 'string' ? error : (error.message as string); - this.addToMessages(originalMessage); - this.obfuscate = true; - } if (options.message) this.message = options.message; if (options.level) this.level = options.level; if (options.functionality) this.functionality = options.functionality; diff --git a/packages/workflow/test/errors/node.error.test.ts b/packages/workflow/test/errors/node.error.test.ts index 4ec8c1d611..7d0783a65c 100644 --- a/packages/workflow/test/errors/node.error.test.ts +++ b/packages/workflow/test/errors/node.error.test.ts @@ -2,7 +2,6 @@ import { mock } from 'jest-mock-extended'; import type { INode } from '@/Interfaces'; import { NodeApiError } from '@/errors/node-api.error'; import { NodeOperationError } from '@/errors/node-operation.error'; -import { ApplicationError } from '@/errors/application.error'; describe('NodeError', () => { const node = mock(); @@ -16,50 +15,4 @@ describe('NodeError', () => { expect(wrapped1).toEqual(apiError); expect(wrapped2).toEqual(opsError); }); - - it('should obfuscate errors not processed by n8n', () => { - const error = new Error('Original error message'); - const nodeOpError = new NodeOperationError(node, error); - - expect(nodeOpError.obfuscate).toBe(true); - expect(nodeOpError.message).toBe('Original error message'); - expect(nodeOpError.messages).toContain('Original error message'); - }); - - it('should not obfuscate errors processed by n8n', () => { - const appError = new ApplicationError('Processed error message'); - const nodeOpError = new NodeOperationError(node, appError); - - expect(nodeOpError.obfuscate).toBe(false); - expect(nodeOpError.message).toBe('Processed error message'); - expect(nodeOpError.messages).not.toContain('Processed error message'); - }); - - it('should not obfuscate string errors', () => { - const errorMessage = 'String error message'; - const nodeOpError = new NodeOperationError(node, errorMessage); - - expect(nodeOpError.obfuscate).toBe(false); - expect(nodeOpError.message).toBe(errorMessage); - expect(nodeOpError.messages).toHaveLength(0); - }); - - it('should not obfuscate error if description provided', () => { - const error = new Error('Initial error message'); - const options = { description: 'Error description' }; - const nodeOpError = new NodeOperationError(node, error, options); - - expect(nodeOpError.obfuscate).toBe(false); - expect(nodeOpError.message).toBe('Initial error message'); - }); - - it('should respect provided options for message and description', () => { - const error = new Error('Initial error message'); - const options = { message: 'Overridden message', description: 'Error description' }; - const nodeOpError = new NodeOperationError(node, error, options); - - expect(nodeOpError.obfuscate).toBe(false); - expect(nodeOpError.message).toBe('Overridden message'); - expect(nodeOpError.description).toBe('Error description'); - }); }); From 611943487aa4e29ef1729222e7b4b0b2ef036c7b Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:07:36 +0300 Subject: [PATCH 014/259] ci: Omit benchmark scope commits from changelog (no-changelog) (#10618) --- .github/scripts/update-changelog.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/scripts/update-changelog.mjs b/.github/scripts/update-changelog.mjs index c0cce214a9..2bc571d267 100644 --- a/.github/scripts/update-changelog.mjs +++ b/.github/scripts/update-changelog.mjs @@ -16,7 +16,11 @@ const changelogStream = conventionalChangelog({ releaseCount: 1, tagPrefix: 'n8n@', transform: (commit, callback) => { - callback(null, commit.header.includes('(no-changelog)') ? undefined : commit); + const hasNoChangelogInHeader = commit.header.includes('(no-changelog)'); + const isBenchmarkScope = commit.scope === 'benchmark'; + + // Ignore commits that have 'benchmark' scope or '(no-changelog)' in the header + callback(null, hasNoChangelogInHeader || isBenchmarkScope ? undefined : commit); }, }).on('error', (err) => { console.error(err.stack); From a12e9edac042957939c63f0a5c35572930632352 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 30 Aug 2024 11:37:24 +0200 Subject: [PATCH 015/259] fix(editor): Add pinned data only to manual executions in execution view (#10605) --- cypress/e2e/19-execution.cy.ts | 41 ++++++ .../fixtures/Execution-pinned-data-check.json | 133 ++++++++++++++++++ packages/editor-ui/src/views/NodeView.vue | 2 +- 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 cypress/fixtures/Execution-pinned-data-check.json diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 1f7d5c332e..d2c463f1bb 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -616,4 +616,45 @@ describe('Execution', () => { errorToast().should('contain', 'Problem in node ‘Telegram‘'); }); + + it('should not show pinned data in production execution', () => { + cy.createFixtureWorkflow('Execution-pinned-data-check.json'); + + workflowPage.getters.zoomToFitButton().click(); + cy.intercept('PATCH', '/rest/workflows/*').as('workflowActivate'); + workflowPage.getters.activatorSwitch().click(); + + cy.wait('@workflowActivate'); + cy.get('body').type('{esc}'); + workflowPage.actions.openNode('Webhook'); + + cy.contains('label', 'Production URL').should('be.visible').click(); + cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite'); + cy.get('.webhook-url').click(); + ndv.getters.backToCanvas().click(); + + cy.readClipboard().then((url) => { + cy.request({ + method: 'GET', + url, + }).then((resp) => { + expect(resp.status).to.eq(200); + }); + }); + + cy.intercept('GET', '/rest/executions/*').as('getExecution'); + executionsTab.actions.switchToExecutionsTab(); + + cy.wait('@getExecution'); + executionsTab.getters + .workflowExecutionPreviewIframe() + .should('be.visible') + .its('0.contentDocument.body') + .should('not.be.empty') + + .then(cy.wrap) + .find('.connection-run-items-label') + .filter(':contains("5 items")') + .should('have.length', 2); + }); }); diff --git a/cypress/fixtures/Execution-pinned-data-check.json b/cypress/fixtures/Execution-pinned-data-check.json new file mode 100644 index 0000000000..20041af117 --- /dev/null +++ b/cypress/fixtures/Execution-pinned-data-check.json @@ -0,0 +1,133 @@ +{ + "name": "PAY-1707", + "nodes": [ + { + "parameters": { + "options": {} + }, + "id": "eaa428a8-eb9d-478a-b997-aed6ed298507", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 920, + 380 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "6b285c91-e7ea-4943-8ba3-59ce01a35d20", + "name": "Edit Fields1", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 920, + 540 + ] + }, + { + "parameters": { + "jsCode": "return Array.from({length: 5}, _ => ({}))" + }, + "id": "70e682aa-dfef-4db7-a158-971ec7976d49", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 700, + 380 + ] + }, + { + "parameters": { + "jsCode": "return Array.from({length: 5}, _ => ({}))" + }, + "id": "d5ee979e-9f53-4e62-8eb2-cdb92be8ea6e", + "name": "Code1", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 700, + 540 + ] + }, + { + "parameters": { + "path": "dd660366-ca4a-4736-8b1f-454560e87bfb", + "options": {} + }, + "id": "20c33c8a-ab2f-4dd4-990f-6390feeb840c", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + 480, + 440 + ], + "webhookId": "dd660366-ca4a-4736-8b1f-454560e87bfb" + } + ], + "pinData": { + "Code1": [ + { + "json": {} + }, + { + "json": {} + } + ] + }, + "connections": { + "Code1": { + "main": [ + [ + { + "node": "Edit Fields1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Webhook": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + }, + { + "node": "Code1", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1" + }, + "versionId": "01e6693e-54f3-432d-9b1f-922ef92b4ab6", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7" + }, + "id": "hU0gp19G29ehWktc", + "tags": [] +} \ No newline at end of file diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 3fa3ff9f9c..608d9cb82f 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -997,7 +997,7 @@ export default defineComponent({ }); this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID); this.workflowsStore.setWorkflowExecutionData(data); - if (data.workflowData.pinData) { + if (data.workflowData.pinData && data.mode === 'manual') { this.workflowsStore.setWorkflowPinData(data.workflowData.pinData); } From ccb553a550ee82e32017b8d8a6fffc49b850055b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 30 Aug 2024 07:09:47 -0400 Subject: [PATCH 016/259] refactor(editor): Migrate `NodeSettings.vue` to composition API (#10545) --- .../editor-ui/src/components/NodeSettings.vue | 1714 ++++++++--------- 1 file changed, 822 insertions(+), 892 deletions(-) diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 0b5a291e44..86ab5bc366 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -1,7 +1,5 @@ - From 81f4322d456773281aec4b47447465bdffd311fe Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Fri, 30 Aug 2024 16:28:25 +0300 Subject: [PATCH 017/259] fix(Wait Node): Append n8n attribution option (#10585) --- .../nodes-base/nodes/EmailSend/v2/send.operation.ts | 7 ++----- packages/nodes-base/nodes/Form/common.descriptions.ts | 6 ++++++ .../nodes-base/nodes/Form/v2/FormTriggerV2.node.ts | 11 ++--------- .../nodes/Google/Gmail/v2/MessageDescription.ts | 7 ++----- packages/nodes-base/nodes/Telegram/Telegram.node.ts | 7 ++----- packages/nodes-base/nodes/Wait/Wait.node.ts | 5 +++-- packages/nodes-base/utils/descriptions.ts | 8 ++++++++ 7 files changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts index 7ca8ec82dc..0c78e6293c 100644 --- a/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts +++ b/packages/nodes-base/nodes/EmailSend/v2/send.operation.ts @@ -14,6 +14,7 @@ import { createTransport } from 'nodemailer'; import type SMTPTransport from 'nodemailer/lib/smtp-transport'; import { updateDisplayOptions } from '@utils/utilities'; +import { appendAttributionOption } from '../../../utils/descriptions'; const properties: INodeProperties[] = [ // TODO: Add choice for text as text or html (maybe also from name) @@ -137,11 +138,7 @@ const properties: INodeProperties[] = [ default: {}, options: [ { - // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased - displayName: 'Append n8n Attribution', - name: 'appendAttribution', - type: 'boolean', - default: true, + ...appendAttributionOption, description: 'Whether to include the phrase “This email was sent automatically with n8n” to the end of the email', }, diff --git a/packages/nodes-base/nodes/Form/common.descriptions.ts b/packages/nodes-base/nodes/Form/common.descriptions.ts index c3505e9509..fe777edd83 100644 --- a/packages/nodes-base/nodes/Form/common.descriptions.ts +++ b/packages/nodes-base/nodes/Form/common.descriptions.ts @@ -1,4 +1,5 @@ import type { INodeProperties } from 'n8n-workflow'; +import { appendAttributionOption } from '../../utils/descriptions'; export const webhookPath: INodeProperties = { displayName: 'Form Path', @@ -314,3 +315,8 @@ export const respondWithOptions: INodeProperties = { }, ], }; + +export const appendAttributionToForm: INodeProperties = { + ...appendAttributionOption, + description: 'Whether to include the link “Form automated with n8n” at the bottom of the form', +}; diff --git a/packages/nodes-base/nodes/Form/v2/FormTriggerV2.node.ts b/packages/nodes-base/nodes/Form/v2/FormTriggerV2.node.ts index fc530d0aeb..9adead6ad3 100644 --- a/packages/nodes-base/nodes/Form/v2/FormTriggerV2.node.ts +++ b/packages/nodes-base/nodes/Form/v2/FormTriggerV2.node.ts @@ -10,6 +10,7 @@ import { import { formWebhook } from '../utils'; import { + appendAttributionToForm, formDescription, formFields, formRespondMode, @@ -116,15 +117,7 @@ const descriptionV2: INodeTypeDescription = { placeholder: 'Add option', default: {}, options: [ - { - // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased - displayName: 'Append n8n Attribution', - name: 'appendAttribution', - type: 'boolean', - default: true, - description: - 'Whether to include the link “Form automated with n8n” at the bottom of the form', - }, + appendAttributionToForm, { ...respondWithOptions, displayOptions: { diff --git a/packages/nodes-base/nodes/Google/Gmail/v2/MessageDescription.ts b/packages/nodes-base/nodes/Google/Gmail/v2/MessageDescription.ts index 0378eb04e4..c174bc95f4 100644 --- a/packages/nodes-base/nodes/Google/Gmail/v2/MessageDescription.ts +++ b/packages/nodes-base/nodes/Google/Gmail/v2/MessageDescription.ts @@ -1,4 +1,5 @@ import type { INodeProperties } from 'n8n-workflow'; +import { appendAttributionOption } from '../../../../utils/descriptions'; export const messageOperations: INodeProperties[] = [ { @@ -200,11 +201,7 @@ export const messageFields: INodeProperties[] = [ default: {}, options: [ { - // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased - displayName: 'Append n8n Attribution', - name: 'appendAttribution', - type: 'boolean', - default: true, + ...appendAttributionOption, description: 'Whether to include the phrase “This email was sent automatically with n8n” to the end of the email', }, diff --git a/packages/nodes-base/nodes/Telegram/Telegram.node.ts b/packages/nodes-base/nodes/Telegram/Telegram.node.ts index 3a1021b8f0..6ecc26ed79 100644 --- a/packages/nodes-base/nodes/Telegram/Telegram.node.ts +++ b/packages/nodes-base/nodes/Telegram/Telegram.node.ts @@ -11,6 +11,7 @@ import type { import { BINARY_ENCODING, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { addAdditionalFields, apiRequest, getPropertyName } from './GenericFunctions'; +import { appendAttributionOption } from '../../utils/descriptions'; export class Telegram implements INodeType { description: INodeTypeDescription = { @@ -1508,11 +1509,7 @@ export class Telegram implements INodeType { default: {}, options: [ { - // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased - displayName: 'Append n8n Attribution', - name: 'appendAttribution', - type: 'boolean', - default: true, + ...appendAttributionOption, description: 'Whether to include the phrase “This message was sent automatically with n8n” to the end of the message', displayOptions: { diff --git a/packages/nodes-base/nodes/Wait/Wait.node.ts b/packages/nodes-base/nodes/Wait/Wait.node.ts index 9f377df7f9..5da813981b 100644 --- a/packages/nodes-base/nodes/Wait/Wait.node.ts +++ b/packages/nodes-base/nodes/Wait/Wait.node.ts @@ -27,6 +27,7 @@ import { respondWithOptions, formRespondMode, formTitle, + appendAttributionToForm, } from '../Form/common.descriptions'; import { formWebhook } from '../Form/utils'; import { updateDisplayOptions } from '../../utils/utilities'; @@ -436,7 +437,7 @@ export class Wait extends Webhook { responseMode: ['responseNode'], }, }, - options: [respondWithOptions, webhookSuffix], + options: [appendAttributionToForm, respondWithOptions, webhookSuffix], }, { displayName: 'Options', @@ -452,7 +453,7 @@ export class Wait extends Webhook { responseMode: ['onReceived', 'lastNode'], }, }, - options: [webhookSuffix], + options: [appendAttributionToForm, webhookSuffix], }, ], }; diff --git a/packages/nodes-base/utils/descriptions.ts b/packages/nodes-base/utils/descriptions.ts index fb6772a12b..75a1116c81 100644 --- a/packages/nodes-base/utils/descriptions.ts +++ b/packages/nodes-base/utils/descriptions.ts @@ -41,6 +41,14 @@ export const looseTypeValidationProperty: INodeProperties = { default: true, }; +export const appendAttributionOption: INodeProperties = { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Append n8n Attribution', + name: 'appendAttribution', + type: 'boolean', + default: true, +}; + export const encodeDecodeOptions: INodePropertyOptions[] = [ { name: 'armscii8', From a20c915e5736f83029aec5ae5dc2d05b6e023ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Fri, 30 Aug 2024 16:45:18 +0200 Subject: [PATCH 018/259] test(editor): Increase test coverage for users settings page and modal (#10623) --- packages/editor-ui/src/__tests__/utils.ts | 28 ++ .../src/components/DeleteUserModal.test.ts | 145 ++++++++ packages/editor-ui/src/constants.ts | 1 + .../editor-ui/src/stores/projects.store.ts | 3 +- .../src/views/SettingsUsersView.test.ts | 351 +++++++++++------- 5 files changed, 398 insertions(+), 130 deletions(-) create mode 100644 packages/editor-ui/src/components/DeleteUserModal.test.ts diff --git a/packages/editor-ui/src/__tests__/utils.ts b/packages/editor-ui/src/__tests__/utils.ts index 91e8d1a573..fe7cabe6cf 100644 --- a/packages/editor-ui/src/__tests__/utils.ts +++ b/packages/editor-ui/src/__tests__/utils.ts @@ -4,6 +4,9 @@ import type { ISettingsState } from '@/Interface'; import { UserManagementAuthenticationMethod } from '@/Interface'; import { defaultSettings } from './defaults'; import { APP_MODALS_ELEMENT_ID } from '@/constants'; +import type { Mock } from 'vitest'; +import type { Store, StoreDefinition } from 'pinia'; +import type { ComputedRef } from 'vue'; /** * Retries the given assertion until it passes or the timeout is reached @@ -108,3 +111,28 @@ export const createAppModals = () => { export const cleanupAppModals = () => { document.body.innerHTML = ''; }; + +/** + * Typescript helper for mocking pinia store actions return value + * + * @see https://pinia.vuejs.org/cookbook/testing.html#Mocking-the-returned-value-of-an-action + */ +export const mockedStore = unknown>( + useStore: TStoreDef, +): TStoreDef extends StoreDefinition + ? Store< + Id, + State, + Record, + { + [K in keyof Actions]: Actions[K] extends (...args: infer Args) => infer ReturnT + ? Mock + : Actions[K]; + } + > & { + [K in keyof Getters]: Getters[K] extends ComputedRef ? T : never; + } + : ReturnType => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return useStore() as any; +}; diff --git a/packages/editor-ui/src/components/DeleteUserModal.test.ts b/packages/editor-ui/src/components/DeleteUserModal.test.ts new file mode 100644 index 0000000000..11a5d786b6 --- /dev/null +++ b/packages/editor-ui/src/components/DeleteUserModal.test.ts @@ -0,0 +1,145 @@ +import { createComponentRenderer } from '@/__tests__/render'; +import DeleteUserModal from './DeleteUserModal.vue'; +import { createTestingPinia } from '@pinia/testing'; +import { getDropdownItems } from '@/__tests__/utils'; +import { createProjectListItem } from '@/__tests__/data/projects'; +import { createUser } from '@/__tests__/data/users'; + +import { DELETE_USER_MODAL_KEY } from '@/constants'; +import { ProjectTypes } from '@/types/projects.types'; +import userEvent from '@testing-library/user-event'; +import { useUsersStore } from '@/stores/users.store'; +import { STORES } from '@/constants'; + +const ModalStub = { + template: ` +
+ + + + +
+ `, +}; + +const loggedInUser = createUser(); +const invitedUser = createUser({ firstName: undefined }); +const user = createUser(); + +const initialState = { + [STORES.UI]: { + modalsById: { + [DELETE_USER_MODAL_KEY]: { + open: true, + }, + }, + modalStack: [DELETE_USER_MODAL_KEY], + }, + [STORES.PROJECTS]: { + projects: [ + ProjectTypes.Personal, + ProjectTypes.Personal, + ProjectTypes.Team, + ProjectTypes.Team, + ].map(createProjectListItem), + }, + [STORES.USERS]: { + usersById: { + [loggedInUser.id]: loggedInUser, + [user.id]: user, + [invitedUser.id]: invitedUser, + }, + }, +}; + +const global = { + stubs: { + Modal: ModalStub, + }, +}; + +const renderModal = createComponentRenderer(DeleteUserModal); +let pinia: ReturnType; + +describe('DeleteUserModal', () => { + beforeEach(() => { + pinia = createTestingPinia({ initialState }); + }); + + it('should delete invited users', async () => { + const { getByTestId } = renderModal({ + props: { + activeId: invitedUser.id, + }, + global, + pinia, + }); + + const userStore = useUsersStore(); + + await userEvent.click(getByTestId('confirm-delete-user-button')); + + expect(userStore.deleteUser).toHaveBeenCalledWith({ id: invitedUser.id }); + }); + + it('should delete user and transfer workflows and credentials', async () => { + const { getByTestId, getAllByRole } = renderModal({ + props: { + activeId: user.id, + }, + global, + pinia, + }); + + const confirmButton = getByTestId('confirm-delete-user-button'); + expect(confirmButton).toBeDisabled(); + + await userEvent.click(getAllByRole('radio')[0]); + + const projectSelect = getByTestId('project-sharing-select'); + expect(projectSelect).toBeVisible(); + + const projectSelectDropdownItems = await getDropdownItems(projectSelect); + await userEvent.click(projectSelectDropdownItems[0]); + + const userStore = useUsersStore(); + + expect(confirmButton).toBeEnabled(); + await userEvent.click(confirmButton); + + expect(userStore.deleteUser).toHaveBeenCalledWith({ + id: user.id, + transferId: expect.any(String), + }); + }); + + it('should delete user without transfer', async () => { + const { getByTestId, getAllByRole, getByRole } = renderModal({ + props: { + activeId: user.id, + }, + global, + pinia, + }); + + const userStore = useUsersStore(); + + const confirmButton = getByTestId('confirm-delete-user-button'); + expect(confirmButton).toBeDisabled(); + + await userEvent.click(getAllByRole('radio')[1]); + + const input = getByRole('textbox'); + + await userEvent.type(input, 'delete all '); + expect(confirmButton).toBeDisabled(); + + await userEvent.type(input, 'data'); + expect(confirmButton).toBeEnabled(); + + await userEvent.click(confirmButton); + expect(userStore.deleteUser).toHaveBeenCalledWith({ + id: user.id, + }); + }); +}); diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index e655eed6fe..3f2771cd10 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -639,6 +639,7 @@ export const enum STORES { PUSH = 'push', ASSISTANT = 'assistant', BECOME_TEMPLATE_CREATOR = 'becomeTemplateCreator', + PROJECTS = 'projects', } export const enum SignInType { diff --git a/packages/editor-ui/src/stores/projects.store.ts b/packages/editor-ui/src/stores/projects.store.ts index 6bc61d246c..98ecf52a56 100644 --- a/packages/editor-ui/src/stores/projects.store.ts +++ b/packages/editor-ui/src/stores/projects.store.ts @@ -18,8 +18,9 @@ import { hasPermission } from '@/utils/rbac/permissions'; import type { IWorkflowDb } from '@/Interface'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useCredentialsStore } from '@/stores/credentials.store'; +import { STORES } from '@/constants'; -export const useProjectsStore = defineStore('projects', () => { +export const useProjectsStore = defineStore(STORES.PROJECTS, () => { const route = useRoute(); const rootStore = useRootStore(); const settingsStore = useSettingsStore(); diff --git a/packages/editor-ui/src/views/SettingsUsersView.test.ts b/packages/editor-ui/src/views/SettingsUsersView.test.ts index 7a4fe732a6..573395405e 100644 --- a/packages/editor-ui/src/views/SettingsUsersView.test.ts +++ b/packages/editor-ui/src/views/SettingsUsersView.test.ts @@ -1,25 +1,70 @@ import { within } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; -import { createPinia, setActivePinia } from 'pinia'; import { createComponentRenderer } from '@/__tests__/render'; -import { cleanupAppModals, createAppModals, getDropdownItems } from '@/__tests__/utils'; -import ModalRoot from '@/components/ModalRoot.vue'; -import DeleteUserModal from '@/components/DeleteUserModal.vue'; +import { getDropdownItems, mockedStore } from '@/__tests__/utils'; import SettingsUsersView from '@/views/SettingsUsersView.vue'; -import { useProjectsStore } from '@/stores/projects.store'; import { useUsersStore } from '@/stores/users.store'; import { createUser } from '@/__tests__/data/users'; -import { createProjectListItem } from '@/__tests__/data/projects'; import { useRBACStore } from '@/stores/rbac.store'; -import { DELETE_USER_MODAL_KEY, EnterpriseEditionFeature } from '@/constants'; -import * as usersApi from '@/api/users'; import { useSettingsStore } from '@/stores/settings.store'; -import { defaultSettings } from '@/__tests__/defaults'; -import { ProjectTypes } from '@/types/projects.types'; +import { createTestingPinia, type TestingOptions } from '@pinia/testing'; +import { merge } from 'lodash-es'; +import { useUIStore } from '@/stores/ui.store'; +import { useSSOStore } from '@/stores/sso.store'; +import { STORES } from '@/constants'; + +const loggedInUser = createUser(); +const invitedUser = createUser({ + firstName: undefined, + inviteAcceptUrl: 'dummy', + role: 'global:admin', +}); +const user = createUser(); +const userWithDisabledSSO = createUser({ + settings: { allowSSOManualLogin: true }, +}); + +const initialState = { + [STORES.USERS]: { + currentUserId: loggedInUser.id, + usersById: { + [loggedInUser.id]: loggedInUser, + [invitedUser.id]: invitedUser, + [user.id]: user, + [userWithDisabledSSO.id]: userWithDisabledSSO, + }, + }, + [STORES.SETTINGS]: { settings: { enterprise: { advancedPermissions: true } } }, +}; + +const getInitialState = (state: TestingOptions['initialState'] = {}) => + merge({}, initialState, state); + +const copy = vi.fn(); +vi.mock('@/composables/useClipboard', () => ({ + useClipboard: () => ({ + copy, + }), +})); + +const renderView = createComponentRenderer(SettingsUsersView); + +const triggerUserAction = async (userListItem: HTMLElement, action: string) => { + expect(userListItem).toBeInTheDocument(); + + const actionToggle = within(userListItem).getByTestId('action-toggle'); + const actionToggleButton = within(actionToggle).getByRole('button'); + expect(actionToggleButton).toBeVisible(); + + await userEvent.click(actionToggle); + const actionToggleId = actionToggleButton.getAttribute('aria-controls'); + + const actionDropdown = document.getElementById(actionToggleId as string) as HTMLElement; + await userEvent.click(within(actionDropdown).getByTestId(`action-${action}`)); +}; const showToast = vi.fn(); const showError = vi.fn(); - vi.mock('@/composables/useToast', () => ({ useToast: () => ({ showToast, @@ -27,157 +72,205 @@ vi.mock('@/composables/useToast', () => ({ }), })); -const wrapperComponentWithModal = { - components: { SettingsUsersView, ModalRoot, DeleteUserModal }, - template: ` -
- - - - -
- `, -}; - -const renderComponent = createComponentRenderer(wrapperComponentWithModal); - -const loggedInUser = createUser(); -const users = Array.from({ length: 3 }, createUser); -const projects = [ - ProjectTypes.Personal, - ProjectTypes.Personal, - ProjectTypes.Team, - ProjectTypes.Team, -].map(createProjectListItem); - -let pinia: ReturnType; -let projectsStore: ReturnType; -let usersStore: ReturnType; -let rbacStore: ReturnType; - describe('SettingsUsersView', () => { - beforeEach(() => { - pinia = createPinia(); - setActivePinia(pinia); - projectsStore = useProjectsStore(); - usersStore = useUsersStore(); - rbacStore = useRBACStore(); - - createAppModals(); - - useSettingsStore().settings.enterprise = { - ...defaultSettings.enterprise, - [EnterpriseEditionFeature.AdvancedExecutionFilters]: true, - }; - - vi.spyOn(rbacStore, 'hasScope').mockReturnValue(true); - vi.spyOn(usersApi, 'getUsers').mockResolvedValue(users); - vi.spyOn(usersStore, 'allUsers', 'get').mockReturnValue(users); - vi.spyOn(projectsStore, 'getAllProjects').mockImplementation( - async () => await Promise.resolve(), - ); - vi.spyOn(projectsStore, 'projects', 'get').mockReturnValue(projects); - - usersStore.currentUserId = loggedInUser.id; - + afterEach(() => { + copy.mockReset(); showToast.mockReset(); showError.mockReset(); }); - afterEach(() => { - cleanupAppModals(); + it('hides invite button visibility based on user permissions', async () => { + const pinia = createTestingPinia({ initialState: getInitialState() }); + const userStore = useUsersStore(pinia); + // @ts-expect-error: mocked getter + userStore.currentUser = createUser({ isDefaultUser: true }); + + const { queryByTestId } = renderView({ pinia }); + + expect(queryByTestId('settings-users-invite-button')).not.toBeInTheDocument(); }); - it('should show confirmation modal before deleting user and delete with transfer', async () => { - const deleteUserSpy = vi.spyOn(usersStore, 'deleteUser').mockImplementation(async () => {}); + describe('Below quota', () => { + const pinia = createTestingPinia({ initialState: getInitialState() }); - const { getByTestId } = renderComponent({ pinia }); + const settingsStore = useSettingsStore(pinia); + // @ts-expect-error: mocked getter + settingsStore.isBelowUserQuota = false; - const userListItem = getByTestId(`user-list-item-${users[0].email}`); - expect(userListItem).toBeInTheDocument(); + it('disables the invite button', async () => { + const { getByTestId } = renderView({ pinia }); - const actionToggle = within(userListItem).getByTestId('action-toggle'); - const actionToggleButton = within(actionToggle).getByRole('button'); - expect(actionToggleButton).toBeVisible(); + expect(getByTestId('settings-users-invite-button')).toBeDisabled(); + }); - await userEvent.click(actionToggle); - const actionToggleId = actionToggleButton.getAttribute('aria-controls'); + it('allows the user to upgrade', async () => { + const { getByTestId } = renderView({ pinia }); + const uiStore = useUIStore(); - const actionDropdown = document.getElementById(actionToggleId as string) as HTMLElement; - const actionDelete = within(actionDropdown).getByTestId('action-delete'); - await userEvent.click(actionDelete); + const actionBox = getByTestId('action-box'); + expect(actionBox).toBeInTheDocument(); - const modal = getByTestId('deleteUser-modal'); - expect(modal).toBeVisible(); - const confirmButton = within(modal).getByTestId('confirm-delete-user-button'); - expect(confirmButton).toBeDisabled(); + await userEvent.click(await within(actionBox).findByText('View plans')); - await userEvent.click(within(modal).getAllByRole('radio')[0]); - - const projectSelect = getByTestId('project-sharing-select'); - expect(projectSelect).toBeVisible(); - - const projectSelectDropdownItems = await getDropdownItems(projectSelect); - await userEvent.click(projectSelectDropdownItems[0]); - - expect(confirmButton).toBeEnabled(); - await userEvent.click(confirmButton); - expect(deleteUserSpy).toHaveBeenCalledWith({ - id: users[0].id, - transferId: expect.any(String), + expect(uiStore.goToUpgrade).toHaveBeenCalledWith('settings-users', 'upgrade-users'); }); }); - it('should show confirmation modal before deleting user and delete without transfer', async () => { - const deleteUserSpy = vi.spyOn(usersStore, 'deleteUser').mockImplementation(async () => {}); + it('disables the invite button on SAML login', async () => { + const pinia = createTestingPinia({ initialState: getInitialState() }); + const ssoStore = useSSOStore(pinia); + ssoStore.isSamlLoginEnabled = true; - const { getByTestId } = renderComponent({ pinia }); + const { getByTestId } = renderView({ pinia }); - const userListItem = getByTestId(`user-list-item-${users[0].email}`); - expect(userListItem).toBeInTheDocument(); + expect(getByTestId('settings-users-invite-button')).toBeDisabled(); + }); - const actionToggle = within(userListItem).getByTestId('action-toggle'); - const actionToggleButton = within(actionToggle).getByRole('button'); - expect(actionToggleButton).toBeVisible(); + it('shows the invite modal', async () => { + const pinia = createTestingPinia({ initialState: getInitialState() }); + const { getByTestId } = renderView({ pinia }); - await userEvent.click(actionToggle); - const actionToggleId = actionToggleButton.getAttribute('aria-controls'); + const uiStore = useUIStore(); + await userEvent.click(getByTestId('settings-users-invite-button')); - const actionDropdown = document.getElementById(actionToggleId as string) as HTMLElement; - const actionDelete = within(actionDropdown).getByTestId('action-delete'); - await userEvent.click(actionDelete); + expect(uiStore.openModal).toHaveBeenCalledWith('inviteUser'); + }); - const modal = getByTestId('deleteUser-modal'); - expect(modal).toBeVisible(); - const confirmButton = within(modal).getByTestId('confirm-delete-user-button'); - expect(confirmButton).toBeDisabled(); + it('shows warning when advanced permissions are not enabled', async () => { + const pinia = createTestingPinia({ + initialState: getInitialState({ + [STORES.SETTINGS]: { settings: { enterprise: { advancedPermissions: false } } }, + }), + }); - await userEvent.click(within(modal).getAllByRole('radio')[1]); + const { getByText } = renderView({ pinia }); - const input = within(modal).getByRole('textbox'); + expect(getByText('to unlock the ability to create additional admin users')); + }); - await userEvent.type(input, 'delete all '); - expect(confirmButton).toBeDisabled(); + describe('per user actions', () => { + it('should copy invite link to clipboard', async () => { + const action = 'copyInviteLink'; - await userEvent.type(input, 'data'); - expect(confirmButton).toBeEnabled(); + const pinia = createTestingPinia({ initialState: getInitialState() }); - await userEvent.click(confirmButton); - expect(deleteUserSpy).toHaveBeenCalledWith({ - id: users[0].id, + const { getByTestId } = renderView({ pinia }); + + await triggerUserAction(getByTestId(`user-list-item-${invitedUser.email}`), action); + + expect(copy).toHaveBeenCalledWith(invitedUser.inviteAcceptUrl); + expect(showToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })); + }); + + it('should re invite users', async () => { + const action = 'reinvite'; + + const pinia = createTestingPinia({ initialState: getInitialState() }); + + const settingsStore = useSettingsStore(pinia); + // @ts-expect-error: mocked getter + settingsStore.isSmtpSetup = true; + + const userStore = useUsersStore(); + + const { getByTestId } = renderView({ pinia }); + + await triggerUserAction(getByTestId(`user-list-item-${invitedUser.email}`), action); + + expect(userStore.reinviteUser).toHaveBeenCalled(); + expect(showToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })); + }); + + it('should show delete users modal with the right permissions', async () => { + const action = 'delete'; + + const pinia = createTestingPinia({ initialState: getInitialState() }); + + const rbacStore = mockedStore(useRBACStore); + rbacStore.hasScope.mockReturnValue(true); + + const { getByTestId } = renderView({ pinia }); + + await triggerUserAction(getByTestId(`user-list-item-${user.email}`), action); + + const uiStore = useUIStore(); + expect(uiStore.openDeleteUserModal).toHaveBeenCalledWith(user.id); + }); + + it('should allow coping reset password link', async () => { + const action = 'copyPasswordResetLink'; + + const pinia = createTestingPinia({ initialState: getInitialState() }); + + const rbacStore = mockedStore(useRBACStore); + rbacStore.hasScope.mockReturnValue(true); + + const userStore = mockedStore(useUsersStore); + userStore.getUserPasswordResetLink.mockResolvedValue({ link: 'dummy-reset-password' }); + + const { getByTestId } = renderView({ pinia }); + + await triggerUserAction(getByTestId(`user-list-item-${user.email}`), action); + + expect(userStore.getUserPasswordResetLink).toHaveBeenCalledWith(user); + + expect(copy).toHaveBeenCalledWith('dummy-reset-password'); + expect(showToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })); + }); + + it('should enable SSO manual login', async () => { + const action = 'allowSSOManualLogin'; + + const pinia = createTestingPinia({ initialState: getInitialState() }); + + const settingsStore = useSettingsStore(pinia); + // @ts-expect-error: mocked getter + settingsStore.isSamlLoginEnabled = true; + + const userStore = useUsersStore(); + + const { getByTestId } = renderView({ pinia }); + + await triggerUserAction(getByTestId(`user-list-item-${user.email}`), action); + expect(userStore.updateOtherUserSettings).toHaveBeenCalledWith(user.id, { + allowSSOManualLogin: true, + }); + expect(showToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })); + }); + + it('should disable SSO manual login', async () => { + const action = 'disallowSSOManualLogin'; + + const pinia = createTestingPinia({ initialState: getInitialState() }); + + const settingsStore = useSettingsStore(pinia); + // @ts-expect-error: mocked getter + settingsStore.isSamlLoginEnabled = true; + + const userStore = useUsersStore(); + + const { getByTestId } = renderView({ pinia }); + + await triggerUserAction(getByTestId(`user-list-item-${userWithDisabledSSO.email}`), action); + + expect(userStore.updateOtherUserSettings).toHaveBeenCalledWith(userWithDisabledSSO.id, { + allowSSOManualLogin: false, + }); + expect(showToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' })); }); }); it("should show success toast when changing a user's role", async () => { - const updateGlobalRoleSpy = vi.spyOn(usersStore, 'updateGlobalRole').mockResolvedValue(); + const pinia = createTestingPinia({ initialState: getInitialState() }); - const { getByTestId } = createComponentRenderer(SettingsUsersView)({ - pinia, - }); + const rbacStore = mockedStore(useRBACStore); + rbacStore.hasScope.mockReturnValue(true); - const userListItem = getByTestId(`user-list-item-${users.at(-1)?.email}`); + const userStore = useUsersStore(); + + const { getByTestId } = renderView({ pinia }); + + const userListItem = getByTestId(`user-list-item-${invitedUser.email}`); expect(userListItem).toBeInTheDocument(); const roleSelect = within(userListItem).getByTestId('user-role-select'); @@ -185,7 +278,7 @@ describe('SettingsUsersView', () => { const roleDropdownItems = await getDropdownItems(roleSelect); await userEvent.click(roleDropdownItems[0]); - expect(updateGlobalRoleSpy).toHaveBeenCalledWith( + expect(userStore.updateGlobalRole).toHaveBeenCalledWith( expect.objectContaining({ newRoleName: 'global:member' }), ); From 15f311c89076409178dc808b1b739926e37ed70e Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:49:50 +0300 Subject: [PATCH 019/259] ci: Fixes to benchmarks in cloud (#10626) --- .../workflows/benchmark-destroy-nightly.yml | 2 +- .github/workflows/benchmark-nightly.yml | 4 ++++ .../infra/modules/benchmark-vm/vars.tf | 2 -- packages/@n8n/benchmark/infra/vars.tf | 4 ++-- packages/@n8n/benchmark/scripts/bootstrap.sh | 20 +++++++++++++++++-- .../@n8n/benchmark/scripts/runForN8nSetup.mjs | 2 ++ .../@n8n/benchmark/scripts/runInCloud.mjs | 4 +--- 7 files changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/benchmark-destroy-nightly.yml b/.github/workflows/benchmark-destroy-nightly.yml index e4b3e0d981..2ce4b51b3b 100644 --- a/.github/workflows/benchmark-destroy-nightly.yml +++ b/.github/workflows/benchmark-destroy-nightly.yml @@ -2,7 +2,7 @@ name: Destroy Benchmark Env on: schedule: - - cron: '0 1 * * *' + - cron: '0 4 * * *' workflow_dispatch: permissions: diff --git a/.github/workflows/benchmark-nightly.yml b/.github/workflows/benchmark-nightly.yml index f413a4aab7..2de86abaf7 100644 --- a/.github/workflows/benchmark-nightly.yml +++ b/.github/workflows/benchmark-nightly.yml @@ -58,6 +58,10 @@ jobs: tenant-id: ${{ env.ARM_TENANT_ID }} subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }} + - name: Destroy any existing environment + run: pnpm destroy-cloud-env + working-directory: packages/@n8n/benchmark + - name: Run the benchmark with debug logging if: github.event.inputs.debug == 'true' run: pnpm benchmark-in-cloud --n8nTag ${{ inputs.n8n_tag || 'nightly' }} --benchmarkTag ${{ inputs.benchmark_tag || 'latest' }} --debug diff --git a/packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf b/packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf index b7a3f18d77..69e6cd3572 100644 --- a/packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf +++ b/packages/@n8n/benchmark/infra/modules/benchmark-vm/vars.tf @@ -21,8 +21,6 @@ variable "ssh_public_key" { variable "vm_size" { description = "VM Size" - # 8 vCPUs, 32 GiB memory - default = "Standard_DC8_v2" } variable "tags" { diff --git a/packages/@n8n/benchmark/infra/vars.tf b/packages/@n8n/benchmark/infra/vars.tf index 379bafe80b..cb90a5ccc3 100644 --- a/packages/@n8n/benchmark/infra/vars.tf +++ b/packages/@n8n/benchmark/infra/vars.tf @@ -15,8 +15,8 @@ variable "host_size_family" { variable "vm_size" { description = "VM Size" - # 2 vCPUs, 8 GiB memory - default = "Standard_DC2s_v2" + # 8 vCPUs, 32 GiB memory + default = "Standard_DC8_v2" } variable "number_of_vms" { diff --git a/packages/@n8n/benchmark/scripts/bootstrap.sh b/packages/@n8n/benchmark/scripts/bootstrap.sh index 665e46d877..d7b3ed2fde 100644 --- a/packages/@n8n/benchmark/scripts/bootstrap.sh +++ b/packages/@n8n/benchmark/scripts/bootstrap.sh @@ -8,6 +8,22 @@ set -euo pipefail; CURRENT_USER=$(whoami) # Mount the data disk +# First wait for the disk to become available +WAIT_TIME=0 +MAX_WAIT_TIME=60 + +while [ ! -e /dev/sdc ]; do + if [ $WAIT_TIME -ge $MAX_WAIT_TIME ]; then + echo "Error: /dev/sdc did not become available within $MAX_WAIT_TIME seconds." + exit 1 + fi + + echo "Waiting for /dev/sdc to be available... ($WAIT_TIME/$MAX_WAIT_TIME)" + sleep 1 + WAIT_TIME=$((WAIT_TIME + 1)) +done + +# Then mount it if [ -d "/n8n" ]; then echo "Data disk already mounted. Clearing it..." sudo rm -rf /n8n/* @@ -28,8 +44,8 @@ curl -fsSL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh sudo -E bash nodesource_setup.sh # Install docker, docker compose and nodejs -sudo DEBIAN_FRONTEND=noninteractive apt-get update -sudo DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io docker-compose nodejs +sudo DEBIAN_FRONTEND=noninteractive apt-get update -yq +sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq docker.io docker-compose nodejs # Add the current user to the docker group sudo usermod -aG docker "$CURRENT_USER" diff --git a/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs b/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs index c7f1e88904..a6f4aeafe7 100755 --- a/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs +++ b/packages/@n8n/benchmark/scripts/runForN8nSetup.mjs @@ -30,6 +30,8 @@ async function main() { const runDir = path.join(baseRunDir, n8nSetupToUse); fs.emptyDirSync(runDir); + // Make sure the n8n container user (node) has write permissions to the run directory + await $`chmod 777 ${runDir}`; const dockerComposeClient = new DockerComposeClient({ $: $({ diff --git a/packages/@n8n/benchmark/scripts/runInCloud.mjs b/packages/@n8n/benchmark/scripts/runInCloud.mjs index dee3a30f06..bc55cf52b5 100755 --- a/packages/@n8n/benchmark/scripts/runInCloud.mjs +++ b/packages/@n8n/benchmark/scripts/runInCloud.mjs @@ -122,9 +122,7 @@ async function ensureVmIsReachable(sshClient) { * @returns Path where the scripts are located on the VM */ async function transferScriptsToVm(sshClient) { - await sshClient.ssh('rm -rf ~/n8n'); - - await sshClient.ssh('git clone --depth=1 https://github.com/n8n-io/n8n.git'); + await sshClient.ssh('rm -rf ~/n8n && git clone --depth=1 https://github.com/n8n-io/n8n.git'); return '~/n8n/packages/@n8n/benchmark/scripts'; } From ed66db77b6b07150578634cb9ce165c2fa2b43c2 Mon Sep 17 00:00:00 2001 From: Cornelius Suermann Date: Fri, 30 Aug 2024 17:01:44 +0200 Subject: [PATCH 020/259] docs: Add 'benchmark' scope to PR Title Conventions documentation (#10624) --- .github/pull_request_title_conventions.md | 44 ++++++++++++----------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/pull_request_title_conventions.md b/.github/pull_request_title_conventions.md index 0fc951d0e4..8073b54094 100644 --- a/.github/pull_request_title_conventions.md +++ b/.github/pull_request_title_conventions.md @@ -4,16 +4,16 @@ We have very precise rules over how Pull Requests (to the `master` branch) must A PR title consists of these elements: -``` +```text (): │ │ │ │ │ └─⫸ Summary: In imperative present tense. | | Capitalized | | No period at the end. │ │ - │ └─⫸ Scope: API|core|editor|* Node|benchmark + │ └─⫸ Scope: API | benchmark | core | editor | * Node │ - └─⫸ Type: build|ci|docs|feat|fix|perf|refactor|test + └─⫸ Type: build | ci | docs | feat | fix | perf | refactor | test ``` - PR title @@ -27,35 +27,37 @@ A PR title consists of these elements: The structure looks like this: -### **Type** +## Type Must be one of the following: -- `feat` - A new feature -- `fix` - A bug fix -- `perf` - A code change that improves performance -- `test` - Adding missing tests or correcting existing tests -- `docs` - Documentation only changes -- `refactor` - A code change that neither fixes a bug nor adds a feature -- `build` - Changes that affect the build system or external dependencies (example scopes: broccoli, npm) -- `ci` - Changes to our CI configuration files and scripts (e.g. Github actions) +| type | description | appears in changelog | +| --- | --- | --- | +| `feat` | A new feature | ✅ | +| `fix` | A bug fix | ✅ | +| `perf` | A code change that improves performance | ✅ | +| `test` | Adding missing tests or correcting existing tests | ❌ | +| `docs` | Documentation only changes | ❌ | +| `refactor` | A behavior-neutral code change that neither fixes a bug nor adds a feature | ❌ | +| `build` | Changes that affect the build system or external dependencies (TypeScript, Jest, pnpm, etc.) | ❌ | +| `ci` | Changes to CI configuration files and scripts (e.g. Github actions) | ❌ | -If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any BREAKING CHANGE (see Footer section below), the commit will always appear in the changelog. +> BREAKING CHANGES (see Footer section below), will **always** appear in the changelog unless suffixed with `no-changelog`. -### **Scope (optional)** +## Scope (optional) The scope should specify the place of the commit change as long as the commit clearly addresses one of the following supported scopes. (Otherwise, omit the scope!) - `API` - changes to the _public_ API +- `benchmark` - changes to the benchmark cli - `core` - changes to the core / private API / backend of n8n - `editor` - changes to the Editor UI - `* Node` - changes to a specific node or trigger node (”`*`” to be replaced with the node name, not its display name), e.g. - mattermost → Mattermost Node - microsoftToDo → Microsoft To Do Node - n8n → n8n Node -- `benchmark` - changes to the Benchmark cli -### **Summary** +## Summary The summary contains succinct description of the change: @@ -65,15 +67,15 @@ The summary contains succinct description of the change: - do _not_ include Linear ticket IDs etc. (e.g. N8N-1234) - suffix with “(no-changelog)” for commits / PRs that should not get mentioned in the changelog. -### **Body (optional)** +## Body (optional) Just as in the **summary**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior. -### **Footer (optional)** +## Footer (optional) The footer can contain information about breaking changes and deprecations and is also the place to [reference GitHub issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), Linear tickets, and other PRs that this commit closes or is related to. For example: -``` +```text BREAKING CHANGE: @@ -84,7 +86,7 @@ Fixes # or -``` +```text DEPRECATED: @@ -103,7 +105,7 @@ A Breaking Change section should start with the phrase "`BREAKING CHANGE:` " fol Similarly, a Deprecation section should start with "`DEPRECATED:` " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path. -### **Revert commits** +### Revert commits If the commit reverts a previous commit, it should begin with `revert:` , followed by the header of the reverted commit. From ad4137499b7d93ce38eac4b9c263493fdab1c5a1 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Fri, 30 Aug 2024 19:03:47 +0300 Subject: [PATCH 021/259] build: Fix `cli` nodemon config (#10628) --- packages/cli/nodemon.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/nodemon.json b/packages/cli/nodemon.json index 393762d234..ee2f85fbea 100644 --- a/packages/cli/nodemon.json +++ b/packages/cli/nodemon.json @@ -1,6 +1,6 @@ { "ignore": ["**/*.spec.ts", ".git", "node_modules"], - "watch": ["commands", "index.ts", "src"], + "watch": ["dist"], "exec": "npm start", - "ext": "ts" + "ext": "js" } From 7cfe56d727deac97cbbc0393ae1699fc72c1226e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Sun, 1 Sep 2024 20:03:10 +0200 Subject: [PATCH 022/259] refactor(editor): Migrate `MainSidebar.vue` to composition API (no-changelog) (#10538) --- packages/editor-ui/src/App.vue | 8 +- .../editor-ui/src/components/MainSidebar.vue | 566 +++++++++--------- 2 files changed, 271 insertions(+), 303 deletions(-) diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index d348f6ae79..03a4023119 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -11,10 +11,10 @@ import { loadLanguage } from '@/plugins/i18n'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { APP_MODALS_ELEMENT_ID, HIRING_BANNER, VIEWS } from '@/constants'; import { useRootStore } from '@/stores/root.store'; -import { useAssistantStore } from './stores/assistant.store'; -import { useUIStore } from './stores/ui.store'; -import { useUsersStore } from './stores/users.store'; -import { useSettingsStore } from './stores/settings.store'; +import { useAssistantStore } from '@/stores/assistant.store'; +import { useUIStore } from '@/stores/ui.store'; +import { useUsersStore } from '@/stores/users.store'; +import { useSettingsStore } from '@/stores/settings.store'; import { useHistoryHelper } from '@/composables/useHistoryHelper'; const route = useRoute(); diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 3cff24734f..4ae570141f 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -1,317 +1,285 @@ - - + diff --git a/packages/editor-ui/src/composables/useExecutionHelpers.ts b/packages/editor-ui/src/composables/useExecutionHelpers.ts index 9740f8dc06..a65182c475 100644 --- a/packages/editor-ui/src/composables/useExecutionHelpers.ts +++ b/packages/editor-ui/src/composables/useExecutionHelpers.ts @@ -8,6 +8,7 @@ export interface IExecutionUIData { startTime: string; runningTime: string; showTimestamp: boolean; + tags: Array<{ id: string; name: string }>; } export function useExecutionHelpers() { @@ -20,6 +21,7 @@ export function useExecutionHelpers() { label: 'Status unknown', runningTime: '', showTimestamp: true, + tags: execution.annotation?.tags ?? [], }; if (execution.status === 'new') { diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 3f2771cd10..f44046139e 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -48,6 +48,7 @@ export const DELETE_USER_MODAL_KEY = 'deleteUser'; export const INVITE_USER_MODAL_KEY = 'inviteUser'; export const DUPLICATE_MODAL_KEY = 'duplicate'; export const TAGS_MANAGER_MODAL_KEY = 'tagsManager'; +export const ANNOTATION_TAGS_MANAGER_MODAL_KEY = 'annotationTagsManager'; export const VERSIONS_MODAL_KEY = 'versions'; export const WORKFLOW_SETTINGS_MODAL_KEY = 'settings'; export const WORKFLOW_LM_CHAT_MODAL_KEY = 'lmChat'; @@ -630,6 +631,7 @@ export const enum STORES { NODE_TYPES = 'nodeTypes', CREDENTIALS = 'credentials', TAGS = 'tags', + ANNOTATION_TAGS = 'annotationTags', VERSIONS = 'versions', NODE_CREATOR = 'nodeCreator', WEBHOOKS = 'webhooks', @@ -691,6 +693,8 @@ export const MORE_ONBOARDING_OPTIONS_EXPERIMENT = { variant: 'variant', }; +export const EXECUTION_ANNOTATION_EXPERIMENT = '023_execution_annotation'; + export const EXPERIMENTS_TO_TRACK = [ ASK_AI_EXPERIMENT.name, TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, diff --git a/packages/editor-ui/src/permissions.spec.ts b/packages/editor-ui/src/permissions.spec.ts index bef37aa9a8..e7946ce421 100644 --- a/packages/editor-ui/src/permissions.spec.ts +++ b/packages/editor-ui/src/permissions.spec.ts @@ -5,6 +5,7 @@ import type { Scope } from '@n8n/permissions'; describe('permissions', () => { it('getResourcePermissions for empty scopes', () => { expect(getResourcePermissions()).toEqual({ + annotationTag: {}, auditLogs: {}, banner: {}, communityPackage: {}, @@ -58,6 +59,7 @@ describe('permissions', () => { ]; const permissionRecord: PermissionsRecord = { + annotationTag: {}, auditLogs: {}, banner: {}, communityPackage: {}, diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 3fd9714b8e..e1c12c350d 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -28,6 +28,8 @@ "clientSecret": "Client Secret" } }, + "generic.annotations": "Annotations", + "generic.annotationData": "Highlighted data", "generic.any": "Any", "generic.cancel": "Cancel", "generic.close": "Close", @@ -51,6 +53,7 @@ "generic.beta": "beta", "generic.yes": "Yes", "generic.no": "No", + "generic.rating": "Rating", "generic.retry": "Retry", "generic.error": "Something went wrong", "generic.settings": "Settings", @@ -96,6 +99,9 @@ "activationModal.yourTriggersWillNowFire": "Your triggers will now fire production executions automatically.", "activationModal.yourWorkflowWillNowListenForEvents": "Your workflow will now listen for events from {serviceName} and trigger executions.", "activationModal.yourWorkflowWillNowRegularlyCheck": "Your workflow will now regularly check {serviceName} for events and trigger executions for them.", + "annotationTagsManager.manageTags": "Manage execution tags", + "annotationTagsView.usage": "Usage (all workflows)", + "annotationTagsView.inUse": "{count} execution | {count} executions", "auth.changePassword": "Change password", "auth.changePassword.currentPassword": "Current password", "auth.changePassword.mfaCode": "Two-factor code", @@ -759,12 +765,23 @@ "executionView.onPaste.title": "Cannot paste here", "executionView.onPaste.message": "This view is read-only. Switch to Workflow tab to be able to edit the current workflow", "executionView.notFound.message": "Execution with id '{executionId}' could not be found!", + "executionAnnotationView.data.notFound": "Show important data from executions here by adding an execution data node to your workflow", + "executionAnnotationView.vote.error": "Unable to save annotation vote", + "executionAnnotationView.tag.error": "Unable to save annotation tags", + "executionAnnotationView.addTag": "Add tag", + "executionAnnotationView.chooseOrCreateATag": "Choose or create a tag", + "executionsFilter.annotation.tags": "Execution tags", + "executionsFilter.annotation.rating": "Rating", + "executionsFilter.annotation.rating.all": "Any rating", + "executionsFilter.annotation.rating.good": "Good", + "executionsFilter.annotation.rating.bad": "Bad", + "executionsFilter.annotation.selectVoteFilter": "Select Rating", "executionsFilter.selectStatus": "Select Status", "executionsFilter.selectWorkflow": "Select Workflow", "executionsFilter.start": "Execution start", "executionsFilter.startDate": "Earliest", "executionsFilter.endDate": "Latest", - "executionsFilter.savedData": "Custom data (saved in execution)", + "executionsFilter.savedData": "Highlighted data", "executionsFilter.savedDataKey": "Key", "executionsFilter.savedDataKeyPlaceholder": "ID", "executionsFilter.savedDataValue": "Value (exact match)", @@ -772,7 +789,7 @@ "executionsFilter.reset": "Reset all", "executionsFilter.customData.inputTooltip": "Upgrade plan to filter executions by custom data set at runtime. {link}", "executionsFilter.customData.inputTooltip.link": "View plans", - "executionsFilter.customData.docsTooltip": "Filter executions by data that you have explicitly saved in them (by calling $execution.customData.set(key, value)). {link}", + "executionsFilter.customData.docsTooltip": "Filter executions by data you have saved in them using an ‘Execution Data’ node. {link}", "executionsFilter.customData.docsTooltip.link": "More info", "expressionEdit.anythingInside": "Anything inside ", "expressionEdit.isJavaScript": " is JavaScript.", @@ -965,6 +982,8 @@ "ndv.httpRequest.credentialOnly.docsNotice": "Use the {nodeName} docs to construct your request. We'll take care of the authentication part if you add a {nodeName} credential below.", "noTagsView.readyToOrganizeYourWorkflows": "Ready to organize your workflows?", "noTagsView.withWorkflowTagsYouReFree": "With workflow tags, you're free to create the perfect tagging system for your flows", + "noAnnotationTagsView.title": "Organize your executions", + "noAnnotationTagsView.description": "Execution tags help you label and identify different classes of execution. Plus once you tag an execution, it’s never deleted", "node.thisIsATriggerNode": "This is a Trigger node. Learn more", "node.activateDeactivateNode": "Activate/Deactivate Node", "node.changeColor": "Change color", @@ -1914,6 +1933,8 @@ "tagsManager.couldNotDeleteTag": "Could not delete tag", "tagsManager.done": "Done", "tagsManager.manageTags": "Manage tags", + "tagsManager.showError.onFetch.title": "Could not fetch tags", + "tagsManager.showError.onFetch.message": "A problem occurred when trying to fetch tags", "tagsManager.showError.onCreate.message": "A problem occurred when trying to create the tag '{escapedName}'", "tagsManager.showError.onCreate.title": "Could not create tag", "tagsManager.showError.onDelete.message": "A problem occurred when trying to delete the tag '{escapedName}'", diff --git a/packages/editor-ui/src/stores/executions.store.ts b/packages/editor-ui/src/stores/executions.store.ts index af3850fd11..c96443ea41 100644 --- a/packages/editor-ui/src/stores/executions.store.ts +++ b/packages/editor-ui/src/stores/executions.store.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; -import type { IDataObject, ExecutionSummary } from 'n8n-workflow'; +import type { IDataObject, ExecutionSummary, AnnotationVote } from 'n8n-workflow'; import type { ExecutionFilterType, ExecutionsQueryFilter, @@ -82,9 +82,12 @@ export const useExecutionsStore = defineStore('executions', () => { const allExecutions = computed(() => [...currentExecutions.value, ...executions.value]); function addExecution(execution: ExecutionSummaryWithScopes) { - executionsById.value[execution.id] = { - ...execution, - mode: execution.mode, + executionsById.value = { + ...executionsById.value, + [execution.id]: { + ...execution, + mode: execution.mode, + }, }; } @@ -185,6 +188,24 @@ export const useExecutionsStore = defineStore('executions', () => { } } + async function annotateExecution( + id: string, + data: { tags?: string[]; vote?: AnnotationVote | null }, + ): Promise { + const updatedExecution: ExecutionSummaryWithScopes = await makeRestApiRequest( + rootStore.restApiContext, + 'PATCH', + `/executions/${id}`, + data, + ); + + addExecution(updatedExecution); + + if (updatedExecution.id === activeExecution.value?.id) { + activeExecution.value = updatedExecution; + } + } + async function stopCurrentExecution(executionId: string): Promise { return await makeRestApiRequest( rootStore.restApiContext, @@ -245,6 +266,7 @@ export const useExecutionsStore = defineStore('executions', () => { return { loading, + annotateExecution, executionsById, executions, executionsCount, diff --git a/packages/editor-ui/src/stores/rbac.store.ts b/packages/editor-ui/src/stores/rbac.store.ts index a45d0964ae..d51bbc8538 100644 --- a/packages/editor-ui/src/stores/rbac.store.ts +++ b/packages/editor-ui/src/stores/rbac.store.ts @@ -14,6 +14,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => { const scopesByResourceId = ref>>({ workflow: {}, tag: {}, + annotationTag: {}, user: {}, credential: {}, variable: {}, diff --git a/packages/editor-ui/src/stores/tags.store.ts b/packages/editor-ui/src/stores/tags.store.ts index 4dab82a8cb..fade74a39c 100644 --- a/packages/editor-ui/src/stores/tags.store.ts +++ b/packages/editor-ui/src/stores/tags.store.ts @@ -1,4 +1,4 @@ -import * as tagsApi from '@/api/tags'; +import { createTagsApi } from '@/api/tags'; import { STORES } from '@/constants'; import type { ITag } from '@/Interface'; import { defineStore } from 'pinia'; @@ -6,109 +6,129 @@ import { useRootStore } from './root.store'; import { computed, ref } from 'vue'; import { useWorkflowsStore } from './workflows.store'; -export const useTagsStore = defineStore(STORES.TAGS, () => { - const tagsById = ref>({}); - const loading = ref(false); - const fetchedAll = ref(false); - const fetchedUsageCount = ref(false); +const apiMapping = { + [STORES.TAGS]: createTagsApi('/tags'), + [STORES.ANNOTATION_TAGS]: createTagsApi('/annotation-tags'), +}; - const rootStore = useRootStore(); - const workflowsStore = useWorkflowsStore(); +const createTagsStore = (id: STORES.TAGS | STORES.ANNOTATION_TAGS) => { + const tagsApi = apiMapping[id]; - // Computed + return defineStore( + id, + () => { + const tagsById = ref>({}); + const loading = ref(false); + const fetchedAll = ref(false); + const fetchedUsageCount = ref(false); - const allTags = computed(() => { - return Object.values(tagsById.value).sort((a, b) => a.name.localeCompare(b.name)); - }); + const rootStore = useRootStore(); + const workflowsStore = useWorkflowsStore(); - const isLoading = computed(() => loading.value); + // Computed - const hasTags = computed(() => Object.keys(tagsById.value).length > 0); + const allTags = computed(() => { + return Object.values(tagsById.value).sort((a, b) => a.name.localeCompare(b.name)); + }); - // Methods + const isLoading = computed(() => loading.value); - const setAllTags = (loadedTags: ITag[]) => { - tagsById.value = loadedTags.reduce((accu: { [id: string]: ITag }, tag: ITag) => { - accu[tag.id] = tag; + const hasTags = computed(() => Object.keys(tagsById.value).length > 0); - return accu; - }, {}); - fetchedAll.value = true; - }; + // Methods - const upsertTags = (toUpsertTags: ITag[]) => { - toUpsertTags.forEach((toUpsertTag) => { - const tagId = toUpsertTag.id; - const currentTag = tagsById.value[tagId]; - if (currentTag) { - const newTag = { - ...currentTag, - ...toUpsertTag, - }; - tagsById.value = { - ...tagsById.value, - [tagId]: newTag, - }; - } else { - tagsById.value = { - ...tagsById.value, - [tagId]: toUpsertTag, - }; - } - }); - }; + const setAllTags = (loadedTags: ITag[]) => { + tagsById.value = loadedTags.reduce((accu: { [id: string]: ITag }, tag: ITag) => { + accu[tag.id] = tag; - const deleteTag = (id: string) => { - const { [id]: deleted, ...rest } = tagsById.value; - tagsById.value = rest; - }; + return accu; + }, {}); + fetchedAll.value = true; + }; - const fetchAll = async (params?: { force?: boolean; withUsageCount?: boolean }) => { - const { force = false, withUsageCount = false } = params || {}; - if (!force && fetchedAll.value && fetchedUsageCount.value === withUsageCount) { - return Object.values(tagsById.value); - } + const upsertTags = (toUpsertTags: ITag[]) => { + toUpsertTags.forEach((toUpsertTag) => { + const tagId = toUpsertTag.id; + const currentTag = tagsById.value[tagId]; + if (currentTag) { + const newTag = { + ...currentTag, + ...toUpsertTag, + }; + tagsById.value = { + ...tagsById.value, + [tagId]: newTag, + }; + } else { + tagsById.value = { + ...tagsById.value, + [tagId]: toUpsertTag, + }; + } + }); + }; - loading.value = true; - const retrievedTags = await tagsApi.getTags(rootStore.restApiContext, Boolean(withUsageCount)); - setAllTags(retrievedTags); - loading.value = false; - return retrievedTags; - }; + const deleteTag = (id: string) => { + const { [id]: deleted, ...rest } = tagsById.value; + tagsById.value = rest; + }; - const create = async (name: string) => { - const createdTag = await tagsApi.createTag(rootStore.restApiContext, { name }); - upsertTags([createdTag]); - return createdTag; - }; + const fetchAll = async (params?: { force?: boolean; withUsageCount?: boolean }) => { + const { force = false, withUsageCount = false } = params || {}; + if (!force && fetchedAll.value && fetchedUsageCount.value === withUsageCount) { + return Object.values(tagsById.value); + } - const rename = async ({ id, name }: { id: string; name: string }) => { - const updatedTag = await tagsApi.updateTag(rootStore.restApiContext, id, { name }); - upsertTags([updatedTag]); - return updatedTag; - }; + loading.value = true; + const retrievedTags = await tagsApi.getTags( + rootStore.restApiContext, + Boolean(withUsageCount), + ); + setAllTags(retrievedTags); + loading.value = false; + return retrievedTags; + }; - const deleteTagById = async (id: string) => { - const deleted = await tagsApi.deleteTag(rootStore.restApiContext, id); + const create = async (name: string) => { + const createdTag = await tagsApi.createTag(rootStore.restApiContext, { name }); + upsertTags([createdTag]); + return createdTag; + }; - if (deleted) { - deleteTag(id); - workflowsStore.removeWorkflowTagId(id); - } + const rename = async ({ id, name }: { id: string; name: string }) => { + const updatedTag = await tagsApi.updateTag(rootStore.restApiContext, id, { name }); + upsertTags([updatedTag]); + return updatedTag; + }; - return deleted; - }; + const deleteTagById = async (id: string) => { + const deleted = await tagsApi.deleteTag(rootStore.restApiContext, id); - return { - allTags, - isLoading, - hasTags, - tagsById, - fetchAll, - create, - rename, - deleteTagById, - upsertTags, - deleteTag, - }; -}); + if (deleted) { + deleteTag(id); + workflowsStore.removeWorkflowTagId(id); + } + + return deleted; + }; + + return { + allTags, + isLoading, + hasTags, + tagsById, + fetchAll, + create, + rename, + deleteTagById, + upsertTags, + deleteTag, + }; + }, + {}, + ); +}; + +export const useTagsStore = createTagsStore(STORES.TAGS); + +export const useAnnotationTagsStore = createTagsStore(STORES.ANNOTATION_TAGS); diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index 6ade0750be..18cb3fbe37 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -19,6 +19,7 @@ import { PERSONALIZATION_MODAL_KEY, STORES, TAGS_MANAGER_MODAL_KEY, + ANNOTATION_TAGS_MANAGER_MODAL_KEY, NPS_SURVEY_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS, @@ -108,6 +109,7 @@ export const useUIStore = defineStore(STORES.UI, () => { PERSONALIZATION_MODAL_KEY, INVITE_USER_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, + ANNOTATION_TAGS_MANAGER_MODAL_KEY, NPS_SURVEY_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_LM_CHAT_MODAL_KEY, diff --git a/packages/editor-ui/src/utils/executionUtils.ts b/packages/editor-ui/src/utils/executionUtils.ts index db75c5687a..ad2712572c 100644 --- a/packages/editor-ui/src/utils/executionUtils.ts +++ b/packages/editor-ui/src/utils/executionUtils.ts @@ -9,7 +9,9 @@ export function getDefaultExecutionFilters(): ExecutionFilterType { startDate: '', endDate: '', tags: [], + annotationTags: [], metadata: [], + vote: 'all', }; } @@ -25,6 +27,14 @@ export const executionFilterToQueryFilter = ( queryFilter.tags = filter.tags; } + if (!isEmpty(filter.annotationTags)) { + queryFilter.annotationTags = filter.annotationTags; + } + + if (filter.vote !== 'all') { + queryFilter.vote = filter.vote; + } + if (!isEmpty(filter.metadata)) { queryFilter.metadata = filter.metadata; } @@ -54,6 +64,7 @@ export const executionFilterToQueryFilter = ( queryFilter.status = ['canceled']; break; } + return queryFilter; }; diff --git a/packages/editor-ui/src/views/WorkflowsView.vue b/packages/editor-ui/src/views/WorkflowsView.vue index 86df4f4408..3bc5a8b914 100644 --- a/packages/editor-ui/src/views/WorkflowsView.vue +++ b/packages/editor-ui/src/views/WorkflowsView.vue @@ -2,9 +2,9 @@ import { defineComponent } from 'vue'; import ResourcesListLayout, { type IResource } from '@/components/layouts/ResourcesListLayout.vue'; import WorkflowCard from '@/components/WorkflowCard.vue'; +import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue'; import { EnterpriseEditionFeature, MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants'; import type { ITag, IUser, IWorkflowDb } from '@/Interface'; -import TagsDropdown from '@/components/TagsDropdown.vue'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui.store'; import { useSettingsStore } from '@/stores/settings.store'; @@ -36,7 +36,7 @@ const WorkflowsView = defineComponent({ components: { ResourcesListLayout, WorkflowCard, - TagsDropdown, + WorkflowTagsDropdown, ProjectTabs, }, data() { @@ -432,7 +432,7 @@ export default WorkflowsView; color="text-base" class="mb-3xs" /> - This feature is available on our Pro and Enterprise plans. More Info.", + "Save important data using this node. It will be displayed on each execution for easy reference and you can filter by it.
Filtering is available on Pro and Enterprise plans. More Info", name: 'notice', type: 'notice', default: '', @@ -38,9 +38,9 @@ export class ExecutionData implements INodeType { noDataExpression: true, options: [ { - name: 'Save Execution Data for Search', + name: 'Save Highlight Data (for Search/review)', value: 'save', - action: 'Save execution data for search', + action: 'Save Highlight Data (for search/review)', }, ], }, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index e0ac865381..d828fa58b4 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2435,6 +2435,8 @@ export interface NodeExecutionWithMetadata extends INodeExecutionData { pairedItem: IPairedItemData | IPairedItemData[]; } +export type AnnotationVote = 'up' | 'down'; + export interface ExecutionSummary { id: string; finished?: boolean; @@ -2452,6 +2454,13 @@ export interface ExecutionSummary { nodeExecutionStatus?: { [key: string]: IExecutionSummaryNodeExecutionResult; }; + annotation?: { + vote: AnnotationVote; + tags: Array<{ + id: string; + name: string; + }>; + }; } export interface IExecutionSummaryNodeExecutionResult { From 14952eb83b561cee1b61bc5d6cc3748d2bfe16aa Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:57:07 +0300 Subject: [PATCH 027/259] fix: Re-enable infra provisioning and teardown (no-changelog) (#10636) --- packages/@n8n/benchmark/scripts/clients/terraformClient.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@n8n/benchmark/scripts/clients/terraformClient.mjs b/packages/@n8n/benchmark/scripts/clients/terraformClient.mjs index 8615c941ba..b156998f92 100644 --- a/packages/@n8n/benchmark/scripts/clients/terraformClient.mjs +++ b/packages/@n8n/benchmark/scripts/clients/terraformClient.mjs @@ -30,7 +30,7 @@ export class TerraformClient { console.log('Provisioning cloud environment...'); await this.$$`terraform init`; - // await this.$$`terraform apply -input=false -auto-approve`; + await this.$$`terraform apply -input=false -auto-approve`; const privateKeyName = await this.extractPrivateKey(); @@ -50,7 +50,7 @@ export class TerraformClient { console.log('Destroying cloud environment...'); - // await this.$$`terraform destroy -input=false -auto-approve`; + await this.$$`terraform destroy -input=false -auto-approve`; } async getTerraformOutput(key) { From 6bb6a5c6cd1da3503a1a2b35bcf4c685cd3f964f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 2 Sep 2024 16:47:20 +0200 Subject: [PATCH 028/259] fix(core): Flush responses for ai streaming endpoints (#10633) --- .../controllers/ai-assistant.controller.ts | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/controllers/ai-assistant.controller.ts b/packages/cli/src/controllers/ai-assistant.controller.ts index 6248fa3bb0..fb11e15d1a 100644 --- a/packages/cli/src/controllers/ai-assistant.controller.ts +++ b/packages/cli/src/controllers/ai-assistant.controller.ts @@ -1,31 +1,40 @@ -import { Post, RestController } from '@/decorators'; -import { AiAssistantService } from '@/services/ai-assistant.service'; -import { AiAssistantRequest } from '@/requests'; -import { Response } from 'express'; +import type { Response } from 'express'; import type { AiAssistantSDK } from '@n8n_io/ai-assistant-sdk'; -import { Readable, promises } from 'node:stream'; -import { InternalServerError } from 'express-openapi-validator/dist/openapi.validator'; +import { WritableStream } from 'node:stream/web'; import { strict as assert } from 'node:assert'; import { ErrorReporterProxy } from 'n8n-workflow'; +import { Post, RestController } from '@/decorators'; +import { InternalServerError } from '@/errors/response-errors/internal-server.error'; +import { AiAssistantRequest } from '@/requests'; +import { AiAssistantService } from '@/services/ai-assistant.service'; + +type FlushableResponse = Response & { flush: () => void }; + @RestController('/ai-assistant') export class AiAssistantController { constructor(private readonly aiAssistantService: AiAssistantService) {} @Post('/chat', { rateLimit: { limit: 100 } }) - async chat(req: AiAssistantRequest.Chat, res: Response) { + async chat(req: AiAssistantRequest.Chat, res: FlushableResponse) { try { - const stream = await this.aiAssistantService.chat(req.body, req.user); - - if (stream.body) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - await promises.pipeline(Readable.fromWeb(stream.body), res); + const aiResponse = await this.aiAssistantService.chat(req.body, req.user); + if (aiResponse.body) { + res.header('Content-type', 'application/json-lines').flush(); + await aiResponse.body.pipeTo( + new WritableStream({ + write(chunk) { + res.write(chunk); + res.flush(); + }, + }), + ); + res.end(); } } catch (e) { - // todo add sentry reporting assert(e instanceof Error); ErrorReporterProxy.error(e); - throw new InternalServerError({ message: `Something went wrong: ${e.message}` }); + throw new InternalServerError(`Something went wrong: ${e.message}`); } } @@ -38,7 +47,7 @@ export class AiAssistantController { } catch (e) { assert(e instanceof Error); ErrorReporterProxy.error(e); - throw new InternalServerError({ message: `Something went wrong: ${e.message}` }); + throw new InternalServerError(`Something went wrong: ${e.message}`); } } } From 7fd0c71bdc0e3d15902d100368ffe777ccf81adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 2 Sep 2024 17:38:48 +0200 Subject: [PATCH 029/259] feat(core): Use ES2021 as the tsconfig target for all backend packages (no-changelog) (#10639) --- packages/@n8n/nodes-langchain/tsconfig.json | 1 - packages/node-dev/src/tsconfig-build.json | 4 ++-- packages/nodes-base/tsconfig.json | 1 - packages/workflow/tsconfig.json | 1 - tsconfig.json | 4 ++-- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/@n8n/nodes-langchain/tsconfig.json b/packages/@n8n/nodes-langchain/tsconfig.json index f210bbc5b3..a0bd21149f 100644 --- a/packages/@n8n/nodes-langchain/tsconfig.json +++ b/packages/@n8n/nodes-langchain/tsconfig.json @@ -1,7 +1,6 @@ { "extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"], "compilerOptions": { - "lib": ["es2020", "es2022.error"], "tsBuildInfoFile": "dist/typecheck.tsbuildinfo", // TODO: remove all options below this line "useUnknownInCatchVariables": false diff --git a/packages/node-dev/src/tsconfig-build.json b/packages/node-dev/src/tsconfig-build.json index 4b67e701b9..25a20b69ab 100644 --- a/packages/node-dev/src/tsconfig-build.json +++ b/packages/node-dev/src/tsconfig-build.json @@ -3,8 +3,8 @@ "strict": true, "module": "commonjs", "moduleResolution": "node", - "target": "es2019", - "lib": ["es2019", "es2020"], + "target": "es2021", + "lib": ["es2021"], "importHelpers": true, "esModuleInterop": true, "declaration": true, diff --git a/packages/nodes-base/tsconfig.json b/packages/nodes-base/tsconfig.json index 7e4ed688fb..83f9bf92a4 100644 --- a/packages/nodes-base/tsconfig.json +++ b/packages/nodes-base/tsconfig.json @@ -1,7 +1,6 @@ { "extends": ["../../tsconfig.json", "../../tsconfig.backend.json"], "compilerOptions": { - "lib": ["dom", "es2020", "es2022.error"], "paths": { "@test/*": ["./test/*"], "@utils/*": ["./utils/*"] diff --git a/packages/workflow/tsconfig.json b/packages/workflow/tsconfig.json index 0b486eab47..2f0507b565 100644 --- a/packages/workflow/tsconfig.json +++ b/packages/workflow/tsconfig.json @@ -6,7 +6,6 @@ "paths": { "@/*": ["./*"] }, - "lib": ["es2020", "es2022.error", "dom"], "tsBuildInfoFile": "dist/typecheck.tsbuildinfo" }, "include": ["src/**/*.ts", "test/**/*.ts"] diff --git a/tsconfig.json b/tsconfig.json index 1b0bd25650..5734019388 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,8 @@ "strict": true, "module": "commonjs", "moduleResolution": "node", - "target": "es2019", - "lib": ["es2019", "es2020", "es2022.error"], + "target": "es2021", + "lib": ["es2021", "es2022.error", "dom"], "removeComments": true, "useUnknownInCatchVariables": true, "forceConsistentCasingInFileNames": true, From ce39933766fa18107f4082de0cba0b6702cbbbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Tue, 3 Sep 2024 10:06:16 +0200 Subject: [PATCH 030/259] fix(editor): Allow disabling SSO when config request fails (#10635) --- .../editor-ui/src/views/SettingsSso.test.ts | 203 ++++++++++++++++++ packages/editor-ui/src/views/SettingsSso.vue | 21 +- 2 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 packages/editor-ui/src/views/SettingsSso.test.ts diff --git a/packages/editor-ui/src/views/SettingsSso.test.ts b/packages/editor-ui/src/views/SettingsSso.test.ts new file mode 100644 index 0000000000..793f28bda2 --- /dev/null +++ b/packages/editor-ui/src/views/SettingsSso.test.ts @@ -0,0 +1,203 @@ +import { createTestingPinia } from '@pinia/testing'; +import { createComponentRenderer } from '@/__tests__/render'; +import SettingsSso from './SettingsSso.vue'; +import { useSSOStore } from '@/stores/sso.store'; +import { useUIStore } from '@/stores/ui.store'; +import { within, waitFor } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import { mockedStore } from '@/__tests__/utils'; + +const renderView = createComponentRenderer(SettingsSso); + +const samlConfig = { + metadata: 'metadata dummy', + metadataUrl: + 'https://dev-qqkrykgkoo0p63d5.eu.auth0.com/samlp/metadata/KR1cSrRrxaZT2gV8ZhPAUIUHtEY4duhN', + entityID: 'https://n8n-tunnel.myhost.com/rest/sso/saml/metadata', + returnUrl: 'https://n8n-tunnel.myhost.com/rest/sso/saml/acs', +}; + +const telemetryTrack = vi.fn(); +vi.mock('@/composables/useTelemetry', () => ({ + useTelemetry: () => ({ + track: telemetryTrack, + }), +})); + +const showError = vi.fn(); +vi.mock('@/composables/useToast', () => ({ + useToast: () => ({ + showError, + }), +})); + +const confirmMessage = vi.fn(); +vi.mock('@/composables/useMessage', () => ({ + useMessage: () => ({ + confirm: confirmMessage, + }), +})); + +describe('SettingsSso View', () => { + beforeEach(() => { + telemetryTrack.mockReset(); + confirmMessage.mockReset(); + showError.mockReset(); + }); + + it('should show upgrade banner when enterprise SAML is disabled', async () => { + const pinia = createTestingPinia(); + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = false; + + const uiStore = useUIStore(); + + const { getByTestId } = renderView({ pinia }); + + const actionBox = getByTestId('sso-content-unlicensed'); + expect(actionBox).toBeInTheDocument(); + + await userEvent.click(await within(actionBox).findByText('See plans')); + expect(uiStore.goToUpgrade).toHaveBeenCalledWith('sso', 'upgrade-sso'); + }); + + it('should show user SSO config', async () => { + const pinia = createTestingPinia(); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + + ssoStore.getSamlConfig.mockResolvedValue(samlConfig); + + const { getAllByTestId } = renderView({ pinia }); + + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(1); + + await waitFor(async () => { + const copyInputs = getAllByTestId('copy-input'); + expect(copyInputs[0].textContent).toContain(samlConfig.returnUrl); + expect(copyInputs[1].textContent).toContain(samlConfig.entityID); + }); + }); + + it('allows user to toggle SSO', async () => { + const pinia = createTestingPinia(); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isSamlLoginEnabled = false; + + ssoStore.getSamlConfig.mockResolvedValue(samlConfig); + + const { getByTestId } = renderView({ pinia }); + + const toggle = getByTestId('sso-toggle'); + + expect(toggle.textContent).toContain('Deactivated'); + + await userEvent.click(toggle); + expect(toggle.textContent).toContain('Activated'); + + await userEvent.click(toggle); + expect(toggle.textContent).toContain('Deactivated'); + }); + + it("allows user to fill Identity Provider's URL", async () => { + confirmMessage.mockResolvedValueOnce('confirm'); + + const pinia = createTestingPinia(); + const windowOpenSpy = vi.spyOn(window, 'open'); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + + const { getByTestId } = renderView({ pinia }); + + const saveButton = getByTestId('sso-save'); + expect(saveButton).toBeDisabled(); + + const urlinput = getByTestId('sso-provider-url'); + + expect(urlinput).toBeVisible(); + await userEvent.type(urlinput, samlConfig.metadataUrl); + + expect(saveButton).not.toBeDisabled(); + await userEvent.click(saveButton); + + expect(ssoStore.saveSamlConfig).toHaveBeenCalledWith( + expect.objectContaining({ metadataUrl: samlConfig.metadataUrl }), + ); + + expect(ssoStore.testSamlConfig).toHaveBeenCalled(); + expect(windowOpenSpy).toHaveBeenCalled(); + + expect(telemetryTrack).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ identity_provider: 'metadata' }), + ); + + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); + }); + + it("allows user to fill Identity Provider's XML", async () => { + confirmMessage.mockResolvedValueOnce('confirm'); + + const pinia = createTestingPinia(); + const windowOpenSpy = vi.spyOn(window, 'open'); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + + const { getByTestId } = renderView({ pinia }); + + const saveButton = getByTestId('sso-save'); + expect(saveButton).toBeDisabled(); + + await userEvent.click(getByTestId('radio-button-xml')); + + const xmlInput = getByTestId('sso-provider-xml'); + + expect(xmlInput).toBeVisible(); + await userEvent.type(xmlInput, samlConfig.metadata); + + expect(saveButton).not.toBeDisabled(); + await userEvent.click(saveButton); + + expect(ssoStore.saveSamlConfig).toHaveBeenCalledWith( + expect.objectContaining({ metadata: samlConfig.metadata }), + ); + + expect(ssoStore.testSamlConfig).toHaveBeenCalled(); + expect(windowOpenSpy).toHaveBeenCalled(); + + expect(telemetryTrack).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ identity_provider: 'xml' }), + ); + + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); + }); + + it('PAY-1812: allows user to disable SSO even if config request failed', async () => { + const pinia = createTestingPinia(); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isSamlLoginEnabled = true; + + const error = new Error('Request failed with status code 404'); + ssoStore.getSamlConfig.mockRejectedValue(error); + + const { getByTestId } = renderView({ pinia }); + + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(1); + + await waitFor(async () => { + expect(showError).toHaveBeenCalledWith(error, 'error'); + const toggle = getByTestId('sso-toggle'); + expect(toggle.textContent).toContain('Activated'); + await userEvent.click(toggle); + expect(toggle.textContent).toContain('Deactivated'); + }); + }); +}); diff --git a/packages/editor-ui/src/views/SettingsSso.vue b/packages/editor-ui/src/views/SettingsSso.vue index 34ee28e647..a73733392b 100644 --- a/packages/editor-ui/src/views/SettingsSso.vue +++ b/packages/editor-ui/src/views/SettingsSso.vue @@ -134,6 +134,15 @@ const goToUpgrade = () => { void uiStore.goToUpgrade('sso', 'upgrade-sso'); }; +const isToggleSsoDisabled = computed(() => { + /** Allow users to disable SSO even if config request fails */ + if (ssoStore.isSamlLoginEnabled) { + return false; + } + + return !ssoSettingsSaved.value; +}); + onMounted(async () => { if (!ssoStore.isEnterpriseSamlEnabled) { return; @@ -162,7 +171,8 @@ onMounted(async () => { @@ -205,11 +215,18 @@ onMounted(async () => { name="metadataUrl" size="large" :placeholder="i18n.baseText('settings.sso.settings.ips.url.placeholder')" + data-test-id="sso-provider-url" /> {{ i18n.baseText('settings.sso.settings.ips.url.help') }}
- + {{ i18n.baseText('settings.sso.settings.ips.xml.help') }}
From 5eba5343191665cd4639632ba303464176c279c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Tue, 3 Sep 2024 10:41:59 +0200 Subject: [PATCH 031/259] fix(editor): Fix notification rendering HTML as text (#10642) --- .../NodeViewUnfinishedWorkflowMessage.vue | 18 ++++++++++++++++++ packages/editor-ui/src/views/NodeView.v2.vue | 8 +++----- packages/editor-ui/src/views/NodeView.vue | 11 +++-------- 3 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 packages/editor-ui/src/components/NodeViewUnfinishedWorkflowMessage.vue diff --git a/packages/editor-ui/src/components/NodeViewUnfinishedWorkflowMessage.vue b/packages/editor-ui/src/components/NodeViewUnfinishedWorkflowMessage.vue new file mode 100644 index 0000000000..e074d624eb --- /dev/null +++ b/packages/editor-ui/src/components/NodeViewUnfinishedWorkflowMessage.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index 9446dc1391..04cd656e8d 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -11,6 +11,7 @@ import { ref, useCssModule, watch, + h, } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue'; @@ -97,6 +98,7 @@ import type { PinDataSource } from '@/composables/usePinnedData'; import { useClipboard } from '@/composables/useClipboard'; import { useBeforeUnload } from '@/composables/useBeforeUnload'; import { getResourcePermissions } from '@/permissions'; +import NodeViewUnfinishedWorkflowMessage from '@/components/NodeViewUnfinishedWorkflowMessage.vue'; const LazyNodeCreation = defineAsyncComponent( async () => await import('@/components/Node/NodeCreation.vue'), @@ -1062,11 +1064,7 @@ function onExecutionOpenedWithWaitTill(data: IExecutionResponse) { if ((data as ExecutionSummary).waitTill) { toast.showMessage({ title: i18n.baseText('nodeView.thisExecutionHasntFinishedYet'), - message: `${i18n.baseText('nodeView.refresh')} ${i18n.baseText( - 'nodeView.toSeeTheLatestStatus', - )}.
${i18n.baseText( - 'nodeView.moreInfo', - )}`, + message: h(NodeViewUnfinishedWorkflowMessage), type: 'warning', duration: 0, }); diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 608d9cb82f..7d95ed54f6 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -1,5 +1,5 @@ + + + + + + diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts b/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts new file mode 100644 index 0000000000..870127f0d3 --- /dev/null +++ b/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.stories.ts @@ -0,0 +1,30 @@ +import DemoComponent from './DemoComponent.vue'; +import type { StoryFn } from '@storybook/vue3'; + +export default { + title: 'Assistant/AskAssistantLoadingMessageTransitions', + component: DemoComponent, + argTypes: {}, +}; + +const Template: StoryFn = (args, { argTypes }) => ({ + setup: () => ({ args }), + props: Object.keys(argTypes), + components: { + DemoComponent, + }, + template: '', +}); + +export const Default = Template.bind({}); +Default.args = {}; + +export const Horizontal = Template.bind({}); +Horizontal.args = { + animationType: 'slide-horizontal', +}; + +export const Fade = Template.bind({}); +Fade.args = { + animationType: 'fade', +}; diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue b/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue new file mode 100644 index 0000000000..deaf903af5 --- /dev/null +++ b/packages/design-system/src/components/AskAssistantLoadingMessage/DemoComponent.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/AskAssistantLoadingMessage.spec.ts b/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/AskAssistantLoadingMessage.spec.ts new file mode 100644 index 0000000000..86f9b8ae51 --- /dev/null +++ b/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/AskAssistantLoadingMessage.spec.ts @@ -0,0 +1,13 @@ +import { render } from '@testing-library/vue'; +import AssistantLoadingMessage from '../AssistantLoadingMessage.vue'; + +describe('AssistantLoadingMessage', () => { + it('renders loading message correctly', () => { + const { container } = render(AssistantLoadingMessage, { + props: { + loadingMessage: 'Thinking...', + }, + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/__snapshots__/AskAssistantLoadingMessage.spec.ts.snap b/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/__snapshots__/AskAssistantLoadingMessage.spec.ts.snap new file mode 100644 index 0000000000..32d8db1bba --- /dev/null +++ b/packages/design-system/src/components/AskAssistantLoadingMessage/__tests__/__snapshots__/AskAssistantLoadingMessage.spec.ts.snap @@ -0,0 +1,71 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`AssistantLoadingMessage > renders loading message correctly 1`] = ` +
+
+
+
+ + + + + + + + + + +
+
+
+ + + +
+
+
+`; diff --git a/packages/design-system/src/locale/lang/en.ts b/packages/design-system/src/locale/lang/en.ts index 449a617f91..a9357c4ce9 100644 --- a/packages/design-system/src/locale/lang/en.ts +++ b/packages/design-system/src/locale/lang/en.ts @@ -38,12 +38,14 @@ export default { 'assistantChat.sessionEndMessage.2': 'button in n8n', 'assistantChat.you': 'You', 'assistantChat.quickRepliesTitle': 'Quick reply 👇', - 'assistantChat.placeholder.1': (options: string[]) => - `Hi ${options[0][0] || 'there'}, I'm ${options[0][1]} and I'm here to assist you with building workflows.`, + 'assistantChat.placeholder.1': () => + "I'm your Assistant, here to guide you through your journey with n8n.", 'assistantChat.placeholder.2': - "Whenever you encounter a task that I can help with, you'll see the", - 'assistantChat.placeholder.3': 'button.', - 'assistantChat.placeholder.4': 'Clicking it starts a chat session with me.', + "While I'm still learning, I'm already equipped to help you debug any errors you might encounter.", + 'assistantChat.placeholder.3': "If you run into an issue with a node, you'll see the", + 'assistantChat.placeholder.4': 'button', + 'assistantChat.placeholder.5': + "Clicking it will start a chat with me, and I'll do my best to assist you!", 'assistantChat.inputPlaceholder': 'Enter your response...', 'inlineAskAssistantButton.asked': 'Asked', } as N8nLocale; diff --git a/packages/design-system/src/types/assistant.ts b/packages/design-system/src/types/assistant.ts index 40e1340d46..b1cab7a748 100644 --- a/packages/design-system/src/types/assistant.ts +++ b/packages/design-system/src/types/assistant.ts @@ -32,6 +32,7 @@ export namespace ChatUI { export interface QuickReply { type: string; text: string; + isFeedback?: boolean; } export interface ErrorMessage { diff --git a/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue b/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue index ab4e62efa3..9ae4b29a5f 100644 --- a/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue +++ b/packages/editor-ui/src/components/AskAssistant/AskAssistantChat.vue @@ -16,6 +16,8 @@ const user = computed(() => ({ lastName: usersStore.currentUser?.lastName ?? '', })); +const loadingMessage = computed(() => assistantStore.assistantThinkingMessage); + function onResize(data: { direction: string; x: number; width: number }) { assistantStore.updateWindowWidth(data.width); } @@ -24,7 +26,7 @@ function onResizeDebounced(data: { direction: string; x: number; width: number } void useDebounce().callDebounced(onResize, { debounceTime: 10, trailing: true }, data); } -async function onUserMessage(content: string, quickReplyType?: string) { +async function onUserMessage(content: string, quickReplyType?: string, isFeedback = false) { await assistantStore.sendMessage({ text: content, quickReplyType }); const task = 'error'; const solutionCount = @@ -33,9 +35,10 @@ async function onUserMessage(content: string, quickReplyType?: string) { (msg) => msg.role === 'assistant' && !['text', 'event'].includes(msg.type), ).length : null; - if (quickReplyType === 'all-good' || quickReplyType === 'still-stuck') { + if (isFeedback) { telemetry.track('User gave feedback', { task, + chat_session_id: assistantStore.currentSessionId, is_quick_reply: !!quickReplyType, is_positive: quickReplyType === 'all-good', solution_count: solutionCount, @@ -83,6 +86,8 @@ function onClose() { :user="user" :messages="assistantStore.chatMessages" :streaming="assistantStore.streaming" + :loading-message="loadingMessage" + :session-id="assistantStore.currentSessionId" @close="onClose" @message="onUserMessage" @code-replace="onCodeReplace" diff --git a/packages/editor-ui/src/composables/useToast.ts b/packages/editor-ui/src/composables/useToast.ts index 609683b7e1..ea0a367de3 100644 --- a/packages/editor-ui/src/composables/useToast.ts +++ b/packages/editor-ui/src/composables/useToast.ts @@ -22,7 +22,7 @@ const messageDefaults: Partial> = { position: 'bottom-right', zIndex: 1900, // above NDV and below the modals offset: 64, - appendTo: '#node-view-root', + appendTo: '#app-grid', customClass: 'content-toast', }; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index e1c12c350d..5afaf7f2a3 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -147,6 +147,8 @@ "aiAssistant.serviceError.message": "Unable to connect to n8n's AI service", "aiAssistant.codeUpdated.message.title": "Assistant modified workflow", "aiAssistant.codeUpdated.message.body": "Open the {nodeName} node to see the changes", + "aiAssistant.thinkingSteps.analyzingError": "Analyzing the error...", + "aiAssistant.thinkingSteps.thinking": "Thinking...", "banners.confirmEmail.message.1": "To secure your account and prevent future access issues, please confirm your", "banners.confirmEmail.message.2": "email address.", "banners.confirmEmail.button": "Confirm email", diff --git a/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts b/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts index 279f9e0b01..ffca695066 100644 --- a/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts +++ b/packages/editor-ui/src/stores/__tests__/assistant.store.test.ts @@ -311,7 +311,6 @@ describe('AI Assistant store', () => { }; const assistantStore = useAssistantStore(); await assistantStore.initErrorHelper(context); - expect(assistantStore.chatMessages.length).toBe(2); expect(apiSpy).toHaveBeenCalled(); }); }); diff --git a/packages/editor-ui/src/stores/assistant.store.ts b/packages/editor-ui/src/stores/assistant.store.ts index bad9e8aee2..05299ad6c0 100644 --- a/packages/editor-ui/src/stores/assistant.store.ts +++ b/packages/editor-ui/src/stores/assistant.store.ts @@ -31,7 +31,7 @@ import { useUIStore } from './ui.store'; export const MAX_CHAT_WIDTH = 425; export const MIN_CHAT_WIDTH = 250; -export const DEFAULT_CHAT_WIDTH = 325; +export const DEFAULT_CHAT_WIDTH = 330; export const ENABLED_VIEWS = [...EDITABLE_CANVAS_VIEWS, VIEWS.EXECUTION_PREVIEW]; const READABLE_TYPES = ['code-diff', 'text', 'block']; @@ -63,6 +63,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { const currentSessionActiveExecutionId = ref(); const currentSessionWorkflowId = ref(); const lastUnread = ref(); + const nodeExecutionStatus = ref<'not_executed' | 'success' | 'error'>('not_executed'); + // This is used to show a message when the assistant is performing intermediate steps + // We use streaming for assistants that support it, and this for agents + const assistantThinkingMessage = ref(); const isExperimentEnabled = computed( () => getVariant(AI_ASSISTANT_EXPERIMENT.name) === AI_ASSISTANT_EXPERIMENT.variant, @@ -117,6 +121,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { lastUnread.value = undefined; currentSessionActiveExecutionId.value = undefined; suggestions.value = {}; + nodeExecutionStatus.value = 'not_executed'; } // As assistant sidebar opens and closes, use window width to calculate the container width @@ -140,6 +145,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { const messages = [...chatMessages.value].filter( (msg) => !(msg.id === id && msg.role === 'assistant'), ); + assistantThinkingMessage.value = undefined; // TODO: simplify assistantMessages.forEach((msg) => { if (msg.type === 'message') { @@ -190,6 +196,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { quickReplies: msg.quickReplies, read, }); + } else if (msg.type === 'intermediate-step') { + assistantThinkingMessage.value = msg.text; } }); chatMessages.value = messages; @@ -226,14 +234,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { }); } - function addEmptyAssistantMessage(id: string) { - chatMessages.value.push({ - id, - role: 'assistant', - type: 'text', - content: '', - read: false, - }); + function addLoadingAssistantMessage(message: string) { + assistantThinkingMessage.value = message; } function addUserMessage(content: string, id: string) { @@ -249,6 +251,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { function handleServiceError(e: unknown, id: string) { assert(e instanceof Error); stopStreaming(); + assistantThinkingMessage.value = undefined; addAssistantError(`${locale.baseText('aiAssistant.serviceError.message')}: (${e.message})`, id); } @@ -316,7 +319,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { const availableAuthOptions = getNodeAuthOptions(nodeType); authType = availableAuthOptions.find((option) => option.value === credentialInUse); } - addEmptyAssistantMessage(id); + addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.analyzingError')); openChat(); streaming.value = true; @@ -351,7 +354,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { assert(currentSessionId.value); const id = getRandomId(); - addEmptyAssistantMessage(id); + addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking')); streaming.value = true; chatWithAssistant( rootStore.restApiContext, @@ -369,21 +372,30 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { (e) => handleServiceError(e, id), ); } - async function onNodeExecution(pushEvent: IPushDataNodeExecuteAfter) { if (!chatSessionError.value || pushEvent.nodeName !== chatSessionError.value.node.name) { return; } - if (pushEvent.data.error) { + if (pushEvent.data.error && nodeExecutionStatus.value !== 'error') { await sendEvent('node-execution-errored', pushEvent.data.error); - } else if (pushEvent.data.executionStatus === 'success') { + nodeExecutionStatus.value = 'error'; + telemetry.track('User executed node after assistant suggestion', { + task: 'error', + chat_session_id: currentSessionId.value, + success: false, + }); + } else if ( + pushEvent.data.executionStatus === 'success' && + nodeExecutionStatus.value !== 'success' + ) { await sendEvent('node-execution-succeeded'); + nodeExecutionStatus.value = 'success'; + telemetry.track('User executed node after assistant suggestion', { + task: 'error', + chat_session_id: currentSessionId.value, + success: true, + }); } - telemetry.track('User executed node after assistant suggestion', { - task: 'error', - chat_session_id: currentSessionId.value, - success: pushEvent.data.executionStatus === 'success', - }); } async function sendMessage( @@ -396,10 +408,16 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { const id = getRandomId(); try { addUserMessage(chatMessage.text, id); - addEmptyAssistantMessage(id); + addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking')); streaming.value = true; assert(currentSessionId.value); + if ( + chatMessage.quickReplyType === 'new-suggestion' && + nodeExecutionStatus.value !== 'not_executed' + ) { + nodeExecutionStatus.value = 'not_executed'; + } chatWithAssistant( rootStore.restApiContext, { @@ -415,6 +433,12 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { () => onDoneStreaming(id), (e) => handleServiceError(e, id), ); + telemetry.track('User sent message in Assistant', { + message: chatMessage.text, + is_quick_reply: !!chatMessage.quickReplyType, + chat_session_id: currentSessionId.value, + message_number: chatMessages.value.filter((msg) => msg.role === 'user').length, + }); } catch (e: unknown) { // in case of assert handleServiceError(e, id); @@ -566,5 +590,6 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { resetAssistantChat, chatWindowOpen, addAssistantMessages, + assistantThinkingMessage, }; }); diff --git a/packages/editor-ui/src/types/assistant.types.ts b/packages/editor-ui/src/types/assistant.types.ts index 86f3576afe..6cbfa61ed0 100644 --- a/packages/editor-ui/src/types/assistant.types.ts +++ b/packages/editor-ui/src/types/assistant.types.ts @@ -76,6 +76,7 @@ export namespace ChatRequest { role: 'assistant'; type: 'message'; text: string; + step?: 'n8n_documentation' | 'n8n_forum'; } interface AssistantSummaryMessage { @@ -98,8 +99,21 @@ export namespace ChatRequest { text: string; } + interface AgentThinkingStep { + role: 'assistant'; + type: 'intermediate-step'; + text: string; + step: string; + } + export type MessageResponse = - | ((AssistantChatMessage | CodeDiffMessage | AssistantSummaryMessage | AgentChatMessage) & { + | (( + | AssistantChatMessage + | CodeDiffMessage + | AssistantSummaryMessage + | AgentChatMessage + | AgentThinkingStep + ) & { quickReplies?: QuickReplyOption[]; }) | EndSessionMessage; From 650389d90763a45c037e74a1a1193c3cbe103a16 Mon Sep 17 00:00:00 2001 From: jeanpaul Date: Tue, 3 Sep 2024 11:20:00 +0200 Subject: [PATCH 033/259] feat(Postgres PGVector Store Node): Add PGVector vector store node (#10517) --- .../VectorStorePGVector.node.ts | 276 ++++++++++++++++++ .../VectorStorePGVector/postgres.svg | 1 + packages/@n8n/nodes-langchain/package.json | 1 + 3 files changed, 278 insertions(+) create mode 100644 packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/VectorStorePGVector.node.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/postgres.svg diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/VectorStorePGVector.node.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/VectorStorePGVector.node.ts new file mode 100644 index 0000000000..7e14eb4887 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/VectorStorePGVector.node.ts @@ -0,0 +1,276 @@ +import { type INodeProperties } from 'n8n-workflow'; +import { + PGVectorStore, + type DistanceStrategy, + type PGVectorStoreArgs, +} from '@langchain/community/vectorstores/pgvector'; +import { configurePostgres } from 'n8n-nodes-base/dist/nodes/Postgres/v2/transport'; +import type { PostgresNodeCredentials } from 'n8n-nodes-base/dist/nodes/Postgres/v2/helpers/interfaces'; +import type pg from 'pg'; +import { createVectorStoreNode } from '../shared/createVectorStoreNode'; +import { metadataFilterField } from '../../../utils/sharedFields'; + +type CollectionOptions = { + useCollection?: boolean; + collectionName?: string; + collectionTableName?: string; +}; + +type ColumnOptions = { + idColumnName: string; + vectorColumnName: string; + contentColumnName: string; + metadataColumnName: string; +}; + +const sharedFields: INodeProperties[] = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'string', + default: 'n8n_vectors', + description: + 'The table name to store the vectors in. If table does not exist, it will be created.', + }, +]; + +const collectionField: INodeProperties = { + displayName: 'Collection', + name: 'collection', + type: 'fixedCollection', + description: 'Collection of vectors', + default: { + values: { + useCollection: false, + collectionName: 'n8n', + collectionTable: 'n8n_vector_collections', + }, + }, + typeOptions: {}, + placeholder: 'Add Collection Settings', + options: [ + { + name: 'values', + displayName: 'Collection Settings', + values: [ + { + displayName: 'Use Collection', + name: 'useCollection', + type: 'boolean', + default: false, + }, + { + displayName: 'Collection Name', + name: 'collectionName', + type: 'string', + default: 'n8n', + required: true, + displayOptions: { show: { useCollection: [true] } }, + }, + { + displayName: 'Collection Table Name', + name: 'collectionTableName', + type: 'string', + default: 'n8n_vector_collections', + required: true, + displayOptions: { show: { useCollection: [true] } }, + }, + ], + }, + ], +}; + +const columnNamesField: INodeProperties = { + displayName: 'Column Names', + name: 'columnNames', + type: 'fixedCollection', + description: 'The names of the columns in the PGVector table', + default: { + values: { + idColumnName: 'id', + vectorColumnName: 'embedding', + contentColumnName: 'text', + metadataColumnName: 'metadata', + }, + }, + typeOptions: {}, + placeholder: 'Set Column Names', + options: [ + { + name: 'values', + displayName: 'Column Name Settings', + values: [ + { + displayName: 'ID Column Name', + name: 'idColumnName', + type: 'string', + default: 'id', + required: true, + }, + { + displayName: 'Vector Column Name', + name: 'vectorColumnName', + type: 'string', + default: 'embedding', + required: true, + }, + { + displayName: 'Content Column Name', + name: 'contentColumnName', + type: 'string', + default: 'text', + required: true, + }, + { + displayName: 'Metadata Column Name', + name: 'metadataColumnName', + type: 'string', + default: 'metadata', + required: true, + }, + ], + }, + ], +}; + +const distanceStrategyField: INodeProperties = { + displayName: 'Distance Strategy', + name: 'distanceStrategy', + type: 'options', + default: 'cosine', + description: 'The method to calculate the distance between two vectors', + options: [ + { + name: 'Cosine', + value: 'cosine', + }, + { + name: 'Inner Product', + value: 'innerProduct', + }, + { + name: 'Euclidean', + value: 'euclidean', + }, + ], +}; + +const insertFields: INodeProperties[] = [ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [collectionField, columnNamesField], + }, +]; + +const retrieveFields: INodeProperties[] = [ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [distanceStrategyField, collectionField, columnNamesField, metadataFilterField], + }, +]; + +export const VectorStorePGVector = createVectorStoreNode({ + meta: { + description: 'Work with your data in Postgresql with the PGVector extension', + icon: 'file:postgres.svg', + displayName: 'Postgres PGVector Store', + docsUrl: + 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.vectorstoresupabase/', + name: 'vectorStorePGVector', + credentials: [ + { + name: 'postgres', + required: true, + testedBy: 'postgresConnectionTest', + }, + ], + operationModes: ['load', 'insert', 'retrieve'], + }, + sharedFields, + insertFields, + loadFields: retrieveFields, + retrieveFields, + async getVectorStoreClient(context, filter, embeddings, itemIndex) { + const tableName = context.getNodeParameter('tableName', itemIndex, '', { + extractValue: true, + }) as string; + const credentials = await context.getCredentials('postgres'); + const pgConf = await configurePostgres.call(context, credentials as PostgresNodeCredentials); + const pool = pgConf.db.$pool as unknown as pg.Pool; + + const config: PGVectorStoreArgs = { + pool, + tableName, + filter, + }; + + const collectionOptions = context.getNodeParameter( + 'options.collection.values', + 0, + {}, + ) as CollectionOptions; + + if (collectionOptions && collectionOptions.useCollection) { + config.collectionName = collectionOptions.collectionName; + config.collectionTableName = collectionOptions.collectionTableName; + } + + config.columns = context.getNodeParameter('options.columnNames.values', 0, { + idColumnName: 'id', + vectorColumnName: 'embedding', + contentColumnName: 'text', + metadataColumnName: 'metadata', + }) as ColumnOptions; + + config.distanceStrategy = context.getNodeParameter( + 'options.distanceStrategy', + 0, + 'cosine', + ) as DistanceStrategy; + + return await PGVectorStore.initialize(embeddings, config); + }, + async populateVectorStore(context, embeddings, documents, itemIndex) { + // NOTE: if you are to create the HNSW index before use, you need to consider moving the distanceStrategy field to + // shared fields, because you need that strategy when creating the index. + const tableName = context.getNodeParameter('tableName', itemIndex, '', { + extractValue: true, + }) as string; + const credentials = await context.getCredentials('postgres'); + const pgConf = await configurePostgres.call(context, credentials as PostgresNodeCredentials); + const pool = pgConf.db.$pool as unknown as pg.Pool; + + const config: PGVectorStoreArgs = { + pool, + tableName, + }; + + const collectionOptions = context.getNodeParameter( + 'options.collection.values', + 0, + {}, + ) as CollectionOptions; + + if (collectionOptions && collectionOptions.useCollection) { + config.collectionName = collectionOptions.collectionName; + config.collectionTableName = collectionOptions.collectionTableName; + } + + config.columns = context.getNodeParameter('options.columnNames.values', 0, { + idColumnName: 'id', + vectorColumnName: 'embedding', + contentColumnName: 'text', + metadataColumnName: 'metadata', + }) as ColumnOptions; + + await PGVectorStore.fromDocuments(documents, embeddings, config); + }, +}); diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/postgres.svg b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/postgres.svg new file mode 100644 index 0000000000..da7be289e2 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePGVector/postgres.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 7f64abea65..d26fcb0b46 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -108,6 +108,7 @@ "dist/nodes/vector_store/VectorStoreInMemory/VectorStoreInMemory.node.js", "dist/nodes/vector_store/VectorStoreInMemoryInsert/VectorStoreInMemoryInsert.node.js", "dist/nodes/vector_store/VectorStoreInMemoryLoad/VectorStoreInMemoryLoad.node.js", + "dist/nodes/vector_store/VectorStorePGVector/VectorStorePGVector.node.js", "dist/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.js", "dist/nodes/vector_store/VectorStorePineconeInsert/VectorStorePineconeInsert.node.js", "dist/nodes/vector_store/VectorStorePineconeLoad/VectorStorePineconeLoad.node.js", From b4a391536c4a478bb1fa2d18c7baf4a546164440 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:27:38 +0300 Subject: [PATCH 034/259] ci: Upgrade validate-n8n-pull-request-title to v2.1.0 (#10645) --- .github/workflows/check-pr-title.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml index 761cc5b8c7..931057cac1 100644 --- a/.github/workflows/check-pr-title.yml +++ b/.github/workflows/check-pr-title.yml @@ -28,6 +28,6 @@ jobs: - name: Validate PR title id: validate_pr_title - uses: n8n-io/validate-n8n-pull-request-title@v2.0.1 + uses: n8n-io/validate-n8n-pull-request-title@v2.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f0c61d029a61877d80dafd3c19c7f99a58608371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 3 Sep 2024 11:51:29 +0200 Subject: [PATCH 035/259] refactor(core): Improve license lifecycle logging (no-changelog) (#10643) --- packages/cli/src/license.ts | 9 ++++++++- .../src/services/__tests__/orchestration.service.test.ts | 3 --- .../orchestration/main/handle-command-message-main.ts | 6 +----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/license.ts b/packages/cli/src/license.ts index ee8bfd0fae..21ef6c577f 100644 --- a/packages/cli/src/license.ts +++ b/packages/cli/src/license.ts @@ -109,6 +109,7 @@ export class License { }); await this.manager.initialize(); + this.logger.debug('License initialized'); } catch (e: unknown) { if (e instanceof Error) { this.logger.error('Could not initialize license manager sdk', e); @@ -132,6 +133,8 @@ export class License { } async onFeatureChange(_features: TFeatures): Promise { + this.logger.debug('License feature change detected', _features); + if (config.getEnv('executions.mode') === 'queue' && config.getEnv('multiMainSetup.enabled')) { const isMultiMainLicensed = _features[LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES] as | boolean @@ -198,14 +201,15 @@ export class License { } await this.manager.activate(activationKey); + this.logger.debug('License activated'); } async reload(): Promise { if (!this.manager) { return; } - this.logger.debug('Reloading license'); await this.manager.reload(); + this.logger.debug('License reloaded'); } async renew() { @@ -214,6 +218,7 @@ export class License { } await this.manager.renew(); + this.logger.debug('License renewed'); } @OnShutdown() @@ -227,6 +232,7 @@ export class License { } await this.manager.shutdown(); + this.logger.debug('License shut down'); } isFeatureEnabled(feature: BooleanLicenseFeature) { @@ -392,5 +398,6 @@ export class License { async reinit() { this.manager?.reset(); await this.init('main', true); + this.logger.debug('License reinitialized'); } } diff --git a/packages/cli/src/services/__tests__/orchestration.service.test.ts b/packages/cli/src/services/__tests__/orchestration.service.test.ts index 3aec2a607e..5cb10c6598 100644 --- a/packages/cli/src/services/__tests__/orchestration.service.test.ts +++ b/packages/cli/src/services/__tests__/orchestration.service.test.ts @@ -14,7 +14,6 @@ import { handleCommandMessageMain } from '@/services/orchestration/main/handle-c import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service'; import * as helpers from '@/services/orchestration/helpers'; import { ExternalSecretsManager } from '@/external-secrets/external-secrets-manager.ee'; -import { Logger } from '@/logger'; import { Push } from '@/push'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { mockInstance } from '@test/mocking'; @@ -47,7 +46,6 @@ const workerRestartEventBusResponse: RedisServiceWorkerResponseObject = { }; describe('Orchestration Service', () => { - const logger = mockInstance(Logger); mockInstance(Push); mockInstance(RedisService); mockInstance(ExternalSecretsManager); @@ -112,7 +110,6 @@ describe('Orchestration Service', () => { expect(responseFalseId).toBeDefined(); expect(responseFalseId!.command).toEqual('reloadLicense'); expect(responseFalseId!.senderId).toEqual('test'); - expect(logger.error).toHaveBeenCalled(); }); test('should reject command messages from itself', async () => { diff --git a/packages/cli/src/services/orchestration/main/handle-command-message-main.ts b/packages/cli/src/services/orchestration/main/handle-command-message-main.ts index 9ea171b062..18930aa0c8 100644 --- a/packages/cli/src/services/orchestration/main/handle-command-message-main.ts +++ b/packages/cli/src/services/orchestration/main/handle-command-message-main.ts @@ -53,11 +53,7 @@ export async function handleCommandMessageMain(messageString: string) { } if (isMainInstance && !config.getEnv('multiMainSetup.enabled')) { - // at this point in time, only a single main instance is supported, thus this command _should_ never be caught currently - logger.error( - 'Received command to reload license via Redis, but this should not have happened and is not supported on the main instance yet.', - ); - return message; + return message; // this main is the sender, so disregard } await Container.get(License).reload(); break; From e5aba60afff93364d91f17c00ea18d38d9dbc970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 3 Sep 2024 12:31:30 +0200 Subject: [PATCH 036/259] fix(core): Tighten check for company size survey answer (#10646) --- .../__tests__/me.controller.test.ts | 20 +++++++++++++++++++ .../cli/src/controllers/survey-answers.dto.ts | 2 +- packages/cli/test/integration/me.api.test.ts | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/controllers/__tests__/me.controller.test.ts b/packages/cli/src/controllers/__tests__/me.controller.test.ts index 391c50599c..74bd4da987 100644 --- a/packages/cli/src/controllers/__tests__/me.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/me.controller.test.ts @@ -352,6 +352,26 @@ describe('MeController', () => { ); }); + it('should not flag XSS attempt for `<` sign in company size', async () => { + const req = mock(); + req.body = { + version: 'v4', + personalization_survey_submitted_at: '2024-08-06T12:19:51.268Z', + personalization_survey_n8n_version: '1.0.0', + companySize: '<20', + otherCompanyIndustryExtended: ['test'], + automationGoalSm: ['test'], + usageModes: ['test'], + email: 'test@email.com', + role: 'test', + roleOther: 'test', + reportedSource: 'test', + reportedSourceOther: 'test', + }; + + await expect(controller.storeSurveyAnswers(req)).resolves.toEqual({ success: true }); + }); + test.each([ 'automationGoalDevops', 'companyIndustryExtended', diff --git a/packages/cli/src/controllers/survey-answers.dto.ts b/packages/cli/src/controllers/survey-answers.dto.ts index f115a6992b..25d9bfacfc 100644 --- a/packages/cli/src/controllers/survey-answers.dto.ts +++ b/packages/cli/src/controllers/survey-answers.dto.ts @@ -45,7 +45,7 @@ export class PersonalizationSurveyAnswersV4 implements IPersonalizationSurveyAns @IsString({ each: true }) otherCompanyIndustryExtended?: string[] | null; - @NoXss() + @IsEnum(['<20', '20-99', '100-499', '500-999', '1000+', 'personalUser']) @Expose() @IsOptional() @IsString() diff --git a/packages/cli/test/integration/me.api.test.ts b/packages/cli/test/integration/me.api.test.ts index 829b296fe6..72ecb09b32 100644 --- a/packages/cli/test/integration/me.api.test.ts +++ b/packages/cli/test/integration/me.api.test.ts @@ -400,7 +400,7 @@ const SURVEY: IPersonalizationSurveyAnswersV4 = { automationGoalDevopsOther: 'test', companyIndustryExtended: ['test'], otherCompanyIndustryExtended: ['test'], - companySize: 'test', + companySize: '20-99', companyType: 'test', automationGoalSm: ['test'], automationGoalSmOther: 'test', From 32ce65c1af11b8e6ba4f3da51b48296221bae8c3 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Tue, 3 Sep 2024 15:11:44 +0300 Subject: [PATCH 037/259] feat(editor): Overhaul node insert position computation in new canvas (no-changelog) (#10637) --- packages/editor-ui/src/Interface.ts | 1 + packages/editor-ui/src/__tests__/mocks.ts | 6 +- .../src/components/Node/NodeCreation.vue | 10 +- .../Node/NodeCreator/NodeCreator.vue | 4 +- .../src/components/canvas/Canvas.vue | 25 +- .../nodes/render-types/CanvasNodeDefault.vue | 4 +- .../useCanvasOperations.spec.ts.snap | 8 +- .../__tests__/useCanvasOperations.spec.ts | 897 +++++++++++------- .../src/composables/useCanvasOperations.ts | 283 +++--- .../editor-ui/src/stores/credentials.store.ts | 1 + .../editor-ui/src/stores/nodeCreator.store.ts | 11 +- packages/editor-ui/src/stores/ui.store.ts | 3 + packages/editor-ui/src/utils/canvasUtilsV2.ts | 3 +- packages/editor-ui/src/utils/nodeViewUtils.ts | 3 + packages/editor-ui/src/views/NodeView.v2.vue | 29 +- 15 files changed, 805 insertions(+), 483 deletions(-) diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index e49b0491b1..f7ad20f9ea 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1805,6 +1805,7 @@ export type ToggleNodeCreatorOptions = { createNodeActive: boolean; source?: NodeCreatorOpenSource; nodeCreatorView?: NodeFilterType; + hasAddedNodes?: boolean; }; export type AppliedThemeOption = 'light' | 'dark'; diff --git a/packages/editor-ui/src/__tests__/mocks.ts b/packages/editor-ui/src/__tests__/mocks.ts index cb15a9bf68..993d5870a5 100644 --- a/packages/editor-ui/src/__tests__/mocks.ts +++ b/packages/editor-ui/src/__tests__/mocks.ts @@ -49,18 +49,18 @@ export const mockNode = ({ }) => mock({ id, name, type, position, disabled, issues, typeVersion, parameters }); export const mockNodeTypeDescription = ({ - name, + name = SET_NODE_TYPE, version = 1, credentials = [], inputs = [NodeConnectionType.Main], outputs = [NodeConnectionType.Main], }: { - name: INodeTypeDescription['name']; + name?: INodeTypeDescription['name']; version?: INodeTypeDescription['version']; credentials?: INodeTypeDescription['credentials']; inputs?: INodeTypeDescription['inputs']; outputs?: INodeTypeDescription['outputs']; -}) => +} = {}) => mock({ name, displayName: name, diff --git a/packages/editor-ui/src/components/Node/NodeCreation.vue b/packages/editor-ui/src/components/Node/NodeCreation.vue index dd6f876d30..38c89d135a 100644 --- a/packages/editor-ui/src/components/Node/NodeCreation.vue +++ b/packages/editor-ui/src/components/Node/NodeCreation.vue @@ -23,7 +23,7 @@ const LazyNodeCreator = defineAsyncComponent( ); const props = withDefaults(defineProps(), { - createNodeActive: false, + createNodeActive: false, // Determines if the node creator is open }); const emit = defineEmits<{ @@ -88,13 +88,15 @@ function addStickyNote() { emit('addNodes', getAddedNodesAndConnections([{ type: STICKY_NODE_TYPE, position }])); } -function closeNodeCreator() { - emit('toggleNodeCreator', { createNodeActive: false }); +function closeNodeCreator(hasAddedNodes = false) { + if (props.createNodeActive) { + emit('toggleNodeCreator', { createNodeActive: false, hasAddedNodes }); + } } function nodeTypeSelected(nodeTypes: string[]) { emit('addNodes', getAddedNodesAndConnections(nodeTypes.map((type) => ({ type })))); - closeNodeCreator(); + closeNodeCreator(true); } diff --git a/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue b/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue index b446ade75e..d93e83ebfa 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue @@ -101,8 +101,8 @@ watch( ); // Close node creator when the last view stacks is closed -watch(viewStacksLength, (viewStacksLength) => { - if (viewStacksLength === 0) { +watch(viewStacksLength, (value) => { + if (value === 0) { emit('closeNodeCreator'); setShowScrim(false); } diff --git a/packages/editor-ui/src/components/canvas/Canvas.vue b/packages/editor-ui/src/components/canvas/Canvas.vue index b456ec0df1..841028fa05 100644 --- a/packages/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/editor-ui/src/components/canvas/Canvas.vue @@ -57,7 +57,11 @@ const emit = defineEmits<{ 'create:connection:start': [handle: ConnectStartEvent]; 'create:connection': [connection: Connection]; 'create:connection:end': [connection: Connection, event?: MouseEvent]; - 'create:connection:cancelled': [handle: ConnectStartEvent, event?: MouseEvent]; + 'create:connection:cancelled': [ + handle: ConnectStartEvent, + position: XYPosition, + event?: MouseEvent, + ]; 'click:connection:add': [connection: Connection]; 'click:pane': [position: XYPosition]; 'run:workflow': []; @@ -227,7 +231,7 @@ function onConnectEnd(event?: MouseEvent) { if (connectedHandle.value) { emit('create:connection:end', connectedHandle.value, event); } else if (connectingHandle.value) { - emit('create:connection:cancelled', connectingHandle.value, event); + emit('create:connection:cancelled', connectingHandle.value, getProjectedPosition(event), event); } connectedHandle.value = undefined; @@ -291,14 +295,19 @@ function emitWithLastSelectedNode(emitFn: (id: string) => void) { const defaultZoom = 1; const zoom = ref(defaultZoom); -function onClickPane(event: MouseEvent) { +function getProjectedPosition(event?: MouseEvent) { const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }; - const position = project({ - x: event.offsetX - bounds.left, - y: event.offsetY - bounds.top, - }); + const offsetX = event?.clientX ?? 0; + const offsetY = event?.clientY ?? 0; - emit('click:pane', position); + return project({ + x: offsetX - bounds.left, + y: offsetY - bounds.top, + }); +} + +function onClickPane(event: MouseEvent) { + emit('click:pane', getProjectedPosition(event)); } async function onFitView() { diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue index 2f50454baa..2bbb2421d1 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue @@ -145,8 +145,8 @@ function openContextMenu(event: MouseEvent) { */ &.configuration { - --canvas-node--width: 76px; - --canvas-node--height: 76px; + --canvas-node--width: 80px; + --canvas-node--height: 80px; background: var(--canvas-node--background, var(--node-type-supplemental-background)); border: var(--canvas-node-border-width) solid diff --git a/packages/editor-ui/src/composables/__tests__/__snapshots__/useCanvasOperations.spec.ts.snap b/packages/editor-ui/src/composables/__tests__/__snapshots__/useCanvasOperations.spec.ts.snap index 875db00093..079a5d2f51 100644 --- a/packages/editor-ui/src/composables/__tests__/__snapshots__/useCanvasOperations.spec.ts.snap +++ b/packages/editor-ui/src/composables/__tests__/__snapshots__/useCanvasOperations.spec.ts.snap @@ -9,7 +9,7 @@ exports[`useCanvasOperations > copyNodes > should copy nodes 1`] = ` "parameters": {}, "id": "1", "name": "Node 1", - "type": "type", + "type": "n8n-nodes-base.set", "position": [ 40, 40 @@ -20,7 +20,7 @@ exports[`useCanvasOperations > copyNodes > should copy nodes 1`] = ` "parameters": {}, "id": "2", "name": "Node 2", - "type": "type", + "type": "n8n-nodes-base.set", "position": [ 40, 40 @@ -44,7 +44,7 @@ exports[`useCanvasOperations > cutNodes > should copy and delete nodes 1`] = ` "parameters": {}, "id": "1", "name": "Node 1", - "type": "type", + "type": "n8n-nodes-base.set", "position": [ 40, 40 @@ -55,7 +55,7 @@ exports[`useCanvasOperations > cutNodes > should copy and delete nodes 1`] = ` "parameters": {}, "id": "2", "name": "Node 2", - "type": "type", + "type": "n8n-nodes-base.set", "position": [ 40, 40 diff --git a/packages/editor-ui/src/composables/__tests__/useCanvasOperations.spec.ts b/packages/editor-ui/src/composables/__tests__/useCanvasOperations.spec.ts index 7e6c9a0921..0a6b75d1ef 100644 --- a/packages/editor-ui/src/composables/__tests__/useCanvasOperations.spec.ts +++ b/packages/editor-ui/src/composables/__tests__/useCanvasOperations.spec.ts @@ -1,7 +1,6 @@ -import { createPinia, setActivePinia } from 'pinia'; -import type { Connection } from '@vue-flow/core'; +import { setActivePinia } from 'pinia'; import type { IConnection, Workflow } from 'n8n-workflow'; -import { NodeConnectionType } from 'n8n-workflow'; +import { NodeConnectionType, NodeHelpers } from 'n8n-workflow'; import { useCanvasOperations } from '@/composables/useCanvasOperations'; import type { CanvasNode } from '@/types'; import type { ICredentialsResponse, INodeUi, IWorkflowDb } from '@/Interface'; @@ -20,10 +19,12 @@ import { useRouter } from 'vue-router'; import { mock } from 'vitest-mock-extended'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useCredentialsStore } from '@/stores/credentials.store'; -import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; -import { telemetry } from '@/plugins/telemetry'; -import { useClipboard } from '@/composables/useClipboard'; import { waitFor } from '@testing-library/vue'; +import { createTestingPinia } from '@pinia/testing'; +import { mockedStore } from '@/__tests__/utils'; +import { SET_NODE_TYPE, STORES } from '@/constants'; +import type { Connection } from '@vue-flow/core'; +import { useClipboard } from '@/composables/useClipboard'; vi.mock('vue-router', async (importOriginal) => { const actual = await importOriginal<{}>(); @@ -33,78 +34,92 @@ vi.mock('vue-router', async (importOriginal) => { }; }); +vi.mock('n8n-workflow', async (importOriginal) => { + const actual = await importOriginal<{}>(); + return { + ...actual, + TelemetryHelpers: { + generateNodesGraph: vi.fn().mockReturnValue({ + nodeGraph: { + nodes: [], + }, + }), + }, + }; +}); + vi.mock('@/composables/useClipboard', async () => { const copySpy = vi.fn(); return { useClipboard: vi.fn(() => ({ copy: copySpy })) }; }); -describe('useCanvasOperations', () => { - let workflowsStore: ReturnType; - let uiStore: ReturnType; - let ndvStore: ReturnType; - let historyStore: ReturnType; - let nodeTypesStore: ReturnType; - let credentialsStore: ReturnType; - let canvasOperations: ReturnType; - let workflowHelpers: ReturnType; +vi.mock('@/composables/useTelemetry', () => ({ + useTelemetry: () => ({ track: vi.fn() }), +})); +describe('useCanvasOperations', () => { const router = useRouter(); + const workflowId = 'test'; + const initialState = { + [STORES.NODE_TYPES]: {}, + [STORES.NDV]: {}, + [STORES.WORKFLOWS]: { + workflowId, + workflow: mock({ + id: workflowId, + nodes: [], + connections: {}, + tags: [], + usedCredentials: [], + }), + }, + [STORES.SETTINGS]: { + settings: { + enterprise: {}, + }, + }, + }; + beforeEach(async () => { - const pinia = createPinia(); + const pinia = createTestingPinia({ initialState }); setActivePinia(pinia); - - workflowsStore = useWorkflowsStore(); - uiStore = useUIStore(); - ndvStore = useNDVStore(); - historyStore = useHistoryStore(); - nodeTypesStore = useNodeTypesStore(); - credentialsStore = useCredentialsStore(); - workflowHelpers = useWorkflowHelpers({ router }); - - const workflowId = 'test'; - const workflow = mock({ - id: workflowId, - nodes: [], - connections: {}, - tags: [], - usedCredentials: [], - }); - - workflowsStore.resetWorkflow(); - workflowsStore.resetState(); - workflowHelpers.initState(workflow); - - canvasOperations = useCanvasOperations({ router }); vi.clearAllMocks(); }); describe('requireNodeTypeDescription', () => { it('should return node type description when type and version match', () => { + const nodeTypesStore = useNodeTypesStore(); const type = 'testType'; const version = 1; const expectedDescription = mockNodeTypeDescription({ name: type, version }); - nodeTypesStore.setNodeTypes([expectedDescription]); - const result = canvasOperations.requireNodeTypeDescription(type, version); + nodeTypesStore.nodeTypes = { [type]: { [version]: expectedDescription } }; + + const { requireNodeTypeDescription } = useCanvasOperations({ router }); + const result = requireNodeTypeDescription(type, version); expect(result).toBe(expectedDescription); }); it('should throw an error when node type does not exist', () => { const type = 'nonexistentType'; + const { requireNodeTypeDescription } = useCanvasOperations({ router }); expect(() => { - canvasOperations.requireNodeTypeDescription(type); + requireNodeTypeDescription(type); }).toThrow(); }); it('should return node type description when only type is provided and it exists', () => { + const nodeTypesStore = useNodeTypesStore(); const type = 'testTypeWithoutVersion'; const expectedDescription = mockNodeTypeDescription({ name: type }); - nodeTypesStore.setNodeTypes([expectedDescription]); - const result = canvasOperations.requireNodeTypeDescription(type); + nodeTypesStore.nodeTypes = { [type]: { 2: expectedDescription } }; + + const { requireNodeTypeDescription } = useCanvasOperations({ router }); + const result = requireNodeTypeDescription(type); expect(result).toBe(expectedDescription); }); @@ -112,7 +127,8 @@ describe('useCanvasOperations', () => { describe('addNode', () => { it('should create node with default version when version is undefined', () => { - const result = canvasOperations.addNode( + const { addNode } = useCanvasOperations({ router }); + const result = addNode( { name: 'example', type: 'type', @@ -125,7 +141,8 @@ describe('useCanvasOperations', () => { }); it('should create node with default position when position is not provided', () => { - const result = canvasOperations.addNode( + const { addNode } = useCanvasOperations({ router }); + const result = addNode( { type: 'type', typeVersion: 1, @@ -137,7 +154,8 @@ describe('useCanvasOperations', () => { }); it('should create node with provided position when position is provided', () => { - const result = canvasOperations.addNode( + const { addNode } = useCanvasOperations({ router }); + const result = addNode( { type: 'type', typeVersion: 1, @@ -150,6 +168,7 @@ describe('useCanvasOperations', () => { }); it('should create node with default credentials when only one credential is available', () => { + const credentialsStore = useCredentialsStore(); const credential = mock({ id: '1', name: 'cred', type: 'cred' }); const nodeTypeName = 'type'; const nodeTypeDescription = mockNodeTypeDescription({ @@ -157,14 +176,17 @@ describe('useCanvasOperations', () => { credentials: [{ name: credential.name }], }); - credentialsStore.addCredentials([credential]); + credentialsStore.state.credentials = { + [credential.id]: credential, + }; // @ts-expect-error Known pinia issue when spying on store getters vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => [ credential, ]); - const result = canvasOperations.addNode( + const { addNode } = useCanvasOperations({ router }); + const result = addNode( { type: nodeTypeName, typeVersion: 1, @@ -176,6 +198,7 @@ describe('useCanvasOperations', () => { }); it('should not assign credentials when multiple credentials are available', () => { + const credentialsStore = useCredentialsStore(); const credentialA = mock({ id: '1', name: 'credA', type: 'cred' }); const credentialB = mock({ id: '1', name: 'credB', type: 'cred' }); const nodeTypeName = 'type'; @@ -190,7 +213,8 @@ describe('useCanvasOperations', () => { credentialB, ]); - const result = canvasOperations.addNode( + const { addNode } = useCanvasOperations({ router }); + const result = addNode( { type: 'type', typeVersion: 1, @@ -201,9 +225,11 @@ describe('useCanvasOperations', () => { }); it('should open NDV when specified', async () => { + const ndvStore = useNDVStore(); const nodeTypeDescription = mockNodeTypeDescription({ name: 'type' }); - canvasOperations.addNode( + const { addNode } = useCanvasOperations({ router }); + addNode( { type: 'type', typeVersion: 1, @@ -213,12 +239,123 @@ describe('useCanvasOperations', () => { { openNDV: true }, ); - await waitFor(() => expect(ndvStore.activeNodeName).toBe('Test Name')); + await waitFor(() => expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith('Test Name')); + }); + }); + + describe('resolveNodePosition', () => { + it('should return the node position if it is already set', () => { + const node = createTestNode({ position: [100, 100] }); + const nodeTypeDescription = mockNodeTypeDescription(); + + const { resolveNodePosition } = useCanvasOperations({ router }); + const position = resolveNodePosition(node, nodeTypeDescription); + + expect(position).toEqual([100, 100]); + }); + + it('should place the node at the last cancelled connection position', () => { + const uiStore = mockedStore(useUIStore); + const node = createTestNode({ id: '0' }); + const nodeTypeDescription = mockNodeTypeDescription(); + + vi.spyOn(uiStore, 'lastInteractedWithNode', 'get').mockReturnValue(node); + + uiStore.lastInteractedWithNodeHandle = 'inputs/main/0'; + uiStore.lastCancelledConnectionPosition = [200, 200]; + + const { resolveNodePosition } = useCanvasOperations({ router }); + const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription); + + expect(position).toEqual([200, 160]); + expect(uiStore.lastCancelledConnectionPosition).toBeNull(); + }); + + it('should place the node to the right of the last interacted with node', () => { + const uiStore = mockedStore(useUIStore); + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + + const node = createTestNode({ id: '0' }); + const nodeTypeDescription = mockNodeTypeDescription(); + + uiStore.lastInteractedWithNode = createTestNode({ + position: [100, 100], + type: 'test', + typeVersion: 1, + }); + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); + workflowsStore.getCurrentWorkflow.mockReturnValue( + createTestWorkflowObject(workflowsStore.workflow), + ); + + const { resolveNodePosition } = useCanvasOperations({ router }); + const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription); + + expect(position).toEqual([320, 100]); + }); + + it('should place the node below the last interacted with node if it has non-main outputs', () => { + const uiStore = mockedStore(useUIStore); + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + + const node = createTestNode({ id: '0' }); + const nodeTypeDescription = mockNodeTypeDescription(); + + uiStore.lastInteractedWithNode = createTestNode({ + position: [100, 100], + type: 'test', + typeVersion: 1, + }); + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); + workflowsStore.getCurrentWorkflow.mockReturnValue( + createTestWorkflowObject(workflowsStore.workflow), + ); + + vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValue([ + { type: NodeConnectionType.AiTool }, + ]); + vi.spyOn(NodeHelpers, 'getConnectionTypes').mockReturnValue([NodeConnectionType.AiTool]); + + const { resolveNodePosition } = useCanvasOperations({ router }); + const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription); + + expect(position).toEqual([460, 100]); + }); + + it('should place the node at the last clicked position if no other position is set', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + + const node = createTestNode({ id: '0' }); + const nodeTypeDescription = mockNodeTypeDescription(); + + workflowsStore.workflowTriggerNodes = [ + createTestNode({ id: 'trigger', position: [100, 100] }), + ]; + + const { resolveNodePosition, lastClickPosition } = useCanvasOperations({ router }); + lastClickPosition.value = [300, 300]; + + const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription); + + expect(position).toEqual([300, 300]); + }); + + it('should place the trigger node at the root if it is the first trigger node', () => { + const node = createTestNode({ id: '0' }); + const nodeTypeDescription = mockNodeTypeDescription(); + + const { resolveNodePosition } = useCanvasOperations({ router }); + const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription); + + expect(position).toEqual([0, 0]); }); }); describe('updateNodesPosition', () => { it('records history for multiple node position updates when tracking is enabled', () => { + const historyStore = useHistoryStore(); const events = [ { id: 'node1', position: { x: 100, y: 100 } }, { id: 'node2', position: { x: 200, y: 200 } }, @@ -226,19 +363,21 @@ describe('useCanvasOperations', () => { const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo'); const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo'); - canvasOperations.updateNodesPosition(events, { trackHistory: true, trackBulk: true }); + const { updateNodesPosition } = useCanvasOperations({ router }); + updateNodesPosition(events, { trackHistory: true, trackBulk: true }); expect(startRecordingUndoSpy).toHaveBeenCalled(); expect(stopRecordingUndoSpy).toHaveBeenCalled(); }); it('updates positions for multiple nodes', () => { + const workflowsStore = mockedStore(useWorkflowsStore); const events = [ { id: 'node1', position: { x: 100, y: 100 } }, { id: 'node2', position: { x: 200, y: 200 } }, ]; const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById'); - vi.spyOn(workflowsStore, 'getNodeById') + workflowsStore.getNodeById .mockReturnValueOnce( createTestNode({ id: events[0].id, @@ -252,7 +391,8 @@ describe('useCanvasOperations', () => { }), ); - canvasOperations.updateNodesPosition(events); + const { updateNodesPosition } = useCanvasOperations({ router }); + updateNodesPosition(events); expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2); expect(setNodePositionByIdSpy).toHaveBeenCalledWith('node1', [100, 100]); @@ -260,11 +400,13 @@ describe('useCanvasOperations', () => { }); it('does not record history when trackHistory is false', () => { + const historyStore = useHistoryStore(); const events = [{ id: 'node1', position: { x: 100, y: 100 } }]; const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo'); const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo'); - canvasOperations.updateNodesPosition(events, { trackHistory: false, trackBulk: false }); + const { updateNodesPosition } = useCanvasOperations({ router }); + updateNodesPosition(events, { trackHistory: false, trackBulk: false }); expect(startRecordingUndoSpy).not.toHaveBeenCalled(); expect(stopRecordingUndoSpy).not.toHaveBeenCalled(); @@ -273,9 +415,7 @@ describe('useCanvasOperations', () => { describe('updateNodePosition', () => { it('should update node position', () => { - const setNodePositionByIdSpy = vi - .spyOn(workflowsStore, 'setNodePositionById') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); const id = 'node1'; const position: CanvasNode['position'] = { x: 10, y: 20 }; const node = createTestNode({ @@ -285,40 +425,49 @@ describe('useCanvasOperations', () => { name: 'Node 1', }); - vi.spyOn(workflowsStore, 'getNodeById').mockReturnValueOnce(node); + workflowsStore.getNodeById.mockReturnValueOnce(node); - canvasOperations.updateNodePosition(id, position); + const { updateNodePosition } = useCanvasOperations({ router }); + updateNodePosition(id, position); - expect(setNodePositionByIdSpy).toHaveBeenCalledWith(id, [position.x, position.y]); + expect(workflowsStore.setNodePositionById).toHaveBeenCalledWith(id, [position.x, position.y]); }); }); describe('setNodeSelected', () => { it('should set last selected node when node id is provided and node exists', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const uiStore = useUIStore(); const nodeId = 'node1'; const nodeName = 'Node 1'; workflowsStore.getNodeById = vi.fn().mockReturnValue({ name: nodeName }); uiStore.lastSelectedNode = ''; - canvasOperations.setNodeSelected(nodeId); + const { setNodeSelected } = useCanvasOperations({ router }); + setNodeSelected(nodeId); expect(uiStore.lastSelectedNode).toBe(nodeName); }); it('should not change last selected node when node id is provided but node does not exist', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const uiStore = useUIStore(); const nodeId = 'node1'; workflowsStore.getNodeById = vi.fn().mockReturnValue(undefined); uiStore.lastSelectedNode = 'Existing Node'; - canvasOperations.setNodeSelected(nodeId); + const { setNodeSelected } = useCanvasOperations({ router }); + setNodeSelected(nodeId); expect(uiStore.lastSelectedNode).toBe('Existing Node'); }); it('should clear last selected node when node id is not provided', () => { + const uiStore = useUIStore(); uiStore.lastSelectedNode = 'Existing Node'; - canvasOperations.setNodeSelected(); + const { setNodeSelected } = useCanvasOperations({ router }); + setNodeSelected(); expect(uiStore.lastSelectedNode).toBe(''); }); @@ -326,54 +475,74 @@ describe('useCanvasOperations', () => { describe('addNodes', () => { it('should add nodes at specified positions', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = useNodeTypesStore(); const nodeTypeName = 'type'; const nodes = [ mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] }), mockNode({ name: 'Node 2', type: nodeTypeName, position: [100, 240] }), ]; - nodeTypesStore.setNodeTypes([ - mockNodeTypeDescription({ - name: nodeTypeName, - }), - ]); + workflowsStore.getCurrentWorkflow.mockReturnValue( + createTestWorkflowObject(workflowsStore.workflow), + ); - await canvasOperations.addNodes(nodes, {}); + nodeTypesStore.nodeTypes = { + [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, + }; - expect(workflowsStore.workflow.nodes).toHaveLength(2); - expect(workflowsStore.workflow.nodes[0]).toHaveProperty('name', nodes[0].name); - expect(workflowsStore.workflow.nodes[0]).toHaveProperty('parameters', {}); - expect(workflowsStore.workflow.nodes[0]).toHaveProperty('type', nodeTypeName); - expect(workflowsStore.workflow.nodes[0]).toHaveProperty('typeVersion', 1); - expect(workflowsStore.workflow.nodes[0]).toHaveProperty('position'); + const { addNodes } = useCanvasOperations({ router }); + await addNodes(nodes, {}); + + expect(workflowsStore.addNode).toHaveBeenCalledTimes(2); + expect(workflowsStore.addNode.mock.calls[0][0]).toMatchObject({ + name: nodes[0].name, + type: nodeTypeName, + typeVersion: 1, + position: [40, 40], + parameters: {}, + }); + expect(workflowsStore.addNode.mock.calls[1][0]).toMatchObject({ + name: nodes[1].name, + type: nodeTypeName, + typeVersion: 1, + position: [100, 240], + parameters: {}, + }); }); it('should add nodes at current position when position is not specified', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); const nodeTypeName = 'type'; const nodes = [ mockNode({ name: 'Node 1', type: nodeTypeName, position: [120, 120] }), mockNode({ name: 'Node 2', type: nodeTypeName, position: [180, 320] }), ]; - const workflowStoreAddNodeSpy = vi.spyOn(workflowsStore, 'addNode'); - nodeTypesStore.setNodeTypes([ - mockNodeTypeDescription({ - name: nodeTypeName, - }), - ]); + workflowsStore.getCurrentWorkflow.mockReturnValue( + createTestWorkflowObject(workflowsStore.workflow), + ); - await canvasOperations.addNodes(nodes, { position: [50, 60] }); + nodeTypesStore.nodeTypes = { + [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, + }; - expect(workflowStoreAddNodeSpy).toHaveBeenCalledTimes(2); - expect(workflowStoreAddNodeSpy.mock.calls[0][0].position).toEqual( + const { addNodes } = useCanvasOperations({ router }); + await addNodes(nodes, { position: [50, 60] }); + + expect(workflowsStore.addNode).toHaveBeenCalledTimes(2); + expect(workflowsStore.addNode.mock.calls[0][0].position).toEqual( expect.arrayContaining(nodes[0].position), ); - expect(workflowStoreAddNodeSpy.mock.calls[1][0].position).toEqual( + expect(workflowsStore.addNode.mock.calls[1][0].position).toEqual( expect.arrayContaining(nodes[1].position), ); }); it('should adjust the position of nodes with multiple inputs', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = useNodeTypesStore(); const nodeTypeName = 'type'; const nodes = [ mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [40, 40] }), @@ -382,18 +551,12 @@ describe('useCanvasOperations', () => { ]; const setNodePositionByIdSpy = vi.spyOn(workflowsStore, 'setNodePositionById'); - vi.spyOn(workflowsStore, 'getNodeByName') - .mockReturnValueOnce(nodes[1]) - .mockReturnValueOnce(nodes[2]); - vi.spyOn(workflowsStore, 'getNodeById') - .mockReturnValueOnce(nodes[1]) - .mockReturnValueOnce(nodes[2]); + workflowsStore.getNodeByName.mockReturnValueOnce(nodes[1]).mockReturnValueOnce(nodes[2]); + workflowsStore.getNodeById.mockReturnValueOnce(nodes[1]).mockReturnValueOnce(nodes[2]); - nodeTypesStore.setNodeTypes([ - mockNodeTypeDescription({ - name: nodeTypeName, - }), - ]); + nodeTypesStore.nodeTypes = { + [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, + }; vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockImplementation(() => mock({ @@ -406,7 +569,8 @@ describe('useCanvasOperations', () => { }), ); - await canvasOperations.addNodes(nodes, {}); + const { addNodes } = useCanvasOperations({ router }); + await addNodes(nodes, {}); expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2); expect(setNodePositionByIdSpy).toHaveBeenCalledWith(nodes[1].id, expect.any(Object)); @@ -416,12 +580,14 @@ describe('useCanvasOperations', () => { describe('revertAddNode', () => { it('deletes node if it exists', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); const node = createTestNode(); - vi.spyOn(workflowsStore, 'getNodeByName').mockReturnValueOnce(node); - vi.spyOn(workflowsStore, 'getNodeById').mockReturnValueOnce(node); + workflowsStore.getNodeByName.mockReturnValueOnce(node); + workflowsStore.getNodeById.mockReturnValueOnce(node); const removeNodeByIdSpy = vi.spyOn(workflowsStore, 'removeNodeById'); - await canvasOperations.revertAddNode(node.name); + const { revertAddNode } = useCanvasOperations({ router }); + await revertAddNode(node.name); expect(removeNodeByIdSpy).toHaveBeenCalledWith(node.id); }); @@ -429,18 +595,12 @@ describe('useCanvasOperations', () => { describe('deleteNode', () => { it('should delete node and track history', () => { - const removeNodeByIdSpy = vi - .spyOn(workflowsStore, 'removeNodeById') - .mockImplementation(() => {}); - const removeNodeConnectionsByIdSpy = vi - .spyOn(workflowsStore, 'removeNodeConnectionsById') - .mockImplementation(() => {}); - const removeNodeExecutionDataByIdSpy = vi - .spyOn(workflowsStore, 'removeNodeExecutionDataById') - .mockImplementation(() => {}); - const pushCommandToUndoSpy = vi - .spyOn(historyStore, 'pushCommandToUndo') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); + const historyStore = mockedStore(useHistoryStore); + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); + workflowsStore.incomingConnectionsByNodeName.mockReturnValue({}); const id = 'node1'; const node: INodeUi = createTestNode({ @@ -450,29 +610,24 @@ describe('useCanvasOperations', () => { name: 'Node 1', }); - vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(node); + workflowsStore.getNodeById.mockReturnValue(node); - canvasOperations.deleteNode(id, { trackHistory: true }); + const { deleteNode } = useCanvasOperations({ router }); + deleteNode(id, { trackHistory: true }); - expect(removeNodeByIdSpy).toHaveBeenCalledWith(id); - expect(removeNodeConnectionsByIdSpy).toHaveBeenCalledWith(id); - expect(removeNodeExecutionDataByIdSpy).toHaveBeenCalledWith(id); - expect(pushCommandToUndoSpy).toHaveBeenCalledWith(new RemoveNodeCommand(node)); + expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(id); + expect(workflowsStore.removeNodeConnectionsById).toHaveBeenCalledWith(id); + expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(id); + expect(historyStore.pushCommandToUndo).toHaveBeenCalledWith(new RemoveNodeCommand(node)); }); it('should delete node without tracking history', () => { - const removeNodeByIdSpy = vi - .spyOn(workflowsStore, 'removeNodeById') - .mockImplementation(() => {}); - const removeNodeConnectionsByIdSpy = vi - .spyOn(workflowsStore, 'removeNodeConnectionsById') - .mockImplementation(() => {}); - const removeNodeExecutionDataByIdSpy = vi - .spyOn(workflowsStore, 'removeNodeExecutionDataById') - .mockImplementation(() => {}); - const pushCommandToUndoSpy = vi - .spyOn(historyStore, 'pushCommandToUndo') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); + const historyStore = mockedStore(useHistoryStore); + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); + workflowsStore.incomingConnectionsByNodeName.mockReturnValue({}); const id = 'node1'; const node = createTestNode({ @@ -483,84 +638,91 @@ describe('useCanvasOperations', () => { parameters: {}, }); - vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(node); + workflowsStore.getNodeById.mockReturnValue(node); - canvasOperations.deleteNode(id, { trackHistory: false }); + const { deleteNode } = useCanvasOperations({ router }); + deleteNode(id, { trackHistory: false }); - expect(removeNodeByIdSpy).toHaveBeenCalledWith(id); - expect(removeNodeConnectionsByIdSpy).toHaveBeenCalledWith(id); - expect(removeNodeExecutionDataByIdSpy).toHaveBeenCalledWith(id); - expect(pushCommandToUndoSpy).not.toHaveBeenCalled(); + expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(id); + expect(workflowsStore.removeNodeConnectionsById).toHaveBeenCalledWith(id); + expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(id); + expect(historyStore.pushCommandToUndo).not.toHaveBeenCalled(); }); it('should connect adjacent nodes when deleting a node surrounded by other nodes', () => { - nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'node' })]); + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + + nodeTypesStore.nodeTypes = { + [SET_NODE_TYPE]: { 1: mockNodeTypeDescription({ name: SET_NODE_TYPE }) }, + }; + const nodes = [ createTestNode({ id: 'input', - type: 'node', + type: SET_NODE_TYPE, position: [10, 20], name: 'Input Node', }), createTestNode({ id: 'middle', - type: 'node', + type: SET_NODE_TYPE, position: [10, 20], name: 'Middle Node', }), createTestNode({ id: 'output', - type: 'node', + type: SET_NODE_TYPE, position: [10, 20], name: 'Output Node', }), ]; - workflowsStore.setNodes(nodes); - workflowsStore.setConnections({ - 'Input Node': { - main: [ - [ - { - node: 'Middle Node', - type: NodeConnectionType.Main, - index: 0, - }, - ], - ], - }, - 'Middle Node': { - main: [ - [ - { - node: 'Output Node', - type: NodeConnectionType.Main, - index: 0, - }, - ], - ], - }, - }); - canvasOperations.deleteNode('middle'); - expect(workflowsStore.allConnections).toEqual({ - 'Input Node': { + workflowsStore.workflow.nodes = nodes; + workflowsStore.workflow.connections = { + [nodes[0].name]: { main: [ [ { - node: 'Output Node', + node: nodes[1].name, type: NodeConnectionType.Main, index: 0, }, ], ], }, - }); + [nodes[1].name]: { + main: [ + [ + { + node: nodes[2].name, + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }; + + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); + workflowsStore.incomingConnectionsByNodeName.mockReturnValue({}); + + workflowsStore.getNodeById.mockReturnValue(nodes[1]); + + const { deleteNode } = useCanvasOperations({ router }); + deleteNode(nodes[1].id); + + expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id); + expect(workflowsStore.removeNodeConnectionsById).toHaveBeenCalledWith(nodes[1].id); + expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(nodes[1].id); + expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id); }); }); describe('revertDeleteNode', () => { it('should revert delete node', () => { - const addNodeSpy = vi.spyOn(workflowsStore, 'addNode').mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); const node = createTestNode({ id: 'node1', @@ -570,37 +732,42 @@ describe('useCanvasOperations', () => { parameters: {}, }); - canvasOperations.revertDeleteNode(node); + const { revertDeleteNode } = useCanvasOperations({ router }); + revertDeleteNode(node); - expect(addNodeSpy).toHaveBeenCalledWith(node); + expect(workflowsStore.addNode).toHaveBeenCalledWith(node); }); }); describe('renameNode', () => { it('should rename node', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const ndvStore = mockedStore(useNDVStore); const oldName = 'Old Node'; const newName = 'New Node'; const workflowObject = createTestWorkflowObject(); workflowObject.renameNode = vi.fn(); - - vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockReturnValue(workflowObject); - + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName }); ndvStore.activeNodeName = oldName; - await canvasOperations.renameNode(oldName, newName); + const { renameNode } = useCanvasOperations({ router }); + await renameNode(oldName, newName); expect(workflowObject.renameNode).toHaveBeenCalledWith(oldName, newName); expect(ndvStore.activeNodeName).toBe(newName); }); it('should not rename node when new name is same as old name', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const ndvStore = mockedStore(useNDVStore); const oldName = 'Old Node'; workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName }); ndvStore.activeNodeName = oldName; - await canvasOperations.renameNode(oldName, oldName); + const { renameNode } = useCanvasOperations({ router }); + await renameNode(oldName, oldName); expect(ndvStore.activeNodeName).toBe(oldName); }); @@ -608,22 +775,32 @@ describe('useCanvasOperations', () => { describe('revertRenameNode', () => { it('should revert node renaming', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const ndvStore = mockedStore(useNDVStore); const oldName = 'Old Node'; const currentName = 'New Node'; + + const workflowObject = createTestWorkflowObject(); + workflowObject.renameNode = vi.fn(); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: currentName }); ndvStore.activeNodeName = currentName; - await canvasOperations.revertRenameNode(currentName, oldName); + const { revertRenameNode } = useCanvasOperations({ router }); + await revertRenameNode(currentName, oldName); expect(ndvStore.activeNodeName).toBe(oldName); }); it('should not revert node renaming when old name is same as new name', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const ndvStore = mockedStore(useNDVStore); const oldName = 'Old Node'; workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName }); ndvStore.activeNodeName = oldName; - await canvasOperations.revertRenameNode(oldName, oldName); + const { revertRenameNode } = useCanvasOperations({ router }); + await revertRenameNode(oldName, oldName); expect(ndvStore.activeNodeName).toBe(oldName); }); @@ -631,22 +808,28 @@ describe('useCanvasOperations', () => { describe('setNodeActive', () => { it('should set active node name when node exists', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const ndvStore = mockedStore(useNDVStore); const nodeId = 'node1'; const nodeName = 'Node 1'; workflowsStore.getNodeById = vi.fn().mockReturnValue({ name: nodeName }); ndvStore.activeNodeName = ''; - canvasOperations.setNodeActive(nodeId); + const { setNodeActive } = useCanvasOperations({ router }); + setNodeActive(nodeId); expect(ndvStore.activeNodeName).toBe(nodeName); }); it('should not change active node name when node does not exist', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const ndvStore = mockedStore(useNDVStore); const nodeId = 'node1'; workflowsStore.getNodeById = vi.fn().mockReturnValue(undefined); ndvStore.activeNodeName = 'Existing Node'; - canvasOperations.setNodeActive(nodeId); + const { setNodeActive } = useCanvasOperations({ router }); + setNodeActive(nodeId); expect(ndvStore.activeNodeName).toBe('Existing Node'); }); @@ -654,10 +837,12 @@ describe('useCanvasOperations', () => { describe('setNodeActiveByName', () => { it('should set active node name', () => { + const ndvStore = useNDVStore(); const nodeName = 'Node 1'; ndvStore.activeNodeName = ''; - canvasOperations.setNodeActiveByName(nodeName); + const { setNodeActiveByName } = useCanvasOperations({ router }); + setNodeActiveByName(nodeName); expect(ndvStore.activeNodeName).toBe(nodeName); }); @@ -665,19 +850,20 @@ describe('useCanvasOperations', () => { describe('toggleNodesDisabled', () => { it('disables nodes based on provided ids', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); const nodes = [ createTestNode({ id: '1', name: 'A' }), createTestNode({ id: '2', name: 'B' }), ]; - vi.spyOn(workflowsStore, 'getNodesByIds').mockReturnValue(nodes); - const updateNodePropertiesSpy = vi.spyOn(workflowsStore, 'updateNodeProperties'); + workflowsStore.getNodesByIds.mockReturnValue(nodes); - canvasOperations.toggleNodesDisabled([nodes[0].id, nodes[1].id], { + const { toggleNodesDisabled } = useCanvasOperations({ router }); + toggleNodesDisabled([nodes[0].id, nodes[1].id], { trackHistory: true, trackBulk: true, }); - expect(updateNodePropertiesSpy).toHaveBeenCalledWith({ + expect(workflowsStore.updateNodeProperties).toHaveBeenCalledWith({ name: nodes[0].name, properties: { disabled: true, @@ -688,12 +874,14 @@ describe('useCanvasOperations', () => { describe('revertToggleNodeDisabled', () => { it('re-enables a previously disabled node', () => { + const workflowsStore = mockedStore(useWorkflowsStore); const nodeName = 'testNode'; const node = createTestNode({ name: nodeName }); - vi.spyOn(workflowsStore, 'getNodeByName').mockReturnValue(node); + workflowsStore.getNodeByName.mockReturnValue(node); const updateNodePropertiesSpy = vi.spyOn(workflowsStore, 'updateNodeProperties'); - canvasOperations.revertToggleNodeDisabled(nodeName); + const { revertToggleNodeDisabled } = useCanvasOperations({ router }); + revertToggleNodeDisabled(nodeName); expect(updateNodePropertiesSpy).toHaveBeenCalledWith({ name: nodeName, @@ -706,25 +894,19 @@ describe('useCanvasOperations', () => { describe('addConnections', () => { it('should create connections between nodes', async () => { - const nodeTypeName = 'type'; + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + const nodeTypeName = SET_NODE_TYPE; + const nodeType = mockNodeTypeDescription({ + name: nodeTypeName, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + }); const nodes = [ mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [40, 40] }), mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [40, 40] }), mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [40, 40] }), ]; - - nodeTypesStore.setNodeTypes([ - mockNodeTypeDescription({ - name: nodeTypeName, - }), - ]); - - await canvasOperations.addNodes(nodes, {}); - - vi.spyOn(workflowsStore, 'getNodeById') - .mockReturnValueOnce(nodes[0]) - .mockReturnValueOnce(nodes[1]); - const connections = [ { source: nodes[0].id, @@ -744,11 +926,20 @@ describe('useCanvasOperations', () => { }, ]; - const addConnectionSpy = vi.spyOn(workflowsStore, 'addConnection'); + workflowsStore.workflow.nodes = nodes; + nodeTypesStore.nodeTypes = { + [nodeTypeName]: { 1: nodeType }, + }; - canvasOperations.addConnections(connections); + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); + workflowsStore.getNodeById.mockReturnValueOnce(nodes[0]).mockReturnValueOnce(nodes[1]); + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeType); - expect(addConnectionSpy).toHaveBeenCalledWith({ + const { addConnections } = useCanvasOperations({ router }); + addConnections(connections); + + expect(workflowsStore.addConnection).toHaveBeenCalledWith({ connection: [ { index: 0, @@ -767,54 +958,58 @@ describe('useCanvasOperations', () => { describe('createConnection', () => { it('should not create a connection if source node does not exist', () => { - const addConnectionSpy = vi - .spyOn(workflowsStore, 'addConnection') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); + const uiStore = mockedStore(useUIStore); const connection: Connection = { source: 'nonexistent', target: 'targetNode' }; - vi.spyOn(workflowsStore, 'getNodeById').mockReturnValueOnce(undefined); + workflowsStore.getNodeById.mockReturnValueOnce(undefined); - canvasOperations.createConnection(connection); + const { createConnection } = useCanvasOperations({ router }); + createConnection(connection); - expect(addConnectionSpy).not.toHaveBeenCalled(); + expect(workflowsStore.addConnection).not.toHaveBeenCalled(); expect(uiStore.stateIsDirty).toBe(false); }); it('should not create a connection if target node does not exist', () => { - const addConnectionSpy = vi - .spyOn(workflowsStore, 'addConnection') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); + const uiStore = mockedStore(useUIStore); const connection: Connection = { source: 'sourceNode', target: 'nonexistent' }; - vi.spyOn(workflowsStore, 'getNodeById') + workflowsStore.getNodeById .mockReturnValueOnce(createTestNode()) .mockReturnValueOnce(undefined); - canvasOperations.createConnection(connection); + const { createConnection } = useCanvasOperations({ router }); + createConnection(connection); - expect(addConnectionSpy).not.toHaveBeenCalled(); + expect(workflowsStore.addConnection).not.toHaveBeenCalled(); expect(uiStore.stateIsDirty).toBe(false); }); it('should create a connection if source and target nodes exist and connection is allowed', () => { - const addConnectionSpy = vi - .spyOn(workflowsStore, 'addConnection') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); + const uiStore = mockedStore(useUIStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + + const nodeTypeDescription = mockNodeTypeDescription({ + name: SET_NODE_TYPE, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + }); const nodeA = createTestNode({ id: 'a', - type: 'node', + type: nodeTypeDescription.name, name: 'Node A', }); const nodeB = createTestNode({ id: 'b', - type: 'node', + type: nodeTypeDescription.name, name: 'Node B', }); - vi.spyOn(workflowsStore, 'getNodeById').mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB); - const connection: Connection = { source: nodeA.id, sourceHandle: `outputs/${NodeConnectionType.Main}/0`, @@ -822,18 +1017,25 @@ describe('useCanvasOperations', () => { targetHandle: `inputs/${NodeConnectionType.Main}/0`, }; - const nodeTypeDescription = mockNodeTypeDescription({ - name: 'node', - inputs: [NodeConnectionType.Main], - }); + nodeTypesStore.nodeTypes = { + node: { 1: nodeTypeDescription }, + }; - nodeTypesStore.setNodeTypes([nodeTypeDescription]); - canvasOperations.editableWorkflowObject.value.nodes[nodeA.name] = nodeA; - canvasOperations.editableWorkflowObject.value.nodes[nodeB.name] = nodeB; + workflowsStore.workflow.nodes = [nodeA, nodeB]; + workflowsStore.getNodeById.mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB); + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); - canvasOperations.createConnection(connection); + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); - expect(addConnectionSpy).toHaveBeenCalledWith({ + const { createConnection, editableWorkflowObject } = useCanvasOperations({ router }); + + editableWorkflowObject.value.nodes[nodeA.name] = nodeA; + editableWorkflowObject.value.nodes[nodeB.name] = nodeB; + + createConnection(connection); + + expect(workflowsStore.addConnection).toHaveBeenCalledWith({ connection: [ { index: 0, node: nodeA.name, type: NodeConnectionType.Main }, { index: 0, node: nodeB.name, type: NodeConnectionType.Main }, @@ -845,29 +1047,32 @@ describe('useCanvasOperations', () => { describe('revertCreateConnection', () => { it('deletes connection if both source and target nodes exist', () => { + const workflowsStore = mockedStore(useWorkflowsStore); const connection: [IConnection, IConnection] = [ { node: 'sourceNode', type: NodeConnectionType.Main, index: 0 }, { node: 'targetNode', type: NodeConnectionType.Main, index: 0 }, ]; const testNode = createTestNode(); - const removeConnectionSpy = vi.spyOn(workflowsStore, 'removeConnection'); - vi.spyOn(workflowsStore, 'getNodeByName').mockReturnValue(testNode); - vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(testNode); + workflowsStore.getNodeByName.mockReturnValue(testNode); + workflowsStore.getNodeById.mockReturnValue(testNode); - canvasOperations.revertCreateConnection(connection); + const { revertCreateConnection } = useCanvasOperations({ router }); + revertCreateConnection(connection); - expect(removeConnectionSpy).toHaveBeenCalled(); + expect(workflowsStore.removeConnection).toHaveBeenCalled(); }); }); describe('isConnectionAllowed', () => { it('should return false if source and target nodes are the same', () => { const node = mockNode({ id: '1', type: 'testType', name: 'Test Node' }); - expect(canvasOperations.isConnectionAllowed(node, node, NodeConnectionType.Main)).toBe(false); + const { isConnectionAllowed } = useCanvasOperations({ router }); + expect(isConnectionAllowed(node, node, NodeConnectionType.Main)).toBe(false); }); it('should return false if target node type does not have inputs', () => { + const nodeTypesStore = mockedStore(useNodeTypesStore); const sourceNode = mockNode({ id: '1', type: 'sourceType', @@ -882,14 +1087,16 @@ describe('useCanvasOperations', () => { name: 'targetType', inputs: [], }); - nodeTypesStore.setNodeTypes([nodeTypeDescription]); + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); - expect( - canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main), - ).toBe(false); + const { isConnectionAllowed } = useCanvasOperations({ router }); + expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(false); }); it('should return false if target node does not exist in the workflow', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + const sourceNode = mockNode({ id: '1', type: 'sourceType', @@ -904,14 +1111,18 @@ describe('useCanvasOperations', () => { name: 'targetType', inputs: [NodeConnectionType.Main], }); - nodeTypesStore.setNodeTypes([nodeTypeDescription]); - expect( - canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main), - ).toBe(false); + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); + + const { isConnectionAllowed } = useCanvasOperations({ router }); + expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(false); }); it('should return false if input type does not match connection type', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); const sourceNode = mockNode({ id: '1', type: 'sourceType', @@ -929,16 +1140,21 @@ describe('useCanvasOperations', () => { inputs: [NodeConnectionType.AiTool], }); - nodeTypesStore.setNodeTypes([nodeTypeDescription]); - canvasOperations.editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode; - canvasOperations.editableWorkflowObject.value.nodes[targetNode.name] = targetNode; + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); - expect( - canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main), - ).toBe(false); + const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router }); + + editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode; + editableWorkflowObject.value.nodes[targetNode.name] = targetNode; + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); + + expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(false); }); it('should return false if source node type is not allowed by target node input filter', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); const sourceNode = mockNode({ id: '1', type: 'sourceType', @@ -965,16 +1181,22 @@ describe('useCanvasOperations', () => { ], }); - nodeTypesStore.setNodeTypes([nodeTypeDescription]); - canvasOperations.editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode; - canvasOperations.editableWorkflowObject.value.nodes[targetNode.name] = targetNode; + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); - expect( - canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main), - ).toBe(false); + const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router }); + + editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode; + editableWorkflowObject.value.nodes[targetNode.name] = targetNode; + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); + + expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(false); }); it('should return true if all conditions including filter are met', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + const sourceNode = mockNode({ id: '1', type: 'sourceType', @@ -1001,16 +1223,22 @@ describe('useCanvasOperations', () => { ], }); - nodeTypesStore.setNodeTypes([nodeTypeDescription]); - canvasOperations.editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode; - canvasOperations.editableWorkflowObject.value.nodes[targetNode.name] = targetNode; + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); - expect( - canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main), - ).toBe(true); + const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router }); + + editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode; + editableWorkflowObject.value.nodes[targetNode.name] = targetNode; + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); + + expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(true); }); it('should return true if all conditions are met and no filter is set', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + const sourceNode = mockNode({ id: '1', type: 'sourceType', @@ -1034,51 +1262,50 @@ describe('useCanvasOperations', () => { ], }); - nodeTypesStore.setNodeTypes([nodeTypeDescription]); - canvasOperations.editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode; - canvasOperations.editableWorkflowObject.value.nodes[targetNode.name] = targetNode; + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); - expect( - canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main), - ).toBe(true); + const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router }); + + editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode; + editableWorkflowObject.value.nodes[targetNode.name] = targetNode; + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); + + expect(isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main)).toBe(true); }); }); describe('deleteConnection', () => { it('should not delete a connection if source node does not exist', () => { - const removeConnectionSpy = vi - .spyOn(workflowsStore, 'removeConnection') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); const connection: Connection = { source: 'nonexistent', target: 'targetNode' }; - vi.spyOn(workflowsStore, 'getNodeById') + workflowsStore.getNodeById .mockReturnValueOnce(undefined) .mockReturnValueOnce(createTestNode()); - canvasOperations.deleteConnection(connection); + const { deleteConnection } = useCanvasOperations({ router }); + deleteConnection(connection); - expect(removeConnectionSpy).not.toHaveBeenCalled(); + expect(workflowsStore.removeConnection).not.toHaveBeenCalled(); }); it('should not delete a connection if target node does not exist', () => { - const removeConnectionSpy = vi - .spyOn(workflowsStore, 'removeConnection') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); const connection: Connection = { source: 'sourceNode', target: 'nonexistent' }; - vi.spyOn(workflowsStore, 'getNodeById') + workflowsStore.getNodeById .mockReturnValueOnce(createTestNode()) .mockReturnValueOnce(undefined); - canvasOperations.deleteConnection(connection); + const { deleteConnection } = useCanvasOperations({ router }); + deleteConnection(connection); - expect(removeConnectionSpy).not.toHaveBeenCalled(); + expect(workflowsStore.removeConnection).not.toHaveBeenCalled(); }); it('should delete a connection if source and target nodes exist', () => { - const removeConnectionSpy = vi - .spyOn(workflowsStore, 'removeConnection') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); const nodeA = createTestNode({ id: 'a', @@ -1099,11 +1326,12 @@ describe('useCanvasOperations', () => { targetHandle: `inputs/${NodeConnectionType.Main}/0`, }; - vi.spyOn(workflowsStore, 'getNodeById').mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB); + workflowsStore.getNodeById.mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB); - canvasOperations.deleteConnection(connection); + const { deleteConnection } = useCanvasOperations({ router }); + deleteConnection(connection); - expect(removeConnectionSpy).toHaveBeenCalledWith({ + expect(workflowsStore.removeConnection).toHaveBeenCalledWith({ connection: [ { index: 0, node: nodeA.name, type: NodeConnectionType.Main }, { index: 0, node: nodeB.name, type: NodeConnectionType.Main }, @@ -1114,81 +1342,98 @@ describe('useCanvasOperations', () => { describe('revertDeleteConnection', () => { it('should revert delete connection', () => { - const addConnectionSpy = vi - .spyOn(workflowsStore, 'addConnection') - .mockImplementation(() => {}); + const workflowsStore = mockedStore(useWorkflowsStore); const connection: [IConnection, IConnection] = [ { node: 'sourceNode', type: NodeConnectionType.Main, index: 1 }, { node: 'targetNode', type: NodeConnectionType.Main, index: 2 }, ]; - canvasOperations.revertDeleteConnection(connection); + const { revertDeleteConnection } = useCanvasOperations({ router }); + revertDeleteConnection(connection); - expect(addConnectionSpy).toHaveBeenCalledWith({ connection }); + expect(workflowsStore.addConnection).toHaveBeenCalledWith({ connection }); }); }); describe('duplicateNodes', () => { it('should duplicate nodes', async () => { - nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]); - const telemetrySpy = vi.spyOn(telemetry, 'track'); + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = useNodeTypesStore(); + const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE }); + + nodeTypesStore.nodeTypes = { + [SET_NODE_TYPE]: { 1: nodeTypeDescription }, + }; const nodes = buildImportNodes(); - workflowsStore.setNodes(nodes); + workflowsStore.workflow.nodes = nodes; + workflowsStore.getNodesByIds.mockReturnValue(nodes); + workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({}); + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); + workflowsStore.getWorkflow.mockReturnValue(workflowObject); + + const canvasOperations = useCanvasOperations({ router }); const duplicatedNodeIds = await canvasOperations.duplicateNodes(['1', '2']); + expect(duplicatedNodeIds.length).toBe(2); expect(duplicatedNodeIds).not.toContain('1'); expect(duplicatedNodeIds).not.toContain('2'); - expect(workflowsStore.workflow.nodes.length).toEqual(4); - expect(telemetrySpy).toHaveBeenCalledWith( - 'User duplicated nodes', - expect.objectContaining({ node_graph_string: expect.any(String), workflow_id: 'test' }), - ); }); }); describe('copyNodes', () => { it('should copy nodes', async () => { - nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]); - const telemetrySpy = vi.spyOn(telemetry, 'track'); - const nodes = buildImportNodes(); - workflowsStore.setNodes(nodes); + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = useNodeTypesStore(); + const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE }); + + nodeTypesStore.nodeTypes = { + [SET_NODE_TYPE]: { 1: nodeTypeDescription }, + }; + + const nodes = buildImportNodes(); + workflowsStore.workflow.nodes = nodes; + workflowsStore.getNodesByIds.mockReturnValue(nodes); + workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({}); + + const { copyNodes } = useCanvasOperations({ router }); + await copyNodes(['1', '2']); - await canvasOperations.copyNodes(['1', '2']); expect(useClipboard().copy).toHaveBeenCalledTimes(1); expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot(); - expect(telemetrySpy).toHaveBeenCalledWith( - 'User copied nodes', - expect.objectContaining({ node_types: ['type', 'type'], workflow_id: 'test' }), - ); }); }); describe('cutNodes', () => { it('should copy and delete nodes', async () => { - nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]); - const telemetrySpy = vi.spyOn(telemetry, 'track'); - const nodes = buildImportNodes(); - workflowsStore.setNodes(nodes); + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeTypesStore = useNodeTypesStore(); + const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE }); - await canvasOperations.cutNodes(['1', '2']); + nodeTypesStore.nodeTypes = { + [SET_NODE_TYPE]: { 1: nodeTypeDescription }, + }; + + const nodes = buildImportNodes(); + workflowsStore.workflow.nodes = nodes; + workflowsStore.getNodesByIds.mockReturnValue(nodes); + workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({}); + + const { cutNodes } = useCanvasOperations({ router }); + await cutNodes(['1', '2']); expect(useClipboard().copy).toHaveBeenCalledTimes(1); expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot(); - expect(telemetrySpy).toHaveBeenCalledWith( - 'User copied nodes', - expect.objectContaining({ node_types: ['type', 'type'], workflow_id: 'test' }), - ); - expect(workflowsStore.getNodes().length).toBe(0); }); }); }); function buildImportNodes() { return [ - mockNode({ id: '1', name: 'Node 1', type: 'type' }), - mockNode({ id: '2', name: 'Node 2', type: 'type' }), + mockNode({ id: '1', name: 'Node 1', type: SET_NODE_TYPE }), + mockNode({ id: '2', name: 'Node 2', type: SET_NODE_TYPE }), ].map((node) => { // Setting position in mockNode will wrap it in a Proxy // This causes deepCopy to remove position -> set position after instead diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts index caaed00196..c5ddbd4b90 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.ts @@ -17,7 +17,7 @@ import { useDataSchema } from '@/composables/useDataSchema'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { useI18n } from '@/composables/useI18n'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; -import { usePinnedData, type PinDataSource } from '@/composables/usePinnedData'; +import { type PinDataSource, usePinnedData } from '@/composables/usePinnedData'; import { useTelemetry } from '@/composables/useTelemetry'; import { useToast } from '@/composables/useToast'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; @@ -65,6 +65,13 @@ import { parseCanvasConnectionHandleString, } from '@/utils/canvasUtilsV2'; import * as NodeViewUtils from '@/utils/nodeViewUtils'; +import { + CONFIGURABLE_NODE_SIZE, + CONFIGURATION_NODE_SIZE, + DEFAULT_NODE_SIZE, + GRID_SIZE, + PUSH_NODES_OFFSET, +} from '@/utils/nodeViewUtils'; import { isValidNodeConnectionType } from '@/utils/typeGuards'; import type { Connection } from '@vue-flow/core'; import type { @@ -358,6 +365,7 @@ export function useCanvasOperations({ router }: { router: ReturnType & { position?: INodeUi['position'] }, + nodeTypeDescription: INodeTypeDescription, + ) { + let position: XYPosition | undefined = node.position; + let pushOffsets: XYPosition = [40, 40]; + // Available when + // - clicking the plus button of a node handle + // - dragging an edge / connection of a node handle + // - selecting a node, adding a node via the node creator const lastInteractedWithNode = uiStore.lastInteractedWithNode; + // Available when clicking the plus button of a node edge / connection const lastInteractedWithNodeConnection = uiStore.lastInteractedWithNodeConnection; + // Available when dragging an edge / connection from a node + const lastInteractedWithNodeHandle = uiStore.lastInteractedWithNodeHandle; + + const { type: connectionType, index: connectionIndex } = parseCanvasConnectionHandleString( + lastInteractedWithNodeHandle ?? lastInteractedWithNodeConnection?.sourceHandle ?? '', + ); + + const nodeSize = + connectionType === NodeConnectionType.Main ? DEFAULT_NODE_SIZE : CONFIGURATION_NODE_SIZE; + if (lastInteractedWithNode) { - const lastSelectedNodeTypeDescription = nodeTypesStore.getNodeType( + const lastInteractedWithNodeTypeDescription = nodeTypesStore.getNodeType( lastInteractedWithNode.type, lastInteractedWithNode.typeVersion, ); - const lastInteractedWithNodeObject = editableWorkflowObject.value.getNode( - lastInteractedWithNode.name, - ); - if (lastInteractedWithNodeConnection) { - shiftDownstreamNodesPosition(lastInteractedWithNode.name, NodeViewUtils.PUSH_NODES_OFFSET, { - trackHistory: true, - }); - } - - // This position is set in `onMouseUp` when pulling connections - const newNodeInsertPosition = canvasStore.newNodeInsertPosition; + const newNodeInsertPosition = uiStore.lastCancelledConnectionPosition; if (newNodeInsertPosition) { - canvasStore.newNodeInsertPosition = null; - return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, [ - newNodeInsertPosition[0] + NodeViewUtils.GRID_SIZE, - newNodeInsertPosition[1] - NodeViewUtils.NODE_SIZE / 2, - ]); - } else { - let yOffset = 0; + // When pulling / cancelling a connection. + // The new node should be placed at the same position as the mouse up event, + // designated by the `newNodeInsertPosition` value. - // Compute the y offset for the new node based on the number of main outputs of the source node - if (uiStore.lastInteractedWithNodeConnection) { - const sourceNodeType = nodeTypesStore.getNodeType( - lastInteractedWithNode.type, - lastInteractedWithNode.typeVersion, + const xOffset = connectionType === NodeConnectionType.Main ? 0 : -nodeSize[0] / 2; + const yOffset = connectionType === NodeConnectionType.Main ? -nodeSize[1] / 2 : 0; + + position = [newNodeInsertPosition[0] + xOffset, newNodeInsertPosition[1] + yOffset]; + + uiStore.lastCancelledConnectionPosition = null; + } else if (lastInteractedWithNodeTypeDescription) { + // When + // - clicking the plus button of a node handle + // - clicking the plus button of a node edge / connection + // - selecting a node, adding a node via the node creator + + let yOffset = 0; + if (lastInteractedWithNodeConnection) { + // When clicking the plus button of a node edge / connection + // Compute the y offset for the new node based on the number of main outputs of the source node + // and shift the downstream nodes accordingly + + shiftDownstreamNodesPosition(lastInteractedWithNode.name, PUSH_NODES_OFFSET, { + trackHistory: true, + }); + + const yOffsetValuesByOutputCount = [ + [-nodeSize[1], nodeSize[1]], + [-nodeSize[1] - 2 * GRID_SIZE, 0, nodeSize[1] - 2 * GRID_SIZE], + [ + -2 * nodeSize[1] - 2 * GRID_SIZE, + -nodeSize[1], + nodeSize[1], + 2 * nodeSize[1] - 2 * GRID_SIZE, + ], + ]; + + const lastInteractedWithNodeOutputs = NodeHelpers.getNodeOutputs( + editableWorkflowObject.value, + lastInteractedWithNode, + lastInteractedWithNodeTypeDescription, + ); + const lastInteractedWithNodeOutputTypes = NodeHelpers.getConnectionTypes( + lastInteractedWithNodeOutputs, + ); + const lastInteractedWithNodeMainOutputs = lastInteractedWithNodeOutputTypes.filter( + (output) => output === NodeConnectionType.Main, ); - if (sourceNodeType) { - const offsets = [ - [-100, 100], - [-140, 0, 140], - [-240, -100, 100, 240], - ]; - - const sourceNodeOutputs = NodeHelpers.getNodeOutputs( - editableWorkflowObject.value, - lastInteractedWithNode, - sourceNodeType, - ); - const sourceNodeOutputTypes = NodeHelpers.getConnectionTypes(sourceNodeOutputs); - const sourceNodeOutputMainOutputs = sourceNodeOutputTypes.filter( - (output) => output === NodeConnectionType.Main, - ); - - if (sourceNodeOutputMainOutputs.length > 1) { - const { index: sourceOutputIndex } = parseCanvasConnectionHandleString( - uiStore.lastInteractedWithNodeConnection.sourceHandle, - ); - const offset = offsets[sourceNodeOutputMainOutputs.length - 2]; - yOffset = offset[sourceOutputIndex]; - } + if (lastInteractedWithNodeMainOutputs.length > 1) { + const yOffsetValues = + yOffsetValuesByOutputCount[lastInteractedWithNodeMainOutputs.length - 2]; + yOffset = yOffsetValues[connectionIndex]; } } @@ -913,80 +940,96 @@ export function useCanvasOperations({ router }: { router: ReturnType 0 && + outputTypes.every((outputName) => outputName !== NodeConnectionType.Main) && + lastInteractedWithNodeObject + ) { + // When the added node has only non-main outputs (configuration nodes) + // We want to place the new node directly below the last interacted with node. + + const lastInteractedWithNodeInputs = NodeHelpers.getNodeInputs( + editableWorkflowObject.value, + lastInteractedWithNodeObject, + lastInteractedWithNodeTypeDescription, + ); + const lastInteractedWithNodeInputTypes = NodeHelpers.getConnectionTypes( + lastInteractedWithNodeInputs, + ); + const lastInteractedWithNodeScopedInputTypes = ( + lastInteractedWithNodeInputTypes || [] + ).filter((input) => input !== NodeConnectionType.Main); + const scopedConnectionIndex = lastInteractedWithNodeScopedInputTypes.findIndex( + (inputType) => outputs[0] === inputType, + ); + + const lastInteractedWithNodeWidthDivisions = Math.max( + lastInteractedWithNodeScopedInputTypes.length + 1, + 1, + ); + + position = [ + lastInteractedWithNode.position[0] + + (CONFIGURABLE_NODE_SIZE[0] / lastInteractedWithNodeWidthDivisions) * + (scopedConnectionIndex + 1) - + nodeSize[0] / 2, + lastInteractedWithNode.position[1] + PUSH_NODES_OFFSET, + ]; + } else { + // When the node has only main outputs, mixed outputs, or no outputs at all + // We want to place the new node directly to the right of the last interacted with node. + + const lastInteractedWithNodeInputs = NodeHelpers.getNodeInputs( + editableWorkflowObject.value, + lastInteractedWithNode, + lastInteractedWithNodeTypeDescription, + ); + const lastInteractedWithNodeInputTypes = NodeHelpers.getConnectionTypes( + lastInteractedWithNodeInputs, + ); + + let pushOffset = PUSH_NODES_OFFSET; if ( - lastInteractedWithNodeObject && - outputTypes.length > 0 && - outputTypes.every((outputName) => outputName !== NodeConnectionType.Main) + !!lastInteractedWithNodeInputTypes.find((input) => input !== NodeConnectionType.Main) ) { - const lastSelectedInputs = NodeHelpers.getNodeInputs( - editableWorkflowObject.value, - lastInteractedWithNodeObject, - lastSelectedNodeTypeDescription, - ); - const lastSelectedInputTypes = NodeHelpers.getConnectionTypes(lastSelectedInputs); - - const scopedConnectionIndex = (lastSelectedInputTypes || []) - .filter((input) => input !== NodeConnectionType.Main) - .findIndex((inputType) => outputs[0] === inputType); - - return NodeViewUtils.getNewNodePosition( - workflowsStore.allNodes, - [ - lastInteractedWithNode.position[0] + - (NodeViewUtils.NODE_SIZE / - (Math.max(lastSelectedNodeTypeDescription?.inputs?.length ?? 1), 1)) * - scopedConnectionIndex, - lastInteractedWithNode.position[1] + NodeViewUtils.PUSH_NODES_OFFSET, - ], - [100, 0], - ); - } else { - // Has only main outputs or no outputs at all - const inputs = NodeHelpers.getNodeInputs( - editableWorkflowObject.value, - lastInteractedWithNode, - lastSelectedNodeTypeDescription, - ); - const inputsTypes = NodeHelpers.getConnectionTypes(inputs); - - let pushOffset = NodeViewUtils.PUSH_NODES_OFFSET; - if (!!inputsTypes.find((input) => input !== NodeConnectionType.Main)) { - // If the node has scoped inputs, push it down a bit more - pushOffset += 150; - } - - // If a node is active then add the new node directly after the current one - return NodeViewUtils.getNewNodePosition( - workflowsStore.allNodes, - [ - lastInteractedWithNode.position[0] + pushOffset, - lastInteractedWithNode.position[1] + yOffset, - ], - [100, 0], - ); + // If the node has scoped inputs, push it down a bit more + pushOffset += 140; } + + // If a node is active then add the new node directly after the current one + position = [ + lastInteractedWithNode.position[0] + pushOffset, + lastInteractedWithNode.position[1] + yOffset, + ]; } } } - // If added node is a trigger and it's the first one added to the canvas - // we place it at canvasAddButtonPosition to replace the canvas add button - const position = ( - nodeTypesStore.isTriggerNode(node.type) && triggerNodes.value.length === 0 - ? [0, 0] - : // If no node is active find a free spot - lastClickPosition.value - ) as XYPosition; + if (!position) { + if (nodeTypesStore.isTriggerNode(node.type) && triggerNodes.value.length === 0) { + // When added node is a trigger, and it's the first one added to the canvas + // we place it at root to replace the canvas add button - return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position); + position = [0, 0]; + } else { + // When no position is set, we place the node at the last clicked position + + position = lastClickPosition.value; + } + } + + return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position, pushOffsets); } function resolveNodeName(node: INodeUi) { @@ -1219,7 +1262,7 @@ export function useCanvasOperations({ router }: { router: ReturnType { // #endregion return { + state, getCredentialOwnerName, getCredentialsByType, getCredentialById, diff --git a/packages/editor-ui/src/stores/nodeCreator.store.ts b/packages/editor-ui/src/stores/nodeCreator.store.ts index 6668b78237..db6ce26eaa 100644 --- a/packages/editor-ui/src/stores/nodeCreator.store.ts +++ b/packages/editor-ui/src/stores/nodeCreator.store.ts @@ -90,7 +90,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { setTimeout(() => { if (creatorView) { - openNodeCreator({ + setNodeCreatorState({ createNodeActive: true, nodeCreatorView: creatorView, }); @@ -110,7 +110,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { }); } - function openNodeCreator({ + function setNodeCreatorState({ source, createNodeActive, nodeCreatorView, @@ -200,7 +200,6 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { uiStore.lastSelectedNode = sourceNode.name; uiStore.lastSelectedNodeEndpointUuid = connection.sourceHandle ?? null; uiStore.lastSelectedNodeOutputIndex = index; - // canvasStore.newNodeInsertPosition = null; if (isVueFlowConnection(connection)) { uiStore.lastInteractedWithNodeConnection = connection; @@ -208,7 +207,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { uiStore.lastInteractedWithNodeHandle = connection.sourceHandle ?? null; uiStore.lastInteractedWithNodeId = sourceNode.id; - openNodeCreator({ + setNodeCreatorState({ source: eventSource, createNodeActive: true, nodeCreatorView, @@ -231,7 +230,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { ndvStore.activeNodeName = null; setSelectedView(TRIGGER_NODE_CREATOR_VIEW); setShowScrim(true); - openNodeCreator({ + setNodeCreatorState({ source, createNodeActive: true, nodeCreatorView: TRIGGER_NODE_CREATOR_VIEW, @@ -276,7 +275,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { setOpenSource, setActions, setMergeNodes, - openNodeCreator, + setNodeCreatorState, openSelectiveNodeCreator, openNodeCreatorForConnectingNode, openNodeCreatorForTriggerNodes, diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index 18cb3fbe37..e854ac656c 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -200,6 +200,7 @@ export const useUIStore = defineStore(STORES.UI, () => { const lastInteractedWithNodeConnection = ref(null); const lastInteractedWithNodeHandle = ref(null); const lastInteractedWithNodeId = ref(null); + const lastCancelledConnectionPosition = ref(null); const settingsStore = useSettingsStore(); const workflowsStore = useWorkflowsStore(); @@ -624,6 +625,7 @@ export const useUIStore = defineStore(STORES.UI, () => { lastInteractedWithNodeConnection.value = null; lastInteractedWithNodeHandle.value = null; lastInteractedWithNodeId.value = null; + lastCancelledConnectionPosition.value = null; } return { @@ -652,6 +654,7 @@ export const useUIStore = defineStore(STORES.UI, () => { lastInteractedWithNodeHandle, lastInteractedWithNodeId, lastInteractedWithNode, + lastCancelledConnectionPosition, nodeViewOffsetPosition, nodeViewMoveInProgress, nodeViewInitialized, diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.ts b/packages/editor-ui/src/utils/canvasUtilsV2.ts index 948c24f1e0..e73890fe9b 100644 --- a/packages/editor-ui/src/utils/canvasUtilsV2.ts +++ b/packages/editor-ui/src/utils/canvasUtilsV2.ts @@ -103,9 +103,8 @@ export function mapLegacyConnectionToCanvasConnection( export function parseCanvasConnectionHandleString(handle: string | null | undefined) { const [mode, type, index] = (handle ?? '').split('/'); - const resolvedType = isValidNodeConnectionType(type) ? type : NodeConnectionType.Main; const resolvedMode = isValidCanvasConnectionMode(mode) ? mode : CanvasConnectionMode.Output; - + const resolvedType = isValidNodeConnectionType(type) ? type : NodeConnectionType.Main; let resolvedIndex = parseInt(index, 10); if (isNaN(resolvedIndex)) { resolvedIndex = 0; diff --git a/packages/editor-ui/src/utils/nodeViewUtils.ts b/packages/editor-ui/src/utils/nodeViewUtils.ts index a0e87c96de..d28d8f1c97 100644 --- a/packages/editor-ui/src/utils/nodeViewUtils.ts +++ b/packages/editor-ui/src/utils/nodeViewUtils.ts @@ -38,6 +38,9 @@ const MIN_X_TO_SHOW_OUTPUT_LABEL = 90; const MIN_Y_TO_SHOW_OUTPUT_LABEL = 100; export const NODE_SIZE = 100; +export const DEFAULT_NODE_SIZE = [100, 100]; +export const CONFIGURATION_NODE_SIZE = [80, 80]; +export const CONFIGURABLE_NODE_SIZE = [256, 100]; export const PLACEHOLDER_TRIGGER_NODE_SIZE = 100; export const DEFAULT_START_POSITION_X = 180; export const DEFAULT_START_POSITION_Y = 240; diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index 04cd656e8d..5ce133bca9 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -34,7 +34,11 @@ import type { ToggleNodeCreatorOptions, XYPosition, } from '@/Interface'; -import type { Connection, ViewportTransform } from '@vue-flow/core'; +import type { + Connection, + ViewportTransform, + XYPosition as VueFlowXYPosition, +} from '@vue-flow/core'; import type { CanvasConnectionCreateData, CanvasEventBusEvents, @@ -738,12 +742,20 @@ function onRevertCreateConnection({ connection }: { connection: [IConnection, IC revertCreateConnection(connection); } -function onCreateConnectionCancelled(event: ConnectStartEvent, mouseEvent?: MouseEvent) { +function onCreateConnectionCancelled( + event: ConnectStartEvent, + position: VueFlowXYPosition, + mouseEvent?: MouseEvent, +) { const preventDefault = (mouseEvent?.target as HTMLElement).classList?.contains('clickable'); if (preventDefault) { return; } + uiStore.lastInteractedWithNodeId = event.nodeId; + uiStore.lastInteractedWithNodeHandle = event.handleId; + uiStore.lastCancelledConnectionPosition = [position.x, position.y]; + setTimeout(() => { nodeCreatorStore.openNodeCreatorForConnectingNode({ connection: { @@ -874,11 +886,15 @@ async function onOpenNodeCreatorForTriggerNodes(source: NodeCreatorOpenSource) { } function onOpenNodeCreatorFromCanvas(source: NodeCreatorOpenSource) { - onOpenNodeCreator({ createNodeActive: true, source }); + onToggleNodeCreator({ createNodeActive: true, source }); } -function onOpenNodeCreator(options: ToggleNodeCreatorOptions) { - nodeCreatorStore.openNodeCreator(options); +function onToggleNodeCreator(options: ToggleNodeCreatorOptions) { + nodeCreatorStore.setNodeCreatorState(options); + + if (!options.createNodeActive && !options.hasAddedNodes) { + uiStore.resetLastInteractedWith(); + } } function onCreateSticky() { @@ -1378,7 +1394,6 @@ function selectNodes(ids: string[]) { function onClickPane(position: CanvasNode['position']) { lastClickPosition.value = [position.x, position.y]; - canvasStore.newNodeInsertPosition = [position.x, position.y]; uiStore.isCreateNodeActive = false; } @@ -1563,7 +1578,7 @@ onDeactivated(() => { v-if="!isCanvasReadOnly" :create-node-active="uiStore.isCreateNodeActive" :node-view-scale="viewportTransform.zoom" - @toggle-node-creator="onOpenNodeCreator" + @toggle-node-creator="onToggleNodeCreator" @add-nodes="onAddNodesAndConnections" /> From c97a96d314d4cd72b9a971993af5d5f1e32ccf48 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:35:13 +0300 Subject: [PATCH 038/259] build: Add `reset` script (#10627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- package.json | 4 +- pnpm-lock.yaml | 255 +++++++++++++----------------------------- scripts/ensure-zx.mjs | 17 +++ scripts/reset.mjs | 30 +++++ 4 files changed, 129 insertions(+), 177 deletions(-) create mode 100644 scripts/ensure-zx.mjs create mode 100644 scripts/reset.mjs diff --git a/package.json b/package.json index b4c470cbea..415697e250 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat", "dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core", "clean": "turbo run clean --parallel", + "reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs", "format": "turbo run format && node scripts/format.mjs", "lint": "turbo run lint", "lintfix": "turbo run lintfix", @@ -55,7 +56,8 @@ "tsc-alias": "^1.8.7", "tsc-watch": "^6.0.4", "turbo": "2.0.6", - "typescript": "*" + "typescript": "*", + "zx": "^8.1.4" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b769d8a6a2..7e993b188c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,6 +174,9 @@ importers: typescript: specifier: ^5.5.2 version: 5.5.2 + zx: + specifier: ^8.1.4 + version: 8.1.4 cypress: dependencies: @@ -559,7 +562,7 @@ importers: version: 8.1.4(@types/react@18.0.27)(encoding@0.1.13)(prettier@3.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/addon-interactions': specifier: ^8.1.4 - version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) + version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) '@storybook/addon-links': specifier: ^8.1.4 version: 8.1.4(react@18.2.0) @@ -571,7 +574,7 @@ importers: version: 8.1.4(@types/react@18.0.27)(encoding@0.1.13)(prettier@3.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/test': specifier: ^8.1.4 - version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) + version: 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) '@storybook/vue3': specifier: ^8.1.4 version: 8.1.4(encoding@0.1.13)(prettier@3.2.5)(vue@3.4.21(typescript@5.5.2)) @@ -1820,7 +1823,7 @@ importers: devDependencies: '@langchain/core': specifier: ^0.2.18 - version: 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + version: 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -13130,8 +13133,8 @@ packages: vue-component-type-helpers@2.0.19: resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==} - vue-component-type-helpers@2.1.2: - resolution: {integrity: sha512-URuxnrOhO9lUG4LOAapGWBaa/WOLDzzyAbL+uKZqT7RS+PFy0cdXI2mUSh7GaMts6vtHaeVbGk7trd0FPJi65Q==} + vue-component-type-helpers@2.1.4: + resolution: {integrity: sha512-aVqB3KxwpM76cYRkpnezl1J62E/1omzHQfx1yuz7zcbxmzmP/PeSgI20NEmkdeGnjZPVzm0V9fB4ZyRu5BBj4A==} vue-demi@0.14.5: resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} @@ -16045,8 +16048,8 @@ snapshots: url-join: 4.0.1 zod: 3.23.8 optionalDependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - langchain: 0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + langchain: 0.2.11(m7otbkpxyspoz5trt2pa3dcs6u) transitivePeerDependencies: - encoding @@ -16469,7 +16472,7 @@ snapshots: '@langchain/anthropic@0.2.9(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: '@anthropic-ai/sdk': 0.22.0(encoding@0.1.13) - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) fast-xml-parser: 4.3.5 zod: 3.23.8 zod-to-json-schema: 3.23.0(zod@3.23.8) @@ -16481,7 +16484,7 @@ snapshots: '@langchain/cohere@0.0.10(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) cohere-ai: 7.10.1(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -16559,78 +16562,23 @@ snapshots: - pyodide - supports-color - '@langchain/community@0.2.20(yatsnfdsa55wls5pvl4axjr4ti)': + '@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0)': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@langchain/openai': 0.2.5(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1)) - binary-extensions: 2.2.0 - expr-eval: 2.0.2 - flat: 5.0.2 - js-yaml: 4.1.0 - langchain: 0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1) - langsmith: 0.1.34(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)) + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.12 + langsmith: 0.1.39(@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0))(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) + ml-distance: 4.0.1 + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 uuid: 10.0.0 zod: 3.23.8 zod-to-json-schema: 3.23.0(zod@3.23.8) - optionalDependencies: - '@aws-sdk/client-bedrock-runtime': 3.535.0 - '@aws-sdk/client-s3': 3.478.0 - '@aws-sdk/credential-provider-node': 3.535.0 - '@azure/storage-blob': 12.18.0(encoding@0.1.13) - '@getzep/zep-cloud': 1.0.11(@langchain/core@0.2.18)(encoding@0.1.13)(langchain@0.2.11) - '@getzep/zep-js': 0.9.0 - '@google-ai/generativelanguage': 2.5.0(encoding@0.1.13) - '@google-cloud/storage': 7.12.1(encoding@0.1.13) - '@huggingface/inference': 2.7.0 - '@mozilla/readability': 0.5.0 - '@pinecone-database/pinecone': 3.0.0 - '@qdrant/js-client-rest': 1.9.0(typescript@5.5.2) - '@smithy/eventstream-codec': 2.2.0 - '@smithy/protocol-http': 3.3.0 - '@smithy/signature-v4': 2.2.1 - '@smithy/util-utf8': 2.3.0 - '@supabase/postgrest-js': 1.15.2 - '@supabase/supabase-js': 2.43.4 - '@xata.io/client': 0.28.4(typescript@5.5.2) - cheerio: 1.0.0-rc.12 - cohere-ai: 7.10.1(encoding@0.1.13) - crypto-js: 4.2.0 - d3-dsv: 2.0.0 - epub2: 3.0.2(ts-toolbelt@9.6.0) - google-auth-library: 9.10.0(encoding@0.1.13) - html-to-text: 9.0.5 - ignore: 5.2.4 - ioredis: 5.3.2 - jsdom: 23.0.1 - jsonwebtoken: 9.0.2 - lodash: 4.17.21 - mammoth: 1.7.2 - mongodb: 6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1) - mysql2: 3.11.0 - pdf-parse: 1.1.1 - pg: 8.12.0 - redis: 4.6.14 - ws: 8.17.1 transitivePeerDependencies: - - '@gomomento/sdk-web' - - '@langchain/anthropic' - - '@langchain/aws' - - '@langchain/cohere' - - '@langchain/google-genai' - - '@langchain/google-vertexai' - - '@langchain/google-vertexai-web' - - '@langchain/groq' - - '@langchain/mistralai' - - '@langchain/ollama' - - axios - - encoding - - fast-xml-parser - - handlebars + - langchain - openai - - peggy - - pyodide - - supports-color - optional: true '@langchain/core@0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))': dependencies: @@ -16650,27 +16598,9 @@ snapshots: - langchain - openai - '@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': + '@langchain/google-common@0.0.22(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.1.39(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)) - ml-distance: 4.0.1 - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.0(zod@3.23.8) - transitivePeerDependencies: - - langchain - - openai - - '@langchain/google-common@0.0.22(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': - dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) uuid: 10.0.0 zod-to-json-schema: 3.23.0(zod@3.23.8) transitivePeerDependencies: @@ -16678,10 +16608,10 @@ snapshots: - openai - zod - '@langchain/google-gauth@0.0.21(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': + '@langchain/google-gauth@0.0.21(encoding@0.1.13)(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@langchain/google-common': 0.0.22(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + '@langchain/google-common': 0.0.22(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) google-auth-library: 8.9.0(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -16693,7 +16623,7 @@ snapshots: '@langchain/google-genai@0.0.23(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': dependencies: '@google/generative-ai': 0.7.1 - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) zod-to-json-schema: 3.23.0(zod@3.23.8) transitivePeerDependencies: - langchain @@ -16702,8 +16632,8 @@ snapshots: '@langchain/google-vertexai@0.0.21(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))(zod@3.23.8)': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@langchain/google-gauth': 0.0.21(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + '@langchain/google-gauth': 0.0.21(encoding@0.1.13)(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) transitivePeerDependencies: - encoding - langchain @@ -16713,8 +16643,8 @@ snapshots: '@langchain/groq@0.0.15(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@langchain/openai': 0.2.5(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1)) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) + '@langchain/openai': 0.2.5(encoding@0.1.13)(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u)) groq-sdk: 0.3.2(encoding@0.1.13) zod: 3.23.8 zod-to-json-schema: 3.23.0(zod@3.23.8) @@ -16726,7 +16656,7 @@ snapshots: '@langchain/mistralai@0.0.27(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) '@mistralai/mistralai': 0.4.0(encoding@0.1.13) uuid: 10.0.0 zod: 3.23.8 @@ -16738,25 +16668,13 @@ snapshots: '@langchain/ollama@0.0.2(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) ollama: 0.5.6 uuid: 10.0.0 transitivePeerDependencies: - langchain - openai - '@langchain/openai@0.2.5(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))': - dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - js-tiktoken: 1.0.12 - openai: 4.53.0(encoding@0.1.13) - zod: 3.23.8 - zod-to-json-schema: 3.23.0(zod@3.23.8) - transitivePeerDependencies: - - encoding - - langchain - - supports-color - '@langchain/openai@0.2.5(encoding@0.1.13)(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))': dependencies: '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) @@ -16769,6 +16687,19 @@ snapshots: - langchain - supports-color + '@langchain/openai@0.2.5(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))': + dependencies: + '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) + js-tiktoken: 1.0.12 + openai: 4.53.0(encoding@0.1.13) + zod: 3.23.8 + zod-to-json-schema: 3.23.0(zod@3.23.8) + transitivePeerDependencies: + - encoding + - langchain + - supports-color + optional: true + '@langchain/pinecone@0.0.8(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13))': dependencies: '@langchain/core': 0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)) @@ -16797,9 +16728,9 @@ snapshots: - langchain - openai - '@langchain/textsplitters@0.0.3(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13))': + '@langchain/textsplitters@0.0.3(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0)': dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) js-tiktoken: 1.0.12 transitivePeerDependencies: - langchain @@ -18112,11 +18043,11 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/addon-interactions@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))': + '@storybook/addon-interactions@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': dependencies: '@storybook/global': 5.0.0 '@storybook/instrumenter': 8.1.4 - '@storybook/test': 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) + '@storybook/test': 8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) '@storybook/types': 8.1.4 polished: 4.2.2 ts-dedent: 2.2.0 @@ -18564,14 +18495,14 @@ snapshots: - prettier - supports-color - '@storybook/test@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))': + '@storybook/test@8.1.4(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': dependencies: '@storybook/client-logger': 8.1.4 '@storybook/core-events': 8.1.4 '@storybook/instrumenter': 8.1.4 '@storybook/preview-api': 8.1.4 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1)) + '@testing-library/jest-dom': 6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1)) '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) '@vitest/expect': 1.3.1 '@vitest/spy': 1.3.1 @@ -18634,7 +18565,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.5.2) - vue-component-type-helpers: 2.1.2 + vue-component-type-helpers: 2.1.4 transitivePeerDependencies: - encoding - prettier @@ -18734,7 +18665,7 @@ snapshots: jest: 29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)) vitest: 1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1) - '@testing-library/jest-dom@6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))': + '@testing-library/jest-dom@6.4.2(@jest/globals@29.6.2)(@types/jest@29.5.3)(jest@29.6.2(@types/node@18.16.16)(ts-node@10.9.2(@types/node@18.16.16)(typescript@5.5.2)))(vitest@1.6.0(@types/node@18.16.16)(sass@1.64.1)(terser@5.16.1))': dependencies: '@adobe/css-tools': 4.3.2 '@babel/runtime': 7.23.6 @@ -23951,17 +23882,17 @@ snapshots: kuler@2.0.0: {} - langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1): + langchain@0.2.11(axios@1.7.4)(openai@4.53.0): dependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@langchain/openai': 0.2.5(encoding@0.1.13)(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1)) - '@langchain/textsplitters': 0.0.3(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)) + '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) + '@langchain/openai': 0.2.5(langchain@0.2.11(axios@1.7.4)(openai@4.53.0)) + '@langchain/textsplitters': 0.0.3(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) binary-extensions: 2.2.0 js-tiktoken: 1.0.12 js-yaml: 4.1.0 jsonpointer: 5.0.1 langchainhub: 0.0.8 - langsmith: 0.1.34(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)) + langsmith: 0.1.34(@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0))(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) ml-distance: 4.0.1 openapi-types: 12.1.3 p-retry: 4.6.2 @@ -23970,35 +23901,7 @@ snapshots: zod: 3.23.8 zod-to-json-schema: 3.23.0(zod@3.23.8) optionalDependencies: - '@aws-sdk/client-s3': 3.478.0 - '@aws-sdk/credential-provider-node': 3.535.0 - '@azure/storage-blob': 12.18.0(encoding@0.1.13) - '@langchain/anthropic': 0.2.9(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@langchain/cohere': 0.0.10(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@langchain/community': 0.2.20(yatsnfdsa55wls5pvl4axjr4ti) - '@langchain/google-genai': 0.0.23(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) - '@langchain/google-vertexai': 0.0.21(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13))(zod@3.23.8) - '@langchain/groq': 0.0.15(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@langchain/mistralai': 0.0.27(encoding@0.1.13)(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@langchain/ollama': 0.0.2(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - '@pinecone-database/pinecone': 3.0.0 - '@supabase/supabase-js': 2.43.4 - '@xata.io/client': 0.28.4(typescript@5.5.2) axios: 1.7.4(debug@4.3.6) - cheerio: 1.0.0-rc.12 - d3-dsv: 2.0.0 - epub2: 3.0.2(ts-toolbelt@9.6.0) - fast-xml-parser: 4.4.1 - handlebars: 4.7.8 - html-to-text: 9.0.5 - ignore: 5.2.4 - ioredis: 5.3.2 - jsdom: 23.0.1 - mammoth: 1.7.2 - mongodb: 6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1) - pdf-parse: 1.1.1 - redis: 4.6.14 - ws: 8.17.1 transitivePeerDependencies: - encoding - openai @@ -24059,6 +23962,20 @@ snapshots: langchainhub@0.0.8: {} + langsmith@0.1.34(@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0))(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0): + dependencies: + '@types/uuid': 9.0.7 + commander: 10.0.1 + lodash.set: 4.3.2 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 9.0.1 + optionalDependencies: + '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) + langchain: 0.2.11(axios@1.7.4)(openai@4.53.0) + openai: 4.53.0(encoding@0.1.13) + optional: true + langsmith@0.1.34(@langchain/core@0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)): dependencies: '@types/uuid': 9.0.7 @@ -24072,19 +23989,17 @@ snapshots: langchain: 0.2.11(m7otbkpxyspoz5trt2pa3dcs6u) openai: 4.53.0(encoding@0.1.13) - langsmith@0.1.34(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)): + langsmith@0.1.39(@langchain/core@0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0))(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0): dependencies: '@types/uuid': 9.0.7 commander: 10.0.1 - lodash.set: 4.3.2 p-queue: 6.6.2 p-retry: 4.6.2 uuid: 9.0.1 optionalDependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - langchain: 0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1) + '@langchain/core': 0.2.18(langchain@0.2.11(axios@1.7.4)(openai@4.53.0))(openai@4.53.0) + langchain: 0.2.11(axios@1.7.4)(openai@4.53.0) openai: 4.53.0(encoding@0.1.13) - optional: true langsmith@0.1.39(@langchain/core@0.2.18(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(m7otbkpxyspoz5trt2pa3dcs6u))(openai@4.53.0(encoding@0.1.13)): dependencies: @@ -24098,18 +24013,6 @@ snapshots: langchain: 0.2.11(m7otbkpxyspoz5trt2pa3dcs6u) openai: 4.53.0(encoding@0.1.13) - langsmith@0.1.39(@langchain/core@0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)))(langchain@0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1))(openai@4.53.0(encoding@0.1.13)): - dependencies: - '@types/uuid': 9.0.7 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 9.0.1 - optionalDependencies: - '@langchain/core': 0.2.18(langchain@0.2.11)(openai@4.53.0(encoding@0.1.13)) - langchain: 0.2.11(@aws-sdk/client-s3@3.478.0)(@aws-sdk/credential-provider-node@3.535.0)(@azure/storage-blob@12.18.0(encoding@0.1.13))(@langchain/anthropic@0.2.9)(@langchain/cohere@0.0.10)(@langchain/community@0.2.20)(@langchain/google-genai@0.0.23)(@langchain/google-vertexai@0.0.21)(@langchain/groq@0.0.15)(@langchain/mistralai@0.0.27)(@langchain/ollama@0.0.2)(@pinecone-database/pinecone@3.0.0)(@supabase/supabase-js@2.43.4)(@xata.io/client@0.28.4(typescript@5.5.2))(axios@1.7.4)(cheerio@1.0.0-rc.12)(d3-dsv@2.0.0)(encoding@0.1.13)(epub2@3.0.2(ts-toolbelt@9.6.0))(fast-xml-parser@4.4.1)(handlebars@4.7.8)(html-to-text@9.0.5)(ignore@5.2.4)(ioredis@5.3.2)(jsdom@23.0.1)(mammoth@1.7.2)(mongodb@6.3.0(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(openai@4.53.0(encoding@0.1.13))(pdf-parse@1.1.1)(redis@4.6.14)(ws@8.17.1) - openai: 4.53.0(encoding@0.1.13) - lazy-ass@1.6.0: {} lazy-universal-dotenv@4.0.0: @@ -28146,7 +28049,7 @@ snapshots: vue-component-type-helpers@2.0.19: {} - vue-component-type-helpers@2.1.2: {} + vue-component-type-helpers@2.1.4: {} vue-demi@0.14.5(vue@3.4.21(typescript@5.5.2)): dependencies: diff --git a/scripts/ensure-zx.mjs b/scripts/ensure-zx.mjs new file mode 100644 index 0000000000..61f3265bfb --- /dev/null +++ b/scripts/ensure-zx.mjs @@ -0,0 +1,17 @@ +import { accessSync, constants } from 'node:fs'; +import { execSync } from 'node:child_process'; + +const ZX_PATH = 'node_modules/.bin/zx'; + +if (!zxExists()) { + execSync('pnpm --frozen-lockfile --filter n8n-monorepo install', { stdio: 'inherit' }); +} + +function zxExists() { + try { + accessSync(ZX_PATH, constants.F_OK); + return true; + } catch { + return false; + } +} diff --git a/scripts/reset.mjs b/scripts/reset.mjs new file mode 100644 index 0000000000..48aecf761b --- /dev/null +++ b/scripts/reset.mjs @@ -0,0 +1,30 @@ +// Resets the repository by deleting all untracked files except for few exceptions. +import { $, echo, fs } from 'zx'; + +$.verbose = true; +process.env.FORCE_COLOR = '1'; + +const excludePatterns = ['/.vscode/', '/.idea/', '.env']; +const excludeFlags = excludePatterns.map((exclude) => ['-e', exclude]).flat(); + +echo( + `This will delete all untracked files except for those matching the following patterns: ${excludePatterns.map((x) => `"${x}"`).join(', ')}.`, +); + +const answer = await question('❓ Do you want to continue? (y/n) '); + +if (!['y', 'Y', ''].includes(answer)) { + echo('Aborting...'); + process.exit(0); +} + +echo('🧹 Cleaning untracked files...'); +await $({ verbose: false })`git clean -fxd ${excludeFlags}`; +// In case node_modules is not removed by git clean +fs.removeSync('node_modules'); + +echo('⏬ Running pnpm install...'); +await $`pnpm install`; + +echo('🏗️ Running pnpm build...'); +await $`pnpm build`; From d5d7b24f55091b3da0f42de4b044f7908674ba7a Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:57:49 +0300 Subject: [PATCH 039/259] feat(benchmark): Add benchmark scenario for binary files (#10648) --- .../scenarios/binaryData/binaryData.json | 67 +++++++++++++++++++ .../binaryData/binaryData.manifest.json | 7 ++ .../scenarios/binaryData/binaryData.script.js | 22 ++++++ .../benchmark/src/testExecution/k6Executor.ts | 3 +- 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 packages/@n8n/benchmark/scenarios/binaryData/binaryData.json create mode 100644 packages/@n8n/benchmark/scenarios/binaryData/binaryData.manifest.json create mode 100644 packages/@n8n/benchmark/scenarios/binaryData/binaryData.script.js diff --git a/packages/@n8n/benchmark/scenarios/binaryData/binaryData.json b/packages/@n8n/benchmark/scenarios/binaryData/binaryData.json new file mode 100644 index 0000000000..a74b1c2d38 --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/binaryData/binaryData.json @@ -0,0 +1,67 @@ +{ + "createdAt": "2024-09-03T11:51:56.540Z", + "updatedAt": "2024-09-03T12:22:21.000Z", + "name": "Binary Data", + "active": true, + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "binary-files-benchmark", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [0, 0], + "id": "bfe19f12-3655-440f-be5c-8d71665c6353", + "name": "Webhook", + "webhookId": "109d7b13-93ad-42b0-a9ce-ca49e1817b35" + }, + { + "parameters": { "respondWith": "binary", "options": {} }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [740, 0], + "id": "cd957c9b-6b7a-4423-aac3-6df4d8bb571e", + "name": "Respond to Webhook" + }, + { + "parameters": { + "operation": "write", + "fileName": "=file-{{ Date.now() }}-{{ Math.random() }}.js", + "dataPropertyName": "file", + "options": {} + }, + "type": "n8n-nodes-base.readWriteFile", + "typeVersion": 1, + "position": [260, 0], + "id": "f2ce4709-7697-4bc6-8eca-6c222485297a", + "name": "Write File to Disk" + }, + { + "parameters": { "fileSelector": "={{ $json.fileName }}", "options": {} }, + "type": "n8n-nodes-base.readWriteFile", + "typeVersion": 1, + "position": [500, 0], + "id": "198e8a6c-81a3-4b34-b099-501961a02006", + "name": "Read File from Disk" + } + ], + "connections": { + "Webhook": { "main": [[{ "node": "Write File to Disk", "type": "main", "index": 0 }]] }, + "Write File to Disk": { + "main": [[{ "node": "Read File from Disk", "type": "main", "index": 0 }]] + }, + "Read File from Disk": { + "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] + } + }, + "settings": { "executionOrder": "v1" }, + "staticData": null, + "meta": null, + "pinData": {}, + "versionId": "8dd197c0-d1ea-43c3-9f88-9d11e7b081a0", + "triggerCount": 1, + "tags": [] +} diff --git a/packages/@n8n/benchmark/scenarios/binaryData/binaryData.manifest.json b/packages/@n8n/benchmark/scenarios/binaryData/binaryData.manifest.json new file mode 100644 index 0000000000..13f2a8a127 --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/binaryData/binaryData.manifest.json @@ -0,0 +1,7 @@ +{ + "$schema": "../scenario.schema.json", + "name": "BinaryData", + "description": "Send a binary file to a webhook, write it to FS, read it from FS and receive it back", + "scenarioData": { "workflowFiles": ["binaryData.json"] }, + "scriptPath": "binaryData.script.js" +} diff --git a/packages/@n8n/benchmark/scenarios/binaryData/binaryData.script.js b/packages/@n8n/benchmark/scenarios/binaryData/binaryData.script.js new file mode 100644 index 0000000000..95867245bf --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/binaryData/binaryData.script.js @@ -0,0 +1,22 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +const apiBaseUrl = __ENV.API_BASE_URL; + +const file = open(__ENV.SCRIPT_FILE_PATH, 'b'); +const filename = String(__ENV.SCRIPT_FILE_PATH).split('/').pop(); + +export default function () { + const data = { + filename, + file: http.file(file, filename, 'application/javascript'), + }; + + const res = http.post(`${apiBaseUrl}/webhook/binary-files-benchmark`, data); + + check(res, { + 'is status 200': (r) => r.status === 200, + 'has correct content type': (r) => + r.headers['Content-Type'] === 'application/javascript; charset=utf-8', + }); +} diff --git a/packages/@n8n/benchmark/src/testExecution/k6Executor.ts b/packages/@n8n/benchmark/src/testExecution/k6Executor.ts index a491f84a32..da272ce69f 100644 --- a/packages/@n8n/benchmark/src/testExecution/k6Executor.ts +++ b/packages/@n8n/benchmark/src/testExecution/k6Executor.ts @@ -60,6 +60,7 @@ export function handleSummary(data) { env: { API_BASE_URL: this.opts.n8nApiBaseUrl, K6_CLOUD_TOKEN: this.opts.k6ApiToken, + SCRIPT_FILE_PATH: augmentedTestScriptPath, }, })`${k6ExecutablePath} run ${flattedFlags} ${augmentedTestScriptPath}`; @@ -82,7 +83,7 @@ export function handleSummary(data) { const augmentedTestScript = `${testScript}\n\n${summaryScript}`; - const tempFilePath = tmpfile(`${scenarioRunName}.ts`, augmentedTestScript); + const tempFilePath = tmpfile(`${scenarioRunName}.js`, augmentedTestScript); return tempFilePath; } From 35e6a87cbae8bf255a45d43ab72b7f8083f0acfa Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:09:37 +0300 Subject: [PATCH 040/259] feat(benchmark): Add scenario for expressions with Set node (#10647) --- .../setNodeExpressions.json | 91 +++++++++++++++++++ .../setNodeExpressions.manifest.json | 7 ++ .../setNodeExpressions.script.js | 11 +++ 3 files changed, 109 insertions(+) create mode 100644 packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.json create mode 100644 packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.manifest.json create mode 100644 packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.script.js diff --git a/packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.json b/packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.json new file mode 100644 index 0000000000..fabb15a5a7 --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.json @@ -0,0 +1,91 @@ +{ + "createdAt": "2024-09-03T11:30:26.333Z", + "updatedAt": "2024-09-03T11:42:52.000Z", + "name": "Set Node Expressions", + "active": false, + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "set-expressions-benchmark", + "responseMode": "responseNode", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [40, 0], + "id": "5babc228-2b89-48cb-8337-28416e867874", + "name": "Webhook", + "webhookId": "f6f1750d-b734-496f-afe8-26e8e393ca87" + }, + { + "parameters": { "respondWith": "allIncomingItems", "options": {} }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [640, 0], + "id": "4146a3fb-403c-4cfc-9d38-8af4d16a8440", + "name": "Respond to Webhook" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "48c46098-f411-41f7-8f0a-1da372340a4e", + "name": "oneToOneCopy", + "value": "={{ $json.headers.host }}", + "type": "string" + }, + { + "id": "5d90808b-1c1a-4065-ac51-6d61bd03e564", + "name": "={{ $json.headers['user-agent'].slice(0, 4) }}", + "value": "Set key with expression", + "type": "string" + }, + { + "id": "8a74ac24-1f43-43ba-969d-87bfd2f401ce", + "name": "Multiple variables", + "value": "={{ $json.executionMode + ' ' + $json.webhookUrl }}", + "type": "string" + }, + { + "id": "93eba201-79d9-4305-a246-f9c8ec50ebab", + "name": "Static value", + "value": 42, + "type": "number" + }, + { + "id": "0470a712-c795-44ab-9dcc-05a3f67698bb", + "name": "Object", + "value": "={{ $json.headers }}", + "type": "object" + }, + { + "id": "eb671167-da14-4b55-8eea-31ab7bedae10", + "name": "Array", + "value": "={{ Object.values($json.headers) }}", + "type": "array" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [360, 0], + "id": "0cb5e82d-f61e-4d91-8fa9-365e382a4d75", + "name": "Edit Fields" + } + ], + "connections": { + "Webhook": { "main": [[{ "node": "Edit Fields", "type": "main", "index": 0 }]] }, + "Edit Fields": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] } + }, + "settings": { "executionOrder": "v1" }, + "staticData": null, + "meta": null, + "pinData": {}, + "versionId": "04fd543e-3923-4092-8c2b-2b4262ccbb38", + "triggerCount": 0, + "tags": [] +} diff --git a/packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.manifest.json b/packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.manifest.json new file mode 100644 index 0000000000..850120aec1 --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.manifest.json @@ -0,0 +1,7 @@ +{ + "$schema": "../scenario.schema.json", + "name": "SetNodeExpressions", + "description": "Expressions in a Set node", + "scenarioData": { "workflowFiles": ["setNodeExpressions.json"] }, + "scriptPath": "setNodeExpressions.script.js" +} diff --git a/packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.script.js b/packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.script.js new file mode 100644 index 0000000000..4bea17eb9f --- /dev/null +++ b/packages/@n8n/benchmark/scenarios/setNodeExpressions/setNodeExpressions.script.js @@ -0,0 +1,11 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +const apiBaseUrl = __ENV.API_BASE_URL; + +export default function () { + const res = http.post(`${apiBaseUrl}/webhook/set-expressions-benchmark`, {}); + check(res, { + 'is status 200': (r) => r.status === 200, + }); +} From 2ea2bfe762c02047e522f28dd97f197735b3fb46 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:52:12 +0300 Subject: [PATCH 041/259] feat: Reintroduce collaboration feature (#10602) --- .../__tests__/collaboration.state.test.ts | 96 +++++++++ .../collaboration/collaboration.message.ts | 35 ++++ .../collaboration/collaboration.service.ts | 120 +++++++++++ .../src/collaboration/collaboration.state.ts | 110 ++++++++++ .../src/collaboration/collaboration.types.ts | 7 + packages/cli/src/interfaces.ts | 21 +- .../src/push/__tests__/websocket.push.test.ts | 65 +++++- packages/cli/src/push/abstract.push.ts | 38 +++- packages/cli/src/push/index.ts | 17 +- packages/cli/src/push/sse.push.ts | 5 +- packages/cli/src/push/types.ts | 7 + packages/cli/src/push/websocket.push.ts | 32 ++- packages/cli/src/server.ts | 14 +- .../collaboration.service.test.ts | 188 ++++++++++++++++++ packages/editor-ui/src/Interface.ts | 11 + .../MainHeader/CollaborationPane.vue | 82 ++++++++ .../components/MainHeader/WorkflowDetails.vue | 2 + .../__tests__/CollaborationPane.test.ts | 102 ++++++++++ .../src/composables/useBeforeUnload.ts | 24 ++- packages/editor-ui/src/constants.ts | 1 + .../src/stores/collaboration.store.ts | 86 ++++++++ packages/editor-ui/src/views/NodeView.v2.vue | 6 + 22 files changed, 1046 insertions(+), 23 deletions(-) create mode 100644 packages/cli/src/collaboration/__tests__/collaboration.state.test.ts create mode 100644 packages/cli/src/collaboration/collaboration.message.ts create mode 100644 packages/cli/src/collaboration/collaboration.service.ts create mode 100644 packages/cli/src/collaboration/collaboration.state.ts create mode 100644 packages/cli/src/collaboration/collaboration.types.ts create mode 100644 packages/cli/test/integration/collaboration/collaboration.service.test.ts create mode 100644 packages/editor-ui/src/components/MainHeader/CollaborationPane.vue create mode 100644 packages/editor-ui/src/components/__tests__/CollaborationPane.test.ts create mode 100644 packages/editor-ui/src/stores/collaboration.store.ts diff --git a/packages/cli/src/collaboration/__tests__/collaboration.state.test.ts b/packages/cli/src/collaboration/__tests__/collaboration.state.test.ts new file mode 100644 index 0000000000..4435645d7a --- /dev/null +++ b/packages/cli/src/collaboration/__tests__/collaboration.state.test.ts @@ -0,0 +1,96 @@ +import { CollaborationState } from '../collaboration.state'; +import type { CacheService } from '@/services/cache/cache.service'; +import { mock } from 'jest-mock-extended'; + +const origDate = global.Date; + +const mockDateFactory = (currentDate: string) => { + return class CustomDate extends origDate { + constructor() { + super(currentDate); + } + } as DateConstructor; +}; + +describe('CollaborationState', () => { + let collaborationState: CollaborationState; + let mockCacheService: jest.Mocked; + + beforeEach(() => { + mockCacheService = mock(); + collaborationState = new CollaborationState(mockCacheService); + }); + + afterEach(() => { + global.Date = origDate; + }); + + const workflowId = 'workflow'; + + describe('addActiveWorkflowUser', () => { + it('should add workflow user with correct cache key and value', async () => { + // Arrange + global.Date = mockDateFactory('2023-01-01T00:00:00.000Z'); + + // Act + await collaborationState.addActiveWorkflowUser(workflowId, 'userId'); + + // Assert + expect(mockCacheService.setHash).toHaveBeenCalledWith('collaboration:workflow', { + userId: '2023-01-01T00:00:00.000Z', + }); + }); + }); + + describe('removeActiveWorkflowUser', () => { + it('should remove workflow user with correct cache key', async () => { + // Act + await collaborationState.removeActiveWorkflowUser(workflowId, 'userId'); + + // Assert + expect(mockCacheService.deleteFromHash).toHaveBeenCalledWith( + 'collaboration:workflow', + 'userId', + ); + }); + }); + + describe('getActiveWorkflowUsers', () => { + it('should get workflows with correct cache key', async () => { + // Act + const users = await collaborationState.getActiveWorkflowUsers(workflowId); + + // Assert + expect(mockCacheService.getHash).toHaveBeenCalledWith('collaboration:workflow'); + expect(users).toBeEmptyArray(); + }); + + it('should get workflow users that are not expired', async () => { + // Arrange + const nowMinus16Minutes = new Date(); + nowMinus16Minutes.setMinutes(nowMinus16Minutes.getMinutes() - 16); + const now = new Date().toISOString(); + + mockCacheService.getHash.mockResolvedValueOnce({ + expiredUserId: nowMinus16Minutes.toISOString(), + notExpiredUserId: now, + }); + + // Act + const users = await collaborationState.getActiveWorkflowUsers(workflowId); + + // Assert + expect(users).toEqual([ + { + lastSeen: now, + userId: 'notExpiredUserId', + }, + ]); + // removes expired users from the cache + expect(mockCacheService.deleteFromHash).toHaveBeenCalledWith( + 'collaboration:workflow', + 'expiredUserId', + ); + }); + }); +}); diff --git a/packages/cli/src/collaboration/collaboration.message.ts b/packages/cli/src/collaboration/collaboration.message.ts new file mode 100644 index 0000000000..e61a9fe9ac --- /dev/null +++ b/packages/cli/src/collaboration/collaboration.message.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +export type CollaborationMessage = WorkflowOpenedMessage | WorkflowClosedMessage; + +export const workflowOpenedMessageSchema = z + .object({ + type: z.literal('workflowOpened'), + workflowId: z.string().min(1), + }) + .strict(); + +export const workflowClosedMessageSchema = z + .object({ + type: z.literal('workflowClosed'), + workflowId: z.string().min(1), + }) + .strict(); + +export const workflowMessageSchema = z.discriminatedUnion('type', [ + workflowOpenedMessageSchema, + workflowClosedMessageSchema, +]); + +export type WorkflowOpenedMessage = z.infer; + +export type WorkflowClosedMessage = z.infer; + +export type WorkflowMessage = z.infer; + +/** + * Parses the given message and ensure it's of type WorkflowMessage + */ +export const parseWorkflowMessage = async (msg: unknown) => { + return await workflowMessageSchema.parseAsync(msg); +}; diff --git a/packages/cli/src/collaboration/collaboration.service.ts b/packages/cli/src/collaboration/collaboration.service.ts new file mode 100644 index 0000000000..775d3791fc --- /dev/null +++ b/packages/cli/src/collaboration/collaboration.service.ts @@ -0,0 +1,120 @@ +import type { Workflow } from 'n8n-workflow'; +import { Service } from 'typedi'; +import { Push } from '../push'; +import type { WorkflowClosedMessage, WorkflowOpenedMessage } from './collaboration.message'; +import { parseWorkflowMessage } from './collaboration.message'; +import type { IActiveWorkflowUsersChanged } from '../interfaces'; +import type { OnPushMessage } from '@/push/types'; +import { UserRepository } from '@/databases/repositories/user.repository'; +import type { User } from '@/databases/entities/user'; +import { CollaborationState } from '@/collaboration/collaboration.state'; +import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; +import { UserService } from '@/services/user.service'; +import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow'; + +/** + * Service for managing collaboration feature between users. E.g. keeping + * track of active users for a workflow. + */ +@Service() +export class CollaborationService { + constructor( + private readonly push: Push, + private readonly state: CollaborationState, + private readonly userRepository: UserRepository, + private readonly userService: UserService, + private readonly sharedWorkflowRepository: SharedWorkflowRepository, + ) {} + + init() { + this.push.on('message', async (event: OnPushMessage) => { + try { + await this.handleUserMessage(event.userId, event.msg); + } catch (error) { + ErrorReporterProxy.error( + new ApplicationError('Error handling CollaborationService push message', { + extra: { + msg: event.msg, + userId: event.userId, + }, + cause: error, + }), + ); + } + }); + } + + async handleUserMessage(userId: User['id'], msg: unknown) { + const workflowMessage = await parseWorkflowMessage(msg); + + if (workflowMessage.type === 'workflowOpened') { + await this.handleWorkflowOpened(userId, workflowMessage); + } else if (workflowMessage.type === 'workflowClosed') { + await this.handleWorkflowClosed(userId, workflowMessage); + } + } + + private async handleWorkflowOpened(userId: User['id'], msg: WorkflowOpenedMessage) { + const { workflowId } = msg; + + if (!(await this.hasUserAccessToWorkflow(userId, workflowId))) { + return; + } + + await this.state.addActiveWorkflowUser(workflowId, userId); + + await this.sendWorkflowUsersChangedMessage(workflowId); + } + + private async handleWorkflowClosed(userId: User['id'], msg: WorkflowClosedMessage) { + const { workflowId } = msg; + + if (!(await this.hasUserAccessToWorkflow(userId, workflowId))) { + return; + } + + await this.state.removeActiveWorkflowUser(workflowId, userId); + + await this.sendWorkflowUsersChangedMessage(workflowId); + } + + private async sendWorkflowUsersChangedMessage(workflowId: Workflow['id']) { + // We have already validated that all active workflow users + // have proper access to the workflow, so we don't need to validate it again + const activeWorkflowUsers = await this.state.getActiveWorkflowUsers(workflowId); + const workflowUserIds = activeWorkflowUsers.map((user) => user.userId); + + if (workflowUserIds.length === 0) { + return; + } + const users = await this.userRepository.getByIds(this.userRepository.manager, workflowUserIds); + + const msgData: IActiveWorkflowUsersChanged = { + workflowId, + activeUsers: await Promise.all( + users.map(async (user) => ({ + user: await this.userService.toPublic(user), + lastSeen: activeWorkflowUsers.find((activeUser) => activeUser.userId === user.id)! + .lastSeen, + })), + ), + }; + + this.push.sendToUsers('activeWorkflowUsersChanged', msgData, workflowUserIds); + } + + private async hasUserAccessToWorkflow(userId: User['id'], workflowId: Workflow['id']) { + const user = await this.userRepository.findOneBy({ + id: userId, + }); + if (!user) { + return false; + } + + const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ + 'workflow:read', + ]); + + return !!workflow; + } +} diff --git a/packages/cli/src/collaboration/collaboration.state.ts b/packages/cli/src/collaboration/collaboration.state.ts new file mode 100644 index 0000000000..d110bf20dd --- /dev/null +++ b/packages/cli/src/collaboration/collaboration.state.ts @@ -0,0 +1,110 @@ +import type { ActiveWorkflowUser } from '@/collaboration/collaboration.types'; +import { Time } from '@/constants'; +import type { Iso8601DateTimeString } from '@/interfaces'; +import { CacheService } from '@/services/cache/cache.service'; +import type { User } from '@/databases/entities/user'; +import { type Workflow } from 'n8n-workflow'; +import { Service } from 'typedi'; + +type WorkflowCacheHash = Record; + +/** + * State management for the collaboration service. Workflow active + * users are stored in a hash in the following format: + * { + * [workflowId] -> { + * [userId] -> lastSeenAsIso8601String + * } + * } + */ +@Service() +export class CollaborationState { + /** + * After how many minutes of inactivity a user should be removed + * as being an active user of a workflow. + */ + public readonly inactivityCleanUpTime = 15 * Time.minutes.toMilliseconds; + + constructor(private readonly cache: CacheService) {} + + /** + * Mark user active for given workflow + */ + async addActiveWorkflowUser(workflowId: Workflow['id'], userId: User['id']) { + const cacheKey = this.formWorkflowCacheKey(workflowId); + const cacheEntry: WorkflowCacheHash = { + [userId]: new Date().toISOString(), + }; + + await this.cache.setHash(cacheKey, cacheEntry); + } + + /** + * Remove user from workflow's active users + */ + async removeActiveWorkflowUser(workflowId: Workflow['id'], userId: User['id']) { + const cacheKey = this.formWorkflowCacheKey(workflowId); + + await this.cache.deleteFromHash(cacheKey, userId); + } + + async getActiveWorkflowUsers(workflowId: Workflow['id']): Promise { + const cacheKey = this.formWorkflowCacheKey(workflowId); + + const cacheValue = await this.cache.getHash(cacheKey); + if (!cacheValue) { + return []; + } + + const workflowActiveUsers = this.cacheHashToWorkflowActiveUsers(cacheValue); + const [expired, stillActive] = this.splitToExpiredAndStillActive(workflowActiveUsers); + + if (expired.length > 0) { + void this.removeExpiredUsersForWorkflow(workflowId, expired); + } + + return stillActive; + } + + private formWorkflowCacheKey(workflowId: Workflow['id']) { + return `collaboration:${workflowId}`; + } + + private splitToExpiredAndStillActive(workflowUsers: ActiveWorkflowUser[]) { + const expired: ActiveWorkflowUser[] = []; + const stillActive: ActiveWorkflowUser[] = []; + + for (const user of workflowUsers) { + if (this.hasUserExpired(user.lastSeen)) { + expired.push(user); + } else { + stillActive.push(user); + } + } + + return [expired, stillActive]; + } + + private async removeExpiredUsersForWorkflow( + workflowId: Workflow['id'], + expiredUsers: ActiveWorkflowUser[], + ) { + const cacheKey = this.formWorkflowCacheKey(workflowId); + await Promise.all( + expiredUsers.map(async (user) => await this.cache.deleteFromHash(cacheKey, user.userId)), + ); + } + + private cacheHashToWorkflowActiveUsers(workflowCacheEntry: WorkflowCacheHash) { + return Object.entries(workflowCacheEntry).map(([userId, lastSeen]) => ({ + userId, + lastSeen, + })); + } + + private hasUserExpired(lastSeenString: Iso8601DateTimeString) { + const expiryTime = new Date(lastSeenString).getTime() + this.inactivityCleanUpTime; + + return Date.now() > expiryTime; + } +} diff --git a/packages/cli/src/collaboration/collaboration.types.ts b/packages/cli/src/collaboration/collaboration.types.ts new file mode 100644 index 0000000000..d2a0591395 --- /dev/null +++ b/packages/cli/src/collaboration/collaboration.types.ts @@ -0,0 +1,7 @@ +import type { Iso8601DateTimeString } from '@/interfaces'; +import type { User } from '@/databases/entities/user'; + +export type ActiveWorkflowUser = { + userId: User['id']; + lastSeen: Iso8601DateTimeString; +}; diff --git a/packages/cli/src/interfaces.ts b/packages/cli/src/interfaces.ts index 78a597e5b3..8b44008261 100644 --- a/packages/cli/src/interfaces.ts +++ b/packages/cli/src/interfaces.ts @@ -290,7 +290,13 @@ export type IPushData = | PushDataWorkerStatusMessage | PushDataWorkflowActivated | PushDataWorkflowDeactivated - | PushDataWorkflowFailedToActivate; + | PushDataWorkflowFailedToActivate + | PushDataActiveWorkflowUsersChanged; + +type PushDataActiveWorkflowUsersChanged = { + data: IActiveWorkflowUsersChanged; + type: 'activeWorkflowUsersChanged'; +}; type PushDataWorkflowFailedToActivate = { data: IWorkflowFailedToActivate; @@ -362,6 +368,19 @@ export type PushDataNodeDescriptionUpdated = { type: 'nodeDescriptionUpdated'; }; +/** DateTime in the Iso8601 format, e.g. 2024-10-31T00:00:00.123Z */ +export type Iso8601DateTimeString = string; + +export interface IActiveWorkflowUser { + user: PublicUser; + lastSeen: Iso8601DateTimeString; +} + +export interface IActiveWorkflowUsersChanged { + workflowId: Workflow['id']; + activeUsers: IActiveWorkflowUser[]; +} + export interface IActiveWorkflowAdded { workflowId: Workflow['id']; } diff --git a/packages/cli/src/push/__tests__/websocket.push.test.ts b/packages/cli/src/push/__tests__/websocket.push.test.ts index b1202b3a13..158264827c 100644 --- a/packages/cli/src/push/__tests__/websocket.push.test.ts +++ b/packages/cli/src/push/__tests__/websocket.push.test.ts @@ -7,6 +7,7 @@ import { Logger } from '@/logger'; import type { PushDataExecutionRecovered } from '@/interfaces'; import { mockInstance } from '@test/mocking'; +import type { User } from '@/databases/entities/user'; jest.useFakeTimers(); @@ -27,6 +28,7 @@ const createMockWebSocket = () => new MockWebSocket() as unknown as jest.Mocked< describe('WebSocketPush', () => { const pushRef1 = 'test-session1'; const pushRef2 = 'test-session2'; + const userId: User['id'] = 'test-user'; mockInstance(Logger); const webSocketPush = Container.get(WebSocketPush); @@ -35,27 +37,31 @@ describe('WebSocketPush', () => { beforeEach(() => { jest.resetAllMocks(); + mockWebSocket1.removeAllListeners(); + mockWebSocket2.removeAllListeners(); }); it('can add a connection', () => { - webSocketPush.add(pushRef1, mockWebSocket1); + webSocketPush.add(pushRef1, userId, mockWebSocket1); expect(mockWebSocket1.listenerCount('close')).toBe(1); expect(mockWebSocket1.listenerCount('pong')).toBe(1); + expect(mockWebSocket1.listenerCount('message')).toBe(1); }); it('closes a connection', () => { - webSocketPush.add(pushRef1, mockWebSocket1); + webSocketPush.add(pushRef1, userId, mockWebSocket1); mockWebSocket1.emit('close'); + expect(mockWebSocket1.listenerCount('message')).toBe(0); expect(mockWebSocket1.listenerCount('close')).toBe(0); expect(mockWebSocket1.listenerCount('pong')).toBe(0); }); it('sends data to one connection', () => { - webSocketPush.add(pushRef1, mockWebSocket1); - webSocketPush.add(pushRef2, mockWebSocket2); + webSocketPush.add(pushRef1, userId, mockWebSocket1); + webSocketPush.add(pushRef2, userId, mockWebSocket2); const data: PushDataExecutionRecovered = { type: 'executionRecovered', data: { @@ -80,8 +86,8 @@ describe('WebSocketPush', () => { }); it('sends data to all connections', () => { - webSocketPush.add(pushRef1, mockWebSocket1); - webSocketPush.add(pushRef2, mockWebSocket2); + webSocketPush.add(pushRef1, userId, mockWebSocket1); + webSocketPush.add(pushRef2, userId, mockWebSocket2); const data: PushDataExecutionRecovered = { type: 'executionRecovered', data: { @@ -105,12 +111,55 @@ describe('WebSocketPush', () => { }); it('pings all connections', () => { - webSocketPush.add(pushRef1, mockWebSocket1); - webSocketPush.add(pushRef2, mockWebSocket2); + webSocketPush.add(pushRef1, userId, mockWebSocket1); + webSocketPush.add(pushRef2, userId, mockWebSocket2); jest.runOnlyPendingTimers(); expect(mockWebSocket1.ping).toHaveBeenCalled(); expect(mockWebSocket2.ping).toHaveBeenCalled(); }); + + it('sends data to all users connections', () => { + webSocketPush.add(pushRef1, userId, mockWebSocket1); + webSocketPush.add(pushRef2, userId, mockWebSocket2); + const data: PushDataExecutionRecovered = { + type: 'executionRecovered', + data: { + executionId: 'test-execution-id', + }, + }; + + webSocketPush.sendToUsers('executionRecovered', data, [userId]); + + const expectedMsg = JSON.stringify({ + type: 'executionRecovered', + data: { + type: 'executionRecovered', + data: { + executionId: 'test-execution-id', + }, + }, + }); + expect(mockWebSocket1.send).toHaveBeenCalledWith(expectedMsg); + expect(mockWebSocket2.send).toHaveBeenCalledWith(expectedMsg); + }); + + it('emits message event when connection receives data', () => { + const mockOnMessageReceived = jest.fn(); + webSocketPush.on('message', mockOnMessageReceived); + webSocketPush.add(pushRef1, userId, mockWebSocket1); + webSocketPush.add(pushRef2, userId, mockWebSocket2); + + const data = { test: 'data' }; + const buffer = Buffer.from(JSON.stringify(data)); + + mockWebSocket1.emit('message', buffer); + + expect(mockOnMessageReceived).toHaveBeenCalledWith({ + msg: data, + pushRef: pushRef1, + userId, + }); + }); }); diff --git a/packages/cli/src/push/abstract.push.ts b/packages/cli/src/push/abstract.push.ts index 8859540330..20b43283de 100644 --- a/packages/cli/src/push/abstract.push.ts +++ b/packages/cli/src/push/abstract.push.ts @@ -1,6 +1,13 @@ import { assert, jsonStringify } from 'n8n-workflow'; import type { IPushDataType } from '@/interfaces'; import type { Logger } from '@/logger'; +import type { User } from '@/databases/entities/user'; +import { TypedEmitter } from '@/typed-emitter'; +import type { OnPushMessage } from '@/push/types'; + +export interface AbstractPushEvents { + message: OnPushMessage; +} /** * Abstract class for two-way push communication. @@ -8,16 +15,20 @@ import type { Logger } from '@/logger'; * * @emits message when a message is received from a client */ -export abstract class AbstractPush { +export abstract class AbstractPush extends TypedEmitter { protected connections: Record = {}; + protected userIdByPushRef: Record = {}; + protected abstract close(connection: T): void; protected abstract sendToOneConnection(connection: T, data: string): void; - constructor(protected readonly logger: Logger) {} + constructor(protected readonly logger: Logger) { + super(); + } - protected add(pushRef: string, connection: T) { - const { connections } = this; + protected add(pushRef: string, userId: User['id'], connection: T) { + const { connections, userIdByPushRef } = this; this.logger.debug('Add editor-UI session', { pushRef }); const existingConnection = connections[pushRef]; @@ -28,6 +39,15 @@ export abstract class AbstractPush { } connections[pushRef] = connection; + userIdByPushRef[pushRef] = userId; + } + + protected onMessageReceived(pushRef: string, msg: unknown) { + this.logger.debug('Received message from editor-UI', { pushRef, msg }); + + const userId = this.userIdByPushRef[pushRef]; + + this.emit('message', { pushRef, userId, msg }); } protected remove(pushRef?: string) { @@ -36,6 +56,7 @@ export abstract class AbstractPush { this.logger.debug('Removed editor-UI session', { pushRef }); delete this.connections[pushRef]; + delete this.userIdByPushRef[pushRef]; } private sendTo(type: IPushDataType, data: unknown, pushRefs: string[]) { @@ -66,6 +87,15 @@ export abstract class AbstractPush { this.sendTo(type, data, [pushRef]); } + sendToUsers(type: IPushDataType, data: unknown, userIds: Array) { + const { connections } = this; + const userPushRefs = Object.keys(connections).filter((pushRef) => + userIds.includes(this.userIdByPushRef[pushRef]), + ); + + this.sendTo(type, data, userPushRefs); + } + closeAllConnections() { for (const pushRef in this.connections) { // Signal the connection that we want to close it. diff --git a/packages/cli/src/push/index.ts b/packages/cli/src/push/index.ts index defe070196..b01e085cec 100644 --- a/packages/cli/src/push/index.ts +++ b/packages/cli/src/push/index.ts @@ -15,11 +15,13 @@ import { OrchestrationService } from '@/services/orchestration.service'; import { SSEPush } from './sse.push'; import { WebSocketPush } from './websocket.push'; -import type { PushResponse, SSEPushRequest, WebSocketPushRequest } from './types'; +import type { OnPushMessage, PushResponse, SSEPushRequest, WebSocketPushRequest } from './types'; import { TypedEmitter } from '@/typed-emitter'; +import type { User } from '@/databases/entities/user'; type PushEvents = { editorUiConnected: string; + message: OnPushMessage; }; const useWebSockets = config.getEnv('push.backend') === 'websocket'; @@ -33,16 +35,21 @@ const useWebSockets = config.getEnv('push.backend') === 'websocket'; */ @Service() export class Push extends TypedEmitter { + public isBidirectional = useWebSockets; + private backend = useWebSockets ? Container.get(WebSocketPush) : Container.get(SSEPush); constructor(private readonly orchestrationService: OrchestrationService) { super(); + + if (useWebSockets) this.backend.on('message', (msg) => this.emit('message', msg)); } handleRequest(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) { const { ws, query: { pushRef }, + user, } = req; if (!pushRef) { @@ -55,9 +62,9 @@ export class Push extends TypedEmitter { } if (req.ws) { - (this.backend as WebSocketPush).add(pushRef, req.ws); + (this.backend as WebSocketPush).add(pushRef, user.id, req.ws); } else if (!useWebSockets) { - (this.backend as SSEPush).add(pushRef, { req, res }); + (this.backend as SSEPush).add(pushRef, user.id, { req, res }); } else { res.status(401).send('Unauthorized'); return; @@ -90,6 +97,10 @@ export class Push extends TypedEmitter { return this.backend; } + sendToUsers(type: IPushDataType, data: unknown, userIds: Array) { + this.backend.sendToUsers(type, data, userIds); + } + @OnShutdown() onShutdown() { this.backend.closeAllConnections(); diff --git a/packages/cli/src/push/sse.push.ts b/packages/cli/src/push/sse.push.ts index 38779ed730..e78134eac3 100644 --- a/packages/cli/src/push/sse.push.ts +++ b/packages/cli/src/push/sse.push.ts @@ -5,6 +5,7 @@ import { Logger } from '@/logger'; import { AbstractPush } from './abstract.push'; import type { PushRequest, PushResponse } from './types'; +import type { User } from '@/databases/entities/user'; type Connection = { req: PushRequest; res: PushResponse }; @@ -22,8 +23,8 @@ export class SSEPush extends AbstractPush { }); } - add(pushRef: string, connection: Connection) { - super.add(pushRef, connection); + add(pushRef: string, userId: User['id'], connection: Connection) { + super.add(pushRef, userId, connection); this.channel.addClient(connection.req, connection.res); } diff --git a/packages/cli/src/push/types.ts b/packages/cli/src/push/types.ts index a12e582213..0a9d6f5b6c 100644 --- a/packages/cli/src/push/types.ts +++ b/packages/cli/src/push/types.ts @@ -2,6 +2,7 @@ import type { Response } from 'express'; import type { WebSocket } from 'ws'; import type { AuthenticatedRequest } from '@/requests'; +import type { User } from '@/databases/entities/user'; // TODO: move all push related types here @@ -11,3 +12,9 @@ export type SSEPushRequest = PushRequest & { ws: undefined }; export type WebSocketPushRequest = PushRequest & { ws: WebSocket }; export type PushResponse = Response & { req: PushRequest }; + +export interface OnPushMessage { + pushRef: string; + userId: User['id']; + msg: unknown; +} diff --git a/packages/cli/src/push/websocket.push.ts b/packages/cli/src/push/websocket.push.ts index 733eebdc60..79ef00fffb 100644 --- a/packages/cli/src/push/websocket.push.ts +++ b/packages/cli/src/push/websocket.push.ts @@ -2,6 +2,8 @@ import type WebSocket from 'ws'; import { Service } from 'typedi'; import { Logger } from '@/logger'; import { AbstractPush } from './abstract.push'; +import type { User } from '@/databases/entities/user'; +import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow'; function heartbeat(this: WebSocket) { this.isAlive = true; @@ -16,17 +18,43 @@ export class WebSocketPush extends AbstractPush { setInterval(() => this.pingAll(), 60 * 1000); } - add(pushRef: string, connection: WebSocket) { + add(pushRef: string, userId: User['id'], connection: WebSocket) { connection.isAlive = true; connection.on('pong', heartbeat); - super.add(pushRef, connection); + super.add(pushRef, userId, connection); + + const onMessage = (data: WebSocket.RawData) => { + try { + const buffer = Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data); + + this.onMessageReceived(pushRef, JSON.parse(buffer.toString('utf8'))); + } catch (error) { + ErrorReporterProxy.error( + new ApplicationError('Error parsing push message', { + extra: { + userId, + data, + }, + cause: error, + }), + ); + this.logger.error("Couldn't parse message from editor-UI", { + error: error as unknown, + pushRef, + data, + }); + } + }; // Makes sure to remove the session if the connection is closed connection.once('close', () => { connection.off('pong', heartbeat); + connection.off('message', onMessage); this.remove(pushRef); }); + + connection.on('message', onMessage); } protected close(connection: WebSocket): void { diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 9176402a88..82e12b0386 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -27,7 +27,7 @@ import type { ICredentialsOverwrite } from '@/interfaces'; import { CredentialsOverwrites } from '@/credentials-overwrites'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import * as ResponseHelper from '@/response-helper'; -import { setupPushServer, setupPushHandler } from '@/push'; +import { setupPushServer, setupPushHandler, Push } from '@/push'; import { isLdapEnabled } from '@/ldap/helpers.ee'; import { AbstractServer } from '@/abstract-server'; import { PostHogClient } from '@/posthog'; @@ -212,6 +212,18 @@ export class Server extends AbstractServer { const { restEndpoint, app } = this; setupPushHandler(restEndpoint, app); + const push = Container.get(Push); + if (push.isBidirectional) { + const { CollaborationService } = await import('@/collaboration/collaboration.service'); + + const collaborationService = Container.get(CollaborationService); + collaborationService.init(); + } else { + this.logger.warn( + 'Collaboration features are disabled because push is configured unidirectional. Use N8N_PUSH_BACKEND=websocket environment variable to enable them.', + ); + } + if (config.getEnv('executions.mode') === 'queue') { const { ScalingService } = await import('@/scaling/scaling.service'); await Container.get(ScalingService).setupQueue(); diff --git a/packages/cli/test/integration/collaboration/collaboration.service.test.ts b/packages/cli/test/integration/collaboration/collaboration.service.test.ts new file mode 100644 index 0000000000..81b00dc866 --- /dev/null +++ b/packages/cli/test/integration/collaboration/collaboration.service.test.ts @@ -0,0 +1,188 @@ +import { CollaborationService } from '@/collaboration/collaboration.service'; +import { Push } from '@/push'; +import { CacheService } from '@/services/cache/cache.service'; +import { mock } from 'jest-mock-extended'; +import * as testDb from '../shared/test-db'; +import Container from 'typedi'; +import type { User } from '@/databases/entities/user'; +import { createMember, createOwner } from '@test-integration/db/users'; +import type { + WorkflowClosedMessage, + WorkflowOpenedMessage, +} from '@/collaboration/collaboration.message'; +import { createWorkflow, shareWorkflowWithUsers } from '@test-integration/db/workflows'; +import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; +import { mockInstance } from '@test/mocking'; +import { UserService } from '@/services/user.service'; + +describe('CollaborationService', () => { + mockInstance(Push, new Push(mock())); + let pushService: Push; + let collaborationService: CollaborationService; + let owner: User; + let memberWithoutAccess: User; + let memberWithAccess: User; + let workflow: WorkflowEntity; + let userService: UserService; + let cacheService: CacheService; + + beforeAll(async () => { + await testDb.init(); + + pushService = Container.get(Push); + collaborationService = Container.get(CollaborationService); + userService = Container.get(UserService); + cacheService = Container.get(CacheService); + + await cacheService.init(); + + [owner, memberWithAccess, memberWithoutAccess] = await Promise.all([ + createOwner(), + createMember(), + createMember(), + ]); + workflow = await createWorkflow({}, owner); + await shareWorkflowWithUsers(workflow, [memberWithAccess]); + }); + + afterEach(async () => { + jest.resetAllMocks(); + await cacheService.reset(); + }); + + const sendWorkflowOpenedMessage = async (workflowId: string, userId: string) => { + const openMessage: WorkflowOpenedMessage = { + type: 'workflowOpened', + workflowId, + }; + + return await collaborationService.handleUserMessage(userId, openMessage); + }; + + const sendWorkflowClosedMessage = async (workflowId: string, userId: string) => { + const openMessage: WorkflowClosedMessage = { + type: 'workflowClosed', + workflowId, + }; + + return await collaborationService.handleUserMessage(userId, openMessage); + }; + + describe('workflow opened message', () => { + it('should emit activeWorkflowUsersChanged after workflowOpened', async () => { + // Arrange + const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers'); + + // Act + await sendWorkflowOpenedMessage(workflow.id, owner.id); + await sendWorkflowOpenedMessage(workflow.id, memberWithAccess.id); + + // Assert + expect(sendToUsersSpy).toHaveBeenNthCalledWith( + 1, + 'activeWorkflowUsersChanged', + { + activeUsers: [ + { + lastSeen: expect.any(String), + user: { + ...(await userService.toPublic(owner)), + isPending: false, + }, + }, + ], + workflowId: workflow.id, + }, + [owner.id], + ); + expect(sendToUsersSpy).toHaveBeenNthCalledWith( + 2, + 'activeWorkflowUsersChanged', + { + activeUsers: expect.arrayContaining([ + expect.objectContaining({ + lastSeen: expect.any(String), + user: expect.objectContaining({ + id: owner.id, + }), + }), + expect.objectContaining({ + lastSeen: expect.any(String), + user: expect.objectContaining({ + id: memberWithAccess.id, + }), + }), + ]), + workflowId: workflow.id, + }, + [owner.id, memberWithAccess.id], + ); + }); + + it("should not emit activeWorkflowUsersChanged if user don't have access to the workflow", async () => { + const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers'); + + // Act + await sendWorkflowOpenedMessage(workflow.id, memberWithoutAccess.id); + + // Assert + expect(sendToUsersSpy).not.toHaveBeenCalled(); + }); + }); + + describe('workflow closed message', () => { + it('should not emit activeWorkflowUsersChanged after workflowClosed when there are no active users', async () => { + // Arrange + const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers'); + await sendWorkflowOpenedMessage(workflow.id, owner.id); + sendToUsersSpy.mockClear(); + + // Act + await sendWorkflowClosedMessage(workflow.id, owner.id); + + // Assert + expect(sendToUsersSpy).not.toHaveBeenCalled(); + }); + + it('should emit activeWorkflowUsersChanged after workflowClosed when there are active users', async () => { + // Arrange + const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers'); + await sendWorkflowOpenedMessage(workflow.id, owner.id); + await sendWorkflowOpenedMessage(workflow.id, memberWithAccess.id); + sendToUsersSpy.mockClear(); + + // Act + await sendWorkflowClosedMessage(workflow.id, owner.id); + + // Assert + expect(sendToUsersSpy).toHaveBeenCalledWith( + 'activeWorkflowUsersChanged', + { + activeUsers: expect.arrayContaining([ + expect.objectContaining({ + lastSeen: expect.any(String), + user: expect.objectContaining({ + id: memberWithAccess.id, + }), + }), + ]), + workflowId: workflow.id, + }, + [memberWithAccess.id], + ); + }); + + it("should not emit activeWorkflowUsersChanged if user don't have access to the workflow", async () => { + // Arrange + const sendToUsersSpy = jest.spyOn(pushService, 'sendToUsers'); + await sendWorkflowOpenedMessage(workflow.id, owner.id); + sendToUsersSpy.mockClear(); + + // Act + await sendWorkflowClosedMessage(workflow.id, memberWithoutAccess.id); + + // Assert + expect(sendToUsersSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index f7ad20f9ea..0f6f7a927e 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -422,6 +422,16 @@ export interface IExecutionDeleteFilter { ids?: string[]; } +export type PushDataUsersForWorkflow = { + workflowId: string; + activeUsers: Array<{ user: IUser; lastSeen: string }>; +}; + +type PushDataWorkflowUsersChanged = { + data: PushDataUsersForWorkflow; + type: 'activeWorkflowUsersChanged'; +}; + export type IPushData = | PushDataExecutionFinished | PushDataExecutionStarted @@ -436,6 +446,7 @@ export type IPushData = | PushDataWorkerStatusMessage | PushDataActiveWorkflowAdded | PushDataActiveWorkflowRemoved + | PushDataWorkflowUsersChanged | PushDataWorkflowFailedToActivate; export type PushDataActiveWorkflowAdded = { diff --git a/packages/editor-ui/src/components/MainHeader/CollaborationPane.vue b/packages/editor-ui/src/components/MainHeader/CollaborationPane.vue new file mode 100644 index 0000000000..daabc0a178 --- /dev/null +++ b/packages/editor-ui/src/components/MainHeader/CollaborationPane.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 28cca7736f..de687771d8 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -22,6 +22,7 @@ import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue'; import InlineTextEdit from '@/components/InlineTextEdit.vue'; import BreakpointsObserver from '@/components/BreakpointsObserver.vue'; import WorkflowHistoryButton from '@/components/MainHeader/WorkflowHistoryButton.vue'; +import CollaborationPane from '@/components/MainHeader/CollaborationPane.vue'; import { useRootStore } from '@/stores/root.store'; import { useSettingsStore } from '@/stores/settings.store'; @@ -675,6 +676,7 @@ function showCreateWorkflowSuccessToast(id?: string) {
+ { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should show only current workflow users', async () => { + const { getByTestId, queryByTestId } = renderComponent(); + await waitAllPromises(); + + expect(getByTestId('collaboration-pane')).toBeInTheDocument(); + expect(getByTestId('user-stack-avatars')).toBeInTheDocument(); + expect(getByTestId(`user-stack-avatar-${OWNER_USER.id}`)).toBeInTheDocument(); + expect(getByTestId(`user-stack-avatar-${MEMBER_USER.id}`)).toBeInTheDocument(); + expect(queryByTestId(`user-stack-avatar-${MEMBER_USER_2.id}`)).toBeNull(); + }); + + it('should always render owner first in the list', async () => { + const { getByTestId } = renderComponent(); + await waitAllPromises(); + const firstAvatar = getByTestId('user-stack-avatars').querySelector('.n8n-avatar'); + // Owner is second in the store but should be rendered first + expect(firstAvatar).toHaveAttribute('data-test-id', `user-stack-avatar-${OWNER_USER.id}`); + }); +}); diff --git a/packages/editor-ui/src/composables/useBeforeUnload.ts b/packages/editor-ui/src/composables/useBeforeUnload.ts index 5469c43ee8..20095a217a 100644 --- a/packages/editor-ui/src/composables/useBeforeUnload.ts +++ b/packages/editor-ui/src/composables/useBeforeUnload.ts @@ -1,9 +1,11 @@ import { useCanvasStore } from '@/stores/canvas.store'; import { useUIStore } from '@/stores/ui.store'; import { useI18n } from '@/composables/useI18n'; -import { computed } from 'vue'; -import { VIEWS } from '@/constants'; +import { computed, ref } from 'vue'; +import { TIME, VIEWS } from '@/constants'; import type { useRoute } from 'vue-router'; +import { useCollaborationStore } from '@/stores/collaboration.store'; +import { useWorkflowsStore } from '@/stores/workflows.store'; /** * Composable to handle the beforeunload event in canvas views. @@ -15,19 +17,31 @@ import type { useRoute } from 'vue-router'; export function useBeforeUnload({ route }: { route: ReturnType }) { const uiStore = useUIStore(); const canvasStore = useCanvasStore(); + const collaborationStore = useCollaborationStore(); + const workflowsStore = useWorkflowsStore(); const i18n = useI18n(); + const unloadTimeout = ref(null); const isDemoRoute = computed(() => route.name === VIEWS.DEMO); function onBeforeUnload(e: BeforeUnloadEvent) { if (isDemoRoute.value || window.preventNodeViewBeforeUnload) { return; } else if (uiStore.stateIsDirty) { + // A bit hacky solution to detecting users leaving the page after prompt: + // 1. Notify that workflow is closed straight away + collaborationStore.notifyWorkflowClosed(workflowsStore.workflowId); + // 2. If user decided to stay on the page we notify that the workflow is opened again + unloadTimeout.value = setTimeout(() => { + collaborationStore.notifyWorkflowOpened(workflowsStore.workflowId); + }, 5 * TIME.SECOND); + e.returnValue = true; //Gecko + IE return true; //Gecko + Webkit, Safari, Chrome etc. } else { canvasStore.startLoading(i18n.baseText('nodeView.redirecting')); + collaborationStore.notifyWorkflowClosed(workflowsStore.workflowId); return; } } @@ -37,6 +51,12 @@ export function useBeforeUnload({ route }: { route: ReturnType } function removeBeforeUnloadEventBindings() { + collaborationStore.notifyWorkflowClosed(workflowsStore.workflowId); + + if (unloadTimeout.value) { + clearTimeout(unloadTimeout.value); + } + window.removeEventListener('beforeunload', onBeforeUnload); } diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index f44046139e..9cdf0b87b9 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -639,6 +639,7 @@ export const enum STORES { CLOUD_PLAN = 'cloudPlan', RBAC = 'rbac', PUSH = 'push', + COLLABORATION = 'collaboration', ASSISTANT = 'assistant', BECOME_TEMPLATE_CREATOR = 'becomeTemplateCreator', PROJECTS = 'projects', diff --git a/packages/editor-ui/src/stores/collaboration.store.ts b/packages/editor-ui/src/stores/collaboration.store.ts new file mode 100644 index 0000000000..e9f4afe168 --- /dev/null +++ b/packages/editor-ui/src/stores/collaboration.store.ts @@ -0,0 +1,86 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { usePushConnectionStore } from '@/stores/pushConnection.store'; +import { STORES } from '@/constants'; +import type { IUser } from '@/Interface'; +import { useUsersStore } from '@/stores/users.store'; + +type ActiveUsersForWorkflows = { + [workflowId: string]: Array<{ user: IUser; lastSeen: string }>; +}; + +/** + * Store for tracking active users for workflows. I.e. to show + * who is collaboratively viewing/editing the workflow at the same time. + */ +export const useCollaborationStore = defineStore(STORES.COLLABORATION, () => { + const pushStore = usePushConnectionStore(); + const workflowStore = useWorkflowsStore(); + const usersStore = useUsersStore(); + + const usersForWorkflows = ref({}); + const pushStoreEventListenerRemovalFn = ref<(() => void) | null>(null); + + const getUsersForCurrentWorkflow = computed(() => { + return usersForWorkflows.value[workflowStore.workflowId] ?? []; + }); + + function initialize() { + if (pushStoreEventListenerRemovalFn.value) { + return; + } + + pushStoreEventListenerRemovalFn.value = pushStore.addEventListener((event) => { + if (event.type === 'activeWorkflowUsersChanged') { + const workflowId = event.data.workflowId; + usersForWorkflows.value[workflowId] = event.data.activeUsers; + } + }); + } + + function terminate() { + if (typeof pushStoreEventListenerRemovalFn.value === 'function') { + pushStoreEventListenerRemovalFn.value(); + pushStoreEventListenerRemovalFn.value = null; + } + } + + function workflowUsersUpdated(data: ActiveUsersForWorkflows) { + usersForWorkflows.value = data; + } + + function functionRemoveCurrentUserFromActiveUsers(workflowId: string) { + const workflowUsers = usersForWorkflows.value[workflowId]; + if (!workflowUsers) { + return; + } + + usersForWorkflows.value[workflowId] = workflowUsers.filter( + (activeUser) => activeUser.user.id !== usersStore.currentUserId, + ); + } + + function notifyWorkflowOpened(workflowId: string) { + pushStore.send({ + type: 'workflowOpened', + workflowId, + }); + } + + function notifyWorkflowClosed(workflowId: string) { + pushStore.send({ type: 'workflowClosed', workflowId }); + + functionRemoveCurrentUserFromActiveUsers(workflowId); + } + + return { + usersForWorkflows, + initialize, + terminate, + notifyWorkflowOpened, + notifyWorkflowClosed, + workflowUsersUpdated, + getUsersForCurrentWorkflow, + }; +}); diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index 5ce133bca9..4f5f16caa5 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -101,6 +101,7 @@ import { createEventBus } from 'n8n-design-system'; import type { PinDataSource } from '@/composables/usePinnedData'; import { useClipboard } from '@/composables/useClipboard'; import { useBeforeUnload } from '@/composables/useBeforeUnload'; +import { useCollaborationStore } from '@/stores/collaboration.store'; import { getResourcePermissions } from '@/permissions'; import NodeViewUnfinishedWorkflowMessage from '@/components/NodeViewUnfinishedWorkflowMessage.vue'; @@ -134,6 +135,7 @@ const credentialsStore = useCredentialsStore(); const environmentsStore = useEnvironmentsStore(); const externalSecretsStore = useExternalSecretsStore(); const rootStore = useRootStore(); +const collaborationStore = useCollaborationStore(); const executionsStore = useExecutionsStore(); const canvasStore = useCanvasStore(); const npsSurveyStore = useNpsSurveyStore(); @@ -338,6 +340,8 @@ async function initializeWorkspaceForExistingWorkflow(id: string) { } await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflow.value.homeProject); + + collaborationStore.notifyWorkflowOpened(id); } catch (error) { toast.showError(error, i18n.baseText('openWorkflow.workflowNotFoundError')); @@ -1456,6 +1460,7 @@ watch( onBeforeMount(() => { if (!isDemoRoute.value) { pushConnectionStore.pushConnect(); + collaborationStore.initialize(); } }); @@ -1509,6 +1514,7 @@ onBeforeUnmount(() => { onDeactivated(() => { removeBeforeUnloadEventBindings(); + collaborationStore.terminate(); }); From 36177b0943cf72bae3b0075453498dd1e41684d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 3 Sep 2024 17:58:26 +0200 Subject: [PATCH 042/259] fix(core): Declutter webhook insertion errors (#10650) --- packages/cli/src/webhooks/__tests__/webhook.service.test.ts | 4 ++-- packages/cli/src/webhooks/webhook.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/webhooks/__tests__/webhook.service.test.ts b/packages/cli/src/webhooks/__tests__/webhook.service.test.ts index 13c921c65d..6a970aa583 100644 --- a/packages/cli/src/webhooks/__tests__/webhook.service.test.ts +++ b/packages/cli/src/webhooks/__tests__/webhook.service.test.ts @@ -179,12 +179,12 @@ describe('WebhookService', () => { }); describe('createWebhook()', () => { - test('should create the webhook', async () => { + test('should store webhook in DB', async () => { const mockWebhook = createWebhook('GET', 'user/:id'); await webhookService.storeWebhook(mockWebhook); - expect(webhookRepository.insert).toHaveBeenCalledWith(mockWebhook); + expect(webhookRepository.upsert).toHaveBeenCalledWith(mockWebhook, ['method', 'webhookPath']); }); }); }); diff --git a/packages/cli/src/webhooks/webhook.service.ts b/packages/cli/src/webhooks/webhook.service.ts index c72edc18d1..3d7e75871d 100644 --- a/packages/cli/src/webhooks/webhook.service.ts +++ b/packages/cli/src/webhooks/webhook.service.ts @@ -93,7 +93,7 @@ export class WebhookService { async storeWebhook(webhook: WebhookEntity) { void this.cacheService.set(webhook.cacheKey, webhook); - return await this.webhookRepository.insert(webhook); + await this.webhookRepository.upsert(webhook, ['method', 'webhookPath']); } createWebhook(data: Partial) { From f114035a6b4470c5a8e9ec2f470a9a667e0dd7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Wed, 4 Sep 2024 08:04:35 +0200 Subject: [PATCH 043/259] refactor(editor): Remove Trial logic in personalization modal and port to script setup (#10649) Co-authored-by: Csaba Tuncsik --- .../components/PersonalizationModal.test.ts | 157 +++ .../src/components/PersonalizationModal.vue | 1101 ++++++++--------- .../__tests__/PersonalizationModal.spec.ts | 144 --- 3 files changed, 648 insertions(+), 754 deletions(-) create mode 100644 packages/editor-ui/src/components/PersonalizationModal.test.ts delete mode 100644 packages/editor-ui/src/components/__tests__/PersonalizationModal.spec.ts diff --git a/packages/editor-ui/src/components/PersonalizationModal.test.ts b/packages/editor-ui/src/components/PersonalizationModal.test.ts new file mode 100644 index 0000000000..160beefccd --- /dev/null +++ b/packages/editor-ui/src/components/PersonalizationModal.test.ts @@ -0,0 +1,157 @@ +import userEvent from '@testing-library/user-event'; +import { createComponentRenderer } from '@/__tests__/render'; +import { getDropdownItems, mockedStore } from '@/__tests__/utils'; +import { createUser } from '@/__tests__/data/users'; +import { useSettingsStore } from '@/stores/settings.store'; +import PersonalizationModal from '@/components/PersonalizationModal.vue'; +import { useUsersStore } from '@/stores/users.store'; +import { createTestingPinia } from '@pinia/testing'; +import { + COMPANY_TYPE_KEY, + EMAIL_KEY, + COMPANY_INDUSTRY_EXTENDED_KEY, + OTHER_COMPANY_INDUSTRY_EXTENDED_KEY, + MARKETING_AUTOMATION_GOAL_KEY, + OTHER_MARKETING_AUTOMATION_GOAL_KEY, + ROLE_KEY, + ROLE_OTHER_KEY, + DEVOPS_AUTOMATION_GOAL_OTHER_KEY, + DEVOPS_AUTOMATION_GOAL_KEY, +} from '@/constants'; + +const renderModal = createComponentRenderer(PersonalizationModal, { + global: { + stubs: { + Modal: { + template: ` +
+ + + + +
+ `, + }, + }, + }, +}); + +describe('PersonalizationModal', () => { + it('mounts', () => { + const { getByTitle } = renderModal({ pinia: createTestingPinia() }); + expect(getByTitle('Customize n8n to you')).toBeInTheDocument(); + }); + + it('shows user input when needed for desktop deployment', () => { + const pinia = createTestingPinia(); + const usersStore = mockedStore(useUsersStore); + usersStore.currentUser = createUser({ firstName: undefined }); + + const settingsStore = mockedStore(useSettingsStore); + settingsStore.isDesktopDeployment = true; + + const { getByTestId } = renderModal({ pinia }); + expect(getByTestId(EMAIL_KEY)).toBeInTheDocument(); + }); + + describe('Company field', () => { + it('allows completion of company related fields', async () => { + const { getByTestId } = renderModal({ pinia: createTestingPinia() }); + + const companyTypeSelect = getByTestId(COMPANY_TYPE_KEY); + + const otherTypeOfCompanyOption = [...(await getDropdownItems(companyTypeSelect))].find( + (node) => node.textContent === 'Other', + ) as Element; + + await userEvent.click(otherTypeOfCompanyOption); + + const industrySelect = getByTestId(COMPANY_INDUSTRY_EXTENDED_KEY); + expect(industrySelect).toBeInTheDocument(); + + const otherIndustryOption = [...(await getDropdownItems(industrySelect))].find( + (node) => node.textContent === 'Other (please specify)', + ) as Element; + + await userEvent.click(otherIndustryOption); + + expect(getByTestId(OTHER_COMPANY_INDUSTRY_EXTENDED_KEY)).toBeInTheDocument(); + }); + + it('shows only company and source select when not used for work', async () => { + const { getByTestId, baseElement } = renderModal({ pinia: createTestingPinia() }); + + const companyTypeSelect = getByTestId(COMPANY_TYPE_KEY); + + const nonWorkOption = [...(await getDropdownItems(companyTypeSelect))].find( + (node) => node.textContent === "I'm not using n8n for work", + ) as Element; + + await userEvent.click(nonWorkOption); + + expect(baseElement.querySelectorAll('input').length).toBe(2); + }); + }); + + it('allows completion of role related fields', async () => { + const { getByTestId, queryByTestId } = renderModal({ pinia: createTestingPinia() }); + + const roleSelect = getByTestId(ROLE_KEY); + const roleItems = [...(await getDropdownItems(roleSelect))]; + + const devOps = roleItems.find((node) => node.textContent === 'Devops') as Element; + const engineering = roleItems.find((node) => node.textContent === 'Engineering') as Element; + const it = roleItems.find((node) => node.textContent === 'IT') as Element; + const other = roleItems.find( + (node) => node.textContent === 'Other (please specify)', + ) as Element; + + await userEvent.click(devOps); + const automationGoalSelect = getByTestId(DEVOPS_AUTOMATION_GOAL_KEY); + expect(automationGoalSelect).toBeInTheDocument(); + + await userEvent.click(engineering); + expect(automationGoalSelect).toBeInTheDocument(); + + await userEvent.click(it); + expect(automationGoalSelect).toBeInTheDocument(); + + const otherGoalsItem = [...(await getDropdownItems(automationGoalSelect))].find( + (node) => node.textContent === 'Other', + ) as Element; + + await userEvent.click(otherGoalsItem); + expect(getByTestId(DEVOPS_AUTOMATION_GOAL_OTHER_KEY)).toBeInTheDocument(); + + await userEvent.click(other); + expect(queryByTestId(DEVOPS_AUTOMATION_GOAL_KEY)).not.toBeInTheDocument(); + expect(getByTestId(ROLE_OTHER_KEY)).toBeInTheDocument(); + }); + + it('allows completion of marketing and sales related fields', async () => { + const { getByTestId } = renderModal({ pinia: createTestingPinia() }); + + const companyTypeSelect = getByTestId(COMPANY_TYPE_KEY); + + const anyWorkOption = [...(await getDropdownItems(companyTypeSelect))].find( + (node) => node.textContent !== "I'm not using n8n for work", + ) as Element; + + await userEvent.click(anyWorkOption); + + const roleSelect = getByTestId(ROLE_KEY); + const salesAndMarketingOption = [...(await getDropdownItems(roleSelect))].find( + (node) => node.textContent === 'Sales and Marketing', + ) as Element; + + await userEvent.click(salesAndMarketingOption); + + const salesAndMarketingSelect = getByTestId(MARKETING_AUTOMATION_GOAL_KEY); + const otherItem = [...(await getDropdownItems(salesAndMarketingSelect))].find( + (node) => node.textContent === 'Other', + ) as Element; + + await userEvent.click(otherItem); + expect(getByTestId(OTHER_MARKETING_AUTOMATION_GOAL_KEY)).toBeInTheDocument(); + }); +}); diff --git a/packages/editor-ui/src/components/PersonalizationModal.vue b/packages/editor-ui/src/components/PersonalizationModal.vue index 13e360c3c1..1eab5e4b20 100644 --- a/packages/editor-ui/src/components/PersonalizationModal.vue +++ b/packages/editor-ui/src/components/PersonalizationModal.vue @@ -1,6 +1,5 @@ - diff --git a/packages/editor-ui/src/components/TriggerPanel.vue b/packages/editor-ui/src/components/TriggerPanel.vue index db8a64736a..0702f34c49 100644 --- a/packages/editor-ui/src/components/TriggerPanel.vue +++ b/packages/editor-ui/src/components/TriggerPanel.vue @@ -441,7 +441,7 @@ export default defineComponent({
-   +   { {{ `${$locale.baseText('versionCard.version')} ${version.name}` }} - + {
diff --git a/packages/editor-ui/src/components/WorkflowSettings.vue b/packages/editor-ui/src/components/WorkflowSettings.vue index 2d915611ff..1ecac538a6 100644 --- a/packages/editor-ui/src/components/WorkflowSettings.vue +++ b/packages/editor-ui/src/components/WorkflowSettings.vue @@ -573,7 +573,7 @@ export default defineComponent({ {{ $locale.baseText('workflowSettings.errorWorkflow') + ':' }} diff --git a/packages/editor-ui/src/components/banners/V1Banner.vue b/packages/editor-ui/src/components/banners/V1Banner.vue index 72077f1a3c..6ea90c712c 100644 --- a/packages/editor-ui/src/components/banners/V1Banner.vue +++ b/packages/editor-ui/src/components/banners/V1Banner.vue @@ -17,14 +17,14 @@ const hasOwnerPermission = computed(() => hasPermission(['instanceOwner']));