From b23697bd2b57d92b592da8f2c7e58b569fd36100 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 13 Feb 2021 01:34:36 -0500 Subject: [PATCH 1/5] Implement, doc, and test store.bind() to bind criteria to the store --- API.md | 10 ++++++++ lib/store.js | 22 +++++++++++++++-- test/store.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/API.md b/API.md index d2a0827..98043d4 100644 --- a/API.md +++ b/API.md @@ -12,6 +12,7 @@ Dynamic, declarative configurations - [`store.load(document)`](#storeloaddocument) - [`store.get(key, [criteria])`](#storegetkey-criteria) - [`store.meta(key, [criteria])`](#storemetakey-criteria) + - [`store.bind(criteria)`](#storebindcriteria) - [Document Format](#document-format) - [Basic Structure](#basic-structure) - [Environment Variables](#environment-variables) @@ -89,6 +90,15 @@ Returns the metadata found after applying the criteria. If the key is invalid or const value = store.meta('/c', { size: 'big' }); ``` +#### `store.bind([criteria])` + +Binds criteria directly to the store, effectively setting it as default criteria. When `criteria` is passed to [`store.get()`](#storegetkey-criteria) or [`store.meta()`](#storemetakey-criteria), it is merged into the bound criteria. When `store.bind()` is called multiple times, the criteria from each call are merged together. Calling `store.bind()` without an argument will reset the bound criteria. + +```javascript +store.bind({ size: 'big' }); +const value = store.get('/c'); +``` + ## Document Format Confidence builds on top of a plain object as its document. diff --git a/lib/store.js b/lib/store.js index 9e25593..bad7d29 100755 --- a/lib/store.js +++ b/lib/store.js @@ -13,6 +13,8 @@ module.exports = internals.Store = class Store { constructor(document) { + this._boundCriteria = {}; + this.load(document || {}); } @@ -22,20 +24,38 @@ module.exports = internals.Store = class Store { Hoek.assert(!err, err); this._tree = Hoek.clone(document); + + return this; } get(key, criteria, applied) { + criteria = this._getCriteria(criteria); + const node = internals.getNode(this._tree, key, criteria, applied); return internals.walk(node, criteria, applied); } meta(key, criteria) { + criteria = this._getCriteria(criteria); + const node = internals.getNode(this._tree, key, criteria); return (typeof node === 'object' ? node.$meta : undefined); } + bind(criteria) { + + this._boundCriteria = criteria === undefined ? {} : this._getCriteria(criteria); + + return this; + } + + _getCriteria(criteria) { + + return Hoek.applyToDefaults(this._boundCriteria, criteria || {}); + } + // Validate tree structure static validate(node) { @@ -75,8 +95,6 @@ module.exports = internals.Store = class Store { internals.getNode = function (tree, key, criteria, applied) { - criteria = criteria || {}; - const path = []; if (key !== '/') { const invalid = key.replace(/\/(\w+)/g, ($0, $1) => { diff --git a/test/store.js b/test/store.js index 78f060e..61a31c5 100755 --- a/test/store.js +++ b/test/store.js @@ -405,6 +405,72 @@ describe('validate()', () => { }); }); +describe('bind()', () => { + + it('binds criteria for get()', () => { + + const store = new Confidence.Store(); + store.load(tree).bind({ + a: { b: 1, c: 2 } + }); + + expect(store.get('/key10')).to.equal({ a: 1, b: 2 }); + expect(store.get('/key10', { a: { b: 3 } })).to.equal({ a: 3, b: 2 }); + }); + + it('binds criteria for meta()', () => { + + const store = new Confidence.Store({ + m: { + $filter: 'a.b', + x: { + $meta: 'got x' + }, + $default: { + $meta: 'got default m' + } + }, + n: { + $filter: 'a.c', + y: { + $meta: 'got y' + }, + $default: { + $meta: 'got default n' + } + } + }); + + store.bind({ + a: { b: 'x', c: 'z' } + }); + + expect(store.meta('/m')).to.equal('got x'); + expect(store.meta('/n')).to.equal('got default n'); + expect(store.meta('/m', { a: { b: 'z' } })).to.equal('got default m'); + expect(store.meta('/n', { a: { c: 'y' } })).to.equal('got y'); + }); + + it('accumulates bindings', () => { + + const store = new Confidence.Store(tree); + store.bind({ a: { b: 1 } }); + store.bind({ a: { c: 2 } }); + + expect(store.get('/key10')).to.equal({ a: 1, b: 2 }); + expect(store.get('/key10', { a: { b: 3 } })).to.equal({ a: 3, b: 2 }); + }); + + it('resets bindings when no arguments are passed', () => { + + const store = new Confidence.Store(tree); + store.bind({ a: { b: 1, c: 2 } }); + store.bind(); + + expect(store.get('/key10')).to.equal({ b: 123 }); + }); +}); + describe('_logApplied', () => { it('adds the filter to the list of applied filters if node or criteria is not defined ', () => { From 9b5f3ef51a394c89913597e5531ce3518ec2d4a5 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 13 Feb 2021 13:04:07 -0500 Subject: [PATCH 2/5] Remove from store logic and tests --- lib/schema.js | 18 +++------ lib/store.js | 8 ++-- test/store.js | 103 ++++++++++++-------------------------------------- 3 files changed, 33 insertions(+), 96 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 15ad2a8..f14ea62 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -152,18 +152,12 @@ exports.store = internals.store = internals.Joi.object().keys({ $param: internals.Joi.string().regex(/^\w+(?:\.\w+)*$/, { name: 'Alphanumeric Characters and "_"' }), $value: internals.alternatives, $replace: internals.Joi.boolean().invalid(false), - $env: internals.Joi.string().regex(/^\w+$/, { name: 'Alphanumeric Characters and "_"' }), $coerce: internals.Joi.string().valid('number', 'array', 'boolean', 'object'), $splitToken: internals.Joi.alternatives([ internals.Joi.string(), internals.Joi.object().instance(RegExp) ]), - $filter: internals.Joi.alternatives([ - internals.Joi.string().regex(/^\w+(?:\.\w+)*$/, { name: 'Alphanumeric Characters and "_"' }), - internals.Joi.object().keys({ - $env: internals.Joi.string().regex(/^\w+$/, { name: 'Alphanumeric Characters and "_"' }).required() - }) - ]), + $filter: internals.Joi.string().regex(/^\w+(?:\.\w+)*$/, { name: 'Alphanumeric Characters and "_"' }), $base: internals.alternatives, $default: internals.alternatives, $id: internals.Joi.string(), @@ -180,14 +174,12 @@ exports.store = internals.store = internals.Joi.object().keys({ .notInstanceOf(Error) .notInstanceOf(RegExp) .notInstanceOf(Date) - .without('$value', ['$filter', '$range', '$base', '$default', '$id', '$param', '$env']) - .without('$param', ['$filter', '$range', '$base', '$id', '$value', '$env']) - .without('$env', ['$filter', '$range', '$base', '$id', '$value', '$param']) + .without('$value', ['$filter', '$range', '$base', '$default', '$id', '$param']) + .without('$param', ['$filter', '$range', '$base', '$id', '$value']) .withPattern('$value', /^([^\$].*)$/, { name: '$value directive can only be used with $meta or $default or nothing' }) .withPattern('$param', /^([^\$].*)$/, { name: '$param directive can only be used with $meta or $default or nothing' }) - .withPattern('$env', /^([^\$].*)$/, { name: '$env directive can only be used with $meta or $default or nothing' }) - .withPattern('$default', /^((\$param)|(\$filter)|(\$env))$/, { inverse: true, name: '$default directive requires $filter or $param or $env' }) - .withPattern('$coerce', /^((\$param)|(\$env))$/, { inverse: true, name: '$coerce directive requires $param or $env' }) + .withPattern('$default', /^((\$param)|(\$filter))$/, { inverse: true, name: '$default directive requires $filter or $param' }) + .withPattern('$coerce', /^((\$param))$/, { inverse: true, name: '$coerce directive requires $param' }) .with('$range', '$filter') .with('$base', '$filter') .with('$splitToken', '$coerce') diff --git a/lib/store.js b/lib/store.js index bad7d29..5fc1f82 100755 --- a/lib/store.js +++ b/lib/store.js @@ -153,7 +153,7 @@ internals.filter = function (node, criteria, applied) { // Filter const filter = node.$filter; - const criterion = typeof filter === 'object' ? Hoek.reach(process.env, filter.$env) : Hoek.reach(criteria, filter); + const criterion = Hoek.reach(criteria, filter); if (criterion !== undefined) { if (node.$range) { @@ -195,11 +195,9 @@ internals.walk = function (node, criteria, applied) { return internals.walk(node.$value, criteria, applied); } - if (Object.prototype.hasOwnProperty.call(node, '$env') || Object.prototype.hasOwnProperty.call(node, '$param')) { + if (Object.prototype.hasOwnProperty.call(node, '$param')) { - const raw = Object.prototype.hasOwnProperty.call(node, '$param') ? - Hoek.reach(criteria, node.$param, applied) : - Hoek.reach(process.env, node.$env, applied); + const raw = Hoek.reach(criteria, node.$param, applied); const value = internals.coerce(raw, node.$coerce || 'string', { splitToken: node.$splitToken || ',' }); diff --git a/test/store.js b/test/store.js index 61a31c5..fdc2d2c 100755 --- a/test/store.js +++ b/test/store.js @@ -14,22 +14,6 @@ const internals = {}; const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; -internals.replaceEnv = (obj) => { - - const replaced = {}; - for (const key in obj) { - if (obj[key]) { - replaced[key] = process.env[key] ? process.env[key] : null; - process.env[key] = obj[key]; - } - else { - delete process.env[key]; - } - } - - return replaced; -}; - const tree = { // Fork key1: 'abc', @@ -67,19 +51,6 @@ const tree = { }, key4: [12, 13, { $filter: 'none', x: 10, $default: 14 }], key5: {}, - key6: { - $filter: { $env: 'NODE_ENV' }, - production: { - animal: 'chicken', - color: 'orange' - }, - staging: { - animal: 'cow' - }, - $base: { - color: 'red' - } - }, key7: { $filter: 'env', production: [ @@ -115,22 +86,11 @@ const tree = { $param: 'a.c', $default: 123, $meta: 'param with default' - } - }, - key11: { - a: { - $env: 'KEY1', - $meta: 'env without default' - }, - b: { - $env: 'KEY2', - $default: 'abc', - $meta: 'env with default' }, - port: { - $env: 'PORT', + c: { + $param: 'a.d', $coerce: 'number', - $default: 3000 + $meta: 'param with coercion' } }, ab: { @@ -153,13 +113,13 @@ const tree = { arrayMerge2: { $filter: 'env', $base: { $value: ['a'] }, $default: ['b'], dev: [] }, arrayMerge3: { $filter: 'env', $base: ['a'], $default: { $value: ['b'] }, dev: {} }, arrayMerge4: { $filter: 'env', $base: ['a'], $default: ['b'], dev: {} }, - coerceArray1: { $env: 'ARRAY', $coerce: 'array', $default: ['a'] }, - coerceArray2: { $env: 'ARRAY', $coerce: 'array', $splitToken: '/', $default: ['a'] }, - coerceArray3: { $env: 'ARRAY', $coerce: 'array', $splitToken: /-/i, $default: ['a'] }, + coerceArray1: { $param: 'arr', $coerce: 'array', $default: ['a'] }, + coerceArray2: { $param: 'arr', $coerce: 'array', $splitToken: '/', $default: ['a'] }, + coerceArray3: { $param: 'arr', $coerce: 'array', $splitToken: /-/i, $default: ['a'] }, - coerceBoolean1: { $env: 'BOOLEAN', $coerce: 'boolean', $default: true }, + coerceBoolean1: { $param: 'bool', $coerce: 'boolean', $default: true }, - coerceObject1: { $env: 'OBJECT', $coerce: 'object', $default: { a: 'b' } }, + coerceObject1: { $param: 'obj', $coerce: 'object', $default: { a: 'b' } }, noProto: Object.create(null), $meta: { @@ -172,14 +132,12 @@ describe('get()', () => { const store = new Confidence.Store(); store.load(tree); - const get = function (key, result, criteria, applied, env) { + const get = function (key, result, criteria, applied) { it('gets value for ' + key + (criteria ? ' with criteria ' + JSON.stringify(criteria) : ''), () => { - const originalEnv = internals.replaceEnv(env || {}); const resultApplied = []; const value = store.get(key, criteria, applied ? resultApplied : null); - internals.replaceEnv(originalEnv); expect(value).to.equal(result); if (applied) { @@ -197,8 +155,6 @@ describe('get()', () => { get('/key2/deeper', undefined, { env: 'qa' }); get('/key2/deeper', undefined); get('/key5', {}); - get('/key6', { animal: 'chicken', color: 'orange' }, {}, [{ filter: { $env: 'NODE_ENV' }, valueId: 'production' }], { NODE_ENV: 'production' }); - get('/key6', { color: 'red', animal: 'cow' }, {}, [{ filter: { $env: 'NODE_ENV' }, valueId: 'staging' }], { NODE_ENV: 'staging' }); get('/key7', [{ animal: 'cat' },{ animal: 'chicken' },{ animal: 'dog' }], { env: 'production' }); get('/key7', [{ animal: 'cat' },{ animal: 'cow' }], { env: 'staging' }); get('/key8', [{ animal: 'chicken' },{ animal: 'dog' }], { env: 'production' }); @@ -206,14 +162,12 @@ describe('get()', () => { get('/key10', { b: 123 }); get('/key10', { a: 'abc', b: 789 }, { a: { b: 'abc', c: 789 } }); get('/key10', { a: 'abc', b: 123 }, { a: { b: 'abc', c: null } }); - get('/key11', { a: 'env', b: 'abc', port: 3000 }, {}, [], { KEY1: 'env' }); - get('/key11', { a: 'env', b: '3000', port: 4000 }, {}, [], { KEY1: 'env', KEY2: 3000, PORT: '4000' }); - get('/key11', { a: 'env', b: '3000', port: 3000 }, {}, [], { KEY1: 'env', KEY2: 3000, PORT: 'abc' }); + get('/key10', { b: 123, c: 3000 }, { a: { d: '3000' } }); + get('/key10', { b: 123 }, { a: { d: 'abc' } }); const slashResult = { key1: 'abc', key10: { b: 123 }, - key11: { b: 'abc', port: 3000 }, key2: 2, key3: { sub1: 0 }, key4: [12, 13, 14], @@ -258,22 +212,22 @@ describe('get()', () => { get('/arrayMerge4', {}, { env: 'dev' }); get('/coerceArray1', ['a'], {}, [], {}); - get('/coerceArray1', ['a', 'b'], {}, [], { ARRAY: 'a,b' }); - get('/coerceArray1', ['a'], {}, [], { ARRAY: '' }); - get('/coerceArray2', ['a', 'b'], {}, [], { ARRAY: 'a/b' }); - get('/coerceArray3', ['a', 'b'], {}, [], { ARRAY: 'a-b' }); + get('/coerceArray1', ['a', 'b'], { arr: 'a,b' }, []); + get('/coerceArray1', ['a'], { arr: '' }, []); + get('/coerceArray2', ['a', 'b'], { arr: 'a/b' }, []); + get('/coerceArray3', ['a', 'b'], { arr: 'a-b' }, []); get('/coerceBoolean1', true, {}, [], {}); - get('/coerceBoolean1', true, {}, [], { 'BOOLEAN': 'true' }); - get('/coerceBoolean1', true, {}, [], { 'BOOLEAN': 'TRUE' }); - get('/coerceBoolean1', false, {}, [], { 'BOOLEAN': 'false' }); - get('/coerceBoolean1', false, {}, [], { 'BOOLEAN': 'FALSE' }); - get('/coerceBoolean1', true, {}, [], { 'BOOLEAN': 'NOT A BOOLEAN' }); - get('/coerceBoolean1', true, {}, [], { 'BOOLEAN': '' }); + get('/coerceBoolean1', true, { bool: 'true' }, []); + get('/coerceBoolean1', true, { bool: 'TRUE' }, []); + get('/coerceBoolean1', false, { bool: 'false' }, []); + get('/coerceBoolean1', false, { bool: 'FALSE'}, []); + get('/coerceBoolean1', true, { bool: 'NOT A BOOLEAN' }, []); + get('/coerceBoolean1', true, { bool: '' }, []); - get('/coerceObject1', { a: 'b' }, {}, [], {}); - get('/coerceObject1', { b: 'a' }, {}, [], { 'OBJECT': '{"b":"a"}' }); - get('/coerceObject1', { a: 'b' }, {}, [], { 'OBJECT': 'BROKEN JSON' }); + get('/coerceObject1', { a: 'b' }, {}, []); + get('/coerceObject1', { b: 'a' }, { obj: '{"b":"a"}' }, []); + get('/coerceObject1', { a: 'b' }, { obj: 'BROKEN JSON' }, []); it('fails on invalid key', () => { @@ -298,7 +252,6 @@ describe('meta()', () => { validate('returns nested meta', '/key3/sub1', 'something'); validate('returns undefined for missing meta', '/key1', undefined); validate('return param meta', '/key10/a', 'param without default'); - validate('return env meta', '/key11/b', 'env with default'); }); describe('load()', () => { @@ -341,7 +294,6 @@ describe('validate()', () => { // object $filter with env validate('empty object filter', { key: { $filter: {} } }); validate('object filter without env', { key: { $filter: { a: 'b' } } }); - validate('object filter without additionl key', { key: { $filter: { $env: 'NODE_ENV', a: 'b' } } }); // unknown $ directives validate('invalid default', { key: { $default: { $b: 5 } } }); @@ -354,18 +306,13 @@ describe('validate()', () => { validate('value with default', { key: { $value: 1, $default: '1' } }); validate('value with range', { key: { $value: 1, $range: [{ limit: 10, value: 4 }] } }); validate('value with param', { key: { $value: 1, $param: 'a.b' } }); - validate('value with env', { key: { $value: 1, $env: 'NODE_ENV' } }); validate('value with non-directive keys', { key: { $value: 1, a: 1 } }); validate('param with filter', { key: { $param: 'a.b', $filter: 'a' } }); validate('param with range', { key: { $param: 'a.b', $range: [{ limit: 10, value: 4 }] } }); - validate('param with env', { key: { $param: 'a.b', $env: 'NODE_ENV' } }); validate('param with non-directive keys', { key: { $param: 'a.b', a: 1 } }); - validate('env with filter', { key: { $env: 'NODE_ENV', $filter: 'a' } }); - validate('env with $range', { key: { $env: 'NODE_ENV', $range: [{ limit: 10, value: 4 }] } }); - validate('env with non-directive keys', { key: { $env: 'NODE_ENV', a: 1 } }); validate('filter without any value', { key: { $filter: '1' } }); validate('filter with only default', { key: { $filter: 'a', $default: 1 } }); - validate('default value without a filter or env or param', { key: { $default: 1 } }); + validate('default value without a filter or param', { key: { $default: 1 } }); // $range validate('non-array range', { key: { $filter: 'a', $range: {}, $default: 1 } }); From 18f32c8a557efbcc27fe54184d5ba4fdecf4f027 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 13 Feb 2021 13:59:39 -0500 Subject: [PATCH 3/5] Fix empty string array coercion --- lib/store.js | 2 +- test/store.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/store.js b/lib/store.js index 5fc1f82..fde03d9 100755 --- a/lib/store.js +++ b/lib/store.js @@ -240,7 +240,7 @@ internals.coerce = function (value, type, options) { break; case 'array': if (typeof value === 'string') { - result = value.split(options.splitToken); + result = value ? value.split(options.splitToken) : []; } else { result = undefined; diff --git a/test/store.js b/test/store.js index fdc2d2c..0fb4031 100755 --- a/test/store.js +++ b/test/store.js @@ -213,7 +213,7 @@ describe('get()', () => { get('/coerceArray1', ['a'], {}, [], {}); get('/coerceArray1', ['a', 'b'], { arr: 'a,b' }, []); - get('/coerceArray1', ['a'], { arr: '' }, []); + get('/coerceArray1', [], { arr: '' }, []); get('/coerceArray2', ['a', 'b'], { arr: 'a/b' }, []); get('/coerceArray3', ['a', 'b'], { arr: 'a-b' }, []); @@ -221,7 +221,7 @@ describe('get()', () => { get('/coerceBoolean1', true, { bool: 'true' }, []); get('/coerceBoolean1', true, { bool: 'TRUE' }, []); get('/coerceBoolean1', false, { bool: 'false' }, []); - get('/coerceBoolean1', false, { bool: 'FALSE'}, []); + get('/coerceBoolean1', false, { bool: 'FALSE' }, []); get('/coerceBoolean1', true, { bool: 'NOT A BOOLEAN' }, []); get('/coerceBoolean1', true, { bool: '' }, []); From 1a393349f80da5aa97568b84d69308dacec20031 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 13 Feb 2021 14:12:15 -0500 Subject: [PATCH 4/5] Remove docs about environment variables, clarify coercion behaviors. --- API.md | 223 +++++++++++++-------------------------------------------- 1 file changed, 50 insertions(+), 173 deletions(-) diff --git a/API.md b/API.md index 98043d4..87338d8 100644 --- a/API.md +++ b/API.md @@ -15,9 +15,8 @@ Dynamic, declarative configurations - [`store.bind(criteria)`](#storebindcriteria) - [Document Format](#document-format) - [Basic Structure](#basic-structure) - - [Environment Variables](#environment-variables) - - [Coercing value](#coercing-value) - [Criteria Parameters](#criteria-parameters) + - [Coercing value](#coercing-value) - [Filters](#filters) - [Ranges](#ranges) - [Metadata](#metadata) @@ -127,69 +126,38 @@ Keys can have children: } ``` -### Environment Variables - -In many scenarios, configuration documents may need to pull values from environment variables. Confidence allows you to refer to environment variables using `$env` directive. - -```json -{ - "mysql": { - "host": { "$env" : "MYSQL_HOST" }, - "port": { "$env" : "MYSQL_PORT" }, - "user": { "$env" : "MYSQL_USER" }, - "password": { "$env" : "MYSQL_PASSWORD" }, - "database": { "$env" : "MYSQL_DATABASE" }, - } -} -``` - -With following Enviornment Variables: - -```sh -MYSQL_HOST=xxx.xxx.xxx.xxx -MYSQL_PORT=3306 -MYSQL_USER=user1 -MYSQL_PASSWORD=some_password -MYSQL_DATABASE=live_db -``` +### Criteria Parameters -The result is: +In many scenarios, configuration documents may need to pull values fron `criteria`. Confidence allows you to refer to `criteria` using `$param` directive. ```json { "mysql": { - "host": "xxx.xxx.xxx.xxx", - "port": "3306", - "user": "user1", - "password": "some_password", - "database": "live_db" + "host": { "$param" : "credentials.mysql.host" }, + "port": { "$param" : "credentials.mysql.port" }, + "user": { "$param" : "credentials.mysql.user" }, + "password": { "$param" : "credentials.mysql.password" }, + "database": { "$param" : "credentials.mysql.database" }, } } ``` -`$default` directive allows to fallback to default values in case an environment variable is not set. +With following `criteria`: ```json { - "mysql": { - "host": { "$env" : "MYSQL_HOST" }, - "port": { "$env" : "MYSQL_PORT", "$default": 3306 }, - "user": { "$env" : "MYSQL_USER" }, - "password": { "$env" : "MYSQL_PASSWORD" }, - "database": { "$env" : "MYSQL_DATABASE" }, + "crendentials": { + "mysql": { + "host": "xxx.xxx.xxx.xxx", + "port": 3306, + "user": "user1", + "password": "some_password", + "database": "live_db" + } } } ``` -With following Enviornment Variables: - -```sh -MYSQL_HOST=xxx.xxx.xxx.xxx -MYSQL_USER=user1 -MYSQL_PASSWORD=some_password -MYSQL_DATABASE=live_db -``` - The result is: ```json @@ -204,58 +172,36 @@ The result is: } ``` -#### Coercing value - -`$coerce` directive allows you to coerce values to different types. In case the coercing fails, it falls back to `$default` directive, if present. Otherwise it return `undefined`. +`$default` directive allows to fallback to default values in case a criteria is `undefined` or `null`. ```json { "mysql": { - "host": { "$env" : "MYSQL_HOST" }, - "port": { - "$env" : "MYSQL_PORT", - "$coerce": "number", - "$default": 3306 - }, - "user": { "$env" : "MYSQL_USER" }, - "password": { "$env" : "MYSQL_PASSWORD" }, - "database": { "$env" : "MYSQL_DATABASE" }, + "host": { "$param" : "credentials.mysql.host" }, + "port": { "$param" : "credentials.mysql.port", "$default": 3306 }, + "user": { "$param" : "credentials.mysql.user" }, + "password": { "$param" : "credentials.mysql.password" }, + "database": { "$param" : "credentials.mysql.database" }, } } -``` - -With following Environment Variables: -```sh -MYSQL_HOST=xxx.xxx.xxx.xxx -MYSQL_PORT=3316 -MYSQL_USER=user1 -MYSQL_PASSWORD=some_password -MYSQL_DATABASE=live_db ``` -The result is: +With following `criteria`: ```json { - "mysql": { - "host": "xxx.xxx.xxx.xxx", - "port": 3316, - "user": "user1", - "password": "some_password", - "database": "live_db" + "credentials": { + "mysql": { + "host": "xxx.xxx.xxx.xxx", + "port": null, + "user": "user1", + "password": "some_password", + "database": "live_db" + } } } ``` -With following Environment Variables: - -```sh -MYSQL_HOST=xxx.xxx.xxx.xxx -MYSQL_PORT=unknown -MYSQL_USER=user1 -MYSQL_PASSWORD=some_password -MYSQL_DATABASE=live_db -``` The result is: @@ -271,24 +217,16 @@ The result is: } ``` -Value can be coerced to : - - `number` : applying `Number(value)` - - `boolean` : checking whether the value equal `true` or `false` case insensitive - - `array` : applying a `value.split(token)` with `token` (by default `','`) modifiable by setting the key `$splitToken` to either a string or a regex - - `object` : applying a `JSON.parse(value)` - -### Criteria Parameters +#### Coercing value -In many scenarios, configuration documents may need to pull values fron `criteria`. Confidence allows you to refer to `criteria` using `$param` directive. +`$coerce` directive allows you to coerce values to different types. In case the coercing fails, it falls back to `$default` directive, if present. Otherwise it returns `undefined`. ```json { - "mysql": { - "host": { "$param" : "credentials.mysql.host" }, - "port": { "$param" : "credentials.mysql.port" }, - "user": { "$param" : "credentials.mysql.user" }, - "password": { "$param" : "credentials.mysql.password" }, - "database": { "$param" : "credentials.mysql.database" }, + "port": { + "$param" : "service.port", + "$coerce": "number", + "$default": 3000 } } ``` @@ -297,14 +235,8 @@ With following `criteria`: ```json { - "crendentials": { - "mysql": { - "host": "xxx.xxx.xxx.xxx", - "port": 3306, - "user": "user1", - "password": "some_password", - "database": "live_db" - } + "service": { + "port": "4000" } } ``` @@ -313,43 +245,16 @@ The result is: ```json { - "mysql": { - "host": "xxx.xxx.xxx.xxx", - "port": "3306", - "user": "user1", - "password": "some_password", - "database": "live_db" - } + "port": 4000 } ``` -`$default` directive allows to fallback to default values in case a criteria is `undefined` or `null`. - -```json -{ - "mysql": { - "host": { "$param" : "credentials.mysql.host" }, - "port": { "$param" : "credentials.mysql.port", "$default": 3306 }, - "user": { "$param" : "credentials.mysql.user" }, - "password": { "$param" : "credentials.mysql.password" }, - "database": { "$param" : "credentials.mysql.database" }, - } -} - -``` - With following `criteria`: ```json { - "credentials": { - "mysql": { - "host": "xxx.xxx.xxx.xxx", - "port": null, - "user": "user1", - "password": "some_password", - "database": "live_db" - } + "service": { + "port": "unknown" } } ``` @@ -358,16 +263,16 @@ The result is: ```json { - "mysql": { - "host": "xxx.xxx.xxx.xxx", - "port": 3306, - "user": "user1", - "password": "some_password", - "database": "live_db" - } + "port": 3000 } ``` +Value can be coerced to: + - `number` : applying `Number(value)`. + - `boolean` : checking whether the value equal `true` or `false` case insensitive. + - `array` : applying a `value.split(token)` with `token` (by default `','`) modifiable by setting the key `$splitToken` to either a string or a regex. Empty strings are coerced to `[]`. + - `object` : applying JSON parsing to the string. + ### Filters @@ -411,34 +316,6 @@ Filters can have a default value which will be used if the provided criteria set } } ``` -Filters can also refer to environment variables using `$env` directive. - -```json -{ - "key1": "abc", - "key2": { - "$filter": { "$env": "NODE_ENV" }, - "production": { - "host": { "$env" : "MYSQL_HOST" }, - "port": { - "$env" : "MYSQL_PORT", - "$coerce": "number", - "$default": 3306 - }, - "user": { "$env" : "MYSQL_USER" }, - "password": { "$env" : "MYSQL_PASSWORD" }, - "database": { "$env" : "MYSQL_DATABASE" }, - }, - "$default": { - "host": "127.0.0.1", - "port": 3306, - "user": "dev", - "password": "password", - "database": "dev_db" - } - } -} -``` ### Ranges From ab56ebe3a3bf2847c95dc5c2efebadab181a2b28 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 13 Feb 2021 14:15:47 -0500 Subject: [PATCH 5/5] Protect from prototype posioning during object coercion --- lib/store.js | 3 ++- package.json | 1 + test/store.js | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/store.js b/lib/store.js index fde03d9..67b8382 100755 --- a/lib/store.js +++ b/lib/store.js @@ -3,6 +3,7 @@ // Load modules const Hoek = require('@hapi/hoek'); +const Bourne = require('@hapi/bourne'); const Schema = require('./schema'); // Declare internals @@ -267,7 +268,7 @@ internals.coerce = function (value, type, options) { break; case 'object': try { - result = JSON.parse(value); + result = Bourne.parse(value); } catch (e) { result = undefined; diff --git a/package.json b/package.json index ba23464..8958d16 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "dependencies": { "@hapi/hoek": "9.x.x", + "@hapi/bourne": "2.x.x", "alce": "1.x.x", "joi": "17.x.x", "yargs": "16.x.x" diff --git a/test/store.js b/test/store.js index 0fb4031..5d069ef 100755 --- a/test/store.js +++ b/test/store.js @@ -228,6 +228,7 @@ describe('get()', () => { get('/coerceObject1', { a: 'b' }, {}, []); get('/coerceObject1', { b: 'a' }, { obj: '{"b":"a"}' }, []); get('/coerceObject1', { a: 'b' }, { obj: 'BROKEN JSON' }, []); + get('/coerceObject1', { a: 'b' }, { obj: '{"b":"a","__proto__":"x"}' }, []); it('fails on invalid key', () => {