Skip to content

Commit 641559f

Browse files
committed
feat: Add EnvModule to help with loading and handling environment variables
1 parent 797c471 commit 641559f

10 files changed

+167
-31
lines changed

apps/nest/src/app.controller.spec.ts

-24
This file was deleted.

apps/nest/src/app.controller.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { AppController } from './app.controller';
2+
import { AppService } from './app.service';
3+
import { TestContainer, Supertest } from '@spuxx/nest-utils';
4+
5+
describe('AppController', () => {
6+
let supertest: Supertest;
7+
8+
beforeEach(async () => {
9+
const container = await TestContainer.create({
10+
controllers: [AppController],
11+
providers: [AppService],
12+
enableEndToEnd: true,
13+
});
14+
supertest = container.supertest;
15+
});
16+
17+
describe('root', () => {
18+
it('should be successful', async () => {
19+
const response = await supertest.get('/');
20+
expect(response.statusCode).toBe(200);
21+
expect(response.text).toContain('Hello!');
22+
});
23+
});
24+
});

apps/nest/src/app.module.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Module } from '@nestjs/common';
22
import { AppController } from './app.controller';
33
import { AppService } from './app.service';
4-
import { NestUtilsModule } from '@spuxx/nest-utils';
4+
import { EnvModule } from './env/env.module';
55

66
@Module({
7-
imports: [NestUtilsModule],
7+
imports: [
8+
EnvModule,
9+
],
810
controllers: [AppController],
911
providers: [AppService],
1012
})

apps/nest/src/app.service.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { Injectable } from '@nestjs/common';
2-
import { NestUtilsService } from '@spuxx/nest-utils';
2+
import { EnvModule } from './env/env.module';
33

44
@Injectable()
55
export class AppService {
6-
constructor(private readonly utils: NestUtilsService) {}
7-
86
getHello(): string {
9-
return this.utils.getHello();
7+
return `Hello! The application is running since ${EnvModule.get('START_TIME')}.`;
108
}
119
}

apps/nest/src/env/env.definiton.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { IsDate, IsString } from 'class-validator';
2+
3+
export class Environment {
4+
@IsString()
5+
APP_NAME: string = 'nest';
6+
7+
@IsDate()
8+
START_TIME: Date;
9+
}

apps/nest/src/env/env.module.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { EnvModuleMixin } from '@spuxx/nest-utils';
2+
import { Environment } from './env.definiton';
3+
4+
export class EnvModule extends EnvModuleMixin<Environment>(Environment) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { IsBooleanString, IsDate, IsNumber, IsString } from 'class-validator';
2+
import { TestContainer } from '../testing/container';
3+
import { EnvModuleMixin } from './env.module-mixin';
4+
5+
describe('registration', () => {
6+
class Env {
7+
@IsString()
8+
SOME_STRING: string;
9+
}
10+
class EnvModule extends EnvModuleMixin<Env>(Env) {}
11+
12+
it('should properly load the environment variables and module', async () => {
13+
vi.stubEnv('SOME_STRING', 'foo');
14+
const container = await TestContainer.create({
15+
imports: [EnvModule],
16+
});
17+
const module = container.module.get<EnvModule>(EnvModule);
18+
expect(module).toBeDefined();
19+
expect(EnvModule.get('SOME_STRING')).toBe('foo');
20+
});
21+
});
22+
23+
describe('validation', () => {
24+
it('should throw an error due to a missing environment variable', () => {
25+
vi.unstubAllEnvs();
26+
class Env {
27+
@IsString()
28+
SOME_STRING: string;
29+
}
30+
class EnvModule extends EnvModuleMixin<Env>(Env) {}
31+
expect(
32+
TestContainer.create({
33+
imports: [EnvModule],
34+
}),
35+
).rejects.toThrowError(`An instance of Env has failed the validation:
36+
- property SOME_STRING has failed the following constraints: isString`);
37+
});
38+
39+
it('should throw an error due to a type mismatch', () => {
40+
vi.stubEnv('SOME_BOOLEAN', 'foo');
41+
class Env {
42+
@IsBooleanString()
43+
SOME_BOOLEAN: 'true' | 'false';
44+
}
45+
class EnvModule extends EnvModuleMixin<Env>(Env) {}
46+
expect(
47+
TestContainer.create({
48+
imports: [EnvModule],
49+
}),
50+
).rejects.toThrowError(`An instance of Env has failed the validation:
51+
- property SOME_BOOLEAN has failed the following constraints: isBooleanString`);
52+
});
53+
54+
it('should implicitly perform certain conversions', async () => {
55+
vi.stubEnv('SOME_DATE', '2024-08-13T17:34:00Z');
56+
vi.stubEnv('SOME_NUMBER', '123');
57+
class Env {
58+
@IsDate()
59+
SOME_DATE: Date;
60+
@IsNumber()
61+
SOME_NUMBER: number;
62+
}
63+
class EnvModule extends EnvModuleMixin<Env>(Env) {}
64+
await TestContainer.create({
65+
imports: [EnvModule],
66+
});
67+
expect(EnvModule.get('SOME_DATE')).toEqual(new Date('2024-08-13T17:34:00Z'));
68+
expect(EnvModule.get('SOME_NUMBER')).toBe(123);
69+
});
70+
});
71+
72+
describe('decorators', () => {
73+
it('should work with decorators', () => {
74+
vi.stubEnv('HELLO_ROUTE', '/hello');
75+
});
76+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Logger, Module } from '@nestjs/common';
2+
import { plainToInstance } from 'class-transformer';
3+
import { validateSync } from 'class-validator';
4+
5+
export function EnvModuleMixin<TEnv extends object>(env: new (...args: unknown[]) => TEnv) {
6+
@Module({})
7+
class EnvModule {
8+
static get env(): TEnv {
9+
return plainToInstance(env, process.env, {
10+
enableImplicitConversion: true,
11+
});
12+
}
13+
14+
constructor() {
15+
EnvModule.validate();
16+
}
17+
18+
/**
19+
* Validates the currently stored instance of the environment class.
20+
*/
21+
static validate(): void {
22+
const errors = validateSync(this.env, {
23+
skipMissingProperties: false,
24+
});
25+
26+
if (errors.length > 0) {
27+
Logger.error(
28+
'Failed to validate environment variables. Did you forget to provide some that are required?',
29+
this.constructor.name,
30+
);
31+
throw new Error(errors.toString());
32+
}
33+
}
34+
35+
/**
36+
* Returns the value of the environment variable with the given key.
37+
* @param key The key of the environment variable.
38+
* @returns The value of the environment variable.
39+
*/
40+
static get<TKey extends keyof TEnv>(key: TKey): TEnv[TKey] {
41+
return EnvModule.env[key];
42+
}
43+
}
44+
45+
return EnvModule;
46+
}

packages/nest-utils/src/env/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './env.module-mixin';

packages/nest-utils/src/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export * from './nest-utils.module';
1+
export * from './env';
22
export * from './nest-utils.service';
33

44
// As long as NestJS does not support TypeScript's newer resolution algorithms like 'Node16',

0 commit comments

Comments
 (0)