diff --git a/lib/typeorm-core.module.ts b/lib/typeorm-core.module.ts index 07851f1ba..50f6bc3e6 100644 --- a/lib/typeorm-core.module.ts +++ b/lib/typeorm-core.module.ts @@ -35,7 +35,35 @@ import { TYPEORM_MODULE_ID, TYPEORM_MODULE_OPTIONS } from './typeorm.constants'; @Global() @Module({}) export class TypeOrmCoreModule implements OnApplicationShutdown { - private readonly logger = new Logger('TypeOrmModule'); + private static readonly logger = new Logger('TypeOrmModule'); + + private static validateDataSourceNames( + options: TypeOrmModuleOptions | TypeOrmModuleOptions[], + ): void { + const configs = Array.isArray(options) ? options : [options]; + const names = new Map(); + + configs.forEach((config) => { + if (!config) { + return; + } + const name = (config as any).name || 'default'; + const count = names.get(name) || 0; + names.set(name, count + 1); + }); + + const duplicates = Array.from(names.entries()) + .filter(([_, occurrences]) => occurrences > 1) + .map(([name]) => name); + + if (duplicates.length > 0) { + this.logger.warn( + `⚠️ WARNING: Duplicate DataSource names detected: [${duplicates.join(', ')}]. ` + + `Each DataSource should have a unique name to avoid conflicts.` + + `Multiple DataSources with default name will result in unexpected behaviour.`, + ); + } + } constructor( @Inject(TYPEORM_MODULE_OPTIONS) @@ -44,6 +72,7 @@ export class TypeOrmCoreModule implements OnApplicationShutdown { ) {} static forRoot(options: TypeOrmModuleOptions = {}): DynamicModule { + this.validateDataSourceNames(options); const typeOrmModuleOptions = { provide: TYPEORM_MODULE_OPTIONS, useValue: options, @@ -83,6 +112,7 @@ export class TypeOrmCoreModule implements OnApplicationShutdown { const dataSourceProvider = { provide: getDataSourceToken(options as DataSourceOptions), useFactory: async (typeOrmOptions: TypeOrmModuleOptions) => { + this.validateDataSourceNames(typeOrmOptions); if (options.name) { return await this.createDataSourceFactory( { @@ -147,7 +177,7 @@ export class TypeOrmCoreModule implements OnApplicationShutdown { await dataSource.destroy(); } } catch (e) { - this.logger.error(e?.message); + TypeOrmCoreModule.logger.error(e?.message); } } diff --git a/tests/e2e/typeorm-duplicate-datasource-names.spec.ts b/tests/e2e/typeorm-duplicate-datasource-names.spec.ts new file mode 100644 index 000000000..beceb124a --- /dev/null +++ b/tests/e2e/typeorm-duplicate-datasource-names.spec.ts @@ -0,0 +1,143 @@ +import { TypeOrmCoreModule } from '../../lib/typeorm-core.module'; +import { Logger } from '@nestjs/common'; + +describe('TypeOrmModule - validateDataSourceNames', () => { + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + warnSpy = jest + .spyOn((TypeOrmCoreModule as any).logger, 'warn') + .mockImplementation(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should log a warning when duplicate data source names are provided', () => { + const options = [ + { name: 'default', type: 'sqlite', database: ':memory:' }, + { name: 'secondary', type: 'sqlite', database: ':memory:' }, + { name: 'secondary', type: 'sqlite', database: ':memory:' }, + ]; + + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining( + 'Duplicate DataSource names detected: [secondary]', + ), + ); + }); + + it('should not log a warning when all data source names are unique', () => { + const options = [ + { name: 'default', type: 'sqlite', database: ':memory:' }, + { name: 'secondary', type: 'sqlite', database: ':memory:' }, + ]; + + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should treat missing names as "default" and log warning if repeated', () => { + const options = [ + { type: 'sqlite', database: ':memory:' }, + { type: 'sqlite', database: ':memory:' }, + ]; + + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Duplicate DataSource names detected: [default]'), + ); + }); + + it('should not log warning for a single configuration object', () => { + const options = { name: 'default', type: 'sqlite', database: ':memory:' }; + + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should handle an empty array without throwing or logging', () => { + const options: any[] = []; + + expect(() => { + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + }).not.toThrow(); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should handle mixed named and unnamed configurations correctly', () => { + const options = [ + { name: 'primary', type: 'sqlite', database: ':memory:' }, + { type: 'sqlite', database: ':memory:' }, + { type: 'sqlite', database: ':memory:' }, + ]; + + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Duplicate DataSource names detected: [default]'), + ); + }); + + it('should log warning when multiple different duplicates exist', () => { + const options = [ + { name: 'alpha', type: 'sqlite', database: ':memory:' }, + { name: 'alpha', type: 'sqlite', database: ':memory:' }, + { name: 'beta', type: 'sqlite', database: ':memory:' }, + { name: 'beta', type: 'sqlite', database: ':memory:' }, + ]; + + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining( + 'Duplicate DataSource names detected: [alpha, beta]', + ), + ); + }); + + it('should handle case-sensitive names as different (no warning)', () => { + const options = [ + { name: 'Main', type: 'sqlite', database: ':memory:' }, + { name: 'main', type: 'sqlite', database: ':memory:' }, + ]; + + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should not crash if one of the configs is null or undefined', () => { + const options = [ + { name: 'alpha', type: 'sqlite', database: ':memory:' }, + null as any, + undefined as any, + { name: 'beta', type: 'sqlite', database: ':memory:' }, + ]; + + expect(() => { + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + }).not.toThrow(); + }); + + it('should log warning if all names are missing (multiple "default")', () => { + const options = [ + { type: 'sqlite', database: ':memory:' }, + { type: 'sqlite', database: ':memory:' }, + { type: 'sqlite', database: ':memory:' }, + ]; + + (TypeOrmCoreModule as any)['validateDataSourceNames'](options); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Duplicate DataSource names detected: [default]'), + ); + }); +});