var fs = require('fs'); var path = require('path'); var ConfigLoader = exports; /** * Load application config from `config.json` and friends. * @param {String} rootDir Directory where to look for files. * @param {String} env Environment, usually `process.env.NODE_ENV` * @returns {Object} */ ConfigLoader.loadAppConfig = function(rootDir, env) { return loadNamed(rootDir, env, 'config', mergeAppConfig); }; /** * Load data-sources config from `datasources.json` and friends. * @param {String} rootDir Directory where to look for files. * @param {String} env Environment, usually `process.env.NODE_ENV` * @returns {Object} */ ConfigLoader.loadDataSources = function(rootDir, env) { return loadNamed(rootDir, env, 'datasources', mergeDataSourceConfig); }; /** * Load model config from `model-config.json` and friends. * @param {String} rootDir Directory where to look for files. * @param {String} env Environment, usually `process.env.NODE_ENV` * @returns {Object} */ ConfigLoader.loadModels = function(rootDir, env) { /*jshint unused:false */ return tryReadJsonConfig(rootDir, 'model-config') || {}; }; /*-- Implementation --*/ /** * Load named configuration. * @param {String} rootDir Directory where to look for files. * @param {String} env Environment, usually `process.env.NODE_ENV` * @param {String} name * @param {function(target:Object, config:Object, filename:String)} mergeFn * @returns {Object} */ function loadNamed(rootDir, env, name, mergeFn) { var files = findConfigFiles(rootDir, env, name); var configs = loadConfigFiles(files); return mergeConfigurations(configs, mergeFn); } /** * Search `appRootDir` for all files containing configuration for `name`. * @param {String} appRootDir * @param {String} env Environment, usually `process.env.NODE_ENV` * @param {String} name * @returns {Array.} Array of absolute file paths. */ function findConfigFiles(appRootDir, env, name) { var master = ifExists(name + '.json'); if (!master) return []; var candidates = [ master, ifExistsWithAnyExt(name + '.local'), ifExistsWithAnyExt(name + '.' + env) ]; return candidates.filter(function(c) { return c !== undefined; }); function ifExists(fileName) { var filepath = path.resolve(appRootDir, fileName); return fs.existsSync(filepath) ? filepath : undefined; } function ifExistsWithAnyExt(fileName) { return ifExists(fileName + '.js') || ifExists(fileName + '.json'); } } /** * Load configuration files into an array of objects. * Attach non-enumerable `_filename` property to each object. * @param {Array.} files * @returns {Array.} */ function loadConfigFiles(files) { return files.map(function(f) { var config = require(f); Object.defineProperty(config, '_filename', { enumerable: false, value: f }); return config; }); } /** * Merge multiple configuration objects into a single one. * @param {Array.} configObjects * @param {function(target:Object, config:Object, filename:String)} mergeFn */ function mergeConfigurations(configObjects, mergeFn) { var result = configObjects.shift() || {}; while(configObjects.length) { var next = configObjects.shift(); mergeFn(result, next, next._filename); } return result; } function mergeDataSourceConfig(target, config, fileName) { for (var ds in target) { var err = mergeObjects(target[ds], config[ds]); if (err) { throw new Error('Cannot apply ' + fileName + ' to `' + ds + '`: ' + err); } } } function mergeAppConfig(target, config, fileName) { var err = mergeObjects(target, config); if (err) { throw new Error('Cannot apply ' + fileName + ': ' + err); } } function mergeObjects(target, config, keyPrefix) { for (var key in config) { var fullKey = keyPrefix ? keyPrefix + '.' + key : key; var err = mergeSingleItemOrProperty(target, config, key, fullKey); if (err) return err; } return null; // no error } function mergeSingleItemOrProperty(target, config, key, fullKey) { var origValue = target[key]; var newValue = config[key]; if (!hasCompatibleType(origValue, newValue)) { return 'Cannot merge values of incompatible types for the option `' + fullKey + '`.'; } if (Array.isArray(origValue)) { return mergeArrays(origValue, newValue, fullKey); } if (typeof origValue === 'object') { return mergeObjects(origValue, newValue, fullKey); } target[key] = newValue; return null; // no error } function mergeArrays(target, config, keyPrefix) { if (target.length !== config.length) { return 'Cannot merge array values of different length' + ' for the option `' + keyPrefix + '`.'; } // Use for(;;) to iterate over undefined items, for(in) would skip them. for (var ix=0; ix < target.length; ix++) { var fullKey = keyPrefix + '[' + ix + ']'; var err = mergeSingleItemOrProperty(target, config, ix, fullKey); if (err) return err; } return null; // no error } function hasCompatibleType(origValue, newValue) { if (origValue === null || origValue === undefined) return true; if (Array.isArray(origValue)) return Array.isArray(newValue); if (typeof origValue === 'object') return typeof newValue === 'object'; // Note: typeof Array() is 'object' too, // we don't need to explicitly check array types return typeof newValue !== 'object'; } /** * Try to read a config file with .json extension * @param cwd Dirname of the file * @param fileName Name of the file without extension * @returns {Object|undefined} Content of the file, undefined if not found. */ function tryReadJsonConfig(cwd, fileName) { try { return require(path.join(cwd, fileName + '.json')); } catch(e) { if(e.code !== 'MODULE_NOT_FOUND') { throw e; } } }