diff --git a/lib/config.module.ts b/lib/config.module.ts index 757d21a8..55a6ac89 100644 --- a/lib/config.module.ts +++ b/lib/config.module.ts @@ -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 | undefined = undefined; - let config = options.ignoreEnvFile ? {} : this.loadEnvFile(options); + let config = options.ignoreEnvFile + ? {} + : this.loadEnvFile(envFilePaths, options); if (!options.ignoreEnvVars) { config = { @@ -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], @@ -173,12 +180,9 @@ export class ConfigModule { } private static loadEnvFile( + envFilePaths: string[], options: ConfigModuleOptions, ): Record { - const envFilePaths = Array.isArray(options.envFilePath) - ? options.envFilePath - : [options.envFilePath || resolve(process.cwd(), '.env')]; - let config: ReturnType = {}; for (const envFilePath of envFilePaths) { if (fs.existsSync(envFilePath)) { diff --git a/lib/config.service.ts b/lib/config.service.ts index 4f33f008..4f67c3c9 100644 --- a/lib/config.service.ts +++ b/lib/config.service.ts @@ -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'; @@ -20,7 +22,6 @@ type ValidatedResult< T, > = WasValidated extends true ? T : T | undefined; - /** * @publicApi */ @@ -35,7 +36,6 @@ export interface ConfigGetOptions { type KeyOf = keyof T extends never ? string : keyof T; - /** * @publicApi */ @@ -54,6 +54,7 @@ export class ConfigService< private readonly cache: Partial = {} as any; private _isCacheEnabled = false; + private envFilePaths: string[] = []; constructor( @Optional() @@ -201,6 +202,30 @@ export class ConfigService< return value as Exclude; } + /** + * Sets a configuration value based on property path. + * @param propertyPath + * @param value + */ + set(propertyPath: KeyOf, 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( propertyPath: KeyOf, @@ -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 = {}; + 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); + } + } + } } diff --git a/tests/e2e/update-env.spec.ts b/tests/e2e/update-env.spec.ts new file mode 100644 index 00000000..e6d5cde3 --- /dev/null +++ b/tests/e2e/update-env.spec.ts @@ -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('support@myapp.test'); + expect(updatedUrl).toEqual('yourapp.test'); + expect(updatedEmail).toEqual('support@yourapp.test'); + }); + + 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('support@yourapp.test'); + }); + + afterEach(async () => { + process.env = originalEnv; + await app.close(); + }); +});