Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unscoped variables support #24

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 32 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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[];
}
```

Expand Down Expand Up @@ -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**
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Или "^9.1.4" ?

2. @nestjs/core >=7.2.0

## License

Expand Down
5 changes: 0 additions & 5 deletions e2e-test/dummy.e2e-spec.ts

This file was deleted.

4 changes: 3 additions & 1 deletion e2e-test/file.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(FirstConfig);
const secondConfig = app.get<SecondConfig>(SecondConfig);
Expand Down
4 changes: 3 additions & 1 deletion e2e-test/process.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(FirstConfig);
const secondConfig = app.get<SecondConfig>(SecondConfig);
Expand Down
4 changes: 3 additions & 1 deletion e2e-test/raw.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(FirstConfig);
const secondConfig = app.get<SecondConfig>(SecondConfig);
Expand Down
39 changes: 39 additions & 0 deletions e2e-test/unscoped.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -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>(TestUnscopedConfig);

expect(testConfig).toMatchObject({
stringVar: 'stringVarOfSomeVariableOutOfModuleScope',
});

await app.close();
});
});
9 changes: 8 additions & 1 deletion src/decorator.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand All @@ -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 });
}
5 changes: 0 additions & 5 deletions src/dummy.spec.ts

This file was deleted.

45 changes: 44 additions & 1 deletion src/lib/factory.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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';
Expand Down
8 changes: 7 additions & 1 deletion src/lib/factory.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
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 {
public createConfig(
configStorage: ConfigStorage,
ConfigClass: ClassType,
): typeof ConfigClass.prototype {
if (ConfigClass[UNSCOPED_CONFIG_SYMBOL]) {
return plainToClass(ConfigClass, configStorage[UNSCOPED_CONFIG_SYMBOL], {
excludeExtraneousValues: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there will be different behaviour with @UnscopedConfig and usuall @Config. So after migration could be implicit consequences

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, but without excludeExtraneousValues all envs will be automatically exposed to class instance which is more terrible than the inconsistent behavior. Moreover, I do not understand why this option is not enabled in the @Config mode. And, I hope, @UnscopedConfig is more for migration cases than for everyday use, because implementation is one big workaround.

I think we should describe excludeExtraneousValues behavior in readme of @UnscopedConfig and enable it for @Config in next major version.

P.S. But in general, you gave me a strange idea how to reduce workaroundness of this solution, I'll try to implement it.

});
}

let name = ConfigClass[ENV_CONFIG_NAME_SYMBOL];
if (!name) {
// TODO: warning
Expand Down
31 changes: 18 additions & 13 deletions src/lib/parser.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});

Expand Down
5 changes: 4 additions & 1 deletion src/lib/parser.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/lib/symbols.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const ENV_CONFIG_NAME_SYMBOL = Symbol.for('ENV_CONFIG_NAME');
export const UNSCOPED_CONFIG_SYMBOL = Symbol.for('UNSCOPED_ENV_CONFIG');
2 changes: 1 addition & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export declare type ClassType = {
};

export type ConfigStorage = {
[name: string]:
[name: string | symbol]:
| string
| {
[name: string]: string | number | boolean;
Expand Down