Skip to content

Commit 9c42639

Browse files
Merge pull request #1409 from jmcdo29/feat/conditional-module
feat: create a conditional module
2 parents 57ed7ca + a9466c4 commit 9c42639

File tree

4 files changed

+184
-0
lines changed

4 files changed

+184
-0
lines changed

lib/conditional.module.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { DynamicModule, Logger, ModuleMetadata } from '@nestjs/common';
2+
import { ConfigModule } from './config.module';
3+
4+
/**
5+
* @publicApi
6+
*/
7+
export class ConditionalModule {
8+
/**
9+
* @publicApi
10+
*/
11+
static async registerWhen(
12+
module: Required<ModuleMetadata>['imports'][number],
13+
condition: string | ((env: NodeJS.ProcessEnv) => boolean),
14+
options?: { timeout: number },
15+
) {
16+
let configResolved = false;
17+
const { timeout = 5000 } = options ?? {};
18+
setTimeout(() => {
19+
if (!configResolved) {
20+
throw new Error(
21+
`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`,
22+
);
23+
}
24+
}, timeout);
25+
const returnModule: Required<
26+
Pick<DynamicModule, 'module' | 'imports' | 'exports'>
27+
> = { module: ConditionalModule, imports: [], exports: [] };
28+
if (typeof condition === 'string') {
29+
const key = condition;
30+
condition = env => {
31+
return env[key]?.toLowerCase() !== 'false';
32+
};
33+
}
34+
await ConfigModule.envVariablesLoaded;
35+
configResolved = true;
36+
const evaluation = condition(process.env);
37+
if (evaluation) {
38+
returnModule.imports.push(module);
39+
returnModule.exports.push(module);
40+
} else {
41+
Logger.debug(
42+
`${condition.toString()} evaluated to false. Skipping the registration of ${module.toString()}`,
43+
ConditionalModule.name,
44+
);
45+
}
46+
return returnModule;
47+
}
48+
}

lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './conditional.module';
12
export * from './config.module';
23
export * from './config.service';
34
export * from './types';

tests/e2e/.env.conditional

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FOO="use it"
2+
FOO_FALSE="false"
3+
FOO_DYNAMIC="yes"
4+
FOO_CUSTOM="yeah!"
5+
BAR="yay"
6+
FOOBAR="do it"
7+
QUU="nested!"

tests/e2e/conditional.module.spec.ts

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Injectable, Module } from '@nestjs/common';
2+
import { Test } from '@nestjs/testing';
3+
import { ConfigModule, ConditionalModule } from '../../lib';
4+
import { join } from 'path';
5+
6+
@Injectable()
7+
class FooService {}
8+
9+
@Injectable()
10+
class FooDynamicService {}
11+
12+
@Module({
13+
providers: [FooService],
14+
exports: [FooService],
15+
})
16+
class FooModule {
17+
static forRoot() {
18+
return {
19+
module: FooModule,
20+
providers: [FooDynamicService],
21+
exports: [FooDynamicService],
22+
};
23+
}
24+
}
25+
26+
@Injectable()
27+
class BarService {}
28+
29+
@Module({
30+
providers: [BarService],
31+
exports: [BarService],
32+
})
33+
class BarModule {}
34+
35+
@Module({
36+
providers: [
37+
{
38+
provide: 'quu',
39+
useValue: 42,
40+
},
41+
],
42+
exports: ['quu'],
43+
})
44+
class QuuModule {}
45+
46+
@Module({
47+
imports: [ConditionalModule.registerWhen(QuuModule, 'QUU')],
48+
exports: [ConditionalModule],
49+
})
50+
class FooBarModule {}
51+
52+
describe('ConditionalModule', () => {
53+
it('it should work for a regular module', async () => {
54+
const modRef = await Test.createTestingModule({
55+
imports: [
56+
ConfigModule.forRoot({
57+
envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
58+
}),
59+
ConditionalModule.registerWhen(FooModule, 'FOO'),
60+
],
61+
}).compile();
62+
expect(modRef.get(FooService, { strict: false })).toBeDefined();
63+
await modRef.close();
64+
});
65+
it('should work for a dynamic module', async () => {
66+
const modRef = await Test.createTestingModule({
67+
imports: [
68+
ConfigModule.forRoot({
69+
envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
70+
}),
71+
ConditionalModule.registerWhen(FooModule.forRoot(), 'FOO_DYNAMIC'),
72+
],
73+
}).compile();
74+
expect(modRef.get(FooDynamicService, { strict: false })).toBeDefined();
75+
await modRef.close();
76+
});
77+
it('should not register when the value is false', async () => {
78+
const modRef = await Test.createTestingModule({
79+
imports: [
80+
ConfigModule.forRoot({
81+
envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
82+
}),
83+
ConditionalModule.registerWhen(FooModule, 'FOO_FALSE'),
84+
],
85+
}).compile();
86+
expect(() => modRef.get(FooService, { strict: false })).toThrow();
87+
await modRef.close();
88+
});
89+
it('should work for a custom condition', async () => {
90+
const modRef = await Test.createTestingModule({
91+
imports: [
92+
ConfigModule.forRoot({
93+
envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
94+
}),
95+
ConditionalModule.registerWhen(FooModule, env => {
96+
return env.FOO_CUSTOM === 'yeah!';
97+
}),
98+
],
99+
}).compile();
100+
expect(modRef.get(FooService, { strict: false })).toBeDefined();
101+
await modRef.close();
102+
});
103+
it('should handle two conditional modules', async () => {
104+
const modRef = await Test.createTestingModule({
105+
imports: [
106+
ConfigModule.forRoot({
107+
envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
108+
}),
109+
ConditionalModule.registerWhen(FooModule, 'FOO'),
110+
ConditionalModule.registerWhen(BarModule, 'BAR'),
111+
],
112+
}).compile();
113+
expect(modRef.get(FooService, { strict: false })).toBeDefined();
114+
expect(modRef.get(BarService, { strict: false })).toBeDefined();
115+
await modRef.close();
116+
});
117+
it('should handle nested conditional module', async () => {
118+
const modRef = await Test.createTestingModule({
119+
imports: [
120+
ConfigModule.forRoot({
121+
envFilePath: join(process.cwd(), 'tests', 'e2e', '.env.conditional'),
122+
}),
123+
ConditionalModule.registerWhen(FooBarModule, 'FOOBAR'),
124+
],
125+
}).compile();
126+
expect(modRef.get('quu', { strict: false })).toBeDefined();
127+
});
128+
});

0 commit comments

Comments
 (0)