Skip to content

FGRibreau/common-env

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation


common-env configuration through environment variables finally done right.

Build Status Coverage Status NPM version Downloads

Get help on Codementor available-for-advisory extra Slack

Philosophy

Here is my principle:

uslide_52

* besides i18n translation key and things like that of course (well, now that we've got symbols in ES6...)

NPM

npm install common-env

Usage

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

env.getOrDie(envVarName)

env.getOrElse(envVarName, default)

env.getOrElseAll(object)

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

Events

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

Specifying multiple aliases

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.

How to handle environment variable arrays

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.

How to specify environment variable arrays

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']

How to specify environment variable arrays using $aliases

// 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.

fail-fast behaviour

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.

How to define type converters

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)

How common-env resolves environment variables

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.

How to retrieve old common-env logging behaviour

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
  }
});

How to set silent (or secure) values in output logger

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: ***"

Donate

I maintain this project in my free time, if it helped you please support my work via paypal or Bitcoins, thanks a lot!