From 6336050065b25d7a464ab18357164d412905bae1 Mon Sep 17 00:00:00 2001 From: Marco Martins Date: Fri, 14 May 2021 19:24:15 +0200 Subject: [PATCH] Publish environment-decoder --- .github/workflows/main.yml | 17 +++++++ .github/workflows/publish.yml | 21 ++++++++ .github/workflows/pull_request.yml | 17 +++++++ .gitignore | 9 +--- LICENSE.md | 19 +++++++ README.md | 81 +++++++++++++++++++++++++++++- package.json | 20 ++++++-- src/debug.ts | 9 ---- src/index.ts | 28 ++++++++--- src/types.ts | 23 --------- tsconfig.json | 8 +-- yarn.lock | 8 +-- 12 files changed, 196 insertions(+), 64 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/pull_request.yml create mode 100644 LICENSE.md delete mode 100644 src/debug.ts delete mode 100644 src/types.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5d20257 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,17 @@ +name: Main + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + name: Build + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 14 + - run: yarn --frozen-lockfile + - run: yarn build \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e86ce01 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,21 @@ +name: Publish + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + name: Publish to NPM + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 14 + registry-url: https://registry.npmjs.org/ + - run: yarn --frozen-lockfile + - run: yarn build + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..77d5fe9 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,17 @@ +name: Pull Request + +on: + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + name: Build + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 14 + - run: yarn --frozen-lockfile + - run: yarn build \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3231922..d7d6d30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,8 @@ .idea/ -.env -*.env.json - node_modules/ dist/ -build/ .npmrc -*.log - -.nyc_output -coverage \ No newline at end of file +*.log \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ca826e8 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2021 Marco Daniel Martins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index bc8b0eb..0888d71 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,82 @@ # environment decoder -> A decoder for your `process.env` into a typed object +> A decoder for the `process.env` -`environment-decoder` allows you to define an "interface" for the environment variables you need for your application. \ No newline at end of file +`environment-decoder` allows you to define an "interface" for the environment variables you need for your application +and validates them. + +This library took a big inspiration from typescript-json-decoder, the main differences are that `process.env` is just a +record type (no need to nested decode) +and since all `process.env` values default to `string` in `environment-decoder` you can cast the value to the desired +type ([see usage](#usage)). + +## Idea (read struggle) + +For most applications we have to **trust** that the environment flags are set **correctly** and often we see cases like: + +```typescript +server.listen(Number(process.env.PORT || 8080)) + +// or + +// we are certain this is set by someone +if (process.env.FEATURE_FLAG!) { + // ... +} + +// or + +// sure, this is fine +const baseURL = process.env.BASE_URL || '' +fetch(`${baseURL}/api`) + .then(response => response.json()) +``` + +This creates a lot of uncertainty: are the environment flags set? what are their "real" types? + +## Usage + +With `environment-decoder` you define a decoder for your environment variable names, and their corresponded types. + +Since all environment variables are set as `string`, the decoder type primitives are written with `asType` as we will be +casting (and validating) each variable. + +```typescript +import {environmentDecoder, asBoolean, asString, asNumber} from 'environment-decoder' + +const myEnv = environmentDecoder({ + BASE_URL: asString, + PORT: asNumber, + FEATURE_FLAG: asBoolean +}) + +console.log(myEnv.BASE_PATH) // will output the process.env.BASE_PATH value +``` + +You can also use the output type created by `environmentDecoder` with `DecodeType`: + +```typescript +import {environmentDecoder, asBoolean, asString, asNumber, DecodeType} from 'environment-decoder' + +const myEnv = environmentDecoder({ + BASE_URL: asString, + PORT: asNumber, + FEATURE_FLAG: asBoolean +}) + +type MyEnvType = DecodeType + +const funWithEnv = (envParam: MyEnvType) => { + console.log(envParam.FEATURE_FLAG) +} +```` + +## Notes + +`environment-decoder` will throw exceptions for: + +* the environment variables are not set (will list all missing variables) +* the environment variable cannot be cast to type (ex: using `asNumber` on `abcde`) + +It would be recommended to use `environmentDecoder` at the entry point of the application in order to catch errors as +early as possible. \ No newline at end of file diff --git a/package.json b/package.json index 4f22305..9619cd8 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,25 @@ { "name": "environment-decoder", - "version": "0.0.1", + "description": "A decoder for the process.env", + "author": "Marco Daniel Martins ", + "version": "1.0.0", + "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", - "license": "MIT", + "keywords": [ + "environment", + "decoder", + "typescript" + ], + "repository": { + "type": "git", + "url": "https://github.com/MarcoDaniels/environment-decoder" + }, "scripts": { - "build": "tsc", - "debug": "yarn build && node dist/debug.js" + "build": "tsc" }, "devDependencies": { - "@types/node": "15.0.2", + "@types/node": "15.0.3", "typescript": "4.2.4" } } diff --git a/src/debug.ts b/src/debug.ts deleted file mode 100644 index 71f34b6..0000000 --- a/src/debug.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {environmentDecoder, asString, asNumber} from "./" - -const env = environmentDecoder({ - USER: asString, - WHAT: asNumber, - WHY: asString -}) - -console.log(env) diff --git a/src/index.ts b/src/index.ts index aa6c4cb..711c2e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,22 @@ -import {DecodeFnType, JSType, DecodeType} from './types' +type JSTypePrimitive = string | boolean | number | null | undefined +type JSTypeObject = { [key: string]: JSType } +type JSType = JSTypePrimitive | JSTypeObject -export {DecodeType} from './types' +type DecoderTypePrimitive = string +type DecoderTypeObject = { [key: string]: unknown } +type DecoderType = DecoderTypePrimitive | DecoderTypeObject + +type DecodeFnType = (input: JSType) => T + +type DecodeDecoder = [decoder] extends [DecoderTypePrimitive] + ? decoder + : { [key in keyof decoder]: DecodeType } + +export type DecodeType = (decoder extends DecodeFnType + ? [DecodeType] + : decoder extends DecoderType + ? [DecodeDecoder] + : [decoder])[0] export const asString: DecodeFnType = (s: JSType) => { if (typeof s !== 'string') { @@ -35,24 +51,22 @@ export const asBoolean: DecodeFnType = (b: JSType) => { return b } -const decode = (decoder: D): DecodeFnType => decoder as any - export const environmentDecoder = (schemaType: S): DecodeType => { const environment = process.env const schema = Object.entries(schemaType) const missing = schema.filter(([key]) => !environment.hasOwnProperty(key)).map(([key]) => key) if (missing.length) { - throw (`Missing environment variables: \n${missing.join(`\n`)}\n`) + throw `Missing environment variables: \n${missing.join(`\n`)}\n` } return schema .map(([key, decoder]: [string, any]) => { try { const value = environment[key] - return [key, decode(decoder)(value)] + return [key, decoder(value)] } catch (message) { - throw (`Error for environment "${key}": ${message}\n`) + throw `Error for environment "${key}": ${message}\n` } }) .reduce((acc, [key, value]) => ({...acc, [key]: value}), {}) diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index f4f0c9b..0000000 --- a/src/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type JSTypePrimitive = string | boolean | number | null | undefined -export type JSTypeObject = { [key: string]: JSType } -export type JSType = JSTypePrimitive | JSTypeObject - -export type DecoderTypePrimitive = string -export type DecoderTypeObject = { [key: string]: unknown } -export type DecoderType = DecoderTypePrimitive | DecoderTypeObject - -export type DecodeFnType = (input: JSType) => T - -export type DecodeType = - (decoder extends DecodeFnType ? - [DecodeType] : decoder extends DecoderType ? - [DecodeDecoder] : [decoder] - )[0] - -export type DecodeDecoder = - [decoder] extends [DecoderTypePrimitive] ? - decoder : - [decoder] extends [[infer decoderA, infer decoderB]] ? - [DecodeType, DecodeType] : - { [key in keyof decoder]: DecodeType } - diff --git a/tsconfig.json b/tsconfig.json index 296cdf8..5427d8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,11 +3,7 @@ "baseUrl": ".", "rootDir": "src", "outDir": "dist", - "target": "ES2020", - "lib": [ - "ES2020", - "dom" - ], + "target": "ES5", "module": "commonjs", "moduleResolution": "node", "sourceMap": false, @@ -27,5 +23,5 @@ "forceConsistentCasingInFileNames": true, "skipLibCheck": true }, - "exclude": ["node_modules", "coverage", "dist"] + "exclude": ["node_modules", "dist"] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e4baa14..3917d8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@15.0.2": - version "15.0.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.2.tgz#51e9c0920d1b45936ea04341aa3e2e58d339fb67" - integrity sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA== +"@types/node@15.0.3": + version "15.0.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.3.tgz#ee09fcaac513576474c327da5818d421b98db88a" + integrity sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ== typescript@4.2.4: version "4.2.4"