diff --git a/CHANGELOG.md b/CHANGELOG.md index 6240974..873809c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 5.1.0 (09/09/19) +* Ability to add custom accessors in PR #72 (thanks @todofixthis) +* Improved TypeScript tests +* Fixed warning generated by husky + ## 5.0.0 (14/06/19) * Return values from `asArray()` are now more intuitive & consitent * `asUrlString()` and `asUrlObject`now use the built-in `URL` class in Node.js diff --git a/README.md b/README.md index 90a5426..69e0157 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,34 @@ let validEmail = env.get('ADMIN').checkEmail() let invalidEmail = env.get('ADMIN').checkEmail('github.com') ``` +This feature is also available for TypeScript users. The `ExtensionFn` type is +expoed to help in the creation of these new accessors. + +```ts +import { from, ExtensionFn, EnvVarError } from 'env-var' + +// Environment variable that we will use for this example: +process.env.ADMIN = 'admin@example.com' + +const checkEmail: ExtensionFn = (value) => { + const split = String(value).split('@') + + // Validating email addresses is hard. + if (split.length !== 2) { + throw new Error('must contain exactly one "@"') + } + + return value +} + +const env = from(process.env, { + checkEmail +}) + +// Returns the email string if it's valid, otherwise it will throw +env.get('ADMIN').checkEmail() +``` + ### get([varname, [default]]) You can call this function 3 different ways: diff --git a/env-var.d.ts b/env-var.d.ts index 6cdba8a..00dee75 100644 --- a/env-var.d.ts +++ b/env-var.d.ts @@ -207,9 +207,9 @@ interface IOptionalVariable { asEnum: (validValues: string[]) => string|undefined; } -declare class EnvVarError extends Error {} +export class EnvVarError extends Error {} -interface IEnv { +interface IEnv { /** * Returns an object containing all current environment variables */ @@ -218,18 +218,21 @@ interface IEnv { /** * Gets an environment variable that is possibly not set to a value */ - get (varName: string): IOptionalVariable; + get (varName: string): OptionalVariable; /** * Gets an environment variable, using the default value if it is not already set */ - get (varName: string, defaultValue: string): IPresentVariable; + get (varName: string, defaultValue: string): PresentVariable; /** * Returns a new env-var instance, where the given object is used for the environment variable mapping. * Use this when writing unit tests or in environments outside node.js. */ - from(values: NodeJS.ProcessEnv): IEnv; + from(values: NodeJS.ProcessEnv, extensions?: T): IEnv< + IPresentVariable & Record ReturnType>, + IOptionalVariable & Record ReturnType|undefined> + >; /** * This is the error type used to represent error returned by this module. @@ -238,5 +241,16 @@ interface IEnv { EnvVarError: EnvVarError } -declare const env: IEnv; -export = env; +export type Extensions = { + [key: string]: ExtensionFn +} +export type RaiseErrorFn = (error: string) => void +export type ExtensionFn = (value: string, ...args: any[]) => T + +export function get(): {[varName: string]: string} +export function get(varName: string): IOptionalVariable; +export function get(varName: string, defaultValue: string): IPresentVariable; +export function from(values: NodeJS.ProcessEnv, extensions?: T): IEnv< + IPresentVariable & Record ReturnType>, + IOptionalVariable & Record ReturnType|undefined> +>; diff --git a/package.json b/package.json index 017fdf7..d52f91e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "env-var", - "version": "5.0.0", + "version": "5.1.0", "description": "Verification, sanatization, and type coercion for environment variables in Node.js", "main": "env-var.js", "typings": "env-var.d.ts", @@ -9,10 +9,14 @@ "coverage": "nyc mocha test/ && nyc report --reporter=lcov", "check-coverage": "nyc check-coverage --statements 100 --branches 100 --functions 100 --lines 100", "unit": "mocha test/", - "precommit": "npm run lint && npm run unit", "lint": "standard example/*.js *.js \"lib/**/*.js\" test/*.js --fix", "test": "npm run unit && npm run coverage && npm run check-coverage && npm run lint && npm run ts-verify", - "ts-verify": "tsc test/index.ts --noEmit --target es5" + "ts-verify": "tsc && node test/types/index.js" + }, + "husky": { + "hooks": { + "pre-commit": "npm run lint && npm run unit" + } }, "repository": { "type": "git", @@ -59,6 +63,6 @@ "typescript": "~3.1.3" }, "engines": { - "node": ">=6.0" + "node": ">=8" } } diff --git a/test/index.ts b/test/index.ts deleted file mode 100644 index 348b3f6..0000000 --- a/test/index.ts +++ /dev/null @@ -1,88 +0,0 @@ - -import * as env from '../env-var'; -import * as assert from 'assert'; -import * as url from 'url'; - -function test () { - // BASE 64 - process.env.BASE64 = 'aGVsbG8=' - assert.equal( - env.get('BASE64').required().convertFromBase64().asString(), - process.env.STRING - ) - - // STRINGS - process.env.STRING = 'string' - assert.equal(env.get('STRING').required().asString(), process.env.STRING) - assert.equal(env.get('SOME_STRING').asString(), process.env.STRING) - - - // INTEGERS - process.env.INT = '0' - process.env.INT_POSITIVE = '2' - process.env.INT_NEGATIVE = '-2' - - const intVal = env.get('INT').required().asInt(); - const intValPositive = env.get('INT_POSITIVE').asIntPositive(); - const intValNegative = env.get('INT_NEGATIVE').asIntNegative(); - - assert.equal(intVal, -2) - assert.equal(intValPositive, 2) - assert.equal(intValNegative, -2) - - - // FLOATs - process.env.FLOAT = '13.234234' - process.env.FLOAT_POSITIVE = '13.234234' - process.env.FLOAT_NEGATIVE = '-13.234234' - - const floatVal = env.get('SOME_FLOAT').required().asFloat(); - const floatValPositive = env.get('SOME_FLOAT').asFloatPositive(); - const floatValNegative = env.get('SOME_FLOAT').asFloatNegative(); - - assert.equal(floatVal, 13.234234) - assert.equal(floatValPositive, 13.234234) - assert.equal(floatValNegative, -13.234234) - - - // BOOLEAN - process.env.BOOL = '0' - process.env.BOOL_STRICT = 'true' - - const asBool = env.get('BOOL').required().asBool(); - const asBoolStrict = env.get('BOOL_STRICT').asBoolStrict(); - - assert.equal(asBool, false) - assert.equal(asBoolStrict, true) - - - // ARRAY - process.env.ARRAY_COMMA = '1,2,3' - process.env.ARRAY_HYPHEN = '1,2,3' - - assert.equal(env.get('ARRAY_COMMA').asArray(), ['1', '2', '3']) - assert.equal(env.get('ARRAY_HYPHEN', '-').asArray(), ['1', '2', '3']) - - - // JSON - process.env.JSON_OBJECT = JSON.stringify({ test: 'testing' }) - process.env.JSON_ARRAY = JSON.stringify([1,2,3]) - - assert.equal(env.get('JSON_OBJECT').asJson(), { test: 'testing' }) - assert.equal(env.get('JSON_OBJECT').asJsonObject(), { test: 'testing' }) - assert.equal(env.get('JSON_ARRAY').asJsonArray(), [1,2,3]) - - - // URLS - process.env.URL_STRING = 'http://google.com' - - assert.equal(env.get('URL_STRING').asUrlObject(), url.parse(process.env.URL_STRING)) - assert.equal(env.get('URL_STRING').asUrlString(), 'http://google.com') - - // FROM - const newEnv = env.from(process.env) - assert.equal(newEnv.get('STRING').required().asString(), process.env.STRING) - assert.equal(newEnv.get('SOME_STRING').asString(), process.env.STRING) -} - -export default test; diff --git a/test/types/.gitignore b/test/types/.gitignore new file mode 100644 index 0000000..4e57eef --- /dev/null +++ b/test/types/.gitignore @@ -0,0 +1,2 @@ +*.js +*.js.map diff --git a/test/types/index.ts b/test/types/index.ts new file mode 100644 index 0000000..a6c66a7 --- /dev/null +++ b/test/types/index.ts @@ -0,0 +1,125 @@ + +import * as env from '../../'; +import * as assert from 'assert'; +import * as url from 'url'; + +function test () { + // BASE 64 + process.env.BASE64 = 'aGVsbG8=' + assert.equal( + env.get('BASE64').required().convertFromBase64().asString(), + 'hello' + ) + + // STRINGS + process.env.STRING = 'string' + assert.equal(env.get('STRING').required().asString(), process.env.STRING) + assert.equal(env.get('SOME_STRING').asString(), undefined) + + + // INTEGERS + process.env.INT = '0' + process.env.INT_POSITIVE = '2' + process.env.INT_NEGATIVE = '-2' + + const intVal = env.get('INT').required().asInt(); + const intValPositive = env.get('INT_POSITIVE').asIntPositive(); + const intValNegative = env.get('INT_NEGATIVE').asIntNegative(); + + assert.equal(intVal, 0) + assert.equal(intValPositive, 2) + assert.equal(intValNegative, -2) + + + // FLOATs + process.env.FLOAT = '13.234234' + process.env.FLOAT_POSITIVE = '13.234234' + process.env.FLOAT_NEGATIVE = '-13.234234' + + const floatVal = env.get('FLOAT').required().asFloat(); + const floatValPositive = env.get('FLOAT_POSITIVE').asFloatPositive(); + const floatValNegative = env.get('FLOAT_NEGATIVE').asFloatNegative(); + + assert.equal(floatVal, 13.234234) + assert.equal(floatValPositive, 13.234234) + assert.equal(floatValNegative, -13.234234) + + + // BOOLEAN + process.env.BOOL = '0' + process.env.BOOL_STRICT = 'true' + + const asBool = env.get('BOOL').required().asBool(); + const asBoolStrict = env.get('BOOL_STRICT').asBoolStrict(); + + assert.equal(asBool, false) + assert.equal(asBoolStrict, true) + + + // ARRAY + process.env.ARRAY_COMMA = '1,2,3' + process.env.ARRAY_HYPHEN = '1-2-3' + + assert.deepEqual(env.get('ARRAY_COMMA').asArray(), ['1', '2', '3']) + assert.deepEqual(env.get('ARRAY_HYPHEN').asArray('-'), ['1', '2', '3']) + + + // JSON + process.env.JSON_OBJECT = JSON.stringify({ test: 'testing' }) + process.env.JSON_ARRAY = JSON.stringify([1,2,3]) + + assert.deepEqual(env.get('JSON_OBJECT').asJson(), { test: 'testing' }) + assert.deepEqual(env.get('JSON_OBJECT').asJsonObject(), { test: 'testing' }) + assert.deepEqual(env.get('JSON_ARRAY').asJsonArray(), [1,2,3]) + + + // URLS + process.env.URL_STRING = 'http://google.com/' + + assert.deepEqual(env.get('URL_STRING').asUrlObject(), new url.URL(process.env.URL_STRING)) + assert.equal(env.get('URL_STRING').asUrlString(), 'http://google.com/') + + // FROM + const newEnv = env.from(process.env) + assert.equal(newEnv.get('STRING').required().asString(), process.env.STRING) + assert.equal(newEnv.get('SOME_STRING').asString(), undefined) + + // FROM WITH EXTENSIONS + interface EmailParts { + username: string + domain: string + } + + const asEmailComponents: env.ExtensionFn = (value) => { + const parts = value.split('@') + + if (parts.length != 2) { + throw new env.EnvVarError('probably not an email') + } else { + return { + username: parts[0], + domain: parts[1] + } + } + } + + const extrasEnv = env.from({ + EMAIL: 'hello@example.com' + }, { + asEmailComponents + }) + + assert.equal( + extrasEnv.get('MISSING_EMAIL').asEmailComponents(), + undefined + ) + assert.deepEqual( + extrasEnv.get('EMAIL').asEmailComponents(), + { + domain: 'example.com', + username: 'hello' + } + ) +} + +test() diff --git a/tsconfig.json b/tsconfig.json index c35b73d..4728463 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,8 @@ "noImplicitAny": true, "target": "es5", "moduleResolution": "node", - "skipLibCheck": true - } + "skipLibCheck": true, + "strict": true + }, + "files": ["test/types/index.ts"] }