mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-05 02:59:27 +02:00
test: Migrate @n8n/db from Jest to Vitest (no-changelog) (#31560)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ea800f715d
commit
d7d2071bdd
|
|
@ -1,10 +0,0 @@
|
|||
const baseConfig = require('../../../jest.config');
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
transform: {
|
||||
...baseConfig.transform,
|
||||
'^.+\\.ts$': ['ts-jest', { isolatedModules: false }],
|
||||
},
|
||||
};
|
||||
|
|
@ -12,9 +12,9 @@
|
|||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:unit": "jest",
|
||||
"test:dev": "jest --watch",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:dev": "vitest --silent=false",
|
||||
"migration:new": "node scripts/new-migration.mjs"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
|
|
@ -47,7 +47,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@types/lodash": "catalog:",
|
||||
"express": "5.1.0"
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"express": "5.1.0",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vitest-mock-extended": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,38 +2,39 @@
|
|||
import type { Logger } from '@n8n/backend-common';
|
||||
import type { DatabaseConfig } from '@n8n/config';
|
||||
import type { DataSource } from '@n8n/typeorm';
|
||||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import type { ErrorReporter } from 'n8n-core';
|
||||
import type TimersPromises from 'timers/promises';
|
||||
import { setTimeout as setTimeoutP } from 'timers/promises';
|
||||
import type { Mock, MockedFunction } from 'vitest';
|
||||
import { mock, mockDeep } from 'vitest-mock-extended';
|
||||
|
||||
import { DbConnectionMonitor } from '../db-connection-monitor';
|
||||
|
||||
// The monitor uses `setTimeout` from `timers/promises` for recovery backoff.
|
||||
// Mocking it lets us drive the recovery loop deterministically without juggling
|
||||
// jest fake timers against async/await microtask ordering.
|
||||
jest.mock('timers/promises', () => {
|
||||
const actual = jest.requireActual<typeof TimersPromises>('timers/promises');
|
||||
return { ...actual, setTimeout: jest.fn() };
|
||||
// fake timers against async/await microtask ordering.
|
||||
vi.mock('timers/promises', async () => {
|
||||
const actual = await vi.importActual<typeof TimersPromises>('timers/promises');
|
||||
return { ...actual, setTimeout: vi.fn() };
|
||||
});
|
||||
const mockedSetTimeoutP = setTimeoutP as jest.MockedFunction<typeof setTimeoutP>;
|
||||
const mockedSetTimeoutP = setTimeoutP as MockedFunction<typeof setTimeoutP>;
|
||||
|
||||
const flushMicrotasks = async () => await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
describe('DbConnectionMonitor', () => {
|
||||
let monitor: DbConnectionMonitor;
|
||||
let onConnectedChange: jest.MockedFunction<(connected: boolean) => void>;
|
||||
let onConnectedChange: MockedFunction<(connected: boolean) => void>;
|
||||
const errorReporter = mock<ErrorReporter>();
|
||||
const databaseConfig = mock<DatabaseConfig>({ pingTimeoutMs: 5_000 });
|
||||
const logger = mock<Logger>();
|
||||
const dataSource = mockDeep<DataSource>({ options: { type: 'postgres' } });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
// Default: never resolves, so query wins the ping timeout race and
|
||||
// recovery backoff stays suspended unless a test overrides it.
|
||||
mockedSetTimeoutP.mockImplementation(async () => await new Promise(() => {}));
|
||||
onConnectedChange = jest.fn();
|
||||
onConnectedChange = vi.fn();
|
||||
monitor = new DbConnectionMonitor(
|
||||
dataSource,
|
||||
onConnectedChange,
|
||||
|
|
@ -110,7 +111,7 @@ describe('DbConnectionMonitor', () => {
|
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
dataSource.query.mockResolvedValue([{ '1': 1 }]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const scheduleNextPingSpy = jest.spyOn(monitor as any, 'scheduleNextPing');
|
||||
const scheduleNextPingSpy = vi.spyOn(monitor as any, 'scheduleNextPing');
|
||||
|
||||
// @ts-expect-error private property
|
||||
await monitor.ping();
|
||||
|
|
@ -150,7 +151,7 @@ describe('DbConnectionMonitor', () => {
|
|||
.mockRejectedValueOnce(
|
||||
new Error('Client has encountered a connection error and is not queryable'),
|
||||
);
|
||||
const recoverSpy = jest
|
||||
const recoverSpy = vi
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.spyOn(monitor as any, 'recoverDataSource')
|
||||
.mockResolvedValue(undefined);
|
||||
|
|
@ -171,7 +172,7 @@ describe('DbConnectionMonitor', () => {
|
|||
// @ts-expect-error readonly property
|
||||
dataSource.isInitialized = true;
|
||||
dataSource.query.mockRejectedValue(new Error('pool poisoned'));
|
||||
const recoverSpy = jest
|
||||
const recoverSpy = vi
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.spyOn(monitor as any, 'recoverDataSource')
|
||||
.mockResolvedValue(undefined);
|
||||
|
|
@ -234,7 +235,7 @@ describe('DbConnectionMonitor', () => {
|
|||
});
|
||||
|
||||
it('should execute ping on schedule', () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const scheduledMonitor = new DbConnectionMonitor(
|
||||
dataSource,
|
||||
|
|
@ -244,20 +245,20 @@ describe('DbConnectionMonitor', () => {
|
|||
errorReporter,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pingSpy = jest.spyOn(scheduledMonitor as any, 'ping');
|
||||
const pingSpy = vi.spyOn(scheduledMonitor as any, 'ping');
|
||||
|
||||
// @ts-expect-error private property
|
||||
scheduledMonitor.scheduleNextPing();
|
||||
jest.advanceTimersByTime(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
expect(pingSpy).toHaveBeenCalled();
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('should not schedule another ping after stop', () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const scheduledMonitor = new DbConnectionMonitor(
|
||||
dataSource,
|
||||
|
|
@ -267,16 +268,16 @@ describe('DbConnectionMonitor', () => {
|
|||
errorReporter,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pingSpy = jest.spyOn(scheduledMonitor as any, 'ping');
|
||||
const pingSpy = vi.spyOn(scheduledMonitor as any, 'ping');
|
||||
|
||||
scheduledMonitor.stop();
|
||||
// @ts-expect-error private property
|
||||
scheduledMonitor.scheduleNextPing();
|
||||
jest.advanceTimersByTime(1000);
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
expect(pingSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -487,8 +488,8 @@ describe('DbConnectionMonitor', () => {
|
|||
dataSource.isInitialized = true;
|
||||
dataSource.destroy.mockResolvedValue();
|
||||
dataSource.initialize.mockResolvedValue(dataSource);
|
||||
const on = jest.fn();
|
||||
(dataSource as unknown as { driver: { master: { on: jest.Mock } } }).driver = {
|
||||
const on = vi.fn();
|
||||
(dataSource as unknown as { driver: { master: { on: Mock } } }).driver = {
|
||||
master: { on },
|
||||
};
|
||||
|
||||
|
|
@ -531,7 +532,7 @@ describe('DbConnectionMonitor', () => {
|
|||
};
|
||||
|
||||
it('should attach an error listener to the Postgres driver pool', () => {
|
||||
const on = jest.fn();
|
||||
const on = vi.fn();
|
||||
setDriver({ master: { on } });
|
||||
|
||||
monitor.start();
|
||||
|
|
@ -541,7 +542,7 @@ describe('DbConnectionMonitor', () => {
|
|||
|
||||
it('should mark the connection unhealthy when the pool emits an error', () => {
|
||||
let handler: ((cause: unknown) => void) | undefined;
|
||||
const on = jest.fn((_event: string, h: (cause: unknown) => void) => {
|
||||
const on = vi.fn((_event: string, h: (cause: unknown) => void) => {
|
||||
handler = h;
|
||||
});
|
||||
setDriver({ master: { on } });
|
||||
|
|
@ -590,7 +591,7 @@ describe('DbConnectionMonitor', () => {
|
|||
|
||||
describe('stop', () => {
|
||||
it('should clear the ping timer', () => {
|
||||
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
|
||||
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
|
||||
// @ts-expect-error private property
|
||||
monitor.pingTimer = setTimeout(() => {}, 1000);
|
||||
|
||||
|
|
@ -615,7 +616,7 @@ describe('DbConnectionMonitor', () => {
|
|||
// initial state is "connected". If the default flipped to false, the first failed
|
||||
// ping would be a no-op transition (false → false) and the owner's state machine
|
||||
// would stay stuck at the manually-set `true` while reality is `false`.
|
||||
const freshOnConnectedChange = jest.fn();
|
||||
const freshOnConnectedChange = vi.fn();
|
||||
const freshMonitor = new DbConnectionMonitor(
|
||||
dataSource,
|
||||
freshOnConnectedChange,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ModuleRegistry } from '@n8n/backend-common';
|
||||
import type { GlobalConfig, InstanceSettingsConfig } from '@n8n/config';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import path from 'path';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { postgresMigrations } from '../../migrations/postgresdb';
|
||||
import { sqliteMigrations } from '../../migrations/sqlite';
|
||||
|
|
@ -24,7 +24,7 @@ describe('DbConnectionOptions', () => {
|
|||
moduleRegistry,
|
||||
);
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
beforeEach(() => vi.resetAllMocks());
|
||||
|
||||
const commonOptions = {
|
||||
entityPrefix: 'test_prefix_',
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
import type { Logger } from '@n8n/backend-common';
|
||||
import type { DatabaseConfig } from '@n8n/config';
|
||||
import { DataSource, type DataSourceOptions } from '@n8n/typeorm';
|
||||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import type { ErrorReporter } from 'n8n-core';
|
||||
import { DbConnectionTimeoutError } from 'n8n-workflow';
|
||||
import type { Mock } from 'vitest';
|
||||
import { mock, mockDeep } from 'vitest-mock-extended';
|
||||
|
||||
import * as migrationHelper from '../../migrations/migration-helpers';
|
||||
import type { Migration } from '../../migrations/migration-types';
|
||||
|
|
@ -12,14 +13,14 @@ import { DbConnection } from '../db-connection';
|
|||
import { DbConnectionMonitor } from '../db-connection-monitor';
|
||||
import type { DbConnectionOptions } from '../db-connection-options';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
jest.mock('@n8n/typeorm', () => ({
|
||||
vi.mock('@n8n/typeorm', async () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
...(await vi.importActual<typeof import('@n8n/typeorm')>('@n8n/typeorm')),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
DataSource: jest.fn(),
|
||||
...jest.requireActual('@n8n/typeorm'),
|
||||
DataSource: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../db-connection-monitor');
|
||||
vi.mock('../db-connection-monitor');
|
||||
|
||||
describe('DbConnection', () => {
|
||||
let dbConnection: DbConnection;
|
||||
|
|
@ -42,11 +43,15 @@ describe('DbConnection', () => {
|
|||
const monitor = mock<DbConnectionMonitor>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
|
||||
connectionOptions.getOptions.mockReturnValue(postgresOptions);
|
||||
(DataSource as jest.Mock) = jest.fn().mockImplementation(() => dataSource);
|
||||
jest.mocked(DbConnectionMonitor).mockImplementation(() => monitor);
|
||||
vi.mocked(DbConnectionMonitor).mockImplementation(function () {
|
||||
return monitor;
|
||||
});
|
||||
(DataSource as unknown as Mock) = vi.fn(function () {
|
||||
return dataSource;
|
||||
});
|
||||
|
||||
dbConnection = new DbConnection(errorReporter, connectionOptions, databaseConfig, logger);
|
||||
});
|
||||
|
|
@ -101,7 +106,9 @@ describe('DbConnection', () => {
|
|||
it('should wrap migrations and run them', async () => {
|
||||
dataSource.runMigrations.mockResolvedValue([]);
|
||||
|
||||
const wrapMigrationSpy = jest.spyOn(migrationHelper, 'wrapMigration').mockImplementation();
|
||||
const wrapMigrationSpy = vi
|
||||
.spyOn(migrationHelper, 'wrapMigration')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
expect(dataSource.runMigrations).not.toHaveBeenCalled();
|
||||
expect(dbConnection.connectionState.migrated).toBe(false);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ describe('migrationHelpers.wrapMigration', () => {
|
|||
class TestMigration implements IrreversibleMigration {
|
||||
async up() {}
|
||||
}
|
||||
const originalUp = jest.fn();
|
||||
const originalUp = vi.fn();
|
||||
TestMigration.prototype.up = originalUp;
|
||||
|
||||
//
|
||||
|
|
@ -48,7 +48,7 @@ describe('migrationHelpers.wrapMigration', () => {
|
|||
|
||||
async down() {}
|
||||
}
|
||||
const originalDown = jest.fn();
|
||||
const originalDown = vi.fn();
|
||||
TestMigration.prototype.down = originalDown;
|
||||
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Driver, QueryRunner, Table } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { Column } from '../column';
|
||||
import { AddColumns, AddEnumCheck, CreateTable, DropEnumCheck } from '../table';
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ describe('Migration Helpers', () => {
|
|||
const queryRunner = dataSource.createQueryRunner();
|
||||
|
||||
// Spy on queryRunner.query to capture the actual SQL being executed
|
||||
const querySpy = jest.spyOn(queryRunner, 'query');
|
||||
const querySpy = vi.spyOn(queryRunner, 'query');
|
||||
|
||||
// Copy all data using copyTable (should trigger multiple batches with OFFSET)
|
||||
await copyTable(queryRunner, '', testTableName, destTableName);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe('CredentialDependencyRepository', () => {
|
|||
const repository = Container.get(CredentialDependencyRepository);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('findCredentialIdsByDependencyId', () => {
|
||||
|
|
@ -39,11 +39,11 @@ describe('CredentialDependencyRepository', () => {
|
|||
describe('upsertDependenciesForCredential', () => {
|
||||
it('deduplicates ids and inserts once with orIgnore', async () => {
|
||||
const qb = {
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
into: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
orIgnore: jest.fn().mockReturnThis(),
|
||||
execute: jest.fn().mockResolvedValue(undefined),
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
into: vi.fn().mockReturnThis(),
|
||||
values: vi.fn().mockReturnThis(),
|
||||
orIgnore: vi.fn().mockReturnThis(),
|
||||
execute: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
entityManager.createQueryBuilder.mockReturnValue(qb as never);
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ describe('CredentialDependencyRepository', () => {
|
|||
{ dependencyId: 'provider-keep' } as CredentialDependency,
|
||||
]);
|
||||
|
||||
const upsertSpy = jest
|
||||
const upsertSpy = vi
|
||||
.spyOn(repository, 'upsertDependenciesForCredential')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ describe('CredentialDependencyRepository', () => {
|
|||
describe('addCredentialDependencyExistsFilter', () => {
|
||||
it('applies the EXISTS dependency filter using andWhere', () => {
|
||||
const qb = {
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
andWhere: vi.fn().mockReturnThis(),
|
||||
};
|
||||
const filter = {
|
||||
dependencyType: 'externalSecretProvider',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Container } from '@n8n/di';
|
||||
import type { SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { In } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { CredentialsEntity } from '../../entities';
|
||||
import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager';
|
||||
|
|
@ -12,7 +12,7 @@ describe('CredentialsRepository', () => {
|
|||
const credentialsRepository = Container.get(CredentialsRepository);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('findManyAndCount', () => {
|
||||
|
|
@ -58,13 +58,13 @@ describe('CredentialsRepository', () => {
|
|||
|
||||
describe('findAllGlobalCredentials', () => {
|
||||
it('applies dependency filter through query builder when provided', async () => {
|
||||
const andWhereSpy = jest.fn().mockReturnThis();
|
||||
const getManySpy = jest.fn().mockResolvedValue([]);
|
||||
const andWhereSpy = vi.fn().mockReturnThis();
|
||||
const getManySpy = vi.fn().mockResolvedValue([]);
|
||||
const qb = mock<SelectQueryBuilder<CredentialsEntity>>({
|
||||
andWhere: andWhereSpy,
|
||||
getMany: getManySpy,
|
||||
});
|
||||
jest.spyOn(credentialsRepository, 'createQueryBuilder').mockReturnValue(qb);
|
||||
vi.spyOn(credentialsRepository, 'createQueryBuilder').mockReturnValue(qb);
|
||||
|
||||
await credentialsRepository.findAllGlobalCredentials({
|
||||
filters: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Container } from '@n8n/di';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import { EvaluationCollection } from '../../entities/evaluation-collection.ee';
|
||||
import { TestRun } from '../../entities/test-run.ee';
|
||||
|
|
@ -10,12 +11,12 @@ describe('EvaluationCollectionRepository', () => {
|
|||
const repo = Container.get(EvaluationCollectionRepository);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('createCollection', () => {
|
||||
it('persists the collection with insightsCache initialised to null', async () => {
|
||||
(entityManager.create as jest.Mock).mockImplementation(
|
||||
(entityManager.create as Mock).mockImplementation(
|
||||
(_target: unknown, entityLike: unknown) => entityLike as EvaluationCollection,
|
||||
);
|
||||
entityManager.save.mockImplementationOnce(async (_target, entity) => entity);
|
||||
|
|
@ -62,11 +63,11 @@ describe('EvaluationCollectionRepository', () => {
|
|||
] as EvaluationCollection[];
|
||||
entityManager.find.mockResolvedValueOnce(collections);
|
||||
const qb = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
groupBy: jest.fn().mockReturnThis(),
|
||||
getRawMany: jest.fn().mockResolvedValueOnce([
|
||||
select: vi.fn().mockReturnThis(),
|
||||
addSelect: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
groupBy: vi.fn().mockReturnThis(),
|
||||
getRawMany: vi.fn().mockResolvedValueOnce([
|
||||
{ collectionId: 'col-a', count: '3' },
|
||||
{ collectionId: 'col-b', count: '0' },
|
||||
]),
|
||||
|
|
@ -185,7 +186,7 @@ describe('EvaluationCollectionRepository', () => {
|
|||
workflowId: 'wf-1',
|
||||
} as EvaluationCollection;
|
||||
entityManager.findOne.mockResolvedValueOnce(existing);
|
||||
(entityManager.create as jest.Mock).mockImplementation(
|
||||
(entityManager.create as Mock).mockImplementation(
|
||||
(_target: unknown, entityLike: unknown) => entityLike as EvaluationCollection,
|
||||
);
|
||||
entityManager.save.mockImplementationOnce(async (_target, entity) => entity);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { UpsertEvaluationConfigDto } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import { EvaluationConfig } from '../../entities/evaluation-config.ee';
|
||||
import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager';
|
||||
|
|
@ -33,7 +34,7 @@ describe('EvaluationConfigRepository', () => {
|
|||
}) as UpsertEvaluationConfigDto;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('listByWorkflowId', () => {
|
||||
|
|
@ -78,7 +79,7 @@ describe('EvaluationConfigRepository', () => {
|
|||
describe('createForWorkflow', () => {
|
||||
it('persists a new config with the supplied id and workflow id', async () => {
|
||||
const payload = buildPayload();
|
||||
(entityManager.create as jest.Mock).mockImplementation(
|
||||
(entityManager.create as Mock).mockImplementation(
|
||||
(_target: unknown, entityLike: unknown) => entityLike as EvaluationConfig,
|
||||
);
|
||||
entityManager.save.mockImplementationOnce(async (_target, entity) => entity);
|
||||
|
|
@ -169,13 +170,13 @@ describe('EvaluationConfigRepository', () => {
|
|||
describe('countDistinctWorkflowsWithConfigs', () => {
|
||||
it('returns the count of distinct workflowIds that have at least one config', async () => {
|
||||
const qbMock = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
distinct: jest.fn().mockReturnThis(),
|
||||
getCount: jest.fn().mockResolvedValueOnce(7),
|
||||
select: vi.fn().mockReturnThis(),
|
||||
distinct: vi.fn().mockReturnThis(),
|
||||
getCount: vi.fn().mockResolvedValueOnce(7),
|
||||
};
|
||||
jest
|
||||
.spyOn(repo, 'createQueryBuilder')
|
||||
.mockReturnValueOnce(qbMock as unknown as ReturnType<typeof repo.createQueryBuilder>);
|
||||
vi.spyOn(repo, 'createQueryBuilder').mockReturnValueOnce(
|
||||
qbMock as unknown as ReturnType<typeof repo.createQueryBuilder>,
|
||||
);
|
||||
|
||||
expect(await repo.countDistinctWorkflowsWithConfigs()).toBe(7);
|
||||
expect(qbMock.select).toHaveBeenCalledWith('evaluation_config.workflowId');
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import type { SqliteConfig } from '@n8n/config';
|
|||
import { Container } from '@n8n/di';
|
||||
import type { SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { In, LessThan, LessThanOrEqual, And, Not } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { BinaryDataService } from 'n8n-core';
|
||||
import type { IRunExecutionData, IWorkflowBase } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { ExecutionEntity } from '../../entities';
|
||||
import type { IExecutionResponse } from '../../entities/types-db';
|
||||
|
|
@ -29,7 +29,7 @@ describe('ExecutionRepository', () => {
|
|||
const executionRepository = Container.get(ExecutionRepository);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('getExecutionsForPublicApi', () => {
|
||||
|
|
@ -403,7 +403,7 @@ describe('ExecutionRepository', () => {
|
|||
},
|
||||
} as unknown;
|
||||
|
||||
const updateSpy = jest.spyOn(executionRepository, 'updateExistingExecution');
|
||||
const updateSpy = vi.spyOn(executionRepository, 'updateExistingExecution');
|
||||
|
||||
const result = await executionRepository.stopDuringRun(mockExecution as IExecutionResponse);
|
||||
|
||||
|
|
@ -511,8 +511,8 @@ describe('ExecutionRepository', () => {
|
|||
describe('getWaitingExecutions', () => {
|
||||
const mockDate = new Date('2023-12-28 12:34:56.789Z');
|
||||
|
||||
beforeAll(() => jest.useFakeTimers().setSystemTime(mockDate));
|
||||
afterAll(() => jest.useRealTimers());
|
||||
beforeAll(() => vi.useFakeTimers().setSystemTime(mockDate));
|
||||
afterAll(() => vi.useRealTimers());
|
||||
|
||||
test.each(['sqlite', 'postgresdb'] as const)(
|
||||
'on %s, should only return executions with status=waiting',
|
||||
|
|
@ -543,11 +543,11 @@ describe('ExecutionRepository', () => {
|
|||
const workflowId = nanoid();
|
||||
const binaryDataService = Container.get(BinaryDataService);
|
||||
|
||||
jest.spyOn(executionRepository, 'createQueryBuilder').mockReturnValue(
|
||||
vi.spyOn(executionRepository, 'createQueryBuilder').mockReturnValue(
|
||||
mock<SelectQueryBuilder<ExecutionEntity>>({
|
||||
select: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn().mockResolvedValue([{ id: '1', workflowId }]),
|
||||
select: vi.fn().mockReturnThis(),
|
||||
andWhere: vi.fn().mockReturnThis(),
|
||||
getMany: vi.fn().mockResolvedValue([{ id: '1', workflowId }]),
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -578,7 +578,7 @@ describe('ExecutionRepository', () => {
|
|||
status: 'success',
|
||||
});
|
||||
|
||||
const txCallback = jest.fn();
|
||||
const txCallback = vi.fn();
|
||||
entityManager.transaction.mockImplementation(async (fn: unknown) => {
|
||||
await (fn as (em: typeof entityManager) => Promise<unknown>)(entityManager);
|
||||
txCallback();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import random from 'lodash/random';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { SecretsProviderConnection } from '../../entities';
|
||||
import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager';
|
||||
|
|
@ -24,7 +24,7 @@ describe('SecretsProviderConnectionRepository', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Container } from '@n8n/di';
|
||||
import { In, type SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { Mocked } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { SharedCredentials } from '../../entities';
|
||||
import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager';
|
||||
|
|
@ -10,10 +11,10 @@ describe('SharedCredentialsRepository', () => {
|
|||
const entityManager = mockEntityManager(SharedCredentials);
|
||||
const sharedCredentialsRepository = Container.get(SharedCredentialsRepository);
|
||||
|
||||
let queryBuilder: jest.Mocked<SelectQueryBuilder<SharedCredentials>>;
|
||||
let queryBuilder: Mocked<SelectQueryBuilder<SharedCredentials>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
|
||||
queryBuilder = mock<SelectQueryBuilder<SharedCredentials>>();
|
||||
queryBuilder.where.mockReturnThis();
|
||||
|
|
@ -21,7 +22,7 @@ describe('SharedCredentialsRepository', () => {
|
|||
queryBuilder.innerJoin.mockReturnThis();
|
||||
queryBuilder.select.mockReturnThis();
|
||||
|
||||
jest.spyOn(sharedCredentialsRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
vi.spyOn(sharedCredentialsRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
});
|
||||
|
||||
describe('findByCredentialIds', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Container } from '@n8n/di';
|
||||
import type { SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { Mocked } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { SharedWorkflow } from '../../entities';
|
||||
import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager';
|
||||
|
|
@ -10,10 +11,10 @@ describe('SharedWorkflowRepository', () => {
|
|||
mockEntityManager(SharedWorkflow);
|
||||
const sharedWorkflowRepository = Container.get(SharedWorkflowRepository);
|
||||
|
||||
let queryBuilder: jest.Mocked<SelectQueryBuilder<SharedWorkflow>>;
|
||||
let queryBuilder: Mocked<SelectQueryBuilder<SharedWorkflow>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
|
||||
queryBuilder = mock<SelectQueryBuilder<SharedWorkflow>>();
|
||||
queryBuilder.where.mockReturnThis();
|
||||
|
|
@ -21,7 +22,7 @@ describe('SharedWorkflowRepository', () => {
|
|||
queryBuilder.innerJoin.mockReturnThis();
|
||||
queryBuilder.select.mockReturnThis();
|
||||
|
||||
jest.spyOn(sharedWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
vi.spyOn(sharedWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
});
|
||||
|
||||
describe('getSharedPersonalWorkflowsCount', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { GlobalConfig } from '@n8n/config';
|
||||
import { In, type SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { Mock, Mocked } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { WorkflowEntity } from '../../entities';
|
||||
import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager';
|
||||
|
|
@ -26,10 +27,10 @@ describe('WorkflowRepository', () => {
|
|||
workflowHistoryRepository,
|
||||
);
|
||||
|
||||
let queryBuilder: jest.Mocked<SelectQueryBuilder<WorkflowEntity>>;
|
||||
let queryBuilder: Mocked<SelectQueryBuilder<WorkflowEntity>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
|
||||
queryBuilder = mock<SelectQueryBuilder<WorkflowEntity>>();
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ describe('WorkflowRepository', () => {
|
|||
writable: true,
|
||||
});
|
||||
|
||||
jest.spyOn(workflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
vi.spyOn(workflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
});
|
||||
|
||||
describe('applyNameFilter', () => {
|
||||
|
|
@ -118,7 +119,7 @@ describe('WorkflowRepository', () => {
|
|||
await workflowRepository.getMany(workflowIds, options);
|
||||
|
||||
// andWhere should not be called for name filter
|
||||
const nameFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) =>
|
||||
const nameFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) =>
|
||||
call[0]?.includes('workflow.name'),
|
||||
);
|
||||
expect(nameFilterCalls).toHaveLength(0);
|
||||
|
|
@ -133,7 +134,7 @@ describe('WorkflowRepository', () => {
|
|||
await workflowRepository.getMany(workflowIds, options);
|
||||
|
||||
// andWhere should not be called for name filter
|
||||
const nameFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) =>
|
||||
const nameFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) =>
|
||||
call[0]?.includes('workflow.name'),
|
||||
);
|
||||
expect(nameFilterCalls).toHaveLength(0);
|
||||
|
|
@ -151,7 +152,7 @@ describe('WorkflowRepository', () => {
|
|||
sharedWorkflowRepository,
|
||||
workflowHistoryRepository,
|
||||
);
|
||||
jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
|
||||
const workflowIds = ['workflow1'];
|
||||
const options = {
|
||||
|
|
@ -190,13 +191,13 @@ describe('WorkflowRepository', () => {
|
|||
|
||||
await workflowRepository.getMany(workflowIds, options);
|
||||
|
||||
const andWhereCall = (queryBuilder.andWhere as jest.Mock).mock.calls.find((call) =>
|
||||
const andWhereCall = (queryBuilder.andWhere as Mock).mock.calls.find((call) =>
|
||||
call[0]?.includes('workflow.name'),
|
||||
);
|
||||
|
||||
expect(andWhereCall).toBeDefined();
|
||||
expect(andWhereCall[0]).toContain('workflow.name');
|
||||
expect(andWhereCall[0]).toContain('workflow.description');
|
||||
expect(andWhereCall![0]).toContain('workflow.name');
|
||||
expect(andWhereCall![0]).toContain('workflow.description');
|
||||
});
|
||||
|
||||
it('should handle special characters in search query', async () => {
|
||||
|
|
@ -293,7 +294,7 @@ describe('WorkflowRepository', () => {
|
|||
|
||||
await workflowRepository.getMany(workflowIds, {});
|
||||
|
||||
const leftJoinCalls = (queryBuilder.leftJoin as jest.Mock).mock.calls.filter(
|
||||
const leftJoinCalls = (queryBuilder.leftJoin as Mock).mock.calls.filter(
|
||||
(call) => call[0] === 'workflow.activeVersion',
|
||||
);
|
||||
expect(leftJoinCalls).toHaveLength(0);
|
||||
|
|
@ -307,7 +308,7 @@ describe('WorkflowRepository', () => {
|
|||
|
||||
await workflowRepository.getMany(workflowIds, options);
|
||||
|
||||
const leftJoinCalls = (queryBuilder.leftJoin as jest.Mock).mock.calls.filter(
|
||||
const leftJoinCalls = (queryBuilder.leftJoin as Mock).mock.calls.filter(
|
||||
(call) => call[0] === 'workflow.activeVersion',
|
||||
);
|
||||
expect(leftJoinCalls).toHaveLength(0);
|
||||
|
|
@ -372,7 +373,7 @@ describe('WorkflowRepository', () => {
|
|||
sharedWorkflowRepository,
|
||||
workflowHistoryRepository,
|
||||
);
|
||||
jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
|
||||
const workflowIds = ['workflow1'];
|
||||
const options = {
|
||||
|
|
@ -401,7 +402,7 @@ describe('WorkflowRepository', () => {
|
|||
sharedWorkflowRepository,
|
||||
workflowHistoryRepository,
|
||||
);
|
||||
jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
|
||||
const workflowIds = ['workflow1'];
|
||||
const options = {
|
||||
|
|
@ -438,7 +439,7 @@ describe('WorkflowRepository', () => {
|
|||
await workflowRepository.getMany(workflowIds, options);
|
||||
|
||||
// leftJoin should not be called for activeVersion since it's already joined
|
||||
const activeVersionJoinCalls = (queryBuilder.leftJoin as jest.Mock).mock.calls.filter(
|
||||
const activeVersionJoinCalls = (queryBuilder.leftJoin as Mock).mock.calls.filter(
|
||||
(call) => call[0] === 'workflow.activeVersion',
|
||||
);
|
||||
expect(activeVersionJoinCalls).toHaveLength(0);
|
||||
|
|
@ -458,7 +459,7 @@ describe('WorkflowRepository', () => {
|
|||
|
||||
await workflowRepository.getMany(workflowIds, options);
|
||||
|
||||
const triggerFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) =>
|
||||
const triggerFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) =>
|
||||
call[0]?.includes?.('triggerNodeType'),
|
||||
);
|
||||
expect(triggerFilterCalls).toHaveLength(0);
|
||||
|
|
@ -472,7 +473,7 @@ describe('WorkflowRepository', () => {
|
|||
|
||||
await workflowRepository.getMany(workflowIds, options);
|
||||
|
||||
const triggerFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) =>
|
||||
const triggerFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) =>
|
||||
call[0]?.includes?.('triggerNodeType'),
|
||||
);
|
||||
expect(triggerFilterCalls).toHaveLength(0);
|
||||
|
|
@ -486,7 +487,7 @@ describe('WorkflowRepository', () => {
|
|||
|
||||
await workflowRepository.getMany(workflowIds, options);
|
||||
|
||||
const triggerFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) =>
|
||||
const triggerFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) =>
|
||||
call[0]?.includes?.('triggerNodeType'),
|
||||
);
|
||||
expect(triggerFilterCalls).toHaveLength(0);
|
||||
|
|
@ -505,10 +506,10 @@ describe('WorkflowRepository', () => {
|
|||
await workflowRepository.getMany(workflowIds, options);
|
||||
|
||||
// Should have called andWhere for both name and triggerNodeTypes filters
|
||||
const nameFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) =>
|
||||
const nameFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) =>
|
||||
call[0]?.includes?.('workflow.name'),
|
||||
);
|
||||
const triggerFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) =>
|
||||
const triggerFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) =>
|
||||
call[0]?.includes?.('triggerNodeType'),
|
||||
);
|
||||
expect(nameFilterCalls.length).toBeGreaterThan(0);
|
||||
|
|
@ -521,7 +522,7 @@ describe('WorkflowRepository', () => {
|
|||
|
||||
describe('findByIds', () => {
|
||||
it('should return an empty array and not call the database when no workflow ids are provided', async () => {
|
||||
const findSpy = jest.spyOn(workflowRepository, 'find');
|
||||
const findSpy = vi.spyOn(workflowRepository, 'find');
|
||||
const workflowIds: string[] = [];
|
||||
const result = await workflowRepository.findByIds(workflowIds);
|
||||
|
||||
|
|
@ -530,7 +531,7 @@ describe('WorkflowRepository', () => {
|
|||
});
|
||||
|
||||
it('should call the database when workflow ids are provided', async () => {
|
||||
const findSpy = jest.spyOn(workflowRepository, 'find').mockResolvedValue([]);
|
||||
const findSpy = vi.spyOn(workflowRepository, 'find').mockResolvedValue([]);
|
||||
const workflowIds = ['workflow1'];
|
||||
const result = await workflowRepository.findByIds(workflowIds);
|
||||
expect(result).toEqual([]);
|
||||
|
|
@ -601,7 +602,7 @@ describe('WorkflowRepository', () => {
|
|||
sharedWorkflowRepository,
|
||||
workflowHistoryRepository,
|
||||
);
|
||||
jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder);
|
||||
queryBuilder.getMany.mockResolvedValue([]);
|
||||
|
||||
const result = await sqliteWorkflowRepository.findByCredentialResolverId('resolver-123');
|
||||
|
|
@ -624,13 +625,13 @@ describe('WorkflowRepository', () => {
|
|||
|
||||
describe('clearCredentialResolverId', () => {
|
||||
it('should use PostgreSQL jsonb removal for postgresdb', async () => {
|
||||
const mockExecute = jest.fn().mockResolvedValue({ affected: 1 });
|
||||
const mockUpdateWhere = jest.fn().mockReturnValue({ execute: mockExecute });
|
||||
const mockSet = jest.fn().mockReturnValue({ where: mockUpdateWhere });
|
||||
const mockUpdate = jest.fn().mockReturnValue({ set: mockSet });
|
||||
const mockExecute = vi.fn().mockResolvedValue({ affected: 1 });
|
||||
const mockUpdateWhere = vi.fn().mockReturnValue({ execute: mockExecute });
|
||||
const mockSet = vi.fn().mockReturnValue({ where: mockUpdateWhere });
|
||||
const mockUpdate = vi.fn().mockReturnValue({ set: mockSet });
|
||||
const updateQb = { update: mockUpdate } as unknown as SelectQueryBuilder<WorkflowEntity>;
|
||||
|
||||
jest.spyOn(workflowRepository, 'createQueryBuilder').mockReturnValue(updateQb);
|
||||
vi.spyOn(workflowRepository, 'createQueryBuilder').mockReturnValue(updateQb);
|
||||
|
||||
await workflowRepository.clearCredentialResolverId('resolver-123');
|
||||
|
||||
|
|
@ -646,10 +647,10 @@ describe('WorkflowRepository', () => {
|
|||
});
|
||||
|
||||
it('should use SQLite json_remove for sqlite', async () => {
|
||||
const mockExecute = jest.fn().mockResolvedValue({ affected: 1 });
|
||||
const mockUpdateWhere = jest.fn().mockReturnValue({ execute: mockExecute });
|
||||
const mockSet = jest.fn().mockReturnValue({ where: mockUpdateWhere });
|
||||
const mockUpdate = jest.fn().mockReturnValue({ set: mockSet });
|
||||
const mockExecute = vi.fn().mockResolvedValue({ affected: 1 });
|
||||
const mockUpdateWhere = vi.fn().mockReturnValue({ execute: mockExecute });
|
||||
const mockSet = vi.fn().mockReturnValue({ where: mockUpdateWhere });
|
||||
const mockUpdate = vi.fn().mockReturnValue({ set: mockSet });
|
||||
const updateQb = { update: mockUpdate } as unknown as SelectQueryBuilder<WorkflowEntity>;
|
||||
|
||||
const sqliteConfig = mockInstance(GlobalConfig, {
|
||||
|
|
@ -662,7 +663,7 @@ describe('WorkflowRepository', () => {
|
|||
sharedWorkflowRepository,
|
||||
workflowHistoryRepository,
|
||||
);
|
||||
jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(updateQb);
|
||||
vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(updateQb);
|
||||
|
||||
await sqliteWorkflowRepository.clearCredentialResolverId('resolver-123');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { DatabaseConfig } from '@n8n/config';
|
||||
import { QueryFailedError } from '@n8n/typeorm';
|
||||
import type { DataSource, EntityManager } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { OperationalError } from 'n8n-workflow';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { DbLockService } from '../db-lock.service';
|
||||
|
||||
|
|
@ -11,12 +11,13 @@ describe('DbLockService', () => {
|
|||
const dataSource = mock<DataSource>();
|
||||
const databaseConfig = mock<DatabaseConfig>();
|
||||
|
||||
const transactionMock = jest.fn<Promise<unknown>, [(tx: EntityManager) => Promise<unknown>]>();
|
||||
const transactionMock =
|
||||
vi.fn<(...args: [(tx: EntityManager) => Promise<unknown>]) => Promise<unknown>>();
|
||||
|
||||
let service: DbLockService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
vi.resetAllMocks();
|
||||
transactionMock.mockImplementation(async (fn) => await fn(mockTx));
|
||||
dataSource.manager.transaction = transactionMock as never;
|
||||
mockTx.query.mockResolvedValue([]);
|
||||
|
|
@ -26,7 +27,7 @@ describe('DbLockService', () => {
|
|||
describe('withLock', () => {
|
||||
it('should acquire advisory lock and execute fn on Postgres', async () => {
|
||||
databaseConfig.type = 'postgresdb';
|
||||
const fn = jest.fn().mockResolvedValue('result');
|
||||
const fn = vi.fn().mockResolvedValue('result');
|
||||
|
||||
const result = await service.withLock(1001, fn);
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ describe('DbLockService', () => {
|
|||
|
||||
it('should skip advisory lock on SQLite and still execute fn', async () => {
|
||||
databaseConfig.type = 'sqlite';
|
||||
const fn = jest.fn().mockResolvedValue('result');
|
||||
const fn = vi.fn().mockResolvedValue('result');
|
||||
|
||||
const result = await service.withLock(1001, fn);
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ describe('DbLockService', () => {
|
|||
|
||||
it('should set lock_timeout when timeoutMs is provided', async () => {
|
||||
databaseConfig.type = 'postgresdb';
|
||||
const fn = jest.fn().mockResolvedValue('result');
|
||||
const fn = vi.fn().mockResolvedValue('result');
|
||||
|
||||
await service.withLock(1001, fn, { timeoutMs: 5000 });
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ describe('DbLockService', () => {
|
|||
|
||||
it('should not set lock_timeout when timeoutMs is not provided', async () => {
|
||||
databaseConfig.type = 'postgresdb';
|
||||
const fn = jest.fn().mockResolvedValue('result');
|
||||
const fn = vi.fn().mockResolvedValue('result');
|
||||
|
||||
await service.withLock(1001, fn);
|
||||
|
||||
|
|
@ -68,7 +69,7 @@ describe('DbLockService', () => {
|
|||
|
||||
it('should throw OperationalError when lock timeout is exceeded', async () => {
|
||||
databaseConfig.type = 'postgresdb';
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
const timeoutError = new QueryFailedError(
|
||||
'SELECT pg_advisory_xact_lock($1)',
|
||||
[1001],
|
||||
|
|
@ -86,7 +87,7 @@ describe('DbLockService', () => {
|
|||
|
||||
it('should include timeout details in OperationalError message', async () => {
|
||||
databaseConfig.type = 'postgresdb';
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
const timeoutError = new QueryFailedError(
|
||||
'SELECT pg_advisory_xact_lock($1)',
|
||||
[1001],
|
||||
|
|
@ -102,7 +103,7 @@ describe('DbLockService', () => {
|
|||
|
||||
it('should propagate non-timeout errors unchanged', async () => {
|
||||
databaseConfig.type = 'postgresdb';
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
const otherError = new Error('connection lost');
|
||||
|
||||
mockTx.query.mockRejectedValueOnce(otherError);
|
||||
|
|
@ -115,7 +116,7 @@ describe('DbLockService', () => {
|
|||
describe('tryWithLock', () => {
|
||||
it('should execute fn when lock is acquired', async () => {
|
||||
databaseConfig.type = 'postgresdb';
|
||||
const fn = jest.fn().mockResolvedValue('result');
|
||||
const fn = vi.fn().mockResolvedValue('result');
|
||||
mockTx.query.mockResolvedValueOnce([{ pg_try_advisory_xact_lock: true }]);
|
||||
|
||||
const result = await service.tryWithLock(1001, fn);
|
||||
|
|
@ -127,7 +128,7 @@ describe('DbLockService', () => {
|
|||
|
||||
it('should throw OperationalError when lock is already held', async () => {
|
||||
databaseConfig.type = 'postgresdb';
|
||||
const fn = jest.fn();
|
||||
const fn = vi.fn();
|
||||
mockTx.query.mockResolvedValueOnce([{ pg_try_advisory_xact_lock: false }]);
|
||||
|
||||
const error = await service.tryWithLock(1001, fn).catch((e: unknown) => e);
|
||||
|
|
@ -140,7 +141,7 @@ describe('DbLockService', () => {
|
|||
|
||||
it('should skip advisory lock on SQLite and execute fn', async () => {
|
||||
databaseConfig.type = 'sqlite';
|
||||
const fn = jest.fn().mockResolvedValue('result');
|
||||
const fn = vi.fn().mockResolvedValue('result');
|
||||
|
||||
const result = await service.tryWithLock(1001, fn);
|
||||
|
||||
|
|
@ -156,7 +157,7 @@ describe('DbLockService', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should serialize concurrent withLock calls for the same lockId', async () => {
|
||||
|
|
@ -166,13 +167,13 @@ describe('DbLockService', () => {
|
|||
resolveFirst = r;
|
||||
});
|
||||
|
||||
const fn1 = jest.fn(async () => {
|
||||
const fn1 = vi.fn(async () => {
|
||||
executionOrder.push('fn1-start');
|
||||
await firstBlocking;
|
||||
executionOrder.push('fn1-end');
|
||||
return 'first';
|
||||
});
|
||||
const fn2 = jest.fn(async () => {
|
||||
const fn2 = vi.fn(async () => {
|
||||
executionOrder.push('fn2-start');
|
||||
return 'second';
|
||||
});
|
||||
|
|
@ -199,11 +200,11 @@ describe('DbLockService', () => {
|
|||
resolveFirst = r;
|
||||
});
|
||||
|
||||
const fn1 = jest.fn(async () => {
|
||||
const fn1 = vi.fn(async () => {
|
||||
await firstBlocking;
|
||||
return 'first';
|
||||
});
|
||||
const fn2 = jest.fn(async () => 'second');
|
||||
const fn2 = vi.fn(async () => 'second');
|
||||
|
||||
const p1 = service.withLock(1001, fn1);
|
||||
const p2 = service.withLock(9999, fn2);
|
||||
|
|
@ -220,23 +221,23 @@ describe('DbLockService', () => {
|
|||
});
|
||||
|
||||
it('should reject with OperationalError when withLock timeout expires', async () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
let resolveFirst!: () => void;
|
||||
const firstBlocking = new Promise<void>((r) => {
|
||||
resolveFirst = r;
|
||||
});
|
||||
|
||||
const fn1 = jest.fn(async () => {
|
||||
const fn1 = vi.fn(async () => {
|
||||
await firstBlocking;
|
||||
return 'first';
|
||||
});
|
||||
const fn2 = jest.fn().mockResolvedValue('second');
|
||||
const fn2 = vi.fn().mockResolvedValue('second');
|
||||
|
||||
const p1 = service.withLock(1001, fn1);
|
||||
const p2 = service.withLock(1001, fn2, { timeoutMs: 100 });
|
||||
|
||||
jest.advanceTimersByTime(100);
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
await expect(p2).rejects.toThrow(OperationalError);
|
||||
await expect(p2).rejects.toThrow(/Timed out waiting for DbLock 1001 after 100ms/);
|
||||
|
|
@ -252,11 +253,11 @@ describe('DbLockService', () => {
|
|||
resolveFirst = r;
|
||||
});
|
||||
|
||||
const fn1 = jest.fn(async () => {
|
||||
const fn1 = vi.fn(async () => {
|
||||
await firstBlocking;
|
||||
return 'first';
|
||||
});
|
||||
const fn2 = jest.fn().mockResolvedValue('second');
|
||||
const fn2 = vi.fn().mockResolvedValue('second');
|
||||
|
||||
const p1 = service.withLock(1001, fn1);
|
||||
|
||||
|
|
@ -273,8 +274,8 @@ describe('DbLockService', () => {
|
|||
});
|
||||
|
||||
it('should release lock when fn throws in withLock', async () => {
|
||||
const fn1 = jest.fn().mockRejectedValue(new Error('fn1 failed'));
|
||||
const fn2 = jest.fn().mockResolvedValue('second');
|
||||
const fn1 = vi.fn().mockRejectedValue(new Error('fn1 failed'));
|
||||
const fn2 = vi.fn().mockResolvedValue('second');
|
||||
|
||||
await expect(service.withLock(1001, fn1)).rejects.toThrow('fn1 failed');
|
||||
|
||||
|
|
@ -283,8 +284,8 @@ describe('DbLockService', () => {
|
|||
});
|
||||
|
||||
it('should release lock when fn throws in tryWithLock', async () => {
|
||||
const fn1 = jest.fn().mockRejectedValue(new Error('fn1 failed'));
|
||||
const fn2 = jest.fn().mockResolvedValue('second');
|
||||
const fn1 = vi.fn().mockRejectedValue(new Error('fn1 failed'));
|
||||
const fn2 = vi.fn().mockResolvedValue('second');
|
||||
|
||||
await expect(service.tryWithLock(1001, fn1)).rejects.toThrow('fn1 failed');
|
||||
|
||||
|
|
@ -298,12 +299,12 @@ describe('DbLockService', () => {
|
|||
resolveFirst = r;
|
||||
});
|
||||
|
||||
const fn1 = jest.fn(async () => {
|
||||
const fn1 = vi.fn(async () => {
|
||||
await firstBlocking;
|
||||
return 'first';
|
||||
});
|
||||
const fn2 = jest.fn().mockResolvedValue('second');
|
||||
const fn3 = jest.fn().mockResolvedValue('third');
|
||||
const fn2 = vi.fn().mockResolvedValue('second');
|
||||
const fn3 = vi.fn().mockResolvedValue('third');
|
||||
|
||||
const p1 = service.withLock(1001, fn1);
|
||||
const p2 = service.withLock(1001, fn2);
|
||||
|
|
@ -331,17 +332,17 @@ describe('DbLockService', () => {
|
|||
resolve2 = r;
|
||||
});
|
||||
|
||||
const fn1 = jest.fn(async () => {
|
||||
const fn1 = vi.fn(async () => {
|
||||
executionOrder.push('fn1');
|
||||
await blocking1;
|
||||
return 'first';
|
||||
});
|
||||
const fn2 = jest.fn(async () => {
|
||||
const fn2 = vi.fn(async () => {
|
||||
executionOrder.push('fn2');
|
||||
await blocking2;
|
||||
return 'second';
|
||||
});
|
||||
const fn3 = jest.fn(async () => {
|
||||
const fn3 = vi.fn(async () => {
|
||||
executionOrder.push('fn3');
|
||||
return 'third';
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { applyWorkflowBooleanSettingFilter } from '../apply-workflow-boolean-set
|
|||
|
||||
function createMockQb() {
|
||||
const qb = {
|
||||
andWhere: jest.fn(),
|
||||
where: jest.fn(),
|
||||
orWhere: jest.fn(),
|
||||
andWhere: vi.fn(),
|
||||
where: vi.fn(),
|
||||
orWhere: vi.fn(),
|
||||
} as unknown as SelectQueryBuilder<object>;
|
||||
return qb;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import type { TestCaseExecution } from '../../entities';
|
||||
import { getTestRunFinalResult } from '../get-final-test-result';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { DataSource, EntityManager, type EntityMetadata } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { Class } from 'n8n-core';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { mockInstance } from './mock-instance';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Container, type Constructable } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
export const mockInstance = <T>(
|
||||
serviceClass: Constructable<T>,
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@
|
|||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/__tests__/**"]
|
||||
"exclude": ["src/**/__tests__/**", "src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"],
|
||||
"types": ["node", "vitest/globals"],
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
|
|
|||
156
packages/@n8n/db/vite.config.ts
Normal file
156
packages/@n8n/db/vite.config.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decorators';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import ts from 'typescript';
|
||||
import { mergeConfig, type Plugin } from 'vite';
|
||||
import { configDefaults } from 'vitest/config';
|
||||
|
||||
/**
|
||||
* Vite plugin that transpiles this package's TypeORM entity files (`src/entities/**`)
|
||||
* through the real TypeScript compiler (a full `ts.LanguageService`, not single-file
|
||||
* `transpileModule`). Every other source file is left to Vite's fast oxc transform.
|
||||
*
|
||||
* TypeORM entities rely on `emitDecoratorMetadata` to derive column types from the
|
||||
* reflected `design:type`. For a string-literal union column (e.g.
|
||||
* `providerType: AuthProviderType`, where the alias is imported from another file),
|
||||
* only `tsc` with cross-file type information collapses the union to `String`. Vite's
|
||||
* oxc transform — and SWC — emit `Object` instead, which TypeORM rejects at
|
||||
* `DataSource.initialize()`. Single-file `transpileModule` also emits `Object` because
|
||||
* it can't resolve the imported alias. A full Program is required, which mirrors the
|
||||
* old jest config that set `isolatedModules: false` for exactly this reason.
|
||||
*
|
||||
* Scoping to `src/entities/**` keeps the cost contained: only the ~50 entity files pay
|
||||
* the tsc price (and the Program is rooted there), while DI `@Service` constructor
|
||||
* metadata — which oxc emits correctly — keeps the fast path for the rest of `src`.
|
||||
*/
|
||||
function tscDecoratorTransform(): Plugin {
|
||||
const projectDir = __dirname;
|
||||
const entitiesDir = path.resolve(projectDir, 'src', 'entities') + path.sep;
|
||||
let emit: ((fileName: string) => { code: string; map: unknown } | null) | undefined;
|
||||
|
||||
function createEmitter() {
|
||||
const configPath = ts.findConfigFile(projectDir, ts.sys.fileExists, 'tsconfig.json');
|
||||
if (!configPath) {
|
||||
throw new Error('Could not find tsconfig.json for @n8n/db');
|
||||
}
|
||||
|
||||
const { config } = ts.readConfigFile(configPath, ts.sys.readFile);
|
||||
const parsed = ts.parseJsonConfigFileContent(config, ts.sys, projectDir);
|
||||
// Root the Program at the entity files only; their imported types (e.g. the union
|
||||
// aliases in `types-db.ts`, related entities) are still resolved on demand via the
|
||||
// host's snapshot reads, so cross-file metadata stays correct.
|
||||
const rootFiles = parsed.fileNames.filter((f) => path.normalize(f).startsWith(entitiesDir));
|
||||
|
||||
const options: ts.CompilerOptions = {
|
||||
...parsed.options,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: true,
|
||||
verbatimModuleSyntax: false,
|
||||
isolatedModules: false,
|
||||
sourceMap: true,
|
||||
inlineSources: true,
|
||||
skipLibCheck: true,
|
||||
noEmit: false,
|
||||
noEmitOnError: false,
|
||||
declaration: false,
|
||||
declarationMap: false,
|
||||
composite: false,
|
||||
incremental: false,
|
||||
tsBuildInfoFile: undefined,
|
||||
};
|
||||
|
||||
const versions = new Map<string, number>();
|
||||
const contents = new Map<string, string>();
|
||||
for (const f of rootFiles) {
|
||||
versions.set(path.normalize(f), 0);
|
||||
}
|
||||
|
||||
// Re-read `fileName` from disk and, if its content changed since the last read,
|
||||
// bump the script version so the LanguageService invalidates its cached emit.
|
||||
function refresh(fileName: string): string | undefined {
|
||||
const norm = path.normalize(fileName);
|
||||
const next = fs.existsSync(norm) ? fs.readFileSync(norm, 'utf-8') : undefined;
|
||||
if (next !== contents.get(norm)) {
|
||||
if (next === undefined) {
|
||||
contents.delete(norm);
|
||||
} else {
|
||||
contents.set(norm, next);
|
||||
}
|
||||
|
||||
versions.set(norm, (versions.get(norm) ?? 0) + 1);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
const host: ts.LanguageServiceHost = {
|
||||
getScriptFileNames: () => Array.from(versions.keys()),
|
||||
getScriptVersion: (f) => String(versions.get(path.normalize(f)) ?? 0),
|
||||
getScriptSnapshot: (f) => {
|
||||
const cached = contents.get(path.normalize(f));
|
||||
const text = cached ?? (fs.existsSync(f) ? fs.readFileSync(f, 'utf-8') : undefined);
|
||||
return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text);
|
||||
},
|
||||
getCurrentDirectory: () => projectDir,
|
||||
getCompilationSettings: () => options,
|
||||
getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
|
||||
fileExists: ts.sys.fileExists,
|
||||
readFile: ts.sys.readFile,
|
||||
readDirectory: ts.sys.readDirectory,
|
||||
directoryExists: ts.sys.directoryExists,
|
||||
getDirectories: ts.sys.getDirectories,
|
||||
};
|
||||
|
||||
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
|
||||
|
||||
return (fileName: string) => {
|
||||
const norm = path.normalize(fileName);
|
||||
// Pick up on-disk edits (watch mode) by bumping the script version when the
|
||||
// content changes; otherwise the LanguageService reuses a stale cached emit.
|
||||
if (refresh(norm) === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const output = service.getEmitOutput(norm);
|
||||
const js = output.outputFiles.find((f) => f.name.endsWith('.js'));
|
||||
const map = output.outputFiles.find((f) => f.name.endsWith('.js.map'));
|
||||
if (!js) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { code: js.text, map: map ? (JSON.parse(map.text) as unknown) : null };
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'tsc-decorator-transform',
|
||||
enforce: 'pre',
|
||||
transform(_code, id) {
|
||||
const file = id.split('?')[0];
|
||||
if (!file.startsWith(entitiesDir) || !/\.tsx?$/.test(file)) return null;
|
||||
emit ??= createEmitter();
|
||||
const result = emit(file);
|
||||
return result ? { code: result.code, map: result.map as never } : null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default mergeConfig(
|
||||
createVitestConfigWithDecorators({
|
||||
// The n8n root jest.config sets `restoreMocks: true`, and most test files silently
|
||||
// rely on it — omit this and mocks bleed between tests.
|
||||
restoreMocks: true,
|
||||
}),
|
||||
{
|
||||
plugins: [tscDecoratorTransform()],
|
||||
test: {
|
||||
// Vitest 4's default exclude is only node_modules/.git — it does NOT cover dist.
|
||||
// Without this, compiled test files left in dist (tsc never deletes orphaned
|
||||
// output) get collected and fail (CJS `require('vitest')`). The build also
|
||||
// excludes test files now, but this guards against pre-existing stale artifacts.
|
||||
exclude: [...configDefaults.exclude, '**/dist/**'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -1531,12 +1531,27 @@ importers:
|
|||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../typescript-config
|
||||
'@n8n/vitest-config':
|
||||
specifier: workspace:*
|
||||
version: link:../vitest-config
|
||||
'@types/lodash':
|
||||
specifier: 'catalog:'
|
||||
version: 4.17.17
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
|
||||
express:
|
||||
specifier: 5.1.0
|
||||
version: 5.1.0
|
||||
typescript:
|
||||
specifier: 6.0.2
|
||||
version: 6.0.2
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
|
||||
vitest-mock-extended:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.0(typescript@6.0.2)(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
|
||||
|
||||
packages/@n8n/decorators:
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user