Finished completely rewriting both library and unit tests

This commit is contained in:
Jonatan Nilsson 2021-06-22 19:25:00 +00:00
parent 1104c0d3ad
commit 1014cdec86
57 changed files with 2785 additions and 2929 deletions

View file

@ -23,26 +23,10 @@ commands:
key: v{{ .Environment.CIRCLE_CACHE_VERSION }}-{{ arch }}-npm-cache-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "package-lock.json" }}
paths:
- ~/.npm/_cacache
coverage:
steps:
- run:
command: npm run cover
- run:
command: npm run coveralls
jobs:
node-v8:
node-v14:
docker:
- image: node:8
steps:
- test-nodejs
node-v10:
docker:
- image: node:10
steps:
- test-nodejs
node-v12:
docker:
- image: node:12
- image: node:14
steps:
- test-nodejs
@ -50,6 +34,4 @@ workflows:
version: 2
node-multi-build:
jobs:
- node-v8
- node-v10
- node-v12
- node-v14

View file

@ -1,2 +0,0 @@
node_modules
usage.js

View file

@ -1,11 +0,0 @@
{
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 6
},
"rules": {
"no-unused-vars": "error"
}
}

1
.gitattributes vendored
View file

@ -1 +0,0 @@
package-lock.json binary

14
.gitignore vendored
View file

