Skip to content

Commit

Permalink
Merge pull request #44 from af/strict-proxy
Browse files Browse the repository at this point in the history
Add strict-mode proxy wrapper
  • Loading branch information
af authored Aug 14, 2017
2 parents c7f46bb + beef35e commit 4680535
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 18 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ positional arguments:
* `environment` - An object containing your env vars (eg. `process.env`)
* `validators` - An object that specifies the format of required vars.
* `options` - An (optional) object, which supports the following keys:
* `strict` - (default: `false`) If true, the output of `cleanEnv` will *only*
contain the env vars that were specified in the `validators` argument.
* `strict` - (default: `false`) Enable more rigorous behavior. See "Strict Mode" below
* `reporter` - Pass in a function to override the default error handling and
console output. See `lib/reporter.js` for the default implementation.
* `transformer` - A function used to transform the cleaned environment object
Expand Down Expand Up @@ -135,6 +134,16 @@ const env = cleanEnv(process.env, myValidators, {
```


## Strict mode

By passing the `{ strict: true }` option, envalid gives you extra tight guarantees
about the cleaned env object:

* The env object will *only* contain the env vars that were specified by your `validators`.
* Any attempt to access an invalid/missing property on the env object will cause a thrown error.
* Any attempt to mutate the cleaned env object will cause a thrown error.


## `.env` File Support

Envalid wraps the very handy [dotenv](https://www.npmjs.com/package/dotenv) package,
Expand Down
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ function cleanEnv(inputEnv, specs = {}, options = {}) {
const reporter = options.reporter || require('./lib/reporter')
reporter({ errors, env: output })

if (options.strict) output = require('./lib/strictProxy')(output)

return Object.freeze(output)
}

Expand Down
25 changes: 25 additions & 0 deletions lib/strictProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Wrap the environment object with a Proxy that throws when:
* a) trying to mutate an env var
* b) trying to access an invalid (unset) env var
*
* @return {Object} - Proxied environment object with get/set traps
*/
module.exports = envObj => new Proxy(envObj, {
get(target, name) {
// These checks are needed because calling console.log on a
// proxy that throws crashes the entire process. This whitelists
// the necessary properties for `console.log(envObj)` to work.
if (['inspect', Symbol.toStringTag].includes(name)) return envObj[name]
if (name.toString() === 'Symbol(util.inspect.custom)') return envObj[name]

const varExists = envObj.hasOwnProperty(name)
if (!varExists) throw new Error(`[envalid] Environment var not found: ${name}`)

return envObj[name]
},

set(name) {
throw new Error(`[envalid] Attempt to mutate environment value: ${name}`)
},
})
8 changes: 0 additions & 8 deletions tests/test_basics.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ test('string passthrough', () => {
assertPassthrough({ FOO: 'bar' }, { FOO: str() })
})

test('strict option: only specified fields are passed through', () => {
const opts = { strict: true }
const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, {
FOO: str()
}, opts)
assert.deepEqual(env, { FOO: 'bar' })
})

test('transformer option: allow transformation of keys', () => {
const lowerCaseKey = (acc, [key, value]) => Object.assign(acc, { [key.toLowerCase()]: value })
const opts = {
Expand Down
8 changes: 0 additions & 8 deletions tests/test_dotenv.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ test('.env contents are cleaned', () => {
assert.deepEqual(env, { FOO: 'bar', BAR: 'asdfasdf', MYNUM: 4 })
})

test('.env test in strict mode', () => {
const opts = { strict: true }
const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, {
MYNUM: num()
}, opts)
assert.deepEqual(env, { MYNUM: 4 })
})

test('can opt out of dotenv with dotEnvPath=null', () => {
const env = cleanEnv({ FOO: 'bar' }, {}, { dotEnvPath: null })
assert.deepEqual(env, { FOO: 'bar' })
Expand Down
55 changes: 55 additions & 0 deletions tests/test_strict.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const fs = require('fs')
const { createGroup, assert } = require('painless')
const { cleanEnv, str, num } = require('..')
const test = createGroup()
const strictOption = { strict: true }


// assert.deepEqual() substitute for assertions on proxied strict-mode env objects
// Chai's deepEqual() performs a few checks that the Proxy chokes on, so rather than
// adding special-case code inside the proxy's get() trap, we use this custom assert
// function
const objStrictDeepEqual = (actual, desired) => {
const desiredKeys = Object.keys(desired)
assert.deepEqual(Object.keys(actual), desiredKeys)
for (const k of desiredKeys) {
assert.strictEqual(actual[k], desired[k])
}
}

test.beforeEach(() => fs.writeFileSync('.env', `
BAR=asdfasdf
MYNUM=4
`))
test.afterEach(() => fs.unlinkSync('.env'))


test('strict option: only specified fields are passed through', () => {
const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, {
FOO: str()
}, strictOption)
objStrictDeepEqual(env, { FOO: 'bar' })
})

test('.env test in strict mode', () => {
const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, {
MYNUM: num()
}, strictOption)
objStrictDeepEqual(env, { MYNUM: 4 })
})

test('strict mode objects throw when invalid attrs are accessed', () => {
const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, {
FOO: str()
}, strictOption)
assert.strictEqual(env.FOO, 'bar')
assert.throws(() => env.ASDF)
})

test('strict mode objects throw when attempting to mutate', () => {
const env = cleanEnv({ FOO: 'bar', BAZ: 'baz' }, {
FOO: str()
}, strictOption)
assert.throws(() => env.FOO = 'foooooo')
})

0 comments on commit 4680535

Please sign in to comment.