diff --git a/lib/conditional.module.ts b/lib/conditional.module.ts new file mode 100644 index 00000000..41875988 --- /dev/null +++ b/lib/conditional.module.ts @@ -0,0 +1,48 @@ +import { DynamicModule, Logger, ModuleMetadata } from '@nestjs/common'; +import { ConfigModule } from './config.module'; + +/** + * @publicApi + */ +export class ConditionalModule { + /** + * @publicApi + */ + static async registerWhen( + module: Required['imports'][number], + condition: string | ((env: NodeJS.ProcessEnv) => boolean), + options?: { timeout: number }, + ) { + let configResolved = false; + const { timeout = 5000 } = options ?? {}; + setTimeout(() => { + if (!configResolved) { + throw new Error( + `Nest was not able to resolve the config variables within ${timeout} milliseconds. Bause of this, the ConditionalModule was not able to determine if ${module.toString()} should be registered or not`, + ); + } + }, timeout); + const returnModule: Required< + Pick + > = { module: ConditionalModule, imports: [], exports: [] }; + if (typeof condition === 'string') { + const key = condition; + condition = env => { + return env[key]?.toLowerCase() !== 'false'; + }; + } + await ConfigModule.envVariablesLoaded; + configResolved = true; + const evaluation = condition(process.env); + if (evaluation) { + returnModule.imports.push(module); + returnModule.exports.push(module); + } else { + Logger.debug( + `${condition.toString()} evaluated to false. Skipping the registration of ${module.toString()}`, + ConditionalModule.name, + ); + } + return returnModule; + } +} diff --git a/lib/index.ts b/lib/index.ts index 947ff4a2..94456d18 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,4 @@ +export * from './conditional.module'; export * from './config.module'; export * from './config.service'; export * from './types'; diff --git a/tests/e2e/.env.conditional b/tests/e2e/.env.conditional new file mode 100644 index 00000000..a2b0c386 --- /dev/null +++ b/tests/e2e/.env.conditional @@ -0,0 +1,7 @@ +FOO="use it" +FOO_FALSE="false" +FOO_DYNAMIC="yes" +FOO_CUSTOM="yeah!" +BAR="yay" +FOOBAR="do it" +QUU="nested!" \ No newline at end of file diff --git a/tests/e2e/conditional.module.spec.ts b/tests/e2e/conditional.module.spec.ts new file mode 100644 index 00000000..b70fa589 --- /dev/null +++ b/tests/e2e/conditional.module.spec.ts @@ -0,0 +1,128 @@ +import { Injectable, Module } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ConfigModule, ConditionalModule } from '../../lib'; +import { join } from 'path'; + +@Injectable() +class FooService {} + +@Injectable() +class FooDynamicService {} + +@Module({ + providers: [FooService], + exports: [FooService], +}) +class FooModule { + static forRoot() { + return { + module: FooModule, + providers: [FooDynamicService], + exports: [FooDynamicService], + }; + } +} + +@Injectable() +class BarService {} + +@Module({ + providers: [BarService], + exports: [BarService], +}) +class BarModule {} + +@Module({ + providers: [ + { + provide: 'quu', + useValue: 42, + }, + ], + exports: ['quu'], +}) +class QuuModule {} + +@Module({ + imports: [ConditionalModule.registerWhen(QuuModule, 'QUU')], + exports: [ConditionalModule], +}) +class FooBarModule {} + +describe('ConditionalModule', () => { + it('it should work for a regular module', async () => { + const modRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'), + }), + ConditionalModule.registerWhen(FooModule, 'FOO'), + ], + }).compile(); + expect(modRef.get(FooService, { strict: false })).toBeDefined(); + await modRef.close(); + }); + it('should work for a dynamic module', async () => { + const modRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'), + }), + ConditionalModule.registerWhen(FooModule.forRoot(), 'FOO_DYNAMIC'), + ], + }).compile(); + expect(modRef.get(FooDynamicService, { strict: false })).toBeDefined(); + await modRef.close(); + }); + it('should not register when the value is false', async () => { + const modRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'), + }), + ConditionalModule.registerWhen(FooModule, 'FOO_FALSE'), + ], + }).compile(); + expect(() => modRef.get(FooService, { strict: false })).toThrow(); + await modRef.close(); + }); + it('should work for a custom condition', async () => { + const modRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'), + }), + ConditionalModule.registerWhen(FooModule, env => { + return env.FOO_CUSTOM === 'yeah!'; + }), + ], + }).compile(); + expect(modRef.get(FooService, { strict: false })).toBeDefined(); + await modRef.close(); + }); + it('should handle two conditional modules', async () => { + const modRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'), + }), + ConditionalModule.registerWhen(FooModule, 'FOO'), + ConditionalModule.registerWhen(BarModule, 'BAR'), + ], + }).compile(); + expect(modRef.get(FooService, { strict: false })).toBeDefined(); + expect(modRef.get(BarService, { strict: false })).toBeDefined(); + await modRef.close(); + }); + it('should handle nested conditional module', async () => { + const modRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'), + }), + ConditionalModule.registerWhen(FooBarModule, 'FOOBAR'), + ], + }).compile(); + expect(modRef.get('quu', { strict: false })).toBeDefined(); + }); +});