Skip to content

Commit

Permalink
feat(): add support for custom parsers
Browse files Browse the repository at this point in the history
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 nestjs#1444
  • Loading branch information
V3RON committed Mar 4, 2024
1 parent 163cdf2 commit bf246e1
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 15 deletions.
18 changes: 9 additions & 9 deletions lib/config.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { DynamicModule, Module } from '@nestjs/common';
import { FactoryProvider } from '@nestjs/common/interfaces';
import { isObject } from '@nestjs/common/utils/shared.utils';
import * as dotenv from 'dotenv';
import { DotenvExpandOptions, expand } from 'dotenv-expand';
import * as fs from 'fs';
import { resolve } from 'path';
Expand All @@ -15,10 +14,11 @@ import {
} from './config.constants';
import { ConfigService } from './config.service';
import { ConfigFactory, ConfigModuleOptions } from './interfaces';
import { ConfigFactoryKeyHost } from './utils';
import { ConfigFactoryKeyHost, getDefaultParser } from './utils';
import { createConfigProvider } from './utils/create-config-factory.util';
import { getRegistrationToken } from './utils/get-registration-token.util';
import { mergeConfigObject } from './utils/merge-configs.util';
import { Parser } from './types';

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

let validatedEnvConfig: Record<string, any> | undefined = undefined;
let config = options.ignoreEnvFile
? {}
: this.loadEnvFile(envFilePaths, options);
: this.loadEnvFile(envFilePaths, parser, options);

if (!options.ignoreEnvVars) {
config = {
Expand Down Expand Up @@ -102,6 +103,7 @@ export class ConfigModule {
(configService as any).isCacheEnabled = true;
}
configService.setEnvFilePaths(envFilePaths);
configService.setParser(parser);
return configService;
},
inject: [CONFIGURATION_SERVICE_TOKEN, ...configProviderTokens],
Expand Down Expand Up @@ -181,15 +183,13 @@ export class ConfigModule {

private static loadEnvFile(
envFilePaths: string[],
parser: Parser,
options: ConfigModuleOptions,
): Record<string, any> {
let config: ReturnType<typeof dotenv.parse> = {};
let config: Record<string, any> = {};
for (const envFilePath of envFilePaths) {
if (fs.existsSync(envFilePath)) {
config = Object.assign(
dotenv.parse(fs.readFileSync(envFilePath)),
config,
);
config = Object.assign(parser(fs.readFileSync(envFilePath)), config);
if (options.expandVariables) {
const expandOptions: DotenvExpandOptions =
typeof options.expandVariables === 'object'
Expand Down
17 changes: 13 additions & 4 deletions lib/config.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Inject, Injectable, Optional } from '@nestjs/common';
import { isUndefined } from '@nestjs/common/utils/shared.utils';
import * as dotenv from 'dotenv';
import fs from 'fs';
import get from 'lodash/get';
import has from 'lodash/has';
Expand All @@ -11,7 +10,8 @@ import {
VALIDATED_ENV_PROPNAME,
} from './config.constants';
import { ConfigChangeEvent } from './interfaces/config-change-event.interface';
import { NoInferType, Path, PathValue } from './types';
import { NoInferType, Parser, Path, PathValue } from './types';
import { getDefaultParser } from './utils';

/**
* `ValidatedResult<WasValidated, T>
Expand Down Expand Up @@ -58,6 +58,7 @@ export class ConfigService<
private readonly _changes$ = new Subject<ConfigChangeEvent>();
private _isCacheEnabled = false;
private envFilePaths: string[] = [];
private parser: Parser = getDefaultParser();

constructor(
@Optional()
Expand Down Expand Up @@ -245,6 +246,14 @@ export class ConfigService<
this.envFilePaths = paths;
}

/**
* Sets parser from `config.module.ts`.
* @param parser
*/
setParser(parser: Parser): void {
this.parser = parser;
}

private getFromCache<T = any>(
propertyPath: KeyOf<K>,
defaultValue?: T,
Expand Down Expand Up @@ -301,11 +310,11 @@ export class ConfigService<
}

private updateInterpolatedEnv(propertyPath: string, value: string) {
let config: ReturnType<typeof dotenv.parse> = {};
let config: Record<string, any> = {};
for (const envFilePath of this.envFilePaths) {
if (fs.existsSync(envFilePath)) {
config = Object.assign(
dotenv.parse(fs.readFileSync(envFilePath)),
this.parser(fs.readFileSync(envFilePath)),
config,
);
}
Expand Down
8 changes: 7 additions & 1 deletion lib/interfaces/config-module-options.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ConfigFactory } from './config-factory.interface';
import { DotenvExpandOptions } from 'dotenv-expand'
import { DotenvExpandOptions } from 'dotenv-expand';
import { Parser } from '../types';

/**
* @publicApi
Expand Down Expand Up @@ -66,4 +67,9 @@ export interface ConfigModuleOptions {
* this property is set to true.
*/
expandVariables?: boolean | DotenvExpandOptions;

/**
* A function used to parse a buffer into a configuration object.
*/
parser?: Parser;
}
1 change: 1 addition & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './config-object.type';
export * from './config.type';
export * from './no-infer.type';
export * from './path-value.type';
export * from './parser.type';
1 change: 1 addition & 0 deletions lib/types/parser.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Parser = (buffer: Buffer) => Record<string, any>;
4 changes: 4 additions & 0 deletions lib/utils/get-default-parser.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import * as dotenv from 'dotenv';
import { Parser } from '../types';

export const getDefaultParser = (): Parser => dotenv.parse;
1 change: 1 addition & 0 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './register-as.util';
export * from './get-config-token.util';
export * from './get-default-parser.util';
1 change: 1 addition & 0 deletions tests/e2e/.custom-config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyAibG9yZW0iOiAiaXBzdW0iIH0=
57 changes: 57 additions & 0 deletions tests/e2e/custom-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { ConfigModule } from '../../lib';
import { AppModule } from '../src/app.module';
import { join } from 'path';

describe('Custom parser', () => {
let app: INestApplication;

it(`should use dotenv parser by default`, async () => {
const module = await Test.createTestingModule({
imports: [AppModule.withEnvVars()],
}).compile();

app = module.createNestApplication();
await app.init();
await ConfigModule.envVariablesLoaded;

const envVars = app.get(AppModule).getEnvVariables();
expect(envVars.PORT).toEqual('4000');
});

it(`should use custom parser when provided`, async () => {
const module = await Test.createTestingModule({
imports: [
{
module: AppModule,
imports: [
ConfigModule.forRoot({
envFilePath: join(
process.cwd(),
'tests',
'e2e',
'.custom-config',
),
parser: buffer =>
JSON.parse(
Buffer.from(buffer.toString('utf-8'), 'base64').toString(
'utf-8',
),
),
}),
],
},
],
}).compile();

app = module.createNestApplication();
await app.init();
const envVars = app.get(AppModule).getEnvVariables();
expect(envVars.lorem).toEqual('ipsum');
});

afterEach(async () => {
await app.close();
});
});
2 changes: 1 addition & 1 deletion tests/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class AppModule {
imports: [
ConfigModule.forRoot({
envFilePath: join(__dirname, '.env.expanded'),
expandVariables: { ignoreProcessEnv: true }
expandVariables: { ignoreProcessEnv: true },
}),
],
};
Expand Down

0 comments on commit bf246e1

Please sign in to comment.