confmgr
Version:
Env. Configuration Manager
682 lines (482 loc) • 21.5 kB
Markdown

`confmgr` is (yet another) configuration manager for your NodeJS apps. It is written in Typescript and thus supports Typescript as well as Javascript.
Check out the [documentation](https://chevdor.github.io/confmgr/) for more details.
**Why **another** one?**
`confmgr` brings interesting features that are not available in libs. While focusing on ease of use, it brings features that no longer need to be part of your app such as:
- type conversion (variables from ENV are always strings, `confmgr` converts them for you)
- mandatory values: If you miss a value, `confmgr` will tell you before your app explodes…
- default values: `confmrg` handles default values for you
- regular expressions: `confmgr` allows you defining a regexp that will allow checking the validity of the loaded config
- masked variables: handy for your secrets, read below
**Helper functions**
After getting your config object with `GetConfig()`, you can simple use it with `config['MYAPP_MYMODULE_VAR1']` or use some of the helper methods attached to your config.
`confmgr` brings a set of handy methods such as `Print()` that will display the content of your config while ensuring that your secrets do NOT show up in any log or output.
Printing your config may use colors or not. Here is an example with color coming from running `config.Print()`:
===> TS_SAMPLE_MODULE_01 ENV:
✅ PARAM1: some param1
value: 12
✅ PARAM2: some param2
value: 44
✅ SECRET: some secret
value: ***** <---- Notice how this value has been masked
✅ REGEXP: some regexp
regexp: ^\d{2}_\d{2}
value: 68_77
✅ MANDAT_WITH_DEF: some optional param
value: 42
❌ MANDAT_NO_DEF: some optional param <----- It looks like we forgot to pass a value
value: undefined
If you are using a custom logger, you may also pass it to the `Print()` method so the output gets nicely integrated to your logs while NOT revealing any of your secrets.
You can keep reading this file and/or check the `doc` and `samples` folders.
You can also visit <https://chevdor.gitlab.io/confmgr> to see the old API documentation.
**Using NPM**
npm install --save confmgr
**Using Yarn**
yarn add confmgr
import { ConfigManager } from 'confmgr';
const config = ConfigManager.getInstance('configSpecs.yml').getConfig();
const valid = config.Validate();
console.log(`Your config is${valid ? '' : ' NOT'} valid!`);
config.Print();
The repo contains many more samples in both Typescript and Javascript, using YML and the Specs Factory.
- Written in Typescript
- Available for Typescript or Javascript
- You create your config specs using a Factory or specs in YAML or JSON format
- Your config is loaded from your ENV or an .env file
- Support for multiple .env (debug, etc…)
- Your config is validated according to your specs
- Your config is made available to you in a super easy way
- You can Print your config in color using the console or use your own logger
- protects your secret and will never display them
- will check for value you defined as mandatory and warn you if they are missing
- supports default values and will apply them has needed
- supports type for an easy conversion from string to whatever your app needs
In order to bring a `config` object to your app, you need define some specs first.
That can be done via code using the provided Factory (see samples in the samples folder of the repo). You can also simply provide the specs as a YAML file.
Writing the specs as a YAML file makes it simple to also share the specs of your app to your users as this is very readable.
**Here is how a spec file in the YAML format looks like:**
MYAPP: # Name of your App
MYMODULE: # Name of your module
VAR1: # Here is the simplest example, we named it VAR1
description: some string
VAR2:
description: some number
type: number
default: 42 # If for some reason, this value is not set, confmgr will set the default value
VAR3:
description: some array
type: array
default: ['a', 'b', 'c']
VAR4:
description: some object
type: object
default: { name: "foo", age: 42 }
VAR5:
description: A mandatory variable. If that one is missing, will be unhappy
mandatory: true
VAR6:
description: A optional variable we can count on
default: 42
VAR7:
description: Some variable we check with a regpex
regexp: ^\d{2}_\d{2}
**Here is how a spec file in the JSON format looks like:**
{
"SAMPLE": {
"MODULE": {
"BOOL1": {
"description": "some regexp",
"type": "boolean"
},
"BOOL2": {
"description": "some regexp",
"type": "boolean",
"regexp": {
"pattern": "^true|false$",
"attributes": "i"
}
},
"BOOL3": {
"description": "some regexp",
"regexp": {
"pattern": "^true|false$",
"attributes": "i"
}
},
"URL": {
"description": "some web socket URL",
"default": "ws://localhost:1234",
"regexp": "^wss?://\\w+:\\d+$"
}
}
}
}
You may pass values as Environment Variables as shown below:
MYAPP_MODULE_VAR1='foo bar' node index.js
You may let `confmgr` generate a default config file using:
import { ConfigManager } from 'confmgr'
const config = ConfigManager.getInstance('configSpecs.yml').getConfig()
console.log(config.GenEnv().join('\n'))
You can see this in action in the sample 03. The output looks like:
TS_SAMPLE_MODULE_01_PARAM1=
TS_SAMPLE_MODULE_01_PARAM2=
TS_SAMPLE_MODULE_01_SECRET=
TS_SAMPLE_MODULE_01_BOOL1=true
TS_SAMPLE_MODULE_01_BOOL2=true
TS_SAMPLE_MODULE_01_ARRAY1=[1,"a",true,{"foo":"bar"}]
TS_SAMPLE_MODULE_01_OBJ1={"foo":"bar","list":[1,2,3]}
TS_SAMPLE_MODULE_02_REGEXP=
TS_SAMPLE_MODULE_02_MANDAT_WITH_DEF=42
TS_SAMPLE_MODULE_02_MANDAT_NO_DEF=
You may also simply create an `.env` file at the root of your project:
MYAPP_MODULE_VAR1='foo bar'
You may then simply start your app and the config will be loaded, validated and made available to you.
node index.js
In some case, you may need different `.env` file for production, testing, staging, dev, etc…
The default as we saw above is 'production' and the file that is loaded is `.env`.
If you provide the Environment Variable `NODE_ENV` however and this value is neither empty nor `production`, an alternate `.env.<…>` file will be loaded.
For instance:
- using `NODE_ENV=dev node index.js` to launch your App will load the config from `.env.dev`
- using `NODE_ENV=abc node index.js` to launch your App will load the config from `.env.abc`
Have you ever looks at the environment variables on your machine? Give it a go and issue `env`. That\`s probably not very pretty…
Moreover, say your app needs to access a database, using an ENV called `DATABASE` will add even more to this mess.
For this reason, `confmgr` helps you keeps things nice, tidy and collision-free thanks to a `PREFIX` and `MODULE`. Those are 2 strings, usually in CAPS. Any of your configuration value will be prepended with `PREFIX_MODULE_`.
That makes it also very easy to find all the variables of your app using a shell command such as `env | grep MYAPP_MYMODULE`.
The `PREFIX` is the root of your YAML file. You find the `MODULE` just below and then your variables.
At the moment, only a single PREFIX and a single MODULE is supported.
A variable is usually named in CAPS. In your YAML, it looks like:
MYAPP:
MYMODULE:
VAR1:
description: some string
Having such as spec means our App expects an ENV called `MYAPP_MYMODULE_VAR1` to be defined. At that point, if the variable is missing or contains an incorrect valule, `confmgr` won’t do anything as it does not know yet about our needs.
The only requirements to define a new variables are:
- a name (ie. `VAR1`)
- a description
The description will show up as you Print your config:
===> TS_SAMPLE_MODULE_01 ENV:
✅ PARAM1: some param1
value: 12
✅ PARAM2: some param2
value: 44
✅ SECRET: some secret
value: *****
Options are optional… I said it…
The best to see what they are is to search for the `ConfigItemOptions` type in the code:
export type ConfigItemOptions = {
masked?: boolean // true for tokens, passwords, etc..
regexp?: RegExp | RegexpWithAttributes // validation regexp
mandatory?: boolean // Do we explode if this ENV is not defined and there is no default?
default?: ConfigValue // The default if nothing is provided
type?: Type // If provided, the ConfigManager will do some conversion for us
}
If a variable is marked as `masked=true`, its value will never be shown when using `Print`.
You may add validation your variables using a regular expression.
MYAPP:
MYMODULE:
REGEXP:
description: some regexp
regexp: ^\d{2}_\d{2}
means and enforces that `MYAPP_MYMODULE_REGEXP` contains a value made of 2 digits followed by underscore and another 2 digits.
`12_34` would be valid whereas `123_45` is not.
If your ENV does not contain this value, the default will be applied.
If your ENV does not contain this value and you did not define a `default`, then the validation of your config with `config.Validate()` will fail (= return false) and using `config.Print()` will highlight issues as shown below:
===> TS_SAMPLE_MODULE_01 ENV:
✅ MANDAT_WITH_DEF: some optional param
value: 42
❌ MANDAT_NO_DEF: some optional param
value: undefined
The supported types can also be found in the code:
/**
* This describes the type of the config items.
*/
export type Type = 'string' | 'boolean' | 'number' | 'array' | 'object'
**Javascript**
const ConfigManager = require('confmgr').ConfigManager;
**Typescript**
import { ConfigManager } from 'confmgr';
You may have a look at the samples in the `samples` folder. There are simple demos in Typescript and Javascript.
In short, you first need to define the specs of your config. You can do that using Yaml or using a Factory directly, as shown below:
**Using a Yaml file**
This is the commend option and likely the easiest one.
TS_SAMPLE:
MODULE_01:
PARAM1:
description: some param1
PARAM2:
description: some param2
SECRET:
description: some secret
masked: true
BOOL1:
description: bool1
type: boolean
default: true
BOOL2:
description: bool2
type: boolean
default: true
ARRAY1:
description: some array
default: [1, 'a', true, { foo: bar }]
OBJ1:
description: some object
default: { foo: bar, list: [1, 2, 3] }
MODULE_02:
REGEXP:
description: some regexp
regexp: ^\d{2}_\d{2}
MANDAT_WITH_DEF:
description: some optional param
mandatory: true
default: 42
MANDAT_NO_DEF:
description: some optional param
mandatory: true
**Using the Factory**
import { SpecsFactory } from 'confmgr'
const prefix = 'TS_SAMPLE'
const mod = 'MODULE_01'
const factory = new SpecsFactory({ prefix })
factory.appendSpec(mod, factory.getSpec('PARAM1', 'some param1'))
factory.appendSpec(mod, factory.getSpec('PARAM2', 'some param2'))
factory.appendSpec(
mod,
factory.getSpec('SECRET', 'some secret', { masked: true })
)
factory.appendSpec(
mod,
factory.getSpec('REGEXP', 'some regexp', { regexp: /^\d{2}_\d{2}/ })
)
factory.appendSpec(
mod,
factory.getSpec('MANDAT1', 'some mandatory param', { mandatory: true })
)
const specs = factory.getSpecs()
export default specs
Once you defined the specs for your config, you need to create an `.env` file (unless you plan on passing the config as Environment Variables). It looks as following for the specs mentioned above:
TS_SAMPLE_MODULE_01_PARAM1=12
TS_SAMPLE_MODULE_01_PARAM2=44
TS_SAMPLE_MODULE_01_SECRET=secret
TS_SAMPLE_MODULE_01_REGEXP=68_77
TS_SAMPLE_MODULE_01_MANDAT1=27
You may now use your config:
import { ConfigManager } from 'confmgr'
import mySpecs from './configSpecs'
const config = ConfigManager.getInstance(mySpecs).getConfig()
const valid = config.Validate()
console.log(`Your config is${valid ? '' : ' NOT'} valid!`)
config.Print({ color: true, compact: false })
console.log(`PARAM1=${config.Get('MODULE_01', 'PARAM1')}`)
You should never display your config, especially in your logs, as it may contain some secrets! Instead, use the Print() method of the ConfigManager, it will mask sensible data.
## Dev
First run:
yarn setup
Then you may for instance start with the tests:
yarn test:watch
## Contributing
If you are considering contributing, you will find many ways to apply your skills. There is still a lot that can be done in this project. Feel free to submit any PR you think may improve the lib or stop by the issue list and check the [Contributions Welcome](https://github.com/chevdor/confmgr/issues?label_name%5B%5D=Contribution+Welcome) label, it may give you a few ideas.
You may report any bug or submit your PRs at <https://github.com/chevdor/confmgr>
# Samples
# TS Basic with valid .env
Run with the following:
yarn clean
yarn
yarn start
**Specs**
import { SpecsFactory } from 'confmgr'
const prefix = 'TS_SAMPLE'
const mod = 'MODULE_01'
const factory = new SpecsFactory({ prefix })
factory.appendSpec(mod, factory.getSpec('PARAM1', 'some param1'))
factory.appendSpec(mod, factory.getSpec('PARAM2', 'some param2'))
factory.appendSpec(
mod,
factory.getSpec('SECRET', 'some secret', { masked: true })
)
factory.appendSpec(
mod,
factory.getSpec('REGEXP', 'some regexp', { regexp: /^\d{2}_\d{2}/ })
)
factory.appendSpec(
mod,
factory.getSpec('MANDAT1', 'some mandatory param', { mandatory: true })
)
const specs = factory.getSpecs()
export default specs
**Example**
import { ConfigManager } from 'confmgr'
import mySpecs from './configSpecs'
const config = ConfigManager.getInstance(mySpecs).getConfig()
const valid = config.Validate()
console.log(`Your config is${valid ? '' : ' NOT'} valid!`)
config.Print({ color: true, compact: false })
console.log(`PARAM1=${config.Get('MODULE_01', 'PARAM1')}`)
This is a Typescript sample.
Run with the following:
yarn clean
yarn
yarn start
**Specs**
import { SpecsFactory } from 'confmgr'
const prefix = 'TS_SAMPLE'
const mod = 'MODULE_01'
const factory = new SpecsFactory({ prefix })
factory.appendSpec(mod, factory.getSpec('PARAM1', 'some param1'))
factory.appendSpec(mod, factory.getSpec('PARAM2', 'some param2'))
factory.appendSpec(
mod,
factory.getSpec('SECRET', 'some secret', { masked: true })
)
factory.appendSpec(
mod,
factory.getSpec('REGEXP', 'some regexp', { regexp: /^\d{2}_\d{2}/ })
)
factory.appendSpec(
mod,
factory.getSpec('MANDAT1', 'some mandatory param', { mandatory: true })
)
const specs = factory.getSpecs()
export default specs
**Example**
import { ConfigManager } from 'confmgr'
import mySpecs from './configSpecs'
const config = ConfigManager.getInstance(mySpecs).getConfig()
const valid = config.Validate()
console.log(`Your config is${valid ? '' : ' NOT'} valid!`)
// Here is our custom logger...
function MyLogger(...args): void {
const s = args.join(' ')
console.log(`▶ ${s.replace('\n', '\n▶')}`)
}
config.Print({ color: true, logger: MyLogger })
This is a Typescript sample.
Run with the following:
yarn clean
yarn
yarn start
**Specs**
TS_SAMPLE:
MODULE_01:
PARAM1:
description: some param1
PARAM2:
description: some param2
SECRET:
description: some secret
masked: true
BOOL1:
description: bool1
type: boolean
default: true
BOOL2:
description: bool2
type: boolean
default: true
ARRAY1:
description: some array
default: [1, 'a', true, { foo: bar }]
OBJ1:
description: some object
default: { foo: bar, list: [1, 2, 3] }
MODULE_02:
REGEXP:
description: some regexp
regexp: ^\d{2}_\d{2}
MANDAT_WITH_DEF:
description: some optional param
mandatory: true
default: 42
MANDAT_NO_DEF:
description: some optional param
mandatory: true
**Example**
import { ConfigManager } from 'confmgr'
const config = ConfigManager.getInstance('configSpecs.yml').getConfig()
const valid = config.Validate()
console.log(`Your config is${valid ? '' : ' NOT'} valid!`)
config.Print({ compact: true })
console.log('\nYour default config looks like:')
console.log(config.GenEnv().join('\n'))
console.log('\nHere are some values:')
console.log(` - PARAM1=${config.Get('MODULE_01', 'PARAM1')}`)
console.log(` - BOOL1=${config.Get('MODULE_01', 'BOOL1')}`)
console.log(` - BOOL2=${config.Get('MODULE_01', 'BOOL2')}`)
This is a Javascript sample.
Run with the following:
yarn clean
yarn
yarn start
**Specs**
const SpecsFactory = require('confmgr')
const prefix = 'SAMPLE'
const mod = 'MODULE'
const factory = new SpecsFactory.SpecsFactory({ prefix: prefix })
factory.appendSpec(mod, factory.getSpec('PARAM1', 'some param1'))
factory.appendSpec(mod, factory.getSpec('PARAM2', 'some param2'))
factory.appendSpec(
mod,
factory.getSpec('SECRET', 'some secret', { masked: true })
)
factory.appendSpec(
mod,
factory.getSpec('REGEXP', 'some regexp', { regexp: /^\d{2}_\d{2}/ })
)
factory.appendSpec(
mod,
factory.getSpec('MANDAT1', 'some mandatory param', { mandatory: true })
)
const specs = factory.getSpecs()
exports['default'] = specs
**Example**
const ConfigManager = require('confmgr').ConfigManager
const mySpecs = require('./configSpecs').default
const config = ConfigManager.getInstance(mySpecs).getConfig()
const valid = config.Validate()
console.log(`Your config is${valid ? '' : ' NOT'} valid!`)
config.Print({ logger: console.log })
Copyright 2020 - Wilfried Kopp / Chevdor
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.