Skip to content

Commit e6aacd8

Browse files
committed
feat(): add support for custom parsers
This commit introduces a new configuration property named parser in ConfigModule, enabling the injection of custom parser functions. If not specified, the default implementation, backed by dotenv, is utilized. closes #1444
1 parent 68ccccf commit e6aacd8

File tree

9 files changed

+93
-13
lines changed

9 files changed

+93
-13
lines changed

lib/config.module.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { DynamicModule, Module } from '@nestjs/common';
22
import { FactoryProvider } from '@nestjs/common/interfaces';
33
import { isObject } from '@nestjs/common/utils/shared.utils';
4-
import * as dotenv from 'dotenv';
54
import { DotenvExpandOptions, expand } from 'dotenv-expand';
65
import * as fs from 'fs';
76
import { resolve } from 'path';
@@ -15,10 +14,11 @@ import {
1514
} from './config.constants';
1615
import { ConfigService } from './config.service';
1716
import { ConfigFactory, ConfigModuleOptions } from './interfaces';
18-
import { ConfigFactoryKeyHost } from './utils';
17+
import { ConfigFactoryKeyHost, getDefaultParser } from './utils';
1918
import { createConfigProvider } from './utils/create-config-factory.util';
2019
import { getRegistrationToken } from './utils/get-registration-token.util';
2120
import { mergeConfigObject } from './utils/merge-configs.util';
21+
import { Parser } from './types';
2222

