All files / config index.js

69.62% Statements 55/79
59.09% Branches 39/66
100% Functions 7/7
69.62% Lines 55/79
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230        1x 1x 1x 1x   1x 1x   1x 1x     1x               1x                                 1x 1x 1x   1x 1x 1x             1x                               1x   1x   1x 1x 1x 1x     1x           8x 8x 8x     8x               2x                   1x 1x                       2x 1x     1x 1x       1x                                                   1x 1x 1x 1x 1x 1x 1x                                                                       16x 2x     14x 14x 14x               1x   1x   1x   1x   1x   1x   1x        
// This file exports a configuration object that is used throughout the
// application to customize behavior. That object is built from environment
// variables, an optional configuration file, and from default values.
 
const _ = require('lodash');
const fs = require('fs');
const log4js = require('log4js');
const path = require('path');
 
const SUPPORTED_ENVIRONMENTS = [ 'development', 'production', 'test' ];
const SUPPORTED_LOG_LEVELS = [ 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL' ];
 
const pkg = require(path.join('..', 'package'));
const root = path.normalize(path.join(__dirname, '..'));
 
// Immutable configuration & utility functions
const fixedConfig = {
  logger: createLogger,
  root: root,
  version: pkg.version,
  path: joinProjectPath
};
 
// Configuration from environment variables
const configFromEnvironment = {
  bcryptCost: parseConfigInt(get('BCRYPT_COST')),
  cors: parseConfigBoolean(get('CORS')),
  db: get('DATABASE_URL') || buildDatabaseUrl(),
  docs: {
    browser: get('DOCS_BROWSER'),
    host: get('DOCS_HOST'),
    open: get('DOCS_OPEN'),
    port: get('DOCS_PORT')
  },
  env: process.env.NODE_ENV,
  logLevel: get('LOG_LEVEL'),
  port: parseConfigInt(get('PORT')),
  sessionSecret: get('SESSION_SECRET')
};
 
// Configuration from a local file (`config/local.js` by default, or `$CONFIG`)
let configFromLocalFile = {};
const localConfigFile = path.resolve(root, get('CONFIG') || path.join('config', 'local.js'));
Iif (localConfigFile != joinProjectPath('config', 'local.js') && !fs.existsSync(localConfigFile)) {
  throw new Error(`No configuration file found at ${localConfigFile}`);
} else Eif (fs.existsSync(localConfigFile)) {
  const localConfig = require(localConfigFile);
  configFromLocalFile = _.pick(localConfig,
    'bcryptCost', 'cors', 'db',
    'docs.browser', 'docs.host', 'docs.open', 'docs.port',
    'env', 'logLevel', 'port', 'sessionSecret');
}
 
// Default configuration
const defaultConfig = {
  bcryptCost: 10,
  cors: false,
  db: 'postgres://localhost/biopocket',
  docs: {
    host: '127.0.0.1',
    open: true,
    port: undefined
  },
  env: 'development',
  logLevel: 'INFO',
  port: 3000
};
 
// Environment variables take precedence over the configuration file, and both
// take precedence over the default configuration.
const config = _.merge({}, defaultConfig, configFromLocalFile, configFromEnvironment, fixedConfig);
 
validate(config);
 
const logger = config.logger('config')
logger.debug(`Environment is ${config.env} (change with $NODE_ENV or config.env)`);
logger.debug(`bcrypt cost is ${config.bcryptCost} (change with $BCRYPT_COST or config.bcryptCost)`);
logger.debug(`Log level is ${logger.level} (change with $LOG_LEVEL or config.logLevel)`);
 
// Export the configuration
module.exports = config;
 
// Creates a named log4js logger with trace/debug/info/warn/error/fatal methods
// you can use to log messages concerning a specific component.
function createLogger(name) {
 
  const logger = log4js.getLogger(name);
  Eif (config.logLevel) {
    logger.level = config.logLevel.toUpperCase();
  }
 
  return logger;
}
 
// Returns a path formed by appending the specified segments to the project's
// root directory.
//
//     config.path('foo', 'bar'); // => "/path/to/project/foo/bar"
function joinProjectPath(...segments) {
  return path.join(...([ root ].concat(segments)));
}
 
// Parses a string value as a boolean.
//
// * Returns the specified default value if the value is undefined.
// * To be considered `true`, a boolean string must be "1", "y", "yes", "t" or
//   "true" (case insensitive).
// * If the value is not a boolean, it will be considered `true` if "truthy".
function parseConfigBoolean(value, defaultValue) {
  Eif (value === undefined) {
    return defaultValue;
  } else if (!_.isString(value)) {
    return !!value;
  } else {
    return !!value.match(/^(1|y|yes|t|true)$/i);
  }
}
 
// Parse a string value as an integer.
//
// * Returns the specified default value if the value is undefined.
function parseConfigInt(value, defaultValue) {
  if (value === undefined) {
    return defaultValue;
  }
 
  const parsed = parseInt(value, 10);
  Iif (_.isNaN(parsed)) {
    throw new Error(value + ' is not a valid integer');
  }
 
  return parsed;
}
 
// Constructs a PostgreSQL database URL from several environment variables:
//
// * `$DATABASE_HOST` - The host to connect to (defaults to `localhost`)
// * `$DATABASE_PORT` - The port to connect to on the host (none by default, will use PostgreSQL's default 5432 port)
// * `$DATABASE_NAME` - The name of the database to connect to (defaults to `biopocket`)
// * `$DATABASE_USERNAME` - The name of the PostgreSQL user to connect as (none by default)
// * `$DATABASE_PASSWORD` - The password to authenticate with (none by default)
//
// Returns undefined if none of the variables are set.
//
//     buildDatabaseUrl(); // => undefined
//
//     process.env.DATABASE_NAME = 'biopocket'
//     buildDatabaseUrl(); // => "postgres://localhost/biopocket"
//
//     process.env.DATABASE_HOST = 'db.example.com'
//     process.env.DATABASE_PORT = '1337'
//     process.env.DATABASE_NAME = 'thebiopocketdb'
//     process.env.DATABASE_USERNAME = 'jdoe'
//     process.env.DATABASE_PASSWORD = 'changeme'
//     buildDatabaseUrl(); // => "postgres://jdoe:changeme@db.example.com:1337/thebiopocketdb"
function buildDatabaseUrl() {
 
  const host = get('DATABASE_HOST');
  const port = get('DATABASE_PORT');
  const name = get('DATABASE_NAME');
  const username = get('DATABASE_USERNAME');
  const password = get('DATABASE_PASSWORD');
  Eif (host === undefined && port === undefined && name === undefined && username === undefined && password === undefined) {
    return undefined;
  }
 
  let url = 'postgres://';
 
  // Add credentials (if any)
  if (username) {
    url += username;
 
    if (password) {
      url += `:${password}`;
    }
 
    url += '@';
  }
 
  // Add host and port
  url += `${host || 'localhost'}`;
  if (port) {
    url += `:${port}`;
  }
 
  // Add database name
  url += `/${name || 'biopocket'}`;
 
  return url;
}
 
// Returns a variable from the environment.
//
// Given `"FOO"`, this function will look first in the `$FOO` environment
// variable and returns its value if found. Otherwise, it will look for the
// `$FOO_FILE` environment variable and, if found, will attempt to read the
// contents of the file pointed to by its value. Otherwise it will return
// undefined.
function get(varName) {
  if (_.has(process.env, varName)) {
    return process.env[varName];
  }
 
  const fileVarName = `${varName}_FILE`;
  Eif (!_.has(process.env, fileVarName)) {
    return undefined;
  }
 
  return fs.readFileSync(process.env[fileVarName], 'utf8').trim();
}
 
// Ensures all properties of the configuration are valid.
function validate(config) {
  Iif (!_.isInteger(config.bcryptCost) || config.bcryptCost < 1) {
    throw new Error(`Unsupported bcrypt cost "${config.bcryptCost}" (type ${typeof(config.bcryptCost)}); must be an integer greater than or equal to 1`);
  } else Iif (!_.isBoolean(config.cors)) {
    throw new Error(`Unsupported CORS value "${config.cors}" (type ${typeof(config.cors)}); must be a boolean`);
  } else Iif (!_.isString(config.db) || !config.db.match(/^postgres:\/\//)) {
    throw new Error(`Unsupported database URL "${config.db}" (type ${typeof(config.db)}); must be a string starting with "postgres://"`);
  } else Iif (!_.includes(SUPPORTED_ENVIRONMENTS, config.env)) {
    throw new Error(`Unsupported environment "${JSON.stringify(config.env)}"; must be one of: ${SUPPORTED_ENVIRONMENTS.join(', ')}`);
  } else Iif (!_.isString(config.logLevel) || !_.includes(SUPPORTED_LOG_LEVELS, config.logLevel.toUpperCase())) {
    throw new Error(`Unsupported log level "${config.logLevel}" (type ${typeof(config.logLevel)}); must be one of: ${SUPPORTED_LOG_LEVELS.join(', ')}`);
  } else Iif (!_.isInteger(config.port) || config.port < 1 || config.port > 65535) {
    throw new Error(`Unsupported port number "${config.port}" (type ${typeof(config.port)}); must be an integer between 1 and 65535`);
  } else Iif (!_.isString(config.sessionSecret) || config.sessionSecret == 'changeme') {
    throw new Error(`Unsupported session secret "${config.sessionSecret}" (type ${typeof(config.sessionSecret)}); must be a string different than "changeme"`);
  }
}