Skip to content

Commit

Permalink
Merge pull request #1409 from jmcdo29/feat/conditional-module
Browse files Browse the repository at this point in the history
feat: create a conditional module
  • Loading branch information
kamilmysliwiec authored Sep 12, 2023
2 parents 57ed7ca + a9466c4 commit 9c42639
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 0 deletions.
48 changes: 48 additions & 0 deletions lib/conditional.module.ts
Original file line number Diff line number Diff line change
@@ -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<ModuleMetadata>['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<DynamicModule, 'module' | 'imports' | 'exports'>
> = { 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;
}
}
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './conditional.module';
export * from './config.module';
export * from './config.service';
export * from './types';
Expand Down
7 changes: 7 additions & 0 deletions tests/e2e/.env.conditional
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FOO="use it"
FOO_FALSE="false"
FOO_DYNAMIC="yes"
FOO_CUSTOM="yeah!"
BAR="yay"
FOOBAR="do it"
QUU="nested!"
128 changes: 128 additions & 0 deletions tests/e2e/conditional.module.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});

0 comments on commit 9c42639

Please sign in to comment.