599 lines
16 KiB
JavaScript
599 lines
16 KiB
JavaScript
/*
|
|
* provider.js: Abstraction providing an interface into pluggable configuration storage.
|
|
*
|
|
* (C) 2011, Charlie Robbins and the Contributors.
|
|
*
|
|
*/
|
|
|
|
var async = require('async'),
|
|
common = require('./common');
|
|
|
|
//
|
|
// ### function Provider (options)
|
|
// #### @options {Object} Options for this instance.
|
|
// Constructor function for the Provider object responsible
|
|
// for exposing the pluggable storage features of `nconf`.
|
|
//
|
|
var Provider = exports.Provider = function (options) {
|
|
//
|
|
// Setup default options for working with `stores`,
|
|
// `overrides`, `process.env` and `process.argv`.
|
|
//
|
|
options = options || {};
|
|
this.stores = {};
|
|
this.sources = [];
|
|
this.init(options);
|
|
};
|
|
|
|
//
|
|
// Define wrapper functions for using basic stores
|
|
// in this instance
|
|
//
|
|
|
|
['argv', 'env'].forEach(function (type) {
|
|
Provider.prototype[type] = function () {
|
|
var args = [type].concat(Array.prototype.slice.call(arguments));
|
|
return this.add.apply(this, args);
|
|
};
|
|
});
|
|
|
|
//
|
|
// ### function file (key, options)
|
|
// #### @key {string|Object} Fully qualified options, name of file store, or path.
|
|
// #### @path {string|Object} **Optional** Full qualified options, or path.
|
|
// Adds a new `File` store to this instance. Accepts the following options
|
|
//
|
|
// nconf.file({ file: '.jitsuconf', dir: process.env.HOME, search: true });
|
|
// nconf.file('path/to/config/file');
|
|
// nconf.file('userconfig', 'path/to/config/file');
|
|
// nconf.file('userconfig', { file: '.jitsuconf', search: true });
|
|
//
|
|
Provider.prototype.file = function (key, options) {
|
|
if (arguments.length == 1) {
|
|
options = typeof key === 'string' ? { file: key } : key;
|
|
key = 'file';
|
|
}
|
|
else {
|
|
options = typeof options === 'string'
|
|
? { file: options }
|
|
: options;
|
|
}
|
|
|
|
options.type = 'file';
|
|
return this.add(key, options);
|
|
};
|
|
|
|
//
|
|
// Define wrapper functions for using
|
|
// overrides and defaults
|
|
//
|
|
['defaults', 'overrides'].forEach(function (type) {
|
|
Provider.prototype[type] = function (options) {
|
|
options = options || {};
|
|
if (!options.type) {
|
|
options.type = 'literal';
|
|
}
|
|
|
|
return this.add(type, options);
|
|
};
|
|
});
|
|
|
|
//
|
|
// ### function use (name, options)
|
|
// #### @type {string} Type of the nconf store to use.
|
|
// #### @options {Object} Options for the store instance.
|
|
// Adds (or replaces) a new store with the specified `name`
|
|
// and `options`. If `options.type` is not set, then `name`
|
|
// will be used instead:
|
|
//
|
|
// provider.use('file');
|
|
// provider.use('file', { type: 'file', filename: '/path/to/userconf' })
|
|
//
|
|
Provider.prototype.use = function (name, options) {
|
|
options = options || {};
|
|
var type = options.type || name;
|
|
|
|
function sameOptions (store) {
|
|
return Object.keys(options).every(function (key) {
|
|
return options[key] === store[key];
|
|
});
|
|
}
|
|
|
|
var store = this.stores[name],
|
|
update = store && !sameOptions(store);
|
|
|
|
if (!store || update) {
|
|
if (update) {
|
|
this.remove(name);
|
|
}
|
|
|
|
this.add(name, options);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
//
|
|
// ### function add (name, options)
|
|
// #### @name {string} Name of the store to add to this instance
|
|
// #### @options {Object} Options for the store to create
|
|
// Adds a new store with the specified `name` and `options`. If `options.type`
|
|
// is not set, then `name` will be used instead:
|
|
//
|
|
// provider.add('memory');
|
|
// provider.add('userconf', { type: 'file', filename: '/path/to/userconf' })
|
|
//
|
|
Provider.prototype.add = function (name, options, usage) {
|
|
options = options || {};
|
|
var type = options.type || name;
|
|
|
|
if (!require('../nconf')[common.capitalize(type)]) {
|
|
throw new Error('Cannot add store with unknown type: ' + type);
|
|
}
|
|
|
|
this.stores[name] = this.create(type, options, usage);
|
|
|
|
if (this.stores[name].loadSync) {
|
|
this.stores[name].loadSync();
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
//
|
|
// ### function remove (name)
|
|
// #### @name {string} Name of the store to remove from this instance
|
|
// Removes a store with the specified `name` from this instance. Users
|
|
// are allowed to pass in a type argument (e.g. `memory`) as name if
|
|
// this was used in the call to `.add()`.
|
|
//
|
|
Provider.prototype.remove = function (name) {
|
|
delete this.stores[name];
|
|
return this;
|
|
};
|
|
|
|
//
|
|
// ### function create (type, options)
|
|
// #### @type {string} Type of the nconf store to use.
|
|
// #### @options {Object} Options for the store instance.
|
|
// Creates a store of the specified `type` using the
|
|
// specified `options`.
|
|
//
|
|
Provider.prototype.create = function (type, options, usage) {
|
|
return new (require('../nconf')[common.capitalize(type.toLowerCase())])(options, usage);
|
|
};
|
|
|
|
//
|
|
// ### function init (options)
|
|
// #### @options {Object} Options to initialize this instance with.
|
|
// Initializes this instance with additional `stores` or `sources` in the
|
|
// `options` supplied.
|
|
//
|
|
Provider.prototype.init = function (options) {
|
|
var self = this;
|
|
|
|
//
|
|
// Add any stores passed in through the options
|
|
// to this instance.
|
|
//
|
|
if (options.type) {
|
|
this.add(options.type, options);
|
|
}
|
|
else if (options.store) {
|
|
this.add(options.store.name || options.store.type, options.store);
|
|
}
|
|
else if (options.stores) {
|
|
Object.keys(options.stores).forEach(function (name) {
|
|
var store = options.stores[name];
|
|
self.add(store.name || name || store.type, store);
|
|
});
|
|
}
|
|
|
|
//
|
|
// Add any read-only sources to this instance
|
|
//
|
|
if (options.source) {
|
|
this.sources.push(this.create(options.source.type || options.source.name, options.source));
|
|
}
|
|
else if (options.sources) {
|
|
Object.keys(options.sources).forEach(function (name) {
|
|
var source = options.sources[name];
|
|
self.sources.push(self.create(source.type || source.name || name, source));
|
|
});
|
|
}
|
|
};
|
|
|
|
//
|
|
// ### function get (key, callback)
|
|
// #### @key {string} Key to retrieve for this instance.
|
|
// #### @callback {function} **Optional** Continuation to respond to when complete.
|
|
// Retrieves the value for the specified key (if any).
|
|
//
|
|
Provider.prototype.get = function (key, callback) {
|
|
if (typeof key === 'function') {
|
|
// Allow a * key call to be made
|
|
callback = key;
|
|
key = null;
|
|
}
|
|
|
|
//
|
|
// If there is no callback we can short-circuit into the default
|
|
// logic for traversing stores.
|
|
//
|
|
if (!callback) {
|
|
return this._execute('get', 1, key, callback);
|
|
}
|
|
|
|
//
|
|
// Otherwise the asynchronous, hierarchical `get` is
|
|
// slightly more complicated because we do not need to traverse
|
|
// the entire set of stores, but up until there is a defined value.
|
|
//
|
|
var current = 0,
|
|
names = Object.keys(this.stores),
|
|
self = this,
|
|
response,
|
|
mergeObjs = [];
|
|
|
|
async.whilst(function () {
|
|
return typeof response === 'undefined' && current < names.length;
|
|
}, function (next) {
|
|
var store = self.stores[names[current]];
|
|
current++;
|
|
|
|
if (store.get.length >= 2) {
|
|
return store.get(key, function (err, value) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
response = value;
|
|
|
|
// Merge objects if necessary
|
|
if (response && typeof response === 'object' && !Array.isArray(response)) {
|
|
mergeObjs.push(response);
|
|
response = undefined;
|
|
}
|
|
|
|
next();
|
|
});
|
|
}
|
|
|
|
response = store.get(key);
|
|
|
|
// Merge objects if necessary
|
|
if (response && typeof response === 'object' && !Array.isArray(response)) {
|
|
mergeObjs.push(response);
|
|
response = undefined;
|
|
}
|
|
|
|
next();
|
|
}, function (err) {
|
|
if (!err && mergeObjs.length) {
|
|
response = common.merge(mergeObjs.reverse());
|
|
}
|
|
return err ? callback(err) : callback(null, response);
|
|
});
|
|
};
|
|
|
|
//
|
|
// ### function set (key, value, callback)
|
|
// #### @key {string} Key to set in this instance
|
|
// #### @value {literal|Object} Value for the specified key
|
|
// #### @callback {function} **Optional** Continuation to respond to when complete.
|
|
// Sets the `value` for the specified `key` in this instance.
|
|
//
|
|
Provider.prototype.set = function (key, value, callback) {
|
|
return this._execute('set', 2, key, value, callback);
|
|
};
|
|
|
|
|
|
//
|
|
// ### function required (keys)
|
|
// #### @keys {array} List of keys
|
|
// Throws an error if any of `keys` has no value, otherwise returns `true`
|
|
Provider.prototype.required = function (keys) {
|
|
if (!Array.isArray(keys)) {
|
|
throw new Error('Incorrect parameter, array expected');
|
|
}
|
|
|
|
var missing = [];
|
|
keys.forEach(function(key) {
|
|
if (typeof this.get(key) === 'undefined') {
|
|
missing.push(key);
|
|
}
|
|
}, this);
|
|
|
|
if (missing.length) {
|
|
throw new Error('Missing required keys: ' + missing.join(', '));
|
|
} else {
|
|
return true;
|
|
}
|
|
|
|
};
|
|
|
|
//
|
|
// ### function reset (callback)
|
|
// #### @callback {function} **Optional** Continuation to respond to when complete.
|
|
// Clears all keys associated with this instance.
|
|
//
|
|
Provider.prototype.reset = function (callback) {
|
|
return this._execute('reset', 0, callback);
|
|
};
|
|
|
|
//
|
|
// ### function clear (key, callback)
|
|
// #### @key {string} Key to remove from this instance
|
|
// #### @callback {function} **Optional** Continuation to respond to when complete.
|
|
// Removes the value for the specified `key` from this instance.
|
|
//
|
|
Provider.prototype.clear = function (key, callback) {
|
|
return this._execute('clear', 1, key, callback);
|
|
};
|
|
|
|
//
|
|
// ### function merge ([key,] value [, callback])
|
|
// #### @key {string} Key to merge the value into
|
|
// #### @value {literal|Object} Value to merge into the key
|
|
// #### @callback {function} **Optional** Continuation to respond to when complete.
|
|
// Merges the properties in `value` into the existing object value at `key`.
|
|
//
|
|
// 1. If the existing value `key` is not an Object, it will be completely overwritten.
|
|
// 2. If `key` is not supplied, then the `value` will be merged into the root.
|
|
//
|
|
Provider.prototype.merge = function () {
|
|
var self = this,
|
|
args = Array.prototype.slice.call(arguments),
|
|
callback = typeof args[args.length - 1] === 'function' && args.pop(),
|
|
value = args.pop(),
|
|
key = args.pop();
|
|
|
|
function mergeProperty (prop, next) {
|
|
return self._execute('merge', 2, prop, value[prop], next);
|
|
}
|
|
|
|
if (!key) {
|
|
if (Array.isArray(value) || typeof value !== 'object') {
|
|
return onError(new Error('Cannot merge non-Object into top-level.'), callback);
|
|
}
|
|
|
|
return async.forEach(Object.keys(value), mergeProperty, callback || function () { })
|
|
}
|
|
|
|
return this._execute('merge', 2, key, value, callback);
|
|
};
|
|
|
|
//
|
|
// ### function load (callback)
|
|
// #### @callback {function} Continuation to respond to when complete.
|
|
// Responds with an Object representing all keys associated in this instance.
|
|
//
|
|
Provider.prototype.load = function (callback) {
|
|
var self = this;
|
|
|
|
function getStores () {
|
|
var stores = Object.keys(self.stores);
|
|
stores.reverse();
|
|
return stores.map(function (name) {
|
|
return self.stores[name];
|
|
});
|
|
}
|
|
|
|
function loadStoreSync(store) {
|
|
if (!store.loadSync) {
|
|
throw new Error('nconf store ' + store.type + ' has no loadSync() method');
|
|
}
|
|
|
|
return store.loadSync();
|
|
}
|
|
|
|
function loadStore(store, next) {
|
|
if (!store.load && !store.loadSync) {
|
|
return next(new Error('nconf store ' + store.type + ' has no load() method'));
|
|
}
|
|
|
|
return store.loadSync
|
|
? next(null, store.loadSync())
|
|
: store.load(next);
|
|
}
|
|
|
|
function loadBatch (targets, done) {
|
|
if (!done) {
|
|
return common.merge(targets.map(loadStoreSync));
|
|
}
|
|
|
|
async.map(targets, loadStore, function (err, objs) {
|
|
return err ? done(err) : done(null, common.merge(objs));
|
|
});
|
|
}
|
|
|
|
function mergeSources (data) {
|
|
//
|
|
// If `data` was returned then merge it into
|
|
// the system store.
|
|
//
|
|
if (data && typeof data === 'object') {
|
|
self.use('sources', {
|
|
type: 'literal',
|
|
store: data
|
|
});
|
|
}
|
|
}
|
|
|
|
function loadSources () {
|
|
var sourceHierarchy = self.sources.splice(0);
|
|
sourceHierarchy.reverse();
|
|
|
|
//
|
|
// If we don't have a callback and the current
|
|
// store is capable of loading synchronously
|
|
// then do so.
|
|
//
|
|
if (!callback) {
|
|
mergeSources(loadBatch(sourceHierarchy));
|
|
return loadBatch(getStores());
|
|
}
|
|
|
|
loadBatch(sourceHierarchy, function (err, data) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
mergeSources(data);
|
|
return loadBatch(getStores(), callback);
|
|
});
|
|
}
|
|
|
|
return self.sources.length
|
|
? loadSources()
|
|
: loadBatch(getStores(), callback);
|
|
};
|
|
|
|
//
|
|
// ### function save (callback)
|
|
// #### @callback {function} **optional** Continuation to respond to when
|
|
// complete.
|
|
// Instructs each provider to save. If a callback is provided, we will attempt
|
|
// asynchronous saves on the providers, falling back to synchronous saves if
|
|
// this isn't possible. If a provider does not know how to save, it will be
|
|
// ignored. Returns an object consisting of all of the data which was
|
|
// actually saved.
|
|
//
|
|
Provider.prototype.save = function (value, callback) {
|
|
if (!callback && typeof value === 'function') {
|
|
callback = value;
|
|
value = null;
|
|
}
|
|
|
|
var self = this,
|
|
names = Object.keys(this.stores);
|
|
|
|
function saveStoreSync(memo, name) {
|
|
var store = self.stores[name];
|
|
|
|
//
|
|
// If the `store` doesn't have a `saveSync` method,
|
|
// just ignore it and continue.
|
|
//
|
|
if (store.saveSync) {
|
|
var ret = store.saveSync();
|
|
if (typeof ret == 'object' && ret !== null) {
|
|
memo.push(ret);
|
|
}
|
|
}
|
|
return memo;
|
|
}
|
|
|
|
function saveStore(memo, name, next) {
|
|
var store = self.stores[name];
|
|
|
|
//
|
|
// If the `store` doesn't have a `save` or saveSync`
|
|
// method(s), just ignore it and continue.
|
|
//
|
|
|
|
if (store.save) {
|
|
return store.save(value, function (err, data) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (typeof data == 'object' && data !== null) {
|
|
memo.push(data);
|
|
}
|
|
|
|
next(null, memo);
|
|
});
|
|
}
|
|
else if (store.saveSync) {
|
|
memo.push(store.saveSync());
|
|
}
|
|
|
|
next(null, memo);
|
|
}
|
|
|
|
//
|
|
// If we don't have a callback and the current
|
|
// store is capable of saving synchronously
|
|
// then do so.
|
|
//
|
|
if (!callback) {
|
|
return common.merge(names.reduce(saveStoreSync, []));
|
|
}
|
|
|
|
async.reduce(names, [], saveStore, function (err, objs) {
|
|
return err ? callback(err) : callback(null, common.merge(objs));
|
|
});
|
|
};
|
|
|
|
//
|
|
// ### @private function _execute (action, syncLength, [arguments])
|
|
// #### @action {string} Action to execute on `this.store`.
|
|
// #### @syncLength {number} Function length of the sync version.
|
|
// #### @arguments {Array} Arguments array to apply to the action
|
|
// Executes the specified `action` on all stores for this instance, ensuring a callback supplied
|
|
// to a synchronous store function is still invoked.
|
|
//
|
|
Provider.prototype._execute = function (action, syncLength /* [arguments] */) {
|
|
var args = Array.prototype.slice.call(arguments, 2),
|
|
callback = typeof args[args.length - 1] === 'function' && args.pop(),
|
|
destructive = ['set', 'clear', 'merge', 'reset'].indexOf(action) !== -1,
|
|
self = this,
|
|
response,
|
|
mergeObjs = [],
|
|
keys = Object.keys(this.stores);
|
|
|
|
|
|
function runAction (name, next) {
|
|
var store = self.stores[name];
|
|
|
|
if (destructive && store.readOnly) {
|
|
return next();
|
|
}
|
|
|
|
return store[action].length > syncLength
|
|
? store[action].apply(store, args.concat(next))
|
|
: next(null, store[action].apply(store, args));
|
|
}
|
|
|
|
if (callback) {
|
|
return async.forEach(keys, runAction, function (err) {
|
|
return err ? callback(err) : callback();
|
|
});
|
|
}
|
|
|
|
keys.forEach(function (name) {
|
|
if (typeof response === 'undefined') {
|
|
var store = self.stores[name];
|
|
|
|
if (destructive && store.readOnly) {
|
|
return;
|
|
}
|
|
|
|
response = store[action].apply(store, args);
|
|
|
|
// Merge objects if necessary
|
|
if (response && action === 'get' && typeof response === 'object' && !Array.isArray(response)) {
|
|
mergeObjs.push(response);
|
|
response = undefined;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (mergeObjs.length) {
|
|
response = common.merge(mergeObjs.reverse());
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
//
|
|
// Throw the `err` if a callback is not supplied
|
|
//
|
|
function onError(err, callback) {
|
|
if (callback) {
|
|
return callback(err);
|
|
}
|
|
|
|
throw err;
|
|
}
|