Skip to content

Commit

Permalink
Merge branch 'feature/121' of https://github.com/Sikora00/config into…
Browse files Browse the repository at this point in the history
… Sikora00-feature/121
  • Loading branch information
kamilmysliwiec committed Nov 17, 2020
2 parents 431b3c4 + 865dd43 commit d8c4372
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 8 deletions.
7 changes: 6 additions & 1 deletion lib/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ export class ConfigModule {
const configProviderTokens = providers.map(item => item.provide);
const configServiceProvider = {
provide: ConfigService,
useFactory: (configService: ConfigService) => configService,
useFactory: (configService: ConfigService) => {
if (options.cache) {
configService.cachingEnabled = true;
}
return configService;
},
inject: [CONFIGURATION_SERVICE_TOKEN, ...configProviderTokens],
};
providers.push(configServiceProvider);
Expand Down
77 changes: 70 additions & 7 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 get from 'lodash.get';
import set from 'lodash.set';
import has from 'lodash.has';
import { isUndefined } from 'util';
import {
CONFIGURATION_TOKEN,
Expand All @@ -9,6 +11,17 @@ import { NoInferType } from './types';

@Injectable()
export class ConfigService<K = Record<string, any>> {
get cachingEnabled(): boolean {
return this._cachingEnabled;
}

set cachingEnabled(value: boolean) {
this._cachingEnabled = value;
}

private readonly cache: K = {} as any;
private _cachingEnabled = false;

constructor(
@Optional()
@Inject(CONFIGURATION_TOKEN)
Expand Down Expand Up @@ -39,18 +52,68 @@ export class ConfigService<K = Record<string, any>> {
* @param defaultValue
*/
get<T = any>(propertyPath: keyof K, defaultValue?: T): T | undefined {
if (
this.cachingEnabled &&
has(this.cache as Record<any, any>, propertyPath)
) {
const cachedValue = this.getFromCache(propertyPath, defaultValue);
/** if cached value was once set as undefined always return default value */
return !isUndefined(cachedValue) ? cachedValue : defaultValue;
}

const validatedEnvValue = this.getFromValidatedEnv(propertyPath);
if (!isUndefined(validatedEnvValue)) {
return validatedEnvValue;
}

const processEnvValue = this.getFromProcessEnv(propertyPath);
if (!isUndefined(processEnvValue)) {
return processEnvValue;
}

const internalValue = this.getFromInternalConfig(propertyPath);
if (!isUndefined(internalValue)) {
return internalValue;
}

return defaultValue;
}

private getFromCache<T = any>(
propertyPath: keyof K,
defaultValue?: T,
): T | undefined {
const cachedValue = get(this.cache, propertyPath);
return isUndefined(cachedValue)
? defaultValue
: ((cachedValue as unknown) as T);
}

private getFromValidatedEnv<T = any>(propertyPath: keyof K): T | undefined {
const validatedEnvValue = get(
this.internalConfig[VALIDATED_ENV_PROPNAME],
propertyPath,
);
if (!isUndefined(validatedEnvValue)) {
return (validatedEnvValue as unknown) as T;
}
this.setInCache(propertyPath, validatedEnvValue);

return (validatedEnvValue as unknown) as T;
}

private getFromProcessEnv<T = any>(propertyPath: keyof K): T | undefined {
const processValue = get(process.env, propertyPath);
if (!isUndefined(processValue)) {
return (processValue as unknown) as T;
}
this.setInCache(propertyPath, processValue);

return (processValue as unknown) as T;
}

private getFromInternalConfig<T = any>(propertyPath: keyof K): T | undefined {
const internalValue = get(this.internalConfig, propertyPath);
return isUndefined(internalValue) ? defaultValue : internalValue;
this.setInCache(propertyPath, internalValue);

return internalValue;
}

private setInCache(propertyPath: keyof K, value: any): void {
set(this.cache as Record<any, any>, propertyPath, value);
}
}
8 changes: 8 additions & 0 deletions lib/interfaces/config-module-options.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { ConfigFactory } from './config-factory.interface';

export interface ConfigModuleOptions {
/**
* If "true" values from process.env are read
* only once, the cache will be used after that.
* This will improve the performance.
* See: https://github.com/nodejs/node/issues/3104
*/
cache?: boolean;

/**
* If "true", registers `ConfigModule` as a global module.
* See: https://docs.nestjs.com/modules#global-modules
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"lodash.get": "4.4.2",
"lodash.has": "4.5.2",
"lodash.set": "4.3.2",
"uuid": "8.3.1"
},
Expand All @@ -34,6 +35,7 @@
"@types/hapi__joi": "17.1.6",
"@types/jest": "26.0.15",
"@types/lodash.get": "4.4.6",
"@types/lodash.has": "4.5.2",
"@types/lodash.set": "4.3.6",
"@types/node": "7.10.8",
"@types/uuid": "8.3.0",
Expand Down
62 changes: 62 additions & 0 deletions tests/e2e/cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { ConfigService } from '../../lib';
import { AppModule } from '../src/app.module';

describe('Cache', () => {
let app: INestApplication;
let envBackup: NodeJS.ProcessEnv;
beforeAll(() => {
envBackup = process.env;
});
describe('without cache', () => {
beforeAll(async () => {
process.env['NAME'] = 'TEST';
const moduleRef = await Test.createTestingModule({
imports: [AppModule.withEnvVars()],
}).compile();

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

it(`should return loaded env variables from vars`, () => {
const configService = app.get(ConfigService);
expect(configService.get('NAME')).toEqual('TEST');
});

it(`should return new vars`, () => {
process.env['NAME'] = 'CHANGED';
const configService = app.get(ConfigService);
expect(configService.get('NAME')).toEqual('CHANGED');
});
});

describe('with cache', () => {
beforeAll(async () => {
process.env['NAME'] = 'TEST';
const moduleRef = await Test.createTestingModule({
imports: [AppModule.withCache()],
}).compile();

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

it(`should return loaded env variables from vars`, () => {
const configService = app.get(ConfigService);
expect(configService.get('NAME')).toEqual('TEST');
});

it(`should return cached vars`, () => {
process.env['NAME'] = 'CHANGED';
const configService = app.get(ConfigService);
expect(configService.get('NAME')).toEqual('TEST');
});
});

afterEach(async () => {
process.env = envBackup;
await app.close();
});
});
12 changes: 12 additions & 0 deletions tests/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ export class AppModule {
private readonly dbConfig: ConfigType<typeof databaseConfig>,
) {}

static withCache(): DynamicModule {
return {
module: AppModule,
imports: [
ConfigModule.forRoot({
cache: true,
envFilePath: join(__dirname, '.env'),
}),
],
};
}

static withEnvVars(): DynamicModule {
return {
module: AppModule,
Expand Down

0 comments on commit d8c4372

Please sign in to comment.