2323
/**
2424
* @publicApi
@@ -35,7 +35,7 @@ import { mergeConfigObject } from './utils/merge-configs.util';
3535
})
3636
export class ConfigModule {
3737
/**
38-
* This promise resolves when "dotenv" completes loading environment variables.
38+
* This promise resolves when parser completes loading environment variables.
3939
* When "ignoreEnvFile" is set to true, then it will resolve immediately after the
4040
* "ConfigModule#forRoot" method is called.
4141
*/
@@ -59,11 +59,12 @@ export class ConfigModule {
5959
const envFilePaths = Array.isArray(options.envFilePath)
6060
? options.envFilePath
6161
: [options.envFilePath || resolve(process.cwd(), '.env')];
62+
const parser = options.parser ?? getDefaultParser();
6263

6364
let validatedEnvConfig: Record<string, any> | undefined = undefined;
6465
let config = options.ignoreEnvFile
6566
? {}
66-
: this.loadEnvFile(envFilePaths, options);
67+
: this.loadEnvFile(envFilePaths, parser, options);
6768

6869
if (!options.ignoreEnvVars && options.validatePredefined !== false) {
6970
config = {
@@ -111,6 +112,7 @@ export class ConfigModule {
111112
}
112113

113114
configService.setEnvFilePaths(envFilePaths);
115+
configService.setParser(parser);
114116
return configService;
115117
},
116118
inject: [CONFIGURATION_SERVICE_TOKEN, ...configProviderTokens],
@@ -190,15 +192,13 @@ export class ConfigModule {
190192

191193
private static loadEnvFile(
192194
envFilePaths: string[],
195+
parser: Parser,
193196
options: ConfigModuleOptions,
194197
): Record<string, any> {
195-
let config: ReturnType<typeof dotenv.parse> = {};
198+
let config: Record<string, any> = {};
196199
for (const envFilePath of envFilePaths) {
197200
if (fs.existsSync(envFilePath)) {
198-
config = Object.assign(
199-
dotenv.parse(fs.readFileSync(envFilePath)),
200-
config,
201-
);
201+
config = Object.assign(parser(fs.readFileSync(envFilePath)), config);
202202
if (options.expandVariables) {
203203
const expandOptions: DotenvExpandOptions =
204204
typeof options.expandVariables === 'object'

lib/config.service.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Inject, Injectable, Optional } from '@nestjs/common';
22
import { isUndefined } from '@nestjs/common/utils/shared.utils';
3-
import * as dotenv from 'dotenv';
43
import fs from 'fs';
54
import get from 'lodash/get';
65
import has from 'lodash/has';
@@ -11,7 +10,8 @@ import {
1110
VALIDATED_ENV_PROPNAME,
1211
} from './config.constants';
1312
import { ConfigChangeEvent } from './interfaces/config-change-event.interface';
14-
import { NoInferType, Path, PathValue } from './types';
13+
import { NoInferType, Parser, Path, PathValue } from './types';
14+
import { getDefaultParser } from './utils';
1515

1616
/**
1717
* `ValidatedResult<WasValidated, T>
@@ -67,6 +67,7 @@ export class ConfigService<
6767
private _skipProcessEnv = false;
6868
private _isCacheEnabled = false;
6969
private envFilePaths: string[] = [];
70+
private parser: Parser = getDefaultParser();
7071

7172
constructor(
7273
@Optional()
@@ -259,6 +260,14 @@ export class ConfigService<
259260
this.envFilePaths = paths;
260261
}
261262

263+
/**
264+
* Sets parser from `config.module.ts`.
265+
* @param parser
266+
*/
267+
setParser(parser: Parser): void {
268+
this.parser = parser;
269+
}
270+
262271
private getFromCache<T = any>(
263272
propertyPath: KeyOf<K>,
264273
defaultValue?: T,
@@ -315,11 +324,11 @@ export class ConfigService<
315324
}
316325

317326
private updateInterpolatedEnv(propertyPath: string, value: string): void {
318-
let config: ReturnType<typeof dotenv.parse> = {};
327+
let config: Record<string, any> = {};
319328
for (const envFilePath of this.envFilePaths) {
320329
if (fs.existsSync(envFilePath)) {
321330
config = Object.assign(
322-
dotenv.parse(fs.readFileSync(envFilePath)),
331+
this.parser(fs.readFileSync(envFilePath)),
323332
config,
324333
);
325334
}

lib/interfaces/config-module-options.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DotenvExpandOptions } from 'dotenv-expand';
22
import { ConfigFactory } from './config-factory.interface';
3+
import { Parser } from '../types';
34

45
/**
56
* @publicApi
@@ -84,4 +85,9 @@ export interface ConfigModuleOptions<
8485
* this property is set to true.
8586
*/
8687
expandVariables?: boolean | DotenvExpandOptions;
88+
89+
/**
90+
* A function used to parse a buffer into a configuration object.
91+
*/
92+
parser?: Parser;
8793
}

lib/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './config-object.type';
22
export * from './config.type';
33
export * from './no-infer.type';
44
export * from './path-value.type';
5+
export * from './parser.type';

lib/types/parser.type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type Parser = (buffer: Buffer) => Record<string, any>;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import * as dotenv from 'dotenv';
2+
import { Parser } from '../types';
3+
4+
export const getDefaultParser = (): Parser => dotenv.parse;

lib/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './register-as.util';
22
export * from './get-config-token.util';
3+
export * from './get-default-parser.util';

tests/e2e/.custom-config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
eyAibG9yZW0iOiAiaXBzdW0iIH0=

tests/e2e/custom-parser.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { INestApplication } from '@nestjs/common';
2+
import { Test } from '@nestjs/testing';
3+
import { ConfigModule } from '../../lib';
4+
import { AppModule } from '../src/app.module';
5+
import { join } from 'path';
6+
7+
describe('Custom parser', () => {
8+
let app: INestApplication;
9+
10+
it(`should use dotenv parser by default`, async () => {
11+
const module = await Test.createTestingModule({
12+
imports: [AppModule.withEnvVars()],
13+
}).compile();
14+
15+
app = module.createNestApplication();
16+
await app.init();
17+
await ConfigModule.envVariablesLoaded;
18+
19+
const envVars = app.get(AppModule).getEnvVariables();
20+
expect(envVars.PORT).toEqual('4000');
21+
});
22+
23+
it(`should use custom parser when provided`, async () => {
24+
const module = await Test.createTestingModule({
25+
imports: [
26+
{
27+
module: AppModule,
28+
imports: [
29+
ConfigModule.forRoot({
30+
envFilePath: join(
31+
process.cwd(),
32+
'tests',
33+
'e2e',
34+
'.custom-config',
35+
),
36+
parser: buffer =>
37+
JSON.parse(
38+
Buffer.from(buffer.toString('utf-8'), 'base64').toString(
39+
'utf-8',
40+
),
41+
),
42+
}),
43+
],
44+
},
45+
],
46+
}).compile();
47+
48+
app = module.createNestApplication();
49+
await app.init();
50+
const envVars = app.get(AppModule).getEnvVariables();
51+
expect(envVars.lorem).toEqual('ipsum');
52+
});
53+
54+
afterEach(async () => {
55+
await app.close();
56+
});
57+
});

0 commit comments

Comments
 (0)