From db2261191dc8f85484285fe296183be19bb7dd29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Benitte?= Date: Thu, 14 Sep 2017 20:59:50 +0900 Subject: [PATCH 1/3] docs(init): init proper package documentation --- README.md | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 226 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3d956fa..1b5c8d8 100755 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ A lightweight configuration module powered by yaml. +- [Installation](#installation) +- [Usage](#usage) + - [Environment variables override](#environment-variables-override) + - [NODE_ENV override](#node-env-override) + - [More overrides :smiling_imp:](#more-overrides) + ## Installation Using yarn @@ -23,16 +29,233 @@ npm install @ekino/config ## Usage -This module assumes all your configuration is defined in a single directory: +This module assumes all your configuration is defined in a single directory, +located at the root of your current working directory (`process.cwd()`): ``` ├─ conf/ ├─ base.yaml # the base configuration ├─ env_mapping.yaml # defines mapping between env vars and config keys - └─ dev.yaml # loaded if NODE_ENV is `dev` + └─ dev.yaml # Optional file loaded if NODE_ENV is `dev` +``` + +`base.yaml` is required, it defines the common basic configuration of your application. + +Then to get a config key value in your code: + +```yaml +# /conf/base.yaml +host: base.config.io +external_api: + key: xxxxx +``` + +```javascript +// test.js +const config = require('@ekino/config') +console.log(config.get('host')) +console.log(config.get('external_api.key')) +``` + +If we run this script, we'll have: + +```sh +node test.js +> base.config.io +> xxxxx +``` + +### Environment variables override + +Sometimes you want to override a single value on certain environments, to do so this module provides +a special file called `env_mapping.yaml`, it allows to define per key override according to +environment variable value. + +Assuming we've got the following config files: + +```yaml +# /conf/base.yaml +host: base.config.io +external_api: + key: xxxxx +``` + +```yaml +# /conf/env_mapping.yaml +HOST: host +API_KEY: external_api.key +``` + +And the following code: + +```javascript +// test.js +const config = require('@ekino/config') +console.log(config.get('host')) +console.log(config.get('external_api.key')) +``` + +If we run this script, we'll have: + +```sh +node test.js +> base.config.io +> xxxxx + +HOST=staging.config.io API_KEY=12345 node test.js +> staging.config.io +> 12345 +``` + +The second run outputs a different values because we mapped the `HOST` and `API_KEY` environment variables +to the `host` and `external_api.key` config keys using `env_mapping.yaml`. + +Environment variables can be handy, however when reading them from node, we'll always get a string, +this can be annoying when dealing with boolean values for example. +That's why you can optionally force the type of the gathered environment variables value: + +```yaml +# /conf/env_mapping.yaml +PORT: + key: port + type: number +USE_SSL: + key: use_ssl + type: boolean +``` + +For now we only support `number` and `boolean` types, if you think others could be useful, +do not hesitate to contribute! + +### NODE_ENV override + +If you've got a bunch of variations depending on the environment your're running your application on, +it can be cumbersome to define tens of mappings inside the `env_mapping.yaml` file. + +This module gives you the ability to load overrides depending on the special `NODE_ENV` +environment variable value, let's say we'got those config files: + +```yaml +# /conf/dev.yaml +host: dev.config.io +``` + +```yaml +# /conf/prod.yaml +host: prod.config.io ``` -> Be warned that this module uses synchronous file reads in order to be easily required. +And the following code: + +```javascript +// test.js +const config = require('@ekino/config') +console.log(config.get('host')) +``` + +If we run this script, we'll have: + +```sh +NODE_ENV=dev node test.js +> dev.config.io + +NODE_ENV=prod node test.js +> prod.config.io +``` + +### More overrides + +As we've seen previously with [Environment variables override](#environment-variables-override) +and [NODE_ENV override](#node-env-override), you can easily tweak your config without changing +a single line of code, but if those features aren't sufficient to cover your requirements, +you have another available level of override using `CONF_OVERRIDES`. + +> Please make sure you really need it before using it +> as it makes more unclear what the final config will be. + +Let's say we'got those config files: + +```yaml +# /conf/base.yaml +service: awesome +host: base.config.io +port: 8080 +``` + +```yaml +# /conf/prod.yaml +host: prod.config.io +port: 8081 +``` + +```yaml +# /conf/aws.yaml +host: prod.aws.config.io +``` + +```yaml +# /conf/google.yaml +host: prod.google.config.io +``` + +```yaml +# /conf/extra.yaml +port: 8082 +``` + +And the following code: + +```javascript +// test.js +const config = require('@ekino/config') +console.log(config.get('service')) +console.log(config.get('host')) +console.log(config.get('port')) +``` + +If we run this script, we'll have: + +```sh +node test.js +> awesome # from base.yaml +> base.config.io # from base.yaml +> 8080 # from base.yaml + +NODE_ENV=prod node test.js +> awesome # from base.yaml +> prod.config.io # from prod.yaml +> 8081 # from prod.yaml + +NODE_ENV=prod CONF_OVERRIDES=aws node test.js +> awesome # from base.yaml +> prod.aws.config.io # from aws.yaml +> 8081 # from prod.yaml + +NODE_ENV=prod CONF_OVERRIDES=google node test.js +> awesome # from base.yaml +> prod.google.config.io # from google.yaml +> 8081 # from prod.yaml + +NODE_ENV=prod CONF_OVERRIDES=google,extra node test.js +> awesome # from base.yaml +> prod.google.config.io # from google.yaml +> 8082 # from extra.yaml +``` + +The overrides from files defined in `CONF_OVERRIDES` in the same order they are defined, +so for `CONF_OVERRIDES=google,extra`, it will load `/conf/google.yaml`, then `extra.yaml`. + +For the sake of mental health, if a file is defined twice, it will be ignored, +if you take this example: + +```sh +NODE_ENV=prod CONF_OVERRIDES=aws,prod node test.js +``` + +the second `prod` defined inside `CONF_OVERRIDES` will be ignored as it has already been loaded +because of `NODE_ENV=prod`. + +:warning: The `env_mapping.yaml` will always take precedence over files overrides. [npm-image]: https://img.shields.io/npm/v/@ekino/config.svg?style=flat-square [npm-url]: https://www.npmjs.com/package/@ekino/config From f67e248e0268e95e8198110d830aaf0cb1d15845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Benitte?= Date: Thu, 14 Sep 2017 22:17:12 +0900 Subject: [PATCH 2/3] docs(motivation): add motivation section to readme --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1b5c8d8..ebedf70 100755 --- a/README.md +++ b/README.md @@ -5,14 +5,55 @@ [![Coverage Status][coverage-image]][coverage-url] [![styled with prettier][prettier-image]][prettier-url] -A lightweight configuration module powered by yaml. +A lightweight/opinionated/versatile configuration module powered by yaml. +- [Motivation](#motivation) - [Installation](#installation) - [Usage](#usage) - [Environment variables override](#environment-variables-override) - [NODE_ENV override](#node-env-override) - [More overrides :smiling_imp:](#more-overrides) +## Motivation + +Why the hell another Node.js config package? + +That's a question we asked ourselves before starting this module, +we wanted simple config management with the following features: + +- Simple to use +- Small footprint +- Homogeneous config file format +- No global/specific/pre-configured module to load (eg. require the lib, configure it and then require this file instead directly using the package) +- Human readable +- Concise +- Comments +- Types +- Overrides +- Env variables support + +Several modules already exist, but none of them matched our requirements, +some we're far too limited and others were, in our opinion, really bloated. + +We chose yaml because it automatically covered several requirements, +it's concise compared to json, you can add comments, it supports types +and it's really easy to read it. Yaml also offers other neat features +such as [anchors](http://www.yaml.org/spec/1.2/spec.html#id2765878). + +**We only support yaml config files**, having a project with `json`, `xml`, +`toml`, `ini`, `properties`,… just does not scale when working on big projects, +everyone adding its favorite flavor. + +Then we used environment variables to load overrides or define some specific keys, +it makes really easy to tweak your config depending on the environment +you're running on without touching a single line of code or even a config file. + +We used this code across several projects (a small file comprised of ~100 loc at this time), +and improved it when required. + +And here we are! It's now open source, and we hope it will help others +building awesome things like it did for us. + ## Installation Using yarn @@ -29,6 +70,9 @@ npm install @ekino/config ## Usage +As this module heavily relies on **environment variables**, you could read +[this](https://en.wikipedia.org/wiki/Environment_variable) first if you're not comfortable with them. + This module assumes all your configuration is defined in a single directory, located at the root of your current working directory (`process.cwd()`): @@ -242,8 +286,8 @@ NODE_ENV=prod CONF_OVERRIDES=google,extra node test.js > 8082 # from extra.yaml ``` -The overrides from files defined in `CONF_OVERRIDES` in the same order they are defined, -so for `CONF_OVERRIDES=google,extra`, it will load `/conf/google.yaml`, then `extra.yaml`. +The overrides from files defined in `CONF_OVERRIDES` are loaded in the same order they are defined, +so for `CONF_OVERRIDES=google,extra`, it will load `/conf/google.yaml`, then `/conf/extra.yaml`. For the sake of mental health, if a file is defined twice, it will be ignored, if you take this example: @@ -252,7 +296,7 @@ if you take this example: NODE_ENV=prod CONF_OVERRIDES=aws,prod node test.js ``` -the second `prod` defined inside `CONF_OVERRIDES` will be ignored as it has already been loaded +the second `prod` defined inside `CONF_OVERRIDES` will be ignored as it has been already loaded because of `NODE_ENV=prod`. :warning: The `env_mapping.yaml` will always take precedence over files overrides. From 9a1994604d11d2e287df9093cd7269c0ea3f99d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Benitte?= Date: Fri, 15 Sep 2017 01:18:00 +0900 Subject: [PATCH 3/3] feat(overrides): add ability to define extra overrides --- README.md | 23 ++++- conf/base.yaml | 14 ++- conf/env_mapping.yaml | 21 +++- conf/override_a.yaml | 6 ++ conf/override_b.yaml | 4 + conf/prod.yaml | 7 +- index.js | 146 +++++++++++++++++--------- package.json | 4 +- test/test.js | 235 ++++++++++++++++++++++++++++++++++-------- 9 files changed, 352 insertions(+), 108 deletions(-) create mode 100644 conf/override_a.yaml create mode 100644 conf/override_b.yaml diff --git a/README.md b/README.md index ebedf70..f74bf98 100755 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ A lightweight/opinionated/versatile configuration module powered by yaml. - [Usage](#usage) - [Environment variables override](#environment-variables-override) - [NODE_ENV override](#node-env-override) - - [More overrides :smiling_imp:](#more-overrides) + - [More overrides](#more-overrides) :smiling_imp: + - [Inheritance model](#inheritance-model) ## Motivation @@ -33,7 +34,7 @@ we wanted simple config management with the following features: - Env variables support Several modules already exist, but none of them matched our requirements, -some we're far too limited and others were, in our opinion, really bloated. +some were far too limited and others, in our opinion, really bloated. We chose yaml because it automatically covered several requirements, it's concise compared to json, you can add comments, it supports types @@ -47,6 +48,7 @@ everyone adding its favorite flavor. Then we used environment variables to load overrides or define some specific keys, it makes really easy to tweak your config depending on the environment you're running on without touching a single line of code or even a config file. +Really handy when using Docker heavily. We used this code across several projects (a small file comprised of ~100 loc at this time), and improved it when required. @@ -217,7 +219,7 @@ you have another available level of override using `CONF_OVERRIDES`. > Please make sure you really need it before using it > as it makes more unclear what the final config will be. -Let's say we'got those config files: +Let's say we've got those config files: ```yaml # /conf/base.yaml @@ -296,11 +298,24 @@ if you take this example: NODE_ENV=prod CONF_OVERRIDES=aws,prod node test.js ``` -the second `prod` defined inside `CONF_OVERRIDES` will be ignored as it has been already loaded +the second `prod` value defined inside `CONF_OVERRIDES` will be ignored as it has been already loaded because of `NODE_ENV=prod`. :warning: The `env_mapping.yaml` will always take precedence over files overrides. +### Inheritance model + +``` +base.yaml <— [.yaml] <— [.yaml] <— [env_mapping.yaml] +``` + +*All files surrounded by `[]` are optional.* + +1. Load config from `base.yaml` +2. If `NODE_ENV` is defined & `.yaml` exists, load it +3. If `CONF_OVERRIDES` is defined, load each corresponding file if it exists +4. If `env_mapping.yaml` exists and some environment variables match, override with those values + [npm-image]: https://img.shields.io/npm/v/@ekino/config.svg?style=flat-square [npm-url]: https://www.npmjs.com/package/@ekino/config [travis-image]: https://img.shields.io/travis/ekino/node-config.svg?style=flat-square diff --git a/conf/base.yaml b/conf/base.yaml index 6a2c318..6d6043a 100644 --- a/conf/base.yaml +++ b/conf/base.yaml @@ -1,4 +1,12 @@ -port: 8080 +# +# This is the base config file, loaded for all tests +# +port: 8080 version: 0.0.0 -name: test-app0 -uuid: 01A0 +name: test-app0 +uuid: 01A0 +api: + credentials: + id: base-api-id + key: base-api-key + retries: 3 \ No newline at end of file diff --git a/conf/env_mapping.yaml b/conf/env_mapping.yaml index b0b4fdb..4917d4a 100644 --- a/conf/env_mapping.yaml +++ b/conf/env_mapping.yaml @@ -1,16 +1,27 @@ +# +# Defines environment mapping, loaded for all tests +# PORT: - key: port + key: port type: number + NAME: key: name + VERSION: version + ID: - key: id + key: id type: number + USE_SSL: - key: useSsl + key: useSsl type: boolean + USE_MOCKS: - key: useMocks + key: useMocks type: boolean - \ No newline at end of file + +API_RETRIES: + key: api.retries + type: number \ No newline at end of file diff --git a/conf/override_a.yaml b/conf/override_a.yaml new file mode 100644 index 0000000..dd97fb7 --- /dev/null +++ b/conf/override_a.yaml @@ -0,0 +1,6 @@ +port: 8082 +version: 0.0.2 +api: + credentials: + key: override-a-api-key + diff --git a/conf/override_b.yaml b/conf/override_b.yaml new file mode 100644 index 0000000..1c2429b --- /dev/null +++ b/conf/override_b.yaml @@ -0,0 +1,4 @@ +version: 0.0.3 +api: + credentials: + key: override-b-api-key diff --git a/conf/prod.yaml b/conf/prod.yaml index fd5c013..81211f7 100644 --- a/conf/prod.yaml +++ b/conf/prod.yaml @@ -1,3 +1,6 @@ -port: 8081 +port: 8081 version: 0.0.1 -env: prod +env: prod +api: + credentials: + key: prod-api-key diff --git a/index.js b/index.js index 949227a..8276d0b 100644 --- a/index.js +++ b/index.js @@ -5,15 +5,7 @@ const yaml = require('js-yaml') const fs = require('fs') const path = require('path') -const internals = {} - -internals.cfg = {} - -internals.confPath = process.env.NODE_CONFIG_DIR ? `${process.env.NODE_CONFIG_DIR}` : path.join(process.cwd(), 'conf') - -internals.basePath = path.join(internals.confPath, 'base.yaml') -internals.envMappingPath = path.join(internals.confPath, 'env_mapping.yaml') -internals.overridesPath = process.env.NODE_ENV ? path.join(internals.confPath, `${process.env.NODE_ENV}.yaml`) : null +const internals = { cfg: {} } /** * Get a value from the configuration. Supports dot notation (eg: "key.subkey.subsubkey")... @@ -49,27 +41,56 @@ exports.dump = () => internals.cfg /** ***** Internals **********/ /** - * Read a yaml file and convert it to json. - * WARNING : This use a sync function to read file + * Read a yaml file and convert it to javascript object. + * + * WARNING: This use a sync function to read file + * * @param {string} path * @return {Object} */ internals.read = path => { const content = fs.readFileSync(path, { encoding: 'utf8' }) let result = yaml.safeLoad(content) + return result } +/** + * Try to load a yaml file, if the file does not exist, return null. + * + * @see internals.read + * + * @param {string} path + * @return {Object|null} + */ +internals.readEventually = path => { + try { + const content = internals.read(path) + return content + } catch (e) { + if (!e || e.code !== 'ENOENT') throw e + return null + } +} + +/** + * Cast a value using given type. + * + * @param {string} type + * @param {string} value + * @returns {string|number|boolean} Casted value + */ internals.cast = (type, value) => { switch (type) { case 'number': { const result = Number(value) - if (_.isNaN(result)) throw new Error(`Config error : expected a number got ${value}`) + if (_.isNaN(result)) throw new Error(`Config error: expected a number got ${value}`) return result } case 'boolean': - if (!_.includes(['true', 'false', '0', '1'], value)) throw new Error(`Config error : expected a boolean got ${value}`) + if (!_.includes(['true', 'false', '0', '1'], value)) + throw new Error(`Config error: expected a boolean got ${value}`) return value === 'true' || value === '1' default: @@ -78,62 +99,85 @@ internals.cast = (type, value) => { } /** - * Read env variables override file and set config from env vars + * Read env variables override file and set config from env vars. + * + * @param {Object} mappings * @return {Object} */ -internals.readEnvOverrides = () => { - const result = {} +internals.getEnvOverrides = mappings => { + const overriden = {} - try { - const content = internals.read(internals.envMappingPath) - _.forOwn(content, (mapping, key) => { - if (_.isUndefined(process.env[key])) return true - - let value = process.env[key] - let mappedKey = mapping - - if (mapping.key) { - mappedKey = mapping.key - value = internals.cast(mapping.type, value) - } - _.set(result, mappedKey, value) - }) - } catch (e) { - if (!e || e.code !== 'ENOENT') throw e - } + _.forOwn(mappings, (mapping, key) => { + if (_.isUndefined(process.env[key])) return true - return result + let value = process.env[key] + let mappedKey = mapping + + if (mapping.key) { + mappedKey = mapping.key + value = internals.cast(mapping.type, value) + } + _.set(overriden, mappedKey, value) + }) + + return overriden } /** - * Return the source value if it is an array - * This function is used to customize the default output of _.mergeWith + * Return the source value if it is an array. + * This function is used to customize the default output of `_.mergeWith()`. * * @param {*} objValue: the target field content * @param {*} srcValue: the new value - * @returns {*}: return what we want as a value, or undefined to let the default behaviour kick in + * @returns {*} return what we want as a value, or undefined to let the default behaviour kick in */ -internals.customizer = (objValue, srcValue) => { - return _.isArray(srcValue) ? srcValue : undefined -} +internals.customizer = (objValue, srcValue) => (_.isArray(srcValue) ? srcValue : undefined) + +internals.merge = _.partialRight(_.mergeWith, internals.customizer) /** - * Read base file, override it with env file and finally override it with env vars + * Loads config: + * + * 1. load base.yaml + * 2. if NODE_ENV is defined and a config file matching its value exists, load it + * 3. if CONF_OVERRIDES is defined, try to load corresponding files + * 4. load env_mapping.yaml if it exists and search for overrides */ internals.load = () => { - const base = internals.read(internals.basePath) - let env = {} - if (internals.overridesPath) { - try { - env = internals.read(internals.overridesPath) - } catch (e) { - if (!e || e.code !== 'ENOENT') throw e - } + const confPath = process.env.NODE_CONFIG_DIR + ? `${process.env.NODE_CONFIG_DIR}` + : path.join(process.cwd(), 'conf') + + // load base config (required) + const baseConfig = internals.read(path.join(confPath, 'base.yaml')) + internals.cfg = Object.assign({}, baseConfig) + + // apply file overrides (optional) + let overrideFiles = [] + if (process.env.NODE_ENV) overrideFiles.push(process.env.NODE_ENV) + if (process.env.CONF_OVERRIDES) { + overrideFiles = overrideFiles.concat( + process.env.CONF_OVERRIDES.split(',').filter( + // remove garbage and prevent dupes + override => + !_.isEmpty(override) && override !== 'base' && !overrideFiles.includes(override) + ) + ) } - const envOverrides = internals.readEnvOverrides() + overrideFiles.forEach(overrideFile => { + const overridePath = path.join(confPath, `${overrideFile}.yaml`) + const override = internals.readEventually(overridePath) - internals.cfg = _.mergeWith({}, base, env, envOverrides, internals.customizer) + if (override !== null) internals.merge(internals.cfg, override) + }) + + // apply environment overrides (optional) + const envOverridesConfig = internals.readEventually(path.join(confPath, 'env_mapping.yaml')) + if (envOverridesConfig !== null) { + const envOverrides = internals.getEnvOverrides(envOverridesConfig) + internals.merge(internals.cfg, envOverrides) + } } internals.load() diff --git a/package.json b/package.json index 5e20e98..9a46b4f 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "prettier": "1.x.x" }, "scripts": { - "fmt": "prettier --print-width 140 --tab-width=4 --single-quote --bracket-spacing --no-semi --color --write index.js test/*.js test/**/*.js", - "check-fmt": "prettier --print-width 140 --tab-width=4 --single-quote --bracket-spacing --no-semi --list-different index.js test/*.js test/**/*.js", + "fmt": "prettier --print-width 100 --tab-width=4 --single-quote --bracket-spacing --no-semi --color --write index.js test/*.js test/**/*.js", + "check-fmt": "prettier --print-width 100 --tab-width=4 --single-quote --bracket-spacing --no-semi --list-different index.js test/*.js test/**/*.js", "test": "ava", "test-cover": "nyc ava", "coverage": "nyc report --reporter=text-lcov | coveralls", diff --git a/test/test.js b/test/test.js index 4c304a1..0c8851e 100644 --- a/test/test.js +++ b/test/test.js @@ -6,7 +6,7 @@ test.cb('I can override conf directory path with env variable NODE_CONFIG_DIR', const testPath = `${__dirname}/helpers/child.load_conf.js` const env = { NODE_CONFIG_DIR: 'test/conf/basic' } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.deepEqual(content, { name: 'test-app2', port: 8082, uuid: '01A2', version: '0.0.2' }) @@ -14,47 +14,200 @@ test.cb('I can override conf directory path with env variable NODE_CONFIG_DIR', }) }) -test.cb('I can override config values with NODE_ENV config file in the following order : base, env file', t => { +test.cb( + 'I can override config values with NODE_ENV config file in the following order: base, env file', + t => { + const testPath = `${__dirname}/helpers/child.load_conf.js` + + const env = { NODE_ENV: 'prod' } + const child = childProcess.fork(testPath, { env }) + + child.on('message', content => { + t.deepEqual(content, { + name: 'test-app0', + port: 8081, + uuid: '01A0', + version: '0.0.1', + env: 'prod', + api: { + credentials: { + id: 'base-api-id', + key: 'prod-api-key' + }, + retries: 3 + } + }) + t.end() + }) + } +) + +test.cb( + 'I can override config values with env values in the following order: base, env values', + t => { + const testPath = `${__dirname}/helpers/child.load_conf.js` + + const env = { NODE_ENV: 'dev', PORT: '8083', API_RETRIES: '6' } + const child = childProcess.fork(testPath, { env }) + + child.on('message', content => { + t.deepEqual(content, { + name: 'test-app0', + port: 8083, + uuid: '01A0', + version: '0.0.0', + api: { + credentials: { + id: 'base-api-id', + key: 'base-api-key' + }, + retries: 6 + } + }) + t.end() + }) + } +) + +test.cb( + 'I can override config values with NODE_ENV config file and env values in the following order: base, env file, env values', + t => { + const testPath = `${__dirname}/helpers/child.load_conf.js` + + const env = { NODE_ENV: 'prod', PORT: '8083' } + const child = childProcess.fork(testPath, { env }) + + child.on('message', content => { + t.deepEqual(content, { + name: 'test-app0', + port: 8083, + uuid: '01A0', + version: '0.0.1', + env: 'prod', + api: { + credentials: { + id: 'base-api-id', + key: 'prod-api-key' + }, + retries: 3 + } + }) + t.end() + }) + } +) + +test.cb('I can override config values with config file defined through CONF_OVERRIDES', t => { const testPath = `${__dirname}/helpers/child.load_conf.js` - const env = { NODE_ENV: 'prod' } - const child = childProcess.fork(testPath, { env: env }) + const env = { CONF_OVERRIDES: 'override_a' } + const child = childProcess.fork(testPath, { env }) child.on('message', content => { - t.deepEqual(content, { name: 'test-app0', port: 8081, uuid: '01A0', version: '0.0.1', env: 'prod' }) + t.deepEqual(content, { + name: 'test-app0', + port: 8082, + uuid: '01A0', + version: '0.0.2', + api: { + credentials: { + id: 'base-api-id', + key: 'override-a-api-key' + }, + retries: 3 + } + }) t.end() }) }) -test.cb('I can override config values with env values in the following order : base, env values', t => { - const testPath = `${__dirname}/helpers/child.load_conf.js` - - const env = { NODE_ENV: 'dev', PORT: 8083 } - const child = childProcess.fork(testPath, { env: env }) - - child.on('message', content => { - t.deepEqual(content, { name: 'test-app0', port: 8083, uuid: '01A0', version: '0.0.0' }) - t.end() - }) -}) - -test.cb('I can override config values with NODE_ENV config file and env values in the following order : base, env file, env values', t => { - const testPath = `${__dirname}/helpers/child.load_conf.js` - - const env = { NODE_ENV: 'prod', PORT: 8083 } - const child = childProcess.fork(testPath, { env: env }) - - child.on('message', content => { - t.deepEqual(content, { name: 'test-app0', port: 8083, uuid: '01A0', version: '0.0.1', env: 'prod' }) - t.end() - }) -}) +test.cb( + 'I can override config values with multiple config files defined through CONF_OVERRIDES', + t => { + const testPath = `${__dirname}/helpers/child.load_conf.js` + + const env = { CONF_OVERRIDES: 'override_a,override_b' } + const child = childProcess.fork(testPath, { env }) + + child.on('message', content => { + t.deepEqual(content, { + name: 'test-app0', + port: 8082, + uuid: '01A0', + version: '0.0.3', + api: { + credentials: { + id: 'base-api-id', + key: 'override-b-api-key' + }, + retries: 3 + } + }) + t.end() + }) + } +) + +test.cb( + 'Environment variables mapped through env_mapping should take precedence over CONF_OVERRIDES', + t => { + const testPath = `${__dirname}/helpers/child.load_conf.js` + + const env = { CONF_OVERRIDES: 'override_a,override_b', VERSION: '0.0.4', API_RETRIES: '6' } + const child = childProcess.fork(testPath, { env }) + + child.on('message', content => { + t.deepEqual(content, { + name: 'test-app0', + port: 8082, + uuid: '01A0', + version: '0.0.4', + api: { + credentials: { + id: 'base-api-id', + key: 'override-b-api-key' + }, + retries: 6 + } + }) + t.end() + }) + } +) + +test.cb( + 'A config file occurring several times through NODE_ENV or CONF_OVERRIDES should be ignored', + t => { + const testPath = `${__dirname}/helpers/child.load_conf.js` + + const env = { NODE_ENV: 'prod', CONF_OVERRIDES: 'override_a,override_b,prod' } + const child = childProcess.fork(testPath, { env }) + + child.on('message', content => { + t.deepEqual(content, { + name: 'test-app0', + port: 8082, + uuid: '01A0', + version: '0.0.3', + env: 'prod', + api: { + credentials: { + id: 'base-api-id', + key: 'override-b-api-key' + }, + retries: 3 + } + }) + t.end() + }) + } +) test.cb('I can cast env values overrides', t => { const testPath = `${__dirname}/helpers/child.load_conf.js` const env = { PORT: 8080, NAME: 'app', ID: '12', USE_SSL: 'false', USE_MOCKS: 1 } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.deepEqual(typeof content.port, 'number') @@ -70,7 +223,7 @@ test.cb('I can cast truthy boolean env values overrides', t => { const testPath = `${__dirname}/helpers/child.load_conf.js` const env = { USE_SSL: 'true', USE_MOCKS: 1 } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.is(content.useSsl, true) @@ -83,7 +236,7 @@ test.cb('I can cast falsy boolean env values overrides', t => { const testPath = `${__dirname}/helpers/child.load_conf.js` const env = { USE_SSL: 'false', USE_MOCKS: 0 } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.is(content.useSsl, false) @@ -96,11 +249,11 @@ test.cb('It throws an error when number cast on env value fails', t => { const testPath = `${__dirname}/helpers/child.catch_conf_errors.js` const env = { PORT: 'NotANumber' } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.deepEqual(content.name, 'Error') - t.deepEqual(content.message, 'Config error : expected a number got NotANumber') + t.deepEqual(content.message, 'Config error: expected a number got NotANumber') t.end() }) }) @@ -109,11 +262,11 @@ test.cb('It throws an error when boolean string cast on env value fails', t => { const testPath = `${__dirname}/helpers/child.catch_conf_errors.js` const env = { USE_SSL: 'NotABoolean' } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.deepEqual(content.name, 'Error') - t.deepEqual(content.message, 'Config error : expected a boolean got NotABoolean') + t.deepEqual(content.message, 'Config error: expected a boolean got NotABoolean') t.end() }) }) @@ -122,11 +275,11 @@ test.cb('It throws an error when boolean number cast on env value fails', t => { const testPath = `${__dirname}/helpers/child.catch_conf_errors.js` const env = { USE_MOCKS: 42 } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.deepEqual(content.name, 'Error') - t.deepEqual(content.message, 'Config error : expected a boolean got 42') + t.deepEqual(content.message, 'Config error: expected a boolean got 42') t.end() }) }) @@ -135,7 +288,7 @@ test.cb('It throws an error when env_mapping file is not yaml valid', t => { const testPath = `${__dirname}/helpers/child.catch_conf_errors.js` const env = { NODE_CONFIG_DIR: 'test/conf/malformatted_env_mapping_file', PORT: '8081' } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.deepEqual(content.name, 'YAMLException') @@ -147,7 +300,7 @@ test.cb('It throws an error when base file is not yaml valid', t => { const testPath = `${__dirname}/helpers/child.catch_conf_errors.js` const env = { NODE_CONFIG_DIR: 'test/conf/malformatted_base_file' } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.deepEqual(content.name, 'YAMLException') @@ -159,7 +312,7 @@ test.cb('It throws an error when env file is not yaml valid', t => { const testPath = `${__dirname}/helpers/child.catch_conf_errors.js` const env = { NODE_CONFIG_DIR: 'test/conf/malformatted_env_file', NODE_ENV: 'dev' } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.deepEqual(content.name, 'YAMLException') @@ -171,7 +324,7 @@ test.cb('It throws an error when no base file', t => { const testPath = `${__dirname}/helpers/child.catch_conf_errors.js` const env = { NODE_CONFIG_DIR: 'test/conf/no_base_file', NODE_ENV: 'dev' } - const child = childProcess.fork(testPath, { env: env }) + const child = childProcess.fork(testPath, { env }) child.on('message', content => { t.deepEqual(content.name, 'Error')