From 58f5a947a085720a05a1f6780c3c6216bc0a95ca Mon Sep 17 00:00:00 2001 From: Julian Grinblat Date: Wed, 6 Apr 2022 00:32:51 +0900 Subject: [PATCH] Add isPresent option to object dependencies --- API.md | 7 +++++++ lib/index.d.ts | 23 +++++++++++++------- lib/types/keys.js | 50 +++++++++++++++++++++++++++----------------- test/types/object.js | 17 ++++++++++++++- 4 files changed, 70 insertions(+), 27 deletions(-) diff --git a/API.md b/API.md index 3f2f2427b..dfe4f2b7e 100755 --- a/API.md +++ b/API.md @@ -2274,6 +2274,7 @@ them are required as well where: - `peers` - the string key names of which if one present, all are required. - `options` - optional settings: - `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value. + - `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined` ```js const schema = Joi.object({ @@ -2394,6 +2395,7 @@ Defines a relationship between keys where not all peers can be present at the sa - `peers` - the key names of which if one present, the others may not all be present. - `options` - optional settings: - `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value. + - `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined` ```js const schema = Joi.object({ @@ -2411,6 +2413,7 @@ allowed) where: - `peers` - the key names of which at least one must appear. - `options` - optional settings: - `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value. + - `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined` ```js const schema = Joi.object({ @@ -2428,6 +2431,7 @@ required where: - `peers` - the exclusive key names that must not appear together but where none are required. - `options` - optional settings: - `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value. + - `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined` ```js const schema = Joi.object({ @@ -2566,6 +2570,7 @@ Requires the presence of other keys whenever the specified key is present where: single string value or an array of string values. - `options` - optional settings: - `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value. + - `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined` Note that unlike [`object.and()`](#objectandpeers-options), `with()` creates a dependency only between the `key` and each of the `peers`, not between the `peers` themselves. @@ -2587,6 +2592,7 @@ Forbids the presence of other keys whenever the specified is present where: single string value or an array of string values. - `options` - optional settings: - `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value. + - `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined` ```js const schema = Joi.object({ @@ -2604,6 +2610,7 @@ the same time where: - `peers` - the exclusive key names that must not appear together but where one of them is required. - `options` - optional settings: - `separator` - overrides the default `.` hierarchy separator. Set to `false` to treat the `key` as a literal value. + - `isPresent` - function that overrides the default check for an empty value. Default: `(resolved) => resolved !== undefined` ```js const schema = Joi.object({ diff --git a/lib/index.d.ts b/lib/index.d.ts index 2c0d0c9de..812dd95ee 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -268,6 +268,15 @@ declare namespace Joi { separator?: string | false; } + interface DependencyOptions extends HierarchySeparatorOptions { + /** + * overrides the default check for a present value. + * + * @default (resolved) => resolved !== undefined + */ + isPresent?: (resolved: any) => boolean; + } + interface EmailOptions { /** * if `true`, domains ending with a `.` character are permitted @@ -1673,7 +1682,7 @@ declare namespace Joi { * * Optional settings must be the last argument. */ - and(...peers: Array): this; + and(...peers: Array): this; /** * Appends the allowed object keys. If schema is null, undefined, or {}, no changes will be applied. @@ -1720,21 +1729,21 @@ declare namespace Joi { * * Optional settings must be the last argument. */ - nand(...peers: Array): this; + nand(...peers: Array): this; /** * Defines a relationship between keys where one of the peers is required (and more than one is allowed). * * Optional settings must be the last argument. */ - or(...peers: Array): this; + or(...peers: Array): this; /** * Defines an exclusive relationship between a set of keys where only one is allowed but none are required. * * Optional settings must be the last argument. */ - oxor(...peers: Array): this; + oxor(...peers: Array): this; /** * Specify validation rules for unknown keys matching a pattern. @@ -1772,19 +1781,19 @@ declare namespace Joi { /** * Requires the presence of other keys whenever the specified key is present. */ - with(key: string, peers: string | string[], options?: HierarchySeparatorOptions): this; + with(key: string, peers: string | string[], options?: DependencyOptions): this; /** * Forbids the presence of other keys whenever the specified is present. */ - without(key: string, peers: string | string[], options?: HierarchySeparatorOptions): this; + without(key: string, peers: string | string[], options?: DependencyOptions): this; /** * Defines an exclusive relationship between a set of keys. one of them is required but not at the same time. * * Optional settings must be the last argument. */ - xor(...peers: Array): this; + xor(...peers: Array): this; } interface BinarySchema extends AnySchema { diff --git a/lib/types/keys.js b/lib/types/keys.js index 8f8a950f2..528683f1f 100755 --- a/lib/types/keys.js +++ b/lib/types/keys.js @@ -150,7 +150,7 @@ module.exports = Any.extend({ continue; } - const failed = internals.dependencies[dep.rel](schema, dep, value, state, prefs); + const failed = internals.dependencies[dep.rel](schema, dep, value, state, prefs, dep.options); if (failed) { const report = schema.$_createError(failed.code, value, failed.context, state, prefs); if (prefs.abortEarly) { @@ -595,7 +595,7 @@ internals.dependency = function (schema, rel, key, peers, options) { options = peers.length > 1 && typeof peers[peers.length - 1] === 'object' ? peers.pop() : {}; } - Common.assertOptions(options, ['separator']); + Common.assertOptions(options, ['separator', 'isPresent']); peers = [].concat(peers); @@ -618,20 +618,20 @@ internals.dependency = function (schema, rel, key, peers, options) { const obj = schema.clone(); obj.$_terms.dependencies = obj.$_terms.dependencies || []; - obj.$_terms.dependencies.push(new internals.Dependency(rel, key, paths, peers)); + obj.$_terms.dependencies.push(new internals.Dependency(rel, key, paths, peers, options)); return obj; }; internals.dependencies = { - and(schema, dep, value, state, prefs) { + and(schema, dep, value, state, prefs, options) { const missing = []; const present = []; const count = dep.peers.length; for (const peer of dep.peers) { - if (peer.resolve(value, state, prefs, null, { shadow: false }) === undefined) { + if (internals.isPresent(peer.resolve(value, state, prefs, null, { shadow: false }), options) === false) { missing.push(peer.key); } else { @@ -654,11 +654,11 @@ internals.dependencies = { } }, - nand(schema, dep, value, state, prefs) { + nand(schema, dep, value, state, prefs, options) { const present = []; for (const peer of dep.peers) { - if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) { + if (internals.isPresent(peer.resolve(value, state, prefs, null, { shadow: false }), options)) { present.push(peer.key); } } @@ -680,10 +680,10 @@ internals.dependencies = { }; }, - or(schema, dep, value, state, prefs) { + or(schema, dep, value, state, prefs, options) { for (const peer of dep.peers) { - if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) { + if (internals.isPresent(peer.resolve(value, state, prefs, null, { shadow: false }), options)) { return; } } @@ -697,11 +697,11 @@ internals.dependencies = { }; }, - oxor(schema, dep, value, state, prefs) { + oxor(schema, dep, value, state, prefs, options) { const present = []; for (const peer of dep.peers) { - if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) { + if (internals.isPresent(peer.resolve(value, state, prefs, null, { shadow: false }), options)) { present.push(peer.key); } } @@ -718,10 +718,10 @@ internals.dependencies = { return { code: 'object.oxor', context }; }, - with(schema, dep, value, state, prefs) { + with(schema, dep, value, state, prefs, options) { for (const peer of dep.peers) { - if (peer.resolve(value, state, prefs, null, { shadow: false }) === undefined) { + if (internals.isPresent(peer.resolve(value, state, prefs, null, { shadow: false }), options) === false) { return { code: 'object.with', context: { @@ -735,10 +735,10 @@ internals.dependencies = { } }, - without(schema, dep, value, state, prefs) { + without(schema, dep, value, state, prefs, options) { for (const peer of dep.peers) { - if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) { + if (internals.isPresent(peer.resolve(value, state, prefs, null, { shadow: false }), options)) { return { code: 'object.without', context: { @@ -752,11 +752,11 @@ internals.dependencies = { } }, - xor(schema, dep, value, state, prefs) { + xor(schema, dep, value, state, prefs, options) { const present = []; for (const peer of dep.peers) { - if (peer.resolve(value, state, prefs, null, { shadow: false }) !== undefined) { + if (internals.isPresent(peer.resolve(value, state, prefs, null, { shadow: false }), options)) { present.push(peer.key); } } @@ -787,6 +787,13 @@ internals.keysToLabels = function (schema, keys) { }; +internals.isPresent = function (resolved, options) { + + const isPresent = typeof options.isPresent === 'function' ? options.isPresent : () => resolved !== undefined; + return isPresent(resolved); +}; + + internals.rename = function (schema, value, state, prefs, errors) { const renamed = {}; @@ -992,12 +999,13 @@ internals.unknown = function (schema, value, unprocessed, errors, state, prefs) internals.Dependency = class { - constructor(rel, key, peers, paths) { + constructor(rel, key, peers, paths, options) { this.rel = rel; this.key = key; this.peers = peers; this.paths = paths; + this.options = options; } describe() { @@ -1012,7 +1020,11 @@ internals.Dependency = class { } if (this.peers[0].separator !== '.') { - desc.options = { separator: this.peers[0].separator }; + desc.options = { ...desc.options, separator: this.peers[0].separator }; + } + + if (this.options.isPresent) { + desc.options = { ...desc.options, isPresent: this.options.isPresent }; } return desc; diff --git a/test/types/object.js b/test/types/object.js index e8464bedb..fa0f6a05e 100755 --- a/test/types/object.js +++ b/test/types/object.js @@ -2076,7 +2076,7 @@ describe('object', () => { }); }); - describe('oxor()', () => { + describe.only('oxor()', () => { it('errors when a parameter is not a string', () => { @@ -2176,6 +2176,21 @@ describe('object', () => { [{ a: 'test', b: Object.assign(() => { }, { c: 'test2' }) }, false, '"value" contains a conflict between optional exclusive peers [a, b.c]'] ]); }); + + it('allows setting custom isPresent function', () => { + + const schema = Joi.object({ + 'a': Joi.string().allow(null), + 'b': Joi.string().allow(null) + }) + .oxor('a', 'b', { isPresent: (value) => value !== undefined && value !== null }); + + Helper.validate(schema, [ + [{ a: null, b: null }, true], + [{}, true], + [{ a: 'foo', b: 'bar' }, false, '"value" contains a conflict between optional exclusive peers [a, b]'] + ]); + }); }); describe('pattern()', () => {