├── test ├── types │ ├── .gitignore │ └── index.ts ├── logger.js └── index.js ├── screenshots └── logging.png ├── lib ├── accessors │ ├── string.js │ ├── url-string.js │ ├── json.js │ ├── int.js │ ├── set.js │ ├── int-negative.js │ ├── int-positive.js │ ├── float-negative.js │ ├── float-positive.js │ ├── json-array.js │ ├── json-object.js │ ├── url-object.js │ ├── bool-strict.js │ ├── array.js │ ├── port.js │ ├── enum.js │ ├── float.js │ ├── bool.js │ ├── regexp.js │ ├── index.js │ └── email-string.js ├── logger.js ├── env-error.js └── variable.js ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug-report---.md └── workflows │ ├── npm-publish.yml │ └── ci.yml ├── example ├── catch-error.js ├── catch-error-promise.js ├── logging.js ├── typescript.ts ├── custom-accessor.js └── custom-accessor-2.ts ├── LICENSE ├── package.json ├── env-var.js ├── CONTRIBUTING.md ├── EXAMPLE.md ├── CODE_OF_CONDUCT.md ├── README.md ├── CHANGELOG.md ├── env-var.d.ts └── API.md /test/types/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.js.map 3 | -------------------------------------------------------------------------------- /screenshots/logging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanshortiss/env-var/HEAD/screenshots/logging.png -------------------------------------------------------------------------------- /lib/accessors/string.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function asString (value) { 4 | return value 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | example/typescript.js* 5 | coverage 6 | .nyc_output 7 | package-lock.json 8 | .vscode 9 | -------------------------------------------------------------------------------- /lib/accessors/url-string.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const urlObject = require('./url-object') 4 | 5 | module.exports = function asUrlString (value) { 6 | return urlObject(value).toString() 7 | } 8 | -------------------------------------------------------------------------------- /lib/accessors/json.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function asJson (value) { 4 | try { 5 | return JSON.parse(value) 6 | } catch (e) { 7 | throw new Error('should be valid (parseable) JSON') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/accessors/int.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function asInt (value) { 4 | const n = parseInt(value, 10) 5 | 6 | if (isNaN(n) || n.toString(10) !== value) { 7 | throw new Error('should be a valid integer') 8 | } 9 | 10 | return n 11 | } 12 | -------------------------------------------------------------------------------- /lib/accessors/set.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asArray = require('./array') 4 | 5 | module.exports = function asSet (value, delimiter) { 6 | if (!value.length) { 7 | return new Set() 8 | } else { 9 | return new Set(asArray(value, delimiter)) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/accessors/int-negative.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asInt = require('./int') 4 | 5 | module.exports = function asIntNegative (value) { 6 | const ret = asInt(value) 7 | 8 | if (ret > 0) { 9 | throw new Error('should be a negative integer') 10 | } 11 | 12 | return ret 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/int-positive.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asInt = require('./int') 4 | 5 | module.exports = function asIntPositive (value) { 6 | const ret = asInt(value) 7 | 8 | if (ret < 0) { 9 | throw new Error('should be a positive integer') 10 | } 11 | 12 | return ret 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/float-negative.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asFloat = require('./float') 4 | 5 | module.exports = function asFloatNegative (value) { 6 | const ret = asFloat(value) 7 | 8 | if (ret > 0) { 9 | throw new Error('should be a negative float') 10 | } 11 | 12 | return ret 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/float-positive.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asFloat = require('./float') 4 | 5 | module.exports = function asFloatPositive (value) { 6 | const ret = asFloat(value) 7 | 8 | if (ret < 0) { 9 | throw new Error('should be a positive float') 10 | } 11 | 12 | return ret 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/json-array.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asJson = require('./json') 4 | 5 | module.exports = function asJsonArray (value) { 6 | var ret = asJson(value) 7 | 8 | if (!Array.isArray(ret)) { 9 | throw new Error('should be a parseable JSON Array') 10 | } 11 | 12 | return ret 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/json-object.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asJson = require('./json') 4 | 5 | module.exports = function asJsonObject (value) { 6 | var ret = asJson(value) 7 | 8 | if (Array.isArray(ret)) { 9 | throw new Error('should be a parseable JSON Object') 10 | } 11 | 12 | return ret 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/url-object.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asString = require('./string') 4 | 5 | module.exports = function asUrlObject (value) { 6 | const ret = asString(value) 7 | 8 | try { 9 | return new URL(ret) 10 | } catch (e) { 11 | throw new Error('should be a valid URL') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/bool-strict.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function asBoolStrict (value) { 4 | const val = value.toLowerCase() 5 | 6 | if ((val !== 'false') && (val !== 'true')) { 7 | throw new Error('should be either "true", "false", "TRUE", or "FALSE"') 8 | } 9 | 10 | return val !== 'false' 11 | } 12 | -------------------------------------------------------------------------------- /lib/accessors/array.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asString = require('./string') 4 | 5 | module.exports = function asArray (value, delimiter) { 6 | delimiter = delimiter || ',' 7 | 8 | if (!value.length) { 9 | return [] 10 | } else { 11 | return asString(value).split(delimiter).filter(Boolean) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/port.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asIntPositive = require('./int-positive') 4 | 5 | module.exports = function asPortNumber (value) { 6 | var ret = asIntPositive(value) 7 | 8 | if (ret > 65535) { 9 | throw new Error('cannot assign a port number greater than 65535') 10 | } 11 | 12 | return ret 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Tells the .editorconfg plugin to stop searching once it finds this file 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | quote_type = single 12 | 13 | [*.py] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /lib/accessors/enum.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asString = require('./string') 4 | 5 | module.exports = function asEnum (value, validValues) { 6 | const valueString = asString(value) 7 | 8 | if (validValues.indexOf(valueString) < 0) { 9 | throw new Error(`should be one of [${validValues.join(', ')}]`) 10 | } 11 | 12 | return valueString 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": false, 4 | "lib": ["es2015"], 5 | "target": "es5", 6 | "moduleResolution": "node", 7 | "skipLibCheck": false, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true 11 | }, 12 | "files": ["test/types/index.ts", "example/typescript.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Default logger included with env-var. 5 | * Will not log anything if NODE_ENV is set to production 6 | */ 7 | module.exports = function genLogger (out, prodFlag) { 8 | return function envVarLogger (varname, str) { 9 | if (!prodFlag || !prodFlag.match(/prod|production/)) { 10 | out(`env-var (${varname}): ${str}`) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/float.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function asFloat (value) { 4 | const n = parseFloat(value) 5 | 6 | // Some values are parsed as valid floats despite being obviously invalid, e.g. "1.o" or "192.168.1.1". 7 | // In these cases we would want to throw an error. 8 | if (isNaN(n) || isNaN(value)) { 9 | throw new Error('should be a valid float') 10 | } 11 | 12 | return n 13 | } 14 | -------------------------------------------------------------------------------- /lib/accessors/bool.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function asBool (value) { 4 | const val = value.toLowerCase() 5 | 6 | const allowedValues = [ 7 | 'false', 8 | '0', 9 | 'true', 10 | '1' 11 | ] 12 | 13 | if (allowedValues.indexOf(val) === -1) { 14 | throw new Error('should be either "true", "false", "TRUE", "FALSE", 1, or 0') 15 | } 16 | 17 | return !(((val === '0') || (val === 'false'))) 18 | } 19 | -------------------------------------------------------------------------------- /lib/env-error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Custom error class that can be used to identify errors generated 5 | * by the module 6 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error} 7 | */ 8 | class EnvVarError extends Error { 9 | constructor (message, ...params) { 10 | super(`env-var: ${message}`, ...params) 11 | /* istanbul ignore else */ 12 | if (Error.captureStackTrace) { 13 | Error.captureStackTrace(this, EnvVarError) 14 | } 15 | 16 | this.name = 'EnvVarError' 17 | } 18 | } 19 | 20 | module.exports = EnvVarError 21 | -------------------------------------------------------------------------------- /lib/accessors/regexp.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function asRegExp (value, flags) { 4 | // We have to test the value and flags indivudally if we want to write our 5 | // own error messages,as there is no way to differentiate between the two 6 | // errors except by using string comparisons. 7 | 8 | // Test the flags 9 | try { 10 | RegExp(undefined, flags) 11 | } catch (err) { 12 | throw new Error('invalid regexp flags') 13 | } 14 | 15 | try { 16 | return new RegExp(value, flags) 17 | } catch (err) { 18 | // We know that the regexp is the issue because we tested the flags earlier 19 | throw new Error('should be a valid regexp') 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report \U0001F41B" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug 🐛** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce 📝** 14 | Steps or a code snippet to reproduce the behaviour. 15 | 16 | **Expected behaviour 🤷‍♂️🤷** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots 📷** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Environment (please complete the following information) 💻:** 23 | - OS: [e.g. linux] 24 | - Runtime [e.g. chrome, safari, node.js] 25 | - Version [e.g. 22, 10.x] 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /example/catch-error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * This example demonstrates how you can use bluebird's custom catch 5 | * functionality to respond to different error types. 6 | * 7 | * Here we use the EnvVarError type to catch an error. If you run the program 8 | * without setting CATCH_ERROR it will print "we got an env-var error" 9 | */ 10 | 11 | const env = require('../env-var') 12 | 13 | try { 14 | // will throw if you have not set this variable 15 | env.get('CATCH_ERROR').required().asString() 16 | 17 | // If catch error is set, we'll end up throwing here instead 18 | throw new Error('some other error') 19 | } catch (e) { 20 | if (e instanceof env.EnvVarError) { 21 | console.log('we got an env-var error', e) 22 | } else { 23 | console.log('we got some error that wasn\'t an env-var error', e) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/catch-error-promise.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * This example demonstrates how you can use bluebird's custom catch 5 | * functionality to respond to different error types. 6 | * 7 | * Here we use the EnvVarError type to catch an error. If you run the program 8 | * without setting CATCH_ERROR it will print "we got an env-var error" 9 | */ 10 | 11 | const Promise = require('bluebird') 12 | const env = require('../env-var') 13 | 14 | new Promise((resolve, reject) => { 15 | env.get('CATCH_ERROR').required().asString() 16 | 17 | // If catch error is set, we'll end up throwing here instead 18 | throw new Error('some other error') 19 | }) 20 | .catch(env.EnvVarError, function (e) { 21 | console.log('we got an env-var error', e) 22 | }) 23 | .catch(function (e) { 24 | console.log('we got some error that wasn\'t an env-var error', e) 25 | }) 26 | -------------------------------------------------------------------------------- /lib/accessors/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | asArray: require('./array'), 3 | asSet: require('./set'), 4 | 5 | asBoolStrict: require('./bool-strict'), 6 | asBool: require('./bool'), 7 | 8 | asPortNumber: require('./port'), 9 | asEnum: require('./enum'), 10 | 11 | asFloatNegative: require('./float-negative'), 12 | asFloatPositive: require('./float-positive'), 13 | asFloat: require('./float'), 14 | 15 | asIntNegative: require('./int-negative'), 16 | asIntPositive: require('./int-positive'), 17 | asInt: require('./int'), 18 | 19 | asJsonArray: require('./json-array'), 20 | asJsonObject: require('./json-object'), 21 | asJson: require('./json'), 22 | 23 | asRegExp: require('./regexp'), 24 | 25 | asString: require('./string'), 26 | 27 | asUrlObject: require('./url-object'), 28 | asUrlString: require('./url-string'), 29 | 30 | asEmailString: require('./email-string') 31 | } 32 | -------------------------------------------------------------------------------- /lib/accessors/email-string.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asString = require('./string') 4 | 5 | // eslint-disable-next-line no-control-regex 6 | const EMAIL_REGEX = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\u0001-\u0008\u000b\u000c\u000e-\u001f\u0021\u0023-\u005b\u005d-\u007f]|\\[\u0001-\u0009\u000b\u000c\u000e-\u007f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\u0001-\u0008\u000b\u000c\u000e-\u001f\u0021-\u005a\u0053-\u007f]|\\[\u0001-\u0009\u000b\u000c\u000e-\u007f])+)\])$/ 7 | 8 | module.exports = function asEmailString (value) { 9 | const strValue = asString(value) 10 | 11 | if (!EMAIL_REGEX.test(strValue)) { 12 | throw new Error('should be a valid email address') 13 | } 14 | 15 | return strValue 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm install 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm install 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /example/logging.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * This example demonstrates how you can use bluebird's custom catch 5 | * functionality to respond to different error types. 6 | * 7 | * Here we use the EnvVarError type to catch an error. If you run the program 8 | * without setting CATCH_ERROR it will print "we got an env-var error" 9 | */ 10 | 11 | const defaultJsonArray = JSON.stringify(['luke', 'leia', 'lando', 'chewie']) 12 | 13 | // Load the from and logger functions from env-var 14 | const { from, logger } = require('../env-var') 15 | 16 | // Create an env-var instance and pass it the buitl-in logger 17 | const env = from(process.env, {}, logger) 18 | 19 | // Read variables (this will print logs if NODE_ENV isn't "prod" or "production") 20 | const home = env.get('HOME').asString() 21 | const users = env.get('USERNAMES') 22 | .example(defaultJsonArray) 23 | .default(defaultJsonArray) 24 | .asJsonArray() 25 | 26 | console.log('\nFetched HOME value:', home) 27 | console.log('Fetched USERNAMES value:', users) 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm install 29 | - run: npm test 30 | - name: Coveralls 31 | uses: coverallsapp/github-action@1.1.3 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016, 2024 Evan Shortiss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | const { expect } = require('chai') 6 | 7 | describe('#built-in logger', () => { 8 | const varname = 'SOME_VAR' 9 | const msg = 'this is a test message' 10 | 11 | it('should send a string to the given logger', () => { 12 | let spyCalled = false 13 | const spy = (str) => { 14 | expect(str).to.equal(`env-var (${varname}): ${msg}`) 15 | spyCalled = true 16 | } 17 | 18 | const log = require('../lib/logger')(spy) 19 | 20 | log(varname, msg) 21 | expect(spyCalled).to.equal(true) 22 | }) 23 | 24 | it('should not not send a string to the logger due to "prod" flag', () => { 25 | let spyCalled = false 26 | const spy = (str) => { 27 | spyCalled = true 28 | } 29 | 30 | const log = require('../lib/logger')(spy, 'prod') 31 | 32 | log(varname, msg) 33 | expect(spyCalled).to.equal(false) 34 | }) 35 | 36 | it('should not not send a string to the logger due to "production" flag', () => { 37 | let spyCalled = false 38 | const spy = (str) => { 39 | spyCalled = true 40 | } 41 | 42 | const log = require('../lib/logger')(spy, 'production') 43 | 44 | log(varname, msg) 45 | expect(spyCalled).to.equal(false) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /example/typescript.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as env from '../env-var' 3 | 4 | // Boolean 5 | const doTheThing = env.get('DO_THE_THING').asBool() 6 | 7 | if (doTheThing) { 8 | console.log('did the thing') 9 | } else { 10 | console.log('did not do the thing') 11 | } 12 | 13 | // URL variables 14 | let url = env.get('THE_URL').asUrlString() 15 | 16 | if (!url) { 17 | url = 'http://google.com' 18 | } 19 | 20 | console.log('url is', url) 21 | 22 | // Integers 23 | const requiredInt = env.get('AN_INTEGER').default(10).required().asInt() 24 | 25 | console.log('the integer was', requiredInt) 26 | 27 | // ExtensionFn - Verify this works, and fluid API works with it 28 | const asEmail: env.ExtensionFn = (value) => { 29 | const split = String(value).split('@') 30 | if (split.length !== 2) { 31 | throw new Error('must contain exactly one "@"') 32 | } 33 | return value 34 | } 35 | 36 | const customEnv = env.from({ 37 | ADMIN_EMAIL: 'admin@example.com' 38 | }, { 39 | asEmail 40 | }, env.logger) 41 | 42 | const adminEmail = customEnv.get('ADMIN_EMAIL') 43 | .example('someone@example') 44 | .required() 45 | .asEmail() 46 | 47 | console.log('admin email is:', adminEmail) 48 | 49 | const log: env.LoggerFn = (varname, msg) => { 50 | console.log(`Log for ${varname}: ${msg}`) 51 | } 52 | const loggerEnv = env.from(process.env, {}, log) 53 | console.log(`HOME is set to: ${loggerEnv.get('HOME').asString()}`) 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "env-var", 3 | "version": "7.5.0", 4 | "description": "Verification, sanitization, and type coercion for environment variables in Node.js", 5 | "main": "env-var.js", 6 | "typings": "env-var.d.ts", 7 | "scripts": { 8 | "coverage": "nyc mocha test/*.js && nyc report --reporter=lcov", 9 | "check-coverage": "nyc check-coverage --statements 100 --branches 100 --functions 100 --lines 100", 10 | "unit": "mocha test/*.js", 11 | "lint": "standard example/*.js *.js \"lib/**/*.js\" test/*.js --fix", 12 | "test": "npm run unit && npm run coverage && npm run check-coverage && npm run lint && npm run ts-verify", 13 | "ts-verify": "tsc && mocha test/types/*.js" 14 | }, 15 | "husky": { 16 | "hooks": { 17 | "pre-commit": "npm run lint && npm run unit" 18 | } 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/evanshortiss/env-var.git" 23 | }, 24 | "keywords": [ 25 | "dotenv", 26 | "env", 27 | "process.env", 28 | "process", 29 | "var", 30 | "environment", 31 | "variables", 32 | "variable", 33 | "loader", 34 | "env-var", 35 | "envvar", 36 | "config", 37 | "configuration", 38 | "typescript", 39 | "ts" 40 | ], 41 | "author": "Evan Shortiss", 42 | "license": "MIT", 43 | "files": [ 44 | "lib/", 45 | "env-var.js", 46 | "env-var.d.ts" 47 | ], 48 | "bugs": { 49 | "url": "https://github.com/evanshortiss/env-var/issues" 50 | }, 51 | "homepage": "https://github.com/evanshortiss/env-var", 52 | "devDependencies": { 53 | "@types/chai": "~4.2.10", 54 | "@types/mocha": "~7.0.1", 55 | "@types/node": "~13.13.0", 56 | "bluebird": "~3.7.0", 57 | "chai": "~4.2.0", 58 | "conditional-type-checks": "1.0.5", 59 | "coveralls": "~3.1.0", 60 | "husky": "~4.2.2", 61 | "mocha": "~8.2.0", 62 | "mocha-lcov-reporter": "~1.3.0", 63 | "nyc": "~15.1.0", 64 | "standard": "~14.3.4", 65 | "typescript": "~3.9.0" 66 | }, 67 | "engines": { 68 | "node": ">=10" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/custom-accessor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * This example demonstrates how you can build a custom accessor by composing 5 | * internal accessors available at env.accessors, and attach it to the env-var 6 | * instance. 7 | * 8 | * Here we use the EnvVarError type to catch an error. If you run the program 9 | * without setting CATCH_ERROR it will print "we got an env-var error". 10 | * 11 | * To test out this example, run 'node custom-accessor.js'. 12 | */ 13 | 14 | const env = require('../env-var') 15 | const { from, accessors } = env 16 | 17 | // Add an accessor named 'asIntBetween' which verifies whether the given value is 18 | // between the specified min and max values, inclusive 19 | const envInstance = from(process.env, { 20 | asIntBetween: (value, min, max) => { 21 | let ret, minInt, maxInt 22 | 23 | try { 24 | ret = accessors.asInt(value) 25 | minInt = accessors.asInt(min) 26 | maxInt = accessors.asInt(max) 27 | } catch { 28 | throw new Error('value, min, max must be integers') 29 | } 30 | 31 | if (ret < minInt || ret > maxInt) { 32 | throw new Error( 33 | `should be an integer between the range of [${min}, ${max}]` 34 | ) 35 | } 36 | 37 | return ret 38 | } 39 | }) 40 | 41 | try { 42 | // Will throw an error if you have not set this environment variable 43 | // We specified 'asIntBetween' as the name for the accessor above, 44 | // so now we can call `asIntBetween()` like any other accessor on all 45 | // env-var instances. 46 | 47 | // This will pass 48 | process.env.SERVER_INSTANCES = 10 49 | let serverInstances = envInstance.get('SERVER_INSTANCES').asIntBetween(1, 10) 50 | 51 | // This will fail because min is not an integer 52 | /* process.env['SERVER_INSTANCES'] = 1 53 | serverInstances = envInstance.get('SERVER_INSTANCES').asIntBetween('one', 10) */ 54 | 55 | // This will fail because out of range 56 | process.env.SERVER_INSTANCES = 0 57 | serverInstances = envInstance.get('SERVER_INSTANCES').asIntBetween(1, 10) 58 | 59 | console.log(`SERVER_INSTANCES=${serverInstances}`) 60 | } catch (e) { 61 | if (e instanceof envInstance.EnvVarError) { 62 | console.log('We got an env-var error', e) 63 | } else { 64 | console.log('Unexpected error', e) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /env-var.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const variable = require('./lib/variable') 4 | const EnvVarError = require('./lib/env-error') 5 | 6 | /** 7 | * Returns an "env-var" instance that reads from the given container of values. 8 | * By default, we export an instance that reads from process.env 9 | * @param {Object} container target container to read values from 10 | * @param {Object} extraAccessors additional accessors to attach to the 11 | * resulting object 12 | * @return {Object} a new module instance 13 | */ 14 | const from = (container, extraAccessors, logger) => { 15 | return { 16 | from: from, 17 | 18 | /** 19 | * This is the Error class used to generate exceptions. Can be used to identify 20 | * exceptions and handle them appropriately. 21 | */ 22 | EnvVarError: require('./lib/env-error'), 23 | 24 | /** 25 | * Returns a variable instance with helper functions, or process.env 26 | * @param {String} variableName Name of the environment variable requested 27 | * @return {Object} 28 | */ 29 | get: function (variableName) { 30 | if (!variableName) { 31 | return container 32 | } 33 | 34 | if (arguments.length > 1) { 35 | throw new EnvVarError('It looks like you passed more than one argument to env.get(). Since env-var@6.0.0 this is no longer supported. To set a default value use env.get(TARGET).default(DEFAULT)') 36 | } 37 | 38 | return variable(container, variableName, extraAccessors || {}, logger || function noopLogger () {}) 39 | }, 40 | 41 | /** 42 | * Provides access to the functions that env-var uses to parse 43 | * process.env strings into valid types requested by the API 44 | */ 45 | accessors: require('./lib/accessors/index'), 46 | 47 | /** 48 | * Provides a default logger that can be used to print logs. 49 | * This will not print logs in a production environment (checks process.env.NODE_ENV) 50 | */ 51 | logger: require('./lib/logger')(console.log, container.NODE_ENV) 52 | } 53 | } 54 | 55 | /** 56 | * Makes a best-effort attempt to load environment variables in 57 | * different environments, e.g create-react-app, vite, Node.js 58 | * @returns Object 59 | */ 60 | function getProcessEnv () { 61 | /* istanbul ignore next */ 62 | try { 63 | return process.env 64 | } catch (e) { 65 | return {} 66 | } 67 | } 68 | 69 | /* istanbul ignore next */ 70 | module.exports = from(getProcessEnv()) 71 | -------------------------------------------------------------------------------- /example/custom-accessor-2.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates how you can build a custom accessor by composing 3 | * internal accessors available at env.accessors, and attach it to the env-var 4 | * instance. 5 | * 6 | * Here we use the EnvVarError type to catch an error. If you run the program 7 | * without setting CATCH_ERROR it will print "we got an env-var error". 8 | * 9 | * To test out this example, run 'tsc custom-accessor-2.ts && node custom-accessor-2.js'. 10 | * This was named 'custom-accessor-2.ts' to prevent override of the Javascript-equivalent 11 | * of this example 'custom-accessor.js'. 12 | */ 13 | 14 | import * as env from '../env-var' 15 | const { from, accessors } = env 16 | 17 | // Add an accessor named 'asIntBetween' which verifies whether the given value is 18 | // between the specified min and max values, inclusive 19 | const envInstance = from(process.env, { 20 | asIntBetween: (value, min, max) => { 21 | let ret: number, minInt: number, maxInt: number 22 | 23 | try { 24 | ret = accessors.asInt(value) 25 | minInt = accessors.asInt(min) 26 | maxInt = accessors.asInt(max) 27 | } catch { 28 | throw new Error('value, min, max must be integers') 29 | } 30 | 31 | if (ret < minInt || ret > maxInt) { 32 | throw new Error( 33 | `should be an integer between the range of [${min}, ${max}]` 34 | ) 35 | } 36 | 37 | return ret 38 | } 39 | }) 40 | 41 | try { 42 | // Will throw an error if you have not set this environment variable 43 | // We specified 'asIntBetween' as the name for the accessor above, 44 | // so now we can call `asIntBetween()` like any other accessor on all 45 | // env-var instances. 46 | 47 | // This will pass 48 | process.env.SERVER_INSTANCES = '10' 49 | let serverInstances = envInstance.get('SERVER_INSTANCES').asIntBetween(1, 10) 50 | 51 | // This will fail because min is not an integer 52 | process.env['SERVER_INSTANCES'] = '1' 53 | serverInstances = envInstance.get('SERVER_INSTANCES').asIntBetween('one', 10) 54 | 55 | // This will fail because out of range 56 | /*process.env.SERVER_INSTANCES = '0' 57 | serverInstances = envInstance.get('SERVER_INSTANCES').asIntBetween(1, 10)*/ 58 | 59 | console.log(`SERVER_INSTANCES=${serverInstances}`) 60 | } catch (e) { 61 | if (e instanceof env.EnvVarError) { 62 | console.log('We got an env-var error', e) 63 | } else { 64 | console.log('Unexpected error', e) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Thanks for your interest in contributing to the project! 🎉 4 | 5 | ### General Rules & Tips 6 | 7 | There are just a few things to be aware of when making a contribution. If you have trouble meeting these requirements don't be shy about asking for help: 8 | 9 | * No contribution is too big or too small. Don't be shy, open an issue and we can discuss it 💬 10 | * Generally, opening an issue before making a PR is a good idea unless your PR is addressing a very clear cut bug/error 🐛 11 | * Follow the prevailing code-style. A PR that changes indentation, adds extra formatting, etc. won't be merged unless it aligns with the existing code-style 📝 12 | * If you add/change/remove a feature the relevant tests must be updated and the CI build should pass with sufficient code coverage ✅ 13 | * Be respectful and follow the code of conduct. If you need a TLDR of the code of conduct it's "treat others with respect" 🙏 14 | 15 | ### Adding an Accessor 16 | 17 | If you want to add a new accessor it's pretty straightforward, and an example is outlined below - just make sure it's a reasonably generic use case! 18 | 19 | Start by adding a file to `lib/accessors`, with the name of the type e.g add a 20 | file named `number-zero.js` into that folder and populate it with code 21 | following this structure: 22 | 23 | ```js 24 | /** 25 | * Validate that the environment value is an integer and equals zero. 26 | * This is a strange example, but hopefully demonstrates the idea. 27 | * @param {String} environmentValue this is the string from process.env 28 | */ 29 | module.exports = function numberZero (environmentValue) { 30 | 31 | // Your custom code should go here...below code is an example 32 | 33 | const val = parseInt(environmentValue) 34 | 35 | if (val === 0) { 36 | return ret; 37 | } else { 38 | throw new Error('should be zero') 39 | } 40 | } 41 | ``` 42 | 43 | Next update the `accessors` Object in `getVariableAccessors()` in 44 | `lib/variable.js` to include your new module. The naming convention should be of 45 | the format "asTypeSubtype", so for our `number-zero` example it would be done 46 | like so: 47 | 48 | ```js 49 | asNumberZero: generateAccessor(container, varName, defValue, require('./accessors/number-zero')), 50 | ``` 51 | 52 | Once you've done that, add some unit tests and use it like so: 53 | 54 | ```js 55 | // Uses your new function to ensure the SOME_NUMBER is the integer 0 56 | env.get('SOME_NUMBER').asNumberZero() 57 | ``` 58 | -------------------------------------------------------------------------------- /EXAMPLE.md: -------------------------------------------------------------------------------- 1 | # env-var examples 2 | 3 | This document provides example usage of a customer logger, and integration with `dotenv`. 4 | 5 | For more examples, refer to the `/example` directory. 6 | 7 | ### Directory 8 | 9 | * [Custom logging](#custom-logging) 10 | * [Dotenv](#dotenv) 11 | * [Other examples](#other-examples) 12 | 13 | ## Custom logging 14 | 15 | If you need to filter `env-var` logs based on log levels (e.g. trace logging only) or have your own preferred logger, you can use a custom logging solution such as `pino` easily. 16 | 17 | ### Pino logger example 18 | 19 | ```js 20 | const pino = require('pino')() 21 | const customLogger = (varname, str) => { 22 | // varname is the name of the variable being read, e.g "API_KEY" 23 | // str is the log message, e.g "verifying variable value is not empty" 24 | log.trace(`env-var log (${varname}): ${str}`) 25 | } 26 | 27 | const { from } = require('env-var') 28 | const env = from(process.env, {}, customLogger) 29 | 30 | const API_KEY = env.get('API_KEY').required().asString() 31 | ``` 32 | 33 | ## Dotenv 34 | 35 | You can optionally use [dotenv](https://www.npmjs.com/package/dotenv) with [env-var](https://www.npmjs.com/package/env-var). 36 | 37 | 1. Just `npm install dotenv` and use it whatever way you're used to. 38 | 2. You can use `dotenv` with `env-var` via a `require()` call in your code; 39 | 3. Or you can preload it with the `--require` or `-r` flag in the `node` CLI. 40 | 41 | ### Pre-requisite 42 | 43 | - The examples below assume you have a `.env` file in your repository and it contains a line similar to `MY_VAR=a-string-value!`. 44 | 45 | ### Load dotenv via require() 46 | 47 | This is per the default usage described by [`dotenv` README](https://www.npmjs.com/package/dotenv#usage). 48 | 49 | ```js 50 | // Read in the .env file 51 | require('dotenv').config() 52 | 53 | // Read the MY_VAR entry that dotenv created 54 | const env = require('env-var') 55 | const myVar = env.get('MY_VAR').asString() 56 | ``` 57 | 58 | ### Preload dotenv via CLI Args 59 | 60 | This is per the [preload section](https://www.npmjs.com/package/dotenv#preload) 61 | of the [`dotenv` README](https://www.npmjs.com/package/dotenv#usage).. Run the following code by using the 62 | `node -r dotenv/config your_script.js` command. 63 | 64 | ```js 65 | // This is just a regular node script, but we started it using the command 66 | // "node -r dotenv/config your_script.js" via the terminal. This tells node 67 | // to load our variables using dotenv before running the rest of our script! 68 | 69 | // Read the MY_VAR entry that dotenv created 70 | const env = require('env-var') 71 | const myVar = env.get('MY_VAR').asString() 72 | ``` 73 | 74 | ## Other examples 75 | 76 | The other examples are available in the `/example` directory. 77 | 78 | * `catch-error.js`: demonstrates how you can use bluebird's custom catch functionality to respond to different error types. 79 | * `catch-error-promise.js`: same as `catch-error.promise.js` but with promises. 80 | * `custom-accessor.js`: demonstrates how you can build a custom accessor (e.g. `asIntBetween()`) by composing internal accessors available at `env.accessors`, and attach it to the `env-var` instance. 81 | * `custom-accessor-2.ts`: Typescript version of `custom-accessor.js`. 82 | * `logging.js`: self-explanatory. 83 | * `typescript.ts`: common `env-var` usage in Typescript. -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at evanshortiss@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /lib/variable.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EnvVarError = require('./env-error') 4 | const base64Regex = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/ 5 | 6 | /** 7 | * Returns an Object that contains functions to read and specify the format of 8 | * the variable you wish to have returned 9 | * @param {Object} container Encapsulated container (e.g., `process.env`). 10 | * @param {String} varName Name of the requested property from `container`. 11 | * @param {*} defValue Default value to return if `varName` is invalid. 12 | * @param {Object} extraAccessors Extra accessors to install. 13 | * @return {Object} 14 | */ 15 | module.exports = function getVariableAccessors (container, varName, extraAccessors, logger) { 16 | let isBase64 = false 17 | let isRequired = false 18 | let defValue 19 | let example 20 | 21 | const builtInAccessors = require('./accessors/index') 22 | 23 | /** 24 | * Logs the given string using the provided logger 25 | * @param {String} str 26 | * @param {String} str 27 | */ 28 | function log (str) { 29 | logger(varName, str) 30 | } 31 | 32 | /** 33 | * Throw an error with a consistent type/format. 34 | * @param {String} value 35 | */ 36 | function raiseError (value, msg) { 37 | let errMsg = `"${varName}" ${msg}` 38 | 39 | if (value) { 40 | errMsg = `${errMsg}` 41 | } 42 | 43 | if (example) { 44 | errMsg = `${errMsg}. An example of a valid value would be: ${example}` 45 | } 46 | 47 | throw new EnvVarError(errMsg) 48 | } 49 | 50 | /** 51 | * Returns an accessor wrapped by error handling and args passing logic 52 | * @param {Function} accessor 53 | */ 54 | function generateAccessor (accessor) { 55 | return function () { 56 | let value = container[varName] 57 | 58 | log(`will be read from the environment using "${accessor.name}" accessor`) 59 | 60 | if (typeof value === 'undefined') { 61 | if (typeof defValue === 'undefined' && isRequired) { 62 | log('was not found in the environment, but is required to be set') 63 | // Var is not set, nor is a default. Throw an error 64 | raiseError(undefined, 'is a required variable, but it was not set') 65 | } else if (typeof defValue !== 'undefined') { 66 | log(`was not found in the environment, parsing default value "${defValue}" instead`) 67 | value = defValue 68 | } else { 69 | log('was not found in the environment, but is not required. returning undefined') 70 | // return undefined since variable is not required and 71 | // there's no default value provided 72 | return undefined 73 | } 74 | } 75 | 76 | if (isRequired) { 77 | log('verifying variable value is not an empty string') 78 | // Need to verify that required variables aren't just whitespace 79 | if (value.trim().length === 0) { 80 | raiseError(undefined, 'is a required variable, but its value was empty') 81 | } 82 | } 83 | 84 | if (isBase64) { 85 | log('verifying variable is a valid base64 string') 86 | if (!value.match(base64Regex)) { 87 | raiseError(value, 'should be a valid base64 string if using convertFromBase64') 88 | } 89 | log('converting from base64 to utf8 string') 90 | value = Buffer.from(value, 'base64').toString() 91 | } 92 | 93 | const args = [value].concat(Array.prototype.slice.call(arguments)) 94 | 95 | try { 96 | log(`passing value "${value}" to "${accessor.name}" accessor`) 97 | 98 | const result = accessor.apply( 99 | accessor, 100 | args 101 | ) 102 | 103 | log(`parsed successfully, returning ${result}`) 104 | return result 105 | } catch (error) { 106 | raiseError(value, error.message) 107 | } 108 | } 109 | } 110 | 111 | const accessors = { 112 | /** 113 | * Instructs env-var to first convert the value of the variable from base64 114 | * when reading it using a function such as asString() 115 | */ 116 | convertFromBase64: function () { 117 | log('marking for base64 conversion') 118 | isBase64 = true 119 | 120 | return accessors 121 | }, 122 | 123 | /** 124 | * Set a default value for the variable 125 | * @param {String} value 126 | */ 127 | default: function (value) { 128 | if (typeof value === 'number') { 129 | defValue = value.toString() 130 | } else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { 131 | defValue = JSON.stringify(value) 132 | } else if (typeof value !== 'string') { 133 | throw new EnvVarError('values passed to default() must be of Number, String, Array, or Object type') 134 | } else { 135 | defValue = value 136 | } 137 | 138 | log(`setting default value to "${defValue}"`) 139 | 140 | return accessors 141 | }, 142 | 143 | /** 144 | * Ensures a variable is set in the given environment container. Throws an 145 | * EnvVarError if the variable is not set or a default is not provided 146 | * @param {Boolean} required 147 | */ 148 | required: function (required) { 149 | if (typeof required === 'undefined') { 150 | log('marked as required') 151 | // If no value is passed assume that developer means "true" 152 | // This is to retain support legacy usage (and intuitive) 153 | isRequired = true 154 | } else { 155 | log(`setting required flag to ${required}`) 156 | isRequired = required 157 | } 158 | 159 | return accessors 160 | }, 161 | 162 | /** 163 | * Set an example value for this variable. If the variable value is not set 164 | * or is set to an invalid value this example will be show in error output. 165 | * @param {String} example 166 | */ 167 | example: function (ex) { 168 | example = ex 169 | 170 | return accessors 171 | } 172 | } 173 | 174 | // Attach accessors, and extra accessors if provided. 175 | Object.entries({ 176 | ...builtInAccessors, 177 | ...extraAccessors 178 | }).forEach(([name, accessor]) => { 179 | accessors[name] = generateAccessor(accessor) 180 | }) 181 | 182 | return accessors 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # env-var 2 | 3 |
4 | 5 | [![NPM version](https://img.shields.io/npm/v/env-var.svg?style=flat)](https://www.npmjs.com/package/env-var) 6 | [![TypeScript](https://badgen.net/npm/types/env-var)](http://www.typescriptlang.org/) 7 | [![License](https://badgen.net/npm/license/env-var)](https://opensource.org/licenses/MIT) 8 | [![Coverage Status](https://coveralls.io/repos/github/evanshortiss/env-var/badge.svg?branch=master)](https://coveralls.io/github/evanshortiss/env-var?branch=master) 9 | [![npm downloads](https://img.shields.io/npm/dm/env-var.svg?style=flat)](https://www.npmjs.com/package/env-var) 10 | [![Known Vulnerabilities](https://snyk.io//test/github/evanshortiss/env-var/badge.svg?targetFile=package.json)](https://snyk.io//test/github/evanshortiss/env-var?targetFile=package.json) 11 | 12 | 13 | Verification, sanitization, and type coercion for environment variables in Node.js and web applications. Supports TypeScript! 14 |
15 |
16 |
17 | 18 | * 🏋 Lightweight. Zero dependencies and just ~4.7kB when minified! 19 | * 🧹 Clean and simple code, as [shown here](https://gist.github.com/evanshortiss/0cb049bf676b6138d13384671dad750d). 20 | * 🚫 [Fails fast](https://en.wikipedia.org/wiki/Fail-fast) if your environment is misconfigured. 21 | * 👩‍💻 Friendly error messages and example values for better debugging experience. 22 | * 🎉 TypeScript support provides compile time safety and better developer experience. 23 | * 📦 Support for frontend projects, e.g in React, React Native, Angular, etc. 24 | 25 | ## Contents 26 | 27 | - [API](API.md): The full API set for `env-var` 28 | - [Changelog](CHANGELOG.md) 29 | - [Code of Conduct](CODE_OF_CONDUCT.md) 30 | - [Contributing](CONTRIBUTING.md) 31 | - [Examples](EXAMPLE.md): Example usage of `env-var` 32 | 33 | ## Install 34 | 35 | ### npm 36 | 37 | ```shell 38 | npm install env-var 39 | ``` 40 | 41 | ### yarn 42 | 43 | ```shell 44 | yarn add env-var 45 | ``` 46 | 47 | ## Getting started 48 | 49 | You can use `env-var` in both JavaScript and TypeScript! 50 | 51 | ### Node.js Javascript example 52 | 53 | ```js 54 | const env = require('env-var'); 55 | 56 | // Or using module import syntax: 57 | // import env from 'env-var' 58 | 59 | const PASSWORD = env.get('DB_PASSWORD') 60 | // Throws an error if the DB_PASSWORD variable is not set (optional) 61 | .required() 62 | // Decode DB_PASSWORD from base64 to a utf8 string (optional) 63 | .convertFromBase64() 64 | // Call asString (or other APIs) to get the variable value (required) 65 | .asString(); 66 | 67 | // Read in a port (checks that PORT is in the range 0 to 65535) 68 | // Alternatively, use a default value of 5432 if PORT is not defined 69 | const PORT = env.get('PORT').default('5432').asPortNumber() 70 | ``` 71 | 72 | ### Node.js TypeScript example 73 | 74 | ```ts 75 | import * as env from 'env-var'; 76 | 77 | // Read a PORT environment variable and ensure it's a positive integer. 78 | // An EnvVarError will be thrown if the variable is not set, or if it 79 | // is not a positive integer. 80 | const PORT: number = env.get('PORT').required().asIntPositive(); 81 | ``` 82 | 83 | ### WebApp Example 84 | 85 | When using environment variables in a web application, usually your tooling 86 | such as `vite` imposes special conventions and doesn't expose `process.env`. 87 | Use `from` function to workaround this, and create an `env` object like so: 88 | 89 | ```ts 90 | import { from } from 'env-var' 91 | 92 | const env = from({ 93 | BASE_URL: import.meta.env.BASE_URL, 94 | VITE_CUSTOM_VARIABLE: import.meta.env.CUSTOM_VARIABLE 95 | }) 96 | ``` 97 | 98 | For more examples, refer to the `/example` directory and [EXAMPLE.md](EXAMPLE.md). A summary of the examples available in `/example` is written in the ['Other examples' section of EXAMPLE.md](EXAMPLE.md#other-examples). 99 | 100 | ## API 101 | 102 | The examples above only cover a very small set of `env-var` API calls. There are many others such as `asFloatPositive()`, `asJson()` and `asRegExp()`. For a full list of `env-var` API calls, check out [API.md](API.md). 103 | 104 | You can also create your own custom accessor; refer to the ['extraAccessors' section of API.md](API.md#extraAccessors). 105 | 106 | ## Logging 107 | 108 | Logging is disabled by default in `env-var` to prevent accidental logging of secrets. 109 | 110 | To enable logging, you need to create an `env-var` instance using the `from()` function that the API provides and pass in a logger. 111 | 112 | - A built-in logger is available, but a custom logger is also supported. 113 | - Always exercise caution when logging environment variables! 114 | 115 | ### Using the Built-in Logger 116 | 117 | The built-in logger will print logs only when `NODE_ENV` is **not** set to either `prod` or `production`. 118 | 119 | ```js 120 | const { from, logger } = require('env-var') 121 | const env = from(process.env, {}, logger) 122 | 123 | const API_KEY = env.get('API_KEY').required().asString() 124 | ``` 125 | 126 | This is an example output from the built-in logger generated by running [example/logging.js](example/logging.js): 127 | 128 | ![logging example output](screenshots/logging.png) 129 | 130 | ### Using a Custom Logger 131 | 132 | If you need to filter `env-var` logs based on log levels (e.g. trace logging only) or have your own preferred logger, you can use a custom logging solution such as `pino` easily. 133 | 134 | See the ['Custom logging' section of EXAMPLE.md](EXAMPLE.md#custom-logging) for more information. 135 | 136 | ## Optional integration with dotenv 137 | 138 | You can optionally use [dotenv](https://www.npmjs.com/package/dotenv) with [env-var](https://www.npmjs.com/package/env-var). 139 | 140 | There is no coupling between `dotenv` and `env-var`, but you can easily use them both together. This loose coupling reduces package bloat and allows you to start or stop using one without being forced to do the same for the other. 141 | 142 | See the ['dotenv' section of EXAMPLE.md](EXAMPLE.md#dotenv) for more information. 143 | 144 | ## Contributing 145 | 146 | Contributions are welcomed and discussed in [CONTRIBUTING.md](CONTRIBUTING.md). If you would like to discuss an idea, open an issue or a PR with an initial implementation. 147 | 148 | ## Contributors 149 | 150 | * @aautio 151 | * @avocadomaster 152 | * @caccialdo 153 | * @ChibiBlasphem 154 | * @DigiPie 155 | * @dror-weiss 156 | * @evanshortiss 157 | * @gabrieloczkowski 158 | * @hhravn 159 | * @ineentho 160 | * @itavy 161 | * @jerome-fox 162 | * @joh-klein 163 | * @Lioness100 164 | * @MikeyBurkman 165 | * @pepakriz 166 | * @rmblstrp 167 | * @shawnmclean 168 | * @todofixthis 169 | * @xuo 170 | -------------------------------------------------------------------------------- /test/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as env from '../../'; 3 | import { expect } from 'chai' 4 | import 'mocha' 5 | import { assert, IsExact } from 'conditional-type-checks' 6 | 7 | describe('typescript tests', () => { 8 | describe('#from', () => { 9 | it('should return an env-var instance and read with asString()', () => { 10 | const A_STRING = 'hello, world!' 11 | const e = env.from({ 12 | A_STRING 13 | }) 14 | 15 | expect(e.get('A_STRING').asString()).to.equal(A_STRING) 16 | }) 17 | 18 | it('should return an env-var instance be missing system vars', () => { 19 | // env-var instance with no vars 20 | const e = env.from({}) 21 | 22 | // @ts-expect-error `PATH` is not in container object 23 | expect(e.get('PATH').asString()).to.equal(undefined) 24 | }) 25 | }) 26 | 27 | describe('#accessors', () => { 28 | it('required().asString() should throw if missing', () => { 29 | const e = env.from({}) 30 | 31 | expect(() => { 32 | // @ts-expect-error `A_MISSING_VARIABLE` is not in container object 33 | e.get('A_MISSING_VARIABLE').required().asString() 34 | }).to.throw('env-var: "A_MISSING_VARIABLE" is a required variable, but it was not set') 35 | }) 36 | }) 37 | 38 | describe('#ExtensionFn', () => { 39 | interface EmailComponents { 40 | username: string 41 | domain: string 42 | } 43 | const asEmailComponents: env.ExtensionFn = (value) => { 44 | const parts = value.split('@') 45 | 46 | if (parts.length != 2) { 47 | throw new Error('should be an email') 48 | } else { 49 | return { 50 | username: parts[0], 51 | domain: parts[1] 52 | } 53 | } 54 | } 55 | 56 | it('should return the email parts for a valid email, throw for invalid', () => { 57 | const extendedEnv = env.from({ 58 | VALID_EMAIL: 'hello@example.com', 59 | INVALID_EMAIL: 'oops-example.com' 60 | }, { 61 | asEmailComponents 62 | }) 63 | 64 | // We use required() here to verify chaining typings work 65 | expect( 66 | extendedEnv.get('VALID_EMAIL').required().asEmailComponents() 67 | ).to.deep.equal({ 68 | username: 'hello', 69 | domain: 'example.com' 70 | }) 71 | 72 | expect(() => { 73 | extendedEnv.get('INVALID_EMAIL').asEmailComponents() 74 | }).to.throw('env-var: "INVALID_EMAIL" should be an email') 75 | }) 76 | 77 | it('should support multiple extensions (with correct types)', () => { 78 | const asNumberZero: env.ExtensionFn = (value) => { 79 | const n = parseInt(value) 80 | 81 | if (n === 0) { 82 | return 0 83 | } 84 | 85 | throw new env.EnvVarError('was not zero') 86 | } 87 | 88 | const extendedEnv = env.from({ 89 | EMAIL: 'hello@example.com', 90 | ZERO: '0' 91 | }, { 92 | asEmailComponents, 93 | asNumberZero 94 | }) 95 | 96 | expect( 97 | extendedEnv.get('ZERO').required().asNumberZero() 98 | ).to.equal(0) 99 | 100 | expect( 101 | extendedEnv.get('ZERO').asNumberZero() 102 | ).to.equal(0) 103 | 104 | expect( 105 | extendedEnv.get('EMAIL').required().asEmailComponents() 106 | ).to.deep.equal({ 107 | username: 'hello', 108 | domain: 'example.com' 109 | }) 110 | 111 | expect( 112 | extendedEnv.get('EMAIL').asEmailComponents() 113 | ).to.deep.equal({ 114 | username: 'hello', 115 | domain: 'example.com' 116 | }) 117 | }) 118 | 119 | it('should carry extension functions to a child with from()', () => { 120 | const asNumberZero: env.ExtensionFn = (value) => { 121 | const n = parseInt(value) 122 | 123 | if (n === 0) { 124 | return 0 125 | } 126 | 127 | throw new env.EnvVarError('was not zero') 128 | } 129 | 130 | const extendedEnvA = env.from({ 131 | ZERO: '0' 132 | }) 133 | 134 | const extendedEnvB = extendedEnvA.from({ 135 | ZERO: '0' 136 | }, { 137 | asNumberZero 138 | }) 139 | 140 | expect( 141 | extendedEnvB.get('ZERO').required().asNumberZero() 142 | ).to.equal(0) 143 | 144 | expect( 145 | extendedEnvB.get('ZERO').asNumberZero() 146 | ).to.equal(0) 147 | }) 148 | }) 149 | 150 | describe('asEnum', () => { 151 | const e = env.from({ 152 | ENUM: 'a' 153 | }) 154 | 155 | it('should work with generic defaults', () => { 156 | const enums = e.get('ENUM').required().asEnum(['a', 'b']) 157 | 158 | assert>(true); 159 | }) 160 | 161 | it('should work with generic params', () => { 162 | const enums = e.get('ENUM').required().asEnum<'a' | 'b'>(['a', 'b']) 163 | 164 | assert>(true); 165 | }) 166 | }) 167 | 168 | describe('asRegExp', () => { 169 | const e = env.from({ 170 | REG_EXP: '^.*$' 171 | }) 172 | 173 | it('should return a RegExp instance', () => { 174 | const regExp = e.get('REG_EXP').required().asRegExp() 175 | 176 | assert>(true); 177 | }) 178 | 179 | it('should accept a single string argument for flags', () => { 180 | e.get('REG_EXP').required().asRegExp('ig') 181 | }) 182 | }) 183 | 184 | describe('env.accessors', () => { 185 | describe('#asArray', () => { 186 | it('should return an array of strings', () => { 187 | const arr = env.accessors.asArray('1,2,3') 188 | 189 | expect(arr).to.eql(['1','2','3']) 190 | }) 191 | 192 | it('should return an array of strings split by period chars', () => { 193 | const arr = env.accessors.asArray('1.2.3', '.') 194 | 195 | expect(arr).to.eql(['1','2','3']) 196 | }) 197 | }) 198 | 199 | describe('#asSet', () => { 200 | it('should return a Set of strings', () => { 201 | const set = env.accessors.asSet('1,2,3'); 202 | 203 | expect(set).to.eql(new Set(['1', '2', '3'])); 204 | }); 205 | 206 | it('should return a Set of strings split by period chars', () => { 207 | const set = env.accessors.asSet('1.2.3', '.'); 208 | 209 | expect(set).to.eql(new Set(['1', '2', '3'])); 210 | }); 211 | }); 212 | 213 | describe('#asInt', () => { 214 | it('should return an integer', () => { 215 | const ret = env.accessors.asInt('1') 216 | 217 | expect(ret).to.eql(1) 218 | }) 219 | }) 220 | }) 221 | }) 222 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 7.5.0 (20/05/2024) 2 | * Add `AsSet()` accessor (#173) 3 | 4 | ## 7.4.2 (10/05/2024) 5 | * Fix docstrings for positive/negative int/float validators (#172) 6 | 7 | ## 7.4.1 (29/08/2023) 8 | * Fix 7.4.0 issues with `create-react-app` polyfill (#168) 9 | 10 | ## 7.4.0 (21/08/2023) 11 | * Do not use `process.env` by default in non-Node.js environments (#155) 12 | 13 | ## 7.3.1 (24/04/2023) 14 | * Fix parsing even floating point numbers (#166) 15 | 16 | ## 7.3.0 (06/09/2022) 17 | * Add missing `asEmailString()` typings (#160) 18 | 19 | ## 7.2.0 (01/09/2022) 20 | * Add `asEmailString()` accessor (#146) 21 | 22 | ## 7.1.1 (28/10/2021) 23 | * Fix duplicate identifier error for TypeScript builds (#151) 24 | 25 | ## 7.1.0 (28/10/2021) 26 | * Support type narrowed `process.env`/record and remove unused type (#148) 27 | * Add support for `readonly T[]` generic use with `asEnum()` 28 | 29 | ## 7.0.1 30 | * Fix loose float and int parsing (PR #144) 31 | 32 | ## 7.0.0 (11/11/2020) 33 | * Drop support for Node.js 8 and 13 (support only current, active, and maintenance versions) 34 | * Improve support for browser usage (#138) 35 | * Fix documentation errors (#139) 36 | 37 | ## 6.3.0 (27/07/2020) 38 | * Add `asRegExp` accessor (#136) 39 | * Add better TypeScript example for custom accessors (#133) 40 | 41 | ## 6.2.0 (12/06/2020) 42 | * Add `accessors` property to the public API for use in building `extraAccessors` (#121) 43 | * Add support for logging with a built-in or custom logger (#112) 44 | * Add Node.js v14 to CI builds 45 | * Add single quote rule to `.editorconfig` (#129) 46 | * Add JavaScript example for `extraAccesors` (#129) 47 | * Fix `extraAccessors` args type error (#131) 48 | * Fix types and docs for `asUrlString()` and `asUrlObject()` (#132) 49 | * Update README for `asUrlString()` to mention WHATWG URL behaviour (#126, #129) 50 | 51 | ## 6.1.1 (22/04/2020) 52 | * Fix TS error with *ExtenderTypeOptional* and *ExtenderType* typings (#119) 53 | 54 | ## 6.1.0 (20/04/2020) 55 | * Fix TS error with *extraAccessor* typings (#114) 56 | * Add support for generic types in *asEnum* (#116) 57 | 58 | ## 6.0.4 (04/03/2020) 59 | * Fix compilation error caused by typings generic issue. 60 | 61 | ## 6.0.3 (03/03/2020) 62 | * Fix typings to support `required()`, `convertFromBase64()`, etc. with `ExtensionFn`. 63 | 64 | ## 6.0.2 (29/02/2020) 65 | * Fix `default()` so that it correctly returns an empty string value if provided. 66 | * README improvement by @joh-klein for positive/negative number parsing rules. 67 | 68 | ## 6.0.1 (12/02/2020) 69 | * Fix typings for the `default(value)` function. 70 | 71 | ## 6.0.0 (12/02/2020) 72 | * Add support for setting an example value via the `example(string)` function. 73 | * Passing default values is now performed using the `default(string)` function. 74 | * Defer checks for `required()` until an accessor such as `asString()` is invoked. 75 | * Fix typings issue where `required()` was undefined on a `IPresentVariable`. 76 | * Improve error message output. 77 | 78 | Migration from 5.x to 6.0.0 should be smooth. Change any instance of 79 | `env.get(target, default)` to `env.get(target).default(default)`. For example: 80 | 81 | ```js 82 | // Old 5.x code 83 | const emailAddr = env.get('EMAIL_ADDR', 'admin@example.com').asString() 84 | 85 | // New 6.x compatible code 86 | const emailAddr = env.get('EMAIL_ADDR').default('admin@example.com').asString() 87 | ``` 88 | 89 | ## 5.2.0 (22/11/2019) 90 | * The `required()` function now verifies the variable is not an empty string 91 | 92 | ## 5.1.0 (09/09/2019) 93 | * Ability to add custom accessors in PR #72 (thanks @todofixthis) 94 | * Improved TypeScript tests 95 | * Fixed warning generated by husky 96 | 97 | ## 5.0.0 (14/06/2019) 98 | * Return values from `asArray()` are now more intuitive & consitent 99 | * `asUrlString()` and `asUrlObject`now use the built-in `URL` class in Node.js 100 | to perform validation 101 | * README updated in accordance with changes listed above 102 | 103 | ## 4.1.0 (14/06/2019) 104 | * Add `asPortNumber()` function 105 | * Update documentation structure 106 | 107 | ## 4.0.1 (24/05/2019) 108 | * Add node `process.env` typings to `env.from` 109 | 110 | ## 4.0.0 (09/04/2019) 111 | * Rename `.env.mock()` to `env.from()` 112 | * Change module internals per issue #39 113 | * Update docs related to `env.mock` 114 | 115 | ## 3.5.0 (02/29/2019) 116 | * Update `required()` to support boolean paramter to bypass the check 117 | 118 | ## 3.4.2 (06/11/2018) 119 | * Fix README badge copy/paste error 120 | 121 | ## 3.4.1 (06/11/2018) 122 | * Fix TypeScript definition for "asBoolStrict" function name 123 | 124 | ## 3.4.0 (24/10/2018) 125 | * Add `convertFromBase64()` function 126 | * Enable Greenkeeper 127 | 128 | ## 3.3.0 (26/06/2018) 129 | * Add `asEnum` functionality 130 | 131 | ## 3.2.0 (15/06/2018) 132 | * Remove @types/node dependency 133 | 134 | ## 3.1.0 (11/12/2017) 135 | * Update typings to correctly handle default values for numeric types. 136 | * Ensure an error is thrown when `asArray` does not detect at least a single non-empty value. 137 | 138 | ## 3.0.2 (19/10/2017) 139 | * Restore support for use in browser based applications 140 | 141 | ## 3.0.1 (19/10/2017) 142 | * Fix bug that caused default values to be ignored 143 | 144 | ## 3.0.0 (13/10/2017) 145 | * Public API no longer is a function, instead exposes two functions, `mock` and `get` 146 | * Drop support for Node.js versions less than 4.0 147 | * Rename `asPositiveInt` to `asIntPositive` 148 | * Rename `asNegativeInt` to `asIntNegative` 149 | * Rename `asStrictBool` to `asBoolStrict` 150 | * Add `asFloatPositive` and `asFloatNegative` 151 | * Add `asUrlString` and `asUrlObject` 152 | * Refactor code with consistent errors and structure 153 | * Use `standard` for code quality and formatting 154 | 155 | ## 2.4.3 (5/04/2017) 156 | * Update with build, coverage, and version information badges 157 | 158 | ## 2.4.2 (19/12/2016) 159 | * Fix TypeScript definition file 160 | 161 | ## 2.4.1 (15/12/2016) 162 | * Remove unnecessary code path 163 | 164 | ## 2.4.0 (15/12/2016) 165 | * Add `asArray([delimeter])` to read environment variables as an array by splitting 166 | the varible string on each instance of _delimeter_; 167 | * Add `asJsonArray()` to read in an environment variable that contains a JSON 168 | Array. Similar to `asJson()`, but ensures the variable is an Array. 169 | * Add `asJsonObject()` to read in an environment variable that contains a JSON 170 | Object. Similar to `asJson()`, but ensures the variable is an Object. 171 | 172 | ## 2.3.0 & 2.3.1 (12/12/2016) 173 | * Add typings support for TypeScript 174 | 175 | ## 2.2.0 (28/10/2016) 176 | * Thanks to @itavy for a patch for our _asBool_ parsing and adding the new 177 | _asStrictBool_ function 178 | 179 | ## 2.1.0 (25/10/2016) 180 | * Added _env.mock_ PR from @MikeyBurkman to improve testability 181 | 182 | ## 2.0.0 (27/07/2016) 183 | * Add CI process for node 6, 5, 4, and 0.10 184 | * Add chained functions for variable validations 185 | * Add assertions for _required()_ and various type checks, e.g _asPositiveInt(_) 186 | * Remove node 0.8.x support 187 | * Remove old pattern of returning variables directly 188 | * Continue support for defaults from 1.X 189 | 190 | ## <2.0.0 191 | * Venture forth at thine own risk, for here be dragons 192 | -------------------------------------------------------------------------------- /env-var.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | 4 | import { URL } from 'url'; 5 | 6 | 7 | type PublicAccessors = { 8 | /** 9 | * Converts a number to an integer and verifies it's in port ranges 0-65535 10 | */ 11 | asPortNumber: (input: string) => number 12 | 13 | /** 14 | * Attempt to parse the variable to a float. Throws an exception if parsing fails. 15 | */ 16 | asFloat: (input: string) => number; 17 | 18 | /** 19 | * Performs the same task as asFloat(), but also verifies that the number is positive (greater than or equal to zero). 20 | */ 21 | asFloatPositive: (input: string) => number; 22 | 23 | /** 24 | * Performs the same task as asFloat(), but also verifies that the number is negative (less than or equal to zero). 25 | */ 26 | asFloatNegative: (input: string) => number; 27 | 28 | /** 29 | * Attempt to parse the variable to an integer. Throws an exception if parsing fails. 30 | * This is a strict check, meaning that if the process.env value is 1.2, an exception will be raised rather than rounding up/down. 31 | */ 32 | asInt: (input: string) => number; 33 | 34 | /** 35 | * Performs the same task as asInt(), but also verifies that the number is positive (greater than or equal to zero). 36 | */ 37 | asIntPositive: (input: string) => number; 38 | 39 | /** 40 | * Performs the same task as asInt(), but also verifies that the number is negative (less than or equal to zero). 41 | */ 42 | asIntNegative: (input: string) => number; 43 | 44 | /** 45 | * Return the variable value as a String. Throws an exception if value is not a String. 46 | * It's highly unlikely that a variable will not be a String since all process.env entries you set in bash are Strings by default. 47 | */ 48 | asString: (input: string) => string; 49 | 50 | /** 51 | * Return the variable value as an Email. Throws an exception if value is not an Email. 52 | */ 53 | asEmailString: (input: string) => string; 54 | 55 | /** 56 | * Attempt to parse the variable to a JSON Object or Array. Throws an exception if parsing fails. 57 | */ 58 | asJson: (input: string) => Object|Array; 59 | 60 | /** 61 | * The same as asJson but checks that the data is a JSON Array, e.g [1,2]. 62 | */ 63 | asJsonArray: (input: string) => Array; 64 | 65 | /** 66 | * The same as asJson but checks that the data is a JSON Object, e.g {a: 1}. 67 | */ 68 | asJsonObject: (input: string) => Object; 69 | 70 | /** 71 | * Reads an environment variable as a string, then splits it on each occurrence of the specified delimiter. 72 | * By default a comma is used as the delimiter. For example a var set to "1,2,3" would become ['1', '2', '3']. 73 | */ 74 | asArray: (input: string, delimiter?: string) => Array; 75 | 76 | /** 77 | * Reads an environment variable as a string, then splits it on each occurrence of the specified delimiter. 78 | * By default a comma is used as the delimiter. For example a var set to "1,2,3" would become new Set(['1', '2', '3']). 79 | */ 80 | asSet: (input: string, delimiter?: string) => Set; 81 | 82 | /** 83 | * Attempt to parse the variable to a Boolean. Throws an exception if parsing fails. 84 | * The var must be set to either "true", "false" (upper or lowercase), 0 or 1 to succeed. 85 | */ 86 | asBool: (input: string) => boolean; 87 | 88 | /** 89 | * Attempt to parse the variable to a Boolean. Throws an exception if parsing fails. 90 | * The var must be set to either "true" or "false" (upper or lowercase) to succeed. 91 | */ 92 | asBoolStrict: (input: string) => boolean; 93 | 94 | /** 95 | * Verifies that the variable is a valid URL string and returns the validated 96 | * string. The validation is performed by passing the URL string to the 97 | * Node.js URL constructor. 98 | * 99 | * Note that URLs without paths will have a default path `/` appended when read, e.g. 100 | * `https://api.acme.org` would become `https://api.acme.org/`. Always use URL 101 | * safe utilities included in the Node.js URL module to create 102 | * valid URL strings, instead of error prone string concatenation. 103 | */ 104 | asUrlString: (input: string) => string; 105 | 106 | /** 107 | * Verifies that the variable is a valid URL string using the same method as 108 | * `asUrlString()`, but instead returns the resulting URL instance. 109 | */ 110 | asUrlObject: (input: string) => URL; 111 | 112 | /** 113 | * Verifies that the var being accessed is one of the given values 114 | */ 115 | asEnum: (input: string, validValues: readonly T[]|T[]) => T; 116 | } 117 | 118 | interface VariableAccessors { 119 | /** 120 | * Converts a number to an integer and verifies it's in port ranges 0-65535 121 | */ 122 | asPortNumber: () => AlternateType extends undefined ? undefined|number : number 123 | 124 | /** 125 | * Attempt to parse the variable to a float. Throws an exception if parsing fails. 126 | */ 127 | asFloat: () => AlternateType extends undefined ? undefined|number : number; 128 | 129 | /** 130 | * Performs the same task as asFloat(), but also verifies that the number is positive (greater than or equal to zero). 131 | */ 132 | asFloatPositive: () => AlternateType extends undefined ? undefined|number : number; 133 | 134 | /** 135 | * Performs the same task as asFloat(), but also verifies that the number is negative (less than or equal to zero). 136 | */ 137 | asFloatNegative: () => AlternateType extends undefined ? undefined|number : number; 138 | 139 | /** 140 | * Attempt to parse the variable to an integer. Throws an exception if parsing fails. 141 | * This is a strict check, meaning that if the process.env value is 1.2, an exception will be raised rather than rounding up/down. 142 | */ 143 | asInt: () => AlternateType extends undefined ? undefined|number : number; 144 | 145 | /** 146 | * Performs the same task as asInt(), but also verifies that the number is positive (greater than or equal to zero). 147 | */ 148 | asIntPositive: () => AlternateType extends undefined ? undefined|number : number; 149 | 150 | /** 151 | * Performs the same task as asInt(), but also verifies that the number is negative (less than or equal to zero). 152 | */ 153 | asIntNegative: () => AlternateType extends undefined ? undefined|number : number; 154 | 155 | /** 156 | * Return the variable value as a String. Throws an exception if value is not a String. 157 | * It's highly unlikely that a variable will not be a String since all process.env entries you set in bash are Strings by default. 158 | */ 159 | asString: () => AlternateType extends undefined ? undefined|string : string; 160 | 161 | /** 162 | * Return the variable value as an Email. Throws an exception if value is not an Email. 163 | */ 164 | asEmailString: () => AlternateType extends undefined ? undefined|string : string; 165 | 166 | /** 167 | * Attempt to parse the variable to a JSON Object or Array. Throws an exception if parsing fails. 168 | */ 169 | asJson: () => AlternateType extends undefined ? undefined|Object|Array : Object|Array; 170 | 171 | /** 172 | * The same as asJson but checks that the data is a JSON Array, e.g [1,2]. 173 | */ 174 | asJsonArray: () => AlternateType extends undefined ? undefined|Array : Array; 175 | 176 | /** 177 | * The same as asJson but checks that the data is a JSON Object, e.g {a: 1}. 178 | */ 179 | asJsonObject: () => AlternateType extends undefined ? undefined|Object : Object; 180 | 181 | /** 182 | * Reads an environment variable as a string, then splits it on each occurrence of the specified delimiter. 183 | * By default a comma is used as the delimiter. For example a var set to "1,2,3" would become ['1', '2', '3']. 184 | */ 185 | asArray: (delimiter?: string) => AlternateType extends undefined ? undefined|Array : Array; 186 | 187 | /** 188 | * Reads an environment variable as a string, then splits it on each occurrence of the specified delimiter. 189 | * By default a comma is used as the delimiter. For example a var set to "1,2,3" would become new Set(['1', '2', '3']). 190 | */ 191 | asSet: (delimiter?: string) => AlternateType extends undefined ? undefined|Set : Set; 192 | 193 | /** 194 | * Attempt to parse the variable to a Boolean. Throws an exception if parsing fails. 195 | * The var must be set to either "true", "false" (upper or lowercase), 0 or 1 to succeed. 196 | */ 197 | asBool: () => AlternateType extends undefined ? undefined|boolean : boolean; 198 | 199 | /** 200 | * Attempt to parse the variable to a Boolean. Throws an exception if parsing fails. 201 | * The var must be set to either "true" or "false" (upper or lowercase) to succeed. 202 | */ 203 | asBoolStrict: () => AlternateType extends undefined ? undefined|boolean : boolean; 204 | 205 | /** 206 | * Verifies that the variable is a valid URL string and returns the validated 207 | * string. The validation is performed by passing the URL string to the 208 | * Node.js URL constructor. 209 | * 210 | * Note that URLs without paths will have a default path `/` appended when read, e.g. 211 | * `https://api.acme.org` would become `https://api.acme.org/`. Always use URL 212 | * safe utilities included in the Node.js URL module to create 213 | * valid URL strings, instead of error prone string concatenation. 214 | */ 215 | asUrlString: () => AlternateType extends undefined ? undefined|string : string; 216 | 217 | /** 218 | * Verifies that the variable is a valid URL string using the same method as 219 | * `asUrlString()`, but instead returns the resulting URL instance. 220 | */ 221 | asUrlObject: () => AlternateType extends undefined ? undefined|URL : URL; 222 | 223 | /** 224 | * Verifies that the var being accessed is one of the given values 225 | */ 226 | asEnum: (validValues: readonly T[]|T[]) => AlternateType extends undefined ? undefined|T : T; 227 | 228 | /** 229 | * Verifies that the variable is a valid regular expression and returns the 230 | * validated expression as a RegExp instance. 231 | */ 232 | asRegExp: (flags?: string) => AlternateType extends undefined ? undefined|RegExp : RegExp; 233 | } 234 | 235 | interface IPresentVariable extends VariableAccessors { 236 | /** 237 | * Converts a bas64 environment variable to ut8 238 | */ 239 | convertFromBase64: () => IPresentVariable & ExtenderType 240 | 241 | /** 242 | * Provide an example value that can be used in error output if the variable 243 | * is not set, or is set to an invalid value 244 | */ 245 | example: (example: string) => IPresentVariable & ExtenderType 246 | 247 | /** 248 | * Set a default value for this variable. This will be used if a value is not 249 | * set in the process environment 250 | */ 251 | default: (value: string|number|Record|Array) => IPresentVariable & ExtenderType; 252 | 253 | /** 254 | * Ensures the variable is set on process.env. If it's not set an exception 255 | * will be thrown. Can pass false to bypass the check. 256 | */ 257 | required: (isRequired?: boolean) => IPresentVariable & ExtenderType; 258 | } 259 | 260 | interface IOptionalVariable extends VariableAccessors { 261 | /** 262 | * Decodes a base64-encoded environment variable 263 | */ 264 | convertFromBase64: () => IOptionalVariable & ExtenderTypeOptional; 265 | 266 | /** 267 | * Provide an example value that can be used in error output if the variable 268 | * is not set, or is set to an invalid value 269 | */ 270 | example: (value: string) => IOptionalVariable & ExtenderTypeOptional; 271 | 272 | /** 273 | * Set a default value for this variable. This will be used if a value is not 274 | * set in the process environment 275 | */ 276 | default: (value: string|number|Record|Array) => IPresentVariable & ExtenderType; 277 | 278 | /** 279 | * Ensures the variable is set on process.env. If it's not set an exception will be thrown. 280 | * Can pass false to bypass the check 281 | */ 282 | required: (isRequired?: boolean) => IPresentVariable & ExtenderType; 283 | } 284 | 285 | export class EnvVarError extends Error {} 286 | 287 | interface IEnv { 288 | /** 289 | * Returns an object containing all current environment variables 290 | */ 291 | get (): Container, 292 | 293 | /** 294 | * Gets an environment variable that is possibly not set to a value 295 | */ 296 | get (varName: keyof Container): OptionalVariable; 297 | 298 | /** 299 | * Returns a new env-var instance, where the given object is used for the environment variable mapping. 300 | * Use this when writing unit tests or in environments outside node.js. 301 | */ 302 | from(values: V, extensions?: T, logger?: LoggerFn): IEnv< 303 | IOptionalVariable & ExtenderTypeOptional, 304 | V 305 | >; 306 | 307 | accessors: PublicAccessors 308 | 309 | /** 310 | * This is the error type used to represent error returned by this module. 311 | * Useful for filtering errors and responding appropriately. 312 | */ 313 | EnvVarError: EnvVarError 314 | } 315 | 316 | // Used internally only to support extension fns 317 | type RestParams any> = F extends (value: string, ...args: infer P) => any ? P : any[]; 318 | type ExtenderType = { [P in keyof T]: (...args: RestParams) => ReturnType } 319 | type ExtenderTypeOptional = { [P in keyof T]: (...args: RestParams) => ReturnType|undefined } 320 | 321 | export type Extensions = { 322 | [key: string]: ExtensionFn 323 | } 324 | 325 | export type LoggerFn = (varname: string, str: string) => void 326 | export type RaiseErrorFn = (error: string) => void 327 | export type ExtensionFn = (value: string, ...args: any[]) => T 328 | 329 | export const accessors: PublicAccessors 330 | 331 | export function logger (varname: string, str: string): void 332 | 333 | type IDefaultEnv = IEnv 334 | export const get: IDefaultEnv['get'] 335 | export const from: IDefaultEnv['from'] 336 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | A complete listing of the `env-var` API. 4 | 5 | ## Structure 6 | 7 | * module (env-var) 8 | * [from()](#fromvalues-extraaccessors-logger) 9 | * [get()](#getvarname) 10 | * [variable](#variable) 11 | * [example(string)](#examplestring) 12 | * [default(string)](#defaultdefaultvalue--string) 13 | * [required()](#requiredisrequired--true) 14 | * [covertFromBase64()](#convertfrombase64) 15 | * [asArray()](#asarraydelimiter-string) 16 | * [asBool()](#asbool) 17 | * [asBoolStrict()](#asboolstrict) 18 | * [asEmailString()](#asemailstring) 19 | * [asEnum()](#asenumvalidvalues-string) 20 | * [asFloat()](#asfloat) 21 | * [asFloatNegative()](#asfloatnegative) 22 | * [asFloatPositive()](#asfloatpositive) 23 | * [asInt()](#asint) 24 | * [asIntNegative()](#asintnegative) 25 | * [asIntPositive()](#asintpositive) 26 | * [asJson()](#asjson) 27 | * [asJsonArray()](#asjsonarray) 28 | * [asJsonObject()](#asjsonobject) 29 | * [asPortNumber()](#asportnumber) 30 | * [asRegExp()](#asregexpflags-string) 31 | * [asString()](#asstring) 32 | * [asUrlObject()](#asurlobject) 33 | * [asUrlString()](#asurlstring) 34 | * [EnvVarError()](#envvarerror) 35 | * [accessors](#accessors) 36 | 37 | ## from(values, extraAccessors, logger) 38 | 39 | This function is useful if you are not in a typical Node.js environment, want to set defaults, or for 40 | testing. It allows you to generate an `env-var` instance that reads from the 41 | given `values` instead of the default `process.env` Object. 42 | 43 | ```js 44 | const env = require('env-var').from({ 45 | API_BASE_URL: 'https://my.api.com/' 46 | }) 47 | 48 | // apiUrl will be 'https://my.api.com/' 49 | const apiUrl = env.get('API_BASE_URL').asUrlString() 50 | ``` 51 | 52 | When calling `env.from()` you can also pass an optional parameter containing 53 | custom accessors that will be attached to any variables returned by that 54 | `env-var` instance. This feature is explained in the 55 | [extraAccessors section](#extraAccessors) of these docs. 56 | 57 | Logging can be enabled by passing a logger function that matches the signature: 58 | 59 | ```js 60 | /** 61 | * Logs the provided string argument 62 | * @param {String} varname 63 | * @param {String} str 64 | */ 65 | function yourLoggerFn (varname, str) { 66 | // varname is the name of the variable being read, e.g "API_KEY" 67 | // str is the log message, e.g "verifying variable value is not empty" 68 | } 69 | ``` 70 | 71 | ## get(varname) 72 | 73 | This function has two behaviours: 74 | 75 | 1. Passing a string argument will read that value from the environment 76 | 2. If no argument is passed it will return the entire environment object 77 | 78 | Examples: 79 | 80 | ```js 81 | const env = require('env-var') 82 | 83 | // #1 - Read the requested variable and parse it to a positive integer 84 | const limit = env.get('MAX_CONNECTIONS').asIntPositive() 85 | 86 | // #2 - Returns the entire process.env object 87 | const allVars = env.get() 88 | ``` 89 | 90 | ### variable 91 | 92 | A variable is returned when `env.get(varname)` is called. It exposes the following 93 | functions to validate and access the underlying value, set a default, or set 94 | an example value: 95 | 96 | #### example(string) 97 | 98 | Allows a developer to provide an example of a valid value for the environment 99 | variable. If the variable is not set (and `required()` was called), or the 100 | variable is set incorrectly this will be included in error output to help 101 | developers diagnose the error. 102 | 103 | For example: 104 | 105 | ```js 106 | const env = require('env-var') 107 | 108 | const ADMIN_EMAIL = env.get('ADMIN_EMAIL') 109 | .required() 110 | .example('admin@example.com') 111 | .asString() 112 | ``` 113 | 114 | If *ADMIN_EMAIL* was not set this code would throw an error similar to that 115 | below to help a developer diagnose the issue: 116 | 117 | ``` 118 | env-var: "ADMIN_EMAIL" is a required variable, but it was not set. An example 119 | of a valid value would be "admin@example.com" 120 | ``` 121 | 122 | #### default(defaultValue: string) 123 | 124 | Allows a default value to be provided for use if the desired environment 125 | variable is not set in the program environment, i.e not present on `process.env`. 126 | 127 | Example: 128 | 129 | ```js 130 | const env = require('env-var') 131 | 132 | // Use POOL_SIZE if set, else use a value of 10 133 | const POOL_SIZE = env.get('POOL_SIZE').default('10').asIntPositive() 134 | ``` 135 | 136 | #### required(isRequired = true) 137 | 138 | Ensure the variable is set on `process.env`. If the variable is not set, or is 139 | set to an empty value, this function will cause an `EnvVarError` to be thrown 140 | when you attempt to read the value using `asString` or a similar function. 141 | 142 | The `required()` check can be bypassed by passing `false`, i.e 143 | `required(false)` 144 | 145 | Example: 146 | 147 | ```js 148 | const env = require('env-var') 149 | 150 | // Get the value of NODE_ENV as a string. Could be undefined since we're 151 | // not calling required() before asString() 152 | const NODE_ENV = env.get('NODE_ENV').asString() 153 | 154 | // Read PORT variable and ensure it's in a valid port range. If it's not in 155 | // valid port ranges, not set, or empty an EnvVarError will be thrown 156 | const PORT = env.get('PORT').required().asPortNumber() 157 | 158 | // If mode is production then this is required 159 | const SECRET = env.get('SECRET').required(NODE_ENV === 'production').asString() 160 | ``` 161 | 162 | #### convertFromBase64() 163 | 164 | It's a common need to set an environment variable in base64 format. This 165 | function can be used to decode a base64 environment variable to UTF8. 166 | 167 | For example if we run the script script below, using the command `DB_PASSWORD= 168 | $(echo -n 'secret_password' | base64) node`, we'd get the following results: 169 | 170 | ```js 171 | console.log(process.env.DB_PASSWORD) // prints "c2VjcmV0X3Bhc3N3b3Jk" 172 | 173 | // dbpass will contain the converted value of "secret_password" 174 | const dbpass = env.get('DB_PASSWORD').convertFromBase64().asString() 175 | ``` 176 | 177 | #### asArray([delimiter: string]) 178 | 179 | Reads an environment variable as a string, then splits it on each occurence of 180 | the specified _delimiter_. By default a comma is used as the delimiter. For 181 | example a var set to "1,2,3" would become ['1', '2', '3']. Example outputs for 182 | specific values are: 183 | 184 | * Reading `MY_ARRAY=''` results in `[]` 185 | * Reading `MY_ARRAY='1'` results in `['1']` 186 | * Reading `MY_ARRAY='1,2,3'` results in `['1', '2', '3']` 187 | 188 | #### asBool() 189 | 190 | Attempt to parse the variable to a Boolean. Throws an exception if parsing 191 | fails. The var must be set to either "true", "false" (upper or lowercase), 192 | 0 or 1 to succeed. 193 | 194 | #### asBoolStrict() 195 | 196 | Attempt to parse the variable to a Boolean. Throws an exception if parsing 197 | fails. The var must be set to either "true" or "false" (upper or lowercase) to 198 | succeed. 199 | 200 | #### asEmailString() 201 | 202 | Read the variable and validate that it's an email address. Throws an exception 203 | if the value is not an email address. 204 | 205 | #### asEnum(validValues: string[]) 206 | 207 | Converts the value to a string, and matches against the list of valid values. 208 | If the value is not valid, an error will be raised describing valid input. 209 | 210 | #### asFloat() 211 | 212 | Attempt to parse the variable to a float. Throws an exception if parsing fails. 213 | 214 | #### asFloatNegative() 215 | 216 | Performs the same task as _asFloat()_, but also verifies that the number is 217 | negative (less than or equal to zero). 218 | 219 | #### asFloatPositive() 220 | 221 | Performs the same task as _asFloat()_, but also verifies that the number is 222 | positive (greater than or equal to zero). 223 | 224 | #### asInt() 225 | 226 | Attempt to parse the variable to an integer. Throws an exception if parsing 227 | fails. This is a strict check, meaning that if the *process.env* value is "1.2", 228 | an exception will be raised rather than rounding up/down. 229 | 230 | #### asIntNegative() 231 | 232 | Performs the same task as _asInt()_, but also verifies that the number is 233 | negative (less than or equal to zero). 234 | 235 | #### asIntPositive() 236 | 237 | Performs the same task as _asInt()_, but also verifies that the number is 238 | positive (greater than or equal to zero). 239 | 240 | #### asJson() 241 | 242 | Attempt to parse the variable to a JSON Object or Array. Throws an exception if 243 | parsing fails. 244 | 245 | #### asJsonArray() 246 | 247 | The same as _asJson_ but verifies that the data is a JSON Array, e.g. [1,2]. 248 | 249 | #### asJsonObject() 250 | 251 | The same as _asJson_ but verifies that the data is a JSON Object, e.g. {a: 1}. 252 | 253 | #### asPortNumber() 254 | 255 | Converts the value of the environment variable to an integer and verifies it's 256 | within the valid port range of 0-65535. As a result well known ports are 257 | considered valid by this function. 258 | 259 | #### asRegExp([flags: string]) 260 | 261 | Read in the variable and construct a [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) 262 | instance using its value. An optional `flags` argument is supported. The string 263 | passed for `flags` is passed as the second argument to the `RegExp` constructor. 264 | 265 | #### asString() 266 | 267 | Return the variable value as a String. Throws an exception if value is not a 268 | String. It's highly unlikely that a variable will not be a String since all 269 | `process.env` entries you set in bash are Strings by default. 270 | 271 | #### asUrlObject() 272 | 273 | Verifies that the variable is a valid URL string using the same method as 274 | `asUrlString()`, but instead returns the resulting URL instance. It uses the WHATWG URL constructor. 275 | 276 | #### asUrlString() 277 | 278 | Verifies that the variable is a valid URL string and returns the validated 279 | string. The validation is performed by passing the URL string to the 280 | WHATWG URL constructor. 281 | 282 | Note that URLs without paths will have a default path `/` appended when read, e.g. 283 | `https://api.acme.org` would become `https://api.acme.org/`. Always use URL 284 | safe utilities included in the 285 | [Node.js URL module](https://nodejs.org/docs/latest/api/url.html) to create 286 | valid URL strings, instead of error prone string concatenation. 287 | 288 | ## EnvVarError() 289 | 290 | This is the error class used to represent errors raised by this module. Sample 291 | usage: 292 | 293 | ```js 294 | const env = require('env-var') 295 | let value = null 296 | 297 | try { 298 | // will throw if you have not set this variable 299 | value = env.get('MISSING_VARIABLE').required().asString() 300 | 301 | // if catch error is set, we'll end up throwing here instead 302 | throw new Error('some other error') 303 | } catch (e) { 304 | if (e instanceof env.EnvVarError) { 305 | console.log('we got an env-var error', e) 306 | } else { 307 | console.log('we got some error that wasn\'t an env-var error', e) 308 | } 309 | } 310 | ``` 311 | 312 | ### Examples 313 | 314 | ```js 315 | const env = require('env-var'); 316 | 317 | // Normally these would be set using "export VARNAME" or similar in bash 318 | process.env.STRING = 'test'; 319 | process.env.INTEGER = '12'; 320 | process.env.BOOL = 'false'; 321 | process.env.JSON = '{"key":"value"}'; 322 | process.env.COMMA_ARRAY = '1,2,3'; 323 | process.env.DASH_ARRAY = '1-2-3'; 324 | 325 | // The entire process.env object 326 | const allVars = env.get(); 327 | 328 | // Returns a string. Throws an exception if not set or empty 329 | const stringVar = env.get('STRING').required().asString(); 330 | 331 | // Returns an int, undefined if not set, or throws if set to a non integer value 332 | const intVar = env.get('INTEGER').asInt(); 333 | 334 | // Return a float, or 23.2 if not set 335 | const floatVar = env.get('FLOAT').default('23.2').asFloat(); 336 | 337 | // Return a Boolean. Throws an exception if not set or parsing fails 338 | const boolVar = env.get('BOOL').required().asBool(); 339 | 340 | // Returns a JSON Object, undefined if not set, or throws if set to invalid JSON 341 | const jsonVar = env.get('JSON').asJson(); 342 | 343 | // Returns an array if defined, or undefined if not set 344 | const commaArray = env.get('COMMA_ARRAY').asArray(); 345 | 346 | // Returns an array if defined, or undefined if not set 347 | const commaArray = env.get('DASH_ARRAY').asArray('-'); 348 | 349 | // Returns the enum value if it's one of dev, test, or live 350 | const enumVal = env.get('ENVIRONMENT').asEnum(['dev', 'test', 'live']) 351 | ``` 352 | 353 | ## accessors 354 | 355 | A property that exposes the built-in accessors that this module uses to parse 356 | and validate values. These work similarly to the *asString()* and other 357 | accessors exposed on the *variable* type documented above, however they accept 358 | a *String* as their first argument, e.g: 359 | 360 | ```js 361 | const env = require('env-var') 362 | 363 | // Validate that the string is JSON, and return the parsed result 364 | const myJsonDirectAccessor = env.accessors.asJson(process.env.SOME_JSON) 365 | 366 | const myJsonViaEnvVar = env.get('SOME_JSON').asJson() 367 | ``` 368 | 369 | All of the documented *asX()* accessors above are available. These are useful 370 | if you need to build a custom accessor using the *extraAccessors* functionality 371 | described below. 372 | 373 | ## extraAccessors 374 | 375 | When calling `from()` you can also pass an optional parameter containing 376 | additional accessors that will be attached to any variables gotten by that 377 | `env-var` instance. 378 | 379 | Accessor functions must accept at least one argument: 380 | 381 | - `{*} value`: The value that the accessor should process. 382 | 383 | **Important:** Do not assume that `value` is a string! 384 | 385 | Example: 386 | ```js 387 | const { from } = require('env-var') 388 | 389 | // Environment variable that we will use for this example: 390 | process.env.ADMIN = 'admin@example.com' 391 | 392 | // Add an accessor named 'asEmail' that verifies that the value is a 393 | // valid-looking email address. 394 | const env = from(process.env, { 395 | asEmail: (value) => { 396 | const split = String(value).split('@') 397 | 398 | // Validating email addresses is hard. 399 | if (split.length !== 2) { 400 | throw new Error('must contain exactly one "@"') 401 | } 402 | 403 | return value 404 | } 405 | }) 406 | 407 | // We specified 'asEmail' as the name for the accessor above, so now 408 | // we can call `asEmail()` like any other accessor. 409 | let validEmail = env.get('ADMIN').asEmail() 410 | ``` 411 | 412 | The accessor function may accept additional arguments if desired; these must be 413 | provided explicitly when the accessor is invoked. 414 | 415 | For example, we can modify the `asEmail()` accessor from above so that it 416 | optionally verifies the domain of the email address: 417 | ```js 418 | const { from } = require('env-var') 419 | 420 | // Environment variable that we will use for this example: 421 | process.env.ADMIN = 'admin@example.com' 422 | 423 | // Add an accessor named 'asEmail' that verifies that the value is a 424 | // valid-looking email address. 425 | // 426 | // Note that the accessor function also accepts an optional second 427 | // parameter `requiredDomain` which can be provided when the accessor is 428 | // invoked (see below). 429 | const env = from(process.env, { 430 | asEmail: (value, requiredDomain) => { 431 | const split = String(value).split('@') 432 | 433 | // Validating email addresses is hard. 434 | if (split.length !== 2) { 435 | throw new Error('must contain exactly one "@"') 436 | } 437 | 438 | if (requiredDomain && (split[1] !== requiredDomain)) { 439 | throw new Error(`must end with @${requiredDomain}`) 440 | } 441 | 442 | return value 443 | } 444 | }) 445 | 446 | // We specified 'asEmail' as the name for the accessor above, so now 447 | // we can call `asEmail()` like any other accessor. 448 | // 449 | // `env-var` will provide the first argument for the accessor function 450 | // (`value`), but we declared a second argument `requiredDomain`, which 451 | // we can provide when we invoke the accessor. 452 | 453 | // Calling the accessor without additional parameters accepts an email 454 | // address with any domain. 455 | let validEmail = env.get('ADMIN').asEmail() 456 | 457 | // If we specify a parameter, then the email address must end with the 458 | // domain we specified. 459 | let invalidEmail = env.get('ADMIN').asEmail('github.com') 460 | ``` 461 | 462 | This feature is also available for Typescript users. The `ExtensionFn` type is 463 | exposed to help in the creation of these new accessors. 464 | 465 | ```ts 466 | import { from, ExtensionFn, EnvVarError } from 'env-var' 467 | 468 | // Environment variable that we will use for this example: 469 | process.env.ADMIN = 'admin@example.com' 470 | 471 | const asEmail: ExtensionFn = (value) => { 472 | const split = String(value).split('@') 473 | 474 | // Validating email addresses is hard. 475 | if (split.length !== 2) { 476 | throw new Error('must contain exactly one "@"') 477 | } 478 | 479 | return value 480 | } 481 | 482 | const env = from(process.env, { 483 | asEmail 484 | }) 485 | 486 | // Returns the email string if it's valid, otherwise it will throw 487 | env.get('ADMIN').asEmail() 488 | ``` 489 | 490 | You can view an example of composing built-in accessors made available by 491 | `env.accessors` in an extra accessor at *examples/custom-accessor.js*. 492 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env mocha */ 4 | 5 | const expect = require('chai').expect 6 | const URL = require('url').URL 7 | 8 | describe('env-var', function () { 9 | // Some default env vars for tests 10 | var TEST_VARS = { 11 | VALID_BASE_64: 'aGVsbG8=', // "hello" in base64 12 | INVALID_BASE_64: 'a|GV-sb*G8=', 13 | STRING: 'oh hai', 14 | FLOAT: '12.43', 15 | FLOAT_INTEGER: '1.0', 16 | INTEGER: '5', 17 | BOOL: 'false', 18 | JSON: '{"name":"value"}', 19 | JSON_OBJECT: '{"name":"value"}', 20 | JSON_ARRAY: '[1,2,3]', 21 | COMMA_ARRAY: '1,2,3', 22 | EMPTY_ARRAY: '', 23 | DUPLICATE_ARRAY: '1,1,2,2,3,3', 24 | ARRAY_WITHOUT_DELIMITER: 'value', 25 | ARRAY_WITH_DELIMITER: 'value,', 26 | ARRAY_WITH_DELIMITER_PREFIX: ',value', 27 | DASH_ARRAY: '1-2-3', 28 | URL: 'http://google.com/', 29 | EMAIL: 'email@example.com', 30 | ENUM: 'VALID', 31 | EMPTY_STRING: '', 32 | EMPTY_STRING_WITH_WHITESPACE: ' ' 33 | } 34 | 35 | var mod = require('../env-var.js') 36 | 37 | beforeEach(function () { 38 | // Inject our dummy vars 39 | Object.keys(TEST_VARS).forEach(function (key) { 40 | process.env[key] = TEST_VARS[key] 41 | }) 42 | }) 43 | 44 | describe('getting process.env', function () { 45 | it('should return process.env object when no args provided', function () { 46 | var res = mod.get() 47 | 48 | expect(res).to.be.an('object'); 49 | 50 | ['STRING', 'FLOAT', 'INTEGER', 'BOOL', 'JSON'].forEach(function (name) { 51 | expect(res[name]).to.not.equal(undefined) 52 | }) 53 | }) 54 | }) 55 | 56 | describe('#get(target, default) deprecation message', () => { 57 | it('should throw an error if using pre 6.x syntax', () => { 58 | expect(() => { 59 | mod.get('SOMETHING', 'default somthing value').asString() 60 | }).to.throw('env-var: It looks like you passed more than one argument to env.get(). Since env-var@6.0.0 this is no longer supported. To set a default value use env.get(TARGET).default(DEFAULT)') 61 | }) 62 | }) 63 | 64 | describe('default values', function () { 65 | it('should return the default empty string value', () => { 66 | const ret = mod.get('XXX_NOT_DEFINED').default('').asString() 67 | 68 | expect(ret).to.equal('') 69 | }) 70 | 71 | it('should return the default', function () { 72 | const ret = mod.get('XXX_NOT_DEFINED').default('default').asString() 73 | 74 | expect(ret).to.equal('default') 75 | }) 76 | 77 | it('should support passing a number type', () => { 78 | expect(mod.get('MISSING_NO').default(42).asString()).to.equal('42') 79 | }) 80 | 81 | it('should support passing a number type and returning as a number', () => { 82 | expect(mod.get('MISSING_NO').default(42).asIntPositive()).to.equal(42) 83 | }) 84 | 85 | it('should support passing objects', () => { 86 | const tpl = { 87 | name: 'ok' 88 | } 89 | expect(mod.get('MISSING_OBJECT').default(tpl).asJsonObject()).to.deep.equal(tpl) 90 | }) 91 | 92 | it('should support passing arrays', () => { 93 | const tpl = [1, 2, 3] 94 | expect(mod.get('MISSING_ARRAY').default(tpl).asJsonArray()).to.deep.equal(tpl) 95 | }) 96 | 97 | it('should error on null', () => { 98 | expect(() => { 99 | expect(mod.get('MISSING_NULL_DEFAULT').default(null).asJsonArray()) 100 | }).to.throw('env-var: values passed to default() must be of Number, String, Array, or Object type') 101 | }) 102 | }) 103 | 104 | describe('#convertFromBase64', function () { 105 | it('should return a value from a converted base64 string', function () { 106 | const res = mod.get('VALID_BASE_64').convertFromBase64().asString() 107 | 108 | expect(res).to.be.a('string') 109 | expect(res).to.equal('hello') 110 | }) 111 | 112 | it('should throw an error due to malformed base64', function () { 113 | expect(() => { 114 | mod.get('INVALID_BASE_64').convertFromBase64().asString() 115 | }).throw(/"INVALID_BASE_64" should be a valid base64 string if using convertFromBase64/g) 116 | }) 117 | }) 118 | 119 | describe('#asEnum', function () { 120 | it('should return a string', function () { 121 | expect(mod.get('ENUM').asEnum(['VALID'])).to.be.a('string') 122 | expect(mod.get('ENUM').asEnum(['VALID'])).to.equal('VALID') 123 | }) 124 | 125 | it('should throw when value is not expected', function () { 126 | expect(() => { 127 | expect(mod.get('ENUM').asEnum(['INVALID'])) 128 | }).to.throw('env-var: "ENUM" should be one of [INVALID]') 129 | }) 130 | }) 131 | 132 | describe('#asString', function () { 133 | it('should return a string', function () { 134 | expect(mod.get('STRING').asString()).to.be.a('string') 135 | expect(mod.get('STRING').asString()).to.equal(TEST_VARS.STRING) 136 | }) 137 | }) 138 | 139 | describe('#asUrlString', function () { 140 | it('should return a url string', function () { 141 | expect(mod.get('URL').asUrlString()).to.be.a('string') 142 | expect(mod.get('URL').asUrlString()).to.equal(TEST_VARS.URL) 143 | }) 144 | 145 | it('should throw due to bad url', function () { 146 | process.env.URL = 'not a url' 147 | 148 | expect(() => { 149 | mod.get('URL').asUrlString() 150 | }).to.throw('env-var: "URL" should be a valid URL') 151 | }) 152 | }) 153 | 154 | describe('#asUrlObject', function () { 155 | it('should return a url object', function () { 156 | expect(mod.get('URL').asUrlObject()).to.be.an.instanceOf(URL) 157 | }) 158 | 159 | it('should throw due to bad url', function () { 160 | process.env.URL = 'not a url' 161 | 162 | expect(() => { 163 | mod.get('URL').asUrlObject() 164 | }).to.throw('env-var: "URL" should be a valid URL') 165 | }) 166 | }) 167 | 168 | describe('#asInt', function () { 169 | it('should return an integer', function () { 170 | expect(mod.get('INTEGER').asInt()).to.be.a('number') 171 | expect(mod.get('INTEGER').asInt()).to.equal(parseInt(TEST_VARS.INTEGER)) 172 | }) 173 | 174 | it('should throw an exception - non integer type found', function () { 175 | process.env.INTEGER = '1.2' 176 | 177 | expect(function () { 178 | mod.get('INTEGER').asInt() 179 | }).to.throw() 180 | }) 181 | 182 | it('should throw an exception - non integer type found', function () { 183 | process.env.INTEGER = 'nope' 184 | 185 | expect(function () { 186 | mod.get('INTEGER').asInt() 187 | }).to.throw() 188 | }) 189 | 190 | it('should throw an exception - non integer type found', function () { 191 | process.env.INTEGER = '123nope' 192 | 193 | expect(function () { 194 | mod.get('INTEGER').asInt() 195 | }).to.throw() 196 | }) 197 | }) 198 | 199 | describe('#asIntPositive', function () { 200 | it('should return a positive integer', function () { 201 | expect(mod.get('INTEGER').asIntPositive()).to.be.a('number') 202 | expect(mod.get('INTEGER').asIntPositive()).to.equal(parseInt(TEST_VARS.INTEGER)) 203 | }) 204 | 205 | it('should throw an exception - negative integer found', function () { 206 | process.env.INTEGER = '-10' 207 | 208 | expect(function () { 209 | mod.get('INTEGER').asIntPositive() 210 | }).to.throw() 211 | }) 212 | }) 213 | 214 | describe('#asIntNegative', function () { 215 | it('should return a negative integer', function () { 216 | process.env.INTEGER = '-10' 217 | expect(mod.get('INTEGER').asIntNegative()).to.be.a('number') 218 | expect(mod.get('INTEGER').asIntNegative()).to.equal(parseInt(-10)) 219 | }) 220 | 221 | it('should throw an exception - positive integer found', function () { 222 | expect(function () { 223 | mod.get('INTEGER').asIntNegative() 224 | }).to.throw() 225 | }) 226 | }) 227 | 228 | describe('#asFloat', function () { 229 | it('should return a float', function () { 230 | expect(mod.get('FLOAT').asFloat()).to.be.a('number') 231 | expect(mod.get('FLOAT').asFloat()).to.equal(parseFloat(TEST_VARS.FLOAT)) 232 | expect(mod.get('FLOAT_INTEGER').asFloat()).to.equal(parseFloat(TEST_VARS.FLOAT_INTEGER)) 233 | }) 234 | 235 | it('should throw an exception - non float found', function () { 236 | process.env.FLOAT = '1.o' 237 | 238 | expect(function () { 239 | mod.get('FLOAT').asFloat() 240 | }).to.throw() 241 | }) 242 | 243 | it('should throw an exception - non float found', function () { 244 | process.env.FLOAT = 'nope' 245 | 246 | expect(function () { 247 | mod.get('FLOAT').asFloat() 248 | }).to.throw() 249 | }) 250 | 251 | it('should throw an exception - non float found', function () { 252 | process.env.FLOAT = '192.168.1.1' 253 | 254 | expect(function () { 255 | mod.get('FLOAT').asFloat() 256 | }).to.throw() 257 | }) 258 | }) 259 | 260 | describe('#asFloatPositive', function () { 261 | it('should return a positive float', function () { 262 | expect(mod.get('FLOAT').asFloatPositive()).to.be.a('number') 263 | expect(mod.get('FLOAT').asFloatPositive()).to.equal(parseFloat(TEST_VARS.FLOAT)) 264 | }) 265 | 266 | it('should throw an exception - negative integer found', function () { 267 | process.env.FLOAT = `-${process.env.FLOAT}` 268 | 269 | expect(function () { 270 | mod.get('FLOAT').asFloatPositive() 271 | }).to.throw() 272 | }) 273 | }) 274 | 275 | describe('#asFloatNegative', function () { 276 | it('should return a negative float', function () { 277 | const numberString = `-${TEST_VARS.FLOAT}` 278 | process.env.FLOAT = numberString 279 | expect(mod.get('FLOAT').asFloatNegative()).to.be.a('number') 280 | expect(mod.get('FLOAT').asFloatNegative()).to.equal(parseFloat(numberString)) 281 | }) 282 | 283 | it('should throw an exception - positive integer found', function () { 284 | expect(function () { 285 | mod.get('FLOAT').asFloatNegative() 286 | }).to.throw() 287 | }) 288 | }) 289 | 290 | describe('#asBool', function () { 291 | it('should return a bool - for string "false"', function () { 292 | expect(mod.get('BOOL').asBool()).to.be.a('boolean') 293 | expect(mod.get('BOOL').asBool()).to.equal(false) 294 | }) 295 | 296 | it('should return a bool - for string "FALSE"', function () { 297 | process.env.BOOL = 'FALSE' 298 | expect(mod.get('BOOL').asBool()).to.be.a('boolean') 299 | expect(mod.get('BOOL').asBool()).to.equal(false) 300 | }) 301 | 302 | it('should return a bool - for string "0"', function () { 303 | process.env.BOOL = '0' 304 | expect(mod.get('BOOL').asBool()).to.be.a('boolean') 305 | expect(mod.get('BOOL').asBool()).to.equal(false) 306 | }) 307 | 308 | it('should return a bool - for integer 0', function () { 309 | process.env.BOOL = 0 310 | expect(mod.get('BOOL').asBool()).to.be.a('boolean') 311 | expect(mod.get('BOOL').asBool()).to.equal(false) 312 | }) 313 | 314 | it('should return a bool - for string "true"', function () { 315 | process.env.BOOL = 'true' 316 | expect(mod.get('BOOL').asBool()).to.be.a('boolean') 317 | expect(mod.get('BOOL').asBool()).to.equal(true) 318 | }) 319 | 320 | it('should return a bool - for string "TRUE"', function () { 321 | process.env.BOOL = 'TRUE' 322 | expect(mod.get('BOOL').asBool()).to.be.a('boolean') 323 | expect(mod.get('BOOL').asBool()).to.equal(true) 324 | }) 325 | 326 | it('should return a bool - for string "1"', function () { 327 | process.env.BOOL = '1' 328 | expect(mod.get('BOOL').asBool()).to.be.a('boolean') 329 | expect(mod.get('BOOL').asBool()).to.equal(true) 330 | }) 331 | 332 | it('should return a bool - for integer 1', function () { 333 | process.env.BOOL = 1 334 | expect(mod.get('BOOL').asBool()).to.be.a('boolean') 335 | expect(mod.get('BOOL').asBool()).to.equal(true) 336 | }) 337 | 338 | it('should throw an exception - invalid boolean found', function () { 339 | process.env.BOOL = 'nope' 340 | 341 | expect(function () { 342 | mod.get('BOOL').asBool() 343 | }).to.throw() 344 | }) 345 | }) 346 | 347 | describe('#asBoolStrict', function () { 348 | it('should return a bool - for string "false"', function () { 349 | expect(mod.get('BOOL').asBoolStrict()).to.be.a('boolean') 350 | expect(mod.get('BOOL').asBoolStrict()).to.equal(false) 351 | }) 352 | 353 | it('should return a bool - for string "FALSE"', function () { 354 | process.env.BOOL = 'FALSE' 355 | expect(mod.get('BOOL').asBoolStrict()).to.be.a('boolean') 356 | expect(mod.get('BOOL').asBoolStrict()).to.equal(false) 357 | }) 358 | 359 | it('should throw an exception - for string "0"', function () { 360 | process.env.BOOL = '0' 361 | 362 | expect(function () { 363 | mod.get('BOOL').asBoolStrict() 364 | }).to.throw() 365 | }) 366 | 367 | it('should throw an exception - for integer 0', function () { 368 | process.env.BOOL = 0 369 | 370 | expect(function () { 371 | mod.get('BOOL').asBoolStrict() 372 | }).to.throw() 373 | }) 374 | 375 | it('should return a bool - for string "true"', function () { 376 | process.env.BOOL = 'true' 377 | expect(mod.get('BOOL').asBoolStrict()).to.be.a('boolean') 378 | expect(mod.get('BOOL').asBoolStrict()).to.equal(true) 379 | }) 380 | 381 | it('should return a bool - for string "TRUE"', function () { 382 | process.env.BOOL = 'TRUE' 383 | expect(mod.get('BOOL').asBoolStrict()).to.be.a('boolean') 384 | expect(mod.get('BOOL').asBoolStrict()).to.equal(true) 385 | }) 386 | 387 | it('should throw an exception - for string "1"', function () { 388 | process.env.BOOL = '1' 389 | 390 | expect(function () { 391 | mod.get('BOOL').asBoolStrict() 392 | }).to.throw() 393 | }) 394 | 395 | it('should throw an exception - for integer 1', function () { 396 | process.env.BOOL = 1 397 | 398 | expect(function () { 399 | mod.get('BOOL').asBoolStrict() 400 | }).to.throw() 401 | }) 402 | 403 | it('should throw an exception - invalid boolean found', function () { 404 | process.env.BOOL = 'nope' 405 | 406 | expect(function () { 407 | mod.get('BOOL').asBoolStrict() 408 | }).to.throw() 409 | }) 410 | }) 411 | 412 | describe('#asJson', function () { 413 | it('should return a json object', function () { 414 | expect(mod.get('JSON').asJson()).to.be.an('object') 415 | expect(mod.get('JSON').asJson()).to.deep.equal(JSON.parse(TEST_VARS.JSON)) 416 | }) 417 | 418 | it('should throw an exception - json parsing failed', function () { 419 | process.env.JSON = '{ nope}' 420 | 421 | expect(function () { 422 | mod.get('JSON').asJson() 423 | }).to.throw() 424 | }) 425 | }) 426 | 427 | describe('#required', function () { 428 | it('should not throw if required and found', function () { 429 | expect(mod.get('JSON').required().asJson()).to.be.an('object') 430 | }) 431 | 432 | it('should not throw if required is passed a false argument', function () { 433 | delete process.env.JSON 434 | 435 | expect(mod.get('JSON').required(false).asJson()).to.equal(undefined) 436 | }) 437 | 438 | it('should throw an exception when required, but not set', function () { 439 | delete process.env.JSON 440 | 441 | expect(function () { 442 | mod.get('JSON').required().asJson() 443 | }).to.throw() 444 | }) 445 | 446 | it('should throw an exception when required and set, but empty', function () { 447 | expect(function () { 448 | mod.get('EMPTY_STRING').required().asString() 449 | }).to.throw() 450 | 451 | expect(function () { 452 | mod.get('EMPTY_STRING_WITH_WHITESPACE').required().asString() 453 | }).to.throw() 454 | }) 455 | 456 | it('should throw an exception when required, set, empty and empty default value', function () { 457 | expect(function () { 458 | mod.get('EMPTY_STRING').default('').required().asString() 459 | }).to.throw() 460 | }) 461 | 462 | it('should not throw if required, set, empty but has a default value', function () { 463 | expect(mod.get('XXX_NOT_DEFINED').default('default').required().asString()).to.equal('default') 464 | }) 465 | 466 | it('should return undefined when not set and not required', function () { 467 | delete process.env.STRING 468 | 469 | expect(mod.get('STRING').asString()).to.equal(undefined) 470 | }) 471 | }) 472 | 473 | describe('#asJsonArray', function () { 474 | it('should return undefined', function () { 475 | expect( 476 | mod.get('nope').asJsonArray() 477 | ).to.equal(undefined) 478 | }) 479 | 480 | it('should throw an error - value was an object', function () { 481 | expect(function () { 482 | mod.get('JSON_OBJECT').asJsonArray() 483 | }).to.throw() 484 | }) 485 | 486 | it('should return a JSON Array', function () { 487 | expect( 488 | mod.get('JSON_ARRAY').asJsonArray() 489 | ).to.deep.equal([1, 2, 3]) 490 | }) 491 | }) 492 | 493 | describe('#asJsonObject', function () { 494 | it('should throw an exception', function () { 495 | expect( 496 | mod.get('nope').asJsonObject() 497 | ).to.equal(undefined) 498 | }) 499 | 500 | it('should throw an error - value was an array', function () { 501 | expect(function () { 502 | mod.get('JSON_ARRAY').asJsonObject() 503 | }).to.throw() 504 | }) 505 | 506 | it('should return a JSON Object', function () { 507 | expect( 508 | mod.get('JSON_OBJECT').asJsonObject() 509 | ).to.deep.equal({ name: 'value' }) 510 | }) 511 | }) 512 | 513 | describe('#asArray', function () { 514 | it('should return undefined when not set', function () { 515 | expect(mod.get('.NOPE.').asArray()).to.equal(undefined) 516 | }) 517 | 518 | it('should return an array that was split on commas', function () { 519 | expect(mod.get('COMMA_ARRAY').asArray()).to.deep.equal(['1', '2', '3']) 520 | }) 521 | 522 | it('should return an array that was split on dashes', function () { 523 | expect(mod.get('DASH_ARRAY').asArray('-')).to.deep.equal(['1', '2', '3']) 524 | }) 525 | 526 | it('should return an empty array if empty env var was set', function () { 527 | expect(mod.get('EMPTY_ARRAY').asArray()).to.deep.equal([]) 528 | }) 529 | 530 | it('should return array with only one value if env var doesn\'t contain delimiter', function () { 531 | expect(mod.get('ARRAY_WITHOUT_DELIMITER').asArray()).to.deep.equal(['value']) 532 | }) 533 | 534 | it('should return array with only one value if env var contain delimiter', function () { 535 | expect(mod.get('ARRAY_WITH_DELIMITER').asArray()).to.deep.equal(['value']) 536 | }) 537 | 538 | it('should return array with only one value if env var contain delimiter as prefix', function () { 539 | expect(mod.get('ARRAY_WITH_DELIMITER_PREFIX').asArray()).to.deep.equal(['value']) 540 | }) 541 | }) 542 | 543 | describe('#asSet', function () { 544 | it('should return an empty set when not set', function () { 545 | expect(mod.get('.NOPE.').asSet()).to.deep.equal(undefined) 546 | }) 547 | 548 | it('should return a set that was split on commas', function () { 549 | expect(mod.get('COMMA_ARRAY').asSet()).to.deep.equal(new Set(['1', '2', '3'])) 550 | }) 551 | 552 | it('should return a set that was split on dashes', function () { 553 | expect(mod.get('DASH_ARRAY').asSet('-')).to.deep.equal(new Set(['1', '2', '3'])) 554 | }) 555 | 556 | it('should return an empty set if empty env var was set', function () { 557 | expect(mod.get('EMPTY_ARRAY').asSet()).to.deep.equal(new Set()) 558 | }) 559 | 560 | it('should return set with only one value if env var doesn\'t contain delimiter', function () { 561 | expect(mod.get('ARRAY_WITHOUT_DELIMITER').asSet()).to.deep.equal(new Set(['value'])) 562 | }) 563 | 564 | it('should return set with only one value if env var contain delimiter', function () { 565 | expect(mod.get('ARRAY_WITH_DELIMITER').asSet()).to.deep.equal(new Set(['value'])) 566 | }) 567 | 568 | it('should return set with only one value if env var contain delimiter as prefix', function () { 569 | expect(mod.get('ARRAY_WITH_DELIMITER_PREFIX').asSet()).to.deep.equal(new Set(['value'])) 570 | }) 571 | 572 | it('should return a set of unique values', function () { 573 | expect(mod.get('DUPLICATE_ARRAY').asSet()).to.deep.equal(new Set(['1', '2', '3'])) 574 | }) 575 | }) 576 | 577 | describe('#asPortNumber', function () { 578 | it('should raise an error for ports less than 0', function () { 579 | process.env.PORT_NUMBER = '-2' 580 | 581 | expect(function () { 582 | mod.get('PORT_NUMBER').asPortNumber() 583 | }).to.throw('should be a positive integer') 584 | }) 585 | it('should raise an error for ports greater than 65535', function () { 586 | process.env.PORT_NUMBER = '700000' 587 | 588 | expect(function () { 589 | mod.get('PORT_NUMBER').asPortNumber() 590 | }).to.throw('cannot assign a port number greater than 65535') 591 | }) 592 | 593 | it('should return a number for valid ports', function () { 594 | process.env.PORT_NUMBER = '8080' 595 | 596 | expect(mod.get('PORT_NUMBER').asPortNumber()).to.equal(8080) 597 | }) 598 | }) 599 | 600 | describe('#asRegExp', function () { 601 | it('should raise an error for invalid regular expressions', function () { 602 | process.env.REG_EXP = '*' 603 | 604 | expect(function () { 605 | mod.get('REG_EXP').asRegExp() 606 | }).to.throw('should be a valid regexp') 607 | }) 608 | 609 | it('should raise an error for invalid flags', function () { 610 | process.env.REG_EXP = '^valid$' 611 | 612 | expect(function () { 613 | mod.get('REG_EXP').asRegExp('ii') 614 | }).to.throw('invalid regexp flags') 615 | }) 616 | 617 | it('should return a RegExp object for valid regular expressions', function () { 618 | process.env.REG_EXP = '^valid$' 619 | 620 | const regExp = mod.get('REG_EXP').asRegExp() 621 | 622 | expect(Object.prototype.toString.call(regExp)).to.equal('[object RegExp]') 623 | expect(regExp.toString()).to.equal('/^valid$/') 624 | }) 625 | 626 | it('should accept a flag argument to be passed along as the second argument to the RegExp constructor', function () { 627 | process.env.REG_EXP = '^valid$' 628 | 629 | expect(mod.get('REG_EXP').asRegExp('i').flags).to.equal('i') 630 | }) 631 | }) 632 | 633 | describe('#asEmailString', function () { 634 | it('should return an email address', function () { 635 | expect(mod.get('EMAIL').asEmailString()).to.be.a('string') 636 | expect(mod.get('EMAIL').asEmailString()).to.equal(TEST_VARS.EMAIL) 637 | }) 638 | 639 | it('should throw due to a bad email address', function () { 640 | process.env.EMAIL = '.invalid@example.com' 641 | 642 | expect(() => { 643 | mod.get('EMAIL').asEmailString() 644 | }).to.throw('env-var: "EMAIL" should be a valid email address') 645 | }) 646 | }) 647 | 648 | describe('#example', () => { 649 | let fromMod 650 | 651 | beforeEach(() => { 652 | fromMod = mod.from({ 653 | JSON_CONFIG: '{1,2]' 654 | }) 655 | }) 656 | 657 | const sampleConfig = JSON.stringify({ 658 | maxConnections: 10, 659 | enableSsl: true 660 | }) 661 | 662 | it('should throw an error with a valid example message', () => { 663 | expect(() => { 664 | fromMod.get('JSON_CONFIG').example(sampleConfig).asJsonArray() 665 | }).to.throw(`env-var: "JSON_CONFIG" should be valid (parseable) JSON. An example of a valid value would be: ${sampleConfig}`) 666 | }) 667 | 668 | it('should throw an error with a valid example message', () => { 669 | expect(() => { 670 | fromMod.get('MISSING_JSON_CONFIG').required().example('[1,2,3]').asJsonArray() 671 | }).to.throw('env-var: "MISSING_JSON_CONFIG" is a required variable, but it was not set. An example of a valid value would be: [1,2,3]') 672 | }) 673 | }) 674 | 675 | describe('#from', function () { 676 | var fromMod 677 | 678 | beforeEach(function () { 679 | fromMod = mod.from({ 680 | A_BOOL: 'true', 681 | A_STRING: 'blah' 682 | }) 683 | }) 684 | 685 | it('should send messages to the custom logger', () => { 686 | let spyCallCount = 0 687 | const data = { 688 | JSON: JSON.stringify({ name: 'env-var' }) 689 | } 690 | 691 | const env = mod.from(data, {}, (str) => { 692 | expect(str).to.be.a('string') 693 | spyCallCount++ 694 | }) 695 | 696 | const result = env.get('JSON').asJson() 697 | 698 | expect(result).to.deep.equal({ name: 'env-var' }) 699 | expect(spyCallCount).to.be.greaterThan(0) 700 | }) 701 | 702 | it('should get a from boolean', function () { 703 | expect(fromMod.get('A_BOOL').required().asBool()).to.eql(true) 704 | }) 705 | 706 | it('should get a from string', function () { 707 | expect(fromMod.get('A_STRING').required().asString()).to.eql('blah') 708 | }) 709 | 710 | it('should get undefined for a missing un-required value', function () { 711 | expect(fromMod.get('DONTEXIST').asString()).to.eql(undefined) 712 | }) 713 | 714 | it('should throw an exception on a missing required value', function () { 715 | expect(function () { 716 | fromMod.get('DONTEXIST').required().asJson() 717 | }).to.throw() 718 | }) 719 | 720 | it('should return the from values object if no arguments', function () { 721 | expect(fromMod.get()).to.have.property('A_BOOL', 'true') 722 | expect(fromMod.get()).to.have.property('A_STRING', 'blah') 723 | }) 724 | 725 | describe('#extraAccessors', function () { 726 | it('should add custom accessors to subsequent gotten values', function () { 727 | const fromMod = mod.from({ STRING: 'Hello, world!' }, { 728 | asShout: function (value) { 729 | return value.toUpperCase() 730 | } 731 | }) 732 | 733 | var gotten = fromMod.get('STRING') 734 | 735 | expect(gotten).to.have.property('asShout') 736 | expect(gotten.asShout()).to.equal('HELLO, WORLD!') 737 | }) 738 | 739 | it('should allow overriding existing accessors', function () { 740 | const fromMod = mod.from({ STRING: 'Hello, world!' }, { 741 | asString: function (value) { 742 | // https://stackoverflow.com/a/959004 743 | return value.split('').reverse().join('') 744 | } 745 | }) 746 | 747 | expect(fromMod.get('STRING').asString()).to.equal('!dlrow ,olleH') 748 | }) 749 | 750 | it('should not attach accessors to other env instances', function () { 751 | const fromMod = mod.from({ STRING: 'Hello, world!' }, { 752 | asNull: function (value) { 753 | return null 754 | } 755 | }) 756 | 757 | var otherMod = mod.from({ 758 | STRING: 'Hola, mundo!' 759 | }) 760 | 761 | expect(fromMod.get('STRING')).to.have.property('asNull') 762 | expect(otherMod.get('STRING')).not.to.have.property('asNull') 763 | }) 764 | }) 765 | 766 | describe('#accessors', () => { 767 | describe('#asArray', () => { 768 | it('should return an array of strings', () => { 769 | const arr = fromMod.accessors.asArray('1,2,3') 770 | 771 | expect(arr).to.eql(['1', '2', '3']) 772 | }) 773 | 774 | it('should return an array of strings split by period chars', () => { 775 | const arr = fromMod.accessors.asArray('1.2.3', '.') 776 | 777 | expect(arr).to.eql(['1', '2', '3']) 778 | }) 779 | }) 780 | 781 | describe('#asInt', () => { 782 | it('should return an integer', () => { 783 | const ret = fromMod.accessors.asInt('1') 784 | 785 | expect(ret).to.eql(1) 786 | }) 787 | }) 788 | }) 789 | 790 | describe('#logger', () => { 791 | const varname = 'SOME_VAR' 792 | const msg = 'this is a test message' 793 | 794 | it('should send a string to the given logger', () => { 795 | let spyCalled = false 796 | const spy = (str) => { 797 | expect(str).to.equal(`env-var (${varname}): ${msg}`) 798 | spyCalled = true 799 | } 800 | 801 | const log = require('../lib/logger')(spy) 802 | 803 | log(varname, msg) 804 | expect(spyCalled).to.equal(true) 805 | }) 806 | }) 807 | }) 808 | 809 | describe('#accessors', () => { 810 | describe('#asArray', () => { 811 | it('should return an array of strings', () => { 812 | const arr = mod.accessors.asArray('1,2,3') 813 | 814 | expect(arr).to.eql(['1', '2', '3']) 815 | }) 816 | 817 | it('should return an array of strings split by period chars', () => { 818 | const arr = mod.accessors.asArray('1.2.3', '.') 819 | 820 | expect(arr).to.eql(['1', '2', '3']) 821 | }) 822 | }) 823 | 824 | describe('#asInt', () => { 825 | it('should return an integer', () => { 826 | const ret = mod.accessors.asInt('1') 827 | 828 | expect(ret).to.eql(1) 829 | }) 830 | }) 831 | }) 832 | }) 833 | --------------------------------------------------------------------------------