@ -1,14 +1,4 @@
.DS_Store
config.json
test/fixtures/*.json
!test/fixtures/complete.json
!test/fixtures/malformed.json
!test/fixtures/bom.json
!test/fixtures/no-bom.json
!test/fixtures/secure.json
!test/fixtures/secure-iv.json
node_modules/
node_modules/*
npm-debug.log
package-lock.json
coverage
node_modules
npm-debug.log

View file

@ -1,3 +1,9 @@
v2.0.0 / Tue, 2 Jun 2020
========================
Completely redesigned and re-written with zero dependencies among other things.
v1.0.0 / Tue, 2 Jun 2020
========================

121
README.md
View file

@ -1,14 +1,17 @@
# nconf-lite
Hierarchical node.js configuration with files, environment variables, and atomic object merging.
Nconf-lite is a complete re-written design of original nconf with zero dependancy, tiny and fast while maintaining most if not all of the documented features of the old nconf.
This is a fork of nconf without the bloated yargs dependancy.
It is a hierarchical node.js configuration with files, environment variables, and atomic object merging.
Compared to nconf running at 952KB with over 220 files *installed*, nconf-lite is clocking at measly 42KB with only 11 files of easily reviewable code and a ton more unit test, testing every micro functionality.
## Example
Using nconf is easy; it is designed to be a simple key-value store with support for both local and remote storage. Keys are namespaced and delimited by `:`. Let's dive right into sample usage:
``` js
var nconf = require('nconf');
import Nconf from 'nconf-lite'
const nconf = new Nconf()
//
// Setup nconf to use (in-order):
@ -35,11 +38,7 @@ Using nconf is easy; it is designed to be a simple key-value store with support
//
// Save the configuration object to disk
//
nconf.save(function (err) {
require('fs').readFile('path/to/your/config.json', function (err, data) {
console.dir(JSON.parse(data.toString()))
});
});
nconf.save()
```
If you run the above script:
@ -67,7 +66,8 @@ Configuration management can get complicated very quickly for even trivial appli
A sane default for this could be:
``` js
var nconf = require('nconf');
import Nconf from 'nconf-lite'
const nconf = new Nconf()
//
// 1. any overrides
@ -92,16 +92,6 @@ A sane default for this could be:
//
nconf.file('custom', '/path/to/config.json');
//
// Or searching from a base directory.
// Note: `name` is optional.
//
nconf.file(name, {
file: 'config.json',
dir: 'search/from/here',
search: true
});
//
// 5. Any default values
//
@ -112,17 +102,6 @@ A sane default for this could be:
## API Documentation
The top-level of `nconf` is an instance of the `nconf.Provider` abstracts this all for you into a simple API.
### nconf.add(name, options)
Adds a new store with the specified `name` and `options`. If `options.type` is not set, then `name` will be used instead:
``` js
nconf.add('supplied', { type: 'literal', store: { 'some': 'config' });
nconf.add('user', { type: 'file', file: '/path/to/userconf.json' });
nconf.add('global', { type: 'file', file: '/path/to/globalconf.json' });
```
### nconf.any(names, callback)
Given a set of key names, gets the value of the first key found to be truthy. The key names can be given as separate arguments
or as an array. If the last argument is a function, it will be called with the result; otherwise, the value is returned.
@ -131,36 +110,21 @@ or as an array. If the last argument is a function, it will be called with the r
//
// Get one of 'NODEJS_PORT' and 'PORT' as a return value
//
var port = nconf.any('NODEJS_PORT', 'PORT');
//
// Get one of 'NODEJS_IP' and 'IPADDRESS' using a callback
//
nconf.any(['NODEJS_IP', 'IPADDRESS'], function(err, value) {
console.log('Connect to IP address ' + value);
});
let port = nconf.any('NODEJS_PORT', 'PORT');
```
### nconf.use(name, options)
Similar to `nconf.add`, except that it can replace an existing store if new options are provided
### nconf.use(name)
Fetch a specific store with the specified name.
``` js
//
// Load a file store onto nconf with the specified settings
//
nconf.use('file', { file: '/path/to/some/config-file.json' });
nconf.file('custom', '/path/to/config.json');
//
// Replace the file store with new settings
// Grab the instance and set it to be readonly
//
nconf.use('file', { file: 'path/to/a-new/config-file.json' });
```
### nconf.remove(name)
Removes the store with the specified `name.` The configuration stored at that level will no longer be used for lookup(s).
``` js
nconf.remove('file');
nconf.use('custom').readOnly = true
```
### nconf.required(keys)
@ -185,10 +149,7 @@ config
.file( 'oauth', path.resolve( 'configs', 'oauth', config.get( 'OAUTH:MODE' ) + '.json' ) )
.file( 'app', path.resolve( 'configs', 'app.json' ) )
.required([ 'LOGS_MODE']) // here you should haveLOGS_MODE, otherwise throw an error
.add( 'logs', {
type: 'literal',
store: require( path.resolve( 'configs', 'logs', config.get( 'LOGS_MODE' ) + '.js') )
} )
.literal( 'logs', require( path.resolve( 'configs', 'logs', config.get( 'LOGS_MODE' ) + '.js') ))
.defaults( defaults );
```
@ -216,9 +177,6 @@ If this option is enabled, all calls to `nconf.get()` must pass in a lowercase s
Attempt to parse well-known values (e.g. 'false', 'true', 'null', 'undefined', '3', '5.1' and JSON values)
into their proper types. If a value cannot be parsed, it will remain a string.
#### `readOnly: {true|false}` (defaultL `true`)
Allow values in the env store to be updated in the future. The default is to not allow items in the env store to be updated.
##### `transform: function(obj)`
Pass each key/value pair to the specified function for transformation.
@ -248,9 +206,9 @@ If the return value is falsey, the entry will be dropped from the store, otherwi
//
// Can also specify a separator for nested keys (instead of the default ':')
//
nconf.env('__');
nconf.env({ separator: '__' });
// Get the value of the env variable 'database__host'
var dbHost = nconf.get('database:host');
let dbHost = nconf.get('database:host');
//
// Can also lowerCase keys.
@ -260,10 +218,10 @@ If the return value is falsey, the entry will be dropped from the store, otherwi
// Given an environment variable PORT=3001
nconf.env();
var port = nconf.get('port') // undefined
let port = nconf.get('port') // undefined
nconf.env({ lowerCase: true });
var port = nconf.get('port') // 3001
let port = nconf.get('port') // 3001
//
// Or use all options
@ -281,7 +239,7 @@ If the return value is falsey, the entry will be dropped from the store, otherwi
return obj;
}
});
var dbHost = nconf.get('database:host');
let dbHost = nconf.get('database:host');
```
### Literal
@ -294,7 +252,7 @@ Loads a given object literal into the configuration hierarchy. Both `nconf.defau
```
### File
Based on the Memory store, but provides additional methods `.save()` and `.load()` which allow you to read your configuration to and from file. As with the Memory store, all method calls are synchronous with the exception of `.save()` and `.load()` which take callback functions.
Based on the Memory store, but provides additional methods `.save()` and `.load()` which allow you to read your configuration to and from file. As with the Memory store, all method calls are synchronous includ `.save()` and `.load()`.
It is important to note that setting keys in the File engine will not be persisted to disk until a call to `.save()` is made. Note a custom key must be supplied as the first parameter for hierarchy to work if multiple files are used.
@ -303,6 +261,10 @@ It is important to note that setting keys in the File engine will not be persist
// add multiple files, hierarchically. notice the unique key for each file
nconf.file('user', 'path/to/your/user.json');
nconf.file('global', 'path/to/your/global.json');
// Set a variable in the user store and save it
nconf.user('user').set('some:variable', true)
nconf.user('user').save()
```
The file store is also extensible for multiple file formats, defaulting to `JSON`. To use a custom format, simply pass a format object to the `.use()` method. This object must have `.parse()` and `.stringify()` methods just like the native `JSON` object.
@ -311,7 +273,7 @@ If the file does not exist at the provided path, the store will simply be empty.
#### Encrypting file contents
As of `nconf@0.8.0` it is now possible to encrypt and decrypt file contents using the `secure` option:
Encryption and decrypting file contents using the `secure` option:
``` js
nconf.file('secure-file', {
@ -340,31 +302,9 @@ This will encrypt each key using [`crypto.createCipheriv`](https://nodejs.org/ap
}
```
### Redis
There is a separate Redis-based store available through [nconf-redis][0]. To install and use this store simply:
``` bash
$ npm install nconf
$ npm install nconf-redis
```
Once installing both `nconf` and `nconf-redis`, you must require both modules to use the Redis store:
``` js
var nconf = require('nconf');
//
// Requiring `nconf-redis` will extend the `nconf`
// module.
//
require('nconf-redis');
nconf.use('redis', { host: 'localhost', port: 6379, ttl: 60 * 60 * 1000 });
```
## Installation
``` bash
npm install nconf --save
npm install nconf-lite --save
```
## Run Tests
@ -374,7 +314,8 @@ Tests are written in vows and give complete coverage of all APIs and storage eng
$ npm test
```
#### Author: [Charlie Robbins](http://nodejitsu.com)
#### Original author: [Charlie Robbins](http://nodejitsu.com)
#### Rewriter of all that garbage: TheThing
#### License: MIT
[0]: http://github.com/indexzero/nconf-redis
[0]: http://github.com/nfp-projects/nconf-lite

177
lib/common.mjs Normal file
View file

@ -0,0 +1,177 @@
import fs from 'fs'
import Memory from './stores/memory.mjs'
//
// ### function validkeyvalue(key)
// #### @key {any} key to check
// Return string of key if valid string type key,
// otherwise transform into new key containing
// the error message
export function validkeyvalue(key) {
let type = typeof(key)
if (key && type !== 'string' && type !== 'number') {
return '__invalid_valuetype_of_' + type + '__'
}
return null
}
//
// ### function path (key)
// #### @key {string} The ':' delimited key to split
// Returns a fully-qualified path to a nested nconf key.
// If given null or undefined it should return an empty path.
// '' should still be respected as a path.
//
export function path(key, separator) {
let invalidType = validkeyvalue(key)
if (invalidType) {
return [invalidType]
}
separator = separator || ':'
return key == null
|| key === ''
? []
: key.toString().split(separator)
}
//
// ### function key (arguments)
// Returns a `:` joined string from the `arguments`.
//
export function key(...path) {
return path.map(function(item) {
return validkeyvalue(item) || ('' + item)
}).join(':')
}
//
// ### function key (arguments)
// Returns a joined string from the `arguments`,
// first argument is the join delimiter.
//
export function keyed(separator, ...path) {
return path.map(function(item) {
return validkeyvalue(item) || ('' + item)
}).join(separator)
}
// taken from isobject npm library
export function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false
}
// Return a new recursive deep instance of array of objects
// or values to make sure no original object ever get touched
export function mergeRecursiveArray(arr) {
return arr.map(function(item) {
if (isObject(item)) return mergeRecursive({}, item)
if (Array.isArray(item)) return mergeRecursiveArray(item)
return item
})
}
// Recursively merges the child into the parent.
export function mergeRecursive(parent, child) {
Object.keys(child).forEach(key => {
// Arrays will always overwrite for now
if (Array.isArray(child[key])) {
parent[key] = mergeRecursiveArray(child[key])
} else if (child[key] && typeof child[key] === 'object') {
// We don't wanna support cross merging between array and objects
// so we overwrite the old value (at least for now).
if (parent[key] && Array.isArray(parent[key])) {
parent[key] = mergeRecursive({}, child[key])
} else {
parent[key] = mergeRecursive(parent[key] || {}, child[key])
}
} else {
parent[key] = child[key]
}
})
return parent
}
//
// ### function merge (objs)
// #### @objs {Array} Array of object literals to merge
// Merges the specified `objs` together into a new object.
// This differs from the old logic as it does not affect or chagne
// any of the objects being merged.
//
export function merge(orgOut, orgObjs) {
let out = orgOut
let objs = orgObjs
if (objs === undefined) {
out = {}
objs = orgOut
}
if (!Array.isArray(objs)) {
throw new Error('merge called with non-array of objects')
}
for (let x = 0; x < objs.length; x++) {
out = mergeRecursive(out, objs[x])
}
return out
}
//
// ### function capitalize (str)
// #### @str {string} String to capitalize
// Capitalizes the specified `str` if string, otherwise
// returns the original object
//
export function capitalize(str) {
if (typeof(str) !== 'string' && typeof(str) !== 'number') {
return str
}
let out = str.toString()
return out && (out[0].toString()).toUpperCase() + out.slice(1)
}
//
// ### function parseValues (any)
// #### @any {string} String to parse as json or return as is
// try to parse `any` as a json stringified
//
export function parseValues(value) {
if (value === 'undefined') {
return undefined
}
try {
return JSON.parse(value)
} catch (ignore) {
return value
}
}
//
// ### function transform(map, fn)
// #### @map {object} Object of key/value pairs to apply `fn` to
// #### @fn {function} Transformation function that will be applied to every key/value pair
// transform a set of key/value pairs and return the transformed result
export function transform(map, fn) {
var pairs = Object.keys(map).map(function(key) {
var result = fn(key, map[key])
if (!result) {
return null
} else if (result.key) {
return result
}
throw new Error('Transform function passed to store returned an invalid format: ' + JSON.stringify(result))
})
return pairs
.filter(function(pair) {
return pair !== null
})
.reduce(function(accumulator, pair) {
accumulator[pair.key] = pair.value
return accumulator
}, {})
}

View file

@ -1,40 +0,0 @@
/*
* nconf.js: Top-level include for the nconf module
*
* (C) 2011, Charlie Robbins and the Contributors.
*
*/
var common = require('./nconf/common'),
Provider = require('./nconf/provider').Provider;
//
// `nconf` is by default an instance of `nconf.Provider`.
//
var nconf = module.exports = new Provider();
//
// Expose the version from the package.json
//
nconf.version = require('../package.json').version;
//
// Setup all stores as lazy-loaded getters.
//
['env', 'file', 'literal', 'memory'].forEach(function (store) {
var name = common.capitalize(store);
nconf.__defineGetter__(name, function () {
return require('./nconf/stores/' + store)[name];
});
});
//
// Expose the various components included with nconf
//
nconf.key = common.key;
nconf.path = common.path;
nconf.loadFiles = common.loadFiles;
nconf.loadFilesSync = common.loadFilesSync;
nconf.formats = require('./nconf/formats');
nconf.Provider = Provider;

