mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'master' into ADO-3066-show-errors-in-rlc
This commit is contained in:
commit
c09bfa0940
1
.github/workflows/release-publish.yml
vendored
1
.github/workflows/release-publish.yml
vendored
|
@ -109,7 +109,6 @@ jobs:
|
||||||
context: ./docker/images/n8n
|
context: ./docker/images/n8n
|
||||||
build-args: |
|
build-args: |
|
||||||
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
|
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
|
||||||
N8N_RELEASE_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
provenance: false
|
provenance: false
|
||||||
push: true
|
push: true
|
||||||
|
|
|
@ -4,18 +4,15 @@ FROM n8nio/base:${NODE_VERSION}
|
||||||
ARG N8N_VERSION
|
ARG N8N_VERSION
|
||||||
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
|
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
|
||||||
|
|
||||||
ARG N8N_RELEASE_DATE
|
|
||||||
LABEL org.opencontainers.image.title="n8n"
|
LABEL org.opencontainers.image.title="n8n"
|
||||||
LABEL org.opencontainers.image.description="Workflow Automation Tool"
|
LABEL org.opencontainers.image.description="Workflow Automation Tool"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n"
|
LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n"
|
||||||
LABEL org.opencontainers.image.url="https://n8n.io"
|
LABEL org.opencontainers.image.url="https://n8n.io"
|
||||||
LABEL org.opencontainers.image.version=${N8N_VERSION}
|
LABEL org.opencontainers.image.version=${N8N_VERSION}
|
||||||
LABEL org.opencontainers.image.created=${N8N_RELEASE_DATE}
|
|
||||||
|
|
||||||
ENV N8N_VERSION=${N8N_VERSION}
|
ENV N8N_VERSION=${N8N_VERSION}
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV N8N_RELEASE_TYPE=stable
|
ENV N8N_RELEASE_TYPE=stable
|
||||||
ENV N8N_RELEASE_DATE=${N8N_RELEASE_DATE}
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && \
|
npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && \
|
||||||
npm rebuild --prefix=/usr/local/lib/node_modules/n8n sqlite3 && \
|
npm rebuild --prefix=/usr/local/lib/node_modules/n8n sqlite3 && \
|
||||||
|
|
|
@ -9,9 +9,6 @@ export class GenericConfig {
|
||||||
@Env('N8N_RELEASE_TYPE')
|
@Env('N8N_RELEASE_TYPE')
|
||||||
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev';
|
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev';
|
||||||
|
|
||||||
@Env('N8N_RELEASE_DATE')
|
|
||||||
releaseDate?: Date;
|
|
||||||
|
|
||||||
/** Grace period (in seconds) to wait for components to shut down before process exit. */
|
/** Grace period (in seconds) to wait for components to shut down before process exit. */
|
||||||
@Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT')
|
@Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT')
|
||||||
gracefulShutdownTimeout: number = 30;
|
gracefulShutdownTimeout: number = 30;
|
||||||
|
|
|
@ -10,14 +10,6 @@ export class SentryConfig {
|
||||||
@Env('N8N_FRONTEND_SENTRY_DSN')
|
@Env('N8N_FRONTEND_SENTRY_DSN')
|
||||||
frontendDsn: string = '';
|
frontendDsn: string = '';
|
||||||
|
|
||||||
/**
|
|
||||||
* Version of the n8n instance
|
|
||||||
*
|
|
||||||
* @example '1.73.0'
|
|
||||||
*/
|
|
||||||
@Env('N8N_VERSION')
|
|
||||||
n8nVersion: string = '';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Environment of the n8n instance.
|
* Environment of the n8n instance.
|
||||||
*
|
*
|
||||||
|
|
|
@ -243,7 +243,6 @@ describe('GlobalConfig', () => {
|
||||||
sentry: {
|
sentry: {
|
||||||
backendDsn: '',
|
backendDsn: '',
|
||||||
frontendDsn: '',
|
frontendDsn: '',
|
||||||
n8nVersion: '',
|
|
||||||
environment: '',
|
environment: '',
|
||||||
deploymentName: '',
|
deploymentName: '',
|
||||||
},
|
},
|
||||||
|
@ -326,7 +325,6 @@ describe('GlobalConfig', () => {
|
||||||
DB_LOGGING_MAX_EXECUTION_TIME: '0',
|
DB_LOGGING_MAX_EXECUTION_TIME: '0',
|
||||||
N8N_METRICS: 'TRUE',
|
N8N_METRICS: 'TRUE',
|
||||||
N8N_TEMPLATES_ENABLED: '0',
|
N8N_TEMPLATES_ENABLED: '0',
|
||||||
N8N_RELEASE_DATE: '2025-02-17T13:54:15Z',
|
|
||||||
};
|
};
|
||||||
const config = Container.get(GlobalConfig);
|
const config = Container.get(GlobalConfig);
|
||||||
expect(structuredClone(config)).toEqual({
|
expect(structuredClone(config)).toEqual({
|
||||||
|
@ -358,10 +356,6 @@ describe('GlobalConfig', () => {
|
||||||
...defaultConfig.templates,
|
...defaultConfig.templates,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
generic: {
|
|
||||||
...defaultConfig.generic,
|
|
||||||
releaseDate: new Date('2025-02-17T13:54:15.000Z'),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(mockFs.readFileSync).not.toHaveBeenCalled();
|
expect(mockFs.readFileSync).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -397,15 +391,4 @@ describe('GlobalConfig', () => {
|
||||||
'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd',
|
'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle invalid timestamps', () => {
|
|
||||||
process.env = {
|
|
||||||
N8N_RELEASE_DATE: 'abcd',
|
|
||||||
};
|
|
||||||
const config = Container.get(GlobalConfig);
|
|
||||||
expect(config.generic.releaseDate).toBeUndefined();
|
|
||||||
expect(consoleWarnMock).toHaveBeenCalledWith(
|
|
||||||
'Invalid timestamp value for N8N_RELEASE_DATE: abcd',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,13 @@ import { ensureError, sleep, UserError } from 'n8n-workflow';
|
||||||
|
|
||||||
import type { AbstractServer } from '@/abstract-server';
|
import type { AbstractServer } from '@/abstract-server';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { LICENSE_FEATURES, inDevelopment, inTest } from '@/constants';
|
import {
|
||||||
|
LICENSE_FEATURES,
|
||||||
|
N8N_VERSION,
|
||||||
|
N8N_RELEASE_DATE,
|
||||||
|
inDevelopment,
|
||||||
|
inTest,
|
||||||
|
} from '@/constants';
|
||||||
import * as CrashJournal from '@/crash-journal';
|
import * as CrashJournal from '@/crash-journal';
|
||||||
import * as Db from '@/db';
|
import * as Db from '@/db';
|
||||||
import { getDataDeduplicationService } from '@/deduplication';
|
import { getDataDeduplicationService } from '@/deduplication';
|
||||||
|
@ -63,15 +69,14 @@ export abstract class BaseCommand extends Command {
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
this.errorReporter = Container.get(ErrorReporter);
|
this.errorReporter = Container.get(ErrorReporter);
|
||||||
|
|
||||||
const { releaseDate } = this.globalConfig.generic;
|
const { backendDsn, environment, deploymentName } = this.globalConfig.sentry;
|
||||||
const { backendDsn, n8nVersion, environment, deploymentName } = this.globalConfig.sentry;
|
|
||||||
await this.errorReporter.init({
|
await this.errorReporter.init({
|
||||||
serverType: this.instanceSettings.instanceType,
|
serverType: this.instanceSettings.instanceType,
|
||||||
dsn: backendDsn,
|
dsn: backendDsn,
|
||||||
environment,
|
environment,
|
||||||
release: n8nVersion,
|
release: N8N_VERSION,
|
||||||
serverName: deploymentName,
|
serverName: deploymentName,
|
||||||
releaseDate,
|
releaseDate: N8N_RELEASE_DATE,
|
||||||
});
|
});
|
||||||
initExpressionEvaluator();
|
initExpressionEvaluator();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync, statSync } from 'fs';
|
||||||
import type { n8n } from 'n8n-core';
|
import type { n8n } from 'n8n-core';
|
||||||
import type { ITaskDataConnections } from 'n8n-workflow';
|
import type { ITaskDataConnections } from 'n8n-workflow';
|
||||||
import { jsonParse, TRIMMED_TASK_DATA_CONNECTIONS_KEY } from 'n8n-workflow';
|
import { jsonParse, TRIMMED_TASK_DATA_CONNECTIONS_KEY } from 'n8n-workflow';
|
||||||
|
@ -18,9 +18,10 @@ export const TEMPLATES_DIR = join(CLI_DIR, 'templates');
|
||||||
export const NODES_BASE_DIR = dirname(require.resolve('n8n-nodes-base'));
|
export const NODES_BASE_DIR = dirname(require.resolve('n8n-nodes-base'));
|
||||||
export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist');
|
export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist');
|
||||||
|
|
||||||
export function getN8nPackageJson() {
|
const packageJsonPath = join(CLI_DIR, 'package.json');
|
||||||
return jsonParse<n8n.PackageJson>(readFileSync(join(CLI_DIR, 'package.json'), 'utf8'));
|
const n8nPackageJson = jsonParse<n8n.PackageJson>(readFileSync(packageJsonPath, 'utf8'));
|
||||||
}
|
export const N8N_VERSION = n8nPackageJson.version;
|
||||||
|
export const N8N_RELEASE_DATE = statSync(packageJsonPath).mtime;
|
||||||
|
|
||||||
export const STARTING_NODES = [
|
export const STARTING_NODES = [
|
||||||
'@n8n/n8n-nodes-langchain.manualChatTrigger',
|
'@n8n/n8n-nodes-langchain.manualChatTrigger',
|
||||||
|
@ -28,8 +29,6 @@ export const STARTING_NODES = [
|
||||||
'n8n-nodes-base.manualTrigger',
|
'n8n-nodes-base.manualTrigger',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const N8N_VERSION = getN8nPackageJson().version;
|
|
||||||
|
|
||||||
export const NODE_PACKAGE_PREFIX = 'n8n-nodes-';
|
export const NODE_PACKAGE_PREFIX = 'n8n-nodes-';
|
||||||
|
|
||||||
export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`;
|
export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { InstanceSettings, Logger } from 'n8n-core';
|
||||||
import type { IWorkflowBase } from 'n8n-workflow';
|
import type { IWorkflowBase } from 'n8n-workflow';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { getN8nPackageJson, inDevelopment } from '@/constants';
|
import { inDevelopment, N8N_VERSION } from '@/constants';
|
||||||
import { isApiEnabled } from '@/public-api';
|
import { isApiEnabled } from '@/public-api';
|
||||||
import {
|
import {
|
||||||
ENV_VARS_DOCS_URL,
|
ENV_VARS_DOCS_URL,
|
||||||
|
@ -175,7 +175,7 @@ export class InstanceRiskReporter implements RiskReporter {
|
||||||
private async getOutdatedState() {
|
private async getOutdatedState() {
|
||||||
let versions = [];
|
let versions = [];
|
||||||
|
|
||||||
const localVersion = getN8nPackageJson().version;
|
const localVersion = N8N_VERSION;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
versions = await this.getNextVersions(localVersion).then((v) => this.removeIconData(v));
|
versions = await this.getNextVersions(localVersion).then((v) => this.removeIconData(v));
|
||||||
|
|
|
@ -114,9 +114,8 @@ export const MOCK_PACKAGE: InstalledPackages[] = [
|
||||||
export function simulateOutdatedInstanceOnce(versionName = MOCK_01110_N8N_VERSION.name) {
|
export function simulateOutdatedInstanceOnce(versionName = MOCK_01110_N8N_VERSION.name) {
|
||||||
const baseUrl = Container.get(GlobalConfig).versionNotifications.endpoint + '/';
|
const baseUrl = Container.get(GlobalConfig).versionNotifications.endpoint + '/';
|
||||||
|
|
||||||
jest
|
// @ts-expect-error readonly export
|
||||||
.spyOn(constants, 'getN8nPackageJson')
|
constants.N8N_VERSION = versionName;
|
||||||
.mockReturnValueOnce({ name: 'n8n', version: versionName });
|
|
||||||
|
|
||||||
nock(baseUrl).get(versionName).reply(200, [MOCK_01110_N8N_VERSION, MOCK_09990_N8N_VERSION]);
|
nock(baseUrl).get(versionName).reply(200, [MOCK_01110_N8N_VERSION, MOCK_09990_N8N_VERSION]);
|
||||||
}
|
}
|
||||||
|
@ -124,9 +123,8 @@ export function simulateOutdatedInstanceOnce(versionName = MOCK_01110_N8N_VERSIO
|
||||||
export function simulateUpToDateInstance(versionName = MOCK_09990_N8N_VERSION.name) {
|
export function simulateUpToDateInstance(versionName = MOCK_09990_N8N_VERSION.name) {
|
||||||
const baseUrl = Container.get(GlobalConfig).versionNotifications.endpoint + '/';
|
const baseUrl = Container.get(GlobalConfig).versionNotifications.endpoint + '/';
|
||||||
|
|
||||||
jest
|
// @ts-expect-error readonly export
|
||||||
.spyOn(constants, 'getN8nPackageJson')
|
constants.N8N_VERSION = versionName;
|
||||||
.mockReturnValueOnce({ name: 'n8n', version: versionName });
|
|
||||||
|
|
||||||
nock(baseUrl).persist().get(versionName).reply(200, [MOCK_09990_N8N_VERSION]);
|
nock(baseUrl).persist().get(versionName).reply(200, [MOCK_09990_N8N_VERSION]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,8 @@ type ErrorReporterInitOptions = {
|
||||||
beforeSendFilter?: (event: ErrorEvent, hint: EventHint) => boolean;
|
beforeSendFilter?: (event: ErrorEvent, hint: EventHint) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SIX_WEEKS_IN_MS = 6 * 7 * 24 * 60 * 60 * 1000;
|
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||||
|
const SIX_WEEKS_IN_MS = 6 * 7 * ONE_DAY_IN_MS;
|
||||||
const RELEASE_EXPIRATION_WARNING =
|
const RELEASE_EXPIRATION_WARNING =
|
||||||
'Error tracking disabled because this release is older than 6 weeks.';
|
'Error tracking disabled because this release is older than 6 weeks.';
|
||||||
|
|
||||||
|
@ -86,17 +87,23 @@ export class ErrorReporter {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (releaseDate) {
|
if (releaseDate) {
|
||||||
const releaseExpiresInMs = releaseDate.getTime() + SIX_WEEKS_IN_MS - Date.now();
|
const releaseExpiresAtMs = releaseDate.getTime() + SIX_WEEKS_IN_MS;
|
||||||
if (releaseExpiresInMs <= 0) {
|
const releaseExpiresInMs = () => releaseExpiresAtMs - Date.now();
|
||||||
|
if (releaseExpiresInMs() <= 0) {
|
||||||
this.logger.warn(RELEASE_EXPIRATION_WARNING);
|
this.logger.warn(RELEASE_EXPIRATION_WARNING);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Once this release expires, reject all events
|
const checkForExpiration = () => {
|
||||||
this.expirationTimer = setTimeout(() => {
|
// Once this release expires, reject all events
|
||||||
this.logger.warn(RELEASE_EXPIRATION_WARNING);
|
if (releaseExpiresInMs() <= 0) {
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
this.logger.warn(RELEASE_EXPIRATION_WARNING);
|
||||||
this.report = this.defaultReport;
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
}, releaseExpiresInMs);
|
this.report = this.defaultReport;
|
||||||
|
} else {
|
||||||
|
setTimeout(checkForExpiration, ONE_DAY_IN_MS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkForExpiration();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dsn) return;
|
if (!dsn) return;
|
||||||
|
|
|
@ -0,0 +1,217 @@
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type { IConnections, INode, INodeType, INodeTypes, IPinData } from 'n8n-workflow';
|
||||||
|
import { Workflow } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { toIConnections } from './helpers';
|
||||||
|
import { findTriggerForPartialExecution } from '../find-trigger-for-partial-execution';
|
||||||
|
|
||||||
|
describe('findTriggerForPartialExecution', () => {
|
||||||
|
const nodeTypes = mock<INodeTypes>();
|
||||||
|
|
||||||
|
const createMockWorkflow = (nodes: INode[], connections: IConnections, pinData?: IPinData) =>
|
||||||
|
new Workflow({
|
||||||
|
active: false,
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
nodeTypes,
|
||||||
|
pinData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createNode = (name: string, type: string, disabled = false) =>
|
||||||
|
mock<INode>({ name, type, disabled });
|
||||||
|
const manualTriggerNode = createNode('ManualTrigger', 'n8n-nodes-base.manualTrigger');
|
||||||
|
const disabledTriggerNode = createNode('DisabledTrigger', 'n8n-nodes-base.manualTrigger', true);
|
||||||
|
const pinnedTrigger = createNode('PinnedTrigger', 'n8n-nodes-base.manualTrigger');
|
||||||
|
const setNode = createNode('Set', 'n8n-nodes-base.set');
|
||||||
|
const noOpNode = createNode('No Operation', 'n8n-nodes-base.noOp');
|
||||||
|
const webhookNode = createNode('Webhook', 'n8n-nodes-base.webhook');
|
||||||
|
const webhookNode1 = createNode('Webhook1', 'n8n-nodes-base.webhook');
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nodeTypes.getByNameAndVersion.mockImplementation((type) => {
|
||||||
|
const isTrigger = type.endsWith('Trigger') || type.endsWith('webhook');
|
||||||
|
return mock<INodeType>({
|
||||||
|
description: {
|
||||||
|
group: isTrigger ? ['trigger'] : [],
|
||||||
|
properties: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const testGroups: Record<
|
||||||
|
string,
|
||||||
|
Array<{
|
||||||
|
description: string;
|
||||||
|
nodes: INode[];
|
||||||
|
connections: Array<{ to: INode; from: INode }>;
|
||||||
|
destinationNodeName: string;
|
||||||
|
pinData?: IPinData;
|
||||||
|
expectedTrigger?: INode;
|
||||||
|
}>
|
||||||
|
> = {
|
||||||
|
'Single trigger node': [
|
||||||
|
{
|
||||||
|
description: 'should return the destination node if it is a trigger',
|
||||||
|
nodes: [manualTriggerNode],
|
||||||
|
connections: [],
|
||||||
|
destinationNodeName: manualTriggerNode.name,
|
||||||
|
expectedTrigger: manualTriggerNode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should return a parent trigger node for a non-trigger destination',
|
||||||
|
nodes: [manualTriggerNode, setNode],
|
||||||
|
connections: [{ from: manualTriggerNode, to: setNode }],
|
||||||
|
destinationNodeName: setNode.name,
|
||||||
|
expectedTrigger: manualTriggerNode,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'Multiple trigger nodes': [
|
||||||
|
{
|
||||||
|
description: 'should prioritize webhook nodes when multiple parent triggers exist',
|
||||||
|
nodes: [webhookNode, manualTriggerNode, setNode],
|
||||||
|
connections: [
|
||||||
|
{ from: webhookNode, to: setNode },
|
||||||
|
{ from: manualTriggerNode, to: setNode },
|
||||||
|
],
|
||||||
|
destinationNodeName: setNode.name,
|
||||||
|
expectedTrigger: webhookNode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should handle multiple webhook triggers',
|
||||||
|
nodes: [webhookNode, webhookNode1, setNode],
|
||||||
|
connections: [
|
||||||
|
{ from: webhookNode, to: setNode },
|
||||||
|
{ from: webhookNode1, to: setNode },
|
||||||
|
],
|
||||||
|
destinationNodeName: setNode.name,
|
||||||
|
expectedTrigger: webhookNode1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should prioritize webhook node, even if it is further up',
|
||||||
|
nodes: [manualTriggerNode, setNode, noOpNode, webhookNode],
|
||||||
|
connections: [
|
||||||
|
{ from: manualTriggerNode, to: setNode },
|
||||||
|
{ from: setNode, to: noOpNode },
|
||||||
|
{ from: webhookNode, to: noOpNode },
|
||||||
|
],
|
||||||
|
destinationNodeName: noOpNode.name,
|
||||||
|
expectedTrigger: webhookNode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should ignore disabled parent trigger nodes',
|
||||||
|
nodes: [disabledTriggerNode, manualTriggerNode, setNode],
|
||||||
|
connections: [
|
||||||
|
{ from: disabledTriggerNode, to: setNode },
|
||||||
|
{ from: manualTriggerNode, to: setNode },
|
||||||
|
],
|
||||||
|
destinationNodeName: setNode.name,
|
||||||
|
expectedTrigger: manualTriggerNode,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'No trigger nodes': [
|
||||||
|
{
|
||||||
|
description: 'should return undefined when no valid parent triggers found',
|
||||||
|
nodes: [setNode, noOpNode],
|
||||||
|
connections: [{ from: setNode, to: noOpNode }],
|
||||||
|
destinationNodeName: noOpNode.name,
|
||||||
|
expectedTrigger: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'Trigger node with pinned data': [
|
||||||
|
{
|
||||||
|
description: 'should prioritize pinned trigger nodes',
|
||||||
|
nodes: [pinnedTrigger, manualTriggerNode, setNode],
|
||||||
|
connections: [
|
||||||
|
{ from: pinnedTrigger, to: setNode },
|
||||||
|
{ from: manualTriggerNode, to: setNode },
|
||||||
|
],
|
||||||
|
destinationNodeName: setNode.name,
|
||||||
|
pinData: { [pinnedTrigger.name]: [{ json: { test: true } }] },
|
||||||
|
expectedTrigger: pinnedTrigger,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should prioritize pinned webhook triggers',
|
||||||
|
nodes: [pinnedTrigger, manualTriggerNode, webhookNode, setNode],
|
||||||
|
connections: [
|
||||||
|
{ from: pinnedTrigger, to: setNode },
|
||||||
|
{ from: webhookNode, to: setNode },
|
||||||
|
{ from: manualTriggerNode, to: setNode },
|
||||||
|
],
|
||||||
|
destinationNodeName: setNode.name,
|
||||||
|
pinData: {
|
||||||
|
[pinnedTrigger.name]: [{ json: { test: true } }],
|
||||||
|
[webhookNode.name]: [{ json: { test: true } }],
|
||||||
|
},
|
||||||
|
expectedTrigger: webhookNode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should prioritize the first connected pinned webhook triggers',
|
||||||
|
nodes: [webhookNode, webhookNode1, pinnedTrigger, manualTriggerNode, setNode],
|
||||||
|
connections: [
|
||||||
|
{ from: pinnedTrigger, to: setNode },
|
||||||
|
{ from: webhookNode, to: setNode },
|
||||||
|
{ from: webhookNode1, to: setNode },
|
||||||
|
{ from: manualTriggerNode, to: setNode },
|
||||||
|
],
|
||||||
|
destinationNodeName: setNode.name,
|
||||||
|
pinData: {
|
||||||
|
[pinnedTrigger.name]: [{ json: { test: true } }],
|
||||||
|
[webhookNode.name]: [{ json: { test: true } }],
|
||||||
|
[webhookNode1.name]: [{ json: { test: true } }],
|
||||||
|
},
|
||||||
|
expectedTrigger: webhookNode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should prioritize the first connected pinned webhook triggers (reverse)',
|
||||||
|
nodes: [webhookNode1, webhookNode, pinnedTrigger, manualTriggerNode, setNode],
|
||||||
|
connections: [
|
||||||
|
{ from: pinnedTrigger, to: setNode },
|
||||||
|
{ from: webhookNode1, to: setNode },
|
||||||
|
{ from: webhookNode, to: setNode },
|
||||||
|
{ from: manualTriggerNode, to: setNode },
|
||||||
|
],
|
||||||
|
destinationNodeName: setNode.name,
|
||||||
|
pinData: {
|
||||||
|
[pinnedTrigger.name]: [{ json: { test: true } }],
|
||||||
|
[webhookNode.name]: [{ json: { test: true } }],
|
||||||
|
[webhookNode1.name]: [{ json: { test: true } }],
|
||||||
|
},
|
||||||
|
expectedTrigger: webhookNode1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [group, tests] of Object.entries(testGroups)) {
|
||||||
|
describe(group, () => {
|
||||||
|
test.each(tests)(
|
||||||
|
'$description',
|
||||||
|
({ nodes, connections, destinationNodeName, expectedTrigger, pinData }) => {
|
||||||
|
const workflow = createMockWorkflow(nodes, toIConnections(connections), pinData);
|
||||||
|
expect(findTriggerForPartialExecution(workflow, destinationNodeName)).toBe(
|
||||||
|
expectedTrigger,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Error and Edge Case Handling', () => {
|
||||||
|
it('should handle non-existent destination node gracefully', () => {
|
||||||
|
const workflow = createMockWorkflow([], {});
|
||||||
|
expect(findTriggerForPartialExecution(workflow, 'NonExistentNode')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty workflow', () => {
|
||||||
|
const workflow = createMockWorkflow([], {});
|
||||||
|
expect(findTriggerForPartialExecution(workflow, '')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle workflow with no connections', () => {
|
||||||
|
const workflow = createMockWorkflow([manualTriggerNode], {});
|
||||||
|
expect(findTriggerForPartialExecution(workflow, manualTriggerNode.name)).toBe(
|
||||||
|
manualTriggerNode,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,7 @@
|
||||||
import * as assert from 'assert/strict';
|
import * as assert from 'assert/strict';
|
||||||
import type { INode, Workflow } from 'n8n-workflow';
|
import type { INode, INodeType, Workflow } from 'n8n-workflow';
|
||||||
|
|
||||||
|
const isTriggerNode = (nodeType: INodeType) => nodeType.description.group.includes('trigger');
|
||||||
|
|
||||||
function findAllParentTriggers(workflow: Workflow, destinationNodeName: string) {
|
function findAllParentTriggers(workflow: Workflow, destinationNodeName: string) {
|
||||||
const parentNodes = workflow
|
const parentNodes = workflow
|
||||||
|
@ -17,35 +19,50 @@ function findAllParentTriggers(workflow: Workflow, destinationNodeName: string)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((value) => value !== null)
|
.filter((value) => value !== null)
|
||||||
.filter(({ nodeType }) => nodeType.description.group.includes('trigger'))
|
.filter(({ nodeType }) => isTriggerNode(nodeType))
|
||||||
.map(({ node }) => node);
|
.map(({ node }) => node);
|
||||||
|
|
||||||
return parentNodes;
|
return parentNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: write unit tests for this
|
|
||||||
// TODO: rewrite this using DirectedGraph instead of workflow.
|
// TODO: rewrite this using DirectedGraph instead of workflow.
|
||||||
export function findTriggerForPartialExecution(
|
export function findTriggerForPartialExecution(
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
destinationNodeName: string,
|
destinationNodeName: string,
|
||||||
): INode | undefined {
|
): INode | undefined {
|
||||||
|
// First, check if the destination node itself is a trigger
|
||||||
|
const destinationNode = workflow.getNode(destinationNodeName);
|
||||||
|
if (!destinationNode) return;
|
||||||
|
|
||||||
|
const destinationNodeType = workflow.nodeTypes.getByNameAndVersion(
|
||||||
|
destinationNode.type,
|
||||||
|
destinationNode.typeVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTriggerNode(destinationNodeType) && !destinationNode.disabled) {
|
||||||
|
return destinationNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the destination node wasn't a trigger, we try to find a parent node that's a trigger
|
||||||
const parentTriggers = findAllParentTriggers(workflow, destinationNodeName).filter(
|
const parentTriggers = findAllParentTriggers(workflow, destinationNodeName).filter(
|
||||||
(trigger) => !trigger.disabled,
|
(trigger) => !trigger.disabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Prioritize webhook triggers with pinned-data
|
||||||
const pinnedTriggers = parentTriggers
|
const pinnedTriggers = parentTriggers
|
||||||
// TODO: add the other filters here from `findAllPinnedActivators`, see
|
// TODO: add the other filters here from `findAllPinnedActivators`, see
|
||||||
// copy below.
|
// copy below.
|
||||||
.filter((trigger) => workflow.pinData?.[trigger.name])
|
.filter((trigger) => workflow.pinData?.[trigger.name])
|
||||||
// TODO: Make this sorting more predictable
|
|
||||||
// Put nodes which names end with 'webhook' first, while also reversing the
|
// Put nodes which names end with 'webhook' first, while also reversing the
|
||||||
// order they had in the original array.
|
// order they had in the original array.
|
||||||
.sort((n) => (n.type.endsWith('webhook') ? -1 : 1));
|
.sort((a, b) => (a.type.endsWith('webhook') ? -1 : b.type.endsWith('webhook') ? 1 : 0));
|
||||||
|
|
||||||
if (pinnedTriggers.length) {
|
if (pinnedTriggers.length) {
|
||||||
return pinnedTriggers[0];
|
return pinnedTriggers[0];
|
||||||
} else {
|
|
||||||
return parentTriggers[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prioritize webhook triggers over other parent triggers
|
||||||
|
const webhookTriggers = parentTriggers.filter((trigger) => trigger.type.endsWith('webhook'));
|
||||||
|
return webhookTriggers.length > 0 ? webhookTriggers[0] : parentTriggers[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
//function findAllPinnedActivators(workflow: Workflow, pinData?: IPinData) {
|
//function findAllPinnedActivators(workflow: Workflow, pinData?: IPinData) {
|
||||||
|
|
|
@ -94,11 +94,11 @@
|
||||||
"xss": "catalog:"
|
"xss": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^8.0.2",
|
||||||
|
"@iconify/json": "^2.2.228",
|
||||||
"@n8n/eslint-config": "workspace:*",
|
"@n8n/eslint-config": "workspace:*",
|
||||||
"@n8n/typescript-config": "workspace:*",
|
"@n8n/typescript-config": "workspace:*",
|
||||||
"@n8n/vitest-config": "workspace:*",
|
"@n8n/vitest-config": "workspace:*",
|
||||||
"@faker-js/faker": "^8.0.2",
|
|
||||||
"@iconify/json": "^2.2.228",
|
|
||||||
"@pinia/testing": "^0.1.6",
|
"@pinia/testing": "^0.1.6",
|
||||||
"@types/dateformat": "^3.0.0",
|
"@types/dateformat": "^3.0.0",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
|
@ -111,6 +111,7 @@
|
||||||
"@vitejs/plugin-vue": "catalog:frontend",
|
"@vitejs/plugin-vue": "catalog:frontend",
|
||||||
"@vitest/coverage-v8": "catalog:frontend",
|
"@vitest/coverage-v8": "catalog:frontend",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
|
"fake-indexeddb": "^6.0.0",
|
||||||
"miragejs": "^0.1.48",
|
"miragejs": "^0.1.48",
|
||||||
"unplugin-icons": "^0.19.0",
|
"unplugin-icons": "^0.19.0",
|
||||||
"unplugin-vue-components": "^0.27.2",
|
"unplugin-vue-components": "^0.27.2",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
import { configure } from '@testing-library/vue';
|
import { configure } from '@testing-library/vue';
|
||||||
import 'core-js/proposals/set-methods-v2';
|
import 'core-js/proposals/set-methods-v2';
|
||||||
|
|
||||||
|
@ -64,20 +65,21 @@ Object.defineProperty(window, 'matchMedia', {
|
||||||
});
|
});
|
||||||
|
|
||||||
class Worker {
|
class Worker {
|
||||||
onmessage: (message: string) => void;
|
onmessage = vi.fn();
|
||||||
|
|
||||||
url: string;
|
url: string;
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(url: string) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.onmessage = () => {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postMessage(message: string) {
|
postMessage = vi.fn((message: string) => {
|
||||||
this.onmessage(message);
|
this.onmessage(message);
|
||||||
}
|
});
|
||||||
|
|
||||||
addEventListener() {}
|
addEventListener = vi.fn();
|
||||||
|
|
||||||
|
terminate = vi.fn();
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(window, 'Worker', {
|
Object.defineProperty(window, 'Worker', {
|
||||||
|
|
|
@ -198,6 +198,16 @@ const onSelect = (value: number) => {
|
||||||
expirationDate.value = '';
|
expirationDate.value = '';
|
||||||
showExpirationDateSelector.value = false;
|
showExpirationDateSelector.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function handleEnterKey(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
if (props.mode === 'new') {
|
||||||
|
await onSave();
|
||||||
|
} else {
|
||||||
|
await onEdit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -212,7 +222,7 @@ const onSelect = (value: number) => {
|
||||||
:show-close="true"
|
:show-close="true"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div>
|
<div @keyup.enter="handleEnterKey">
|
||||||
<n8n-card v-if="newApiKey" class="mb-4xs">
|
<n8n-card v-if="newApiKey" class="mb-4xs">
|
||||||
<CopyInput
|
<CopyInput
|
||||||
:label="newApiKey.label"
|
:label="newApiKey.label"
|
||||||
|
|
|
@ -654,7 +654,6 @@ async function onAskAssistantClick() {
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.node-error-view {
|
.node-error-view {
|
||||||
&__header {
|
&__header {
|
||||||
max-width: 960px;
|
|
||||||
margin: 0 auto var(--spacing-s) auto;
|
margin: 0 auto var(--spacing-s) auto;
|
||||||
padding-bottom: var(--spacing-3xs);
|
padding-bottom: var(--spacing-3xs);
|
||||||
background-color: var(--color-background-xlight);
|
background-color: var(--color-background-xlight);
|
||||||
|
@ -755,7 +754,6 @@ async function onAskAssistantClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
max-width: 960px;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border: 1px solid var(--color-foreground-base);
|
border: 1px solid var(--color-foreground-base);
|
||||||
border-radius: var(--border-radius-large);
|
border-radius: var(--border-radius-large);
|
||||||
|
|
|
@ -300,6 +300,12 @@ async function onConnectionStateChange() {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
svg,
|
||||||
|
img {
|
||||||
|
max-width: 28px;
|
||||||
|
max-height: 28px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.providerActions {
|
.providerActions {
|
||||||
|
|
|
@ -18,11 +18,12 @@ import type {
|
||||||
INodeParameterResourceLocator,
|
INodeParameterResourceLocator,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
|
INodePropertyCollection,
|
||||||
INodePropertyOptions,
|
INodePropertyOptions,
|
||||||
IParameterLabel,
|
IParameterLabel,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { CREDENTIAL_EMPTY_VALUE, NodeHelpers } from 'n8n-workflow';
|
import { CREDENTIAL_EMPTY_VALUE, isINodePropertyOptions, NodeHelpers } from 'n8n-workflow';
|
||||||
|
|
||||||
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
|
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
|
||||||
import CredentialsSelect from '@/components/CredentialsSelect.vue';
|
import CredentialsSelect from '@/components/CredentialsSelect.vue';
|
||||||
|
@ -66,7 +67,9 @@ import type { EventBus } from '@n8n/utils/event-bus';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useElementSize } from '@vueuse/core';
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { captureMessage } from '@sentry/vue';
|
||||||
import { completeExpressionSyntax, isStringWithExpressionSyntax } from '@/utils/expressions';
|
import { completeExpressionSyntax, isStringWithExpressionSyntax } from '@/utils/expressions';
|
||||||
|
import { isPresent } from '@/utils/typesUtils';
|
||||||
import CssEditor from './CssEditor/CssEditor.vue';
|
import CssEditor from './CssEditor/CssEditor.vue';
|
||||||
|
|
||||||
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
||||||
|
@ -422,14 +425,11 @@ const editorLanguage = computed<CodeNodeEditorLanguage>(() => {
|
||||||
return getArgument<CodeNodeEditorLanguage>('editorLanguage') ?? 'javaScript';
|
return getArgument<CodeNodeEditorLanguage>('editorLanguage') ?? 'javaScript';
|
||||||
});
|
});
|
||||||
|
|
||||||
const parameterOptions = computed<INodePropertyOptions[] | undefined>(() => {
|
const parameterOptions = computed(() => {
|
||||||
if (!hasRemoteMethod.value) {
|
const options = hasRemoteMethod.value ? remoteParameterOptions.value : props.parameter.options;
|
||||||
// Options are already given
|
const safeOptions = (options ?? []).filter(isValidParameterOption);
|
||||||
return props.parameter.options as INodePropertyOptions[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options get loaded from server
|
return safeOptions;
|
||||||
return remoteParameterOptions.value;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSwitch = computed(
|
const isSwitch = computed(
|
||||||
|
@ -571,6 +571,12 @@ const shouldCaptureForPosthog = computed(() => {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isValidParameterOption(
|
||||||
|
option: INodePropertyOptions | INodeProperties | INodePropertyCollection,
|
||||||
|
): option is INodePropertyOptions {
|
||||||
|
return isINodePropertyOptions(option) && isPresent(option.value) && isPresent(option.name);
|
||||||
|
}
|
||||||
|
|
||||||
function isRemoteParameterOption(option: INodePropertyOptions) {
|
function isRemoteParameterOption(option: INodePropertyOptions) {
|
||||||
return remoteParameterOptionsKeys.value.includes(option.name);
|
return remoteParameterOptionsKeys.value.includes(option.name);
|
||||||
}
|
}
|
||||||
|
@ -1084,6 +1090,28 @@ watch(isModelValueExpression, async (isExpression, wasExpression) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Investigate invalid parameter options
|
||||||
|
// Sentry issue: https://n8nio.sentry.io/issues/6275981089/?project=4503960699273216
|
||||||
|
const unwatchParameterOptions = watch(
|
||||||
|
[remoteParameterOptions, () => props.parameter.options],
|
||||||
|
([remoteOptions, options]) => {
|
||||||
|
const allOptions = [...remoteOptions, ...(options ?? [])];
|
||||||
|
const invalidOptions = allOptions.filter((option) => !isValidParameterOption(option));
|
||||||
|
|
||||||
|
if (invalidOptions.length > 0) {
|
||||||
|
captureMessage('Invalid parameter options', {
|
||||||
|
level: 'error',
|
||||||
|
extra: {
|
||||||
|
invalidOptions,
|
||||||
|
parameter: props.parameter.name,
|
||||||
|
node: node.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
unwatchParameterOptions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
onUpdated(async () => {
|
onUpdated(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
|
|
@ -449,7 +449,7 @@ defineExpose({
|
||||||
padding: 0 0 0 var(--spacing-s);
|
padding: 0 0 0 var(--spacing-s);
|
||||||
|
|
||||||
.parameterInput {
|
.parameterInput {
|
||||||
width: 100%;
|
width: calc(100% - var(--delete-option-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameterInput:first-child {
|
.parameterInput:first-child {
|
||||||
|
|
|
@ -1827,6 +1827,7 @@ defineExpose({ enterEditMode });
|
||||||
:mapping-enabled="mappingEnabled"
|
:mapping-enabled="mappingEnabled"
|
||||||
:distance-from-active="distanceFromActive"
|
:distance-from-active="distanceFromActive"
|
||||||
:run-index="runIndex"
|
:run-index="runIndex"
|
||||||
|
:output-index="currentOutputIndex"
|
||||||
:total-runs="maxRunIndex"
|
:total-runs="maxRunIndex"
|
||||||
:search="search"
|
:search="search"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -29,6 +29,7 @@ const props = withDefaults(
|
||||||
inputData: INodeExecutionData[];
|
inputData: INodeExecutionData[];
|
||||||
mappingEnabled?: boolean;
|
mappingEnabled?: boolean;
|
||||||
distanceFromActive: number;
|
distanceFromActive: number;
|
||||||
|
outputIndex: number | undefined;
|
||||||
runIndex: number | undefined;
|
runIndex: number | undefined;
|
||||||
totalRuns: number | undefined;
|
totalRuns: number | undefined;
|
||||||
search: string | undefined;
|
search: string | undefined;
|
||||||
|
@ -45,7 +46,6 @@ const telemetry = useTelemetry();
|
||||||
|
|
||||||
const selectedJsonPath = ref(nonExistingJsonPath);
|
const selectedJsonPath = ref(nonExistingJsonPath);
|
||||||
const draggingPath = ref<null | string>(null);
|
const draggingPath = ref<null | string>(null);
|
||||||
const displayMode = ref('json');
|
|
||||||
const jsonDataContainer = ref(null);
|
const jsonDataContainer = ref(null);
|
||||||
|
|
||||||
const { height } = useElementSize(jsonDataContainer);
|
const { height } = useElementSize(jsonDataContainer);
|
||||||
|
@ -119,12 +119,13 @@ const getListItemName = (path: string) => {
|
||||||
<LazyRunDataJsonActions
|
<LazyRunDataJsonActions
|
||||||
v-if="!editMode.enabled"
|
v-if="!editMode.enabled"
|
||||||
:node="node"
|
:node="node"
|
||||||
|
:pane-type="paneType"
|
||||||
:push-ref="pushRef"
|
:push-ref="pushRef"
|
||||||
:display-mode="displayMode"
|
|
||||||
:distance-from-active="distanceFromActive"
|
:distance-from-active="distanceFromActive"
|
||||||
:selected-json-path="selectedJsonPath"
|
:selected-json-path="selectedJsonPath"
|
||||||
:json-data="jsonData"
|
:json-data="jsonData"
|
||||||
:pane-type="paneType"
|
:output-index="outputIndex"
|
||||||
|
:run-index="runIndex"
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Draggable
|
<Draggable
|
||||||
|
|
|
@ -0,0 +1,379 @@
|
||||||
|
import { reactive } from 'vue';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
import { waitFor, cleanup, fireEvent, within, screen } from '@testing-library/vue';
|
||||||
|
|
||||||
|
import RunDataJsonActions from './RunDataJsonActions.vue';
|
||||||
|
import { nonExistingJsonPath, VIEWS } from '@/constants';
|
||||||
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { setupServer } from '@/__tests__/server';
|
||||||
|
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
|
vi.mock('vue-router', () => {
|
||||||
|
return {
|
||||||
|
useRouter: () => ({}),
|
||||||
|
useRoute: () => reactive({ meta: {} }),
|
||||||
|
RouterLink: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const copy = vi.fn();
|
||||||
|
vi.mock('@/composables/useClipboard', () => ({
|
||||||
|
useClipboard: () => ({
|
||||||
|
copy,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
async function createPiniaWithActiveNode() {
|
||||||
|
const node = mockNodes[0];
|
||||||
|
const workflow = mock<IWorkflowDb>({
|
||||||
|
id: '1',
|
||||||
|
name: 'Test Workflow',
|
||||||
|
versionId: '1',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
active: true,
|
||||||
|
connections: {},
|
||||||
|
nodes: [node],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
|
||||||
|
workflowsStore.workflow = workflow;
|
||||||
|
workflowsStore.nodeMetadata[node.name] = { pristine: true };
|
||||||
|
workflowsStore.workflowExecutionData = {
|
||||||
|
id: '1',
|
||||||
|
finished: true,
|
||||||
|
mode: 'trigger',
|
||||||
|
status: 'success',
|
||||||
|
createdAt: new Date(),
|
||||||
|
startedAt: new Date(),
|
||||||
|
workflowData: workflow,
|
||||||
|
data: {
|
||||||
|
resultData: {
|
||||||
|
runData: {
|
||||||
|
[node.name]: [
|
||||||
|
{
|
||||||
|
startTime: new Date().getTime(),
|
||||||
|
executionTime: new Date().getTime(),
|
||||||
|
data: {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
id: 1,
|
||||||
|
name: 'First run 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
id: 2,
|
||||||
|
name: 'First run 2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
source: [null],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: new Date().getTime(),
|
||||||
|
executionTime: new Date().getTime(),
|
||||||
|
data: {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
id: 3,
|
||||||
|
name: 'Second run 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
source: [null],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ndvStore.activeNodeName = node.name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pinia,
|
||||||
|
activeNode: ndvStore.activeNode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RunDataJsonActions', () => {
|
||||||
|
let server: ReturnType<typeof setupServer>;
|
||||||
|
|
||||||
|
beforeEach(cleanup);
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
document.body.innerHTML = '<div id="app-grid"></div>';
|
||||||
|
server = setupServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
copy.mockReset();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
server.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy unselected JSON output from latest run on click', async () => {
|
||||||
|
const { pinia, activeNode } = await createPiniaWithActiveNode();
|
||||||
|
const renderComponent = createComponentRenderer(RunDataJsonActions, {
|
||||||
|
props: {
|
||||||
|
node: activeNode,
|
||||||
|
paneType: 'output',
|
||||||
|
pushRef: 'ref',
|
||||||
|
displayMode: 'json',
|
||||||
|
distanceFromActive: 0,
|
||||||
|
selectedJsonPath: nonExistingJsonPath,
|
||||||
|
jsonData: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Second run 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputIndex: 0,
|
||||||
|
runIndex: 1,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$route: {
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
pinia,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByTestId('ndv-json-actions')).toBeInTheDocument());
|
||||||
|
|
||||||
|
const button = within(getByTestId('ndv-json-actions')).getByRole('button');
|
||||||
|
|
||||||
|
await fireEvent.click(button);
|
||||||
|
|
||||||
|
expect(copy).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Second run 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should copy unselected JSON output from selected previous run on click', async () => {
|
||||||
|
const { pinia, activeNode } = await createPiniaWithActiveNode();
|
||||||
|
const renderComponent = createComponentRenderer(RunDataJsonActions, {
|
||||||
|
props: {
|
||||||
|
node: activeNode,
|
||||||
|
paneType: 'output',
|
||||||
|
pushRef: 'ref',
|
||||||
|
displayMode: 'json',
|
||||||
|
distanceFromActive: 0,
|
||||||
|
selectedJsonPath: nonExistingJsonPath,
|
||||||
|
jsonData: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Second run 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputIndex: 0,
|
||||||
|
runIndex: 0,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$route: {
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
pinia,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByTestId('ndv-json-actions')).toBeInTheDocument());
|
||||||
|
|
||||||
|
const button = within(getByTestId('ndv-json-actions')).getByRole('button');
|
||||||
|
|
||||||
|
await fireEvent.click(button);
|
||||||
|
|
||||||
|
expect(copy).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'First run 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'First run 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should copy selected JSON value on 'Copy Selection' click", async () => {
|
||||||
|
const { pinia, activeNode } = await createPiniaWithActiveNode();
|
||||||
|
const renderComponent = createComponentRenderer(RunDataJsonActions, {
|
||||||
|
props: {
|
||||||
|
node: activeNode,
|
||||||
|
paneType: 'output',
|
||||||
|
pushRef: 'ref',
|
||||||
|
displayMode: 'json',
|
||||||
|
distanceFromActive: 0,
|
||||||
|
selectedJsonPath: '[0].name',
|
||||||
|
jsonData: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Second run 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputIndex: 0,
|
||||||
|
runIndex: 1,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$route: {
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent({
|
||||||
|
pinia,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(i18n.baseText('runData.copyValue'))).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const option = screen.getByText(i18n.baseText('runData.copyValue'));
|
||||||
|
|
||||||
|
await fireEvent.click(option);
|
||||||
|
|
||||||
|
expect(copy).toHaveBeenCalledWith('Second run 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should copy selected JSON value's item path on 'Copy Item Path' click", async () => {
|
||||||
|
const { pinia, activeNode } = await createPiniaWithActiveNode();
|
||||||
|
const renderComponent = createComponentRenderer(RunDataJsonActions, {
|
||||||
|
props: {
|
||||||
|
node: activeNode,
|
||||||
|
paneType: 'output',
|
||||||
|
pushRef: 'ref',
|
||||||
|
displayMode: 'json',
|
||||||
|
distanceFromActive: 0,
|
||||||
|
selectedJsonPath: '[0].name',
|
||||||
|
jsonData: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Second run 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputIndex: 0,
|
||||||
|
runIndex: 1,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$route: {
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent({
|
||||||
|
pinia,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(i18n.baseText('runData.copyItemPath'))).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const option = screen.getByText(i18n.baseText('runData.copyItemPath'));
|
||||||
|
|
||||||
|
await fireEvent.click(option);
|
||||||
|
|
||||||
|
expect(copy).toHaveBeenCalledWith('{{ $item("0").$node["Manual Trigger"].json["name"] }}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should copy selected JSON value's parameter path on 'Copy Parameter Path' click", async () => {
|
||||||
|
const { pinia, activeNode } = await createPiniaWithActiveNode();
|
||||||
|
const renderComponent = createComponentRenderer(RunDataJsonActions, {
|
||||||
|
props: {
|
||||||
|
node: activeNode,
|
||||||
|
paneType: 'output',
|
||||||
|
pushRef: 'ref',
|
||||||
|
displayMode: 'json',
|
||||||
|
distanceFromActive: 0,
|
||||||
|
selectedJsonPath: '[0].name',
|
||||||
|
jsonData: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Second run 1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputIndex: 0,
|
||||||
|
runIndex: 1,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$route: {
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent({
|
||||||
|
pinia,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText(i18n.baseText('runData.copyParameterPath'))).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const option = screen.getByText(i18n.baseText('runData.copyParameterPath'));
|
||||||
|
|
||||||
|
await fireEvent.click(option);
|
||||||
|
|
||||||
|
expect(copy).toHaveBeenCalledWith('{{ $node["Manual Trigger"].json["name"] }}');
|
||||||
|
});
|
||||||
|
});
|
|
@ -26,12 +26,11 @@ const props = withDefaults(
|
||||||
node: INodeUi;
|
node: INodeUi;
|
||||||
paneType: string;
|
paneType: string;
|
||||||
pushRef: string;
|
pushRef: string;
|
||||||
displayMode: string;
|
|
||||||
distanceFromActive: number;
|
distanceFromActive: number;
|
||||||
selectedJsonPath: string;
|
selectedJsonPath: string;
|
||||||
jsonData: IDataObject[];
|
jsonData: IDataObject[];
|
||||||
currentOutputIndex?: number;
|
outputIndex: number | undefined;
|
||||||
runIndex?: number;
|
runIndex: number | undefined;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
selectedJsonPath: nonExistingJsonPath,
|
selectedJsonPath: nonExistingJsonPath,
|
||||||
|
@ -71,7 +70,7 @@ function getJsonValue(): string {
|
||||||
selectedValue = clearJsonKey(pinnedData.data.value as object);
|
selectedValue = clearJsonKey(pinnedData.data.value as object);
|
||||||
} else {
|
} else {
|
||||||
selectedValue = executionDataToJson(
|
selectedValue = executionDataToJson(
|
||||||
nodeHelpers.getNodeInputData(props.node, props.runIndex, props.currentOutputIndex),
|
nodeHelpers.getNodeInputData(props.node, props.runIndex, props.outputIndex),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,7 +175,7 @@ function handleCopyClick(commandData: { command: string }) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.actionsGroup">
|
<div :class="$style.actionsGroup" data-test-id="ndv-json-actions">
|
||||||
<n8n-icon-button
|
<n8n-icon-button
|
||||||
v-if="noSelection"
|
v-if="noSelection"
|
||||||
:title="i18n.baseText('runData.copyToClipboard')"
|
:title="i18n.baseText('runData.copyToClipboard')"
|
||||||
|
|
|
@ -314,7 +314,7 @@ const onDragEnd = (el: HTMLElement) => {
|
||||||
@click="toggleNodeAndScrollTop(item.id)"
|
@click="toggleNodeAndScrollTop(item.id)"
|
||||||
/>
|
/>
|
||||||
<VirtualSchemaItem
|
<VirtualSchemaItem
|
||||||
v-else
|
v-else-if="item.type === 'item'"
|
||||||
v-bind="item"
|
v-bind="item"
|
||||||
:search="search"
|
:search="search"
|
||||||
:draggable="mappingEnabled"
|
:draggable="mappingEnabled"
|
||||||
|
@ -323,6 +323,10 @@ const onDragEnd = (el: HTMLElement) => {
|
||||||
@click="toggleLeaf(item.id)"
|
@click="toggleLeaf(item.id)"
|
||||||
>
|
>
|
||||||
</VirtualSchemaItem>
|
</VirtualSchemaItem>
|
||||||
|
|
||||||
|
<N8nTooltip v-else-if="item.type === 'icon'" :content="item.tooltip" placement="top">
|
||||||
|
<N8nIcon :size="14" :icon="item.icon" class="icon" />
|
||||||
|
</N8nTooltip>
|
||||||
</DynamicScrollerItem>
|
</DynamicScrollerItem>
|
||||||
</template>
|
</template>
|
||||||
</DynamicScroller>
|
</DynamicScroller>
|
||||||
|
@ -347,4 +351,11 @@ const onDragEnd = (el: HTMLElement) => {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: var(--spacing-s) var(--spacing-s) var(--spacing-xl) var(--spacing-s);
|
padding: var(--spacing-s) var(--spacing-s) var(--spacing-xl) var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-left: var(--spacing-xl);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,8 +4,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
import { type INodeTypeDescription } from 'n8n-workflow';
|
import { type INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
import { DATA_EDITING_DOCS_URL } from '@/constants';
|
import { SCHEMA_PREVIEW_DOCS_URL } from '@/constants';
|
||||||
import { N8nNotice } from '@n8n/design-system';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -42,24 +41,28 @@ const emit = defineEmits<{
|
||||||
<span v-if="info" class="info">{{ info }}</span>
|
<span v-if="info" class="info">{{ info }}</span>
|
||||||
</div>
|
</div>
|
||||||
<FontAwesomeIcon v-if="isTrigger" class="trigger-icon" icon="bolt" size="xs" />
|
<FontAwesomeIcon v-if="isTrigger" class="trigger-icon" icon="bolt" size="xs" />
|
||||||
<div v-if="itemCount" class="item-count" data-test-id="run-data-schema-node-item-count">
|
<div v-if="itemCount" class="extra-info" data-test-id="run-data-schema-node-item-count">
|
||||||
{{ i18n.baseText('ndv.output.items', { interpolate: { count: itemCount } }) }}
|
{{ i18n.baseText('ndv.output.items', { interpolate: { count: itemCount } }) }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="preview" class="extra-info">
|
||||||
|
{{ i18n.baseText('dataMapping.schemaView.previewNode') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<N8nNotice
|
<div
|
||||||
v-if="preview && !collapsed"
|
v-if="preview && !collapsed"
|
||||||
class="notice"
|
class="notice"
|
||||||
theme="warning"
|
theme="warning"
|
||||||
data-test-id="schema-preview-warning"
|
data-test-id="schema-preview-warning"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<i18n-t keypath="dataMapping.schemaView.preview">
|
<i18n-t keypath="dataMapping.schemaView.preview">
|
||||||
<template #link>
|
<template #link>
|
||||||
<N8nLink :to="DATA_EDITING_DOCS_URL" size="small">
|
<N8nLink :to="SCHEMA_PREVIEW_DOCS_URL" size="small" bold>
|
||||||
{{ i18n.baseText('generic.learnMore') }}
|
{{ i18n.baseText('generic.learnMore') }}
|
||||||
</N8nLink>
|
</N8nLink>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</N8nNotice>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -117,7 +120,7 @@ const emit = defineEmits<{
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-count {
|
.extra-info {
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
@ -126,6 +129,9 @@ const emit = defineEmits<{
|
||||||
.notice {
|
.notice {
|
||||||
margin-left: var(--spacing-2xl);
|
margin-left: var(--spacing-2xl);
|
||||||
margin-top: var(--spacing-2xs);
|
margin-top: var(--spacing-2xs);
|
||||||
margin-bottom: 0;
|
padding-bottom: var(--spacing-2xs);
|
||||||
|
color: var(--color-text-base);
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -43,7 +43,10 @@ const emit = defineEmits<{
|
||||||
:data-node-type="nodeType"
|
:data-node-type="nodeType"
|
||||||
data-target="mappable"
|
data-target="mappable"
|
||||||
class="pill"
|
class="pill"
|
||||||
:class="{ 'pill--highlight': highlight, 'pill--preview': preview }"
|
:class="{
|
||||||
|
'pill--highlight': highlight,
|
||||||
|
'pill--preview': preview,
|
||||||
|
}"
|
||||||
data-test-id="run-data-schema-node-name"
|
data-test-id="run-data-schema-node-name"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon class="type-icon" :icon size="sm" />
|
<FontAwesomeIcon class="type-icon" :icon size="sm" />
|
||||||
|
@ -77,6 +80,7 @@ const emit = defineEmits<{
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
|
color: var(--color-text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
|
@ -98,16 +102,31 @@ const emit = defineEmits<{
|
||||||
}
|
}
|
||||||
|
|
||||||
&.pill--preview {
|
&.pill--preview {
|
||||||
border-style: dashed;
|
/* Cannot use CSS variable inside data URL, so instead switching based on data-theme and media query */
|
||||||
border-width: 1.5px;
|
--schema-preview-dashed-border: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' viewBox='0 0 400 400' fill='none' rx='4' ry='4' stroke='%230000002A' stroke-width='2' stroke-dasharray='4%2c 4' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
|
||||||
|
--schema-preview-dashed-border-dark: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' viewBox='0 0 400 400' fill='none' rx='4' ry='4' stroke='%23FFFFFF2A' stroke-width='2' stroke-dasharray='4%2c 4' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
|
||||||
|
color: var(--color-text-light);
|
||||||
|
background-color: var(--color-run-data-background);
|
||||||
|
border: none;
|
||||||
|
max-width: calc(100% - var(--spacing-l));
|
||||||
|
background-image: var(--schema-preview-dashed-border);
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
border-left: 1.5px dashed var(--color-foreground-light);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body:not([data-theme]) .pill--preview {
|
||||||
|
background-image: var(--schema-preview-dashed-border-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .pill--preview {
|
||||||
|
background-image: var(--schema-preview-dashed-border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
.draggable .pill.pill--highlight {
|
.draggable .pill.pill--highlight {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
border-color: var(--color-primary-tint-1);
|
border-color: var(--color-primary-tint-1);
|
||||||
|
|
|
@ -110,7 +110,12 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
swapopacity="false"
|
swapopacity="false"
|
||||||
symbol="false"
|
symbol="false"
|
||||||
/>
|
/>
|
||||||
<!--v-if-->
|
<div
|
||||||
|
class="extra-info"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="notice"
|
class="notice"
|
||||||
|
@ -119,13 +124,12 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
theme="warning"
|
theme="warning"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
Usually outputs the following fields. Execute the node to see the actual ones.
|
||||||
This is a preview of the schema, execute the node to see the exact schema and data.
|
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="n8n-link"
|
class="n8n-link"
|
||||||
data-v-882a318e=""
|
data-v-882a318e=""
|
||||||
href="https://docs.n8n.io/data/data-editing/"
|
href="https://docs.n8n.io/data/schema-preview/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -133,7 +137,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
class="primary"
|
class="primary"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="n8n-text size-small regular"
|
class="n8n-text size-small bold"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
|
@ -146,7 +150,6 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -328,70 +331,38 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="schema-item draggable"
|
<span
|
||||||
data-test-id="run-data-schema-item"
|
class="n8n-text compact size-14 regular n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger"
|
||||||
data-v-0f5e7239=""
|
|
||||||
data-v-d00cba9a=""
|
data-v-d00cba9a=""
|
||||||
type="item"
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
class="toggle-container"
|
<font-awesome-icon-stub
|
||||||
data-v-0f5e7239=""
|
beat="false"
|
||||||
>
|
beatfade="false"
|
||||||
<!--v-if-->
|
border="false"
|
||||||
</div>
|
bounce="false"
|
||||||
<div
|
class="14"
|
||||||
class="pill pill--preview"
|
fade="false"
|
||||||
data-name="..."
|
fixedwidth="false"
|
||||||
data-nest-level="1"
|
flash="false"
|
||||||
data-target="mappable"
|
flip="false"
|
||||||
data-test-id="run-data-schema-node-name"
|
icon="ellipsis-h"
|
||||||
data-v-0f5e7239=""
|
inverse="false"
|
||||||
>
|
listitem="false"
|
||||||
<font-awesome-icon-stub
|
pulse="false"
|
||||||
beat="false"
|
shake="false"
|
||||||
beatfade="false"
|
spin="false"
|
||||||
border="false"
|
spinpulse="false"
|
||||||
bounce="false"
|
spinreverse="false"
|
||||||
class="type-icon"
|
swapopacity="false"
|
||||||
data-v-0f5e7239=""
|
symbol="false"
|
||||||
fade="false"
|
/>
|
||||||
fixedwidth="false"
|
|
||||||
flash="false"
|
</span>
|
||||||
flip="false"
|
<!--teleport start-->
|
||||||
icon=""
|
<!--teleport end-->
|
||||||
inverse="false"
|
|
||||||
listitem="false"
|
|
||||||
pulse="false"
|
|
||||||
shake="false"
|
|
||||||
size="sm"
|
|
||||||
spin="false"
|
|
||||||
spinpulse="false"
|
|
||||||
spinreverse="false"
|
|
||||||
swapopacity="false"
|
|
||||||
symbol="false"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="content title"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<!--v-if-->
|
|
||||||
...
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="content text"
|
|
||||||
data-test-id="run-data-schema-item-value"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
<span />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -464,7 +435,12 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<div
|
||||||
|
class="extra-info"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="notice"
|
class="notice"
|
||||||
|
@ -473,13 +449,12 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
theme="warning"
|
theme="warning"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
Usually outputs the following fields. Execute the node to see the actual ones.
|
||||||
This is a preview of the schema, execute the node to see the exact schema and data.
|
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="n8n-link"
|
class="n8n-link"
|
||||||
data-v-882a318e=""
|
data-v-882a318e=""
|
||||||
href="https://docs.n8n.io/data/data-editing/"
|
href="https://docs.n8n.io/data/schema-preview/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -487,7 +462,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
class="primary"
|
class="primary"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="n8n-text size-small regular"
|
class="n8n-text size-small bold"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
||||||
|
@ -500,7 +475,6 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -682,70 +656,38 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="schema-item draggable"
|
<span
|
||||||
data-test-id="run-data-schema-item"
|
class="n8n-text compact size-14 regular n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger"
|
||||||
data-v-0f5e7239=""
|
|
||||||
data-v-d00cba9a=""
|
data-v-d00cba9a=""
|
||||||
type="item"
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
class="toggle-container"
|
<font-awesome-icon-stub
|
||||||
data-v-0f5e7239=""
|
beat="false"
|
||||||
>
|
beatfade="false"
|
||||||
<!--v-if-->
|
border="false"
|
||||||
</div>
|
bounce="false"
|
||||||
<div
|
class="14"
|
||||||
class="pill pill--preview"
|
fade="false"
|
||||||
data-name="..."
|
fixedwidth="false"
|
||||||
data-nest-level="1"
|
flash="false"
|
||||||
data-target="mappable"
|
flip="false"
|
||||||
data-test-id="run-data-schema-node-name"
|
icon="ellipsis-h"
|
||||||
data-v-0f5e7239=""
|
inverse="false"
|
||||||
>
|
listitem="false"
|
||||||
<font-awesome-icon-stub
|
pulse="false"
|
||||||
beat="false"
|
shake="false"
|
||||||
beatfade="false"
|
spin="false"
|
||||||
border="false"
|
spinpulse="false"
|
||||||
bounce="false"
|
spinreverse="false"
|
||||||
class="type-icon"
|
swapopacity="false"
|
||||||
data-v-0f5e7239=""
|
symbol="false"
|
||||||
fade="false"
|
/>
|
||||||
fixedwidth="false"
|
|
||||||
flash="false"
|
</span>
|
||||||
flip="false"
|
<!--teleport start-->
|
||||||
icon=""
|
<!--teleport end-->
|
||||||
inverse="false"
|
|
||||||
listitem="false"
|
|
||||||
pulse="false"
|
|
||||||
shake="false"
|
|
||||||
size="sm"
|
|
||||||
spin="false"
|
|
||||||
spinpulse="false"
|
|
||||||
spinreverse="false"
|
|
||||||
swapopacity="false"
|
|
||||||
symbol="false"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="content title"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<!--v-if-->
|
|
||||||
...
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="content text"
|
|
||||||
data-test-id="run-data-schema-item-value"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
<span />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -821,7 +763,7 @@ exports[`VirtualSchema.vue > renders previous nodes schema for AI tools 1`] = `
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
class="item-count"
|
class="extra-info"
|
||||||
data-test-id="run-data-schema-node-item-count"
|
data-test-id="run-data-schema-node-item-count"
|
||||||
data-v-882a318e=""
|
data-v-882a318e=""
|
||||||
>
|
>
|
||||||
|
@ -893,7 +835,7 @@ exports[`VirtualSchema.vue > renders schema for correct output branch 1`] = `
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<div
|
<div
|
||||||
class="item-count"
|
class="extra-info"
|
||||||
data-test-id="run-data-schema-node-item-count"
|
data-test-id="run-data-schema-node-item-count"
|
||||||
data-v-882a318e=""
|
data-v-882a318e=""
|
||||||
>
|
>
|
||||||
|
@ -1447,7 +1389,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
|
||||||
symbol="false"
|
symbol="false"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="item-count"
|
class="extra-info"
|
||||||
data-test-id="run-data-schema-node-item-count"
|
data-test-id="run-data-schema-node-item-count"
|
||||||
data-v-882a318e=""
|
data-v-882a318e=""
|
||||||
>
|
>
|
||||||
|
|
|
@ -261,7 +261,14 @@ export type RenderHeader = {
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Renders = RenderHeader | RenderItem;
|
export type RenderIcon = {
|
||||||
|
id: string;
|
||||||
|
type: 'icon';
|
||||||
|
icon: string;
|
||||||
|
tooltip: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Renders = RenderHeader | RenderItem | RenderIcon;
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
object: 'cube',
|
object: 'cube',
|
||||||
|
@ -285,13 +292,11 @@ const emptyItem = (): RenderItem => ({
|
||||||
type: 'item',
|
type: 'item',
|
||||||
});
|
});
|
||||||
|
|
||||||
const dummyItem = (): RenderItem => ({
|
const moreFieldsItem = (): RenderIcon => ({
|
||||||
id: `dummy-${window.crypto.randomUUID()}`,
|
id: `moreFields-${window.crypto.randomUUID()}`,
|
||||||
icon: '',
|
type: 'icon',
|
||||||
level: 1,
|
icon: 'ellipsis-h',
|
||||||
title: '...',
|
tooltip: useI18n().baseText('dataMapping.schemaView.previewExtraFields'),
|
||||||
type: 'item',
|
|
||||||
preview: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDataEmpty = (schema: Schema) => {
|
const isDataEmpty = (schema: Schema) => {
|
||||||
|
@ -445,7 +450,7 @@ export const useFlattenSchema = () => {
|
||||||
acc.push(...flattenSchema(item));
|
acc.push(...flattenSchema(item));
|
||||||
|
|
||||||
if (item.preview) {
|
if (item.preview) {
|
||||||
acc.push(dummyItem());
|
acc.push(moreFieldsItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|
|
@ -92,6 +92,7 @@ export const BUILTIN_NODES_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/built
|
||||||
export const BUILTIN_CREDENTIALS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/builtin/credentials/`;
|
export const BUILTIN_CREDENTIALS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/builtin/credentials/`;
|
||||||
export const DATA_PINNING_DOCS_URL = `https://${DOCS_DOMAIN}/data/data-pinning/`;
|
export const DATA_PINNING_DOCS_URL = `https://${DOCS_DOMAIN}/data/data-pinning/`;
|
||||||
export const DATA_EDITING_DOCS_URL = `https://${DOCS_DOMAIN}/data/data-editing/`;
|
export const DATA_EDITING_DOCS_URL = `https://${DOCS_DOMAIN}/data/data-editing/`;
|
||||||
|
export const SCHEMA_PREVIEW_DOCS_URL = `https://${DOCS_DOMAIN}/data/schema-preview/`;
|
||||||
export const MFA_DOCS_URL = `https://${DOCS_DOMAIN}/user-management/two-factor-auth/`;
|
export const MFA_DOCS_URL = `https://${DOCS_DOMAIN}/user-management/two-factor-auth/`;
|
||||||
export const NPM_COMMUNITY_NODE_SEARCH_API_URL = 'https://api.npms.io/v2/';
|
export const NPM_COMMUNITY_NODE_SEARCH_API_URL = 'https://api.npms.io/v2/';
|
||||||
export const NPM_PACKAGE_DOCS_BASE_URL = 'https://www.npmjs.com/package/';
|
export const NPM_PACKAGE_DOCS_BASE_URL = 'https://www.npmjs.com/package/';
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { Text, type Extension } from '@codemirror/state';
|
||||||
import { EditorView, hoverTooltip } from '@codemirror/view';
|
import { EditorView, hoverTooltip } from '@codemirror/view';
|
||||||
import * as Comlink from 'comlink';
|
import * as Comlink from 'comlink';
|
||||||
import { NodeConnectionType, type CodeExecutionMode, type INodeExecutionData } from 'n8n-workflow';
|
import { NodeConnectionType, type CodeExecutionMode, type INodeExecutionData } from 'n8n-workflow';
|
||||||
import { ref, toRef, toValue, watch, type MaybeRefOrGetter } from 'vue';
|
import { onBeforeUnmount, ref, toRef, toValue, watch, type MaybeRefOrGetter } from 'vue';
|
||||||
import type { LanguageServiceWorker, RemoteLanguageServiceWorkerInit } from '../types';
|
import type { LanguageServiceWorker, RemoteLanguageServiceWorkerInit } from '../types';
|
||||||
import { typescriptCompletionSource } from './completions';
|
import { typescriptCompletionSource } from './completions';
|
||||||
import { typescriptWorkerFacet } from './facet';
|
import { typescriptWorkerFacet } from './facet';
|
||||||
|
@ -33,11 +33,13 @@ export function useTypescript(
|
||||||
const { debounce } = useDebounce();
|
const { debounce } = useDebounce();
|
||||||
const activeNodeName = ndvStore.activeNodeName;
|
const activeNodeName = ndvStore.activeNodeName;
|
||||||
const worker = ref<Comlink.Remote<LanguageServiceWorker>>();
|
const worker = ref<Comlink.Remote<LanguageServiceWorker>>();
|
||||||
|
const webWorker = ref<Worker>();
|
||||||
|
|
||||||
async function createWorker(): Promise<Extension> {
|
async function createWorker(): Promise<Extension> {
|
||||||
const { init } = Comlink.wrap<RemoteLanguageServiceWorkerInit>(
|
webWorker.value = new Worker(new URL('../worker/typescript.worker.ts', import.meta.url), {
|
||||||
new Worker(new URL('../worker/typescript.worker.ts', import.meta.url), { type: 'module' }),
|
type: 'module',
|
||||||
);
|
});
|
||||||
|
const { init } = Comlink.wrap<RemoteLanguageServiceWorkerInit>(webWorker.value);
|
||||||
worker.value = await init(
|
worker.value = await init(
|
||||||
{
|
{
|
||||||
id: toValue(id),
|
id: toValue(id),
|
||||||
|
@ -125,6 +127,10 @@ export function useTypescript(
|
||||||
forceParse(editor);
|
forceParse(editor);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (webWorker.value) webWorker.value.terminate();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createWorker,
|
createWorker,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { mock, mockDeep } from 'vitest-mock-extended';
|
||||||
|
import type * as tsvfs from '@typescript/vfs';
|
||||||
|
import ts from 'typescript';
|
||||||
|
import { getDiagnostics, tsCategoryToSeverity } from './linter';
|
||||||
|
|
||||||
|
describe('linter > getDiagnostics', () => {
|
||||||
|
it('should return diagnostics for a given file', () => {
|
||||||
|
const env = mockDeep<tsvfs.VirtualTypeScriptEnvironment>();
|
||||||
|
const fileName = 'testFile.ts';
|
||||||
|
|
||||||
|
const semanticDiagnostics = [
|
||||||
|
mock<ts.DiagnosticWithLocation>({
|
||||||
|
file: { fileName },
|
||||||
|
start: 0,
|
||||||
|
length: 10,
|
||||||
|
messageText: 'Semantic error',
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
code: 1234,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const syntacticDiagnostics = [
|
||||||
|
mock<ts.DiagnosticWithLocation>({
|
||||||
|
file: { fileName },
|
||||||
|
start: 15,
|
||||||
|
length: 5,
|
||||||
|
messageText: 'Syntactic warning',
|
||||||
|
category: ts.DiagnosticCategory.Warning,
|
||||||
|
code: 5678,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
env.languageService.getSemanticDiagnostics.mockReturnValue(semanticDiagnostics);
|
||||||
|
env.languageService.getSyntacticDiagnostics.mockReturnValue(syntacticDiagnostics);
|
||||||
|
env.getSourceFile.mockReturnValue({ fileName } as ts.SourceFile);
|
||||||
|
|
||||||
|
const result = getDiagnostics({ env, fileName });
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].message).toBe('Semantic error');
|
||||||
|
expect(result[0].severity).toBe('error');
|
||||||
|
expect(result[1].message).toBe('Syntactic warning');
|
||||||
|
expect(result[1].severity).toBe('warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out ignored diagnostics', () => {
|
||||||
|
const env = mockDeep<tsvfs.VirtualTypeScriptEnvironment>();
|
||||||
|
const fileName = 'testFile.ts';
|
||||||
|
|
||||||
|
const diagnostics = [
|
||||||
|
mock<ts.DiagnosticWithLocation>({
|
||||||
|
file: { fileName },
|
||||||
|
start: 0,
|
||||||
|
length: 10,
|
||||||
|
messageText: 'No implicit any',
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
code: 7006,
|
||||||
|
}),
|
||||||
|
mock<ts.DiagnosticWithLocation>({
|
||||||
|
file: { fileName },
|
||||||
|
start: 0,
|
||||||
|
length: 10,
|
||||||
|
messageText: 'Cannot find module or its corresponding type declarations.',
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
code: 2307,
|
||||||
|
}),
|
||||||
|
mock<ts.DiagnosticWithLocation>({
|
||||||
|
file: { fileName },
|
||||||
|
start: 0,
|
||||||
|
length: 10,
|
||||||
|
messageText: 'Not ignored error',
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
code: 1234,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
env.languageService.getSemanticDiagnostics.mockReturnValue(diagnostics);
|
||||||
|
env.languageService.getSyntacticDiagnostics.mockReturnValue([]);
|
||||||
|
env.getSourceFile.mockReturnValue({ fileName } as ts.SourceFile);
|
||||||
|
|
||||||
|
const result = getDiagnostics({ env, fileName });
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].message).toBe('Not ignored error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map diagnostic categories to severities correctly', () => {
|
||||||
|
const diagnostic = mock<ts.DiagnosticWithLocation>({
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
code: 1234,
|
||||||
|
});
|
||||||
|
|
||||||
|
const severity = tsCategoryToSeverity(diagnostic);
|
||||||
|
expect(severity).toBe('error');
|
||||||
|
});
|
||||||
|
});
|
|
@ -49,8 +49,15 @@ function isDiagnosticWithLocation(
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIgnoredDiagnostic(diagnostic: ts.Diagnostic) {
|
function isIgnoredDiagnostic(diagnostic: ts.Diagnostic) {
|
||||||
// No implicit any
|
switch (diagnostic.code) {
|
||||||
return diagnostic.code === 7006;
|
// No implicit any
|
||||||
|
case 7006:
|
||||||
|
// Cannot find module or its corresponding type declarations.
|
||||||
|
case 2307:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
import type { WorkerInitOptions } from '../types';
|
||||||
|
import { worker } from './typescript.worker';
|
||||||
|
import { type ChangeSet, EditorState } from '@codemirror/state';
|
||||||
|
|
||||||
|
async function createWorker({
|
||||||
|
doc,
|
||||||
|
options,
|
||||||
|
}: { doc?: string; options?: Partial<WorkerInitOptions> } = {}) {
|
||||||
|
const defaultDoc = `
|
||||||
|
function myFunction(){
|
||||||
|
if (true){
|
||||||
|
const myObj = {test: "value"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $input.all();`;
|
||||||
|
const state = EditorState.create({ doc: doc ?? defaultDoc });
|
||||||
|
|
||||||
|
const tsWorker = worker.init(
|
||||||
|
{
|
||||||
|
allNodeNames: [],
|
||||||
|
content: state.doc.toJSON(),
|
||||||
|
id: 'id',
|
||||||
|
inputNodeNames: [],
|
||||||
|
mode: 'runOnceForAllItems',
|
||||||
|
variables: [],
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
async () => ({
|
||||||
|
json: { path: '', type: 'string', value: '' },
|
||||||
|
binary: [],
|
||||||
|
params: { path: '', type: 'string', value: '' },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return await tsWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Typescript Worker', () => {
|
||||||
|
it('should return diagnostics', async () => {
|
||||||
|
const tsWorker = await createWorker();
|
||||||
|
|
||||||
|
expect(tsWorker.getDiagnostics()).toEqual([
|
||||||
|
{
|
||||||
|
from: 10,
|
||||||
|
markClass: 'cm-faded',
|
||||||
|
message: "'myFunction' is declared but its value is never read.",
|
||||||
|
severity: 'warning',
|
||||||
|
to: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 47,
|
||||||
|
markClass: 'cm-faded',
|
||||||
|
message: "'myObj' is declared but its value is never read.",
|
||||||
|
severity: 'warning',
|
||||||
|
to: 52,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept updates from the client and buffer them', async () => {
|
||||||
|
const tsWorker = await createWorker();
|
||||||
|
// Add if statement and remove indentation
|
||||||
|
const changes = [
|
||||||
|
[75, [0, '', ''], 22],
|
||||||
|
[76, [0, '', ''], 22],
|
||||||
|
[77, [0, ' if (true){', ' const myObj = {test: "value"}', ' }'], 22],
|
||||||
|
[77, [1], 13, [2], 30, [2], 23],
|
||||||
|
];
|
||||||
|
|
||||||
|
vi.useFakeTimers({ toFake: ['setTimeout', 'queueMicrotask', 'nextTick'] });
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
tsWorker.updateFile(change as unknown as ChangeSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(tsWorker.getDiagnostics()).toHaveLength(2);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
vi.runAllTicks();
|
||||||
|
|
||||||
|
expect(tsWorker.getDiagnostics()).toHaveLength(3);
|
||||||
|
expect(tsWorker.getDiagnostics()).toEqual([
|
||||||
|
{
|
||||||
|
from: 10,
|
||||||
|
markClass: 'cm-faded',
|
||||||
|
message: "'myFunction' is declared but its value is never read.",
|
||||||
|
severity: 'warning',
|
||||||
|
to: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 47,
|
||||||
|
markClass: 'cm-faded',
|
||||||
|
message: "'myObj' is declared but its value is never read.",
|
||||||
|
severity: 'warning',
|
||||||
|
to: 52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 96,
|
||||||
|
markClass: 'cm-faded',
|
||||||
|
message: "'myObj' is declared but its value is never read.",
|
||||||
|
severity: 'warning',
|
||||||
|
to: 101,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return completions', async () => {
|
||||||
|
const doc = 'return $input.';
|
||||||
|
const tsWorker = await createWorker({ doc });
|
||||||
|
|
||||||
|
const completionResult = await tsWorker.getCompletionsAtPos(doc.length);
|
||||||
|
assert(completionResult !== null);
|
||||||
|
|
||||||
|
const completionLabels = completionResult.result.options.map((c) => c.label);
|
||||||
|
expect(completionLabels).toContain('all()');
|
||||||
|
expect(completionLabels).toContain('first()');
|
||||||
|
});
|
||||||
|
});
|
|
@ -28,7 +28,7 @@ import { until } from '@vueuse/core';
|
||||||
|
|
||||||
self.process = { env: {} } as NodeJS.Process;
|
self.process = { env: {} } as NodeJS.Process;
|
||||||
|
|
||||||
const worker: LanguageServiceWorkerInit = {
|
export const worker: LanguageServiceWorkerInit = {
|
||||||
async init(options, nodeDataFetcher) {
|
async init(options, nodeDataFetcher) {
|
||||||
const loadedNodeTypesMap: Map<string, { type: string; typeName: string }> = reactive(new Map());
|
const loadedNodeTypesMap: Map<string, { type: string; typeName: string }> = reactive(new Map());
|
||||||
|
|
||||||
|
@ -157,11 +157,11 @@ const worker: LanguageServiceWorkerInit = {
|
||||||
});
|
});
|
||||||
|
|
||||||
const applyChangesToCode = bufferChangeSets((bufferedChanges) => {
|
const applyChangesToCode = bufferChangeSets((bufferedChanges) => {
|
||||||
bufferedChanges.iterChanges((start, end, _fromNew, _toNew, text) => {
|
bufferedChanges.iterChanges((start, end, fromNew, _toNew, text) => {
|
||||||
const length = end - start;
|
const length = end - start;
|
||||||
|
|
||||||
env.updateFile(codeFileName, text.toString(), {
|
env.updateFile(codeFileName, text.toString(), {
|
||||||
start: editorPositionToTypescript(start),
|
start: editorPositionToTypescript(fromNew),
|
||||||
length,
|
length,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -661,8 +661,9 @@
|
||||||
"dataMapping.schemaView.emptyData": "No fields - item(s) exist, but they're empty",
|
"dataMapping.schemaView.emptyData": "No fields - item(s) exist, but they're empty",
|
||||||
"dataMapping.schemaView.disabled": "This node is disabled and will just pass data through",
|
"dataMapping.schemaView.disabled": "This node is disabled and will just pass data through",
|
||||||
"dataMapping.schemaView.noMatches": "No results for '{search}'",
|
"dataMapping.schemaView.noMatches": "No results for '{search}'",
|
||||||
"dataMapping.schemaView.preview": "This is a preview of the schema, execute the node to see the exact schema and data. {link}",
|
"dataMapping.schemaView.preview": "Usually outputs the following fields. Execute the node to see the actual ones. {link}",
|
||||||
"dataMapping.schemaView.previewNode": "(schema preview)",
|
"dataMapping.schemaView.previewExtraFields": "There may be more fields. Execute the node to be sure.",
|
||||||
|
"dataMapping.schemaView.previewNode": "Preview",
|
||||||
"displayWithChange.cancelEdit": "Cancel Edit",
|
"displayWithChange.cancelEdit": "Cancel Edit",
|
||||||
"displayWithChange.clickToChange": "Click to Change",
|
"displayWithChange.clickToChange": "Click to Change",
|
||||||
"displayWithChange.setValue": "Set Value",
|
"displayWithChange.setValue": "Set Value",
|
||||||
|
|
|
@ -12,7 +12,13 @@ import type { IExecutionResponse, INodeUi, IWorkflowDb, IWorkflowSettings } from
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
|
||||||
import { SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
import { SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
||||||
import type { IPinData, ExecutionSummary, IConnection, INodeExecutionData } from 'n8n-workflow';
|
import type {
|
||||||
|
IPinData,
|
||||||
|
ExecutionSummary,
|
||||||
|
IConnection,
|
||||||
|
INodeExecutionData,
|
||||||
|
INode,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import { stringSizeInBytes } from '@/utils/typesUtils';
|
import { stringSizeInBytes } from '@/utils/typesUtils';
|
||||||
import { dataPinningEventBus } from '@/event-bus';
|
import { dataPinningEventBus } from '@/event-bus';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
@ -681,6 +687,83 @@ describe('useWorkflowsStore', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setNodes()', () => {
|
||||||
|
it('should transform credential-only nodes', () => {
|
||||||
|
const setNodeId = '1';
|
||||||
|
const credentialOnlyNodeId = '2';
|
||||||
|
workflowsStore.setNodes([
|
||||||
|
mock<INode>({
|
||||||
|
id: setNodeId,
|
||||||
|
name: 'Edit Fields',
|
||||||
|
type: 'n8n-nodes-base.set',
|
||||||
|
}),
|
||||||
|
mock<INode>({
|
||||||
|
id: credentialOnlyNodeId,
|
||||||
|
name: 'AlienVault Request',
|
||||||
|
type: 'n8n-nodes-base.httpRequest',
|
||||||
|
extendsCredential: 'alienVaultApi',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(workflowsStore.workflow.nodes[0].id).toEqual(setNodeId);
|
||||||
|
expect(workflowsStore.workflow.nodes[1].id).toEqual(credentialOnlyNodeId);
|
||||||
|
expect(workflowsStore.workflow.nodes[1].type).toEqual('n8n-creds-base.alienVaultApi');
|
||||||
|
expect(workflowsStore.nodeMetadata).toEqual({
|
||||||
|
'AlienVault Request': { pristine: true },
|
||||||
|
'Edit Fields': { pristine: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateNodeAtIndex', () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
description: 'should update node at given index with provided data',
|
||||||
|
nodeIndex: 0,
|
||||||
|
nodeData: { name: 'Updated Node' },
|
||||||
|
initialNodes: [{ name: 'Original Node' }],
|
||||||
|
expectedNodes: [{ name: 'Updated Node' }],
|
||||||
|
expectedResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should not update node if index is invalid',
|
||||||
|
nodeIndex: -1,
|
||||||
|
nodeData: { name: 'Updated Node' },
|
||||||
|
initialNodes: [{ name: 'Original Node' }],
|
||||||
|
expectedNodes: [{ name: 'Original Node' }],
|
||||||
|
expectedResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should return false if node data is unchanged',
|
||||||
|
nodeIndex: 0,
|
||||||
|
nodeData: { name: 'Original Node' },
|
||||||
|
initialNodes: [{ name: 'Original Node' }],
|
||||||
|
expectedNodes: [{ name: 'Original Node' }],
|
||||||
|
expectedResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'should update multiple properties of a node',
|
||||||
|
nodeIndex: 0,
|
||||||
|
nodeData: { name: 'Updated Node', type: 'newType' },
|
||||||
|
initialNodes: [{ name: 'Original Node', type: 'oldType' }],
|
||||||
|
expectedNodes: [{ name: 'Updated Node', type: 'newType' }],
|
||||||
|
expectedResult: true,
|
||||||
|
},
|
||||||
|
])('$description', ({ nodeIndex, nodeData, initialNodes, expectedNodes, expectedResult }) => {
|
||||||
|
workflowsStore.workflow.nodes = initialNodes as unknown as IWorkflowDb['nodes'];
|
||||||
|
|
||||||
|
const result = workflowsStore.updateNodeAtIndex(nodeIndex, nodeData);
|
||||||
|
|
||||||
|
expect(result).toBe(expectedResult);
|
||||||
|
expect(workflowsStore.workflow.nodes).toEqual(expectedNodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if out of bounds', () => {
|
||||||
|
workflowsStore.workflow.nodes = [];
|
||||||
|
expect(() => workflowsStore.updateNodeAtIndex(0, { name: 'Updated Node' })).toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
// check userVersion behavior
|
// check userVersion behavior
|
||||||
[-1, 1, 1], // userVersion -1, use default (1)
|
[-1, 1, 1], // userVersion -1, use default (1)
|
||||||
|
|
|
@ -62,7 +62,7 @@ import {
|
||||||
SEND_AND_WAIT_OPERATION,
|
SEND_AND_WAIT_OPERATION,
|
||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { findLast } from 'lodash-es';
|
import { findLast, pick, isEqual } from 'lodash-es';
|
||||||
|
|
||||||
import { useRootStore } from '@/stores/root.store';
|
import { useRootStore } from '@/stores/root.store';
|
||||||
import * as workflowsApi from '@/api/workflows';
|
import * as workflowsApi from '@/api/workflows';
|
||||||
|
@ -1117,6 +1117,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
nodeHelpers.assignNodeId(node);
|
nodeHelpers.assignNodeId(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.extendsCredential) {
|
||||||
|
node.type = getCredentialOnlyNodeTypeName(node.extendsCredential);
|
||||||
|
}
|
||||||
|
|
||||||
if (!nodeMetadata.value[node.name]) {
|
if (!nodeMetadata.value[node.name]) {
|
||||||
nodeMetadata.value[node.name] = { pristine: true };
|
nodeMetadata.value[node.name] = { pristine: true };
|
||||||
}
|
}
|
||||||
|
@ -1138,10 +1142,17 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNodeAtIndex(nodeIndex: number, nodeData: Partial<INodeUi>): void {
|
/**
|
||||||
|
* @returns `true` if the object was changed
|
||||||
|
*/
|
||||||
|
function updateNodeAtIndex(nodeIndex: number, nodeData: Partial<INodeUi>): boolean {
|
||||||
if (nodeIndex !== -1) {
|
if (nodeIndex !== -1) {
|
||||||
Object.assign(workflow.value.nodes[nodeIndex], nodeData);
|
const node = workflow.value.nodes[nodeIndex];
|
||||||
|
const changed = !isEqual(pick(node, Object.keys(nodeData)), nodeData);
|
||||||
|
Object.assign(node, nodeData);
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setNodeIssue(nodeIssueData: INodeIssueData): boolean {
|
function setNodeIssue(nodeIssueData: INodeIssueData): boolean {
|
||||||
|
@ -1183,10 +1194,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeData.extendsCredential) {
|
|
||||||
nodeData.type = getCredentialOnlyNodeTypeName(nodeData.extendsCredential);
|
|
||||||
}
|
|
||||||
|
|
||||||
workflow.value.nodes.push(nodeData);
|
workflow.value.nodes.push(nodeData);
|
||||||
// Init node metadata
|
// Init node metadata
|
||||||
if (!nodeMetadata.value[nodeData.name]) {
|
if (!nodeMetadata.value[nodeData.name]) {
|
||||||
|
@ -1270,12 +1277,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
uiStore.stateIsDirty = true;
|
const changed = updateNodeAtIndex(nodeIndex, {
|
||||||
|
|
||||||
updateNodeAtIndex(nodeIndex, {
|
|
||||||
[updateInformation.key]: updateInformation.value,
|
[updateInformation.key]: updateInformation.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
uiStore.stateIsDirty = uiStore.stateIsDirty || changed;
|
||||||
|
|
||||||
const excludeKeys = ['position', 'notes', 'notesInFlow'];
|
const excludeKeys = ['position', 'notes', 'notesInFlow'];
|
||||||
|
|
||||||
if (!excludeKeys.includes(updateInformation.key)) {
|
if (!excludeKeys.includes(updateInformation.key)) {
|
||||||
|
|
|
@ -138,55 +138,62 @@ function onEdit(id: string) {
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</p>
|
</p>
|
||||||
<template v-if="apiKeysSortByCreationDate.length">
|
|
||||||
<el-row
|
|
||||||
v-for="apiKey in apiKeysSortByCreationDate"
|
|
||||||
:key="apiKey.id"
|
|
||||||
:gutter="10"
|
|
||||||
:class="$style.destinationItem"
|
|
||||||
>
|
|
||||||
<el-col>
|
|
||||||
<ApiKeyCard :api-key="apiKey" @delete="onDelete" @edit="onEdit" />
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<div v-if="isPublicApiEnabled && apiKeysSortByCreationDate.length" :class="$style.BottomHint">
|
<div :class="$style.apiKeysContainer">
|
||||||
<N8nText size="small" color="text-light">
|
<template v-if="apiKeysSortByCreationDate.length">
|
||||||
{{
|
<el-row
|
||||||
i18n.baseText(
|
v-for="(apiKey, index) in apiKeysSortByCreationDate"
|
||||||
`settings.api.view.${settingsStore.isSwaggerUIEnabled ? 'tryapi' : 'more-details'}`,
|
:key="apiKey.id"
|
||||||
)
|
:gutter="10"
|
||||||
}}
|
:class="[{ [$style.destinationItem]: index !== apiKeysSortByCreationDate.length - 1 }]"
|
||||||
</N8nText>
|
|
||||||
{{ ' ' }}
|
|
||||||
<n8n-link
|
|
||||||
v-if="isSwaggerUIEnabled"
|
|
||||||
data-test-id="api-playground-link"
|
|
||||||
:to="apiDocsURL"
|
|
||||||
:new-window="true"
|
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
{{ i18n.baseText('settings.api.view.apiPlayground') }}
|
<el-col>
|
||||||
</n8n-link>
|
<ApiKeyCard :api-key="apiKey" @delete="onDelete" @edit="onEdit" />
|
||||||
<n8n-link
|
</el-col>
|
||||||
v-else
|
</el-row>
|
||||||
data-test-id="api-endpoint-docs-link"
|
</template>
|
||||||
:to="apiDocsURL"
|
</div>
|
||||||
:new-window="true"
|
|
||||||
size="small"
|
<div v-if="isPublicApiEnabled && apiKeysSortByCreationDate.length" :class="$style.BottomHint">
|
||||||
>
|
<N8nText size="small" color="text-light">
|
||||||
{{ i18n.baseText(`settings.api.view.external-docs`) }}
|
{{
|
||||||
</n8n-link>
|
i18n.baseText(
|
||||||
</div>
|
`settings.api.view.${settingsStore.isSwaggerUIEnabled ? 'tryapi' : 'more-details'}`,
|
||||||
<div class="mt-m text-right">
|
)
|
||||||
<n8n-button size="large" @click="onCreateApiKey">
|
}}
|
||||||
{{ i18n.baseText('settings.api.create.button') }}
|
</N8nText>
|
||||||
</n8n-button>
|
{{ ' ' }}
|
||||||
</div>
|
<n8n-link
|
||||||
</template>
|
v-if="isSwaggerUIEnabled"
|
||||||
|
data-test-id="api-playground-link"
|
||||||
|
:to="apiDocsURL"
|
||||||
|
:new-window="true"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText('settings.api.view.apiPlayground') }}
|
||||||
|
</n8n-link>
|
||||||
|
<n8n-link
|
||||||
|
v-else
|
||||||
|
data-test-id="api-endpoint-docs-link"
|
||||||
|
:to="apiDocsURL"
|
||||||
|
:new-window="true"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText(`settings.api.view.external-docs`) }}
|
||||||
|
</n8n-link>
|
||||||
|
</div>
|
||||||
|
<div class="mt-m text-right">
|
||||||
|
<n8n-button
|
||||||
|
v-if="isPublicApiEnabled && apiKeysSortByCreationDate.length"
|
||||||
|
size="large"
|
||||||
|
@click="onCreateApiKey"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText('settings.api.create.button') }}
|
||||||
|
</n8n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n8n-action-box
|
<n8n-action-box
|
||||||
v-else-if="!isPublicApiEnabled && cloudPlanStore.userIsTrialing"
|
v-if="!isPublicApiEnabled && cloudPlanStore.userIsTrialing"
|
||||||
data-test-id="public-api-upgrade-cta"
|
data-test-id="public-api-upgrade-cta"
|
||||||
:heading="i18n.baseText('settings.api.trial.upgradePlan.title')"
|
:heading="i18n.baseText('settings.api.trial.upgradePlan.title')"
|
||||||
:description="i18n.baseText('settings.api.trial.upgradePlan.description')"
|
:description="i18n.baseText('settings.api.trial.upgradePlan.description')"
|
||||||
|
@ -246,5 +253,13 @@ function onEdit(id: string) {
|
||||||
|
|
||||||
.BottomHint {
|
.BottomHint {
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.apiKeysContainer {
|
||||||
|
max-height: 45vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -51,7 +51,7 @@ export async function s3ApiRequest(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint.pathname = path;
|
endpoint.pathname = `${endpoint.pathname === '/' ? '' : endpoint.pathname}${path}`;
|
||||||
|
|
||||||
// Sign AWS API request with the user credentials
|
// Sign AWS API request with the user credentials
|
||||||
const signOpts = {
|
const signOpts = {
|
||||||
|
@ -59,7 +59,7 @@ export async function s3ApiRequest(
|
||||||
region: region || credentials.region,
|
region: region || credentials.region,
|
||||||
host: endpoint.host,
|
host: endpoint.host,
|
||||||
method,
|
method,
|
||||||
path: `${path}?${queryToString(query).replace(/\+/g, '%2B')}`,
|
path: `${endpoint.pathname}?${queryToString(query).replace(/\+/g, '%2B')}`,
|
||||||
service: 's3',
|
service: 's3',
|
||||||
body,
|
body,
|
||||||
} as Request;
|
} as Request;
|
||||||
|
|
164
packages/nodes-base/nodes/S3/__tests__/GenericFunctions.test.ts
Normal file
164
packages/nodes-base/nodes/S3/__tests__/GenericFunctions.test.ts
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
import { sign } from 'aws4';
|
||||||
|
import { parseString } from 'xml2js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
s3ApiRequest,
|
||||||
|
s3ApiRequestREST,
|
||||||
|
s3ApiRequestSOAP,
|
||||||
|
s3ApiRequestSOAPAllItems,
|
||||||
|
} from '../GenericFunctions';
|
||||||
|
|
||||||
|
jest.mock('aws4');
|
||||||
|
jest.mock('xml2js');
|
||||||
|
|
||||||
|
describe('S3 Node Generic Functions', () => {
|
||||||
|
let mockContext: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockContext = {
|
||||||
|
getNode: jest.fn().mockReturnValue({ name: 'S3' }),
|
||||||
|
getCredentials: jest.fn().mockResolvedValue({
|
||||||
|
endpoint: 'https://s3.amazonaws.com',
|
||||||
|
accessKeyId: 'test-key',
|
||||||
|
secretAccessKey: 'test-secret',
|
||||||
|
region: 'us-east-1',
|
||||||
|
}),
|
||||||
|
helpers: {
|
||||||
|
request: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('s3ApiRequest', () => {
|
||||||
|
it('should throw error if endpoint does not start with http', async () => {
|
||||||
|
mockContext.getCredentials.mockResolvedValueOnce({
|
||||||
|
endpoint: 'invalid-endpoint',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(s3ApiRequest.call(mockContext, 'test-bucket', 'GET', '/')).rejects.toThrow(
|
||||||
|
'HTTP(S) Scheme is required',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle force path style', async () => {
|
||||||
|
mockContext.getCredentials.mockResolvedValueOnce({
|
||||||
|
endpoint: 'https://s3.amazonaws.com',
|
||||||
|
forcePathStyle: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockContext.helpers.request.mockResolvedValueOnce('success');
|
||||||
|
|
||||||
|
await s3ApiRequest.call(mockContext, 'test-bucket', 'GET', '/test.txt');
|
||||||
|
|
||||||
|
expect(sign).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
path: '/test-bucket/test.txt?',
|
||||||
|
}),
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle supabase url', async () => {
|
||||||
|
mockContext.getCredentials.mockResolvedValueOnce({
|
||||||
|
endpoint: 'https://someurl.supabase.co/storage/v1/s3',
|
||||||
|
region: 'eu-west-2',
|
||||||
|
forcePathStyle: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockContext.helpers.request.mockResolvedValueOnce('success');
|
||||||
|
|
||||||
|
await s3ApiRequest.call(mockContext, 'test-bucket', 'GET', '/test.txt');
|
||||||
|
|
||||||
|
expect(sign).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
path: '/storage/v1/s3/test-bucket/test.txt?',
|
||||||
|
}),
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('s3ApiRequestREST', () => {
|
||||||
|
it('should parse JSON response', async () => {
|
||||||
|
const mockResponse = JSON.stringify({ key: 'value' });
|
||||||
|
mockContext.helpers.request.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await s3ApiRequestREST.call(mockContext, 'test-bucket', 'GET', '/');
|
||||||
|
|
||||||
|
expect(result).toEqual({ key: 'value' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return raw response on parse error', async () => {
|
||||||
|
const mockResponse = 'invalid-json';
|
||||||
|
mockContext.helpers.request.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await s3ApiRequestREST.call(mockContext, 'test-bucket', 'GET', '/');
|
||||||
|
|
||||||
|
expect(result).toBe('invalid-json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('s3ApiRequestSOAP', () => {
|
||||||
|
it('should parse XML response', async () => {
|
||||||
|
const mockXmlResponse = '<root><key>value</key></root>';
|
||||||
|
const mockParsedResponse = { root: { key: 'value' } };
|
||||||
|
|
||||||
|
mockContext.helpers.request.mockResolvedValueOnce(mockXmlResponse);
|
||||||
|
(parseString as jest.Mock).mockImplementation((_, __, callback) =>
|
||||||
|
callback(null, mockParsedResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await s3ApiRequestSOAP.call(mockContext, 'test-bucket', 'GET', '/');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockParsedResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle XML parsing errors', async () => {
|
||||||
|
const mockError = new Error('XML Parse Error');
|
||||||
|
mockContext.helpers.request.mockResolvedValueOnce('<invalid>xml');
|
||||||
|
(parseString as jest.Mock).mockImplementation((_, __, callback) => callback(mockError));
|
||||||
|
|
||||||
|
const result = await s3ApiRequestSOAP.call(mockContext, 'test-bucket', 'GET', '/');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('s3ApiRequestSOAPAllItems', () => {
|
||||||
|
it('should handle pagination with continuation token', async () => {
|
||||||
|
const firstResponse = {
|
||||||
|
ListBucketResult: {
|
||||||
|
Contents: [{ Key: 'file1.txt' }],
|
||||||
|
IsTruncated: 'true',
|
||||||
|
NextContinuationToken: 'token123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const secondResponse = {
|
||||||
|
ListBucketResult: {
|
||||||
|
Contents: [{ Key: 'file2.txt' }],
|
||||||
|
IsTruncated: 'false',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockContext.helpers.request
|
||||||
|
.mockResolvedValueOnce('<xml>first</xml>')
|
||||||
|
.mockResolvedValueOnce('<xml>second</xml>');
|
||||||
|
|
||||||
|
(parseString as jest.Mock)
|
||||||
|
.mockImplementationOnce((_, __, callback) => callback(null, firstResponse))
|
||||||
|
.mockImplementationOnce((_, __, callback) => callback(null, secondResponse));
|
||||||
|
|
||||||
|
const result = await s3ApiRequestSOAPAllItems.call(
|
||||||
|
mockContext,
|
||||||
|
'ListBucketResult.Contents',
|
||||||
|
'test-bucket',
|
||||||
|
'GET',
|
||||||
|
'/',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result).toEqual([{ Key: 'file1.txt' }, { Key: 'file2.txt' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -493,7 +493,7 @@ importers:
|
||||||
version: 3.666.0(@aws-sdk/client-sts@3.666.0)
|
version: 3.666.0(@aws-sdk/client-sts@3.666.0)
|
||||||
'@getzep/zep-cloud':
|
'@getzep/zep-cloud':
|
||||||
specifier: 1.0.12
|
specifier: 1.0.12
|
||||||
version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(c10b80e38f5a8711ccad1e2174de91e6))
|
version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(101f2d8395211f7761efa8fce29a53de))
|
||||||
'@getzep/zep-js':
|
'@getzep/zep-js':
|
||||||
specifier: 0.9.0
|
specifier: 0.9.0
|
||||||
version: 0.9.0
|
version: 0.9.0
|
||||||
|
@ -520,7 +520,7 @@ importers:
|
||||||
version: 0.3.2(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
version: 0.3.2(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
||||||
'@langchain/community':
|
'@langchain/community':
|
||||||
specifier: 0.3.24
|
specifier: 0.3.24
|
||||||
version: 0.3.24(1ea346ff95b1be1e3f1f4333b25e2811)
|
version: 0.3.24(6c18f51b4fff56aeb9d6961328b4ede2)
|
||||||
'@langchain/core':
|
'@langchain/core':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
|
@ -610,7 +610,7 @@ importers:
|
||||||
version: 23.0.1
|
version: 23.0.1
|
||||||
langchain:
|
langchain:
|
||||||
specifier: 0.3.11
|
specifier: 0.3.11
|
||||||
version: 0.3.11(c10b80e38f5a8711ccad1e2174de91e6)
|
version: 0.3.11(101f2d8395211f7761efa8fce29a53de)
|
||||||
lodash:
|
lodash:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
@ -1829,6 +1829,9 @@ importers:
|
||||||
browserslist-to-esbuild:
|
browserslist-to-esbuild:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1(browserslist@4.24.2)
|
version: 2.1.1(browserslist@4.24.2)
|
||||||
|
fake-indexeddb:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
miragejs:
|
miragejs:
|
||||||
specifier: ^0.1.48
|
specifier: ^0.1.48
|
||||||
version: 0.1.48
|
version: 0.1.48
|
||||||
|
@ -8467,6 +8470,10 @@ packages:
|
||||||
resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
|
resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==}
|
||||||
engines: {'0': node >=0.6.0}
|
engines: {'0': node >=0.6.0}
|
||||||
|
|
||||||
|
fake-indexeddb@6.0.0:
|
||||||
|
resolution: {integrity: sha512-YEboHE5VfopUclOck7LncgIqskAqnv4q0EWbYCaxKKjAvO93c+TJIaBuGy8CBFdbg9nKdpN3AuPRwVBJ4k7NrQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
fake-xml-http-request@2.1.2:
|
fake-xml-http-request@2.1.2:
|
||||||
resolution: {integrity: sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==}
|
resolution: {integrity: sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==}
|
||||||
|
|
||||||
|
@ -13312,6 +13319,9 @@ packages:
|
||||||
vue-component-type-helpers@2.2.4:
|
vue-component-type-helpers@2.2.4:
|
||||||
resolution: {integrity: sha512-F66p0XLbAu92BRz6kakHyAcaUSF7HWpWX/THCqL0TxySSj7z/nok5UUMohfNkkCm1pZtawsdzoJ4p1cjNqCx0Q==}
|
resolution: {integrity: sha512-F66p0XLbAu92BRz6kakHyAcaUSF7HWpWX/THCqL0TxySSj7z/nok5UUMohfNkkCm1pZtawsdzoJ4p1cjNqCx0Q==}
|
||||||
|
|
||||||
|
vue-component-type-helpers@2.2.8:
|
||||||
|
resolution: {integrity: sha512-4bjIsC284coDO9om4HPA62M7wfsTvcmZyzdfR0aUlFXqq4tXxM1APyXpNVxPC8QazKw9OhmZNHBVDA6ODaZsrA==}
|
||||||
|
|
||||||
vue-demi@0.14.10:
|
vue-demi@0.14.10:
|
||||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
@ -15922,7 +15932,7 @@ snapshots:
|
||||||
'@gar/promisify@1.1.3':
|
'@gar/promisify@1.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(c10b80e38f5a8711ccad1e2174de91e6))':
|
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(101f2d8395211f7761efa8fce29a53de))':
|
||||||
dependencies:
|
dependencies:
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
node-fetch: 2.7.0(encoding@0.1.13)
|
node-fetch: 2.7.0(encoding@0.1.13)
|
||||||
|
@ -15931,7 +15941,7 @@ snapshots:
|
||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
langchain: 0.3.11(c10b80e38f5a8711ccad1e2174de91e6)
|
langchain: 0.3.11(101f2d8395211f7761efa8fce29a53de)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
|
@ -16450,7 +16460,7 @@ snapshots:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
'@langchain/community@0.3.24(1ea346ff95b1be1e3f1f4333b25e2811)':
|
'@langchain/community@0.3.24(6c18f51b4fff56aeb9d6961328b4ede2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1)
|
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1)
|
||||||
'@ibm-cloud/watsonx-ai': 1.1.2
|
'@ibm-cloud/watsonx-ai': 1.1.2
|
||||||
|
@ -16461,7 +16471,7 @@ snapshots:
|
||||||
flat: 5.0.2
|
flat: 5.0.2
|
||||||
ibm-cloud-sdk-core: 5.1.0
|
ibm-cloud-sdk-core: 5.1.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
langchain: 0.3.11(c10b80e38f5a8711ccad1e2174de91e6)
|
langchain: 0.3.11(101f2d8395211f7761efa8fce29a53de)
|
||||||
langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
openai: 4.78.1(encoding@0.1.13)(zod@3.24.1)
|
openai: 4.78.1(encoding@0.1.13)(zod@3.24.1)
|
||||||
uuid: 10.0.0
|
uuid: 10.0.0
|
||||||
|
@ -16476,7 +16486,7 @@ snapshots:
|
||||||
'@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
|
'@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
|
||||||
'@azure/storage-blob': 12.18.0(encoding@0.1.13)
|
'@azure/storage-blob': 12.18.0(encoding@0.1.13)
|
||||||
'@browserbasehq/sdk': 2.0.0(encoding@0.1.13)
|
'@browserbasehq/sdk': 2.0.0(encoding@0.1.13)
|
||||||
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(c10b80e38f5a8711ccad1e2174de91e6))
|
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(101f2d8395211f7761efa8fce29a53de))
|
||||||
'@getzep/zep-js': 0.9.0
|
'@getzep/zep-js': 0.9.0
|
||||||
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
|
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
|
||||||
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
||||||
|
@ -18341,7 +18351,7 @@ snapshots:
|
||||||
ts-dedent: 2.2.0
|
ts-dedent: 2.2.0
|
||||||
type-fest: 2.19.0
|
type-fest: 2.19.0
|
||||||
vue: 3.5.13(typescript@5.7.2)
|
vue: 3.5.13(typescript@5.7.2)
|
||||||
vue-component-type-helpers: 2.2.4
|
vue-component-type-helpers: 2.2.8
|
||||||
|
|
||||||
'@supabase/auth-js@2.65.0':
|
'@supabase/auth-js@2.65.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -19793,14 +19803,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
axios@1.7.4(debug@4.4.0):
|
|
||||||
dependencies:
|
|
||||||
follow-redirects: 1.15.6(debug@4.4.0)
|
|
||||||
form-data: 4.0.0
|
|
||||||
proxy-from-env: 1.1.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- debug
|
|
||||||
|
|
||||||
axios@1.7.7:
|
axios@1.7.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.6(debug@4.3.6)
|
follow-redirects: 1.15.6(debug@4.3.6)
|
||||||
|
@ -21465,7 +21467,7 @@ snapshots:
|
||||||
|
|
||||||
eslint-import-resolver-node@0.3.9:
|
eslint-import-resolver-node@0.3.9:
|
||||||
dependencies:
|
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
|
is-core-module: 2.13.1
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -21490,7 +21492,7 @@ snapshots:
|
||||||
|
|
||||||
eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
|
eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2)
|
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2)
|
||||||
eslint: 8.57.0
|
eslint: 8.57.0
|
||||||
|
@ -21510,7 +21512,7 @@ snapshots:
|
||||||
array.prototype.findlastindex: 1.2.3
|
array.prototype.findlastindex: 1.2.3
|
||||||
array.prototype.flat: 1.3.2
|
array.prototype.flat: 1.3.2
|
||||||
array.prototype.flatmap: 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
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.0
|
eslint: 8.57.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
|
@ -21857,6 +21859,8 @@ snapshots:
|
||||||
|
|
||||||
extsprintf@1.3.0: {}
|
extsprintf@1.3.0: {}
|
||||||
|
|
||||||
|
fake-indexeddb@6.0.0: {}
|
||||||
|
|
||||||
fake-xml-http-request@2.1.2: {}
|
fake-xml-http-request@2.1.2: {}
|
||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
@ -22001,10 +22005,6 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
debug: 4.3.7
|
debug: 4.3.7
|
||||||
|
|
||||||
follow-redirects@1.15.6(debug@4.4.0):
|
|
||||||
optionalDependencies:
|
|
||||||
debug: 4.4.0(supports-color@8.1.1)
|
|
||||||
|
|
||||||
for-each@0.3.3:
|
for-each@0.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
|
@ -22294,7 +22294,7 @@ snapshots:
|
||||||
array-parallel: 0.1.3
|
array-parallel: 0.1.3
|
||||||
array-series: 0.1.5
|
array-series: 0.1.5
|
||||||
cross-spawn: 4.0.2
|
cross-spawn: 4.0.2
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -22596,7 +22596,7 @@ snapshots:
|
||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 18.16.16
|
'@types/node': 18.16.16
|
||||||
'@types/tough-cookie': 4.0.2
|
'@types/tough-cookie': 4.0.2
|
||||||
axios: 1.7.4(debug@4.4.0)
|
axios: 1.7.4
|
||||||
camelcase: 6.3.0
|
camelcase: 6.3.0
|
||||||
debug: 4.4.0(supports-color@8.1.1)
|
debug: 4.4.0(supports-color@8.1.1)
|
||||||
dotenv: 16.4.5
|
dotenv: 16.4.5
|
||||||
|
@ -22606,7 +22606,7 @@ snapshots:
|
||||||
isstream: 0.1.2
|
isstream: 0.1.2
|
||||||
jsonwebtoken: 9.0.2
|
jsonwebtoken: 9.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
retry-axios: 2.6.0(axios@1.7.4)
|
retry-axios: 2.6.0(axios@1.7.4(debug@4.4.0))
|
||||||
tough-cookie: 4.1.3
|
tough-cookie: 4.1.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -23593,7 +23593,7 @@ snapshots:
|
||||||
|
|
||||||
kuler@2.0.0: {}
|
kuler@2.0.0: {}
|
||||||
|
|
||||||
langchain@0.3.11(c10b80e38f5a8711ccad1e2174de91e6):
|
langchain@0.3.11(101f2d8395211f7761efa8fce29a53de):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
'@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
'@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
||||||
|
@ -25157,7 +25157,7 @@ snapshots:
|
||||||
|
|
||||||
pdf-parse@1.1.1:
|
pdf-parse@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
node-ensure: 0.0.0
|
node-ensure: 0.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -25965,7 +25965,7 @@ snapshots:
|
||||||
|
|
||||||
ret@0.1.15: {}
|
ret@0.1.15: {}
|
||||||
|
|
||||||
retry-axios@2.6.0(axios@1.7.4):
|
retry-axios@2.6.0(axios@1.7.4(debug@4.4.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.7.4
|
axios: 1.7.4
|
||||||
|
|
||||||
|
@ -25992,7 +25992,7 @@ snapshots:
|
||||||
|
|
||||||
rhea@1.0.24:
|
rhea@1.0.24:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -27624,6 +27624,8 @@ snapshots:
|
||||||
|
|
||||||
vue-component-type-helpers@2.2.4: {}
|
vue-component-type-helpers@2.2.4: {}
|
||||||
|
|
||||||
|
vue-component-type-helpers@2.2.8: {}
|
||||||
|
|
||||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.7.2)):
|
vue-demi@0.14.10(vue@3.5.13(typescript@5.7.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13(typescript@5.7.2)
|
vue: 3.5.13(typescript@5.7.2)
|
||||||
|
|
Loading…
Reference in a new issue