Skip to content

Commit

Permalink
Merge branch 'release/0.4.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
allohamora committed Apr 10, 2023
2 parents 3add355 + 724307d commit a143f21
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 40 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [0.4.0](https://github.com/allohamora/config-manager/compare/0.3.0...0.4.0) (2023-04-10)

### Features

- add atLeastOne to for methods ([1b43a5f](https://github.com/allohamora/config-manager/commit/1b43a5f01bf231d7f4935d62b0d7581b080f94d1))
- add EnvPicker export ([9d03bc8](https://github.com/allohamora/config-manager/commit/9d03bc874e0d65416a18ac1b0f3a02af915fbcf0))
- add getFor and getForOrThrow ([51dbf8c](https://github.com/allohamora/config-manager/commit/51dbf8c2c98af632758ce2dcd9e866d184207066))
- add pickFor and pickForOrThrow ([689b5ba](https://github.com/allohamora/config-manager/commit/689b5baabe7acf9c88ae25d668d60deca31b9649))
- improve nodeEnv typings ([3673a1d](https://github.com/allohamora/config-manager/commit/3673a1da2baeba6a0f462f671fa5ec2f99981a8a))

### Bug Fixes

- rename mapIfExist to mapIfExists ([2d1c8e4](https://github.com/allohamora/config-manager/commit/2d1c8e4fb0a7b0d22ff61a23434fdf142e7cf19f))

## [0.3.0](https://github.com/allohamora/config-manager/compare/0.2.2...0.3.0) (2023-04-08)

### Features
Expand Down
61 changes: 60 additions & 1 deletion __tests__/unit/env.manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('EnvManager', () => {
INTEGER: '123',
} as Record<string, string | undefined>;

const env = new EnvManager({ load: () => envStub });
const env = new EnvManager({ load: () => envStub, nodeEnv: () => 'test' as 'test' | 'production' });

describe('pick', () => {
it('returns values', () => {
Expand All @@ -34,4 +34,63 @@ describe('EnvManager', () => {
expect(() => env.pickOrThrow('OPTIONAL')).toThrowError();
});
});

describe('pickFor', () => {
it('returns values', () => {
expect(env.pickFor({ test: 'EMAIL' })?.value()).toBe(envStub.EMAIL);
expect(env.pickFor({ test: 'INTEGER' })?.value()).toBe(envStub.INTEGER);

expect(env.pickFor({ production: 'EMAIL' })?.value()).toEqual(undefined);
expect(env.pickFor({ production: 'INTEGER' })?.value()).toEqual(undefined);

expect(env.pickFor({ test: 'EMPTY_STRING' })?.value()).toEqual(undefined);
expect(env.pickFor({ test: 'SPACE_STRING' })?.value()).toEqual(undefined);
expect(env.pickFor({ test: 'OPTIONAL' })?.value()).toEqual(undefined);
});
});

describe('pickForOrThrow', () => {
it('returns values', () => {
expect(env.pickForOrThrow({ test: 'EMAIL' }).value()).toBe(envStub.EMAIL);
expect(env.pickForOrThrow({ test: 'INTEGER' }).value()).toBe(envStub.INTEGER);
});

it('throws errors', () => {
expect(() => env.pickForOrThrow({ test: 'EMPTY_STRING' })).toThrowError();
expect(() => env.pickForOrThrow({ test: 'SPACE_STRING' })).toThrowError();
expect(() => env.pickForOrThrow({ test: 'OPTIONAL' })).toThrowError();

expect(() => env.pickForOrThrow({ production: 'EMAIL' })).toThrowError();
expect(() => env.pickForOrThrow({ production: 'production' })).toThrowError();
});
});

describe('getFor', () => {
it('returns values', () => {
expect(env.getFor({ test: 'EMAIL' })).toBe(envStub.EMAIL);
expect(env.getFor({ test: 'INTEGER' })).toBe(envStub.INTEGER);

expect(env.getFor({ production: 'EMAIL' })).toEqual(undefined);
expect(env.getFor({ production: 'INTEGER' })).toEqual(undefined);

expect(env.getFor({ test: 'EMPTY_STRING' })).toEqual(envStub.EMPTY_STRING);
expect(env.getFor({ test: 'SPACE_STRING' })).toEqual(envStub.SPACE_STRING);
expect(env.getFor({ test: 'OPTIONAL' })).toEqual(undefined);
});
});

describe('getForOrThrow', () => {
it('returns values', () => {
expect(env.getForOrThrow({ test: 'EMAIL' })).toBe(envStub.EMAIL);
expect(env.getForOrThrow({ test: 'INTEGER' })).toBe(envStub.INTEGER);
expect(env.getForOrThrow({ test: 'EMPTY_STRING' })).toEqual(envStub.EMPTY_STRING);
expect(env.getForOrThrow({ test: 'SPACE_STRING' })).toEqual(envStub.SPACE_STRING);
});

it('throws errors', () => {
expect(() => env.getForOrThrow({ test: 'OPTIONAL' })).toThrowError();
expect(() => env.getForOrThrow({ production: 'EMAIL' })).toThrowError();
expect(() => env.getForOrThrow({ production: 'production' })).toThrowError();
});
});
});
24 changes: 12 additions & 12 deletions __tests__/unit/env.picker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ describe('EnvPicker', () => {
});

it('doesn`t set default value for environment', () => {
expect(new EnvPicker(undefined as string | undefined, 'test').defaultFor({ production: '123' }).value()).toBe(
undefined,
);
expect(new EnvPicker(undefined as string | undefined, 'test').defaultFor({ development: '123' }).value()).toBe(
undefined,
);
expect(
new EnvPicker(undefined as string | undefined, 'test' as string).defaultFor({ production: '123' }).value(),
).toBe(undefined);
expect(
new EnvPicker(undefined as string | undefined, 'test' as string).defaultFor({ development: '123' }).value(),
).toBe(undefined);

expect(new EnvPicker(false as boolean | undefined, 'test').defaultFor({ test: true }).value()).toBe(false);
expect(new EnvPicker('', 'test').defaultFor({ test: '123' }).value()).toBe('');
Expand All @@ -42,18 +42,18 @@ describe('EnvPicker', () => {
});
});

describe('mapIfExist', () => {
describe('mapIfExists', () => {
it('maps state if it exists', () => {
expect(new EnvPicker('123' as string | undefined, 'test').mapIfExist(Number).value()).toBe(123);
expect(new EnvPicker(undefined as string | undefined, 'test').default('123').mapIfExist(Number).value()).toBe(
expect(new EnvPicker('123' as string | undefined, 'test').mapIfExists(Number).value()).toBe(123);
expect(new EnvPicker(undefined as string | undefined, 'test').default('123').mapIfExists(Number).value()).toBe(
123,
);
expect(new EnvPicker('false', 'test').default('123').mapIfExist(JSON.parse).value()).toBe(false);
expect(new EnvPicker('false', 'test').default('123').mapIfExists(JSON.parse).value()).toBe(false);

expect(new EnvPicker(undefined as string | undefined, 'test').mapIfExist((state) => !!state).value()).toBe(
expect(new EnvPicker(undefined as string | undefined, 'test').mapIfExists((state) => !!state).value()).toBe(
undefined,
);
expect(new EnvPicker(null as string | null, 'test').mapIfExist((state) => !!state).value()).toBe(null);
expect(new EnvPicker(null as string | null, 'test').mapIfExists((state) => !!state).value()).toBe(null);
});
});

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@allohamora/config-manager",
"version": "0.3.0",
"version": "0.4.0",
"description": "config manager",
"main": "./dist/index.cjs",
"exports": {
Expand Down
66 changes: 54 additions & 12 deletions src/env/env.manager.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { Manager } from 'src/utils/manager.utils.js';
import { EnvPicker } from './env.picker.js';
import { BaseConfig, AtLeastOne, EnvPicker } from './env.picker.js';

interface Options<C> {
interface Options<E, C> {
load?: () => C;
nodeEnv?: () => string;
nodeEnv?: () => E;
}

export class EnvManager<C extends Record<string, string | undefined>> extends Manager<C> {
export class EnvManager<E extends string, C extends BaseConfig = BaseConfig> extends Manager<C> {
private nodeEnv: string;
private cache = new Map<keyof C, EnvPicker<string | undefined>>();
private cache = new Map<keyof C, EnvPicker<E, string | undefined>>();

constructor({
load = () => process.env as C,
nodeEnv = () => process.env.NODE_ENV ?? 'development',
}: Options<C> = {}) {
nodeEnv = () => (process.env.NODE_ENV ?? 'development') as E,
}: Options<E, C> = {}) {
super({ load });

this.nodeEnv = nodeEnv();
Expand All @@ -29,21 +29,63 @@ export class EnvManager<C extends Record<string, string | undefined>> extends Ma
return value;
}

public pick<K extends keyof C>(key: K): EnvPicker<C[K] | undefined> {
private getKeyOrThrow<K extends keyof C>(envRecord: AtLeastOne<Record<E, K>>): K {
const key = envRecord[this.nodeEnv as E] as string | undefined;

if (!key) {
throw new Error(`key is not found empty in nodeEnv: ${this.nodeEnv}`);
}

return key as K;
}

public pick<K extends keyof C>(key: K): EnvPicker<E, C[K] | undefined> {
if (!this.cache.has(key)) {
this.cache.set(key, new EnvPicker(this.getEnvValue(key), this.nodeEnv));
this.cache.set(key, new EnvPicker(this.getEnvValue(key), this.nodeEnv) as EnvPicker<E, C[K] | undefined>);
}

return this.cache.get(key) as EnvPicker<C[K] | undefined>;
return this.cache.get(key) as EnvPicker<E, C[K] | undefined>;
}

public pickOrThrow<K extends keyof C>(key: K): EnvPicker<C[K]> {
public pickOrThrow<K extends keyof C>(key: K): EnvPicker<E, C[K]> {
const value = this.source[key];

if (value === null || value === undefined || value.trim() === '') {
throw new Error(`key: ${key.toString()} is empty`);
}

return this.pick(key) as EnvPicker<C[K]>;
return this.pick(key) as EnvPicker<E, C[K]>;
}

public pickFor<K extends keyof C>(envRecord: AtLeastOne<Record<E, K>>): undefined | EnvPicker<E, C[K] | undefined> {
const key = envRecord[this.nodeEnv as E] as string | undefined;

if (!key) {
return;
}

return this.pick(key) as EnvPicker<E, C[K] | undefined>;
}

public pickForOrThrow<K extends keyof C>(envRecord: AtLeastOne<Record<E, K>>): EnvPicker<E, C[K]> {
const key = this.getKeyOrThrow(envRecord);

return this.pickOrThrow(key as K);
}

public getFor<K extends keyof C>(envRecord: AtLeastOne<Record<E, K>>) {
const key = envRecord[this.nodeEnv as E] as string | undefined;

if (!key) {
return;
}

return this.get(key as K);
}

public getForOrThrow<K extends keyof C>(envRecord: AtLeastOne<Record<E, K>>) {
const key = this.getKeyOrThrow(envRecord);

return this.getOrThrow(key as K);
}
}
29 changes: 17 additions & 12 deletions src/env/env.picker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type WrappedInEnvPickers<T> = {
[K in keyof T]: EnvPicker<T[K]>;
export type WrappedInEnvPickers<E extends string, T> = {
[K in keyof T]: EnvPicker<E, T[K]>;
};

type MoveOptional<T, R> = T extends null
Expand All @@ -10,22 +10,27 @@ type MoveOptional<T, R> = T extends null
? R | undefined
: R;

export const wrapInEnvPickers = <C extends Record<string, string | undefined>>(config: C, nodeEnv: string) => {
export type BaseConfig = Record<string, string | undefined>;

// original https://stackoverflow.com/a/59987826/15681288
export type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T];

export const wrapInEnvPickers = <E extends string, C extends BaseConfig>(config: C, nodeEnv: E) => {
return Object.keys(config).reduce((result, key) => {
return { ...result, [key]: new EnvPicker(config[key], nodeEnv) };
}, {} as WrappedInEnvPickers<C>);
}, {} as WrappedInEnvPickers<E, C>);
};

export class EnvPicker<S> {
constructor(private state: S, private nodeEnv: string) {}
export class EnvPicker<E extends string, S> {
constructor(private state: S, private nodeEnv: E) {}

public default<NS extends S>(newState: NS): EnvPicker<NS | NonNullable<S>> {
public default<NS extends S>(newState: NS): EnvPicker<E, NS | NonNullable<S>> {
this.state ??= newState;

return this as unknown as EnvPicker<NS | NonNullable<S>>;
return this as unknown as EnvPicker<E, NS | NonNullable<S>>;
}

public defaultFor(envRecord: Record<string, S>) {
public defaultFor(envRecord: AtLeastOne<Record<E, S>>) {
this.state ??= envRecord[this.nodeEnv] as S;

return this;
Expand All @@ -34,15 +39,15 @@ export class EnvPicker<S> {
public map<R>(mapper: (state: S) => R) {
this.state = mapper(this.state) as unknown as S;

return this as unknown as EnvPicker<R>;
return this as unknown as EnvPicker<E, R>;
}

public mapIfExist<R>(mapper: (state: NonNullable<S>) => R) {
public mapIfExists<R>(mapper: (state: NonNullable<S>) => R) {
if (this.state !== null && this.state !== undefined) {
this.state = mapper(this.state as NonNullable<S>) as unknown as S;
}

return this as unknown as EnvPicker<MoveOptional<S, R>>;
return this as unknown as EnvPicker<E, MoveOptional<S, R>>;
}

public value() {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './config/config.manager.js';
export * from './env/env.manager.js';
export { EnvPicker } from './env/env.picker.js';

0 comments on commit a143f21

Please sign in to comment.