diff --git a/packages/@n8n/di/jest.config.js b/packages/@n8n/di/jest.config.js index d6c48554a7..d14f2d60c6 100644 --- a/packages/@n8n/di/jest.config.js +++ b/packages/@n8n/di/jest.config.js @@ -1,2 +1,7 @@ /** @type {import('jest').Config} */ -module.exports = require('../../../jest.config'); +module.exports = { + ...require('../../../jest.config'), + transform: { + '^.+\\.ts$': ['ts-jest', { isolatedModules: false }], + }, +}; diff --git a/packages/@n8n/di/src/__tests__/circular-depedency.test.ts b/packages/@n8n/di/src/__tests__/circular-depedency.test.ts new file mode 100644 index 0000000000..66bce38f7b --- /dev/null +++ b/packages/@n8n/di/src/__tests__/circular-depedency.test.ts @@ -0,0 +1,17 @@ +import { ServiceA } from './fixtures/ServiceA'; +import { ServiceB } from './fixtures/ServiceB'; +import { Container } from '../di'; + +describe('DI Container', () => { + describe('circular dependency', () => { + it('should detect multilevel circular dependencies', () => { + expect(() => Container.get(ServiceA)).toThrow( + '[DI] Circular dependency detected in ServiceB at index 0.\nServiceA -> ServiceB', + ); + + expect(() => Container.get(ServiceB)).toThrow( + '[DI] Circular dependency detected in ServiceB at index 0.\nServiceB', + ); + }); + }); +}); diff --git a/packages/@n8n/di/src/__tests__/fixtures/ServiceA.ts b/packages/@n8n/di/src/__tests__/fixtures/ServiceA.ts new file mode 100644 index 0000000000..83f4c90430 --- /dev/null +++ b/packages/@n8n/di/src/__tests__/fixtures/ServiceA.ts @@ -0,0 +1,8 @@ +// eslint-disable-next-line import/no-cycle +import { ServiceB } from './ServiceB'; +import { Service } from '../../di'; + +@Service() +export class ServiceA { + constructor(readonly b: ServiceB) {} +} diff --git a/packages/@n8n/di/src/__tests__/fixtures/ServiceB.ts b/packages/@n8n/di/src/__tests__/fixtures/ServiceB.ts new file mode 100644 index 0000000000..a0dbd1908a --- /dev/null +++ b/packages/@n8n/di/src/__tests__/fixtures/ServiceB.ts @@ -0,0 +1,8 @@ +// eslint-disable-next-line import/no-cycle +import { ServiceA } from './ServiceA'; +import { Service } from '../../di'; + +@Service() +export class ServiceB { + constructor(readonly a: ServiceA) {} +} diff --git a/packages/@n8n/di/src/di.ts b/packages/@n8n/di/src/di.ts index a4acb98474..08d86eae07 100644 --- a/packages/@n8n/di/src/di.ts +++ b/packages/@n8n/di/src/di.ts @@ -78,13 +78,6 @@ class ContainerClass { if (metadata?.instance) return metadata.instance as T; - // Check for circular dependencies before proceeding with instantiation - if (resolutionStack.includes(type)) { - throw new DIError( - `Circular dependency detected. ${resolutionStack.map((t) => t.name).join(' -> ')}`, - ); - } - // Add current type to resolution stack before resolving dependencies resolutionStack.push(type); @@ -96,9 +89,15 @@ class ContainerClass { } else { const paramTypes = (Reflect.getMetadata('design:paramtypes', type) ?? []) as Constructable[]; - const dependencies = paramTypes.map(

(paramType: Constructable

) => - this.get(paramType), - ); + + const dependencies = paramTypes.map(

(paramType: Constructable

, index: number) => { + if (paramType === undefined) { + throw new DIError( + `Circular dependency detected in ${type.name} at index ${index}.\n${resolutionStack.map((t) => t.name).join(' -> ')}\n`, + ); + } + return this.get(paramType); + }); // Create new instance with resolved dependencies instance = new (type as Constructable)(...dependencies) as T; }