🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more
Socket
Book a DemoInstallSign in
Socket

convict

Package Overview
Dependencies
Maintainers
3
Versions
63
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

convict - npm Package Compare versions

Comparing version

to
0.1.0

.npmignore

13

example/server.js

@@ -5,15 +5,18 @@ const

conf = convict({
var conf = convict({
ip: {
doc: "The IP Address to bind.",
format: 'string = "127.0.0.1"',
format: "ipaddress",
default: "127.0.0.1",
env: "IP_ADDRESS",
},
port: {
format: 'integer = 0',
env: "PORT",
doc: "The port to bind."
doc: "The port to bind.",
format: 'int',
default: 0,
env: "PORT"
}
}).loadFile(__dirname + '/config.json').validate();
var server = http.createServer(function (req, res) {

@@ -20,0 +23,0 @@ res.writeHead(200, {'Content-Type': 'text/plain'});

@@ -1,37 +0,156 @@

// TODO:
// * nesting
// * environment
// * all sorts of error handling
/*
* node-convict
* Configuration management with support for environmental variables, files, and validation.
*
*/
const
orderly = require('orderly'),
JSV = require("JSV").JSV.createEnvironment(),
cjson = require('cjson'),
fs = require('fs');
const cjson = require('cjson');
const fs = require('fs');
const validator = require('validator');
const moment = require('moment');
function buildSchema(name, o, props, fullName, env) {
if (typeof o === 'string' || (o && o.format && typeof o.format === 'string')) {
var fmt = (typeof o === 'string') ? o : o.format;
try {
var schema = orderly.parse(fmt);
if (!schema.optional) schema.required = true;
props[name] = schema;
} catch(e) {
throw "'" + fullName + "' has an invalid format: " + e.toString();
}
if (o.env) {
if (env[o.env]) {
throw "'" + fullName + "' reuses an env variable: " + o.env
var check = validator.check;
// format can be a:
// - predefine type, as seen below
// - an array of enumerated values, e.g. ["production", "development", "testing"]
// - built-in JavaScript type, i.e. Object, Array, String, Number, Boolean
// - or if omitted, the Object.prototype.toString.call of the default value
var types = {
"*": function() { },
int: function(x) {
check(x, 'must be an integer').isInt();
},
nat: function(x) {
check(x, 'must be a positive integer').isInt().min(0);
},
port: function(x) {
check(x, 'Ports must be within range 1 - 65535').isInt().min(1).max(65535);
},
url: function(x) {
check(x, 'must be a URL').isUrl();
},
email: function(x) {
check(x, 'must be an email address').isEmail();
},
ipaddress: function(x) {
check(x, 'must be an IP address').isIP();
},
duration: function(x) {
check(x, 'must be a positive integer').isInt().min(0);
},
timestamp: function(x) {
check(x, 'must be a positive integer').isInt().min(0);
}
};
function validate (instance, schema, errors) {
Object.keys(schema.properties).reduce(function(previousErrors, name) {
var p = schema.properties[name];
if (p.properties) {
var kids = instance[name] || {};
validate(kids, p, previousErrors);
} else {
try {
p._format(instance[name]);
} catch (e) {
previousErrors.push(e);
}
env[o.env] = fullName;
}
} else {
return previousErrors;
}, errors);
return errors;
}
// helper for checking that a value is in the list of valid options
function contains (options, x) {
check(x, 'must be one of the possible values: ' + JSON.stringify(options)).isIn(options);
}
var BUILT_INS = [Object, Array, String, Number, Boolean];
function normalizeSchema (name, o, props, fullName, env) {
if (typeof o === 'object' && !Array.isArray(o) && typeof o.default === 'undefined') {
props[name] = {
properties: {},
additionalProperties: false
};
Object.keys(o).forEach(function(k) {
buildSchema(k, o[k], props[name].properties, fullName + "." + k, env);
normalizeSchema(k, o[k], props[name].properties, fullName + "." + k, env);
});
return;
} else if (typeof o !== 'object' || Array.isArray(o) || o === null) {
o = { default: o };
}
if (typeof o === 'object') {
props[name] = o;
if (o.env) {
if (env[o.env]) {
throw new Error("'" + fullName + "' reuses an env variable: " + o.env);
}
env[o.env] = fullName;
}
// store original format function
var format = o.format;
var newFormat;
if (BUILT_INS.indexOf(format) >= 0) {
// if the format property is a built-in JavaScript constructor,
// check that the value is of that type
newFormat = function(x) {
check(Object.prototype.toString.call(x), 'must be of type ' + format.name)
.equals(Object.prototype.toString.call(new format()));
};
o.format = format.name.toLowerCase();
} else if (typeof format === 'string') {
// store declared type
if (!format in types) {
throw new Error("'" + fullName + "' uses an unknown format type: " + format);
}
// use a predefined type
newFormat = types[format];
} else if (Array.isArray(format)) {
// check that the value is a valid option
newFormat = contains.bind(null, format);
} else if (typeof format === 'function') {
newFormat = format;
} else if (format && typeof format !== 'function') {
throw new Error("'" + fullName + "': `format` must be a function or a known format type.");
}
if (!newFormat && !format) {
// default format is the typeof the default value
var type = Object.prototype.toString.call(o.default);
newFormat = function(x) {
check(Object.prototype.toString.call(x),
' should be of type ' + type.replace(/\[.* |]/g, '')).equals(type);
};
}
o._format = function (x) {
try {
newFormat(x);
} catch (e) {
// attach the value and the property's fullName to the error
e.fullName = fullName;
e.value = x;
throw e;
}
};
} else {
throw new Error("'" + fullName + "' doesn't appear to be a valid schema object: "
+ JSON.stringify(o))
+ '. Note: schema objects must have a default value.';
}
}

@@ -56,3 +175,3 @@

} else {
if (!c[name] && typeof p.default !== 'undefined') c[name] = p.default
if (!c[name] && typeof p.default !== 'undefined') c[name] = coerce(name, p.default, schema);
}

@@ -62,10 +181,10 @@ });

function overlay(from, to) {
function isObj(o) { return (typeof o === 'object' && o !== null); }
function isObj(o) { return (typeof o === 'object' && o !== null); }
function overlay(from, to, schema) {
Object.keys(from).forEach(function(k) {
// leaf
if (Array.isArray(from[k]) || !isObj(from[k])) to[k] = from[k];
// not
else {
if (Array.isArray(from[k]) || !isObj(from[k])) {
to[k] = coerce(k, from[k], schema);
} else {
if (!isObj(to[k])) to[k] = {};

@@ -77,3 +196,3 @@ overlay(from[k], to[k]);

function getType(schema, path) {
function traverseSchema(schema, path) {
var ar = path.split('.');

@@ -90,13 +209,42 @@ var o = schema;

}
return (o && o.type) ? o.type : null;
return o;
}
module.exports = function(def) {
function getFormat(schema, path) {
var o = traverseSchema(schema, path);
return o ? (typeof o.format === 'string' ? o.format : typeof o.default) : null;
}
function coerce(k, v, schema) {
// magic coerceing
var format = getFormat(schema, k);
if (typeof v === 'string') {
switch (format) {
case 'port':
case 'int': v = parseInt(v, 10); break;
case 'number': v = parseFloat(v); break;
case 'boolean': v = ((v === 'false') ? false : true); break;
case 'array': v = v.split(','); break;
case 'timestamp': v = moment(v).valueOf(); break;
case 'duration':
var split = v.split(' ');
if (!split[1].match(/s$/)) split[1] += 's';
v = moment.duration(parseInt(split[0], 10), split[1]).valueOf();
break;
}
}
return v;
}
module.exports = function convict(def) {
var rv = {
template: function() {
},
toString: function() {
return JSON.stringify(this._instance, null, 2)
return JSON.stringify(this._instance, null, 2);
},
toSchemaString: function() {
return JSON.stringify(this._schema, null, 2);
},
get: function(path) {

@@ -112,3 +260,5 @@ var o = JSON.parse(JSON.stringify(this._instance));

}
if (o === undefined) throw "cannot find configuration param '" + path + "'";
if (o === undefined) {
throw new Error("cannot find configuration param '" + path + "'");
}
return o;

@@ -120,3 +270,3 @@ },

return true;
} catch(e) {
} catch (e) {
return false;

@@ -126,10 +276,4 @@ }

set: function(k, v) {
// magic casting
if (typeof v === 'string') {
switch (getType(this._schema, k)) {
case 'integer': v = parseInt(v); break;
case 'number': v = parseFloat(v); break;
case 'boolean': v = ((v === 'false') ? false : true); break;
}
}
v = coerce(k, v, this._schema);
var ar = k.split('.');

@@ -146,3 +290,3 @@ var o = this._instance;

load: function(conf) {
overlay(conf, this._instance);
overlay(conf, this._instance, this._schema);
// environment always overrides config files

@@ -154,3 +298,3 @@ importEnvironment(rv);

var self = this;
if (!Array.isArray(paths)) paths = [ paths ];
if (!Array.isArray(paths)) paths = [paths];
paths.forEach(function(path) {

@@ -164,19 +308,22 @@ overlay(cjson.load(path), self._instance);

validate: function() {
var report = JSV.validate(this._instance, this._schema);
if (report.errors.length) {
var errBuf = "";
for (var i = 0; i < report.errors.length; i++) {
if (errBuf.length) errBuf += "\n";
var e = report.errors[i];
// get the property name in dot notation
if (e.uri) {
errBuf += e.uri.split('/').slice(1).join('.') + ": ";
var errors = validate(this._instance, this._schema, []);
if (errors.length) {
var errBuf = '';
for (var i = 0; i < errors.length; i++) {
if (errBuf.length) errBuf += '\n';
var e = errors[i];
if (e.fullName) {
errBuf += e.fullName + ': ';
}
if (e.message) errBuf += e.message + ": ";
if (e.details) {
errBuf += ((typeof e.details === 'string') ?
e.details : JSON.stringify(e.details));
if (e.message) errBuf += e.message;
if (e.value) {
errBuf += ': value was ' + JSON.stringify(e.value);
}
}
throw errBuf;
throw new Error(errBuf);
}

@@ -186,3 +333,2 @@ return this;

};
// XXX validate definition

@@ -193,16 +339,13 @@ rv._def = def;

rv._schema = {
properties: {},
additionalProperties: false
properties: {}
};
rv._env = { };
rv._env = {};
Object.keys(def).forEach(function(k) {
buildSchema(k, def[k], rv._schema.properties, k, rv._env);
normalizeSchema(k, def[k], rv._schema.properties, k, rv._env);
});
var report = JSV.validate({}, rv._schema);
rv._instance = report.instance.getValue() || {};
addDefaultValues(rv._schema, rv._instance)
rv._instance = {};
addDefaultValues(rv._schema, rv._instance);
importEnvironment(rv);

@@ -212,1 +355,2 @@

};

@@ -5,3 +5,3 @@ {

"description": "Unruly configuration management for nodejs",
"version": "0.0.6",
"version": "0.1.0",
"homepage": "https://github.com/lloyd/node-convict",

@@ -17,10 +17,10 @@ "repository": {

"dependencies": {
"orderly": "1.0.1",
"JSV": "3.5.0",
"cjson": "0.0.6"
"cjson": "0.2.1",
"validator": "0.4.24",
"moment": "1.7.2"
},
"devDependencies": {
"obj_diff": "0.2.0",
"should": "0.5.1",
"mocha": "0.10.2"
"obj_diff": "0.3.0",
"should": "1.2.2",
"mocha": "1.8.1"
},

@@ -27,0 +27,0 @@ "scripts": {

@@ -1,3 +0,169 @@

## What
# node-convict
convict is a node
Convict expands on the standard pattern of configuring node.js applications in a way that is more robust and accessible to collaborators, who may have less interest in digging through imperative code in order to inspect or modify settings. By introducting a configuration schema, convict gives project collaborators more **context** on each setting and enables **validation and early failures** for when configuration goes wrong.
## Features
* **Loading and merging**: configurations are loaded from disk or inline and merged. JSON files are loaded with `cjson` so comments are welcome.
* **Environmental variables**: values can be set from environmental variables
* **Validation**: configurations are validated against your schema, generating an error report with all errors that are found
Example:
var convict = require('convict');
var http = require('http');
// define a schema
var conf = convict({
env: {
doc: "The applicaton environment.",
format: ["production", "development", "test"],
default: "development",
env: "NODE_ENV"
},
ip: {
doc: "The IP address to bind.",
format: "ipaddress",
default: "127.0.0.1",
env: "IP_ADDRESS",
},
port: {
doc: "The port to bind.",
format: "port",
default: 0,
env: "PORT"
}
});
// load environment dependent configuration
var env = conf.get('env');
conf.loadFile('./config/' + env + '.json');
// perform validation
conf.validate();
var server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
});
// consume
server.listen(conf.get('port'), conf.get('ip'), function(x) {
var addy = server.address();
console.log('running on http://' + addy.address + ":" + addy.port);
});
## Schema
A configuration module could look like this:
config.js:
var config = module.exports = convict({
env: {
doc: "The application environment.",
format: ["production", "development", "test"],
default: "development",
env: "NODE_ENV"
}
});
config.loadFile(['./prod.json', './config.json']);
You'll notice that each setting in the schema has four possible properties, each aiding in convict's goal of being more robust and collaborator friendly.
* **Type information**: the `format` property specifies either a built-in convict format (`ipaddress`, `port`, `int`, etc.), or it can be a function to check a custom format. During validation, if a format check fails it will be added to the error report.
* **Default values**: Every setting *must* have a default value.
* **Environmental variables**: If the variable specified by `env` has a value, it will overwrite the setting's default value.
* **Documentation**: The `doc` property is pretty self-explanatory. The nice part about having it in the schema rather than as a comment is that we can call `conf.toSchemaString()` and have it displayed in the output.
### Validation
In order to help detect misconfigurations, convict allows you to define a format for each setting. By defualt, convict checks if the value of the property has the same `typeof` value as the default value specified in the schema. You can define a custom format checking function in the schema by setting the `format` property.
convict provides serveral predefined formats for validation that you can use ([using node-validator](https://github.com/chriso/node-validator#list-of-validation-methods) and moment.js). Most of them are self-explanatory:
* `*` - any value is valid
* `int`
* `port`
* `url`
* `email`
* `ipaddress` - IPv4 addresses
* `duration` - miliseconds or a human readable string (e.g. "5 days")
* `timestamp` - Unix timestamps or date strings recognized by moment.js
* `nat` - positive integer (natural number)
If `format` is set to one of the built-in JavaScript constructors, `Object`, `Array`, `String`, `Number`, or `Boolean`, validation will use Object.prototype.toString.call to check that the setting is the proper type.
You can also provide your own format checking function. For example:
var check = require('validator').check;
var conf = convict({
key: {
doc: "API key",
format: function (val) {
check(val, 'should be a 64 character hex key').regex(/^[a-fA-F0-9]{64}$/);
},
default: '3cec609c9bc601c047af917a544645c50caf8cd606806b4e0a23312441014deb'
}
});
### Coercion
Convict will automatically coerce environmental variables from strings to their proper types when importing them. For instance, values with the format `int`, `nat`, `port`, or `Number` will become numbers after a straight forward `parseInt` or `parseFloat`. `duration` and `timestamp` are also parse and converted into numbers, though they utilize `moment.js` for date parsing.
## API
### `var config = convict(schema)`
`convict()` takes a schema object and returns a convict configuration object. The configuration object has an API for getting and setting values, described below.
### `config.get(name)`
Returns the value of the `name` property. `name` can use dot notation to reference nested values. E.g.:
config.get('database.host');
// or
config.get('database').host;
### `config.has(name)`
Returns `true` if the property `name` is defined, or `false` otherwise. E.g.:
if (config.has('some.property')) {
// do something
}
### `config.set(name, value)`
Sets the value of `name` to value. `name` can use dot notation to reference nested values, e.g. `"database.port"`. If objects in the chain don't yet exist, they will be initialized to empty objects. E.g.:
config.set('property.that.may.not.exist.yet', 'some value');
config.get('property.that.may.not.exist.yet');
// returns "some value"
### `config.load(object)`
This will load and merge a JavaScript object into `config`. E.g.:
config.load({
"env": "test",
"ip": "127.0.0.1",
"port": 80
});
### `config.loadFile(file or [file1, file2, ...])`
This will load and merge one or multiple JSON configuration files into `config`. JSON files are loaded using `cjson`, so they can contain comments. E.g.:
conf.loadFile('./config/' + conf.get('env') + '.json');
Or, loading multiple files at once:
// CONFIG_FILES=/path/to/production.json,/path/to/secrets.json,/path/to/sitespecific.json
conf.loadFile(process.env.CONFIG_FILES.split(','));
### `config.validate()`
Validates `config` against the schema used to initialize it. All errors are collected and thrown at once.
exports.conf = {
arr: "array { any; }*"
arr: {
default: [],
format: Array
}
};
exports.conf = {
env: {
format: 'string ["production", "local"] = "local"',
format: ["production", "local"],
default: "local",
env: "NODE_ENV",

@@ -8,8 +9,10 @@ doc: "The environment that we're running in."

URL: {
format: 'string = "https://browserid.org"',
format: String,
default: "https://browserid.org",
env: "URL",
doc: "The externally visible url of the server",
doc: "The externally visible url of the server"
},
use_minified_resources: {
format: "boolean = false;",
format: Boolean,
default: false,
doc: "All resources should be combined and minified",

@@ -19,5 +22,6 @@ env: "MINIFIED"

var_path: {
format: 'string = "/home/browserid/var"',
format: String,
default: "/home/browserid/var",
doc: "The path the the 'var' directory, where logs and such will go"
}
};

@@ -1,32 +0,52 @@

module.exports = {
exports.conf = {
env: {
format: 'string ["production", "local"]',
format: ["production", "local"],
default: "production",
env: "NODE_ENV"
},
URL: {
format: 'string = "https://browserid.org"',
format: String,
default: "https://browserid.org",
env: 'URL'
},
use_minified_resources: 'boolean = true',
var_path: 'string = "/home/browserid/var"',
use_minified_resources: true,
var_path: "/home/browserid/var",
database: {
driver: 'string ["json", "mysql"] = "mysql"',
user: 'string',
create_schema: 'boolean',
may_write: 'boolean'
driver: {
default: "mysql",
format: ["json", "mysql"]
},
user: 'browserid',
create_schema: true,
may_write: false
},
statsd: {
enabled: 'boolean'
enabled: true
},
bcrypt_work_factor: "integer[6,20] = 12",
authentication_duration: 'string = "2 weeks"',
certificate_validity: 'string = "1 day"',
min_time_between_emails: 'string = "1 minute"',
max_compute_duration: 'string = "10 seconds"',
disable_primary_support: 'boolean = false',
enable_code_version: 'boolean = false',
default_lang: 'array { string; }',
supported_languages: 'array { string; }',
locale_directory: 'string = "locale"',
express_log_format: 'string = "default"'
bcrypt_work_factor: {
default: 12,
format: 'nat'
},
authentication_duration: {
default: "2 weeks",
format: "duration"
},
certificate_validity: {
default: "1 day",
format: "duration"
},
min_time_between_emails: {
default: "1 minute",
format: "duration"
},
max_compute_duration: {
default: "10 seconds",
format: "duration"
},
disable_primary_support: false,
enable_code_version: false,
default_lang: [ 'en-US' ],
supported_languages: [ 'en-US' ],
locale_directory: "locale",
express_log_format: "default"
};
exports.conf = {
ip: {
format: 'string = "127.0.0.1"',
default: "127.0.0.1",
format: "ipaddress",
env: "IP_ADDRESS"
},
port: {
format: 'integer = 0',
default: 0,
format: "port",
env: "PORT"

@@ -9,0 +11,0 @@ }

exports.conf = {
foo: {
format: 'string = "a"',
default: "a",
format: String,
env: "FOO"

@@ -5,0 +6,0 @@ }

exports.conf = {
foo: {
bar: {
format: 'string ["a", "b"]',
default: "a",
format: ["a", "b"],
env: "BAR"

@@ -6,0 +7,0 @@ }

exports.conf = {
bool: {
format: 'boolean',
default: true,
format: Boolean,
env: "BOOL"
},
int: {
format: 'integer',
default: 42,
format: 'int',
env: "INT"
},
num: {
format: 'number',
default: 10.1,
format: Number,
env: 'NUM'
},
array: {
default: ['a', 'b'],
format: Array,
env: 'ARRAY'
}

@@ -19,3 +27,4 @@ };

INT: 77,
NUM: 789.1011
NUM: 789.1011,
ARRAY: "a,b,c"
};
exports.conf = {
ip: {
doc: "The IP Address to bind.",
format: 'string = "127.0.0.1"',
default: "127.0.0.1",
format: 'ipaddress',
env: "IP_ADDRESS",
},
port: {
format: 'integer = 0',
default: 0,
format: 'port',
env: "PORT",

@@ -10,0 +12,0 @@ doc: "The port to bind."

exports.conf = {
top: {
middle: {
leaf: 'string = "foo"',
leaf: "foo",
leaf2: {
format: 'string = "bar"',
default: "bar",
doc: "a second leaf"
}
},
internal_leaf: 'string = "baz"'
internal_leaf: "baz"
}
};

@@ -10,7 +10,7 @@ const should = require('should');

foo: {
bar: "number = 7",
bar: 7,
baz: {
bing: 'string = "foo"',
bing: "foo",
"name with spaces": {
name_with_underscores: "boolean = true"
name_with_underscores: true
}

@@ -28,11 +28,12 @@ }

it('should find a nested value', function() {
(conf.get('foo.bar')).should.equal(7);
var val = conf.get('foo.bar');
should.equal(val, 7);
});
it('should handle three levels of nesting', function() {
(conf.get('foo.baz.bing')).should.equal('foo');
should.equal(conf.get('foo.baz.bing'), 'foo');
});
it('should handle names with spaces and underscores', function() {
(conf.get('foo.baz.name with spaces.name_with_underscores')).should.equal(true);
should.equal(conf.get('foo.baz.name with spaces.name_with_underscores'), true);
});

@@ -39,0 +40,0 @@

@@ -12,3 +12,4 @@ const

} catch(e) {
process.send({error: e.toString() });
console.error(e);
process.send({error: e.message });
process.exit(1);

@@ -15,0 +16,0 @@ }

@@ -6,3 +6,3 @@ const

cp = require('child_process'),
obj_diff = require('obj_diff').diff,
obj_diff = require('obj_diff'),
mocha = require('mocha');

@@ -53,3 +53,3 @@

var n = cp.fork(path.join(__dirname + '/runner.js'), null, { env: env });
var n = cp.fork(path.join(__dirname + '/runner.js'), [], { env: env });

@@ -56,0 +56,0 @@ n.on('message', function(m) {

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet