From 9b72ff5cd9272093d3ca54c43b1c396fa3ee58b3 Mon Sep 17 00:00:00 2001 From: Velmisov Date: Wed, 15 Jul 2020 09:38:05 +0300 Subject: [PATCH 1/4] feat: add full-value config storage --- src/lib/facade.spec.ts | 152 +++++++++++++++++++++++++++------- src/lib/facade.ts | 25 ++++-- src/lib/factory.ts | 8 +- src/lib/global-module.spec.ts | 3 + src/lib/global-module.ts | 8 ++ src/lib/parser.ts | 12 +-- src/lib/storage.spec.ts | 59 +++++++++++++ src/lib/storage.ts | 50 +++++++++++ src/lib/types.ts | 2 +- src/module.spec.ts | 3 + 10 files changed, 274 insertions(+), 48 deletions(-) create mode 100644 src/lib/storage.spec.ts create mode 100644 src/lib/storage.ts diff --git a/src/lib/facade.spec.ts b/src/lib/facade.spec.ts index d8d6b2d..b276893 100644 --- a/src/lib/facade.spec.ts +++ b/src/lib/facade.spec.ts @@ -1,15 +1,50 @@ import { LoggerService } from '@nestjs/common'; import { ConfigFacade } from './facade'; -import { ConfigExtractor } from './extractor'; import { Config, Env } from '../decorator'; import { Integer } from '../types'; +import { ConfigStorage } from './storage'; +import { ConfigExtractor } from './extractor'; +import { ConfigParser } from './parser'; +import { ConfigFactory } from './factory'; +import { ConfigValidator } from './validator'; +import { ConfigLogger } from './logger'; describe('ConfigFacade', () => { - const configExtractor = { extract: jest.fn() }; - const configParser = { parse: jest.fn() }; - const configFactory = { createConfig: jest.fn() }; - const configValidator = { validate: jest.fn() }; - const logger = { error: jest.fn() }; + let configStorage: ConfigStorage; + let configExtractor: jest.Mocked>; + let configParser: jest.Mocked; + let configFactory: jest.Mocked; + let configValidator: jest.Mocked; + let logger: jest.Mocked>; + let configStorageSpies: { + envsGetter: jest.SpyInstance; + envsSetter: jest.SpyInstance; + rawConfigGetter: jest.SpyInstance; + rawConfigSetter: jest.SpyInstance; + parsedGetter: jest.SpyInstance; + parsedSetter: jest.SpyInstance; + addConfig: jest.SpyInstance; + getConfig: jest.SpyInstance; + }; + + beforeEach(() => { + configStorage = new ConfigStorage(); + configExtractor = { extract: jest.fn() }; + configParser = { parse: jest.fn() }; + configFactory = { createConfig: jest.fn() }; + configValidator = { validate: jest.fn() }; + logger = { error: jest.fn() }; + configStorageSpies = { + envsGetter: jest.spyOn(configStorage, 'envs', 'get'), + envsSetter: jest.spyOn(configStorage, 'envs', 'set'), + rawConfigGetter: jest.spyOn(configStorage, 'RAW_CONFIG', 'get'), + rawConfigSetter: jest.spyOn(configStorage, 'RAW_CONFIG', 'set'), + parsedGetter: jest.spyOn(configStorage, 'parsed', 'get'), + parsedSetter: jest.spyOn(configStorage, 'parsed', 'set'), + addConfig: jest.spyOn(configStorage, 'addConfig'), + getConfig: jest.spyOn(configStorage, 'getConfig'), + }; + }); afterEach(() => { jest.clearAllMocks(); @@ -25,18 +60,18 @@ describe('ConfigFacade', () => { [variableName] = variableValue; } - const configStorage = {}; const expectedConfig = { [variableName]: variableValue, }; - configExtractor.extract.mockReturnValueOnce(configStorage); - configParser.parse.mockReturnValueOnce(configStorage); + configExtractor.extract.mockReturnValueOnce({}); + configParser.parse.mockReturnValueOnce({}); configFactory.createConfig.mockReturnValueOnce(new TestConfig()); const configSource = {}; const configFacade = new ConfigFacade( + configStorage, (configExtractor as unknown) as ConfigExtractor, configParser, configFactory, @@ -47,14 +82,16 @@ describe('ConfigFacade', () => { expect(configFacade.createConfig(TestConfig)).toMatchObject(expectedConfig); + expect(configStorageSpies.rawConfigSetter).toHaveBeenCalledWith({}); expect(configExtractor.extract).toHaveBeenCalledWith(configSource); - expect(configParser.parse).toHaveBeenCalledWith(configStorage); - expect(configFactory.createConfig).toHaveBeenCalledWith( - configStorage, - TestConfig, - ); + expect(configStorageSpies.envsSetter).toHaveBeenCalledWith({}); + expect(configParser.parse).toHaveBeenCalledWith({}); + expect(configStorageSpies.parsedSetter).toHaveBeenCalledWith({}); + expect(configStorageSpies.getConfig).toHaveBeenCalledWith(TestConfig); + expect(configFactory.createConfig).toHaveBeenCalledWith({}, TestConfig); expect(configValidator.validate).toHaveBeenCalledWith(expectedConfig); expect(logger.error).not.toHaveBeenCalled(); + expect(configStorageSpies.addConfig).toHaveBeenCalledWith(expectedConfig); }); it('should create config and override default values', () => { @@ -70,20 +107,24 @@ describe('ConfigFacade', () => { [variableName] = variableValue; } - const configStorage = { + const envs = { [`${envModuleName}__${envVariableName}`]: newValue, }; + const parsed = { + [envModuleName]: { [envVariableName]: newValue }, + }; const expectedConfig = { [variableName]: newValue, }; - configExtractor.extract.mockReturnValueOnce(configStorage); - configParser.parse.mockReturnValueOnce(configStorage); + configExtractor.extract.mockReturnValueOnce(envs); + configParser.parse.mockReturnValueOnce(parsed); configFactory.createConfig.mockReturnValueOnce(expectedConfig); const configSource = { fromFile: '.env.test' }; const configFacade = new ConfigFacade( + configStorage, (configExtractor as unknown) as ConfigExtractor, configParser, configFactory, @@ -94,14 +135,16 @@ describe('ConfigFacade', () => { expect(configFacade.createConfig(TestConfig)).toMatchObject(expectedConfig); + expect(configStorageSpies.rawConfigSetter).toHaveBeenCalledWith({}); expect(configExtractor.extract).toHaveBeenCalledWith(configSource); - expect(configParser.parse).toHaveBeenCalledWith(configStorage); - expect(configFactory.createConfig).toHaveBeenCalledWith( - configStorage, - TestConfig, - ); + expect(configStorageSpies.envsSetter).toHaveBeenCalledWith(envs); + expect(configParser.parse).toHaveBeenCalledWith(envs); + expect(configStorageSpies.parsedSetter).toHaveBeenCalledWith(parsed); + expect(configStorageSpies.getConfig).toHaveBeenCalledWith(TestConfig); + expect(configFactory.createConfig).toHaveBeenCalledWith(parsed, TestConfig); expect(configValidator.validate).toHaveBeenCalledWith(expectedConfig); expect(logger.error).not.toHaveBeenCalled(); + expect(configStorageSpies.addConfig).toHaveBeenCalledWith(expectedConfig); }); it('should throw error if validation fails', () => { @@ -118,17 +161,20 @@ describe('ConfigFacade', () => { [variableName] = variableValue; } - const configStorage = { + const envs = { [`${envModuleName}__${envVariableName}`]: newValue, }; + const parsed = { + [envModuleName]: { [envVariableName]: newValue }, + }; const expectedConfig = { [variableName]: newValue, }; const errorMessage = 'error message'; - configExtractor.extract.mockReturnValueOnce(configStorage); - configParser.parse.mockReturnValueOnce(configStorage); + configExtractor.extract.mockReturnValueOnce(envs); + configParser.parse.mockReturnValueOnce(parsed); configFactory.createConfig.mockReturnValueOnce(expectedConfig); configValidator.validate.mockImplementationOnce(() => { throw new Error(errorMessage); @@ -136,6 +182,7 @@ describe('ConfigFacade', () => { const configSource = { raw: { variable: 'value' } }; const configFacade = new ConfigFacade( + configStorage, (configExtractor as unknown) as ConfigExtractor, configParser, configFactory, @@ -148,13 +195,58 @@ describe('ConfigFacade', () => { errorMessage, ); - expect(configExtractor.extract).toHaveBeenCalledWith(configSource); - expect(configParser.parse).toHaveBeenCalledWith(configStorage); - expect(configFactory.createConfig).toHaveBeenCalledWith( - configStorage, - TestConfig, + expect(configStorageSpies.rawConfigSetter).toHaveBeenCalledWith( + configSource.raw, ); + expect(configExtractor.extract).toHaveBeenCalledWith(configSource); + expect(configStorageSpies.envsSetter).toHaveBeenCalledWith(envs); + expect(configParser.parse).toHaveBeenCalledWith(envs); + expect(configStorageSpies.parsedSetter).toHaveBeenCalledWith(parsed); + expect(configStorageSpies.getConfig).toHaveBeenCalledWith(TestConfig); + expect(configFactory.createConfig).toHaveBeenCalledWith(parsed, TestConfig); expect(configValidator.validate).toHaveBeenCalledWith(expectedConfig); expect(logger.error).toHaveBeenCalled(); + expect(configStorageSpies.addConfig).not.toHaveBeenCalled(); + }); + + it('should return already exists config', () => { + const variableName = 'name'; + const variableValue = 'value'; + + @Config('TEST') + class TestConfig { + @Env('VARIABLE') + [variableName] = variableValue; + } + + const expectedConfig = { + [variableName]: variableValue, + }; + + configExtractor.extract.mockReturnValueOnce({}); + configParser.parse.mockReturnValueOnce({}); + configFactory.createConfig.mockReturnValueOnce(new TestConfig()); + + const configSource = {}; + + const configFacade = new ConfigFacade( + configStorage, + (configExtractor as unknown) as ConfigExtractor, + configParser, + configFactory, + configValidator, + (logger as unknown) as LoggerService, + configSource, + ); + + expect(configFacade.createConfig(TestConfig)).toMatchObject(expectedConfig); + jest.clearAllMocks(); + expect(configFacade.createConfig(TestConfig)).toMatchObject(expectedConfig); + + expect(configStorageSpies.getConfig).toHaveBeenCalledWith(TestConfig); + expect(configFactory.createConfig).not.toHaveBeenCalled(); + expect(configValidator.validate).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); + expect(configStorageSpies.addConfig).not.toHaveBeenCalled(); }); }); diff --git a/src/lib/facade.ts b/src/lib/facade.ts index 7d493af..c68670b 100644 --- a/src/lib/facade.ts +++ b/src/lib/facade.ts @@ -1,14 +1,15 @@ import { LoggerService } from '@nestjs/common'; -import { ClassType, ConfigSource, ConfigStorage, ProcessEnv } from './types'; +import { ClassType, ConfigSource } from './types'; +import { ConfigStorage } from './storage'; import { ConfigExtractor } from './extractor'; import { ConfigParser } from './parser'; import { ConfigFactory } from './factory'; import { ConfigValidator } from './validator'; +import { RAW_CONFIG } from '../tokens'; export class ConfigFacade { - private readonly configStorage: ConfigStorage; - constructor( + private readonly configStorage: ConfigStorage, private readonly configExtractor: ConfigExtractor, private readonly configParser: ConfigParser, private readonly configFactory: ConfigFactory, @@ -16,21 +17,31 @@ export class ConfigFacade { private readonly logger: LoggerService, private readonly source: ConfigSource, ) { - const processEnv: ProcessEnv = this.configExtractor.extract(source); - this.configStorage = this.configParser.parse(processEnv); + this.configStorage[RAW_CONFIG] = source.raw || {}; + this.configStorage.envs = this.configExtractor.extract(source); + this.configStorage.parsed = this.configParser.parse( + this.configStorage.envs, + ); } public createConfig(ConfigClass: ClassType): typeof ConfigClass.prototype { - const config = this.configFactory.createConfig( - this.configStorage, + let config = this.configStorage.getConfig(ConfigClass); + if (config) return config; + + config = this.configFactory.createConfig( + this.configStorage.parsed, ConfigClass, ); + try { this.configValidator.validate(config); } catch (err) { this.logger.error({ message: err.message }, 'Config validation error'); throw err; } + + this.configStorage.addConfig(config); + return config; } } diff --git a/src/lib/factory.ts b/src/lib/factory.ts index 9c293d5..bd3802c 100644 --- a/src/lib/factory.ts +++ b/src/lib/factory.ts @@ -1,10 +1,10 @@ -import { ClassType, ConfigStorage } from './types'; +import { ClassType, ParsedConfig } from './types'; import { ENV_CONFIG_NAME_SYMBOL } from './symbols'; import { plainToClass } from '../transformer'; export class ConfigFactory { public createConfig( - configStorage: ConfigStorage, + parsedConfig: ParsedConfig, ConfigClass: ClassType, ): typeof ConfigClass.prototype { let name = ConfigClass[ENV_CONFIG_NAME_SYMBOL]; @@ -12,8 +12,8 @@ export class ConfigFactory { // TODO: warning name = ConfigClass.name; } - if (!configStorage[name]) return new ConfigClass(); + if (!parsedConfig[name]) return new ConfigClass(); - return plainToClass(ConfigClass, configStorage[name]); + return plainToClass(ConfigClass, parsedConfig[name]); } } diff --git a/src/lib/global-module.spec.ts b/src/lib/global-module.spec.ts index 2ab2e28..63566e8 100644 --- a/src/lib/global-module.spec.ts +++ b/src/lib/global-module.spec.ts @@ -3,6 +3,7 @@ import { ConfigGlobalModule } from './global-module'; import { ConfigOptions } from '../options'; import { CONFIG_LOGGER, CONFIG_OPTIONS, RAW_CONFIG } from '../tokens'; import { ConfigLogger } from './logger'; +import { ConfigStorage } from './storage'; import { ConfigExtractor } from './extractor'; import { ConfigParser } from './parser'; import { ConfigFactory } from './factory'; @@ -37,6 +38,7 @@ describe('ConfigGlobalModule', () => { { provide: ConfigFacade, inject: [ + ConfigStorage, ConfigExtractor, ConfigParser, ConfigFactory, @@ -125,6 +127,7 @@ describe('ConfigGlobalModule', () => { { provide: ConfigFacade, inject: [ + ConfigStorage, ConfigExtractor, ConfigParser, ConfigFactory, diff --git a/src/lib/global-module.ts b/src/lib/global-module.ts index 8b353c5..d810eb9 100644 --- a/src/lib/global-module.ts +++ b/src/lib/global-module.ts @@ -10,6 +10,7 @@ import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-refe import * as dotenv from 'dotenv'; import { ConfigOptions } from '../options'; import { ConfigFacade } from './facade'; +import { ConfigStorage } from './storage'; import { ConfigExtractor } from './extractor'; import { ConfigParser } from './parser'; import { ConfigFactory } from './factory'; @@ -21,6 +22,10 @@ import { ProcessEnv } from './types'; @Global() @Module({ providers: [ + { + provide: ConfigStorage, + useClass: ConfigStorage, + }, { provide: ConfigExtractor, useFactory: () => new ConfigExtractor(dotenv.config), @@ -80,6 +85,7 @@ export class ConfigGlobalModule { { provide: ConfigFacade, inject: [ + ConfigStorage, ConfigExtractor, ConfigParser, ConfigFactory, @@ -88,6 +94,7 @@ export class ConfigGlobalModule { RAW_CONFIG, ], useFactory: ( + configStorage: ConfigStorage, configExtractor: ConfigExtractor, configParser: ConfigParser, configFactory: ConfigFactory, @@ -96,6 +103,7 @@ export class ConfigGlobalModule { raw: ProcessEnv, ) => new ConfigFacade( + configStorage, configExtractor, configParser, configFactory, diff --git a/src/lib/parser.ts b/src/lib/parser.ts index f1fb221..6aa6b7d 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -1,10 +1,10 @@ -import { ConfigStorage, ProcessEnv } from './types'; +import { ParsedConfig, ProcessEnv } from './types'; export const ENV_MODULE_SEPARATOR = '__'; export class ConfigParser { - public parse(processEnv: ProcessEnv): ConfigStorage { - const configStorage: ConfigStorage = {}; + public parse(processEnv: ProcessEnv): ParsedConfig { + const parsedConfig: ParsedConfig = {}; Object.entries(processEnv).forEach( ([variable, value]: [string, string]) => { const split = variable.split(ENV_MODULE_SEPARATOR); @@ -12,11 +12,11 @@ export class ConfigParser { return; } const moduleName = split[0]; - configStorage[moduleName] = configStorage[moduleName] || {}; + parsedConfig[moduleName] = parsedConfig[moduleName] || {}; // interpret empty string as undefined - configStorage[moduleName][split[1]] = value || undefined; + parsedConfig[moduleName][split[1]] = value || undefined; }, ); - return configStorage; + return parsedConfig; } } diff --git a/src/lib/storage.spec.ts b/src/lib/storage.spec.ts new file mode 100644 index 0000000..033abbc --- /dev/null +++ b/src/lib/storage.spec.ts @@ -0,0 +1,59 @@ +import { ConfigStorage } from './storage'; +import { RAW_CONFIG } from '../tokens'; + +describe('ConfigStorage', () => { + let configStorage: ConfigStorage; + + beforeEach(() => { + configStorage = new ConfigStorage(); + }); + + it('get/set envs', () => { + const envs = { envVar: 'value' }; + configStorage.envs = envs; + expect(configStorage.envs).toEqual(envs); + }); + + it('get/set RAW_CONFIG', () => { + const rawConfig = { envVar: 'value' }; + configStorage[RAW_CONFIG] = rawConfig; + expect(configStorage[RAW_CONFIG]).toEqual(rawConfig); + }); + + it('get/set parsed', () => { + const parsed = { envModule: { envVar: 'value' } }; + configStorage.parsed = parsed; + expect(configStorage.parsed).toEqual(parsed); + }); + + describe('config', () => { + it('get/set config', () => { + class TestConfig { + env = 'value'; + } + const testConfig = new TestConfig(); + + configStorage.addConfig(testConfig); + expect(configStorage.getConfig(TestConfig)).toEqual(testConfig); + }); + + it('getAllConfigs', () => { + class TestConfig1 { + env = 'value'; + } + const testConfig1 = new TestConfig1(); + + class TestConfig2 { + env = 'value'; + } + const testConfig2 = new TestConfig2(); + + configStorage.addConfig(testConfig1); + configStorage.addConfig(testConfig2); + expect(configStorage.getAllConfigs()).toEqual({ + [TestConfig1.name]: testConfig1, + [TestConfig2.name]: testConfig2, + }); + }); + }); +}); diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..7775ee7 --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,50 @@ +import { RAW_CONFIG } from '../tokens'; +import { ClassType, ParsedConfig, ProcessEnv } from './types'; + +export class ConfigStorage { + private _envs: ProcessEnv; + + private _RAW_CONFIG: ProcessEnv; + + private _parsed: ParsedConfig; + + private configs: { + [name: string]: {}; + } = {}; + + set envs(processEnv: ProcessEnv) { + this._envs = processEnv; + } + + get envs(): ProcessEnv { + return this._envs; + } + + set [RAW_CONFIG](rawConfig: ProcessEnv) { + this._RAW_CONFIG = rawConfig; + } + + get [RAW_CONFIG](): ProcessEnv { + return this._RAW_CONFIG; + } + + set parsed(parsedConfig: ParsedConfig) { + this._parsed = parsedConfig; + } + + get parsed(): ParsedConfig { + return this._parsed; + } + + addConfig(config: {}): void { + this.configs[config.constructor.name] = config; + } + + getConfig(ConfigClass: ClassType): typeof ConfigClass.prototype | undefined { + return this.configs[ConfigClass.name]; + } + + getAllConfigs(): { [name: string]: {} } { + return this.configs; + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 5aa8e9b..3800cc3 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,7 +3,7 @@ export declare type ClassType = { new (...args: any[]): {}; }; -export type ConfigStorage = { +export type ParsedConfig = { [name: string]: | string | { diff --git a/src/module.spec.ts b/src/module.spec.ts index e09047d..9d8a4ae 100644 --- a/src/module.spec.ts +++ b/src/module.spec.ts @@ -5,6 +5,7 @@ import { ConfigGlobalModule } from './lib/global-module'; import { CONFIG_LOGGER, CONFIG_OPTIONS, RAW_CONFIG } from './tokens'; import { ConfigLogger } from './lib/logger'; import { ConfigFacade } from './lib/facade'; +import { ConfigStorage } from './lib/storage'; import { ConfigExtractor } from './lib/extractor'; import { ConfigParser } from './lib/parser'; import { ConfigFactory } from './lib/factory'; @@ -32,6 +33,7 @@ describe('ConfigModule', () => { { provide: ConfigFacade, inject: [ + ConfigStorage, ConfigExtractor, ConfigParser, ConfigFactory, @@ -91,6 +93,7 @@ describe('ConfigModule', () => { { provide: ConfigFacade, inject: [ + ConfigStorage, ConfigExtractor, ConfigParser, ConfigFactory, From 3f0de9f8adc8725534774d58b1ce7405ed81f1d5 Mon Sep 17 00:00:00 2001 From: Velmisov Date: Wed, 15 Jul 2020 12:36:30 +0300 Subject: [PATCH 2/4] feat: make it possible to get configs --- src/lib/facade.spec.ts | 76 +++++++++++++++++++++++++++++++++++++++--- src/lib/facade.ts | 10 +++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/lib/facade.spec.ts b/src/lib/facade.spec.ts index b276893..a3bd5c0 100644 --- a/src/lib/facade.spec.ts +++ b/src/lib/facade.spec.ts @@ -8,6 +8,7 @@ import { ConfigParser } from './parser'; import { ConfigFactory } from './factory'; import { ConfigValidator } from './validator'; import { ConfigLogger } from './logger'; +import { ConfigSource } from './types'; describe('ConfigFacade', () => { let configStorage: ConfigStorage; @@ -68,7 +69,7 @@ describe('ConfigFacade', () => { configParser.parse.mockReturnValueOnce({}); configFactory.createConfig.mockReturnValueOnce(new TestConfig()); - const configSource = {}; + const configSource: ConfigSource = {}; const configFacade = new ConfigFacade( configStorage, @@ -121,7 +122,7 @@ describe('ConfigFacade', () => { configParser.parse.mockReturnValueOnce(parsed); configFactory.createConfig.mockReturnValueOnce(expectedConfig); - const configSource = { fromFile: '.env.test' }; + const configSource: ConfigSource = { fromFile: '.env.test' }; const configFacade = new ConfigFacade( configStorage, @@ -180,7 +181,7 @@ describe('ConfigFacade', () => { throw new Error(errorMessage); }); - const configSource = { raw: { variable: 'value' } }; + const configSource: ConfigSource = { raw: { variable: 'value' } }; const configFacade = new ConfigFacade( configStorage, (configExtractor as unknown) as ConfigExtractor, @@ -227,7 +228,7 @@ describe('ConfigFacade', () => { configParser.parse.mockReturnValueOnce({}); configFactory.createConfig.mockReturnValueOnce(new TestConfig()); - const configSource = {}; + const configSource: ConfigSource = {}; const configFacade = new ConfigFacade( configStorage, @@ -249,4 +250,71 @@ describe('ConfigFacade', () => { expect(logger.error).not.toHaveBeenCalled(); expect(configStorageSpies.addConfig).not.toHaveBeenCalled(); }); + + it('should return all generated configs', () => { + const variableName = 'name'; + const variableValue = 'value'; + + @Config('TEST') + class TestConfig1 { + @Env('VARIABLE') + [variableName] = variableValue; + } + + @Config('TEST') + class TestConfig2 { + @Env('VARIABLE') + [variableName] = variableValue; + } + + const expectedConfig = { + [variableName]: variableValue, + }; + + configExtractor.extract.mockReturnValueOnce({}); + configParser.parse.mockReturnValueOnce({}); + configFactory.createConfig.mockReturnValueOnce(new TestConfig1()); + configFactory.createConfig.mockReturnValueOnce(new TestConfig2()); + configFactory.createConfig.mockReturnValueOnce(new TestConfig1()); + + const configSource: ConfigSource = {}; + + const configFacade = new ConfigFacade( + configStorage, + (configExtractor as unknown) as ConfigExtractor, + configParser, + configFactory, + configValidator, + (logger as unknown) as LoggerService, + configSource, + ); + + configFacade.createConfig(TestConfig1); + configFacade.createConfig(TestConfig2); + configFacade.createConfig(TestConfig1); + + expect(configFacade.getAllGeneratedConfigs()).toEqual({ + [TestConfig1.name]: expectedConfig, + [TestConfig2.name]: expectedConfig, + }); + }); + + it('should return raw config', () => { + configExtractor.extract.mockReturnValueOnce({}); + configParser.parse.mockReturnValueOnce({}); + + const configSource: ConfigSource = { raw: { variable: 'value' } }; + + const configFacade = new ConfigFacade( + configStorage, + (configExtractor as unknown) as ConfigExtractor, + configParser, + configFactory, + configValidator, + (logger as unknown) as LoggerService, + configSource, + ); + + expect(configFacade.getRawConfig()).toEqual(configSource.raw); + }); }); diff --git a/src/lib/facade.ts b/src/lib/facade.ts index c68670b..4ef37b0 100644 --- a/src/lib/facade.ts +++ b/src/lib/facade.ts @@ -1,5 +1,5 @@ import { LoggerService } from '@nestjs/common'; -import { ClassType, ConfigSource } from './types'; +import { ClassType, ConfigSource, ProcessEnv } from './types'; import { ConfigStorage } from './storage'; import { ConfigExtractor } from './extractor'; import { ConfigParser } from './parser'; @@ -44,4 +44,12 @@ export class ConfigFacade { return config; } + + public getAllGeneratedConfigs(): { [name: string]: {} } { + return this.configStorage.getAllConfigs(); + } + + public getRawConfig(): ProcessEnv { + return this.configStorage[RAW_CONFIG]; + } } From cb7d3c74b386624a70056a552754533147108f2d Mon Sep 17 00:00:00 2001 From: Velmisov Date: Wed, 15 Jul 2020 13:57:00 +0300 Subject: [PATCH 3/4] feat: provide repl for config --- src/repl.spec.ts | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ src/repl.ts | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 src/repl.spec.ts create mode 100644 src/repl.ts diff --git a/src/repl.spec.ts b/src/repl.spec.ts new file mode 100644 index 0000000..43f7ae8 --- /dev/null +++ b/src/repl.spec.ts @@ -0,0 +1,72 @@ +import repl from 'repl'; +import { NestFactory } from '@nestjs/core'; +import { NestFactoryStatic } from '@nestjs/core/nest-factory'; +import { INestApplicationContext } from '@nestjs/common'; +import { ConfigREPL } from './repl'; +import { RAW_CONFIG } from './tokens'; + +jest.mock('@nestjs/core'); + +describe('ConfigREPL', () => { + it('startWithAppContext', () => { + const replMock: jest.Mocked> = { + start: jest.fn(), + }; + const configREPL = new ConfigREPL(replMock as typeof repl); + const facadeMock = { + getAllGeneratedConfigs: jest.fn(), + getRawConfig: jest.fn(), + }; + const applicationContext: jest.Mocked> = { + get: jest.fn(), + }; + + applicationContext.get.mockReturnValueOnce(facadeMock); + facadeMock.getAllGeneratedConfigs.mockReturnValueOnce({}); + facadeMock.getRawConfig.mockReturnValueOnce({}); + const replServer = { context: {} }; + replMock.start.mockReturnValueOnce(replServer as repl.REPLServer); + + expect( + configREPL.startWithAppContext( + applicationContext as INestApplicationContext, + ), + ).toEqual({ + context: { [RAW_CONFIG]: {} }, + }); + }); + + it('startWithAppModule', async () => { + const replMock: jest.Mocked> = { + start: jest.fn(), + }; + const configREPL = new ConfigREPL(replMock as typeof repl); + const facadeMock = { + getAllGeneratedConfigs: jest.fn(), + getRawConfig: jest.fn(), + }; + const applicationContext: jest.Mocked> = { + get: jest.fn(), + }; + (NestFactory as jest.Mocked< + NestFactoryStatic + >).createApplicationContext.mockResolvedValueOnce( + applicationContext as INestApplicationContext, + ); + + applicationContext.get.mockReturnValueOnce(facadeMock); + const configs = { + AppConfig: { variable: 'value' }, + CatConfig: { variable: 'value' }, + }; + facadeMock.getAllGeneratedConfigs.mockReturnValueOnce(configs); + const rawConfig = { variable: 'value' }; + facadeMock.getRawConfig.mockReturnValueOnce(rawConfig); + const replServer = { context: {} }; + replMock.start.mockReturnValueOnce(replServer as repl.REPLServer); + + await expect(configREPL.startWithAppModule(undefined)).resolves.toEqual({ + context: { [RAW_CONFIG]: rawConfig, ...configs }, + }); + }); +}); diff --git a/src/repl.ts b/src/repl.ts new file mode 100644 index 0000000..8204aaa --- /dev/null +++ b/src/repl.ts @@ -0,0 +1,52 @@ +import repl, { REPLServer, ReplOptions } from 'repl'; +import { NestFactory } from '@nestjs/core'; +import { INestApplicationContext } from '@nestjs/common'; +import { Type } from '@nestjs/common/interfaces/type.interface'; +import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface'; +import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface'; +import { ConfigFacade } from './lib/facade'; +import { RAW_CONFIG } from './tokens'; + +export class ConfigREPL { + constructor(private readonly _repl: typeof repl) {} + + startWithAppContext( + applicationContext: INestApplicationContext, + options?: ReplOptions, + ): REPLServer { + const configFacade = applicationContext.get(ConfigFacade); + const configs = configFacade.getAllGeneratedConfigs(); + const rawConfig = configFacade.getRawConfig(); + + const server = this._repl.start({ + prompt: 'config > ', + input: process.stdin, + output: process.stdout, + terminal: true, + preview: true, + ...(options || {}), + }); + + Object.assign(server.context, configs); + server.context[RAW_CONFIG] = rawConfig; + + return server; + } + + async startWithAppModule( + AppModule: + | Type // eslint-disable-line @typescript-eslint/no-explicit-any + | DynamicModule + | Promise + | ForwardReference, + options?: ReplOptions, + ): Promise { + const applicationContext = await NestFactory.createApplicationContext( + AppModule, + ); + + return this.startWithAppContext(applicationContext, options); + } +} + +export const configREPL = new ConfigREPL(repl); From 416c9ee9d155bca4b43915b64a2b77c5cd10a4bd Mon Sep 17 00:00:00 2001 From: Velmisov Date: Wed, 15 Jul 2020 13:57:16 +0300 Subject: [PATCH 4/4] test: add repl to example --- example/app.module.ts | 9 ++++++++- example/repl.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 example/repl.ts diff --git a/example/app.module.ts b/example/app.module.ts index f275e05..e376afb 100644 --- a/example/app.module.ts +++ b/example/app.module.ts @@ -1,8 +1,15 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '../src'; import { CatModule } from './cat/cat.module'; +import { AppConfig } from './app.config'; @Module({ - imports: [ConfigModule.forRoot({}), CatModule], + imports: [ + ConfigModule.forRoot({ + fromFile: process.env.NODE_ENV === 'production' ? undefined : '.env', + configs: [AppConfig], + }), + CatModule, + ], }) export class AppModule {} diff --git a/example/repl.ts b/example/repl.ts new file mode 100644 index 0000000..dca2792 --- /dev/null +++ b/example/repl.ts @@ -0,0 +1,16 @@ +import { NestFactory } from '@nestjs/core'; +import { startWithAppContext } from '../src/repl'; +import { AppModule } from './app.module'; + +(async () => { + const applicationContext = await NestFactory.createApplicationContext( + AppModule, + ); + startWithAppContext(applicationContext); +})(); + +/** or built-in: + (async () => { + await startWithAppModule(AppModule); + })(); +*/