181
lib/nconf.mjs Normal file
View file

@ -0,0 +1,181 @@
import fs from 'fs'
import { fileURLToPath } from 'url'
import path from 'path'
import * as common from './common.mjs'
import Literal from './stores/literal.mjs'
import Memory from './stores/memory.mjs'
import File from './stores/file.mjs'
import Env from './stores/env.mjs'
import Argv from './stores/argv.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const pckg = JSON.parse(fs.readFileSync(path.resolve(path.join(__dirname, '../package.json'))))
const AvailableStores = [
['memory', Memory],
['file', File],
['defaults', Literal],
['overrides', Literal],
['literal', Literal],
['env', Env],
['argv', Argv],
]
function Nconf(options) {
let opts = options || {}
this.sources = []
this.using = new Map()
this.version = pckg.version
this.init()
}
Nconf.prototype.key = common.key
Nconf.prototype.path = common.path
Nconf.prototype.init = function() {
AvailableStores.forEach((storeType) => {
let nameCapital = common.capitalize(storeType[0])
let nameLower = storeType[0].toLowerCase()
Object.defineProperty(this, nameCapital, {
value: storeType[1],
writable: false,
enumerable: true,
})
Object.defineProperty(this, nameLower, {
value: function(leName, leOpts) {
let name = leName
let options = leOpts || {}
if (typeof(name) !== 'string') {
name = nameLower
options = leName || {}
}
this.add(name, new this[nameCapital](options))
return this
},
writable: false,
enumerable: true,
})
})
}
Nconf.prototype.any = function(...items) {
let check = items
if (items.length === 1 && Array.isArray(items[0])) {
check = items[0]
}
for (let i = 0; i < check.length; i++) {
let found = this.get(check[i])
if (found) return found
}
return undefined
}
Nconf.prototype.get = function(key) {
let out = []
for (let i = 0; i < this.sources.length; i++) {
let found = this.sources[i].get(key)
if (found && !out.length && (Array.isArray(found) || typeof(found) !== 'object')) {
return found
}
if (found) {
out.push(found)
}
}
if (!out.length) return undefined
return common.merge(out.reverse())
}
Nconf.prototype.set = function(key, value) {
for (let i = 0; i < this.sources.length; i++) {
if (!this.sources[i].readOnly) {
if (this.sources[i].set(key, value))
return this
}
}
return false
}
Nconf.prototype.clear = function(key) {
for (let i = 0; i < this.sources.length; i++) {
this.sources[i].clear(key)
}
if (this.get(key)) {
return false
}
return this
}
Nconf.prototype.load = function() {
for (let i = 0; i < this.sources.length; i++) {
if (typeof(this.sources[i].load) === 'function') {
this.sources[i].load()
}
}
}
Nconf.prototype.save = function() {
for (let i = 0; i < this.sources.length; i++) {
if (typeof(this.sources[i].save) === 'function') {
this.sources[i].save()
}
}
}
Nconf.prototype.reset = function() {
throw new Error('Deprecated, create new instance instead')
}
Nconf.prototype.required = function(...items) {
let check = items
if (items.length === 1 && Array.isArray(items[0])) {
check = items[0]
}
let missing = []
for (let i = 0; i < check.length; i++) {
if (!this.get(check[i])) {
missing.push(check[i])
}
}
if (missing.length) {
throw new Error('Missing required keys: ' + missing.join(', '));
}
return this
}
Nconf.prototype.add = function(name, store) {
let oldStore = this.using.get(name)
if (typeof(store.load) === 'function') {
store.load()
}
if (oldStore) {
this.sources.splice(this.sources.indexOf(oldStore), 1)
this.using.delete(name)
}
this.using.set(name, store)
this.sources.push(store)
}
Nconf.prototype.use = function(name) {
return this.using.get(name)
}
Nconf.register = function(name, val) {
AvailableStores.push([name, val])
let nameCapital = common.capitalize(name)
Object.defineProperty(Nconf, nameCapital, {
value: val,
writable: false,
enumerable: true,
})
}
AvailableStores.forEach((storeType) => {
let nameCapital = common.capitalize(storeType[0])
Object.defineProperty(Nconf, nameCapital, {
value: storeType[1],
writable: false,
enumerable: true,
})
})
export default Nconf

