diff --git a/lib/base.js b/lib/base.js index 488226576..81398d404 100755 --- a/lib/base.js +++ b/lib/base.js @@ -657,7 +657,11 @@ internals.Base = class { prefs.abortEarly = true; prefs._externals = false; - return !Validator.validate(value, this, state, prefs, overrides).errors; + state.snapshot(); + const result = !Validator.validate(value, this, state, prefs, overrides).errors; + state.restore(); + + return result; } $_modify(options) { diff --git a/lib/common.js b/lib/common.js index 4b1734da6..05cb66e98 100755 --- a/lib/common.js +++ b/lib/common.js @@ -163,51 +163,6 @@ exports.preferences = function (target, source) { }; -exports.State = class { - - constructor(path, ancestors, state) { - - this.path = path; - this.ancestors = ancestors; // [parent, ..., root] - - this.mainstay = state.mainstay; - this.schemas = state.schemas; // [current, ..., root] - this.debug = null; - } - - localize(path, ancestors = null, schema = null) { - - const state = new exports.State(path, ancestors, this); - - if (schema && - state.schemas) { - - state.schemas = [internals.schemas(schema), ...state.schemas]; - } - - return state; - } - - nest(schema, debug) { - - const state = new exports.State(this.path, this.ancestors, this); - state.schemas = state.schemas && [internals.schemas(schema), ...state.schemas]; - state.debug = debug; - return state; - } -}; - - -internals.schemas = function (schema) { - - if (exports.isSchema(schema)) { - return { schema }; - } - - return schema; -}; - - exports.tryWithPath = function (fn, key, options = {}) { try { diff --git a/lib/ref.js b/lib/ref.js index 266464405..035b80a55 100755 --- a/lib/ref.js +++ b/lib/ref.js @@ -149,7 +149,7 @@ internals.Ref = class { state.mainstay.shadow && options.shadow !== false) { - resolved = state.mainstay.shadow.get(this.path); + resolved = state.mainstay.shadow.get(this.absolute(state)); } if (resolved === undefined) { @@ -179,6 +179,11 @@ internals.Ref = class { return this.display; } + absolute(state) { + + return [...state.path.slice(0, -this.ancestor), ...this.path]; + } + clone() { return new internals.Ref(this); diff --git a/lib/state.js b/lib/state.js new file mode 100755 index 000000000..e1b80ac5b --- /dev/null +++ b/lib/state.js @@ -0,0 +1,150 @@ +'use strict'; + +const Clone = require('@hapi/hoek/lib/clone'); +const Reach = require('@hapi/hoek/lib/reach'); + +const Common = require('./common'); + + +const internals = { + value: Symbol('value') +}; + + +module.exports = internals.State = class { + + constructor(path, ancestors, state) { + + this.path = path; + this.ancestors = ancestors; // [parent, ..., root] + + this.mainstay = state.mainstay; + this.schemas = state.schemas; // [current, ..., root] + this.debug = null; + } + + localize(path, ancestors = null, schema = null) { + + const state = new internals.State(path, ancestors, this); + + if (schema && + state.schemas) { + + state.schemas = [internals.schemas(schema), ...state.schemas]; + } + + return state; + } + + nest(schema, debug) { + + const state = new internals.State(this.path, this.ancestors, this); + state.schemas = state.schemas && [internals.schemas(schema), ...state.schemas]; + state.debug = debug; + return state; + } + + shadow(value, reason) { + + this.mainstay.shadow = this.mainstay.shadow || new internals.Shadow(); + this.mainstay.shadow.set(this.path, value, reason); + } + + snapshot() { + + if (this.mainstay.shadow) { + this._snapshot = Clone(this.mainstay.shadow.node(this.path)); + } + } + + restore() { + + if (this.mainstay.shadow) { + this.mainstay.shadow.override(this.path, this._snapshot); + this._snapshot = undefined; + } + } +}; + + +internals.schemas = function (schema) { + + if (Common.isSchema(schema)) { + return { schema }; + } + + return schema; +}; + + +internals.Shadow = class { + + constructor() { + + this._values = null; + } + + set(path, value, reason) { + + if (!path.length) { // No need to store root value + return; + } + + if (reason === 'strip' && + typeof path[path.length - 1] === 'number') { // Cannot store stripped array values (due to shift) + + return; + } + + this._values = this._values || new Map(); + + let node = this._values; + for (let i = 0; i < path.length; ++i) { + const segment = path[i]; + let next = node.get(segment); + if (!next) { + next = new Map(); + node.set(segment, next); + } + + node = next; + } + + node[internals.value] = value; + } + + get(path) { + + const node = this.node(path); + if (node) { + return node[internals.value]; + } + } + + node(path) { + + if (!this._values) { + return; + } + + return Reach(this._values, path, { iterables: true }); + } + + override(path, node) { + + if (!this._values) { + return; + } + + const parents = path.slice(0, -1); + const own = path[path.length - 1]; + const parent = Reach(this._values, parents, { iterables: true }); + + if (node) { + parent.set(own, node); + } + else { + parent.delete(own); + } + } +}; diff --git a/lib/types/alternatives.js b/lib/types/alternatives.js index c05c55679..d237bacc3 100755 --- a/lib/types/alternatives.js +++ b/lib/types/alternatives.js @@ -49,11 +49,17 @@ module.exports = Any.extend({ for (let i = 0; i < schema.$_terms.matches.length; ++i) { const item = schema.$_terms.matches[i]; - const result = item.schema.$_validate(value, state.nest(item.schema, `match.${i}`), prefs); + const localState = state.nest(item.schema, `match.${i}`); + localState.snapshot(); + + const result = item.schema.$_validate(value, localState, prefs); if (!result.errors) { ++hits; matched = result.value; } + else { + localState.restore(); + } } if (!hits) { @@ -76,11 +82,15 @@ module.exports = Any.extend({ // Try if (item.schema) { - const result = item.schema.$_validate(value, state.nest(item.schema, `match.${i}`), prefs); + const localState = state.nest(item.schema, `match.${i}`); + localState.snapshot(); + + const result = item.schema.$_validate(value, localState, prefs); if (!result.errors) { return result; } + localState.restore(); errors.push({ schema: item.schema, reports: result.errors }); continue; } diff --git a/lib/types/array.js b/lib/types/array.js index 50a974a19..25f8473d0 100755 --- a/lib/types/array.js +++ b/lib/types/array.js @@ -80,7 +80,7 @@ module.exports = Any.extend({ obj.$_mutateRegister(schema); return obj; }, - validate(value, { state, prefs, error, schema }, { schema: has }) { + validate(value, { state, prefs, error }, { schema: has }) { const ancestors = [value, ...state.ancestors]; for (let i = 0; i < value.length; ++i) { @@ -219,7 +219,10 @@ module.exports = Any.extend({ const requiredChecks = []; let jl = requireds.length; for (let j = 0; j < jl; ++j) { - const res = requireds[j].$_validate(item, state.localize(path, ancestors, requireds[j]), prefs); + const localState = state.localize(path, ancestors, requireds[j]); + localState.snapshot(); + + const res = requireds[j].$_validate(item, localState, prefs); requiredChecks[j] = res; if (!res.errors) { @@ -240,6 +243,8 @@ module.exports = Any.extend({ break; } + + localState.restore(); } if (isValid) { @@ -261,7 +266,10 @@ module.exports = Any.extend({ res = requiredChecks[previousCheck]; } else { - res = inclusion.$_validate(item, state.localize(path, ancestors, inclusion), prefs); + const localState = state.localize(path, ancestors, inclusion); + localState.snapshot(); + + res = inclusion.$_validate(item, localState, prefs); if (!res.errors) { if (inclusion._flags.result === 'strip') { internals.fastSplice(value, i); @@ -281,6 +289,8 @@ module.exports = Any.extend({ isValid = true; break; } + + localState.restore(); } // Return the actual error if only one inclusion defined @@ -308,7 +318,9 @@ module.exports = Any.extend({ continue; } - if (schema.$_terms._inclusions.length && !isValid) { + if (schema.$_terms._inclusions.length && + !isValid) { + if (stripUnknown) { internals.fastSplice(value, i); --i; @@ -333,6 +345,7 @@ module.exports = Any.extend({ return errors.length ? errors : value; }, + priority: true, manifest: false }, diff --git a/lib/types/keys.js b/lib/types/keys.js index fa0c48190..434cf0ccc 100755 --- a/lib/types/keys.js +++ b/lib/types/keys.js @@ -202,7 +202,8 @@ module.exports = Any.extend({ validate(value, { error, prefs, state }, { subject, schema, message }) { const about = subject.resolve(value, state, prefs); - if (schema.$_match(about, state.localize([], [value, ...state.ancestors], schema), prefs)) { + const path = Ref.isRef(subject) ? subject.absolute(state) : []; + if (schema.$_match(about, state.localize(path, [value, ...state.ancestors], schema), prefs)) { return value; } diff --git a/lib/validator.js b/lib/validator.js index 8891b768e..76deb9935 100755 --- a/lib/validator.js +++ b/lib/validator.js @@ -7,6 +7,7 @@ const Reach = require('@hapi/hoek/lib/reach'); const Common = require('./common'); const Errors = require('./errors'); +const State = require('./state'); const internals = { @@ -124,7 +125,7 @@ internals.entry = function (value, schema, prefs) { const links = schema._ids._schemaChain ? new Map() : null; const mainstay = { externals: [], warnings: [], tracer, debug, links }; const schemas = schema._ids._schemaChain ? [{ schema }] : null; - const state = new Common.State([], [], { mainstay, schemas }); + const state = new State([], [], { mainstay, schemas }); // Validate value @@ -510,8 +511,7 @@ internals.finalize = function (value, errors, helpers) { if (schema._flags.result) { result.value = schema._flags.result === 'strip' ? undefined : /* raw */ helpers.original; state.mainstay.tracer.value(state, schema._flags.result, value, result.value); - state.mainstay.shadow = state.mainstay.shadow || new internals.Shadow(); - state.mainstay.shadow.set(state.path, value); + state.shadow(value, schema._flags.result); } // Cache @@ -605,43 +605,6 @@ internals.trim = function (value, schema) { }; -internals.Shadow = class { - - constructor() { - - this._value = null; - } - - set(path, value) { - - if (!path.length) { // No need to store root value - return; - } - - this._value = this._value || new Map(); - - let node = this._value; - for (let i = 0; i < path.length - 1; ++i) { - const segment = path[i]; - let next = node.get(segment); - if (!next) { - next = new Map(); - node.set(segment, next); - } - - node = next; - } - - node.set(path[path.length - 1], value); - } - - get(path) { - - return Reach(this._value, path, { iterables: true }); - } -}; - - internals.ignore = { active: false, debug: Ignore, diff --git a/test/base.js b/test/base.js index d31f73b55..5d17bd166 100755 --- a/test/base.js +++ b/test/base.js @@ -2409,6 +2409,149 @@ describe('any', () => { const schema = Joi.any().strip().strip(false); expect(schema._flags.result).to.not.exist(); }); + + it('strips item inside array item', () => { + + const schema = Joi.array().items( + Joi.array().items( + Joi.number(), + Joi.any().strip() + ), + Joi.any().strip() + ); + + Helper.validate(schema, [ + [['x', ['x']], true, [[]]], + [[1, 2, [1, 2, 'x']], true, [[1, 2]]] + ]); + }); + + it('strips nested keys', () => { + + const schema = Joi.object({ + a: Joi.object({ + x: Joi.any(), + y: Joi.any().strip() + }) + .strip(), + + b: Joi.ref('a.y') + }); + + Helper.validate(schema, [ + [{}, true, {}], + [{ a: { x: 1 } }, true, {}], + [{ a: { x: 1, y: 2 } }, true, {}], + [{ a: { x: 1, y: 2 }, b: 2 }, true, { b: 2 }], + [{ a: { x: 1, y: 2 }, b: 3 }, false, '"b" must be [ref:a.y]'], + [{ b: 1 }, false, '"b" must be [ref:a.y]'] + ]); + }); + + it('references validated stripped value (after child already stripped)', () => { + + const schema = Joi.object({ + a: Joi.object({ + x: Joi.any(), + y: Joi.any().strip() + }) + .strip(), + + b: Joi.ref('a') + }); + + Helper.validate(schema, [ + [{}, true, {}], + [{ a: { x: 1, y: 2 } }, true, {}], + [{ a: { x: 1, y: 2 }, b: { x: 1 } }, true, { b: { x: 1 } }], + [{ a: { x: 1, y: 2 }, b: { x: 1, y: 2 } }, false, '"b" must be [ref:a]'] + ]); + }); + + it('strips key nested in alternative', () => { + + const schema = Joi.object({ + a: Joi.alternatives([ + Joi.object({ + x: Joi.number().strip(), + y: Joi.any() + }), + Joi.object({ + x: Joi.any(), + z: Joi.any() + }) + ]), + + b: Joi.ref('a.x') + }); + + Helper.validate(schema, [ + [{}, true, {}], + [{ a: { x: 1, y: 1 } }, true, { a: { y: 1 } }], + [{ a: { x: 1, z: 1 } }, true], + [{ a: { x: 1, z: 1 }, b: 1 }, true], + [{ a: { x: 1, z: 1 }, b: 2 }, false, '"b" must be [ref:a.x]'], + [{ a: { x: '2', z: 1 }, b: 2 }, false, '"b" must be [ref:a.x]'], + [{ a: { x: '2', z: 1 }, b: '2' }, true] + ]); + }); + + it('strips key in array item', () => { + + const schema = Joi.array() + .items( + Joi.object({ + a: Joi.number().strip(), + b: Joi.ref('a') + }), + Joi.object({ + a: Joi.string(), + b: Joi.ref('a') + }) + ); + + Helper.validate(schema, [ + [[], true], + [[{ a: 'x', b: 'x' }], true], + [[{ a: 1, b: 1 }], true, [{ b: 1 }]], + [[{ a: 'x', b: 'x' }, { a: 1, b: 1 }], true, [{ a: 'x', b: 'x' }, { b: 1 }]], + [[{ a: '1', b: '1' }], true, [{ a: '1', b: '1' }]] + ]); + }); + + it('ignored in matches', () => { + + const schema = Joi.array() + .items(Joi.object({ + a: { + b: Joi.number().strip(), + c: Joi.number() + } + })) + .has(Joi.object({ + a: { + b: Joi.number().min(10), + c: Joi.boolean().truthy(30).strip() // Does not affect result + } + })); + + Helper.validate(schema, [ + [[{ a: { b: '20', c: '30' } }], true, [{ a: { c: 30 } }]] + ]); + }); + + it('retains shadow after match', () => { + + const schema = Joi.object({ + a: Joi.number().strip() + }) + .assert('.a', Joi.number().cast('string').strip().required(), 'error 1') + .assert('.a', Joi.number().strict().required(), 'error 2'); + + Helper.validate(schema, [ + [{ a: '1' }, true, {}] + ]); + }); }); describe('tag()', () => {