Skip to content

Commit

Permalink
Merge pull request #1575 from SeiwonPark/feature/setting-env
Browse files Browse the repository at this point in the history
feat: set or update environment variables dynamically (#1476)
  • Loading branch information
kamilmysliwiec authored Feb 7, 2024
2 parents 88b1d88 + 761ef89 commit 78cd4f2
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 7 deletions.
14 changes: 9 additions & 5 deletions lib/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@ export class ConfigModule {
* @param options
*/
static forRoot(options: ConfigModuleOptions = {}): DynamicModule {
const envFilePaths = Array.isArray(options.envFilePath)
? options.envFilePath
: [options.envFilePath || resolve(process.cwd(), '.env')];

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

if (!options.ignoreEnvVars) {
config = {
Expand Down Expand Up @@ -95,6 +101,7 @@ export class ConfigModule {
if (options.cache) {
(configService as any).isCacheEnabled = true;
}
configService.setEnvFilePaths(envFilePaths);
return configService;
},
inject: [CONFIGURATION_SERVICE_TOKEN, ...configProviderTokens],
Expand Down Expand Up @@ -173,12 +180,9 @@ export class ConfigModule {
}

private static loadEnvFile(
envFilePaths: string[],
options: ConfigModuleOptions,
): Record<string, any> {
const envFilePaths = Array.isArray(options.envFilePath)
? options.envFilePath
: [options.envFilePath || resolve(process.cwd(), '.env')];

let config: ReturnType<typeof dotenv.parse> = {};
for (const envFilePath of envFilePaths) {
if (fs.existsSync(envFilePath)) {
Expand Down
48 changes: 46 additions & 2 deletions lib/config.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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';
import set from 'lodash/set';
Expand All @@ -20,7 +22,6 @@ type ValidatedResult<
T,
> = WasValidated extends true ? T : T | undefined;


/**
* @publicApi
*/
Expand All @@ -35,7 +36,6 @@ export interface ConfigGetOptions {

type KeyOf<T> = keyof T extends never ? string : keyof T;


/**
* @publicApi
*/
Expand All @@ -54,6 +54,7 @@ export class ConfigService<

private readonly cache: Partial<K> = {} as any;
private _isCacheEnabled = false;
private envFilePaths: string[] = [];

constructor(
@Optional()
Expand Down Expand Up @@ -201,6 +202,30 @@ export class ConfigService<

return value as Exclude<T, undefined>;
}
/**
* Sets a configuration value based on property path.
* @param propertyPath
* @param value
*/
set<T = any>(propertyPath: KeyOf<K>, value: T): void {
set(this.internalConfig, propertyPath, value);

if (typeof propertyPath === 'string') {
process.env[propertyPath] = String(value);
this.updateInterpolatedEnv(propertyPath, String(value));
}

if (this.isCacheEnabled) {
this.setInCacheIfDefined(propertyPath, value);
}
}
/**
* Sets env file paths from `config.module.ts` to parse.
* @param paths
*/
setEnvFilePaths(paths: string[]): void {
this.envFilePaths = paths;
}

private getFromCache<T = any>(
propertyPath: KeyOf<K>,
Expand Down Expand Up @@ -256,4 +281,23 @@ export class ConfigService<
): options is ConfigGetOptions {
return options && options?.infer && Object.keys(options).length === 1;
}

private updateInterpolatedEnv(propertyPath: string, value: string) {
let config: ReturnType<typeof dotenv.parse> = {};
for (const envFilePath of this.envFilePaths) {
if (fs.existsSync(envFilePath)) {
config = Object.assign(
dotenv.parse(fs.readFileSync(envFilePath)),
config,
);
}
}

const regex = new RegExp(`\\$\\{?${propertyPath}\\}?`, 'g');
for (const [k, v] of Object.entries(config)) {
if (regex.test(v)) {
process.env[k] = v.replace(regex, value);
}
}
}
}
75 changes: 75 additions & 0 deletions tests/e2e/update-env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '../../lib';
import { AppModule } from '../src/app.module';
import { ConfigService } from '../../lib';

describe('Setting environment variables', () => {
let app: INestApplication;
let module: TestingModule;
let originalEnv: NodeJS.ProcessEnv;

beforeEach(async () => {
originalEnv = { ...process.env };

module = await Test.createTestingModule({
imports: [AppModule.withExpandedEnvVars()],
}).compile();

app = module.createNestApplication();
await app.init();
});

it('should return updated value after set', async () => {
const prevUrl = module.get(ConfigService).get('URL');

module.get(ConfigService).set('URL', 'yourapp.test');

const updatedUrl = module.get(ConfigService).get('URL');

expect(prevUrl).toEqual('myapp.test');
expect(updatedUrl).toEqual('yourapp.test');
});

it('should return value after set', async () => {
const undefinedEnv = module.get(ConfigService).get('UNDEFINED_ENV');

module.get(ConfigService).set('UNDEFINED_ENV', 'defined');

const definedEnv = module.get(ConfigService).get('UNDEFINED_ENV');

expect(undefinedEnv).toEqual(undefined);
expect(definedEnv).toEqual('defined');
});

it('should return updated value with interpolation after set', async () => {
const prevUrl = module.get(ConfigService).get('URL');
const prevEmail = module.get(ConfigService).get('EMAIL');

module.get(ConfigService).set('URL', 'yourapp.test');

const updatedUrl = module.get(ConfigService).get('URL');
const updatedEmail = module.get(ConfigService).get('EMAIL');

expect(prevUrl).toEqual('myapp.test');
expect(prevEmail).toEqual('[email protected]');
expect(updatedUrl).toEqual('yourapp.test');
expect(updatedEmail).toEqual('[email protected]');
});

it(`should return updated process.env property after set`, async () => {
await ConfigModule.envVariablesLoaded;

module.get(ConfigService).set('URL', 'yourapp.test');

const envVars = app.get(AppModule).getEnvVariables();

expect(envVars.URL).toEqual('yourapp.test');
expect(envVars.EMAIL).toEqual('[email protected]');
});

afterEach(async () => {
process.env = originalEnv;
await app.close();
});
});

0 comments on commit 78cd4f2

Please sign in to comment.