Here is my principle:
* besides i18n translation key and things like that of course (well, now that we've got symbols in ES6...)
npm install common-env
var env = require('common-env')();
var config = env.getOrElseAll({
amqp: {
login: {
$default: 'guest',
$aliases: ['ADDON_RABBITMQ_LOGIN', 'LOCAL_RABBITMQ_LOGIN']
},
password: 'guest',
host: 'localhost',
port: 5672
}
});
t.strictEqual(config.amqp.login, 'plop'); // converted from env
getOrElseAll
allows you to specify a configuration object with default values that will be resolved from environment variables.
Let say we start a script with AMQP_LOGIN=plop AMQP_CONNECT=true AMQP_EXCHANGES[0]_NAME=new_exchange FACEBOOK_SCOPE="user,timeline" FACEBOOK_BACKOFF="200,800" node test.js
with test.js
defined as follow:
var env = require('common-env')();
var config = env.getOrElseAll({
amqp: {
login: 'guest',
password: 'guest',
host: 'localhost',
port: 5672,
connect: false,
exchanges:[{
name: 'first_exchange'
},{
name: 'second_exchange'
}]
},
FULL_UPPER_CASE: {
PORT: 8080
},
facebook:{
scope:['user', 'timeline', 'whatelse'],
backOff: [200, 500, 700]
},
MICROSTATS: {
HASHKEY: 'B:mx:global'
}
});
t.strictEqual(config.amqp.login, 'plop'); // extracted and converted from env
t.strictEqual(config.amqp.port, 5672);
t.strictEqual(config.amqp.connect, true); // extracted and converted from env
t.strictEqual(config.amqp.exchanges[0].name, 'new_exchange'); // extracted from env
t.strictEqual(config.FULL_UPPER_CASE.PORT, 8080);
t.strictEqual(config.facebook.scope, ['user', 'timeline']); // extracted and converted from env
t.strictEqual(config.facebook.backoff, [200, 800]); // extracted and converted from env
Common-env will emit the following events:
env:fallback(key, $default)
: each time a environment key was not found and that common-env fallback on$default
.env:found(key, value, $default)
// let say NODE_ENV was set to "production"
var env = require('common-env')();
var config = env
.on('env:found', function (fullKeyName, value, $secure) {
value = $secure ? '***' : value;
console.log('[env] %s was defined, using: %s', fullKeyName, String(value));
})
.on('env:fallback', function (fullKeyName, $default, $secure) {
$default = $secure ? '***' : $default;
console.log('[env] %s was not defined, using default: %s', fullKeyName, String($default));
})
.getOrElseAll({
node: {
env: 'production'
},
redsmin: {
gc: {
enabled: false
}
}
});
// Will print
// [env] NODE_ENV was defined, using: production
// [env] REDSMIN_GC_ENABLED was not defined, using default: false
It's sometimes useful to be able to specify aliases, for instance Clever-cloud or Heroku expose their own environment variable names while your application's internal code may not want to rely on them. You may not want to depend on your hosting provider conventions.
Common-env adds a layer of indirection enabling you to specify environment aliases that won't impact your codebase.
Since v6, common-env is able to read arrays from environment variables. Before going further, please don't forget that environment variables do not support arrays, thus MY_ENV_VAR[0]_A
is not a valid environment variable name, as well as MY_ENV_VAR$0$_A
and so on. In fact, the only supported characters are [0-9_]
. But since we wanted a lot array support we had to find a work-around.
And here is what we did:
Configuration key path | Generated environment key |
---|---|
amqp.exchanges[0].name | AMQP_EXCHANGES__0_NAME |
amqp.exchanges[10].name | AMQP_EXCHANGES__10_NAME |
As you can see, we a replacing [0]
, with __0
and thus common-env is compliant with the limited character support while providing an awesome abstraction for configuration through environment variables.
Note that only the first element of the array will be used as a description for every other element of the array. So in the following code:
const config = env.getOrElseAll({
mysql: {
hosts: [{
host: '127.0.0.1',
port: 3306
}, {
auth: {
$type: env.types.String,
$secure: true
}
}]
}
});
only the first object
{ host: '127.0.0.1', port: 3306 }
will be used as a type template for every defined elements.
One last thing, common-env is smart enough to build plain arrays (not sparse), so if you defined MYSQL_HOSTS__10_PORT=3310
, config.mysql.hosts
will contains 10 objects as you thought it would.
Common-env is able to use arrays as key values for instance:
// test.js
var env = require('common-env')();
var config = env.getOrElse({
amqp:{
hosts:['192.168.1.1', '192.168.1.2']
}
});
console.log(config.amqp.hosts);
Running the above script we can override amqp.hosts
values with the AMQP_HOSTS
environment variable we get:
$ node test.js
['192.168.1.1', '192.168.1.2']
$ AMQP_HOSTS='127.0.0.1' node test.js
['127.0.0.1']
$ AMQP_HOSTS='88.23.21.21,88.23.21.22,88.23.21.23' node test.js
['88.23.21.21', '88.23.21.22', '88.23.21.23']
// test.js
var env = require('common-env')();
var config = env.getOrElse({
amqp:{
hosts:{
$default: ['192.168.1.1', '192.168.1.2'],
$aliases: ['ADDON_RABBITMQ_HOSTS', 'LOCAL_RABBITMQ_HOSTS']
}
}
});
console.log(config.amqp.hosts);
Running the above script we can override amqp.hosts
values with the ADDON_RABBITMQ_HOSTS
or LOCAL_RABBITMQ_HOSTS
environment variable aliases we get:
$ node test.js
['192.168.1.1', '192.168.1.2']
$ ADDON_RABBITMQ_HOSTS='127.0.0.1' node test.js
['127.0.0.1']
$ LOCAL_RABBITMQ_HOSTS='88.23.21.21,88.23.21.22,88.23.21.23' node test.js
['88.23.21.21', '88.23.21.22', '88.23.21.23']
Aliases don't supports arrays in their names and never will.
If $default
is not defined and no environment variables (aliases included) resolve to a value then common-env will throw an error. This error should not be caught in order to make the app crash, following the fail-fast principle.
Since common-env uses $default
to infer the environment variable type, if $default
is not available common-env won't be able to use the right type, for instance:
// ...
var config = env.getOrElseAll({
redis:{
hosts: {
$aliases: ['REDIS_ADDON_PORTS']
}
}
});
config.redis.ports
should be an array of number but instead common-env will fallback to a string because it can't infer what should be the type of config.redis.ports
. That's where $type
is handy if gives you a way to tell common-env how it should convert the value:
// ...
var config = env.getOrElseAll({
redis:{
hosts: {
$aliases: ['REDIS_ADDON_PORTS'],
$type: env.types.Array(env.types.Number)
}
}
Note that $aliases
isn't mandatory with $type
.
As of today, currently supported types are:
env.types.String
env.types.Integer
env.types.Float
env.types.Boolean
env.types.Array(env.types.String)
env.types.Array(env.types.Integer)
env.types.Array(env.types.Float)
env.types.Array(env.types.Boolean)
Let's take the following configuration object:
{
amqp: {
login: {
$default: 'guest',
$aliases: ['ADDON_RABBITMQ_LOGIN', 'LOCAL_RABBITMQ_LOGIN']
},
password: 'guest',
host: 'localhost',
port: 5672
}
}
Here is how common-env will resolve amqp.login
:
- Common-env will first read
ADDON_RABBITMQ_LOGIN
environment variable, if it exists, its value will be used. - If not common-env will read
LOCAL_RABBITMQ_LOGIN
, if it exists, its value will be used. - If not common-env will read
AMQP_LOGIN
, if it exists, its value will be used. - If not common-env will fallback on
$default
value.
Common-env 1.x.x-2.x.x was displaying logs, here is how to retrieve the same behaviour in 3.x.x.
var logger = console;
var config = require('common-env/withLogger')(logger).getOrElseAll({
amqp: {
login: {
$default: 'guest',
$aliases: ['ADDON_RABBITMQ_LOGIN', 'LOCAL_RABBITMQ_LOGIN']
},
password: 'guest',
host: 'localhost',
port: 5672
}
});
var logger = console;
var config = require('common-env/withLogger')(logger).getOrElseAll({
amqp: {
password: {
$default: 'guest',
$secure: true
}
}
});
// Console output:
// [env] AMQP_PASSWORD was not defined, using default: ***"
// [env] AMQP_PASSWORD was defined, using: ***"
I maintain this project in my free time, if it helped you please support my work via paypal or Bitcoins, thanks a lot!