From 4eb18c6792d60d83e789d0f05f7f06e021cbf6f8 Mon Sep 17 00:00:00 2001 From: Gleb Azarov Date: Mon, 7 Nov 2022 21:21:33 +0600 Subject: [PATCH 1/3] test(e2e): switch logs to silent mode --- e2e-test/file.e2e-spec.ts | 4 +++- e2e-test/process.e2e-spec.ts | 4 +++- e2e-test/raw.e2e-spec.ts | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/e2e-test/file.e2e-spec.ts b/e2e-test/file.e2e-spec.ts index 6876057..eabc16f 100644 --- a/e2e-test/file.e2e-spec.ts +++ b/e2e-test/file.e2e-spec.ts @@ -45,7 +45,9 @@ describe('File', () => { }) class AppModule {} - const app = await NestFactory.createApplicationContext(AppModule); + const app = await NestFactory.createApplicationContext(AppModule, { + bufferLogs: true, + }); const firstConfig = app.get(FirstConfig); const secondConfig = app.get(SecondConfig); diff --git a/e2e-test/process.e2e-spec.ts b/e2e-test/process.e2e-spec.ts index 8b56018..13c3413 100644 --- a/e2e-test/process.e2e-spec.ts +++ b/e2e-test/process.e2e-spec.ts @@ -24,7 +24,9 @@ describe('Process', () => { }) class AppModule {} - const app = await NestFactory.createApplicationContext(AppModule); + const app = await NestFactory.createApplicationContext(AppModule, { + bufferLogs: true, + }); const firstConfig = app.get(FirstConfig); const secondConfig = app.get(SecondConfig); diff --git a/e2e-test/raw.e2e-spec.ts b/e2e-test/raw.e2e-spec.ts index bddd3c4..57ee0b2 100644 --- a/e2e-test/raw.e2e-spec.ts +++ b/e2e-test/raw.e2e-spec.ts @@ -32,7 +32,9 @@ describe('Raw', () => { }) class AppModule {} - const app = await NestFactory.createApplicationContext(AppModule); + const app = await NestFactory.createApplicationContext(AppModule, { + bufferLogs: true, + }); const firstConfig = app.get(FirstConfig); const secondConfig = app.get(SecondConfig); From f6280ef8797f47552337ab895a942fa966b88976 Mon Sep 17 00:00:00 2001 From: Gleb Azarov Date: Mon, 7 Nov 2022 21:21:59 +0600 Subject: [PATCH 2/3] chore: delete dummy tests --- e2e-test/dummy.e2e-spec.ts | 5 ----- src/dummy.spec.ts | 5 ----- 2 files changed, 10 deletions(-) delete mode 100644 e2e-test/dummy.e2e-spec.ts delete mode 100644 src/dummy.spec.ts diff --git a/e2e-test/dummy.e2e-spec.ts b/e2e-test/dummy.e2e-spec.ts deleted file mode 100644 index 4aa52c8..0000000 --- a/e2e-test/dummy.e2e-spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('dummy e2e test', () => { - it('true tobe true', () => { - expect(true).toBe(true); - }); -}); diff --git a/src/dummy.spec.ts b/src/dummy.spec.ts deleted file mode 100644 index 8e28c69..0000000 --- a/src/dummy.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('dummy unit test', () => { - it('true tobe true', () => { - expect(true).toBe(true); - }); -}); From c7c1a3386bcd2ba4df8b1e7b31a83235b0e34db7 Mon Sep 17 00:00:00 2001 From: Gleb Azarov Date: Mon, 7 Nov 2022 21:53:49 +0600 Subject: [PATCH 3/3] feat: add unscoped env variables support --- README.md | 53 +++++++++++++++++++++-------------- e2e-test/unscoped.e2e-spec.ts | 39 ++++++++++++++++++++++++++ src/decorator.ts | 9 +++++- src/lib/factory.spec.ts | 45 ++++++++++++++++++++++++++++- src/lib/factory.ts | 8 +++++- src/lib/parser.spec.ts | 31 +++++++++++--------- src/lib/parser.ts | 5 +++- src/lib/symbols.ts | 1 + src/lib/types.ts | 2 +- 9 files changed, 154 insertions(+), 39 deletions(-) create mode 100644 e2e-test/unscoped.e2e-spec.ts diff --git a/README.md b/README.md index 6f8231d..7e0108f 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,13 @@ ![Travis](https://img.shields.io/travis/ukitgroup/nestjs-config/master.svg?style=flat-square) ![Coverage Status](https://coveralls.io/repos/github/ukitgroup/nestjs-config/badge.svg?branch=master) -![node](https://img.shields.io/node/v/@ukitgroup/nestjs-config.svg?style=flat-square) ![npm](https://img.shields.io/npm/v/@ukitgroup/nestjs-config.svg?style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/ukitgroup/nestjs-config.svg?style=flat-square) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/ukitgroup/nestjs-config.svg?style=flat-square) -![David](https://img.shields.io/david/ukitgroup/nestjs-config.svg?style=flat-square) -![David](https://img.shields.io/david/dev/ukitgroup/nestjs-config.svg?style=flat-square) ![license](https://img.shields.io/github/license/ukitgroup/nestjs-config.svg?style=flat-square) ![GitHub last commit](https://img.shields.io/github/last-commit/ukitgroup/nestjs-config.svg?style=flat-square) -![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square) - -## Description ### Convenient modular config for [`nestjs`](https://github.com/nestjs/nest) applications @@ -93,11 +87,11 @@ ConfigModule.forRoot(options: ConfigOptions) ``` ```typescript -ConfigOptions: { - fromFile?: string, - configs?: ClassType[], - imports?: NestModule[], - providers?: Provider[], +interface ConfigOptions { + fromFile?: string; + configs?: ClassType[]; + imports?: NestModule[]; + providers?: Provider[]; } ``` @@ -128,21 +122,21 @@ ConfigModule.forFeature(configs: ClassType[]) ### Decorators | Decorator | Description | -| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| **Common config decorators** | | +|-------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| **Common config decorators** | Import from `@ukitgroup/nestjs-config` | | `@Config(name: string)` | Add prefix to env variables | -| `@Env(name: string)` | Extract env with `name` to this varaible | +| `@UnscopedConfig()` | For config without prefix in env variables names | +| `@Env(name: string)` | Extract env with `name` to this variable | | | | | **Type decorators** | Import from `@ukitgroup/nestjs-config/types` | | `@String()` | String variable (`@IsString`) | -| `@Number()` | Number variable (`parseFloat` + `@IsNumber` | -| `@Integer()` | Integer variable (`parseInt` + `@IsInteger` | -| `@Boolean()` | Boolean variable ('true','false' + @IsBool`) | +| `@Number()` | Number variable (`parseFloat` + `@IsNumber`) | +| `@Integer()` | Integer variable (`parseInt` + `@IsInteger`) | +| `@Boolean()` | Boolean variable ('true', 'false' + `@IsBool`) | | `@Transform(transformFn: Function)` | Custom transformation. Import from `@ukitgroup/nestjs-config/transformer` | | | | | **Validation decorators** | The same as [`class-validator`](https://github.com/typestack/class-validator). Import from `@ukitgroup/nestjs-config/validator`. | - ## Usage **app.module.ts** @@ -283,7 +277,24 @@ or on launch your application APP__HTTP_PORT=3000 CAT__NAME=vasya CAT__WEIGHT=5 node dist/main.js ``` -Also you can see the example on github +Also you can see the [example](./e2e-test) on GitHub. + +## Unscoped configs + +> Okay, that's cool, but what if I need more flexibility in environment variables? For example, we migrate from legacy service, and we need to use old variables names (without module__ prefix). + +I don't think this approach is quite right, try not to use it, but... we have `@UnscopedConfig` decorator instead of `@Config` for this case. But really, try to use `@Config` decorator first 🙂 + +For example, we have env key `LEGACY_VARIABLE`, then config will be similar to this: + +```typescript +@UnscopedConfig() +export class LegacyConfig { + @Env('LEGACY_VARIABLE') + @String() + readonly fieldOne: string; +} +``` ## Transformation @@ -326,8 +337,8 @@ Library will throw an error on launch application: `Cat.weight received 'not_a_n ## Requirements -1. @nestjs/common ^7.2.0 -2. @nestjs/core ^7.2.0 +1. @nestjs/common >=7.2.0 +2. @nestjs/core >=7.2.0 ## License diff --git a/e2e-test/unscoped.e2e-spec.ts b/e2e-test/unscoped.e2e-spec.ts new file mode 100644 index 0000000..4e561eb --- /dev/null +++ b/e2e-test/unscoped.e2e-spec.ts @@ -0,0 +1,39 @@ +/* eslint-disable max-classes-per-file */ +import { Module } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { ConfigModule, UnscopedConfig, Env } from '../src'; +import { String } from '../src/types'; + +describe('UnscopedConfig', () => { + @UnscopedConfig() + class TestUnscopedConfig { + @Env('SOME_UNSCOPED_VARIABLE') + @String() + stringVar: string; + } + + @Module({ + imports: [ConfigModule.forRoot({ configs: [TestUnscopedConfig] })], + }) + class AppModule {} + + beforeAll(() => { + Object.assign(process.env, { + SOME_UNSCOPED_VARIABLE: 'stringVarOfSomeVariableOutOfModuleScope', + }); + }); + + it('should successfully map envs out of module scope', async () => { + const app = await NestFactory.createApplicationContext(AppModule, { + bufferLogs: true, + }); + + const testConfig = app.get(TestUnscopedConfig); + + expect(testConfig).toMatchObject({ + stringVar: 'stringVarOfSomeVariableOutOfModuleScope', + }); + + await app.close(); + }); +}); diff --git a/src/decorator.ts b/src/decorator.ts index 6252a08..0d1d5ac 100644 --- a/src/decorator.ts +++ b/src/decorator.ts @@ -1,5 +1,5 @@ import { Expose } from './transformer'; -import { ENV_CONFIG_NAME_SYMBOL } from './lib/symbols'; +import { ENV_CONFIG_NAME_SYMBOL, UNSCOPED_CONFIG_SYMBOL } from './lib/symbols'; export function Config(name: string): ClassDecorator { return (target: Function): void => { @@ -8,6 +8,13 @@ export function Config(name: string): ClassDecorator { }; } +export function UnscopedConfig(): ClassDecorator { + return (target: Function): void => { + // eslint-disable-next-line no-param-reassign + target[UNSCOPED_CONFIG_SYMBOL] = true; + }; +} + export function Env(name: string): PropertyDecorator { return Expose({ name }); } diff --git a/src/lib/factory.spec.ts b/src/lib/factory.spec.ts index bb1c6a0..1980b7c 100644 --- a/src/lib/factory.spec.ts +++ b/src/lib/factory.spec.ts @@ -1,6 +1,7 @@ import { ConfigFactory } from './factory'; -import { Config, Env } from '../decorator'; +import { Config, Env, UnscopedConfig } from '../decorator'; import { Boolean, Integer, Number, String } from '../types'; +import { UNSCOPED_CONFIG_SYMBOL } from './symbols'; describe('ConfigFactory', () => { const configFactory = new ConfigFactory(); @@ -36,6 +37,48 @@ describe('ConfigFactory', () => { }); }); + describe('Unscoped config', () => { + const variableName = 'variable'; + const envVariableName = 'UNSCOPED_VARIABLE'; + const value = 'value from env'; + + @UnscopedConfig() + class TestConfig { + @Env(envVariableName) + @String() + [variableName]: string; + } + + it('should return config with raw mapping without prefix', () => { + expect( + configFactory.createConfig( + { + [UNSCOPED_CONFIG_SYMBOL]: { + [envVariableName]: value, + }, + }, + TestConfig, + ), + ).toMatchObject({ + [variableName]: value, + }); + }); + + it('should return config without extraneous envs', () => { + expect( + configFactory.createConfig( + { + [UNSCOPED_CONFIG_SYMBOL]: { + [envVariableName]: value, + EXTRANEOUS_ENV: 'extraneous value', + }, + }, + TestConfig, + ), + ).not.toHaveProperty('EXTRANEOUS_ENV'); + }); + }); + describe('Non-decorated config', () => { const variableName = 'variable'; const value = 'value'; diff --git a/src/lib/factory.ts b/src/lib/factory.ts index 9c293d5..e8eef7b 100644 --- a/src/lib/factory.ts +++ b/src/lib/factory.ts @@ -1,5 +1,5 @@ import { ClassType, ConfigStorage } from './types'; -import { ENV_CONFIG_NAME_SYMBOL } from './symbols'; +import { ENV_CONFIG_NAME_SYMBOL, UNSCOPED_CONFIG_SYMBOL } from './symbols'; import { plainToClass } from '../transformer'; export class ConfigFactory { @@ -7,6 +7,12 @@ export class ConfigFactory { configStorage: ConfigStorage, ConfigClass: ClassType, ): typeof ConfigClass.prototype { + if (ConfigClass[UNSCOPED_CONFIG_SYMBOL]) { + return plainToClass(ConfigClass, configStorage[UNSCOPED_CONFIG_SYMBOL], { + excludeExtraneousValues: true, + }); + } + let name = ConfigClass[ENV_CONFIG_NAME_SYMBOL]; if (!name) { // TODO: warning diff --git a/src/lib/parser.spec.ts b/src/lib/parser.spec.ts index b215302..fc1116f 100644 --- a/src/lib/parser.spec.ts +++ b/src/lib/parser.spec.ts @@ -1,33 +1,38 @@ import { ConfigParser, ENV_MODULE_SEPARATOR } from './parser'; +import { UNSCOPED_CONFIG_SYMBOL } from './symbols'; describe('ConfigParser', () => { const configParser = new ConfigParser(); - it('should return empty object if nothing provided', () => { - expect(configParser.parse({})).toMatchObject({}); + it('should return object only with raw envs if nothing provided', () => { + expect(configParser.parse({})).toMatchObject({ + [UNSCOPED_CONFIG_SYMBOL]: {}, + }); }); - it('should return empty object if module not provided', () => { - expect( - configParser.parse({ - SOME_VARIABLE: 'some value', - }), - ).toMatchObject({}); + it('should return object only with raw envs if module not provided', () => { + const env = { + SOME_VARIABLE: 'some value', + }; + + expect(configParser.parse(env)).toMatchObject({ + [UNSCOPED_CONFIG_SYMBOL]: env, + }); }); it('should return values to override module config defaults', () => { const moduleName = 'SOME_MODULE'; const variableName = 'SOME_VARIABLE'; const value = 'some value'; + const env = { + [`${moduleName}${ENV_MODULE_SEPARATOR}${variableName}`]: value, + }; - expect( - configParser.parse({ - [`${moduleName}${ENV_MODULE_SEPARATOR}${variableName}`]: value, - }), - ).toMatchObject({ + expect(configParser.parse(env)).toMatchObject({ [moduleName]: { [variableName]: value, }, + [UNSCOPED_CONFIG_SYMBOL]: env, }); }); diff --git a/src/lib/parser.ts b/src/lib/parser.ts index f1fb221..67820e3 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -1,10 +1,13 @@ import { ConfigStorage, ProcessEnv } from './types'; +import { UNSCOPED_CONFIG_SYMBOL } from './symbols'; export const ENV_MODULE_SEPARATOR = '__'; export class ConfigParser { public parse(processEnv: ProcessEnv): ConfigStorage { - const configStorage: ConfigStorage = {}; + const configStorage: ConfigStorage = { + [UNSCOPED_CONFIG_SYMBOL]: processEnv, + }; Object.entries(processEnv).forEach( ([variable, value]: [string, string]) => { const split = variable.split(ENV_MODULE_SEPARATOR); diff --git a/src/lib/symbols.ts b/src/lib/symbols.ts index df7f76b..66824be 100644 --- a/src/lib/symbols.ts +++ b/src/lib/symbols.ts @@ -1 +1,2 @@ export const ENV_CONFIG_NAME_SYMBOL = Symbol.for('ENV_CONFIG_NAME'); +export const UNSCOPED_CONFIG_SYMBOL = Symbol.for('UNSCOPED_ENV_CONFIG'); diff --git a/src/lib/types.ts b/src/lib/types.ts index 5aa8e9b..e11e335 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -4,7 +4,7 @@ export declare type ClassType = { }; export type ConfigStorage = { - [name: string]: + [name: string | symbol]: | string | { [name: string]: string | number | boolean;