View file

@ -1,175 +0,0 @@
/*
* utils.js: Utility functions for the nconf module.
*
* (C) 2011, Charlie Robbins and the Contributors.
*
*/
var fs = require('fs'),
async = require('async'),
formats = require('./formats'),
Memory = require('./stores/memory').Memory;
var common = exports;
//
// ### function path (key)
// #### @key {string} The ':' delimited key to split
// Returns a fully-qualified path to a nested nconf key.
// If given null or undefined it should return an empty path.
// '' should still be respected as a path.
//
common.path = function (key, separator) {
separator = separator || ':';
return key == null ? [] : key.split(separator);
};
//
// ### function key (arguments)
// Returns a `:` joined string from the `arguments`.
//
common.key = function () {
return Array.prototype.slice.call(arguments).join(':');
};
//
// ### function key (arguments)
// Returns a joined string from the `arguments`,
// first argument is the join delimiter.
//
common.keyed = function () {
return Array.prototype.slice.call(arguments, 1).join(arguments[0]);
};
//
// ### function loadFiles (files, callback)
// #### @files {Object|Array} List of files (or settings object) to load.
// #### @callback {function} Continuation to respond to when complete.
// Loads all the data in the specified `files`.
//
common.loadFiles = function (files, callback) {
if (!files) {
return callback(null, {});
}
var options = Array.isArray(files) ? { files: files } : files;
//
// Set the default JSON format if not already
// specified
//
options.format = options.format || formats.json;
function parseFile (file, next) {
fs.readFile(file, function (err, data) {
return !err
? next(null, options.format.parse(data.toString()))
: next(err);
});
}
async.map(options.files, parseFile, function (err, objs) {
return err ? callback(err) : callback(null, common.merge(objs));
});
};
//
// ### function loadFilesSync (files)
// #### @files {Object|Array} List of files (or settings object) to load.
// Loads all the data in the specified `files` synchronously.
//
common.loadFilesSync = function (files) {
if (!files) {
return;
}
//
// Set the default JSON format if not already
// specified
//
var options = Array.isArray(files) ? { files: files } : files;
options.format = options.format || formats.json;
return common.merge(options.files.map(function (file) {
return options.format.parse(fs.readFileSync(file, 'utf8'));
}));
};
//
// ### function merge (objs)
// #### @objs {Array} Array of object literals to merge
// Merges the specified `objs` using a temporary instance
// of `stores.Memory`.
//
common.merge = function (objs) {
var store = new Memory();
objs.forEach(function (obj) {
Object.keys(obj).forEach(function (key) {
store.merge(key, obj[key]);
});
});
return store.store;
};
//
// ### function capitalize (str)
// #### @str {string} String to capitalize
// Capitalizes the specified `str`.
//
common.capitalize = function (str) {
return str && str[0].toUpperCase() + str.slice(1);
};
//
// ### function parseValues (any)
// #### @any {string} String to parse as native data-type or return as is
// try to parse `any` as a native data-type
//
common.parseValues = function (value) {
var val = value;
try {
val = JSON.parse(value);
} catch (ignore) {
// Check for any other well-known strings that should be "parsed"
if (value === 'undefined'){
val = void 0;
}
}
return val;
};
//
// ### function transform(map, fn)
// #### @map {object} Object of key/value pairs to apply `fn` to
// #### @fn {function} Transformation function that will be applied to every key/value pair
// transform a set of key/value pairs and return the transformed result
common.transform = function(map, fn) {
var pairs = Object.keys(map).map(function(key) {
var obj = { key: key, value: map[key]};
var result = fn.call(null, obj);
if (!result) {
return null;
} else if (result.key) {
return result;
}
var error = new Error('Transform function passed to store returned an invalid format: ' + JSON.stringify(result));
error.name = 'RuntimeError';
throw error;
});
return pairs
.filter(function(pair) {
return pair !== null;
})
.reduce(function(accumulator, pair) {
accumulator[pair.key] = pair.value;
return accumulator;
}, {});
}

