Comparing version 0.2.9 to 0.4.0
@@ -1,125 +0,853 @@ | ||
/******************************************************************************* | ||
* config.js - Main module for node-config | ||
******************************************************************************** | ||
*/ | ||
// Dependencies | ||
var deps = require('../deps'); | ||
var _ = deps._; | ||
var ext = require('./extensions'); | ||
var File = require('fs'); | ||
var Yaml = require('yaml'); | ||
var FileSystem = require('fs'); | ||
// Saved configurations key=moduleName, value=configObj | ||
var savedConfigs = {}; | ||
// Static members | ||
var DEFAULT_CLONE_DEPTH = 6, | ||
FILE_WATCHER_INTERVAL = 3000, | ||
CONFIG_PATH = process.cwd() + '/config/', | ||
runtimeJsonFilename = CONFIG_PATH + 'runtime.json', | ||
originalConfig = null, // Not including the runtime.json values | ||
runtimeJson = {}, // Current runtimeJson extensions | ||
isQueuedForPersistence = false; | ||
// Saved configuration files. key=filename, value=configObj | ||
var savedConfigFiles = {}; | ||
/** | ||
* <p>Runtime Application Configurations</p> | ||
* | ||
* <p> | ||
* The config module exports a singleton object representing all runtime | ||
* configurations for this application deployment. | ||
* </p> | ||
* | ||
* <p> | ||
* Application configurations are stored in files within the config directory | ||
* of your application. The default configuration file is loaded, followed | ||
* by files specific to the deployment type (development, testing, staging, | ||
* production, etc.). | ||
* </p> | ||
* | ||
* <p> | ||
* For example, with the following config/default.yaml file: | ||
* </p> | ||
* | ||
* <pre> | ||
* ... | ||
* customer: | ||
* initialCredit: 500 | ||
* db: | ||
* name: customer | ||
* port: 5984 | ||
* ... | ||
* </pre> | ||
* | ||
* <p> | ||
* The following code loads the customer section into the CONFIG variable: | ||
* <p> | ||
* | ||
* <pre> | ||
* var CONFIG = require('config').customer; | ||
* ... | ||
* newCustomer.creditLimit = CONFIG.initialCredit; | ||
* database.open(CONFIG.db.name, CONFIG.db.port); | ||
* ... | ||
* </pre> | ||
* | ||
* @module config | ||
* @class Config | ||
*/ | ||
/******************************************************************************* | ||
* config() - Build a module configuration object | ||
******************************************************************************** | ||
* This takes a default configuration object for a module, and folds in values | ||
* from configuration files and values passed in from the command line. | ||
* | ||
* See README.md for more information | ||
* | ||
* Input: | ||
* modName - Module name | ||
* defaultConf - The program default configurations | ||
* | ||
* Output: | ||
* conf - The mixed configuration object | ||
*/ | ||
module.exports = function(modName, origConfig) { | ||
/** | ||
* <p>Get the configuration object.</p> | ||
* | ||
* <p> | ||
* The configuration object is a shared singleton object within the applicaiton, | ||
* attained by calling require('config'). | ||
* </p> | ||
* | ||
* <p> | ||
* Usually you'll specify a CONFIG variable at the top of your .js file | ||
* for file/module scope. If you want the root of the object, you can do this: | ||
* </p> | ||
* <pre> | ||
* var CONFIG = require('config'); | ||
* </pre> | ||
* | ||
* <p> | ||
* Sometimes you only care about a specific sub-object within the CONFIG | ||
* object. In that case you could do this at the top of your file: | ||
* </p> | ||
* <pre> | ||
* var CONFIG = require('config').customer; | ||
* or | ||
* var CUSTOMER_CONFIG = require('config').customer; | ||
* </pre> | ||
* | ||
* <script type="text/javascript"> | ||
* document.getElementById("showProtected").style.display = "block"; | ||
* </script> | ||
* | ||
* @method constructor | ||
* @return CONFIG {object} - The top level configuration object | ||
*/ | ||
var Config = function() { | ||
var t = this; | ||
t._loadFileConfigs(); | ||
t._watchForConfigFileChanges(); | ||
t._persistConfigsOnChange(); | ||
}; | ||
/** | ||
* <p>Monitor a configuration value for runtime changes.</p> | ||
* | ||
* <p> | ||
* Configuration values can be changed at runtime by the application or by a | ||
* manual change to the config/runtime.json file. | ||
* This method lets you specify a function to run when a configuration | ||
* value changes. | ||
* </p> | ||
* | ||
* <p> | ||
* This was built for monitoring changes to configuration values, | ||
* but it can be used for watching changes to any javascript object. | ||
* </p> | ||
* | ||
* @method watch | ||
* @param object {object} - The object to watch. | ||
* @param property {string} - The property name to watch. Watch all object properties if null. | ||
* @param handler {function(object, propertyName, priorValue, newValue)} - Handler called when a property change is detected. | ||
* The handler is run along with other handlers registered for notification. | ||
* If your handler changes the value of the property, that change is applied after all handlers have finished processing the current change. | ||
* Then all handlers (including this one) will be called again with the newly changed value. | ||
* @param depth {integer} (optional) - If watching all object properties or if the specified property is an object, this specifies the depth of the object graph to watch for changes. Default 6. | ||
* @return object {object} - The original object is returned - for chaining. | ||
*/ | ||
Config.prototype.watch = function(object, property, handler, depth) { | ||
// Initialize | ||
var mixedConfig =_.cloneDeep(origConfig || {}); | ||
var t = object; | ||
var allProperties = property ? [property] : Object.keys(t); | ||
// If no origConfig is given, return an existing config or all configs | ||
if (origConfig == null) { | ||
return (modName ? savedConfigs[modName] : savedConfigs); | ||
// Depth detection | ||
depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth); | ||
if (depth < 0) { | ||
return; | ||
} | ||
// Mixin configuration files | ||
var argName = '-config'; | ||
_.each(process.argv, function(arg, pos){ | ||
if (argName == arg && ++pos < process.argv.length) { | ||
// Create hidden properties on the object | ||
function makeHiddenProperty(hiddenProp, initialValue) { | ||
Object.defineProperty(t, hiddenProp, { | ||
value: initialValue, | ||
writable : true, | ||
enumerable : false, | ||
configurable : false | ||
}); | ||
} | ||
if (!t.__watchers) | ||
makeHiddenProperty('__watchers', {}); | ||
if (!t.__propertyValues) | ||
makeHiddenProperty('__propertyValues', {}); | ||
// If the filename is relative, make it relative to the process cwd | ||
var configFile = process.argv[pos]; | ||
if (configFile.indexOf('./') == 0 || configFile.indexOf('../') == 0) { | ||
configFile = process.cwd() + '/' + configFile; | ||
} | ||
// Attach watchers to all requested properties | ||
allProperties.forEach(function(prop){ | ||
// Get the configuration object from the file | ||
var configObject = savedConfigFiles[configFile]; | ||
if (!configObject) { | ||
// Setup the property for watching (first time only) | ||
if (typeof(t.__propertyValues[prop]) == 'undefined') { | ||
// Determine the file type | ||
var fileParts = configFile.split('.'); | ||
lastPart = fileParts[fileParts.length - 1].toLowerCase(); | ||
var isYAML = (lastPart == 'yaml'); | ||
var isJSON = (lastPart == 'json'); | ||
var isJS = (!isYAML && !isJSON); | ||
// Don't error re-defining the property if immutable | ||
var descriptor = Object.getOwnPropertyDescriptor(t,prop); | ||
if (descriptor && descriptor.writable === false) | ||
return; | ||
// Load and parse the file into a javascript object | ||
try { | ||
if (isYAML) { | ||
var text = File.readFileSync(configFile).toString(); | ||
// Yaml library doesn't like strings that have newlines but don't end in | ||
// a newline: https://github.com/visionmedia/js-yaml/issues/issue/13 | ||
text += '\n'; | ||
configObject = Yaml.eval(text); | ||
} | ||
else if (isJSON) { | ||
var text = File.readFileSync(configFile).toString(); | ||
configObject = JSON.parse(text); | ||
} | ||
else { | ||
configObject = require(configFile); | ||
} | ||
// Remember the object so the file doesn't have to be parsed | ||
// again for the next module. | ||
savedConfigFiles[configFile] = configObject; | ||
// Copy the value to the hidden field, and add the property to watchers | ||
t.__propertyValues[prop] = [t[prop]]; | ||
t.__watchers[prop] = []; | ||
// Attach the property watcher | ||
Object.defineProperty(t, prop, { | ||
enumerable : true, | ||
get : function(){ | ||
// If more than 1 item is in the values array, | ||
// then we're currently processing watchers. | ||
if (t.__propertyValues[prop].length == 1) | ||
// Current value | ||
return t.__propertyValues[prop][0]; | ||
else | ||
// [0] is prior value, [1] is new value being processed | ||
return t.__propertyValues[prop][1]; | ||
}, | ||
set : function(newValue) { | ||
// Return early if no change | ||
var origValue = t[prop]; | ||
if (origValue === newValue) | ||
return; | ||
// Remember the new value, and return if we're in another setter | ||
t.__propertyValues[prop].push(newValue); | ||
if (t.__propertyValues[prop].length > 2) | ||
return; | ||
// Call all watchers for each change requested | ||
var numIterations = 0; | ||
while (t.__propertyValues[prop].length > 1) { | ||
// Detect recursion | ||
if (++numIterations > 20) { | ||
t.__propertyValues[prop] = [origValue]; | ||
throw new Error('Recursion detected while setting [' + prop + ']'); | ||
} | ||
// Call each watcher for the current values | ||
var oldValue = t.__propertyValues[prop][0]; | ||
newValue = t.__propertyValues[prop][1]; | ||
t.__watchers[prop].forEach(function(watcher) { | ||
try { | ||
watcher(t, prop, oldValue, newValue); | ||
} catch (e) { | ||
// Log an error and continue with subsequent watchers | ||
console.error("Exception in watcher for " + prop); | ||
} | ||
}); | ||
// Done processing this value | ||
t.__propertyValues[prop].splice(0,1); | ||
} | ||
} | ||
catch (e) | ||
{ | ||
console.log("\nError parsing config file: " + configFile); | ||
console.log(e.message); | ||
process.exit(1); | ||
} | ||
} | ||
}); | ||
// Mixin the module from the configuration file object | ||
_.extendDeep(mixedConfig, configObject[modName]); | ||
} // Done setting up the property for watching (first time) | ||
// Add the watcher to the property | ||
t.__watchers[prop].push(handler); | ||
// Recurs if this is an object... | ||
if (typeof(t[prop]) == 'object') { | ||
Config.prototype.watch(t[prop], null, handler, depth - 1); | ||
} | ||
}); // Done processing each property | ||
// Return the original object - for chaining | ||
return t; | ||
}; | ||
/** | ||
* <p> | ||
* Set default configurations for a node.js module. | ||
* </p> | ||
* | ||
* <p> | ||
* This allows module developers to attach their configurations onto the | ||
* default configuration object so they can be configured by the consumers | ||
* of the module. | ||
* </p> | ||
* | ||
* <p>Using the function within your module:</p> | ||
* <pre> | ||
* var CONFIG = require("config"); | ||
* CONFIG.setModuleDefaults("MyModule", { | ||
* templateName: "t-50", | ||
* colorScheme: "green" | ||
* }); | ||
* <br> | ||
* // Template name may be overridden by application config files | ||
* console.log("Template: " + CONFIG.MyModule.templateName); | ||
* </pre> | ||
* | ||
* <p> | ||
* The above example results in a "MyModule" element of the configuration | ||
* object, containing an object with the specified default values. | ||
* </p> | ||
* | ||
* @method setModuleDefaults | ||
* @param moduleName {string} - Name of your module. | ||
* @param defaultProperties {object} - The default module configuration. | ||
* @return moduleConfig {object} - The module level configuration object. | ||
*/ | ||
Config.prototype.setModuleDefaults = function(moduleName, defaultProperties) { | ||
// Copy the properties into a new object | ||
var t = this; | ||
var moduleConfig = t._extendDeep({}, defaultProperties); | ||
// Attach handlers & watchers onto the module config object | ||
t._attachProtoDeep(moduleConfig); | ||
t._persistConfigsOnChange(moduleConfig); | ||
// Extend the module config object with values from originalConfig | ||
if (originalConfig[moduleName]) { | ||
t._extendDeep(moduleConfig, originalConfig[moduleName]); | ||
// Save the mixed module config as the original | ||
originalConfig[moduleName] = moduleConfig; | ||
} | ||
// Extend the module config object with values from runtimeJson | ||
if (runtimeJson[moduleName]) { | ||
t._extendDeep(moduleConfig, runtimeJson[moduleName]); | ||
} | ||
// Attach the object onto the CONFIG object | ||
t[moduleName] = moduleConfig; | ||
}; | ||
/** | ||
* <p>Make a configuration property immutable (assuring it cannot be changed | ||
* from the current value).</p> | ||
* | ||
* <p> | ||
* This method was built for disabling runtime changes to configuration values, | ||
* but it can be applied to any javascript object. | ||
* </p> | ||
* | ||
* <p> | ||
* This operation cannot be un-done. | ||
* </p> | ||
* | ||
* @method makeImmutable | ||
* @param object {object} - The object to attach an immutable property into. | ||
* @param property {string} - The name of the property to make immutable. | ||
* @return object {object} - The original object is returned - for chaining. | ||
*/ | ||
Config.prototype.makeImmutable = function(object, property) { | ||
// Disable writing, and make sure the property cannot be re-configured. | ||
Object.defineProperty(object, property, { | ||
value : object[property], | ||
writable : false, | ||
configurable: false | ||
}); | ||
// Mixin configurations from the command line | ||
// -ModName.FldName FldValue | ||
var modDot = '-' + modName + '.'; | ||
_.each(process.argv, function(arg, pos){ | ||
return object; | ||
}; | ||
// Process if this is in the right format | ||
if (arg.indexOf(modDot) == 0 && ++pos < process.argv.length) { | ||
// Get the field name & value | ||
var fldName = arg.substr(modDot.length); | ||
var fldValue = process.argv[pos]; | ||
// Quote the field value unless it's complex | ||
if (fldValue[0] != '{' && fldValue[0] != '[') { | ||
fldValue = "'" + fldValue.replace(/'/,"\\'") + "'"; | ||
/** | ||
* Monitor the filesystem for configuration file changes. | ||
* | ||
* <p> | ||
* Runtime configuration changes are made by modifying the runtime.json file. | ||
* This paradigm allows multiple application servers to internally notify | ||
* listeners whenever the configuration changes. | ||
* </p> | ||
* | ||
* <p> | ||
* This method attaches the file watcher onto the runtime.json file, reloading | ||
* the file on change and merging the new values into the CONFIG object. | ||
* </p> | ||
* | ||
* @protected | ||
* @method _watchForConfigFileChanges | ||
*/ | ||
Config.prototype._watchForConfigFileChanges = function() { | ||
// Attach the file watcher | ||
var t = this; | ||
FileSystem.watchFile(runtimeJsonFilename, function(curr, prev) { | ||
// no-op if the file hasn't changed | ||
if (curr.mtime == prev.mtime) { | ||
return; | ||
} | ||
// Load the runtime.json file asynchronously. | ||
FileSystem.readFile(runtimeJsonFilename, 'UTF-8', function(err, fileContent) { | ||
// Not much to do on error | ||
if (err) { | ||
console.error("Error loading " + runtimeJsonFilename); | ||
return; | ||
} | ||
// Variable names & values can be complex, so execute them. | ||
eval('mixedConfig.' + fldName + '=' + fldValue); | ||
// Parse the file and mix it in to this config object. | ||
// This notifies listeners | ||
try { | ||
var configObject = JSON.parse(fileContent); | ||
t._extendDeep(t, configObject); | ||
} catch (e) { | ||
console.error("Error parsing " + runtimeJsonFilename, e); | ||
return; | ||
} | ||
}); | ||
}); | ||
}; | ||
/** | ||
* <p> | ||
* Watch the specified object for a change in properties, and persist changes | ||
* to runtime.json when a change is detected. | ||
* </p> | ||
* | ||
* @protected | ||
* @param object {object} - The config object to watch | ||
* @method _persistConfigsOnChange | ||
*/ | ||
Config.prototype._persistConfigsOnChange = function(objectToWatch) { | ||
// Watch for configuration value changes | ||
var t = this; | ||
objectToWatch = objectToWatch || t; | ||
t.watch(objectToWatch, null, function(){ | ||
// Return early if we're already queued up for persisting | ||
if (isQueuedForPersistence) | ||
return; | ||
// Defer persisting until the next tick. This results in a single | ||
// persist across any number of config changes in a single event cycle. | ||
isQueuedForPersistence = true; | ||
process.nextTick(function(){ | ||
// Persist if necessary | ||
var newDiffs = t._diffDeep(originalConfig, t); | ||
if (!t._equalsDeep(newDiffs, runtimeJson)) { | ||
FileSystem.writeFile(runtimeJsonFilename, JSON.stringify(newDiffs, null, 2), 'utf-8', function(error){ | ||
if (error) | ||
console.error("Error writing " + runtimeJsonFilename, error); | ||
}); | ||
}; | ||
// Set up for next time | ||
isQueuedForPersistence = false; | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Load the individual file configurations. | ||
* | ||
* <p> | ||
* This method builds a map of filename to the configuration object defined | ||
* by the file. The search order is: | ||
* </p> | ||
* | ||
* <pre> | ||
* default.EXT | ||
* (hostname).EXT | ||
* (deployment).EXT | ||
* (hostname)-(deployment).EXT | ||
* runtime.json | ||
* </pre> | ||
* | ||
* <p> | ||
* EXT can be yaml, json, or js signifying the file type. yaml is in YAML format, | ||
* json is in strict JSON format, and js is a javascript executable file that is | ||
* require()'d with module.exports being the config object. | ||
* </p> | ||
* | ||
* <p> | ||
* (hostname) is the $HOST environment variable if set, otherwise the | ||
* hostname found from require('os').hostname() | ||
* </p> | ||
* | ||
* <p> | ||
* (deployment) is the deployment type, found in the $NODE_ENV environment | ||
* variable. Defaults to 'development'. | ||
* </p> | ||
* | ||
* <p> | ||
* The runtime.json file contains configuration changes made at runtime either | ||
* manually, or by the application setting a configuration value. | ||
* </p> | ||
* | ||
* @protected | ||
* @method _loadFileConfigs | ||
* @return {this} The configuration object | ||
*/ | ||
Config.prototype._loadFileConfigs = function() { | ||
// Initialize | ||
var t = this; | ||
// Singleton | ||
if (originalConfig) | ||
return t; | ||
// Determine the host name from the OS module, $HOST, or $HOSTNAME | ||
// Remove any . appendages, and default to null if not set | ||
try { | ||
var OS = require('os'); | ||
var hostName = OS.hostname(); | ||
} catch (e) { | ||
hostName = process.env.HOST || process.env.HOSTNAME; | ||
} | ||
hostName = hostName ? hostName.split('.')[0] : null; | ||
// Get the deployment type from NODE_ENV | ||
var deployment = process.env.NODE_ENV || 'development'; | ||
// Read each file in turn | ||
var baseNames = ['default', hostName, deployment, hostName + '-' + deployment]; | ||
var extNames = ['js', 'json', 'yaml']; | ||
baseNames.forEach(function(baseName) { | ||
extNames.forEach(function(extName) { | ||
// Try merging the config object into this object | ||
var fullFilename = CONFIG_PATH + baseName + '.' + extName; | ||
var configObj = t._parseFile(fullFilename); | ||
if (configObj) { | ||
t._extendDeep(t, configObj); | ||
} | ||
}); | ||
}); | ||
// Remember the original configuration | ||
originalConfig = t._cloneDeep(t); | ||
// Extend the original config with any prior runtime.json diffs | ||
runtimeJson = t._parseFile(runtimeJsonFilename); | ||
if (runtimeJson) { | ||
t._extendDeep(t, runtimeJson); | ||
} | ||
// Attach the config.prototype to all sub-objects. | ||
t._attachProtoDeep(t); | ||
// Return the configuration object | ||
return t; | ||
}; | ||
/** | ||
* Parse and return the specified configuration file. | ||
* | ||
* If the file exists in the application config directory, it will | ||
* parse and return it as a JavaScript object. | ||
* | ||
* The file extension determines the parser to use. | ||
* | ||
* .js = File to run that has a module.exports containing the config object | ||
* .json = File is parsed using JSON.parse() | ||
* .yaml = Parsed with a YAML parser | ||
* | ||
* If the file doesn't exist, a null will be returned. | ||
* | ||
* If the file can't be parsed, an exception will be thrown. | ||
* | ||
* @protected | ||
* @method _parseFile | ||
* @param fullFilename {string} The full file path and name | ||
* @return {configObject} The configuration object parsed from the file | ||
*/ | ||
Config.prototype._parseFile = function(fullFilename) { | ||
// Initialize | ||
var extension = fullFilename.substr(fullFilename.lastIndexOf('.') + 1), | ||
configObject = null, | ||
fileContent = null; | ||
// Return null if the file doesn't exist. | ||
// Note that all methods here are the Sync versions. This allows the | ||
// config package to follow the same calling semantics as require('filename') | ||
// which is also synchronous. | ||
try { | ||
var stat = FileSystem.statSync(fullFilename); | ||
if (!stat || stat.size < 1) { | ||
return null; | ||
} | ||
} catch (e1) { | ||
return null; | ||
} | ||
// Try loading the file. | ||
try { | ||
fileContent = FileSystem.readFileSync(fullFilename, 'UTF-8'); | ||
} | ||
catch (e2) { | ||
throw new Error('Config file ' + fullFilename + ' cannot be read'); | ||
} | ||
try { | ||
if (extension == 'yaml') { | ||
// The yaml library doesn't like strings that have newlines but don't | ||
// end in a newline: https://github.com/visionmedia/js-yaml/issues/issue/13 | ||
fileContent += '\n'; | ||
configObject = Yaml.eval(fileContent); | ||
} | ||
else if (extension == 'json') { | ||
configObject = JSON.parse(fileContent); | ||
} | ||
else if (extension == 'js') { | ||
configObject = require(fullFilename); | ||
} | ||
} | ||
catch (e3) { | ||
throw new Error("Cannot parse config file: '" + fullFilename + "': " + e3); | ||
} | ||
return configObject; | ||
}; | ||
/** | ||
* Attach the Config class prototype to all config objects recursively. | ||
* | ||
* <p> | ||
* This allows you to do anything with CONFIG sub-objects as you can do with | ||
* the top-level CONFIG object. It's so you can do this: | ||
* </p> | ||
* | ||
* <pre> | ||
* var CUST_CONFIG = require('config').Customer; | ||
* CUST_CONFIG.watch(...) | ||
* </pre> | ||
* | ||
* @protected | ||
* @method _attachProtoDeep | ||
* @param toObject | ||
* @param depth | ||
* @return toObject | ||
*/ | ||
Config.prototype._attachProtoDeep = function(toObject, depth) { | ||
// Recursion detection | ||
var t = this; | ||
depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth); | ||
if (depth < 0) { | ||
return; | ||
} | ||
// Attach the prototype to this object | ||
toObject.__proto__ = Config.prototype; | ||
// Cycle through each element | ||
for (var prop in toObject) { | ||
// Call recursively if an object | ||
if (t._isObject(toObject[prop])) { | ||
t._attachProtoDeep(toObject[prop], depth - 1); | ||
} | ||
} | ||
// Return the original object | ||
return toObject; | ||
}; | ||
/** | ||
* Return a deep copy of the specified object. | ||
* | ||
* This returns a new object with all elements copied from the specified | ||
* object. Deep copies are made of objects and arrays so you can do anything | ||
* with the returned object without affecting the input object. | ||
* | ||
* @protected | ||
* @method _cloneDeep | ||
* @param copyFrom {object} The original object to copy from | ||
* @param depth {integer} An optional depth to prevent recursion. Default: 20. | ||
* @return {object} A new object with the elements copied from the copyFrom object | ||
*/ | ||
Config.prototype._cloneDeep = function(obj, depth) { | ||
// Recursion detection | ||
var t = this; | ||
depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth); | ||
if (depth < 0) { | ||
return {}; | ||
} | ||
// Create the copy of the correct type | ||
var copy = Array.isArray(obj) ? [] : {}; | ||
// Cycle through each element | ||
for (var prop in obj) { | ||
// Call recursively if an object or array | ||
if (Array.isArray(obj[prop]) || t._isObject(obj[prop])) { | ||
copy[prop] = t._cloneDeep(obj[prop], depth - 1); | ||
} | ||
else { | ||
copy[prop] = obj[prop]; | ||
} | ||
} | ||
// Return the copied object | ||
return copy; | ||
}; | ||
/** | ||
* Return true if two objects have equal contents. | ||
* | ||
* @protected | ||
* @method _equalsDeep | ||
* @param object1 {object} The object to compare from | ||
* @param object2 {object} The object to compare with | ||
* @param depth {integer} An optional depth to prevent recursion. Default: 20. | ||
* @return {boolean} True if both objects have equivalent contents | ||
*/ | ||
Config.prototype._equalsDeep = function(object1, object2, depth) { | ||
// Recursion detection | ||
var t = this; | ||
depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth); | ||
if (depth < 0) { | ||
return {}; | ||
} | ||
// Fast comparisons | ||
if (!object1 || !object2) { | ||
return false; | ||
} | ||
if (object1 === object2) { | ||
return true; | ||
} | ||
if (typeof(object1) != 'object' || typeof(object2) != 'object') { | ||
return false; | ||
} | ||
// They must have the same keys. If their length isn't the same | ||
// then they're not equal. If the keys aren't the same, the value | ||
// comparisons will fail. | ||
if (Object.keys(object1).length != Object.keys(object2).length) { | ||
return false; | ||
} | ||
// Compare the values | ||
for (var prop in object1) { | ||
// Call recursively if an object or array | ||
if (typeof(object1[prop] == 'object')) { | ||
if (!t._equalsDeep(object1[prop], object2[prop], depth - 1)) { | ||
return false; | ||
} | ||
} | ||
else { | ||
if (object1[prop] !== object2[prop]) { | ||
return false; | ||
} | ||
} | ||
} | ||
// Test passed. | ||
return true; | ||
}; | ||
/** | ||
* Returns an object containing all elements that differ between two objects. | ||
* <p> | ||
* This method was designed to be used to create the runtime.json file | ||
* contents, but can be used to get the diffs between any two Javascript objects. | ||
* </p> | ||
* <p> | ||
* It works best when object2 originated by deep copying object1, then | ||
* changes were made to object2, and you want an object that would give you | ||
* the changes made to object1 which resulted in object2. | ||
* </p> | ||
* | ||
* @protected | ||
* @method _diffDeep | ||
* @param object1 {object} The base object to compare to | ||
* @param object2 {object} The object to compare with | ||
* @param depth {integer} An optional depth to prevent recursion. Default: 20. | ||
* @return {object} A differential object, which if extended onto object1 would | ||
* result in object2. | ||
*/ | ||
Config.prototype._diffDeep = function(object1, object2, depth) { | ||
// Recursion detection | ||
var t = this, diff = {}; | ||
depth = (depth === null ? DEFAULT_CLONE_DEPTH : depth); | ||
if (depth < 0) { | ||
return {}; | ||
} | ||
// Process each element from object2, adding any element that's different | ||
// from object 1. | ||
for (var parm in object2) { | ||
var value1 = object1[parm]; | ||
var value2 = object2[parm]; | ||
if (typeof(value2) == 'object') { | ||
if (!(t._equalsDeep(value1, value2))) { | ||
diff[parm] = t._diffDeep(value1, value2, depth - 1); | ||
} | ||
} | ||
else if (value1 !== value2){ | ||
diff[parm] = value2; | ||
} | ||
} | ||
// Return the diff object | ||
return diff; | ||
}; | ||
/** | ||
* Extend an object, and any object it contains. | ||
* | ||
* This does not replace deep objects, but dives into them | ||
* replacing individual elements instead. | ||
* | ||
* @protected | ||
* @method _extendDeep | ||
* @param mergeInto {object} The object to merge into | ||
* @param mergeFrom... {object...} - Any number of objects to merge from | ||
* @param depth {integer} An optional depth to prevent recursion. Default: 20. | ||
* @return {object} The altered mergeInto object is returned | ||
*/ | ||
Config.prototype._extendDeep = function(mergeInto) { | ||
// Initialize | ||
var t = this; | ||
var vargs = Array.prototype.slice.call(arguments, 1); | ||
var depth = vargs.pop(); | ||
if (typeof(depth) != 'number') { | ||
vargs.push(depth); | ||
depth = DEFAULT_CLONE_DEPTH; | ||
} | ||
// Recursion detection | ||
if (depth < 0) { | ||
return mergeInto; | ||
} | ||
// Cycle through each object to extend | ||
vargs.forEach(function(mergeFrom) { | ||
// Cycle through each element of the object to merge from | ||
for (var prop in mergeFrom) { | ||
// Extend recursively if both elements are objects | ||
if (t._isObject(mergeInto[prop]) && t._isObject(mergeFrom[prop])) { | ||
t._extendDeep(mergeInto[prop], mergeFrom[prop], depth - 1); | ||
} | ||
// Copy recursively if the mergeFrom element is an object (or array or fn) | ||
else if (mergeFrom[prop] && typeof mergeFrom[prop] == 'object') { | ||
mergeInto[prop] = t._cloneDeep(mergeFrom[prop], depth - 1); | ||
} | ||
// Simple assignment otherwise | ||
else { | ||
mergeInto[prop] = mergeFrom[prop]; | ||
} | ||
} | ||
}); | ||
// Remember the configuration, and return it | ||
return savedConfigs[modName] = mixedConfig; | ||
// Chain | ||
return mergeInto; | ||
}; | ||
/** | ||
* Is the specified argument a regular javascript object? | ||
* | ||
* The argument is an object if it's a JS object, but not an array. | ||
* | ||
* @protected | ||
* @method _isObject | ||
* @param arg {MIXED} An argument of any type. | ||
* @return {boolean} TRUE if the arg is an object, FALSE if not | ||
*/ | ||
Config.prototype._isObject = function(obj) { | ||
if (Array.isArray(obj)) { | ||
return false; | ||
} | ||
return typeof obj == 'object'; | ||
}; | ||
// The module exports a singleton instance of the Config class so the | ||
// instance is immediately available on require(), and the prototype methods | ||
// aren't a part of the object namespace when inspected. | ||
module.exports = new Config(); |
{ | ||
"name": "config", | ||
"version": "0.2.9", | ||
"version": "0.4.0", | ||
"main": "./lib/config.js", | ||
"description": "Runtime configuration for node.js deployment", | ||
"description": "Configuration control for production node deployments", | ||
"modules" : { "test" : "./test" }, | ||
@@ -10,11 +10,9 @@ "author": "Loren West <open_source@lorenwest.com>", | ||
"dependencies": { | ||
"underscore" : "1.1.4", | ||
"vows" : ">=0.5.3", | ||
"eyes" : ">=0.1.6", | ||
"yaml" : ">=0.1.1" | ||
"yaml" : "0.1.x", | ||
"vows" : "0.5.x" | ||
}, | ||
"engines": {"node": ">=0.2.4"}, | ||
"engines": {"node": ">0.4.x"}, | ||
"scripts": { | ||
"test" : "vows test/*.js --spec" | ||
"test" : "./node_modules/vows/bin/vows test/*.js --spec" | ||
} | ||
} |
175
README.md
node-config | ||
=========== | ||
Runtime configuration for node.js deployment | ||
Configuration control for production node deployments | ||
@@ -9,154 +9,62 @@ Introduction | ||
node-config lets you apply a consistent pattern to module development | ||
making it easy to work with deployment configuration parameters. | ||
Node-config is a configuration system for Node.js application server | ||
deployments. It lets you define a default set of application parameters, | ||
and tune them for different runtime environments (development, qa, | ||
staging, production, etc.). | ||
As a module developer, node-config lets you define your parameters, | ||
then gets out of your way. | ||
Parameters defined by node-config can be monitored and tuned at runtime | ||
without bouncing your production servers. | ||
When running applications following this pattern, node-config lets you | ||
easily discover and set deployment-specific configuration parameters. | ||
Online documentation is available at <http://lorenwest.github.com/node-config/latest> | ||
Synopsis | ||
-------- | ||
Quick Start | ||
----------- | ||
Configurations are defined at the top of your module. The following example | ||
is for a *Customers* module: | ||
**In your project directory, install and verify using npm:** | ||
// Configuration parameters and default values | ||
var config = require('config')('Customers', { | ||
dbHost: 'localhost', | ||
dbPort: 5984, | ||
dbName: 'customers', | ||
syncFrequency: 60 // Minutes between synchronizations | ||
}); | ||
my-project$ npm install config | ||
my-project$ npm test config | ||
This gives you a *config* variable for your module, along with default values | ||
to use during development. | ||
**Edit the default configuration file (.js, .json, or .yaml):** | ||
Use the *config* object anywhere in your module: | ||
my-project$ mkdir config | ||
my-project$ vi config/default.yaml | ||
// Connect to the database | ||
var dbConn = db.connect(config.dbHost, config.dbPort); | ||
(example default.yaml file): | ||
When running the application, you can specify module configuration overrides | ||
on the command line, or in a configuration file. | ||
Customer: | ||
dbHost: localhost | ||
dbPort: 5984 | ||
dbName: customers | ||
For example, the above *dbHost* value of the *Customers* module can be | ||
overridden on the command line, or in a config file. | ||
**Edit the production configuration file:** | ||
Parameters specified on the command line: | ||
$ node billing.js -Customers.dbHost smokin-db | ||
my-project$ vi config/production.yaml | ||
Parameters specified in a configuration file: | ||
$ node billing.js -config ./smokeTest.js | ||
(example production.yaml file): | ||
Any number of command line parameters and/or config files can be specified when | ||
running the application. | ||
Customer: | ||
dbHost: prod-db-server | ||
Installation & Testing | ||
---------------------- | ||
**Use the configuration in your code:** | ||
Node-config installs with *npm* and comes with an extensive suite of tests to | ||
make sure it performs well in your deployment environment. | ||
var CONFIG = require('config').Customer; | ||
... | ||
db.connect(CONFIG.dbHost, CONFIG.dbPort, CONFIG.dbName); | ||
To install and test node-config: | ||
$ npm install config | ||
$ npm test config | ||
**Start your application server:** | ||
my-project$ export NODE_ENV=production | ||
my-project$ node app.js | ||
How It Works | ||
------------ | ||
Running in this configuration, CONFIG.dbPort and CONFIG.dbName | ||
will come from the `default.yaml` file, and CONFIG.dbHost will | ||
come from the `production.yaml` file. | ||
When developing a module, use the following pattern for defining parameters | ||
and default values: | ||
// Customers.js - Customer management utilities | ||
// Configuration parameters and default values | ||
var config = require('config')('Customers', { | ||
dbHost: 'localhost', | ||
dbPort: 5984, | ||
dbName: 'customers', | ||
syncFrequency: 60 // Minutes between synchronizations | ||
}); | ||
When the application runs, node-config extends these default values with | ||
overrides from configuration files, followed by command line overrides, | ||
in the order they're specified on the command line. | ||
Configuration Files | ||
------------------- | ||
Configuration files let you define application deployment configurations in | ||
one place. | ||
A configuration file is a JavaScript module that exports a single configuration | ||
object containing the modules, parameters, and values you want to override for | ||
your application. | ||
The format of the object is best described by example *smokeTest.js*: | ||
// Configuration overrides for smoke testing | ||
module.exports = { | ||
'mod-sync': { | ||
remoteHost: 'smokin-sync', | ||
remotePort: 5984 | ||
}, | ||
Customers: { | ||
dbHost: 'smokin-db', | ||
syncFrequency: 1 | ||
} | ||
}; | ||
When running the app, you can specify this config file and additional | ||
parameters at the same time: | ||
$ node billing.js -config smokeTest.js -Customers.dbPort 5985 | ||
This results in a *config* object in the *Customers* module with these values: | ||
{ | ||
dbHost: 'smokin-db', | ||
dbPort: 5985, | ||
dbName: 'customers', | ||
syncFrequency: 1 | ||
} | ||
Advanced Usage | ||
-------------- | ||
**Programmatic configuration** If you'd rather specify configuration files | ||
and parameter overrides in your application, just add the command line | ||
arguments programatically before the module is loaded. | ||
// Always use port 5994 for the Customers service | ||
process.argv.push('-Customers.dbPort', 5994); | ||
require('Customers'); | ||
**Configuration discovery** You can retrieve the current configuration for | ||
any module by omitting the second parameter, or all configurations by | ||
omitting all parameters to config. | ||
// Load the Customer module parameters | ||
var custConfig = require('config')('Customers'); | ||
// Load all application parameters | ||
var allConfigs = require('config')(); | ||
**Complex command line variables** Variable names and values passed on through | ||
the command line can be strings, objects, or arrays. | ||
$ node billing.js -Customers.custTemplate.region North | ||
$ node billing.js -Customers.mailings[1] WelcomeMailing | ||
$ node billing.js -Customers.mailingsSent [3,4,6] | ||
$ node billing.js -Customers.homeOffice '{street:"4778 S. Main"}' | ||
See Also | ||
-------- | ||
The node-monitor project is a good example of a module that makes use of | ||
the node-config module. It's also a pretty good way to monitor your running | ||
node.js application. | ||
[node-config] - Online documentation<br> | ||
[node-monitor] - Monitor your running node applications | ||
@@ -170,2 +78,5 @@ License | ||
Copyright (c) 2010 Loren West | ||
Copyright (c) 2011 Loren West | ||
[node-config]: http://lorenwest.github.com/node-config/latest | ||
[node-monitor]: http://lorenwest.github.com/node-monitor/latest |
@@ -1,198 +0,172 @@ | ||
/******************************************************************************* | ||
* config-test.js - Test for the node-config library | ||
******************************************************************************** | ||
*/ | ||
// Hardcode $NODE_ENV=test for testing | ||
process.env.NODE_ENV='test'; | ||
// Dependencies | ||
var deps = require('../deps'); | ||
var _ = deps._; | ||
var vows = deps.vows; | ||
var assert = deps.assert; | ||
var config = require('../lib/config'); | ||
var vows = require('vows'); | ||
assert = require('assert'), | ||
CONFIG = require('../lib/config'), | ||
FileSystem = require('fs'), | ||
originalWatchedValue = CONFIG.watchThisValue, | ||
newWatchedValue = Math.floor(Math.random() * 100000); | ||
// Module default parameters | ||
var defaultParms = { | ||
dbHost: 'localhost', | ||
dbPort: 5984, | ||
dbName: 'customers', | ||
custTemplate: { | ||
credit: 200, | ||
region: 'Central', | ||
mailings: ['intro', 'month1'] | ||
} | ||
}; | ||
// These tests require the directory to be the root of the node-config project | ||
process.chdir(__dirname + '/..'); | ||
var CONFIG_PATH = process.cwd() + '/config/', | ||
runtimeJsonFilename = CONFIG_PATH + 'runtime.json'; | ||
/******************************************************************************* | ||
* ConfigTest | ||
******************************************************************************** | ||
*/ | ||
/** | ||
* <p>Unit tests for the node-config library. To run type:</p> | ||
* <pre>npm test config</pre> | ||
* | ||
* @class ConfigTest | ||
*/ | ||
exports.ConfigTest = vows.describe('Test suite for node-config').addBatch({ | ||
'Library initialization': { | ||
'Config library is available': function() { | ||
assert.isFunction(config); | ||
assert.isObject(CONFIG); | ||
}, | ||
'Config utils are included with the library': function() { | ||
// Normal underscore + config extensions | ||
assert.isFunction(_); | ||
assert.isFunction(_.cloneDeep); | ||
'Config extensions are included with the library': function() { | ||
assert.isFunction(CONFIG._cloneDeep); | ||
} | ||
}, | ||
'Configuration Tests': { | ||
'Configuration file Tests': { | ||
topic: function() { | ||
// Remember the original command line argument values | ||
orig = {argv:process.argv}; | ||
return orig; | ||
return CONFIG; | ||
}, | ||
'Default configuration is correct': function() { | ||
process.argv = []; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = { | ||
dbHost: 'localhost', | ||
dbPort: 5984, | ||
dbName: 'customers', | ||
custTemplate: { | ||
credit: 200, | ||
region: 'Central', | ||
mailings: ['intro', 'month1'] | ||
} | ||
}; | ||
assert.deepEqual(conf, shouldBe); | ||
'Loading configurations from a JS module is correct': function() { | ||
assert.equal(CONFIG.Customers.dbHost, 'base'); | ||
assert.equal(CONFIG.TestModule.parm1, 'value1'); | ||
}, | ||
'Alpha configuration was mixed in': function() { | ||
process.argv = ['arg1', '-config', './config/alpha.js']; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.extendDeep({}, defaultParms, { | ||
dbHost:"alpha", | ||
dbPort:5999 | ||
}); | ||
assert.deepEqual(conf, shouldBe); | ||
'Loading configurations from a JSON file is correct': function() { | ||
assert.equal(CONFIG.AnotherModule.parm1, 'value1'); | ||
}, | ||
'Multiple configurations can be mixed in': function() { | ||
process.argv = ['-config', './config/base.js', 'arg1', | ||
'-config', './config/alpha.js', 'arg2']; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.extendDeep({}, defaultParms, { | ||
dbName:'base_customers', | ||
dbHost:"alpha", | ||
dbPort:5999 | ||
}); | ||
assert.deepEqual(conf, shouldBe); | ||
'Loading configurations from a YAML file is correct': function() { | ||
assert.equal(CONFIG.AnotherModule.parm2, 'value2'); | ||
}, | ||
'Command line configurations can specified': function() { | ||
process.argv = ['-Customers.dbName', 'cmdLineName', | ||
"-Customers.newArg", "Beth's"]; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.extendDeep({}, defaultParms, { | ||
dbName:'cmdLineName', | ||
newArg:"Beth's" | ||
}); | ||
assert.deepEqual(conf, shouldBe); | ||
'Loading prior runtime.json configurations is correct': function() { | ||
assert.equal(CONFIG.Customers.dbName, 'override_from_runtime_json'); | ||
} | ||
}, | ||
'Assuring a configuration can be made immutable': { | ||
topic: function() { | ||
CONFIG.makeImmutable(CONFIG.TestModule, 'parm1'); | ||
CONFIG.TestModule.parm1 = "setToThis"; | ||
return CONFIG.TestModule.parm1; | ||
}, | ||
'Command line configurations override file configurations': function() { | ||
process.argv = ['-config', './config/base.js', 'arg1', | ||
'-Customers.dbName', 'cmdLineName']; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.extendDeep({}, defaultParms, { | ||
dbHost:'base', | ||
dbName:'cmdLineName' | ||
}); | ||
assert.deepEqual(conf, shouldBe); | ||
'The makeImmutable() method is available': function() { | ||
assert.isFunction(CONFIG.makeImmutable); | ||
}, | ||
'Configurations can be programmatically extended': function() { | ||
process.argv = ['arg1', '-config', './config/production']; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.extendDeep({}, defaultParms, { | ||
dbHost:'production', | ||
dbName:'base_customers', | ||
dbPort:4456, | ||
custTemplate:{region:"North"} | ||
}); | ||
assert.deepEqual(conf, shouldBe); | ||
'Correctly unable to change an immutable configuration': function(value) { | ||
assert.isTrue(value != "setToThis"); | ||
}, | ||
'Configurations can be programmatically extended': function() { | ||
process.argv = ['arg1', '-config', './config/production']; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.extendDeep({}, defaultParms, { | ||
dbHost:'production', | ||
dbName:'base_customers', | ||
dbPort:4456, | ||
custTemplate:{region:"North"} | ||
'Left the original value intact after attempting the change': function(value) { | ||
assert.equal(value, "value1"); | ||
} | ||
}, | ||
'Configuration for module developers': { | ||
topic: function() { | ||
// Set some parameters for the test module | ||
CONFIG.setModuleDefaults("TestModule", { | ||
parm1: 1000, parm2: 2000 | ||
}); | ||
assert.deepEqual(conf, shouldBe); | ||
return CONFIG; | ||
}, | ||
'Command line variable names can be complex': function() { | ||
process.argv = ['-Customers.custTemplate["region"]', 'Northeast']; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.extendDeep({}, defaultParms, { | ||
custTemplate:{region:"Northeast"} | ||
'The setModuleDefaults() method is available': function() { | ||
assert.isFunction(CONFIG.setModuleDefaults); | ||
}, | ||
'The module config is in the CONFIG object': function(config) { | ||
assert.isTrue(typeof(config.TestModule) != "undefined"); | ||
}, | ||
'Local configurations are mixed in': function(config) { | ||
assert.equal(config.TestModule.parm1, "value1"); | ||
}, | ||
'Defaults remain intact unless overridden': function(config) { | ||
assert.equal(config.TestModule.parm2, 2000); | ||
} | ||
}, | ||
'Change Notification Tests': { | ||
topic: function() { | ||
// Attach this topic as a watcher | ||
var cb = this.callback; | ||
CONFIG.watch(CONFIG, null, function(obj, prop, oldValue, newValue){ | ||
cb(null, {obj:obj, prop:prop, oldValue:oldValue, newValue:newValue}); | ||
}); | ||
assert.deepEqual(conf, shouldBe); | ||
// Write the new watched value out to the runtime.json file | ||
CONFIG.watchThisValue = newWatchedValue; | ||
}, | ||
'Command line values can be complex': function() { | ||
process.argv = ['-Customers.custTemplate["mailings"][1]', '{name:"intro",mailed:"Y"}']; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.cloneDeep(defaultParms); | ||
shouldBe.custTemplate.mailings[1] = {name:"intro", mailed:"Y"}; | ||
assert.deepEqual(conf, shouldBe); | ||
'The watch() method is available': function() { | ||
assert.isFunction(CONFIG.watch); | ||
}, | ||
'Specific configurations are discoverable': function() { | ||
// Read the current Customers parameters | ||
var conf = config('Customers'); | ||
var shouldBe = _.cloneDeep(defaultParms); | ||
shouldBe.custTemplate.mailings[1] = {name:"intro", mailed:"Y"}; | ||
assert.deepEqual(conf, shouldBe); | ||
'The change handler callback was fired': function(err, obj) { | ||
assert.isTrue(true); | ||
}, | ||
'All configurations are discoverable': function() { | ||
var allConfs = config(); | ||
assert.isObject(allConfs); | ||
assert.isObject(allConfs.Customers); | ||
var conf = allConfs.Customers; | ||
var shouldBe = _.cloneDeep(defaultParms); | ||
shouldBe.custTemplate.mailings[1] = {name:"intro", mailed:"Y"}; | ||
assert.deepEqual(conf, shouldBe); | ||
'And it was called on the correct object': function(err, obj) { | ||
assert.isTrue(obj.obj === CONFIG); | ||
}, | ||
'JSON configuration files can be loaded': function() { | ||
process.argv = ['-config', './config/base.json', 'arg1', | ||
'-config', './config/alpha.js', 'arg2']; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.extendDeep({}, defaultParms, { | ||
dbName:'base_customers', | ||
dbHost:"alpha", | ||
dbPort:5999 | ||
}); | ||
assert.deepEqual(conf, shouldBe); | ||
'And it was called with the correct parameter': function(err, obj) { | ||
assert.equal(obj.prop, 'watchThisValue'); | ||
}, | ||
'And it has the correct prior value': function(err, obj) { | ||
assert.equal(obj.oldValue, originalWatchedValue); | ||
}, | ||
'And it has the correct new value': function(err, obj) { | ||
assert.equal(obj.newValue, newWatchedValue); | ||
}, | ||
'YAML configuration files can be loaded': function() { | ||
process.argv = ['-config', './config/base.yaml', 'arg1', | ||
'-config', './config/alpha.js', 'arg2']; | ||
var conf = config('Customers', defaultParms); | ||
var shouldBe = _.extendDeep({}, defaultParms, { | ||
dbName:'base_customers', | ||
dbHost:"alpha", | ||
dbPort:5999 | ||
}); | ||
assert.deepEqual(conf, shouldBe); | ||
'And the config value was correctly set': function(err, obj) { | ||
assert.equal(CONFIG.watchThisValue, newWatchedValue); | ||
}, | ||
'Resetting command line args': function(orig) { | ||
process.argv = orig.argv; | ||
assert.deepEqual(process.argv, orig.argv); | ||
'waiting for O/S change notification...': function(err, obj) { | ||
// This is just a message for the next test | ||
assert.isTrue(true); | ||
} | ||
}, | ||
'Runtime Configuration Changes are Persisted to runtime.json': { | ||
topic: function() { | ||
// Watch the file for changes | ||
var t = this; | ||
FileSystem.watchFile(runtimeJsonFilename, function(){ | ||
t.callback(null, CONFIG._parseFile(runtimeJsonFilename)); | ||
}); | ||
}, | ||
'The O/S notified us of the configuration file change': function(err, runtimeObj) { | ||
assert.isTrue(!err); | ||
}, | ||
'Prior configuration values were kept intact': function(err, runtimeObj) { | ||
assert.equal(runtimeObj.Customers.dbName, "override_from_runtime_json"); | ||
}, | ||
'Changed configuration values were persisted': function(err, runtimeObj) { | ||
assert.equal(runtimeObj.watchThisValue, CONFIG.watchThisValue); | ||
} | ||
} | ||
}); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
2
10
1
618883
39
1709
81
+ Addedvows@0.5.13(transitive)
+ Addedyaml@0.1.2(transitive)
- Removedeyes@>=0.1.6
- Removedunderscore@1.1.4
- Removedbalanced-match@1.0.2(transitive)
- Removedbrace-expansion@1.1.11(transitive)
- Removedconcat-map@0.0.1(transitive)
- Removeddiff@4.0.2(transitive)
- Removedfs.realpath@1.0.0(transitive)
- Removedglob@7.2.3(transitive)
- Removedinflight@1.0.6(transitive)
- Removedinherits@2.0.4(transitive)
- Removedminimatch@3.1.2(transitive)
- Removedonce@1.4.0(transitive)
- Removedpath-is-absolute@1.0.1(transitive)
- Removedunderscore@1.1.4(transitive)
- Removedvows@0.8.3(transitive)
- Removedwrappy@1.0.2(transitive)
- Removedyaml@2.6.0(transitive)
Updatedvows@0.5.x
Updatedyaml@0.1.x