loopback-boot/lib/config-loader.js

260 lines
7.4 KiB
JavaScript

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') || {};
};
/**
* Load middleware config from `middleware.json` and friends.
* @param {String} rootDir Directory where to look for files.
* @param {String} env Environment, usually `process.env.NODE_ENV`
* @returns {Object}
*/
ConfigLoader.loadMiddleware = function(rootDir, env) {
return loadNamed(rootDir, env, 'middleware', mergeMiddlewareConfig);
};
/**
* Load component config from `component-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.loadComponents = function(rootDir, env) {
return loadNamed(rootDir, env, 'component-config', mergeComponentConfig);
};
/*-- 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.<String>} 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.<String>} files
* @returns {Array.<Object>}
*/
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.<Object>} 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 mergeMiddlewareConfig(target, config, fileName) {
var err;
for (var phase in config) {
if (phase in target) {
err = mergePhaseConfig(target[phase], config[phase], phase);
} else {
err = 'The phase "' + phase + '" is not defined in the main config.';
}
if (err)
throw new Error('Cannot apply ' + fileName + ': ' + err);
}
}
function mergePhaseConfig(target, config, phase) {
var err;
for (var middleware in config) {
if (middleware in target) {
err = mergeObjects(target[middleware], config[middleware]);
} else {
err = 'The middleware "' + middleware + '" in phase "' + phase + '"' +
'is not defined in the main config.';
}
if (err) return err;
}
}
function mergeComponentConfig(target, config, fileName) {
for (var c in target) {
var err = mergeObjects(target[c], config[c]);
if (err) {
throw new Error('Cannot apply ' + fileName + ' to `' + c + '`: ' + 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 {string} cwd Dirname of the file
* @param {string} 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;
}
}
}