View file

@ -1,28 +0,0 @@
/*
* formats.js: Default formats supported by nconf
*
* (C) 2011, Charlie Robbins and the Contributors.
*
*/
var ini = require('ini');
var formats = exports;
//
// ### @json
// Standard JSON format which pretty prints `.stringify()`.
//
formats.json = {
stringify: function (obj, replacer, spacing) {
return JSON.stringify(obj, replacer || null, spacing || 2)
},
parse: JSON.parse
};
//
// ### @ini
// Standard INI format supplied from the `ini` module
// http://en.wikipedia.org/wiki/INI_file
//
formats.ini = ini;

View file

@ -1,655 +0,0 @@
/*
* 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`.
//
options = options || {};
this.stores = {};
this.sources = [];
this.init(options);
};
//
// Define wrapper functions for using basic stores
// in this instance
//
['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 || {};
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).filter(x => x !== 'mock'),
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 any (keys, callback)
// #### @keys {array|string...} Array of keys to query, or a variable list of strings
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Retrieves the first truthy value (if any) for the specified list of keys.
//
Provider.prototype.any = function (keys, callback) {
if (!Array.isArray(keys)) {
keys = Array.prototype.slice.call(arguments);
if (keys.length > 0 && typeof keys[keys.length - 1] === 'function') {
callback = keys.pop();
} else {
callback = null;
}
}
//
// If there is no callback, use the short-circuited "get"
// on each key in turn.
//
if (!callback) {
var val;
for (var i = 0; i < keys.length; ++i) {
val = this._execute('get', 1, keys[i], callback);
if (val) {
return val;
}
}
return null;
}
var keyIndex = 0,
result,
self = this;
async.whilst(function() {
return !result && keyIndex < keys.length;
}, function(next) {
var key = keys[keyIndex];
keyIndex++;
self.get(key, function(err, v) {
if (err) {
next(err);
} else {
result = v;
next();
}
});
}, function(err) {
return err ? callback(err) : callback(null, result);
});
};
//
// ### 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 this;
}
};
//
// ### 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);
}