From 9d03bc874e0d65416a18ac1b0f3a02af915fbcf0 Mon Sep 17 00:00:00 2001 From: allohamora Date: Mon, 10 Apr 2023 17:19:44 +0300 Subject: [PATCH 1/9] feat: add EnvPicker export --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index ac95aa8..b922925 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './config/config.manager.js'; export * from './env/env.manager.js'; +export { EnvPicker } from './env/env.picker.js'; From 2d1c8e4fb0a7b0d22ff61a23434fdf142e7cf19f Mon Sep 17 00:00:00 2001 From: allohamora Date: Mon, 10 Apr 2023 17:21:10 +0300 Subject: [PATCH 2/9] fix: rename mapIfExist to mapIfExists --- __tests__/unit/env.picker.spec.ts | 12 ++++++------ src/env/env.picker.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/__tests__/unit/env.picker.spec.ts b/__tests__/unit/env.picker.spec.ts index 44f618d..cb52c18 100644 --- a/__tests__/unit/env.picker.spec.ts +++ b/__tests__/unit/env.picker.spec.ts @@ -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); }); }); diff --git a/src/env/env.picker.ts b/src/env/env.picker.ts index 83c5a36..c41973a 100644 --- a/src/env/env.picker.ts +++ b/src/env/env.picker.ts @@ -37,7 +37,7 @@ export class EnvPicker { return this as unknown as EnvPicker; } - public mapIfExist(mapper: (state: NonNullable) => R) { + public mapIfExists(mapper: (state: NonNullable) => R) { if (this.state !== null && this.state !== undefined) { this.state = mapper(this.state as NonNullable) as unknown as S; } From 3673a1da2baeba6a0f462f671fa5ec2f99981a8a Mon Sep 17 00:00:00 2001 From: allohamora Date: Mon, 10 Apr 2023 17:31:58 +0300 Subject: [PATCH 3/9] feat: improve nodeEnv typings --- src/env/env.manager.ts | 24 ++++++++++++------------ src/env/env.picker.ts | 24 +++++++++++++----------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/env/env.manager.ts b/src/env/env.manager.ts index a459e77..7ea6950 100644 --- a/src/env/env.manager.ts +++ b/src/env/env.manager.ts @@ -1,19 +1,19 @@ import { Manager } from 'src/utils/manager.utils.js'; -import { EnvPicker } from './env.picker.js'; +import { BaseConfig, EnvPicker } from './env.picker.js'; -interface Options { +interface Options { load?: () => C; - nodeEnv?: () => string; + nodeEnv?: () => E; } -export class EnvManager> extends Manager { +export class EnvManager extends Manager { private nodeEnv: string; - private cache = new Map>(); + private cache = new Map>(); constructor({ load = () => process.env as C, - nodeEnv = () => process.env.NODE_ENV ?? 'development', - }: Options = {}) { + nodeEnv = () => (process.env.NODE_ENV ?? 'development') as E, + }: Options = {}) { super({ load }); this.nodeEnv = nodeEnv(); @@ -29,21 +29,21 @@ export class EnvManager> extends Ma return value; } - public pick(key: K): EnvPicker { + public pick(key: K): EnvPicker { 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); } - return this.cache.get(key) as EnvPicker; + return this.cache.get(key) as EnvPicker; } - public pickOrThrow(key: K): EnvPicker { + public pickOrThrow(key: K): EnvPicker { 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; + return this.pick(key) as EnvPicker; } } diff --git a/src/env/env.picker.ts b/src/env/env.picker.ts index c41973a..c2cd8dc 100644 --- a/src/env/env.picker.ts +++ b/src/env/env.picker.ts @@ -1,5 +1,5 @@ -export type WrappedInEnvPickers = { - [K in keyof T]: EnvPicker; +export type WrappedInEnvPickers = { + [K in keyof T]: EnvPicker; }; type MoveOptional = T extends null @@ -10,22 +10,24 @@ type MoveOptional = T extends null ? R | undefined : R; -export const wrapInEnvPickers = >(config: C, nodeEnv: string) => { +export type BaseConfig = Record; + +export const wrapInEnvPickers = (config: C, nodeEnv: E) => { return Object.keys(config).reduce((result, key) => { return { ...result, [key]: new EnvPicker(config[key], nodeEnv) }; - }, {} as WrappedInEnvPickers); + }, {} as WrappedInEnvPickers); }; -export class EnvPicker { - constructor(private state: S, private nodeEnv: string) {} +export class EnvPicker { + constructor(private state: S, private nodeEnv: E) {} - public default(newState: NS): EnvPicker> { + public default(newState: NS): EnvPicker> { this.state ??= newState; - return this as unknown as EnvPicker>; + return this as unknown as EnvPicker>; } - public defaultFor(envRecord: Record) { + public defaultFor(envRecord: Partial>) { this.state ??= envRecord[this.nodeEnv] as S; return this; @@ -34,7 +36,7 @@ export class EnvPicker { public map(mapper: (state: S) => R) { this.state = mapper(this.state) as unknown as S; - return this as unknown as EnvPicker; + return this as unknown as EnvPicker; } public mapIfExists(mapper: (state: NonNullable) => R) { @@ -42,7 +44,7 @@ export class EnvPicker { this.state = mapper(this.state as NonNullable) as unknown as S; } - return this as unknown as EnvPicker>; + return this as unknown as EnvPicker>; } public value() { From 89b2759e785279dac8cadbef28834a52a2e35525 Mon Sep 17 00:00:00 2001 From: allohamora Date: Mon, 10 Apr 2023 17:33:24 +0300 Subject: [PATCH 4/9] test: fix nodeEnv wrong type errors --- __tests__/unit/env.picker.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/__tests__/unit/env.picker.spec.ts b/__tests__/unit/env.picker.spec.ts index cb52c18..89c1df6 100644 --- a/__tests__/unit/env.picker.spec.ts +++ b/__tests__/unit/env.picker.spec.ts @@ -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(''); From 689b5baabe7acf9c88ae25d668d60deca31b9649 Mon Sep 17 00:00:00 2001 From: allohamora Date: Mon, 10 Apr 2023 17:51:40 +0300 Subject: [PATCH 5/9] feat: add pickFor and pickForOrThrow --- __tests__/unit/env.manager.spec.ts | 32 +++++++++++++++++++++++++++++- src/env/env.manager.ts | 20 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/__tests__/unit/env.manager.spec.ts b/__tests__/unit/env.manager.spec.ts index 5da825c..066532f 100644 --- a/__tests__/unit/env.manager.spec.ts +++ b/__tests__/unit/env.manager.spec.ts @@ -9,7 +9,7 @@ describe('EnvManager', () => { INTEGER: '123', } as Record; - const env = new EnvManager({ load: () => envStub }); + const env = new EnvManager({ load: () => envStub, nodeEnv: () => 'test' as 'test' | 'production' }); describe('pick', () => { it('returns values', () => { @@ -34,4 +34,34 @@ describe('EnvManager', () => { expect(() => env.pickOrThrow('OPTIONAL')).toThrowError(); }); }); + + describe('pickOrThrow', () => { + 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('pickOrThrow', () => { + 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(); + }); + }); }); diff --git a/src/env/env.manager.ts b/src/env/env.manager.ts index 7ea6950..1728901 100644 --- a/src/env/env.manager.ts +++ b/src/env/env.manager.ts @@ -46,4 +46,24 @@ export class EnvManager ext return this.pick(key) as EnvPicker; } + + public pickFor(envRecord: Partial>): undefined | EnvPicker { + const key = envRecord[this.nodeEnv as E] as string | undefined; + + if (!key) { + return; + } + + return this.pick(key) as EnvPicker; + } + + public pickForOrThrow(envRecord: Partial>): EnvPicker { + 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 this.pickOrThrow(key as K); + } } From cd8d7c15097f91ce8794f9aafb47a01023bd1726 Mon Sep 17 00:00:00 2001 From: allohamora Date: Mon, 10 Apr 2023 17:54:31 +0300 Subject: [PATCH 6/9] test: fix test naming --- __tests__/unit/env.manager.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/unit/env.manager.spec.ts b/__tests__/unit/env.manager.spec.ts index 066532f..4f96e62 100644 --- a/__tests__/unit/env.manager.spec.ts +++ b/__tests__/unit/env.manager.spec.ts @@ -35,7 +35,7 @@ describe('EnvManager', () => { }); }); - describe('pickOrThrow', () => { + describe('pickFor', () => { it('returns values', () => { expect(env.pickFor({ test: 'EMAIL' })?.value()).toBe(envStub.EMAIL); expect(env.pickFor({ test: 'INTEGER' })?.value()).toBe(envStub.INTEGER); @@ -49,7 +49,7 @@ describe('EnvManager', () => { }); }); - describe('pickOrThrow', () => { + describe('pickForOrThrow', () => { it('returns values', () => { expect(env.pickForOrThrow({ test: 'EMAIL' }).value()).toBe(envStub.EMAIL); expect(env.pickForOrThrow({ test: 'INTEGER' }).value()).toBe(envStub.INTEGER); From 51dbf8c2c98af632758ce2dcd9e866d184207066 Mon Sep 17 00:00:00 2001 From: allohamora Date: Mon, 10 Apr 2023 17:57:36 +0300 Subject: [PATCH 7/9] feat: add getFor and getForOrThrow --- __tests__/unit/env.manager.spec.ts | 29 +++++++++++++++++++++++++++++ src/env/env.manager.ts | 26 ++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/__tests__/unit/env.manager.spec.ts b/__tests__/unit/env.manager.spec.ts index 4f96e62..777a41f 100644 --- a/__tests__/unit/env.manager.spec.ts +++ b/__tests__/unit/env.manager.spec.ts @@ -64,4 +64,33 @@ describe('EnvManager', () => { 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(); + }); + }); }); diff --git a/src/env/env.manager.ts b/src/env/env.manager.ts index 1728901..66572ca 100644 --- a/src/env/env.manager.ts +++ b/src/env/env.manager.ts @@ -29,6 +29,16 @@ export class EnvManager ext return value; } + private getKeyOrThrow(envRecord: Partial>): 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(key: K): EnvPicker { if (!this.cache.has(key)) { this.cache.set(key, new EnvPicker(this.getEnvValue(key), this.nodeEnv) as EnvPicker); @@ -58,12 +68,24 @@ export class EnvManager ext } public pickForOrThrow(envRecord: Partial>): EnvPicker { + const key = this.getKeyOrThrow(envRecord); + + return this.pickOrThrow(key as K); + } + + public getFor(envRecord: Partial>) { 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; } - return this.pickOrThrow(key as K); + return this.get(key as K); + } + + public getForOrThrow(envRecord: Partial>) { + const key = this.getKeyOrThrow(envRecord); + + return this.getOrThrow(key as K); } } From 1b43a5f01bf231d7f4935d62b0d7581b080f94d1 Mon Sep 17 00:00:00 2001 From: allohamora Date: Mon, 10 Apr 2023 18:12:05 +0300 Subject: [PATCH 8/9] feat: add atLeastOne to for methods --- src/env/env.manager.ts | 12 ++++++------ src/env/env.picker.ts | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/env/env.manager.ts b/src/env/env.manager.ts index 66572ca..61ed0a7 100644 --- a/src/env/env.manager.ts +++ b/src/env/env.manager.ts @@ -1,5 +1,5 @@ import { Manager } from 'src/utils/manager.utils.js'; -import { BaseConfig, EnvPicker } from './env.picker.js'; +import { BaseConfig, AtLeastOne, EnvPicker } from './env.picker.js'; interface Options { load?: () => C; @@ -29,7 +29,7 @@ export class EnvManager ext return value; } - private getKeyOrThrow(envRecord: Partial>): K { + private getKeyOrThrow(envRecord: AtLeastOne>): K { const key = envRecord[this.nodeEnv as E] as string | undefined; if (!key) { @@ -57,7 +57,7 @@ export class EnvManager ext return this.pick(key) as EnvPicker; } - public pickFor(envRecord: Partial>): undefined | EnvPicker { + public pickFor(envRecord: AtLeastOne>): undefined | EnvPicker { const key = envRecord[this.nodeEnv as E] as string | undefined; if (!key) { @@ -67,13 +67,13 @@ export class EnvManager ext return this.pick(key) as EnvPicker; } - public pickForOrThrow(envRecord: Partial>): EnvPicker { + public pickForOrThrow(envRecord: AtLeastOne>): EnvPicker { const key = this.getKeyOrThrow(envRecord); return this.pickOrThrow(key as K); } - public getFor(envRecord: Partial>) { + public getFor(envRecord: AtLeastOne>) { const key = envRecord[this.nodeEnv as E] as string | undefined; if (!key) { @@ -83,7 +83,7 @@ export class EnvManager ext return this.get(key as K); } - public getForOrThrow(envRecord: Partial>) { + public getForOrThrow(envRecord: AtLeastOne>) { const key = this.getKeyOrThrow(envRecord); return this.getOrThrow(key as K); diff --git a/src/env/env.picker.ts b/src/env/env.picker.ts index c2cd8dc..e2b0b15 100644 --- a/src/env/env.picker.ts +++ b/src/env/env.picker.ts @@ -12,6 +12,9 @@ type MoveOptional = T extends null export type BaseConfig = Record; +// original https://stackoverflow.com/a/59987826/15681288 +export type AtLeastOne = { [K in keyof T]: Pick }[keyof T]; + export const wrapInEnvPickers = (config: C, nodeEnv: E) => { return Object.keys(config).reduce((result, key) => { return { ...result, [key]: new EnvPicker(config[key], nodeEnv) }; @@ -27,7 +30,7 @@ export class EnvPicker { return this as unknown as EnvPicker>; } - public defaultFor(envRecord: Partial>) { + public defaultFor(envRecord: AtLeastOne>) { this.state ??= envRecord[this.nodeEnv] as S; return this; From 724307d16e51da911b2ecfb76ef3542bbf05e148 Mon Sep 17 00:00:00 2001 From: allohamora Date: Mon, 10 Apr 2023 18:13:00 +0300 Subject: [PATCH 9/9] chore(release): 0.4.0 --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24db035..e737d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 178667f..ca721a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@allohamora/config-manager", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@allohamora/config-manager", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "devDependencies": { "@commitlint/cli": "^17.5.1", diff --git a/package.json b/package.json index fe003e6..057ec1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@allohamora/config-manager", - "version": "0.3.0", + "version": "0.4.0", "description": "config manager", "main": "./dist/index.cjs", "exports": {