/* * provider.js: Abstraction providing an interface into pluggable configuration storage. * * (C) 2011, Nodejitsu Inc. * */ 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 // Provider.prototype['argv'] = function(options, usage) { return this.add('argv', options, usage); }; ['env'].forEach(function (type) { Provider.prototype[type] = function (options) { return this.add(type, options); }; }); // // ### 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 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).reverse(), 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 (typeof response === 'object' && !Array.isArray(response)) { mergeObjs.push(response); response = undefined; } next(); }); } response = store.get(key); // Merge objects if necessary if (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 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(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); if (action === 'get') { keys = keys.reverse(); } 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; }