From c7f40a62d3606f5039ff02633ad1dd943e53b31c Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Sun, 16 Jun 2019 00:40:37 -0700 Subject: [PATCH] Sort. Closes #1885 --- API.md | 581 +++---- docs/check-errors-list.js | 8 +- lib/cast.js | 4 +- lib/language.js | 3 + lib/types/array.js | 119 +- test/types/array.js | 3224 ++++++++++++++++++++----------------- 6 files changed, 2085 insertions(+), 1854 deletions(-) diff --git a/API.md b/API.md index 6c6200e03..d025b2076 100755 --- a/API.md +++ b/API.md @@ -58,15 +58,15 @@ - [`any.validate(value, [options], [callback])`](#anyvalidatevalue-options-callback) - [`any.when(condition, options)`](#anywhencondition-options) - [`array` - inherits from `Any`](#array---inherits-from-any) - - [`array.sparse([enabled])`](#arraysparseenabled) - - [`array.single([enabled])`](#arraysingleenabled) + - [`array.has(schema)`](#arrayhasschema) - [`array.items(...types)`](#arrayitemstypes) - - [`array.ordered(...type)`](#arrayorderedtype) - - [`array.min(limit)`](#arrayminlimit) - - [`array.max(limit)`](#arraymaxlimit) - [`array.length(limit)`](#arraylengthlimit) + - [`array.max(limit)`](#arraymaxlimit) + - [`array.min(limit)`](#arrayminlimit) + - [`array.ordered(...type)`](#arrayorderedtype) + - [`array.single([enabled])`](#arraysingleenabled) + - [`array.sparse([enabled])`](#arraysparseenabled) - [`array.unique([comparator, [options]])`](#arrayuniquecomparator-options) - - [`array.has(schema)`](#arrayhasschema) - [`boolean` - inherits from `Any`](#boolean---inherits-from-any) - [`boolean.truthy(...values)`](#booleantruthyvalues) - [`boolean.falsy(...values)`](#booleanfalsyvalues) @@ -181,6 +181,9 @@ - [`array.min`](#arraymin) - [`array.orderedLength`](#arrayorderedlength) - [`array.ref`](#arrayref) + - [`array.sort`](#arraysort) + - [`array.sort.mismatching`](#arraysortmismatching) + - [`array.sort.unsupported`](#arraysortunsupported) - [`array.sparse`](#arraysparse) - [`array.unique`](#arrayunique) - [`array.hasKnown`](#arrayhasknown) @@ -830,7 +833,7 @@ Joi.validate({ }); ``` -💥 Possible validation errors: [`any.default`](#anydefault) +Possible validation errors: [`any.default`](#anydefault) #### `any.describe()` @@ -936,7 +939,7 @@ change the reference and any future assignment. Additionally, when specifying a method you must either have a `description` property on your method or the second parameter is required. -💥 Possible validation errors: [`any.failover`](#anyfailover) +Possible validation errors: [`any.failover`](#anyfailover) #### `any.forbidden()` @@ -948,7 +951,7 @@ const schema = { }; ``` -💥 Possible validation errors: [`any.unknown`](#anyunknown) +Possible validation errors: [`any.unknown`](#anyunknown) #### `any.invalid(...values)` - aliases: `disallow`, `not` @@ -963,7 +966,7 @@ const schema = { }; ``` -💥 Possible validation errors: [`any.invalid`](#anyinvalid) +Possible validation errors: [`any.invalid`](#anyinvalid) #### `any.keep()` @@ -1048,7 +1051,7 @@ Marks a key as required which will not allow `undefined` as value. All keys are const schema = Joi.any().required(); ``` -💥 Possible validation errors: [`any.required`](#anyrequired) +Possible validation errors: [`any.required`](#anyrequired) #### `any.rule(options)` @@ -1142,7 +1145,7 @@ const schema = { }; ``` -💥 Possible validation errors: [`any.allowOnly`](#anyallowonly) +Possible validation errors: [`any.allowOnly`](#anyallowonly) #### `any.validate(value, [options], [callback])` @@ -1299,32 +1302,24 @@ const array = Joi.array().items(Joi.string().valid('a', 'b')); array.validate(['a', 'b', 'a'], (err, value) => { }); ``` -💥 Possible validation errors: [`array.base`](#arraybase) - -#### `array.sparse([enabled])` - -Allows this array to be sparse. `enabled` can be used with a falsy value to go back to the default behavior. - -```js -let schema = Joi.array().sparse(); // undefined values are now allowed -schema = schema.sparse(false); // undefined values are now denied -``` - -💥 Possible validation errors: [`array.sparse`](#arraysparse) +Possible validation errors: [`array.base`](#arraybase) -#### `array.single([enabled])` - -Allows single values to be checked against rules as if it were provided as an array. +#### `array.has(schema)` -`enabled` can be used with a falsy value to go back to the default behavior. +Verifies that a schema validates at least one of the values in the array, where: +- `schema` - the validation rules required to satisfy the check. If the `schema` includes references, they are resolved against + the array item being tested, not the value of the `ref` target. ```js -const schema = Joi.array().items(Joi.number()).single(); -schema.validate([4]); // returns `{ error: null, value: [ 4 ] }` -schema.validate(4); // returns `{ error: null, value: [ 4 ] }` +const schema = Joi.array().items( + Joi.object({ + a: Joi.string(), + b: Joi.number() + }) +).has(Joi.object({ a: Joi.string().valid('a'), b: Joi.number() })) ``` -💥 Possible validation errors: [`array.excludes`](#arrayexcludes), [`array.includes`](#arrayincludes) +Possible validation errors: [`array.hasKnown`](#arrayhasknown), [`array.hasUnknown`](#arrayhasunknown) #### `array.items(...types)` @@ -1343,41 +1338,25 @@ const schema = Joi.array().items(Joi.string().valid('not allowed').forbidden(), const schema = Joi.array().items(Joi.string().label('My string').required(), Joi.number().required()); // If this fails it can result in `[ValidationError: "value" does not contain [My string] and 1 other required value(s)]` ``` -💥 Possible validation errors: [`array.excludes`](#arrayexcludes), [`array.includesRequiredBoth`], [`array.includesRequiredKnowns`], [`array.includesRequiredUnknowns`], [`array.includes`](#arrayincludes) +Possible validation errors: [`array.excludes`](#arrayexcludes), [`array.includesRequiredBoth`], [`array.includesRequiredKnowns`], [`array.includesRequiredUnknowns`], [`array.includes`](#arrayincludes) -#### `array.ordered(...type)` - -Lists the types in sequence order for the array values where: -- `types` - one or more **joi** schema objects to validate against each array item in sequence order. - -If a given type is `.required()` then there must be a matching item with the same index position in the array. -Errors will contain the number of items that didn't match. Any unmatched item having a [label](#anylabelname) will be mentioned explicitly. - -```js -const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required()); // array must have first item as string and second item as number -const schema = Joi.array().ordered(Joi.string().required()).items(Joi.number().required()); // array must have first item as string and 1 or more subsequent items as number -const schema = Joi.array().ordered(Joi.string().required(), Joi.number()); // array must have first item as string and optionally second item as number -``` - -💥 Possible validation errors: [`array.excludes`](#arrayexcludes), [`array.includes`](#arrayincludes), [`array.orderedLength`](#arrayorderedlength) - -#### `array.min(limit)` +#### `array.length(limit)` -Specifies the minimum number of items in the array where: -- `limit` - the lowest number of array items allowed or a reference. +Specifies the exact number of items in the array where: +- `limit` - the number of array items allowed or a reference. ```js -const schema = Joi.array().min(2); +const schema = Joi.array().length(5); ``` ```js const schema = Joi.object({ limit: Joi.number().integer().required(), - numbers: Joi.array().min(Joi.ref('limit')).required() + numbers: Joi.array().length(Joi.ref('limit')).required() }); ``` -💥 Possible validation errors: [`array.min`](#arraymin), [`array.ref`](#arrayref) +Possible validation errors: [`array.length`](#arraylength), [`array.ref`](#arrayref) #### `array.max(limit)` @@ -1395,25 +1374,82 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`array.max`](#arraymax), [`array.ref`](#arrayref) +Possible validation errors: [`array.max`](#arraymax), [`array.ref`](#arrayref) -#### `array.length(limit)` +#### `array.min(limit)` -Specifies the exact number of items in the array where: -- `limit` - the number of array items allowed or a reference. +Specifies the minimum number of items in the array where: +- `limit` - the lowest number of array items allowed or a reference. ```js -const schema = Joi.array().length(5); +const schema = Joi.array().min(2); ``` ```js const schema = Joi.object({ limit: Joi.number().integer().required(), - numbers: Joi.array().length(Joi.ref('limit')).required() + numbers: Joi.array().min(Joi.ref('limit')).required() }); ``` -💥 Possible validation errors: [`array.length`](#arraylength), [`array.ref`](#arrayref) +Possible validation errors: [`array.min`](#arraymin), [`array.ref`](#arrayref) + +#### `array.ordered(...type)` + +Lists the types in sequence order for the array values where: +- `types` - one or more **joi** schema objects to validate against each array item in sequence order. + +If a given type is `.required()` then there must be a matching item with the same index position in the array. +Errors will contain the number of items that didn't match. Any unmatched item having a [label](#anylabelname) will be mentioned explicitly. + +```js +const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required()); // array must have first item as string and second item as number +const schema = Joi.array().ordered(Joi.string().required()).items(Joi.number().required()); // array must have first item as string and 1 or more subsequent items as number +const schema = Joi.array().ordered(Joi.string().required(), Joi.number()); // array must have first item as string and optionally second item as number +``` + +Possible validation errors: [`array.excludes`](#arrayexcludes), [`array.includes`](#arrayincludes), [`array.orderedLength`](#arrayorderedlength) + +#### `array.single([enabled])` + +Allows single values to be checked against rules as if it were provided as an array. + +`enabled` can be used with a falsy value to go back to the default behavior. + +```js +const schema = Joi.array().items(Joi.number()).single(); +schema.validate([4]); // returns `{ error: null, value: [ 4 ] }` +schema.validate(4); // returns `{ error: null, value: [ 4 ] }` +``` + +Possible validation errors: [`array.excludes`](#arrayexcludes), [`array.includes`](#arrayincludes) + +#### `array.sort([options])` + +Requires the array to comply with the specified sort order where: +- `options` - optional settings: + - `order` - the sort order. Allowed values: + - `'ascending'` - sort the array in ascending order. This is the default. + - `'descending'` - sort the array in descending order. + - `by` - a key name or reference to sort array objects by. Defautls to the entire value. + +Notes: +- if the `convert` preference is `true`, the array is modified to match the required sort order. +- `undefined` values are always placed at the end of the array regardless of the sort order. +- can only sort string and number items or item key values. + +Possible validation errors: [`array.sort`](#arraysort), [`array.sort.unsupported`](#arraysortunsupported), [`array.sort.mismatching`](#arraysortmismatching) + +#### `array.sparse([enabled])` + +Allows this array to be sparse. `enabled` can be used with a falsy value to go back to the default behavior. + +```js +let schema = Joi.array().sparse(); // undefined values are now allowed +schema = schema.sparse(false); // undefined values are now denied +``` + +Possible validation errors: [`array.sparse`](#arraysparse) #### `array.unique([comparator, [options]])` @@ -1458,25 +1494,7 @@ schema.validate([{}, {}]); // error: null ``` -💥 Possible validation errors: [`array.unique`](#arrayunique) - -#### `array.has(schema)` - -Verifies that a schema validates at least one of the values in the array, where: -- `schema` - the validation rules required to satisfy the check. If the `schema` includes references, they are resolved against - the array item being tested, not the value of the `ref` target. - -```js -const schema = Joi.array().items( - Joi.object({ - a: Joi.string(), - b: Joi.number() - }) -).has(Joi.object({ a: Joi.string().valid('a'), b: Joi.number() })) -``` - -💥 Possible validation errors: [`array.hasKnown`](#arrayhasknown), [`array.hasUnknown`](#arrayhasunknown) - +Possible validation errors: [`array.unique`](#arrayunique) ### `boolean` - inherits from `Any` @@ -1492,7 +1510,7 @@ boolean.validate(true, (err, value) => { }); // Valid boolean.validate(1, (err, value) => { }); // Invalid ``` -💥 Possible validation errors: [`boolean.base`](#booleanbase) +Possible validation errors: [`boolean.base`](#booleanbase) #### `boolean.truthy(...values)` @@ -1540,7 +1558,7 @@ Supports the same methods of the [`any()`](#any) type. const schema = Joi.binary(); ``` -💥 Possible validation errors: [`binary.base`](#binarybase) +Possible validation errors: [`binary.base`](#binarybase) #### `binary.encoding(encoding)` @@ -1560,7 +1578,7 @@ Specifies the minimum length of the buffer where: const schema = Joi.binary().min(2); ``` -💥 Possible validation errors: [`binary.min`](#binarymin), [`binary.ref`](#binaryref) +Possible validation errors: [`binary.min`](#binarymin), [`binary.ref`](#binaryref) #### `binary.max(limit)` @@ -1571,7 +1589,7 @@ Specifies the maximum length of the buffer where: const schema = Joi.binary().max(10); ``` -💥 Possible validation errors: [`binary.max`](#binarymax), [`binary.ref`](#binaryref) +Possible validation errors: [`binary.max`](#binarymax), [`binary.ref`](#binaryref) #### `binary.length(limit)` @@ -1582,7 +1600,7 @@ Specifies the exact length of the buffer: const schema = Joi.binary().length(5); ``` -💥 Possible validation errors: [`binary.length`](#binarylength), [`binary.ref`](#binaryref) +Possible validation errors: [`binary.length`](#binarylength), [`binary.ref`](#binaryref) ### `date` - inherits from `Any` @@ -1596,7 +1614,7 @@ const date = Joi.date(); date.validate('12-21-2012', (err, value) => { }); ``` -💥 Possible validation errors: [`date.base`](#datebase), [`date.strict`](#datestrict) +Possible validation errors: [`date.base`](#datebase), [`date.strict`](#datestrict) #### `date.min(date)` @@ -1620,7 +1638,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`date.min`](#datemin), [`date.ref`](#dateref) +Possible validation errors: [`date.min`](#datemin), [`date.ref`](#dateref) #### `date.max(date)` @@ -1644,7 +1662,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`date.max`](#datemax), [`date.ref`](#dateref) +Possible validation errors: [`date.max`](#datemax), [`date.ref`](#dateref) #### `date.greater(date)` @@ -1667,7 +1685,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`date.greater`](#dategreater), [`date.ref`](#dateref) +Possible validation errors: [`date.greater`](#dategreater), [`date.ref`](#dateref) #### `date.less(date)` @@ -1689,7 +1707,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`date.less`](#dateless), [`date.ref`](#dateref) +Possible validation errors: [`date.less`](#dateless), [`date.ref`](#dateref) #### `date.iso()` @@ -1699,7 +1717,7 @@ Requires the string value to be in valid ISO 8601 date format. const schema = Joi.date().iso(); ``` -💥 Possible validation errors: [`date.isoDate`](#dateisodate) +Possible validation errors: [`date.isoDate`](#dateisodate) #### `date.timestamp([type])` @@ -1713,7 +1731,7 @@ const schema = Joi.date().timestamp('javascript'); // also, for javascript times const schema = Joi.date().timestamp('unix'); // for unix timestamp (seconds) ``` -💥 Possible validation errors: [`date.timestamp.javascript`](#datetimestampjavascript), [`date.timestamp.unix`](#datetimestampunix) +Possible validation errors: [`date.timestamp.javascript`](#datetimestampjavascript), [`date.timestamp.unix`](#datetimestampunix) ### `func` - inherits from `Any` @@ -1728,7 +1746,7 @@ const func = Joi.func(); func.validate(function () {}, (err, value) => { }); ``` -💥 Possible validation errors: [`function.base`](#functionbase) +Possible validation errors: [`function.base`](#functionbase) #### `func.arity(n)` @@ -1739,7 +1757,7 @@ Specifies the arity of the function where: const schema = Joi.func().arity(2); ``` -💥 Possible validation errors: [`function.arity`](#functionarity) +Possible validation errors: [`function.arity`](#functionarity) #### `func.minArity(n)` @@ -1750,7 +1768,7 @@ Specifies the minimal arity of the function where: const schema = Joi.func().minArity(1); ``` -💥 Possible validation errors: [`function.minArity`](#functionminarity) +Possible validation errors: [`function.minArity`](#functionminarity) #### `func.maxArity(n)` @@ -1761,7 +1779,7 @@ Specifies the maximal arity of the function where: const schema = Joi.func().maxArity(3); ``` -💥 Possible validation errors: [`function.maxArity`](#functionmaxarity) +Possible validation errors: [`function.maxArity`](#functionmaxarity) #### `func.class()` @@ -1771,7 +1789,7 @@ Requires the function to be a class. const schema = Joi.func().class(); ``` -💥 Possible validation errors: [`function.class`](#functionclass) +Possible validation errors: [`function.class`](#functionclass) ### `number` - inherits from `Any` @@ -1791,7 +1809,7 @@ const number = Joi.number(); number.validate(5, (err, value) => { }); ``` -💥 Possible validation errors: [`number.base`](#numberbase) +Possible validation errors: [`number.base`](#numberbase) #### `number.unsafe([enabled])` @@ -1811,7 +1829,7 @@ unsafeNumber.validate(90071992547409924); // value -> 90071992547409920 ``` -💥 Possible validation errors: [`number.unsafe`](#numberunsafe) +Possible validation errors: [`number.unsafe`](#numberunsafe) #### `number.min(limit)` @@ -1829,7 +1847,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`number.min`](#numbermin), [`number.ref`](#numberref) +Possible validation errors: [`number.min`](#numbermin), [`number.ref`](#numberref) #### `number.max(limit)` @@ -1847,7 +1865,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`number.max`](#numbermax), [`number.ref`](#numberref) +Possible validation errors: [`number.max`](#numbermax), [`number.ref`](#numberref) #### `number.greater(limit)` @@ -1864,7 +1882,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`number.greater`](#numbergreater), [`number.ref`](#numberref) +Possible validation errors: [`number.greater`](#numbergreater), [`number.ref`](#numberref) #### `number.less(limit)` @@ -1881,7 +1899,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`number.less`](#numberless), [`number.ref`](#numberref) +Possible validation errors: [`number.less`](#numberless), [`number.ref`](#numberref) #### `number.integer()` @@ -1891,7 +1909,7 @@ Requires the number to be an integer (no floating point). const schema = Joi.number().integer(); ``` -💥 Possible validation errors: [`number.base`](#numberbase) +Possible validation errors: [`number.base`](#numberbase) #### `number.precision(limit)` @@ -1902,7 +1920,7 @@ Specifies the maximum number of decimal places where: const schema = Joi.number().precision(2); ``` -💥 Possible validation errors: [`number.integer`](#numberinteger-1) +Possible validation errors: [`number.integer`](#numberinteger-1) #### `number.multiple(base)` @@ -1915,7 +1933,7 @@ const schema = Joi.number().multiple(3); Notes: `Joi.number.multiple(base)` _uses the modulo operator (%) to determine if a number is multiple of another number. Therefore, it has the normal limitations of Javascript modulo operator. The results with decimal/floats may be incorrect._ -💥 Possible validation errors: [`number.multiple`](#numbermultiple), [`number.ref`](#numberref) +Possible validation errors: [`number.multiple`](#numbermultiple), [`number.ref`](#numberref) #### `number.positive()` @@ -1925,7 +1943,7 @@ Requires the number to be positive. const schema = Joi.number().positive(); ``` -💥 Possible validation errors: [`number.positive`](#numberpositive-1) +Possible validation errors: [`number.positive`](#numberpositive-1) #### `number.negative()` @@ -1935,7 +1953,7 @@ Requires the number to be negative. const schema = Joi.number().negative(); ``` -💥 Possible validation errors: [`number.negative`](#numbernegative-1) +Possible validation errors: [`number.negative`](#numbernegative-1) #### `number.port()` @@ -1945,7 +1963,7 @@ Requires the number to be a TCP port, so between 0 and 65535. const schema = Joi.number().port(); ``` -💥 Possible validation errors: [`number.port`](#numberport-1) +Possible validation errors: [`number.port`](#numberport-1) ### `object` - inherits from `Any` @@ -1964,7 +1982,7 @@ const object = Joi.object({ object.validate({ a: 5 }, (err, value) => { }); ``` -💥 Possible validation errors: [`object.base`](#objectbase) +Possible validation errors: [`object.base`](#objectbase) #### `object.keys([schema])` @@ -2009,7 +2027,7 @@ const schema = Joi.object().keys({ }); ``` -💥 Possible validation errors: [`object.allowUnknown`](#objectallowunknown) +Possible validation errors: [`object.allowUnknown`](#objectallowunknown) While all these three objects defined above will result in the same validation object, there are some differences in using one or another: @@ -2077,7 +2095,7 @@ Specifies the minimum number of keys in the object where: const schema = Joi.object().min(2); ``` -💥 Possible validation errors: [`object.min`](#objectmin), [`object.ref`](#objectref) +Possible validation errors: [`object.min`](#objectmin), [`object.ref`](#objectref) #### `object.max(limit)` @@ -2088,7 +2106,7 @@ Specifies the maximum number of keys in the object where: const schema = Joi.object().max(10); ``` -💥 Possible validation errors: [`object.max`](#objectmax), [`object.ref`](#objectref) +Possible validation errors: [`object.max`](#objectmax), [`object.ref`](#objectref) #### `object.length(limit)` @@ -2099,7 +2117,7 @@ Specifies the exact number of keys in the object where or a reference: const schema = Joi.object().length(5); ``` -💥 Possible validation errors: [`object.length`](#objectlength), [`object.ref`](#objectref) +Possible validation errors: [`object.length`](#objectlength), [`object.ref`](#objectref) #### `object.pattern(pattern, schema)` @@ -2134,7 +2152,7 @@ const schema = Joi.object({ }).and('a', 'b'); ``` -💥 Possible validation errors: [`object.and`](#objectand) +Possible validation errors: [`object.and`](#objectand) #### `object.nand(...peers, [options])` @@ -2150,7 +2168,7 @@ const schema = Joi.object({ }).nand('a', 'b'); ``` -💥 Possible validation errors: [`object.nand`](#objectnand) +Possible validation errors: [`object.nand`](#objectnand) #### `object.or(...peers, [options])` @@ -2167,7 +2185,7 @@ const schema = Joi.object({ }).or('a', 'b'); ``` -💥 Possible validation errors: [`object.missing`](#objectmissing) +Possible validation errors: [`object.missing`](#objectmissing) #### `object.xor(...peers, [options])` @@ -2184,7 +2202,7 @@ const schema = Joi.object({ }).xor('a', 'b'); ``` -💥 Possible validation errors: [`object.xor`](#objectxor), [`object.missing`](#objectmissing) +Possible validation errors: [`object.xor`](#objectxor), [`object.missing`](#objectmissing) #### `object.oxor(...peers, [options])` @@ -2201,7 +2219,7 @@ const schema = Joi.object({ }).oxor('a', 'b'); ``` -💥 Possible validation errors: [`object.oxor`](#objectoxor) +Possible validation errors: [`object.oxor`](#objectoxor) #### `object.with(key, peers, [options])` @@ -2222,7 +2240,7 @@ const schema = Joi.object({ }).with('a', 'b'); ``` -💥 Possible validation errors: [`object.with`](#objectwith) +Possible validation errors: [`object.with`](#objectwith) #### `object.without(key, peers, [options])` @@ -2240,7 +2258,7 @@ const schema = Joi.object({ }).without('a', ['b']); ``` -💥 Possible validation errors: [`object.without`](#objectwithout) +Possible validation errors: [`object.without`](#objectwithout) #### `object.ref()` @@ -2250,7 +2268,7 @@ Requires the object to be a Joi reference. const schema = Joi.object().ref(); ``` -💥 Possible validation errors: [`object.refType`](#objectreftype) +Possible validation errors: [`object.refType`](#objectreftype) #### `object.rename(from, to, [options])` @@ -2306,7 +2324,7 @@ const value = await Joi.compile(schema).validate(input); // value === { x123x: 'x', x1x: 'y', x0x: 'z', x4x: 'test' } ``` -💥 Possible validation errors: [`object.rename.multiple`](#objectrenamemultiple), [`object.rename.override`](#objectrenameoverride) +Possible validation errors: [`object.rename.multiple`](#objectrenamemultiple), [`object.rename.override`](#objectrenameoverride) #### `object.assert(ref, schema, [message])` @@ -2328,7 +2346,7 @@ const schema = Joi.object({ }).assert('d.e', Joi.ref('a.c'), 'equal to a.c'); ``` -💥 Possible validation errors: [`object.assert`](#objectassert) +Possible validation errors: [`object.assert`](#objectassert) #### `object.unknown([allow])` @@ -2339,7 +2357,7 @@ Overrides the handling of unknown keys for the scope of the current object only const schema = Joi.object({ a: Joi.any() }).unknown(); ``` -💥 Possible validation errors: [`object.allowUnknown`](#objectallowunknown) +Possible validation errors: [`object.allowUnknown`](#objectallowunknown) #### `object.type(constructor, [name])` @@ -2351,7 +2369,7 @@ Requires the object to be an instance of a given constructor where: const schema = Joi.object().type(RegExp); ``` -💥 Possible validation errors: [`object.type`](#objecttype) +Possible validation errors: [`object.type`](#objecttype) #### `object.schema([type])` @@ -2362,7 +2380,7 @@ Requires the object to be a Joi schema instance where: const schema = Joi.object().schema(); ``` -💥 Possible validation errors: [`object.schema`](#objectschema-1) +Possible validation errors: [`object.schema`](#objectschema-1) #### `object.requiredKeys(...children)` @@ -2417,7 +2435,7 @@ const schema = Joi.string().min(1).max(10); schema.validate('12345', (err, value) => { }); ``` -💥 Possible validation errors: [`string.base`](#stringbase), [`any.empty`](#anyempty) +Possible validation errors: [`string.base`](#stringbase), [`any.empty`](#anyempty) #### `string.insensitive()` @@ -2444,7 +2462,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`string.min`](#stringmin), [`string.ref`](#stringref) +Possible validation errors: [`string.min`](#stringmin), [`string.ref`](#stringref) #### `string.max(limit, [encoding])` @@ -2463,7 +2481,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`string.max`](#stringmax), [`string.ref`](#stringref) +Possible validation errors: [`string.max`](#stringmax), [`string.ref`](#stringref) #### `string.truncate([enabled])` @@ -2485,7 +2503,7 @@ Algorithm](http://en.wikipedia.org/wiki/Luhn_algorithm)). const schema = Joi.string().creditCard(); ``` -💥 Possible validation errors: [`string.creditCard`](#stringcreditcard-1) +Possible validation errors: [`string.creditCard`](#stringcreditcard-1) #### `string.length(limit, [encoding])` @@ -2504,7 +2522,7 @@ const schema = Joi.object({ }); ``` -💥 Possible validation errors: [`string.length`](#stringlength), [`string.ref`](#stringref) +Possible validation errors: [`string.length`](#stringlength), [`string.ref`](#stringref) #### `string.regex(pattern, [name | options])` @@ -2531,7 +2549,7 @@ const invertedNamedSchema = Joi.string().regex(/^[a-z]+$/, { name: 'alpha', inve invertedNamedSchema.validate('lowercase'); // ValidationError: "value" with value "lowercase" matches the inverted alpha pattern ``` -💥 Possible validation errors: [`string.regex.base`](#stringregexbase), [`string.regex.invert.base`](#stringregexinvertbase), [`string.regex.invert.name`](#stringregexinvertname), [`string.regex.name`](#stringregexname) +Possible validation errors: [`string.regex.base`](#stringregexbase), [`string.regex.invert.base`](#stringregexinvertbase), [`string.regex.invert.name`](#stringregexinvertname), [`string.regex.name`](#stringregexname) #### `string.replace(pattern, replacement)` @@ -2558,7 +2576,7 @@ Requires the string value to only contain a-z, A-Z, and 0-9. const schema = Joi.string().alphanum(); ``` -💥 Possible validation errors: [`string.alphanum`](#stringalphanum-1) +Possible validation errors: [`string.alphanum`](#stringalphanum-1) #### `string.token()` @@ -2568,7 +2586,7 @@ Requires the string value to only contain a-z, A-Z, 0-9, and underscore _. const schema = Joi.string().token(); ``` -💥 Possible validation errors: [`string.token`](#stringtoken-1) +Possible validation errors: [`string.token`](#stringtoken-1) #### `string.domain([options])` @@ -2593,7 +2611,7 @@ Requires the string value to be a valid domain name. const schema = Joi.string().domain(); ``` -💥 Possible validation errors: [`string.domain`](#stringdomain) +Possible validation errors: [`string.domain`](#stringdomain) #### `string.email([options])` @@ -2624,7 +2642,7 @@ Requires the string value to be a valid email address. const schema = Joi.string().email(); ``` -💥 Possible validation errors: [`string.email`](#stringemail) +Possible validation errors: [`string.email`](#stringemail) #### `string.ip([options])` @@ -2645,7 +2663,7 @@ const schema = Joi.string().ip({ }); ``` -💥 Possible validation errors: [`string.ip`](#stringip), [`string.ipVersion`](#stringipversion) +Possible validation errors: [`string.ip`](#stringip), [`string.ipVersion`](#stringipversion) #### `string.uri([options])` @@ -2668,7 +2686,7 @@ const schema = Joi.string().uri({ }); ``` -💥 Possible validation errors: [`string.uri`](#stringuri), [`string.uriCustomScheme`](#stringuricustomscheme), [`string.uriRelativeOnly`](#stringurirelativeonly), [`string.domain`](#stringdomain) +Possible validation errors: [`string.uri`](#stringuri), [`string.uriCustomScheme`](#stringuricustomscheme), [`string.uriRelativeOnly`](#stringurirelativeonly), [`string.domain`](#stringdomain) #### `string.guid()` - aliases: `uuid` @@ -2688,7 +2706,7 @@ const schema = Joi.string().guid({ }); ``` -💥 Possible validation errors: [`string.guid`](#stringguid) +Possible validation errors: [`string.guid`](#stringguid) #### `string.hex([options])` @@ -2700,7 +2718,7 @@ Requires the string value to be a valid hexadecimal string. const schema = Joi.string().hex(); ``` -💥 Possible validation errors: [`string.hex`](#stringhex), [`string.hexAlign`](#stringhexalign) +Possible validation errors: [`string.hex`](#stringhex), [`string.hexAlign`](#stringhexalign) #### `string.base64([options])` @@ -2726,7 +2744,7 @@ paddingOptionalSchema.validate('VE9PTUFOWVNFQ1JFVFM'); // No Error paddingOptionalSchema.validate('VE9PTUFOWVNFQ1JFVFM='); // No Error ``` -💥 Possible validation errors: [`string.base64`](#stringbase64) +Possible validation errors: [`string.base64`](#stringbase64) #### `string.dataUri([options])` @@ -2741,7 +2759,7 @@ schema.validate('VE9PTUFOWVNFQ1JFVFM='); // ValidationError: "value" must be a v schema.validate('data:image/png;base64,VE9PTUFOWVNFQ1JFVFM='); // No Error ``` -💥 Possible validation errors: [`string.dataUri`](#stringdatauri) +Possible validation errors: [`string.dataUri`](#stringdatauri) #### `string.hostname()` @@ -2751,7 +2769,7 @@ Requires the string value to be a valid hostname as per [RFC1123](http://tools.i const schema = Joi.string().hostname(); ``` -💥 Possible validation errors: [`string.hostname`](#stringhostname-1) +Possible validation errors: [`string.hostname`](#stringhostname-1) #### `string.normalize([form])` @@ -2768,7 +2786,7 @@ const schema = Joi.string().normalize('NFKC'); // compatibility composition const schema = Joi.string().normalize('NFKD'); // compatibility decomposition ``` -💥 Possible validation errors: [`string.normalize`](#stringnormalize) +Possible validation errors: [`string.normalize`](#stringnormalize) #### `string.lowercase()` @@ -2779,7 +2797,7 @@ will be forced to lowercase. const schema = Joi.string().lowercase(); ``` -💥 Possible validation errors: [`string.lowercase`](#stringlowercase-1) +Possible validation errors: [`string.lowercase`](#stringlowercase-1) #### `string.uppercase()` @@ -2790,7 +2808,7 @@ will be forced to uppercase. const schema = Joi.string().uppercase(); ``` -💥 Possible validation errors: [`string.uppercase`](#stringuppercase-1) +Possible validation errors: [`string.uppercase`](#stringuppercase-1) #### `string.trim([enabled])` @@ -2805,7 +2823,7 @@ const schema = Joi.string().trim(); const schema = Joi.string().trim(false); // disable trim flag ``` -💥 Possible validation errors: [`string.trim`](#stringtrim) +Possible validation errors: [`string.trim`](#stringtrim) #### `string.isoDate()` @@ -2823,7 +2841,7 @@ schema.validate('20181-11-28T18:25:32+00:00'); // ValidationError: must be a val schema.validate(''); // ValidationError: must be a valid 8601 date ``` -💥 Possible validation errors: [`string.isoDate`](#stringisodate-1) +Possible validation errors: [`string.isoDate`](#stringisodate-1) #### `string.isoDuration()` @@ -2836,7 +2854,7 @@ schema.validate('2018-11-28T18:25:32+00:00'); // ValidationError: must be a vali schema.validate(''); // ValidationError: must be a valid ISO 8601 duration ``` -💥 Possible validation errors: [`string.isoDuration`](#stringisoduration-1) +Possible validation errors: [`string.isoDuration`](#stringisoduration-1) ### `symbol` - inherits from `Any` @@ -2851,7 +2869,7 @@ const schema = Joi.symbol().map({ 'foo': Symbol('foo'), 'bar': Symbol('bar') }); schema.validate('foo', (err, value) => { }); ``` -💥 Possible validation errors: [`symbol.base`](#symbolbase) +Possible validation errors: [`symbol.base`](#symbolbase) #### `symbol.map(map)` @@ -2868,7 +2886,7 @@ const schema = Joi.symbol().map([ ]); ``` -💥 Possible validation errors: [`symbol.map`](#symbolmap) +Possible validation errors: [`symbol.map`](#symbolmap) ### `alternatives` - inherits from `Any` @@ -2884,7 +2902,7 @@ const alt = Joi.alternatives().try([Joi.number(), Joi.string()]); // Same as [Joi.number(), Joi.string()] ``` -💥 Possible validation errors: [`alternatives.base`](#alternativesbase), [`alternatives.types`](#alternativestypes), [`alternatives.match`](#alternativesmatch) +Possible validation errors: [`alternatives.base`](#alternativesbase), [`alternatives.types`](#alternativestypes), [`alternatives.match`](#alternativesmatch) #### `alternatives.try(schemas)` @@ -2969,7 +2987,7 @@ const Person = Joi.object({ }); ``` -💥 Possible validation errors: [`lazy.base`](#lazybase), [`lazy.schema`](#lazyschema) +Possible validation errors: [`lazy.base`](#lazybase), [`lazy.schema`](#lazyschema) ## Errors @@ -3004,14 +3022,10 @@ Check if an Error is a Joi `ValidationError` like: #### `alternatives.base` -**Description** - No alternative was found to test against the input due to try criteria. #### `alternatives.types` -**Description** - The provided input did not match any of the allowed types. Additional local context properties: @@ -3023,8 +3037,6 @@ Additional local context properties: #### `alternatives.match` -**Description** - No alternative matched the input due to specific matching rules for at least one of the alternatives. Additional local context properties: @@ -3037,8 +3049,6 @@ Additional local context properties: #### `any.allowOnly` -**Description** - Only some values were allowed, the input didn't match any of them. Additional local context properties: @@ -3050,8 +3060,6 @@ Additional local context properties: #### `any.default` -**Description** - If your [`any.default()`](#anydefaultvalue-description) generator function throws error, you will have it here. Additional local context properties: @@ -3063,8 +3071,6 @@ Additional local context properties: #### `any.failover` -**Description** - If your [`any.failover()`](#anyfailovervalue-description) generator function throws error, you will have it here. Additional local context properties: @@ -3076,8 +3082,6 @@ Additional local context properties: #### `any.empty` -**Description** - When an empty string is found and denied by invalid values. Additional local context properties: @@ -3089,8 +3093,6 @@ Additional local context properties: #### `any.invalid` -**Description** - The value matched a value listed in the invalid values. Additional local context properties: @@ -3102,26 +3104,18 @@ Additional local context properties: #### `any.required` -**Description** - A required value wasn't present. #### `any.unknown` -**Description** - A value was present while it wasn't expected. #### `array.base` -**Description** - The value is not of Array type or could not be cast to an Array from a string. #### `array.excludes` -**Description** - The array contains a value that is part of the exclusion list. Additional local context properties: @@ -3133,8 +3127,6 @@ Additional local context properties: #### `array.includesRequiredBoth` -**Description** - Some values were expected to be present in the array and are missing. This error happens when we have a mix of labelled and unlabelled schemas. Additional local context properties: @@ -3147,8 +3139,6 @@ Additional local context properties: #### `array.includesRequiredKnowns` -**Description** - Some values were expected to be present in the array and are missing. This error happens when we only have labelled schemas. Additional local context properties: @@ -3160,8 +3150,6 @@ Additional local context properties: #### `array.includesRequiredUnknowns` -**Description** - Some values were expected to be present in the array and are missing. This error happens when we only have unlabelled schemas. Additional local context properties: @@ -3173,8 +3161,6 @@ Additional local context properties: #### `array.includes` -**Description** - The value didn't match any of the allowed types for that array. Additional local context properties: @@ -3186,8 +3172,6 @@ Additional local context properties: #### `array.length` -**Description** - The array is not of the expected length. Additional local context properties: @@ -3199,8 +3183,6 @@ Additional local context properties: #### `array.max` -**Description** - The array has more elements than the maximum allowed. Additional local context properties: @@ -3212,8 +3194,6 @@ Additional local context properties: #### `array.min` -**Description** - The array has less elements than the minimum allowed. Additional local context properties: @@ -3225,8 +3205,6 @@ Additional local context properties: #### `array.orderedLength` -**Description** - Given an [`array.ordered()`](#arrayorderedtype), that array has more elements than it should. Additional local context properties: @@ -3239,8 +3217,6 @@ Additional local context properties: #### `array.ref` -**Description** - A reference was used in one of [`array.min()`](#arrayminlimit), [`array.max()`](#arraymaxlimit) or [`array.length()`](#arraylengthlimit) and the value pointed to by that reference in the input is not a valid number for those rules. Additional local context properties: @@ -3250,9 +3226,34 @@ Additional local context properties: } ``` -#### `array.sparse` +#### `array.sort` + +The array did not match the required sort order. + +Additional local context properties: +```ts +{ + order: string, // 'ascending' or 'descending' + by: string // The object key used for comparison +} +``` + +#### `array.sort.mismatching` + +Failed sorting the array due to mismatching item types. + +#### `array.sort.unsupported` -**Description** +Failed sorting the array due to unsupported item types. + +Additional local context properties: +```ts +{ + type: string // The unsupported array item type +} +``` + +#### `array.sparse` An `undefined` value was found in an array that shouldn't be sparse. @@ -3265,8 +3266,6 @@ Additional local context properties: #### `array.unique` -**Description** - A duplicate value was found in an array. Additional local context properties: @@ -3280,8 +3279,6 @@ Additional local context properties: #### `array.hasKnown` -**Description** - The schema on an [`array.has()`](#arrayhas) was not found in the array. This error happens when the schema is labelled. Additional local context properties: @@ -3293,20 +3290,14 @@ Additional local context properties: #### `array.hasUnknown` -**Description** - The schema on an [`array.has()`](#arrayhas) was not found in the array. This error happens when the schema is unlabelled. #### `binary.base` -**Description** - The value is either not a Buffer or could not be cast to a Buffer from a string. #### `binary.length` -**Description** - The buffer was not of the specified length. Additional local context properties: @@ -3318,8 +3309,6 @@ Additional local context properties: #### `binary.max` -**Description** - The buffer contains more bytes than expected. Additional local context properties: @@ -3331,8 +3320,6 @@ Additional local context properties: #### `binary.min` -**Description** - The buffer contains less bytes than expected. Additional local context properties: @@ -3344,8 +3331,6 @@ Additional local context properties: #### `binary.ref` -**Description** - A reference was used in one of [`binary.min()`](#binaryminlimit), [`binary.max()`](#binarymaxlimit), [`binary.length()`](#binarylengthlimit) and the value pointed to by that reference in the input is not a valid number. Additional local context properties: @@ -3357,20 +3342,14 @@ Additional local context properties: #### `boolean.base` -**Description** - The value is either not a boolean or could not be cast to a boolean from one of the truthy or falsy values. #### `date.base` -**Description** - The value is either not a date or could not be cast to a date from a string or a number. #### `date.greater` -**Description** - The date is over the limit that you set. Additional local context properties: @@ -3382,14 +3361,10 @@ Additional local context properties: #### `date.isoDate` -**Description** - The date does not match the ISO 8601 format. #### `date.less` -**Description** - The date is under the limit that you set. Additional local context properties: @@ -3401,8 +3376,6 @@ Additional local context properties: #### `date.max` -**Description** - The date is over or equal to the limit that you set. Additional local context properties: @@ -3414,8 +3387,6 @@ Additional local context properties: #### `date.min` -**Description** - The date is under or equal to the limit that you set. Additional local context properties: @@ -3427,8 +3398,6 @@ Additional local context properties: #### `date.ref` -**Description** - A reference was used in one of [`date.min()`](#datemindate), [`date.max()`](#datemaxdate), [`date.less()`](#datelessdate) or [`date.greater()`](#dategreaterdate) and the value pointed to by that reference in the input is not a valid date. Additional local context properties: @@ -3440,26 +3409,18 @@ Additional local context properties: #### `date.strict` -**Description** - Occurs when the input is not a Date type and `convert` is disabled. #### `date.timestamp.javascript` -**Description** - Failed to be converted from a string or a number to a date as JavaScript timestamp. #### `date.timestamp.unix` -**Description** - Failed to be converted from a string or a number to a date as Unix timestamp. #### `function.arity` -**Description** - The number of arguments for the function doesn't match the required number. Additional local context properties: @@ -3471,20 +3432,14 @@ Additional local context properties: #### `function.base` -**Description** - The input is not a function. #### `function.class` -**Description** - The input is not a JavaScript class. #### `function.maxArity` -**Description** - The number of arguments for the function is over the required number. Additional local context properties: @@ -3496,8 +3451,6 @@ Additional local context properties: #### `function.minArity` -**Description** - The number of arguments for the function is under the required number. Additional local context properties: @@ -3509,14 +3462,10 @@ Additional local context properties: #### `lazy.base` -**Description** - The lazy function is not set. #### `lazy.schema` -**Description** - The lazy function didn't return a joi schema. Additional local context properties: @@ -3528,14 +3477,10 @@ Additional local context properties: #### `number.base` -**Description** - The value is not a number or could not be cast to a number. #### `number.greater` -**Description** - The number is lower or equal to the limit that you set. Additional local context properties: @@ -3547,14 +3492,10 @@ Additional local context properties: #### `number.integer` -**Description** - The number is not a valid integer. #### `number.less` -**Description** - The number is higher or equal to the limit that you set. Additional local context properties: @@ -3566,8 +3507,6 @@ Additional local context properties: #### `number.max` -**Description** - The number is higher than the limit that you set. Additional local context properties: @@ -3579,8 +3518,6 @@ Additional local context properties: #### `number.min` -**Description** - The number is lower than the limit that you set. Additional local context properties: @@ -3592,8 +3529,6 @@ Additional local context properties: #### `number.multiple` -**Description** - The number could not be divided by the multiple you provided. Additional local context properties: @@ -3605,26 +3540,18 @@ Additional local context properties: #### `number.negative` -**Description** - The number was positive. #### `number.port` -**Description** - The number didn't look like a port number. #### `number.positive` -**Description** - The number was negative. #### `number.precision` -**Description** - The number didn't have the required precision. Additional local context properties: @@ -3636,20 +3563,14 @@ Additional local context properties: #### `number.ref` -**Description** - A reference was used in one of [`number.min()`](#numberminlimit), [`number.max()`](#numbermaxlimit), [`number.less()`](#numberlesslimit), [`number.greater()`](#numbergreaterlimit), or [`number.multiple()`](#numbermultiplebase) and the value pointed to by that reference in the input is not a valid number. #### `number.unsafe` -**Description** - The number is not within the safe range of JavaScript numbers. #### `object.allowUnknown` -**Description** - An unexpected property was found in the object. Additional local context properties: @@ -3661,8 +3582,6 @@ Additional local context properties: #### `object.and` -**Description** - The AND condition between the properties you specified was not satisfied in that object. Additional local context properties: @@ -3677,8 +3596,6 @@ Additional local context properties: #### `object.assert` -**Description** - The schema on an [`object.assert()`](#objectassertref-schema-message) failed to validate. Additional local context properties: @@ -3691,14 +3608,10 @@ Additional local context properties: #### `object.base` -**Description** - The value is not of object type or could not be cast to an object from a string. #### `object.length` -**Description** - The number of keys for this object is not of the expected length. Additional local context properties: @@ -3710,8 +3623,6 @@ Additional local context properties: #### `object.max` -**Description** - The number of keys for this object is over or equal to the limit that you set. Additional local context properties: @@ -3723,8 +3634,6 @@ Additional local context properties: #### `object.min` -**Description** - The number of keys for this object is under or equal to the limit that you set. Additional local context properties: @@ -3736,8 +3645,6 @@ Additional local context properties: #### `object.missing` -**Description** - The OR or XOR condition between the properties you specified was not satisfied in that object, none of it were set. Additional local context properties: @@ -3750,8 +3657,6 @@ Additional local context properties: #### `object.nand` -**Description** - The NAND condition between the properties you specified was not satisfied in that object. Additional local context properties: @@ -3766,8 +3671,6 @@ Additional local context properties: #### `object.ref` -**Description** - A reference was used in one of [`object.min()`](#objectminlimit), [`object.max()`](#objectmaxlimit), [`object.length()`](#objectlengthlimit) and the value pointed to by that reference in the input is not a valid number. Additional local context properties: @@ -3779,14 +3682,10 @@ Additional local context properties: #### `object.refType` -**Description** - The object is not a [`Joi.ref()`](#refkey-options). #### `object.rename.multiple` -**Description** - Another rename was already done to the same target property. Additional local context properties: @@ -3800,8 +3699,6 @@ Additional local context properties: #### `object.rename.override` -**Description** - The target property already exists and you disallowed overrides. Additional local context properties: @@ -3815,8 +3712,6 @@ Additional local context properties: #### `object.schema` -**Description** - The object was not a joi schema. Additional local context properties: @@ -3828,8 +3723,6 @@ Additional local context properties: #### `object.type` -**Description** - The object is not of the type you specified. Additional local context properties: @@ -3841,8 +3734,6 @@ Additional local context properties: #### `object.with` -**Description** - Property that should have been present at the same time as another one was missing. Additional local context properties: @@ -3857,8 +3748,6 @@ Additional local context properties: #### `object.without` -**Description** - Property that should have been absent at the same time as another one was present. Additional local context properties: @@ -3873,8 +3762,6 @@ Additional local context properties: #### `object.xor` -**Description** - The XOR condition between the properties you specified was not satisfied in that object. Additional local context properties: @@ -3887,8 +3774,6 @@ Additional local context properties: #### `object.oxor` -**Description** - The optional XOR condition between the properties you specified was not satisfied in that object. Additional local context properties: @@ -3901,44 +3786,30 @@ Additional local context properties: #### `string.alphanum` -**Description** - The string doesn't only contain alphanumeric characters. #### `string.base64` -**Description** - The string isn't a valid base64 string. #### `string.base` -**Description** - The input is not a string. #### `string.creditCard` -**Description** - The string is not a valid credit card number. #### `string.dataUri` -**Description** - The string is not a valid data URI. #### `string.domain` -**Description** - The string is not a valid domain name. #### `string.email` -**Description** - The string is not a valid e-mail. Additional local context properties: @@ -3950,32 +3821,22 @@ Additional local context properties: #### `string.guid` -**Description** - The string is not a valid GUID. #### `string.hexAlign` -**Description** - The string contains hexadecimal characters but they are not byte-aligned. #### `string.hex` -**Description** - The string is not a valid hexadecimal string. #### `string.hostname` -**Description** - The string is not a valid hostname. #### `string.ipVersion` -**Description** - The string is not a valid IP address considering the provided constraints. Additional local context properties: @@ -3988,8 +3849,6 @@ Additional local context properties: #### `string.ip` -**Description** - The string is not a valid IP address. Additional local context properties: @@ -4001,20 +3860,14 @@ Additional local context properties: #### `string.isoDate` -**Description** - The string is not a valid ISO date string. #### `string.isoDuration` -**Description** - The string must be a valid ISO 8601 duration. #### `string.length` -**Description** - The string is not of the expected length. Additional local context properties: @@ -4027,14 +3880,10 @@ Additional local context properties: #### `string.lowercase` -**Description** - The string isn't all lower-cased. #### `string.max` -**Description** - The string is larger than expected. Additional local context properties: @@ -4047,8 +3896,6 @@ Additional local context properties: #### `string.min` -**Description** - The string is smaller than expected. Additional local context properties: @@ -4061,8 +3908,6 @@ Additional local context properties: #### `string.normalize` -**Description** - The string isn't valid in regards of the normalization form expected. Additional local context properties: @@ -4074,8 +3919,6 @@ Additional local context properties: #### `string.ref` -**Description** - A reference was used in one of [`string.min()`](#stringminlimit-encoding), [`string.max()`](#stringmaxlimit-encoding) or [`string.length()`](#stringlengthlimit-encoding) and the value pointed to by that reference in the input is not a valid number for those rules. Additional local context properties: @@ -4087,8 +3930,6 @@ Additional local context properties: #### `string.regex.base` -**Description** - The string didn't match the regular expression. Additional local context properties: @@ -4101,8 +3942,6 @@ Additional local context properties: #### `string.regex.name` -**Description** - The string didn't match the named regular expression. Additional local context properties: @@ -4115,8 +3954,6 @@ Additional local context properties: #### `string.regex.invert.base` -**Description** - The string matched the regular expression while it shouldn't. Additional local context properties: @@ -4129,8 +3966,6 @@ Additional local context properties: #### `string.regex.invert.name` -**Description** - The string matched the named regular expression while it shouldn't. Additional local context properties: @@ -4143,32 +3978,22 @@ Additional local context properties: #### `string.token` -**Description** - The string isn't a token. #### `string.trim` -**Description** - The string contains whitespaces around it. #### `string.uppercase` -**Description** - The string isn't all upper-cased. #### `string.uri` -**Description** - The string isn't a valid URI. #### `string.uriCustomScheme` -**Description** - The string isn't a valid URI considering the custom schemes. Additional local context properties: @@ -4180,20 +4005,14 @@ Additional local context properties: #### `string.uriRelativeOnly` -**Description** - The string is a valid relative URI. #### `symbol.base` -**Description** - The input is not a Symbol. #### `symbol.map` -**Description** - The input is not a Symbol or could not be converted to one. diff --git a/docs/check-errors-list.js b/docs/check-errors-list.js index 1794eb235..49d133378 100755 --- a/docs/check-errors-list.js +++ b/docs/check-errors-list.js @@ -45,15 +45,13 @@ internals.updateTable = function () { const missing = internals.checkMissing(titles); if (missing.length) { console.log(`Missing: -${missing.map((m) => `#### ${m} +${missing.map((m) => `#### \`${m}\` -**Description** + -**Context** +Additional local context properties: \`\`\`ts { - key: string, // Last element of the path accessing the value, \`undefined\` if at the root - label: string, // Label if defined, otherwise it's the key ... } \`\`\` diff --git a/lib/cast.js b/lib/cast.js index 499589065..4d3cd66e6 100755 --- a/lib/cast.js +++ b/lib/cast.js @@ -75,7 +75,7 @@ internals.appendPath = function (Joi, config) { }; -exports.ref = function (id) { +exports.ref = function (id, options) { - return Ref.isRef(id) ? id : new Ref(id); + return Ref.isRef(id) ? id : new Ref(id, options); }; diff --git a/lib/language.js b/lib/language.js index cee8ab3ab..6b640690b 100755 --- a/lib/language.js +++ b/lib/language.js @@ -33,6 +33,9 @@ exports.errors = { 'array.length': 'must contain {{#limit}} items', 'array.orderedLength': 'must contain at most {{#limit}} items', 'array.ref': 'references "{{#ref}}" which is not a positive integer', + 'array.sort': 'must be sorted in {#order} order by {{#by}}', + 'array.sort.mismatching': 'cannot be sorted due to mismatching types', + 'array.sort.unsupported': 'cannot be sorted due to unsupported type {#type}', 'array.sparse': 'must not be a sparse array item', 'array.unique': 'contains a duplicate value', diff --git a/lib/types/array.js b/lib/types/array.js index 10d18fb38..45b8252cf 100755 --- a/lib/types/array.js +++ b/lib/types/array.js @@ -39,6 +39,15 @@ internals.Array = class extends Any { catch (ignoreErr) { } } + if (!Array.isArray(result.value)) { + return result; + } + + const sort = this._uniqueRules.get('sort'); + if (sort) { + return internals.sort(this, result.value, sort.args.options, state, prefs); + } + return result; } @@ -55,7 +64,7 @@ internals.Array = class extends Any { } if (this._uniqueRules.has('items')) { - value = value.slice(0); // Clone the array so that we don't modify the original + value = value.slice(); // Clone the array so that we don't modify the original } return { value }; @@ -228,6 +237,22 @@ internals.Array = class extends Any { return this._rule('unique', rule); } + sort(options = {}) { + + Common.assertOptions(options, ['by', 'order']); + + const settings = { + order: options.order || 'ascending' + }; + + if (options.by) { + settings.by = Cast.ref(options.by, { ancestor: 0 }); + Hoek.assert(!settings.by.settings.ancestor, 'Cannot sort by ancestor'); + } + + return this._rule('sort', { args: { options: settings }, convert: true }); + } + sparse(enabled) { const value = enabled === undefined ? true : !!enabled; @@ -539,6 +564,22 @@ internals.Array.prototype._rules = { return helpers.error('array.' + alias, { limit: args.limit, value }); }, + sort: function (value, { error, state, prefs, schema }, { options }) { + + const { value: sorted, errors } = internals.sort(schema, value, options, state, prefs); + if (errors) { + return errors; + } + + for (let i = 0; i < value.length; ++i) { + if (value[i] !== sorted[i]) { + return error('array.sort', { order: options.order, by: options.by ? options.by.key : 'value' }); + } + } + + return value; + }, + unique: function (value, { state, error, schema }, { settings }, { path }) { const found = { @@ -637,4 +678,80 @@ internals.validateSingle = function (type, obj) { }; +internals.sort = function (schema, value, settings, state, prefs) { + + const order = settings.order === 'ascending' ? 1 : -1; + const aFirst = -1 * order; + const bFirst = order; + + const sort = (a, b) => { + + let compare = internals.compare(a, b, aFirst, bFirst); + if (compare !== null) { + return compare; + } + + if (settings.by) { + a = settings.by.resolve(a, state, prefs); + b = settings.by.resolve(b, state, prefs); + } + + compare = internals.compare(a, b, aFirst, bFirst); + if (compare !== null) { + return compare; + } + + const type = typeof a; + if (type !== typeof b) { + throw schema.createError('array.sort.mismatching', value, null, state, prefs); + } + + if (type !== 'number' && + type !== 'string') { + + throw schema.createError('array.sort.unsupported', value, { type }, state, prefs); + } + + if (type === 'number') { + return (a - b) * order; + } + + return a < b ? aFirst : bFirst; + }; + + try { + return { value: value.slice().sort(sort) }; + } + catch (err) { + return { errors: err }; + } +}; + + +internals.compare = function (a, b, aFirst, bFirst) { + + if (a === b) { + return 0; + } + + if (a === undefined) { + return 1; // Always last regardless of sort order + } + + if (b === undefined) { + return -1; // Always last regardless of sort order + } + + if (a === null) { + return bFirst; + } + + if (b === null) { + return aFirst; + } + + return null; +}; + + module.exports = new internals.Array(); diff --git a/test/types/array.js b/test/types/array.js index 980ce735d..c69eaddcc 100755 --- a/test/types/array.js +++ b/test/types/array.js @@ -83,21 +83,84 @@ describe('array', () => { }]); }); - describe('items()', () => { + describe('describe()', () => { - it('converts members', async () => { + it('returns an empty description when no rules are applied', () => { - const schema = Joi.array().items(Joi.number()); - const input = ['1', '2', '3']; - const value = await schema.validate(input); - expect(value).to.equal([1, 2, 3]); + const schema = Joi.array(); + const desc = schema.describe(); + expect(desc).to.equal({ + type: 'array', + flags: { sparse: false } + }); }); - it('shows path to errors in array items', () => { + it('returns an updated description when sparse rule is applied', () => { + + const schema = Joi.array().sparse(); + const desc = schema.describe(); + expect(desc).to.equal({ + type: 'array', + flags: { sparse: true } + }); + }); + + it('returns an items array only if items are specified', () => { + + const schema = Joi.array().items().max(5); + const desc = schema.describe(); + expect(desc.items).to.not.exist(); + }); + + it('returns a recursively defined array of items when specified', () => { + + const schema = Joi.array() + .items(Joi.number(), Joi.string()) + .items(Joi.boolean().forbidden()) + .ordered(Joi.number(), Joi.string()) + .ordered(Joi.string().required()); + const desc = schema.describe(); + expect(desc.items).to.have.length(3); + expect(desc).to.equal({ + type: 'array', + flags: { sparse: false }, + orderedItems: [ + { type: 'number', invalids: [Infinity, -Infinity], flags: { unsafe: false } }, + { type: 'string', invalids: [''] }, + { type: 'string', invalids: [''], flags: { presence: 'required' } } + ], + items: [ + { type: 'number', invalids: [Infinity, -Infinity], flags: { unsafe: false } }, + { type: 'string', invalids: [''] }, + { type: 'boolean', flags: { presence: 'forbidden', insensitive: true }, truthy: [true], falsy: [false] } + ] + }); + }); + + it('describes an array with array items', () => { + + const schema = Joi.array().items(Joi.array()); + const desc = schema.describe(); + expect(desc).to.equal({ + type: 'array', + flags: { sparse: false }, + items: [ + { + type: 'array', + flags: { sparse: false } + } + ] + }); + }); + }); + + describe('has()', () => { + + it('shows path to errors in schema', () => { expect(() => { - Joi.array().items({ + Joi.array().has({ a: { b: { c: { @@ -106,139 +169,336 @@ describe('array', () => { } } }); - }).to.throw(Error, 'Invalid schema content: (0.a.b.c.d)'); + }).to.throw(Error, 'Invalid schema content: (a.b.c.d)'); + }); - expect(() => { + it('shows errors in schema', () => { - Joi.array().items({ foo: 'bar' }, undefined); - }).to.throw(Error, 'Invalid schema content: (1)'); + expect(() => Joi.array().has(undefined)).to.throw(Error, 'Invalid schema content: '); }); - it('allows zero size', async () => { + it('works with object.assert', () => { - const schema = Joi.object({ - test: Joi.array().items(Joi.object({ - foo: Joi.string().required() - })) - }); - const input = { test: [] }; + const schema = Joi.array().items( + Joi.object().keys({ + a: { + b: Joi.string(), + c: Joi.number() + }, + d: { + e: Joi.any() + } + }) + ).has(Joi.object().assert('d.e', Joi.ref('a.c'), 'equal to a.c')); - await schema.validate(input); + Helper.validate(schema, [ + [[{ a: { b: 'x', c: 5 }, d: { e: 5 } }], true] + ]); }); - it('returns the first error when only one inclusion', async () => { - - const schema = Joi.object({ - test: Joi.array().items(Joi.object({ - foo: Joi.string().required() - })) - }); - const input = { test: [{ foo: 'a' }, { bar: 2 }] }; + it('does not throw if assertion passes', () => { - const err = await expect(schema.validate(input)).to.reject(); - expect(err.message).to.equal('"test[1].foo" is required'); - expect(err.details).to.equal([{ - message: '"test[1].foo" is required', - path: ['test', 1, 'foo'], - type: 'any.required', - context: { label: 'test[1].foo', key: 'foo' } - }]); + const schema = Joi.array().has(Joi.string()); + Helper.validate(schema, [ + [['foo'], true] + ]); }); - it('validates multiple types added in two calls', () => { - - const schema = Joi.array() - .items(Joi.number()) - .items(Joi.string()); + it('throws with proper message if assertion fails on unknown schema', () => { + const schema = Joi.array().has(Joi.string()); Helper.validate(schema, [ - [[1, 2, 3], true], - [[50, 100, 1000], true], - [[1, 'a', 5, 10], true], - [['joi', 'everydaylowprices', 5000], true] + [[0], false, null, { + message: '"value" does not contain at least one required match', + details: [{ + message: '"value" does not contain at least one required match', + path: [], + type: 'array.hasUnknown', + context: { label: 'value', value: [0] } + }] + }] ]); }); - it('validates multiple types with stripUnknown', () => { - - const schema = Joi.array().items(Joi.number(), Joi.string()).prefs({ stripUnknown: true }); + it('throws with proper message if assertion fails on known schema', () => { + const schema = Joi.array().has(Joi.string().label('foo')); Helper.validate(schema, [ - [[1, 2, 'a'], true, null, [1, 2, 'a']], - [[1, { foo: 'bar' }, 'a', 2], false, null, { - message: '"[1]" does not match any of the allowed types', + [[0], false, null, { + message: '"value" does not contain at least one required match for type "foo"', details: [{ - context: { - key: 1, - label: '[1]', - pos: 1, - value: { foo: 'bar' } - }, - message: '"[1]" does not match any of the allowed types', - path: [1], - type: 'array.includes' + message: '"value" does not contain at least one required match for type "foo"', + path: [], + type: 'array.hasKnown', + context: { label: 'value', patternLabel: 'foo', value: [0] } }] }] ]); }); - it('validates multiple types with stripUnknown (as an object)', () => { - - const schema = Joi.array().items(Joi.number(), Joi.string()).prefs({ stripUnknown: { arrays: true, objects: false } }); + it('shows correct path for error', () => { + const schema = Joi.object({ + arr: Joi.array().has(Joi.string()) + }); Helper.validate(schema, [ - [[1, 2, 'a'], true, null, [1, 2, 'a']], - [[1, { foo: 'bar' }, 'a', 2], true, null, [1, 'a', 2]] + [{ arr: [0] }, false, null, { + message: '"arr" does not contain at least one required match', + details: [{ + message: '"arr" does not contain at least one required match', + path: ['arr'], + type: 'array.hasUnknown', + context: { label: 'arr', key: 'arr', value: [0] } + }] + }] ]); }); - it('allows forbidden to restrict values', async () => { - - const schema = Joi.array().items(Joi.string().valid('four').forbidden(), Joi.string()); - const input = ['one', 'two', 'three', 'four']; + it('supports nested arrays', () => { - const err = await expect(schema.validate(input)).to.reject('"[3]" contains an excluded value'); - expect(err.details).to.equal([{ - message: '"[3]" contains an excluded value', - path: [3], - type: 'array.excludes', - context: { pos: 3, value: 'four', label: '[3]', key: 3 } - }]); + const schema = Joi.object({ + arr: Joi.array().items( + Joi.object({ foo: Joi.array().has(Joi.string()) }) + ) + }); + Helper.validate(schema, [ + [{ arr: [{ foo: ['bar'] }] }, true] + ]); }); - it('allows forbidden to restrict values (ref)', async () => { + it('supports peer item references', () => { const schema = Joi.object({ - array: Joi.array().items(Joi.valid(Joi.ref('...value')).forbidden(), Joi.string()), - value: Joi.string().required() + array: Joi.array() + .items(Joi.number()) + .has(Joi.number().greater(Joi.ref('..0'))) }); - const input = { - array: ['one', 'two', 'three', 'four'], - value: 'four' - }; - - const err = await expect(schema.validate(input)).to.reject('"array[3]" contains an excluded value'); - expect(err.details).to.equal([{ - message: '"array[3]" contains an excluded value', - path: ['array', 3], - type: 'array.excludes', - context: { pos: 3, value: 'four', label: 'array[3]', key: 3 } - }]); + Helper.validate(schema, [ + [{ array: [10, 1, 11, 5] }, true], + [{ array: [10, 1, 2, 5, 12] }, true], + [{ array: [10, 1, 2, 5, 1] }, false, null, { + message: '"array" does not contain at least one required match', + details: [{ + message: '"array" does not contain at least one required match', + path: ['array'], + type: 'array.hasUnknown', + context: { label: 'array', key: 'array', value: [10, 1, 2, 5, 1] } + }] + }] + ]); }); - it('process referenced property first when referenced by an item rule', async () => { + it('provides accurate error message for nested arrays', () => { const schema = Joi.object({ - array: Joi.array().items(Joi.valid(Joi.ref('...value')).forbidden(), Joi.number()), - value: Joi.number().required() + arr: Joi.array().items( + Joi.object({ foo: Joi.array().has(Joi.string()) }) + ) }); + Helper.validate(schema, [ + [{ arr: [{ foo: [0] }] }, false, null, { + message: '"arr[0].foo" does not contain at least one required match', + details: [{ + message: '"arr[0].foo" does not contain at least one required match', + path: ['arr', 0, 'foo'], + type: 'array.hasUnknown', + context: { label: 'arr[0].foo', key: 'foo', value: [0] } + }] + }] + ]); + }); + + it('handles multiple assertions', () => { + + const schema = Joi.array().has(Joi.string()).has(Joi.number()); + Helper.validate(schema, [ + [['foo', 0], true] + ]); + + Helper.validate(schema, [ + [['foo'], false, null, { + message: '"value" does not contain at least one required match', + details: [{ + message: '"value" does not contain at least one required match', + path: [], + type: 'array.hasUnknown', + context: { label: 'value', value: ['foo'] } + }] + }] + ]); + }); + + it('describes the pattern schema', () => { + + const schema = Joi.array().has(Joi.string()).has(Joi.number()); + expect(schema.describe()).to.equal({ + type: 'array', + flags: { sparse: false }, + rules: [ + { name: 'has', arg: { type: 'string', invalids: [''] } }, + { name: 'has', arg: { type: 'number', flags: { unsafe: false }, invalids: [Infinity, -Infinity] } } + ] + }); + }); + }); + + describe('items()', () => { + + it('converts members', async () => { + + const schema = Joi.array().items(Joi.number()); + const input = ['1', '2', '3']; + const value = await schema.validate(input); + expect(value).to.equal([1, 2, 3]); + }); + + it('shows path to errors in array items', () => { + + expect(() => { + + Joi.array().items({ + a: { + b: { + c: { + d: undefined + } + } + } + }); + }).to.throw(Error, 'Invalid schema content: (0.a.b.c.d)'); + + expect(() => { + + Joi.array().items({ foo: 'bar' }, undefined); + }).to.throw(Error, 'Invalid schema content: (1)'); + }); + + it('allows zero size', async () => { + + const schema = Joi.object({ + test: Joi.array().items(Joi.object({ + foo: Joi.string().required() + })) + }); + const input = { test: [] }; + + await schema.validate(input); + }); + + it('returns the first error when only one inclusion', async () => { + + const schema = Joi.object({ + test: Joi.array().items(Joi.object({ + foo: Joi.string().required() + })) + }); + const input = { test: [{ foo: 'a' }, { bar: 2 }] }; + + const err = await expect(schema.validate(input)).to.reject(); + expect(err.message).to.equal('"test[1].foo" is required'); + expect(err.details).to.equal([{ + message: '"test[1].foo" is required', + path: ['test', 1, 'foo'], + type: 'any.required', + context: { label: 'test[1].foo', key: 'foo' } + }]); + }); + + it('validates multiple types added in two calls', () => { + + const schema = Joi.array() + .items(Joi.number()) + .items(Joi.string()); + + Helper.validate(schema, [ + [[1, 2, 3], true], + [[50, 100, 1000], true], + [[1, 'a', 5, 10], true], + [['joi', 'everydaylowprices', 5000], true] + ]); + }); + + it('validates multiple types with stripUnknown', () => { + + const schema = Joi.array().items(Joi.number(), Joi.string()).prefs({ stripUnknown: true }); + + Helper.validate(schema, [ + [[1, 2, 'a'], true, null, [1, 2, 'a']], + [[1, { foo: 'bar' }, 'a', 2], false, null, { + message: '"[1]" does not match any of the allowed types', + details: [{ + context: { + key: 1, + label: '[1]', + pos: 1, + value: { foo: 'bar' } + }, + message: '"[1]" does not match any of the allowed types', + path: [1], + type: 'array.includes' + }] + }] + ]); + }); + + it('validates multiple types with stripUnknown (as an object)', () => { + + const schema = Joi.array().items(Joi.number(), Joi.string()).prefs({ stripUnknown: { arrays: true, objects: false } }); + + Helper.validate(schema, [ + [[1, 2, 'a'], true, null, [1, 2, 'a']], + [[1, { foo: 'bar' }, 'a', 2], true, null, [1, 'a', 2]] + ]); + }); + + it('allows forbidden to restrict values', async () => { + + const schema = Joi.array().items(Joi.string().valid('four').forbidden(), Joi.string()); + const input = ['one', 'two', 'three', 'four']; + + const err = await expect(schema.validate(input)).to.reject('"[3]" contains an excluded value'); + expect(err.details).to.equal([{ + message: '"[3]" contains an excluded value', + path: [3], + type: 'array.excludes', + context: { pos: 3, value: 'four', label: '[3]', key: 3 } + }]); + }); + + it('allows forbidden to restrict values (ref)', async () => { + + const schema = Joi.object({ + array: Joi.array().items(Joi.valid(Joi.ref('...value')).forbidden(), Joi.string()), + value: Joi.string().required() + }); + + const input = { + array: ['one', 'two', 'three', 'four'], + value: 'four' + }; + + const err = await expect(schema.validate(input)).to.reject('"array[3]" contains an excluded value'); + expect(err.details).to.equal([{ + message: '"array[3]" contains an excluded value', + path: ['array', 3], + type: 'array.excludes', + context: { pos: 3, value: 'four', label: 'array[3]', key: 3 } + }]); + }); + + it('process referenced property first when referenced by an item rule', async () => { + + const schema = Joi.object({ + array: Joi.array().items(Joi.valid(Joi.ref('...value')).forbidden(), Joi.number()), + value: Joi.number().required() + }); + + const input = { + array: [1, 2, 3, 4], + value: '4' + }; - const input = { - array: [1, 2, 3, 4], - value: '4' - }; - const err = await expect(schema.validate(input)).to.reject('"array[3]" contains an excluded value'); expect(err.details).to.equal([{ message: '"array[3]" contains an excluded value', @@ -372,19 +632,19 @@ describe('array', () => { }); }); - describe('min()', () => { + describe('length()', () => { it('validates array size', () => { - const schema = Joi.array().min(2); + const schema = Joi.array().length(2); Helper.validate(schema, [ [[1, 2], true], [[1], false, null, { - message: '"value" must contain at least 2 items', + message: '"value" must contain 2 items', details: [{ - message: '"value" must contain at least 2 items', + message: '"value" must contain 2 items', path: [], - type: 'array.min', + type: 'array.length', context: { limit: 2, value: [1], label: 'value' } }] }] @@ -393,35 +653,34 @@ describe('array', () => { it('overrides rule when called multiple times', () => { - const schema = Joi.array().min(2).min(1); + const schema = Joi.array().length(2).length(1); Helper.validate(schema, [ - [[1, 2], true], - [[1], true] + [[1], true], + [[1, 2], false, null, { + message: '"value" must contain 1 items', + details: [{ + message: '"value" must contain 1 items', + path: [], + type: 'array.length', + context: { limit: 1, value: [1, 2], label: 'value' } + }] + }] ]); }); it('throws when limit is not a number', () => { - expect(() => { - - Joi.array().min('a'); - }).to.throw('limit must be a positive integer or reference'); + expect(() => Joi.array().length('a')).to.throw('limit must be a positive integer or reference'); }); it('throws when limit is not an integer', () => { - expect(() => { - - Joi.array().min(1.2); - }).to.throw('limit must be a positive integer or reference'); + expect(() => Joi.array().length(1.2)).to.throw('limit must be a positive integer or reference'); }); it('throws when limit is negative', () => { - expect(() => { - - Joi.array().min(-1); - }).to.throw('limit must be a positive integer or reference'); + expect(() => Joi.array().length(-1)).to.throw('limit must be a positive integer or reference'); }); it('validates array size when a reference', () => { @@ -429,7 +688,7 @@ describe('array', () => { const ref = Joi.ref('limit'); const schema = Joi.object().keys({ limit: Joi.any(), - arr: Joi.array().min(ref) + arr: Joi.array().length(ref) }); Helper.validate(schema, [ [{ @@ -440,11 +699,11 @@ describe('array', () => { limit: 2, arr: [1] }, false, null, { - message: '"arr" must contain at least ref:limit items', + message: '"arr" must contain ref:limit items', details: [{ - message: '"arr" must contain at least ref:limit items', + message: '"arr" must contain ref:limit items', path: ['arr'], - type: 'array.min', + type: 'array.length', context: { limit: ref, value: [1], label: 'arr', key: 'arr' } }] }] @@ -457,7 +716,7 @@ describe('array', () => { limit: Joi.any(), arr: Joi.array(), arr2: Joi.when('arr', { - is: Joi.array().min(Joi.ref('limit')), + is: Joi.array().length(Joi.ref('limit')), then: Joi.array() }) }); @@ -476,7 +735,7 @@ describe('array', () => { const ref = Joi.ref('limit'); const schema = Joi.object().keys({ limit: Joi.any(), - arr: Joi.array().min(ref) + arr: Joi.array().length(ref) }); Helper.validate(schema, [ @@ -635,19 +894,19 @@ describe('array', () => { }); }); - describe('length()', () => { + describe('min()', () => { it('validates array size', () => { - const schema = Joi.array().length(2); + const schema = Joi.array().min(2); Helper.validate(schema, [ [[1, 2], true], [[1], false, null, { - message: '"value" must contain 2 items', + message: '"value" must contain at least 2 items', details: [{ - message: '"value" must contain 2 items', + message: '"value" must contain at least 2 items', path: [], - type: 'array.length', + type: 'array.min', context: { limit: 2, value: [1], label: 'value' } }] }] @@ -656,34 +915,35 @@ describe('array', () => { it('overrides rule when called multiple times', () => { - const schema = Joi.array().length(2).length(1); + const schema = Joi.array().min(2).min(1); Helper.validate(schema, [ - [[1], true], - [[1, 2], false, null, { - message: '"value" must contain 1 items', - details: [{ - message: '"value" must contain 1 items', - path: [], - type: 'array.length', - context: { limit: 1, value: [1, 2], label: 'value' } - }] - }] + [[1, 2], true], + [[1], true] ]); }); it('throws when limit is not a number', () => { - expect(() => Joi.array().length('a')).to.throw('limit must be a positive integer or reference'); + expect(() => { + + Joi.array().min('a'); + }).to.throw('limit must be a positive integer or reference'); }); it('throws when limit is not an integer', () => { - expect(() => Joi.array().length(1.2)).to.throw('limit must be a positive integer or reference'); + expect(() => { + + Joi.array().min(1.2); + }).to.throw('limit must be a positive integer or reference'); }); it('throws when limit is negative', () => { - expect(() => Joi.array().length(-1)).to.throw('limit must be a positive integer or reference'); + expect(() => { + + Joi.array().min(-1); + }).to.throw('limit must be a positive integer or reference'); }); it('validates array size when a reference', () => { @@ -691,7 +951,7 @@ describe('array', () => { const ref = Joi.ref('limit'); const schema = Joi.object().keys({ limit: Joi.any(), - arr: Joi.array().length(ref) + arr: Joi.array().min(ref) }); Helper.validate(schema, [ [{ @@ -702,11 +962,11 @@ describe('array', () => { limit: 2, arr: [1] }, false, null, { - message: '"arr" must contain ref:limit items', + message: '"arr" must contain at least ref:limit items', details: [{ - message: '"arr" must contain ref:limit items', + message: '"arr" must contain at least ref:limit items', path: ['arr'], - type: 'array.length', + type: 'array.min', context: { limit: ref, value: [1], label: 'arr', key: 'arr' } }] }] @@ -719,7 +979,7 @@ describe('array', () => { limit: Joi.any(), arr: Joi.array(), arr2: Joi.when('arr', { - is: Joi.array().length(Joi.ref('limit')), + is: Joi.array().min(Joi.ref('limit')), then: Joi.array() }) }); @@ -738,7 +998,7 @@ describe('array', () => { const ref = Joi.ref('limit'); const schema = Joi.object().keys({ limit: Joi.any(), - arr: Joi.array().length(ref) + arr: Joi.array().min(ref) }); Helper.validate(schema, [ @@ -770,13 +1030,29 @@ describe('array', () => { }); }); - describe('has()', () => { + describe('options()', () => { - it('shows path to errors in schema', () => { + it('ignores stripUnknown when true', async () => { + + const schema = Joi.array().items(Joi.string()).prefs({ stripUnknown: true }); + await expect(schema.validate(['one', 'two', 3, 4, true, false])).to.reject('"[2]" must be a string'); + }); + + it('respects stripUnknown (as an object)', async () => { + + const schema = Joi.array().items(Joi.string()).prefs({ stripUnknown: { arrays: true, objects: false } }); + const value = await schema.validate(['one', 'two', 3, 4, true, false]); + expect(value).to.equal(['one', 'two']); + }); + }); + + describe('ordered()', () => { + + it('shows path to errors in array ordered items', () => { expect(() => { - Joi.array().has({ + Joi.array().ordered({ a: { b: { c: { @@ -785,1169 +1061,839 @@ describe('array', () => { } } }); - }).to.throw(Error, 'Invalid schema content: (a.b.c.d)'); + }).to.throw(Error, 'Invalid schema content: (0.a.b.c.d)'); + + expect(() => { + + Joi.array().ordered({ foo: 'bar' }, undefined); + }).to.throw(Error, 'Invalid schema content: (1)'); }); - it('shows errors in schema', () => { + it('validates input against items in order', async () => { - expect(() => Joi.array().has(undefined)).to.throw(Error, 'Invalid schema content: '); + const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required()); + const input = ['s1', 2]; + const value = await schema.validate(input); + expect(value).to.equal(['s1', 2]); }); - it('works with object.assert', () => { + it('validates input with optional item', async () => { - const schema = Joi.array().items( - Joi.object().keys({ - a: { - b: Joi.string(), - c: Joi.number() - }, - d: { - e: Joi.any() - } - }) - ).has(Joi.object().assert('d.e', Joi.ref('a.c'), 'equal to a.c')); + const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required(), Joi.number()); + const input = ['s1', 2, 3]; - Helper.validate(schema, [ - [[{ a: { b: 'x', c: 5 }, d: { e: 5 } }], true] - ]); + const value = await schema.validate(input); + expect(value).to.equal(['s1', 2, 3]); }); - it('does not throw if assertion passes', () => { + it('validates input without optional item', async () => { - const schema = Joi.array().has(Joi.string()); - Helper.validate(schema, [ - [['foo'], true] - ]); + const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required(), Joi.number()); + const input = ['s1', 2]; + + const value = await schema.validate(input); + expect(value).to.equal(['s1', 2]); }); - it('throws with proper message if assertion fails on unknown schema', () => { + it('validates input without optional item', async () => { - const schema = Joi.array().has(Joi.string()); - Helper.validate(schema, [ - [[0], false, null, { - message: '"value" does not contain at least one required match', - details: [{ - message: '"value" does not contain at least one required match', - path: [], - type: 'array.hasUnknown', - context: { label: 'value', value: [0] } - }] - }] - ]); + const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required(), Joi.number()).sparse(true); + const input = ['s1', 2, undefined]; + + const value = await schema.validate(input); + expect(value).to.equal(['s1', 2, undefined]); }); - it('throws with proper message if assertion fails on known schema', () => { + it('validates input without optional item in a sparse array', async () => { - const schema = Joi.array().has(Joi.string().label('foo')); - Helper.validate(schema, [ - [[0], false, null, { - message: '"value" does not contain at least one required match for type "foo"', - details: [{ - message: '"value" does not contain at least one required match for type "foo"', - path: [], - type: 'array.hasKnown', - context: { label: 'value', patternLabel: 'foo', value: [0] } - }] - }] - ]); + const schema = Joi.array().ordered(Joi.string().required(), Joi.number(), Joi.number().required()).sparse(true); + const input = ['s1', undefined, 3]; + + const value = await schema.validate(input); + expect(value).to.equal(['s1', undefined, 3]); }); - it('shows correct path for error', () => { + it('validates when input matches ordered items and matches regular items', async () => { - const schema = Joi.object({ - arr: Joi.array().has(Joi.string()) - }); - Helper.validate(schema, [ - [{ arr: [0] }, false, null, { - message: '"arr" does not contain at least one required match', - details: [{ - message: '"arr" does not contain at least one required match', - path: ['arr'], - type: 'array.hasUnknown', - context: { label: 'arr', key: 'arr', value: [0] } - }] - }] - ]); + const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required()).items(Joi.number()); + const input = ['s1', 2, 3, 4, 5]; + const value = await schema.validate(input); + expect(value).to.equal(['s1', 2, 3, 4, 5]); }); - it('supports nested arrays', () => { + it('errors when input does not match ordered items', async () => { - const schema = Joi.object({ - arr: Joi.array().items( - Joi.object({ foo: Joi.array().has(Joi.string()) }) - ) - }); - Helper.validate(schema, [ - [{ arr: [{ foo: ['bar'] }] }, true] - ]); + const schema = Joi.array().ordered(Joi.number().required(), Joi.string().required()); + const input = ['s1', 2]; + const err = await expect(schema.validate(input)).to.reject('"[0]" must be a number'); + expect(err.details).to.equal([{ + message: '"[0]" must be a number', + path: [0], + type: 'number.base', + context: { label: '[0]', key: 0, value: 's1' } + }]); }); - it('supports peer item references', () => { + it('errors when input has more items than ordered items', async () => { - const schema = Joi.object({ - array: Joi.array() - .items(Joi.number()) - .has(Joi.number().greater(Joi.ref('..0'))) - }); + const schema = Joi.array().ordered(Joi.number().required(), Joi.string().required()); + const input = [1, 's2', 3]; + const err = await expect(schema.validate(input)).to.reject('"value" must contain at most 2 items'); + expect(err.details).to.equal([{ + message: '"value" must contain at most 2 items', + path: [], + type: 'array.orderedLength', + context: { pos: 2, limit: 2, label: 'value', value: input } + }]); + }); - Helper.validate(schema, [ - [{ array: [10, 1, 11, 5] }, true], - [{ array: [10, 1, 2, 5, 12] }, true], - [{ array: [10, 1, 2, 5, 1] }, false, null, { - message: '"array" does not contain at least one required match', - details: [{ - message: '"array" does not contain at least one required match', - path: ['array'], - type: 'array.hasUnknown', - context: { label: 'array', key: 'array', value: [10, 1, 2, 5, 1] } - }] - }] + it('errors when input has more items than ordered items with abortEarly = false', async () => { + + const schema = Joi.array().ordered(Joi.string(), Joi.number()).prefs({ abortEarly: false }); + const input = [1, 2, 3, 4, 5]; + const err = await expect(schema.validate(input)).to.reject(); + expect(err).to.be.an.error('"[0]" must be a string. "value" must contain at most 2 items'); + expect(err.details).to.have.length(2); + expect(err.details).to.equal([ + { + message: '"[0]" must be a string', + path: [0], + type: 'string.base', + context: { value: 1, label: '[0]', key: 0 } + }, + { + message: '"value" must contain at most 2 items', + path: [], + type: 'array.orderedLength', + context: { pos: 2, limit: 2, label: 'value', value: input } + } ]); }); - it('provides accurate error message for nested arrays', () => { + it('errors when input has less items than ordered items', async () => { - const schema = Joi.object({ - arr: Joi.array().items( - Joi.object({ foo: Joi.array().has(Joi.string()) }) - ) - }); - Helper.validate(schema, [ - [{ arr: [{ foo: [0] }] }, false, null, { - message: '"arr[0].foo" does not contain at least one required match', - details: [{ - message: '"arr[0].foo" does not contain at least one required match', - path: ['arr', 0, 'foo'], - type: 'array.hasUnknown', - context: { label: 'arr[0].foo', key: 'foo', value: [0] } - }] - }] - ]); + const schema = Joi.array().ordered(Joi.number().required(), Joi.string().required()); + const input = [1]; + const err = await expect(schema.validate(input)).to.reject('"value" does not contain 1 required value(s)'); + expect(err.details).to.equal([{ + message: '"value" does not contain 1 required value(s)', + path: [], + type: 'array.includesRequiredUnknowns', + context: { unknownMisses: 1, label: 'value', value: input } + }]); }); - it('handles multiple assertions', () => { - - const schema = Joi.array().has(Joi.string()).has(Joi.number()); - Helper.validate(schema, [ - [['foo', 0], true] - ]); + it('errors when input matches ordered items but not matches regular items', async () => { - Helper.validate(schema, [ - [['foo'], false, null, { - message: '"value" does not contain at least one required match', - details: [{ - message: '"value" does not contain at least one required match', - path: [], - type: 'array.hasUnknown', - context: { label: 'value', value: ['foo'] } - }] - }] - ]); + const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required()).items(Joi.number()).prefs({ abortEarly: false }); + const input = ['s1', 2, 3, 4, 's5']; + const err = await expect(schema.validate(input)).to.reject('"[4]" must be a number'); + expect(err.details).to.equal([{ + message: '"[4]" must be a number', + path: [4], + type: 'number.base', + context: { label: '[4]', key: 4, value: 's5' } + }]); }); - it('describes the pattern schema', () => { + it('errors when input does not match ordered items but matches regular items', async () => { - const schema = Joi.array().has(Joi.string()).has(Joi.number()); - expect(schema.describe()).to.equal({ - type: 'array', - flags: { sparse: false }, - rules: [ - { name: 'has', arg: { type: 'string', invalids: [''] } }, - { name: 'has', arg: { type: 'number', flags: { unsafe: false }, invalids: [Infinity, -Infinity] } } - ] - }); + const schema = Joi.array().ordered(Joi.string(), Joi.number()).items(Joi.number()).prefs({ abortEarly: false }); + const input = [1, 2, 3, 4, 5]; + const err = await expect(schema.validate(input)).to.reject('"[0]" must be a string'); + expect(err.details).to.equal([{ + message: '"[0]" must be a string', + path: [0], + type: 'string.base', + context: { value: 1, label: '[0]', key: 0 } + }]); }); - }); - describe('validate()', () => { - - it('should, by default, allow undefined, allow empty array', () => { + it('errors when input does not match ordered items not matches regular items', async () => { - Helper.validate(Joi.array(), [ - [undefined, true], - [[], true] + const schema = Joi.array().ordered(Joi.string(), Joi.number()).items(Joi.string()).prefs({ abortEarly: false }); + const input = [1, 2, 3, 4, 5]; + const err = await expect(schema.validate(input)).to.reject(); + expect(err).to.be.an.error('"[0]" must be a string. "[2]" must be a string. "[3]" must be a string. "[4]" must be a string'); + expect(err.details).to.have.length(4); + expect(err.details).to.equal([ + { + message: '"[0]" must be a string', + path: [0], + type: 'string.base', + context: { value: 1, label: '[0]', key: 0 } + }, + { + message: '"[2]" must be a string', + path: [2], + type: 'string.base', + context: { value: 3, label: '[2]', key: 2 } + }, + { + message: '"[3]" must be a string', + path: [3], + type: 'string.base', + context: { value: 4, label: '[3]', key: 3 } + }, + { + message: '"[4]" must be a string', + path: [4], + type: 'string.base', + context: { value: 5, label: '[4]', key: 4 } + } ]); }); - it('should, when .required(), deny undefined', () => { + it('errors but continues when abortEarly is set to false', async () => { - Helper.validate(Joi.array().required(), [ - [undefined, false, null, { - message: '"value" is required', - details: [{ - message: '"value" is required', - path: [], - type: 'any.required', - context: { label: 'value' } - }] - }] + const schema = Joi.array().ordered(Joi.number().required(), Joi.string().required()).prefs({ abortEarly: false }); + const input = ['s1', 2]; + const err = await expect(schema.validate(input)).to.reject(); + expect(err).to.be.an.error('"[0]" must be a number. "[1]" must be a string'); + expect(err.details).to.have.length(2); + expect(err.details).to.equal([ + { + message: '"[0]" must be a number', + path: [0], + type: 'number.base', + context: { label: '[0]', key: 0, value: 's1' } + }, + { + message: '"[1]" must be a string', + path: [1], + type: 'string.base', + context: { value: 2, label: '[1]', key: 1 } + } ]); }); - it('allows empty arrays', () => { - - Helper.validate(Joi.array(), [ - [undefined, true], - [[], true] - ]); - }); + it('errors on sparse arrays and continues when abortEarly is set to false', () => { - it('excludes values when items are forbidden', () => { + const schema = Joi.array().ordered( + Joi.number().min(0), + Joi.string().min(2), + Joi.number().max(0), + Joi.string().max(3) + ) + .prefs({ abortEarly: false }); - Helper.validate(Joi.array().items(Joi.string().forbidden()), [ - [['2', '1'], false, null, { - message: '"[0]" contains an excluded value', + Helper.validate(schema, [ + [[0, 'ab', 0, 'ab'], true], + [[undefined, 'foo', 2, 'bar'], false, null, { + message: '"[0]" must not be a sparse array item. "[2]" must be less than or equal to 0', details: [{ - message: '"[0]" contains an excluded value', + message: '"[0]" must not be a sparse array item', path: [0], - type: 'array.excludes', - context: { pos: 0, value: '2', label: '[0]', key: 0 } + type: 'array.sparse', + context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } + }, { + message: '"[2]" must be less than or equal to 0', + path: [2], + type: 'number.max', + context: { key: 2, label: '[2]', limit: 0, value: 2 } }] }], - [['1'], false, null, { - message: '"[0]" contains an excluded value', + [[undefined, 'foo', 2, undefined], false, null, { + message: '"[0]" must not be a sparse array item. "[2]" must be less than or equal to 0. "[3]" must not be a sparse array item', details: [{ - message: '"[0]" contains an excluded value', + message: '"[0]" must not be a sparse array item', path: [0], - type: 'array.excludes', - context: { pos: 0, value: '1', label: '[0]', key: 0 } + type: 'array.sparse', + context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } + }, { + message: '"[2]" must be less than or equal to 0', + path: [2], + type: 'number.max', + context: { key: 2, label: '[2]', limit: 0, value: 2 } + }, { + message: '"[3]" must not be a sparse array item', + path: [3], + type: 'array.sparse', + context: { key: 3, label: '[3]', path: [3], pos: 3, value: undefined } }] - }], - [[2], true] + }] ]); }); - it('allows types to be forbidden', async () => { - - const schema = Joi.array().items(Joi.number().forbidden()); - - const n = [1, 2, 'hippo']; - const err = await expect(schema.validate(n)).to.reject('"[0]" contains an excluded value'); - expect(err.details).to.equal([{ - message: '"[0]" contains an excluded value', - path: [0], - type: 'array.excludes', - context: { pos: 0, value: 1, label: '[0]', key: 0 } - }]); - - const m = ['x', 'y', 'z']; - await schema.validate(m); - }); + it('errors on forbidden items and continues when abortEarly is set to false', () => { - it('validates array of Numbers', () => { + const schema = Joi.array().items(Joi.bool().forbidden()).ordered( + Joi.number().min(0), + Joi.string().min(2), + Joi.number().max(0), + Joi.string().max(3) + ).prefs({ abortEarly: false }); - Helper.validate(Joi.array().items(Joi.number()), [ - [[1, 2, 3], true], - [[50, 100, 1000], true], - [['a', 1, 2], false, null, { - message: '"[0]" must be a number', + Helper.validate(schema, [ + [[0, 'ab', 0, 'ab'], true], + [[undefined, 'foo', 2, 'bar'], false, null, { + message: '"[0]" must not be a sparse array item. "[2]" must be less than or equal to 0', details: [{ - message: '"[0]" must be a number', + message: '"[0]" must not be a sparse array item', path: [0], - type: 'number.base', - context: { label: '[0]', key: 0, value: 'a' } + type: 'array.sparse', + context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } + }, { + message: '"[2]" must be less than or equal to 0', + path: [2], + type: 'number.max', + context: { key: 2, label: '[2]', limit: 0, value: 2 } }] }], - [['1', '2', 4], true] - ]); - }); - - it('validates array of mixed Numbers & Strings', () => { - - Helper.validate(Joi.array().items(Joi.number(), Joi.string()), [ - [[1, 2, 3], true], - [[50, 100, 1000], true], - [[1, 'a', 5, 10], true], - [['joi', 'everydaylowprices', 5000], true] - ]); - }); - - it('validates array of objects with schema', () => { - - Helper.validate(Joi.array().items(Joi.object({ h1: Joi.number().required() })), [ - [[{ h1: 1 }, { h1: 2 }, { h1: 3 }], true], - [[{ h2: 1, h3: 'somestring' }, { h1: 2 }, { h1: 3 }], false, null, { - message: '"[0].h1" is required', + [[undefined, 'foo', 2, undefined], false, null, { + message: '"[0]" must not be a sparse array item. "[2]" must be less than or equal to 0. "[3]" must not be a sparse array item', details: [{ - message: '"[0].h1" is required', - path: [0, 'h1'], - type: 'any.required', - context: { label: '[0].h1', key: 'h1' } - }] - }], - [[1, 2, [1]], false, null, { - message: '"[0]" must be an object', + message: '"[0]" must not be a sparse array item', + path: [0], + type: 'array.sparse', + context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } + }, { + message: '"[2]" must be less than or equal to 0', + path: [2], + type: 'number.max', + context: { key: 2, label: '[2]', limit: 0, value: 2 } + }, { + message: '"[3]" must not be a sparse array item', + path: [3], + type: 'array.sparse', + context: { key: 3, label: '[3]', path: [3], pos: 3, value: undefined } + }] + }], + [[undefined, false, 2, undefined], false, null, { + message: '"[0]" must not be a sparse array item. "[1]" contains an excluded value. "[2]" must be less than or equal to 0. "[3]" must not be a sparse array item', details: [{ - message: '"[0]" must be an object', + message: '"[0]" must not be a sparse array item', path: [0], - type: 'object.base', - context: { label: '[0]', key: 0, value: 1 } + type: 'array.sparse', + context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } + }, { + message: '"[1]" contains an excluded value', + path: [1], + type: 'array.excludes', + context: { key: 1, label: '[1]', pos: 1, value: false } + }, { + message: '"[2]" must be less than or equal to 0', + path: [2], + type: 'number.max', + context: { key: 2, label: '[2]', limit: 0, value: 2 } + }, { + message: '"[3]" must not be a sparse array item', + path: [3], + type: 'array.sparse', + context: { key: 3, label: '[3]', path: [3], pos: 3, value: undefined } }] }] ]); }); - it('errors on array of unallowed mixed types (Array)', () => { + it('strips item', async () => { - Helper.validate(Joi.array().items(Joi.number()), [ - [[1, 2, 3], true], - [[1, 2, [1]], false, null, { - message: '"[2]" must be a number', - details: [{ - message: '"[2]" must be a number', - path: [2], - type: 'number.base', - context: { label: '[2]', key: 2, value: [1] } - }] - }] - ]); + const schema = Joi.array().ordered(Joi.string().required(), Joi.number().strip(), Joi.number().required()); + const input = ['s1', 2, 3]; + const value = await schema.validate(input); + expect(value).to.equal(['s1', 3]); }); - it('errors on invalid number rule using includes', async () => { + it('strips multiple items', async () => { - const schema = Joi.object({ - arr: Joi.array().items(Joi.number().integer()) - }); + const schema = Joi.array().ordered(Joi.string().strip(), Joi.number(), Joi.number().strip()); + const input = ['s1', 2, 3]; + const value = await schema.validate(input); + expect(value).to.equal([2]); + }); - const input = { arr: [1, 2, 2.1] }; - const err = await expect(schema.validate(input)).to.reject('"arr[2]" must be an integer'); - expect(err.details).to.equal([{ - message: '"arr[2]" must be an integer', - path: ['arr', 2], - type: 'number.integer', - context: { value: 2.1, label: 'arr[2]', key: 2 } - }]); + it('references array members', async () => { + + const schema = Joi.array().ordered(Joi.number(), Joi.number().greater(Joi.ref('..0'))); + expect(await schema.validate([1, 2])).to.equal([1, 2]); + await expect(schema.validate([1, 0])).to.reject(); }); + }); - it('validates an array within an object', () => { + describe('single()', () => { - const schema = Joi.object({ - array: Joi.array().items(Joi.string().min(5), Joi.number().min(3)) - }).prefs({ convert: false }); + it('allows a single element', () => { + + const schema = Joi.array().items(Joi.number()).items(Joi.boolean().forbidden()).single(); Helper.validate(schema, [ - [{ array: ['12345'] }, true], - [{ array: ['1'] }, false, null, { - message: '"array[0]" does not match any of the allowed types', + [[1, 2, 3], true], + [1, true], + [['a'], false, null, { + message: '"[0]" must be a number', details: [{ - message: '"array[0]" does not match any of the allowed types', - path: ['array', 0], - type: 'array.includes', - context: { pos: 0, value: '1', label: 'array[0]', key: 0 } + message: '"[0]" must be a number', + path: [0], + type: 'number.base', + context: { label: '[0]', key: 0, value: 'a' } }] }], - [{ array: [3] }, true], - [{ array: ['12345', 3] }, true] + ['a', false, null, { + message: '"value" must be a number', + details: [{ + message: '"value" must be a number', + path: [], + type: 'number.base', + context: { label: 'value', value: 'a' } + }] + }], + [true, false, null, { + message: '"value" contains an excluded value', + details: [{ + message: '"value" contains an excluded value', + path: [], + type: 'array.excludes', + context: { pos: 0, value: true, label: 'value' } + }] + }] ]); }); - it('should not change original value', async () => { - - const schema = Joi.array().items(Joi.number()).unique(); - const input = ['1', '2']; - - const value = await schema.validate(input); - expect(value).to.equal([1, 2]); - expect(input).to.equal(['1', '2']); - }); - - it('returns multiple errors if abort early is false', async () => { + it('allows a single element with multiple types', () => { - const schema = Joi.array().items(Joi.number(), Joi.object()).items(Joi.boolean().forbidden()); - const input = [1, undefined, true, 'a']; + const schema = Joi.array().items(Joi.number(), Joi.string()).single(); - const err = await expect(Joi.validate(input, schema, { abortEarly: false })).to.reject(); - expect(err).to.be.an.error('"[1]" must not be a sparse array item. "[2]" contains an excluded value. "[3]" does not match any of the allowed types'); - expect(err.details).to.equal([{ - message: '"[1]" must not be a sparse array item', - path: [1], - type: 'array.sparse', - context: { - key: 1, - label: '[1]', - path: [1], - pos: 1, - value: undefined - } - }, { - message: '"[2]" contains an excluded value', - path: [2], - type: 'array.excludes', - context: { - pos: 2, - key: 2, - label: '[2]', - value: true - } - }, { - message: '"[3]" does not match any of the allowed types', - path: [3], - type: 'array.includes', - context: { - pos: 3, - key: 3, - label: '[3]', - value: 'a' - } - }]); + Helper.validate(schema, [ + [[1, 2, 3], true], + [1, true], + [[1, 'a'], true], + ['a', true], + [true, false, null, { + message: '"value" does not match any of the allowed types', + details: [{ + message: '"value" does not match any of the allowed types', + path: [], + type: 'array.includes', + context: { pos: 0, value: true, label: 'value' } + }] + }] + ]); }); - it('returns multiple errors if abort early is false across items() and unique()', async () => { + it('errors on single with array items', () => { - const item = Joi.object({ - test: Joi.string(), - hello: Joi.string().required() - }); + expect(() => Joi.array().items(Joi.array()).single()).to.throw('Cannot specify single rule when array has array items'); + expect(() => Joi.array().items(Joi.alternatives([Joi.array()])).single()).to.throw('Cannot specify single rule when array has array items'); - const schema = Joi.array().items(item).unique('test'); + expect(() => Joi.array().single().items(Joi.array())).to.throw('Cannot specify array item with single rule enabled'); + expect(() => Joi.array().single().items(Joi.alternatives([Joi.array()]))).to.throw('Cannot specify array item with single rule enabled'); - const input = [ - { - test: 'test', - hello: 'world' - }, - { - test: 'test' - } - ]; + expect(() => Joi.array().ordered(Joi.array()).single()).to.throw('Cannot specify single rule when array has array items'); + expect(() => Joi.array().ordered(Joi.alternatives([Joi.array()])).single()).to.throw('Cannot specify single rule when array has array items'); - const err = await expect(Joi.validate(input, schema, { abortEarly: false })).to.reject('"[1].hello" is required. "[1]" contains a duplicate value'); - expect(err.details).to.equal([ - { - context: { - key: 'hello', - label: '[1].hello' - }, - message: '"[1].hello" is required', - path: [1, 'hello'], - type: 'any.required' - }, - { - context: { - dupePos: 0, - dupeValue: { - hello: 'world', - test: 'test' - }, - key: 1, - label: '[1]', - path: 'test', - pos: 1, - value: { - test: 'test' - } - }, - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique' - } - ]); + expect(() => Joi.array().single().ordered(Joi.array())).to.throw('Cannot specify array item with single rule enabled'); + expect(() => Joi.array().single().ordered(Joi.alternatives([Joi.array()]))).to.throw('Cannot specify array item with single rule enabled'); }); - }); - - describe('describe()', () => { - it('returns an empty description when no rules are applied', () => { + it('switches the single flag with explicit value', () => { - const schema = Joi.array(); + const schema = Joi.array().single(true); const desc = schema.describe(); expect(desc).to.equal({ type: 'array', - flags: { sparse: false } + flags: { sparse: false, single: true } }); }); - it('returns an updated description when sparse rule is applied', () => { + it('switches the single flag back', () => { - const schema = Joi.array().sparse(); - const desc = schema.describe(); - expect(desc).to.equal({ - type: 'array', - flags: { sparse: true } - }); - }); - - it('returns an items array only if items are specified', () => { - - const schema = Joi.array().items().max(5); - const desc = schema.describe(); - expect(desc.items).to.not.exist(); - }); - - it('returns a recursively defined array of items when specified', () => { - - const schema = Joi.array() - .items(Joi.number(), Joi.string()) - .items(Joi.boolean().forbidden()) - .ordered(Joi.number(), Joi.string()) - .ordered(Joi.string().required()); + const schema = Joi.array().single().single(false); const desc = schema.describe(); - expect(desc.items).to.have.length(3); expect(desc).to.equal({ type: 'array', - flags: { sparse: false }, - orderedItems: [ - { type: 'number', invalids: [Infinity, -Infinity], flags: { unsafe: false } }, - { type: 'string', invalids: [''] }, - { type: 'string', invalids: [''], flags: { presence: 'required' } } - ], - items: [ - { type: 'number', invalids: [Infinity, -Infinity], flags: { unsafe: false } }, - { type: 'string', invalids: [''] }, - { type: 'boolean', flags: { presence: 'forbidden', insensitive: true }, truthy: [true], falsy: [false] } - ] + flags: { sparse: false, single: false } }); }); - it('describes an array with array items', () => { + it('avoids unnecessary cloning when called twice', () => { - const schema = Joi.array().items(Joi.array()); - const desc = schema.describe(); - expect(desc).to.equal({ - type: 'array', - flags: { sparse: false }, - items: [ - { - type: 'array', - flags: { sparse: false } - } - ] - }); + const schema = Joi.array().single(); + expect(schema.single()).to.shallow.equal(schema); }); }); - describe('unique()', () => { + describe('sort()', () => { - it('errors if duplicate numbers, strings, objects, binaries, functions, dates and booleans', () => { + it('validates array sorts order', () => { - const buffer = Buffer.from('hello world'); - const func = function () { }; - const now = new Date(); - const schema = Joi.array().sparse().unique(); + const schema = Joi.array().sort().prefs({ convert: false }); Helper.validate(schema, [ - [[2, 2], false, null, { - message: '"[1]" contains a duplicate value', - details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', + [[1, 2], true], + [['a', 'b'], true], + [['a', 'b', null], true], + [['a', 'b', null, null], true], + [['a', 'b', undefined], true], + [['a', 'b', undefined, undefined], true], + [[1, 0], false, null, { + message: '"value" must be sorted in ascending order by value', + details: [{ + message: '"value" must be sorted in ascending order by value', + path: [], + type: 'array.sort', context: { - pos: 1, - value: 2, - dupePos: 0, - dupeValue: 2, - label: '[1]', - key: 1 + order: 'ascending', + by: 'value', + label: 'value', + value: [1, 0] } }] }], - [[0x2, 2], false, null, { - message: '"[1]" contains a duplicate value', + [['1', '0'], false, null, { + message: '"value" must be sorted in ascending order by value', details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', + message: '"value" must be sorted in ascending order by value', + path: [], + type: 'array.sort', context: { - pos: 1, - value: 2, - dupePos: 0, - dupeValue: 0x2, - label: '[1]', - key: 1 + order: 'ascending', + by: 'value', + label: 'value', + value: ['1', '0'] } }] }], - [['duplicate', 'duplicate'], false, null, { - message: '"[1]" contains a duplicate value', + [[null, 1, 2], false, null, { + message: '"value" must be sorted in ascending order by value', details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', + message: '"value" must be sorted in ascending order by value', + path: [], + type: 'array.sort', context: { - pos: 1, - value: 'duplicate', - dupePos: 0, - dupeValue: 'duplicate', - label: '[1]', - key: 1 + order: 'ascending', + by: 'value', + label: 'value', + value: [null, 1, 2] + } + }] + }] + ]); + }); + + it('validates array sorts order (ascending)', () => { + + const schema = Joi.array().sort({ order: 'ascending' }).prefs({ convert: false }); + + Helper.validate(schema, [ + [[1, 2], true], + [['a', 'b'], true], + [['a', 'b', null], true], + [['a', 'b', null, null], true], + [['a', 'b', undefined], true], + [['a', 'b', undefined, undefined], true], + [['a', 'b', null, undefined], true], + [[1, 0], false, null, { + message: '"value" must be sorted in ascending order by value', + details: [{ + message: '"value" must be sorted in ascending order by value', + path: [], + type: 'array.sort', + context: { + order: 'ascending', + by: 'value', + label: 'value', + value: [1, 0] } }] }], - [[{ a: 'b' }, { a: 'b' }], false, null, { - message: '"[1]" contains a duplicate value', + [['1', '0'], false, null, { + message: '"value" must be sorted in ascending order by value', details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', + message: '"value" must be sorted in ascending order by value', + path: [], + type: 'array.sort', context: { - pos: 1, - value: { a: 'b' }, - dupePos: 0, - dupeValue: { a: 'b' }, - label: '[1]', - key: 1 + order: 'ascending', + by: 'value', + label: 'value', + value: ['1', '0'] } }] }], - [[buffer, buffer], false, null, { - message: '"[1]" contains a duplicate value', + [[null, 1, 2], false, null, { + message: '"value" must be sorted in ascending order by value', details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', + message: '"value" must be sorted in ascending order by value', + path: [], + type: 'array.sort', context: { - pos: 1, - value: buffer, - dupePos: 0, - dupeValue: buffer, - label: '[1]', - key: 1 + order: 'ascending', + by: 'value', + label: 'value', + value: [null, 1, 2] + } + }] + }] + ]); + }); + + it('validates array sorts order (descending)', () => { + + const schema = Joi.array().sort({ order: 'descending' }).prefs({ convert: false }); + + Helper.validate(schema, [ + [[2, 1], true], + [['b', 'a'], true], + [[null, 'b', 'a'], true], + [[null, null, 'b', 'a'], true], + [['b', 'a', undefined], true], + [['b', 'a', undefined, undefined], true], + [[null, 'b', 'a', undefined], true], + [[0, 1], false, null, { + message: '"value" must be sorted in descending order by value', + details: [{ + message: '"value" must be sorted in descending order by value', + path: [], + type: 'array.sort', + context: { + order: 'descending', + by: 'value', + label: 'value', + value: [0, 1] } }] }], - [[func, func], false, null, { - message: '"[1]" contains a duplicate value', + [['0', '1'], false, null, { + message: '"value" must be sorted in descending order by value', details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', + message: '"value" must be sorted in descending order by value', + path: [], + type: 'array.sort', context: { - pos: 1, - value: func, - dupePos: 0, - dupeValue: func, - label: '[1]', - key: 1 + order: 'descending', + by: 'value', + label: 'value', + value: ['0', '1'] } }] }], - [[now, now], false, null, { - message: '"[1]" contains a duplicate value', + [[2, 1, null], false, null, { + message: '"value" must be sorted in descending order by value', details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', + message: '"value" must be sorted in descending order by value', + path: [], + type: 'array.sort', context: { - pos: 1, - value: now, - dupePos: 0, - dupeValue: now, - label: '[1]', - key: 1 + order: 'descending', + by: 'value', + label: 'value', + value: [2, 1, null] + } + }] + }] + ]); + }); + + it('validates array sorts order (object key)', () => { + + const schema = Joi.array().sort({ by: 'x' }).prefs({ convert: false }); + + Helper.validate(schema, [ + [[{ x: 1 }, { x: 2 }], true], + [[{ x: 'a' }, { x: 'b' }], true], + [[{ x: 'a' }, { x: 'b' }, { x: null }], true], + [[{ x: 'a' }, { x: 'b' }, { x: null }, { x: null }], true], + [[{ x: 'a' }, { x: 'b' }, undefined], true], + [[{ x: 'a' }, { x: 'b' }, undefined, undefined], true], + [[{ x: 'a' }, { x: 'b' }, { x: null }, {}, null, undefined, undefined], true], + [[{ x: 1 }, { x: 0 }], false, null, { + message: '"value" must be sorted in ascending order by x', + details: [{ + message: '"value" must be sorted in ascending order by x', + path: [], + type: 'array.sort', + context: { + order: 'ascending', + by: 'x', + label: 'value', + value: [{ x: 1 }, { x: 0 }] } }] }], - [[true, true], false, null, { - message: '"[1]" contains a duplicate value', + [[{ x: '1' }, { x: '0' }], false, null, { + message: '"value" must be sorted in ascending order by x', details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', + message: '"value" must be sorted in ascending order by x', + path: [], + type: 'array.sort', context: { - pos: 1, - value: true, - dupePos: 0, - dupeValue: true, - label: '[1]', - key: 1 + order: 'ascending', + by: 'x', + label: 'value', + value: [{ x: '1' }, { x: '0' }] } }] }], - [[undefined, undefined], false, null, { - message: '"[1]" contains a duplicate value', + [[{ x: null }, { x: 1 }, { x: 2 }], false, null, { + message: '"value" must be sorted in ascending order by x', details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', + message: '"value" must be sorted in ascending order by x', + path: [], + type: 'array.sort', context: { - pos: 1, - dupePos: 0, - dupeValue: undefined, - label: '[1]', - key: 1, - value: undefined + order: 'ascending', + by: 'x', + label: 'value', + value: [{ x: null }, { x: 1 }, { x: 2 }] } }] }] ]); }); - it('errors with the correct details', () => { + it('sorts array (ascending)', () => { - let error = Joi.array().items(Joi.number()).unique().validate([1, 2, 3, 1, 4]).error; - expect(error).to.be.an.error('"[3]" contains a duplicate value'); - expect(error.details).to.equal([{ - context: { - key: 3, - label: '[3]', - pos: 3, - value: 1, - dupePos: 0, - dupeValue: 1 - }, - message: '"[3]" contains a duplicate value', - path: [3], - type: 'array.unique' - }]); + const schema = Joi.array().sort({ order: 'ascending' }); - error = Joi.array().items(Joi.number()).unique((a, b) => a === b).validate([1, 2, 3, 1, 4]).error; - expect(error).to.be.an.error('"[3]" contains a duplicate value'); - expect(error.details).to.equal([{ - context: { - key: 3, - label: '[3]', - pos: 3, - value: 1, - dupePos: 0, - dupeValue: 1 - }, - message: '"[3]" contains a duplicate value', - path: [3], - type: 'array.unique' - }]); + Helper.validate(schema, [ + [[2, 1], true, null, [1, 2]], + [['b', 'a'], true, null, ['a', 'b']], + [[null, 'a', 'b'], true, null, ['a', 'b', null]], + [[null, 'b', 'a'], true, null, ['a', 'b', null]], + [['a', null, 'b'], true, null, ['a', 'b', null]], + [[null, 'a', null, 'b'], true, null, ['a', 'b', null, null]], + [['b', 'a', undefined], true, null, ['a', 'b', undefined]], + [['b', undefined, 'a'], true, null, ['a', 'b', undefined]], + [[0, '1'], false, null, { + message: '"value" cannot be sorted due to mismatching types', + details: [{ + message: '"value" cannot be sorted due to mismatching types', + path: [], + type: 'array.sort.mismatching', + context: { + label: 'value', + value: [0, '1'] + } + }] + }] + ]); + }); - error = Joi.object({ a: Joi.array().items(Joi.number()).unique() }).validate({ a: [1, 2, 3, 1, 4] }).error; - expect(error).to.be.an.error('"a[3]" contains a duplicate value'); - expect(error.details).to.equal([{ - context: { - key: 3, - label: 'a[3]', - pos: 3, - value: 1, - dupePos: 0, - dupeValue: 1 - }, - message: '"a[3]" contains a duplicate value', - path: ['a', 3], - type: 'array.unique' - }]); + it('sorts array (object key)', () => { - error = Joi.object({ a: Joi.array().items(Joi.number()).unique((a, b) => a === b) }).validate({ a: [1, 2, 3, 1, 4] }).error; - expect(error).to.be.an.error('"a[3]" contains a duplicate value'); - expect(error.details).to.equal([{ - context: { - key: 3, - label: 'a[3]', - pos: 3, - value: 1, - dupePos: 0, - dupeValue: 1 - }, - message: '"a[3]" contains a duplicate value', - path: ['a', 3], - type: 'array.unique' - }]); - }); - - it('ignores duplicates if they are of different types', () => { - - const schema = Joi.array().unique(); + const schema = Joi.array().sort({ by: 'x' }); Helper.validate(schema, [ - [[2, '2'], true] + [[{ x: 1 }, { x: 2 }], true, null, [{ x: 1 }, { x: 2 }]], + [[{ x: 'b' }, { x: 'a' }], true, null, [{ x: 'a' }, { x: 'b' }]], + [[{ x: 'b' }, { x: null }, { x: 'a' }], true, null, [{ x: 'a' }, { x: 'b' }, { x: null }]], + [[{}, { x: 'b' }, undefined, null, { x: null }, { x: 'a' }, undefined], true, null, [{ x: 'a' }, { x: 'b' }, { x: null }, {}, null, undefined, undefined]], + [[{ x: 0 }, { x: '1' }], false, null, { + message: '"value" cannot be sorted due to mismatching types', + details: [{ + message: '"value" cannot be sorted due to mismatching types', + path: [], + type: 'array.sort.mismatching', + context: { + label: 'value', + value: [{ x: 0 }, { x: '1' }] + } + }] + }] ]); }); - it('validates without duplicates', () => { + it('errors on unsupported type', async () => { - const buffer = Buffer.from('hello world'); - const buffer2 = Buffer.from('Hello world'); - const func = function () { }; - const func2 = function () { }; - const now = new Date(); - const now2 = new Date(+now + 100); - const schema = Joi.array().unique(); + const schema = Joi.array().sort().prefs({ convert: false }); + await expect(schema.validate([{}, {}])).to.reject('"value" cannot be sorted due to unsupported type object'); + }); - Helper.validate(schema, [ - [[1, 2], true], - [['s1', 's2'], true], - [[{ a: 'b' }, { a: 'c' }], true], - [[buffer, buffer2], true], - [[func, func2], true], - [[now, now2], true], - [[true, false], true] - ]); + it('errors on mismatching types', async () => { + + const schema = Joi.array().sort().prefs({ convert: false }); + await expect(schema.validate([1, 'x'])).to.reject('"value" cannot be sorted due to mismatching types'); }); + }); - it('validates using a comparator', () => { + describe('sparse()', () => { - const schema = Joi.array().unique((left, right) => left.a === right.a); + it('errors on undefined value', () => { + + const schema = Joi.array().items(Joi.number()); Helper.validate(schema, [ - [[{ a: 'b' }, { a: 'c' }], true], - [[{ a: 'b', c: 'd' }, { a: 'c', c: 'd' }], true], - [[{ a: 'b', c: 'd' }, { a: 'b', c: 'd' }], false, null, { - message: '"[1]" contains a duplicate value', + [[undefined], false, null, { + message: '"[0]" must not be a sparse array item', details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', - context: { - pos: 1, - value: { a: 'b', c: 'd' }, - dupePos: 0, - dupeValue: { a: 'b', c: 'd' }, - label: '[1]', - key: 1 - } + message: '"[0]" must not be a sparse array item', + path: [0], + type: 'array.sparse', + context: { label: '[0]', key: 0, path: [0], pos: 0, value: undefined } }] }], - [[{ a: 'b', c: 'c' }, { a: 'b', c: 'd' }], false, null, { - message: '"[1]" contains a duplicate value', + [[2, undefined], false, null, { + message: '"[1]" must not be a sparse array item', details: [{ - message: '"[1]" contains a duplicate value', + message: '"[1]" must not be a sparse array item', path: [1], - type: 'array.unique', - context: { - pos: 1, - value: { a: 'b', c: 'd' }, - dupePos: 0, - dupeValue: { a: 'b', c: 'c' }, - label: '[1]', - key: 1 - } + type: 'array.sparse', + context: { label: '[1]', key: 1, path: [1], pos: 1, value: undefined } }] }] ]); }); - it('validates using a comparator with different types', () => { - - const schema = Joi.array().items(Joi.string(), Joi.object({ a: Joi.string() })).unique((left, right) => { - - if (typeof left === 'object') { - if (typeof right === 'object') { - return left.a === right.a; - } - - return left.a === right; - } - - if (typeof right === 'object') { - return left === right.a; - } + it('errors on undefined value after validation', () => { - return left === right; - }); + const schema = Joi.array().items(Joi.object().empty({})); Helper.validate(schema, [ - [[{ a: 'b' }, { a: 'c' }], true], - [[{ a: 'b' }, 'c'], true], - [[{ a: 'b' }, 'c', { a: 'd' }, 'e'], true], - [[{ a: 'b' }, { a: 'b' }], false, null, { - message: '"[1]" contains a duplicate value', - details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', - context: { - pos: 1, - value: { a: 'b' }, - dupePos: 0, - dupeValue: { a: 'b' }, - label: '[1]', - key: 1 - } - }] - }], - [[{ a: 'b' }, 'b'], false, null, { - message: '"[1]" contains a duplicate value', + [[{ a: 1 }, {}, { c: 3 }], false, null, { + message: '"[1]" must not be a sparse array item', details: [{ - message: '"[1]" contains a duplicate value', + message: '"[1]" must not be a sparse array item', path: [1], - type: 'array.unique', - context: { - pos: 1, - value: 'b', - dupePos: 0, - dupeValue: { a: 'b' }, - label: '[1]', - key: 1 - } + type: 'array.sparse', + context: { label: '[1]', key: 1, path: [1], pos: 1, value: undefined } }] }] ]); }); - it('validates using a path comparator', () => { + it('errors on undefined value after validation with abortEarly false', () => { - let schema = Joi.array().items(Joi.object({ id: Joi.number() })).unique('id'); + const schema = Joi.array().items(Joi.object().empty({})).prefs({ abortEarly: false }); Helper.validate(schema, [ - [[{ id: 1 }, { id: 2 }, { id: 3 }], true], - [[{ id: 1 }, { id: 2 }, {}], true], - [[{ id: 1 }, { id: 2 }, { id: 1 }], false, null, { - message: '"[2]" contains a duplicate value', - details: [{ - context: { - dupePos: 0, - dupeValue: { id: 1 }, - key: 2, - label: '[2]', - path: 'id', - pos: 2, - value: { id: 1 } - }, - message: '"[2]" contains a duplicate value', - path: [2], - type: 'array.unique' - }] - }], - [[{ id: 1 }, { id: 2 }, {}, { id: 3 }, {}], false, null, { - message: '"[4]" contains a duplicate value', - details: [{ - context: { - dupePos: 2, - dupeValue: {}, - key: 4, - label: '[4]', - path: 'id', - pos: 4, - value: {} + [[{ a: 1 }, {}, 3], false, null, { + message: '"[1]" must not be a sparse array item. "[2]" must be an object', + details: [ + { + message: '"[1]" must not be a sparse array item', + path: [1], + type: 'array.sparse', + context: { label: '[1]', key: 1, path: [1], pos: 1, value: undefined } }, - message: '"[4]" contains a duplicate value', - path: [4], - type: 'array.unique' - }] + { + message: '"[2]" must be an object', + path: [2], + type: 'object.base', + context: { label: '[2]', key: 2, value: 3 } + } + ] }] ]); + }); - schema = Joi.array().items(Joi.object({ nested: { id: Joi.number() } })).unique('nested.id'); + it('errors on undefined value after validation with required', () => { + + const schema = Joi.array().items(Joi.object().empty({}).required()); Helper.validate(schema, [ - [[{ nested: { id: 1 } }, { nested: { id: 2 } }, { nested: { id: 3 } }], true], - [[{ nested: { id: 1 } }, { nested: { id: 2 } }, {}], true], - [[{ nested: { id: 1 } }, { nested: { id: 2 } }, { nested: { id: 1 } }], false, null, { - message: '"[2]" contains a duplicate value', - details: [{ - context: { - dupePos: 0, - dupeValue: { nested: { id: 1 } }, - key: 2, - label: '[2]', - path: 'nested.id', - pos: 2, - value: { nested: { id: 1 } } - }, - message: '"[2]" contains a duplicate value', - path: [2], - type: 'array.unique' - }] - }], - [[{ nested: { id: 1 } }, { nested: { id: 2 } }, {}, { nested: { id: 3 } }, {}], false, null, { - message: '"[4]" contains a duplicate value', + [[{}, { c: 3 }], false, null, { + message: '"[0]" is required', details: [{ - context: { - dupePos: 2, - dupeValue: {}, - key: 4, - label: '[4]', - path: 'nested.id', - pos: 4, - value: {} - }, - message: '"[4]" contains a duplicate value', - path: [4], - type: 'array.unique' + message: '"[0]" is required', + path: [0], + type: 'any.required', + context: { label: '[0]', key: 0 } }] }] ]); + }); - schema = Joi.array().items(Joi.object({ nested: { id: Joi.number() } })).unique('nested'); - - Helper.validate(schema, [ - [[{ nested: { id: 1 } }, { nested: { id: 2 } }, { nested: { id: 3 } }], true], - [[{ nested: { id: 1 } }, { nested: { id: 2 } }, {}], true], - [[{ nested: { id: 1 } }, { nested: { id: 2 } }, { nested: { id: 1 } }], false, null, { - message: '"[2]" contains a duplicate value', - details: [{ - context: { - dupePos: 0, - dupeValue: { nested: { id: 1 } }, - key: 2, - label: '[2]', - path: 'nested', - pos: 2, - value: { nested: { id: 1 } } - }, - message: '"[2]" contains a duplicate value', - path: [2], - type: 'array.unique' - }] - }], - [[{ nested: { id: 1 } }, { nested: { id: 2 } }, {}, { nested: { id: 3 } }, {}], false, null, { - message: '"[4]" contains a duplicate value', - details: [{ - context: { - dupePos: 2, - dupeValue: {}, - key: 4, - label: '[4]', - path: 'nested', - pos: 4, - value: {} - }, - message: '"[4]" contains a duplicate value', - path: [4], - type: 'array.unique' - }] - }] - ]); - }); - - it('ignores undefined value when ignoreUndefined is true', () => { - - const schema = Joi.array().unique('a', { ignoreUndefined: true }); - - Helper.validate(schema, [ - [[{ a: 'b' }, { a: 'c' }], true], - [[{ c: 'd' }, { c: 'd' }], true], - [[{ a: 'b', c: 'd' }, { a: 'b', c: 'd' }], false, null, { - message: '"[1]" contains a duplicate value', - details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', - context: { - pos: 1, - value: { a: 'b', c: 'd' }, - dupePos: 0, - dupeValue: { a: 'b', c: 'd' }, - label: '[1]', - key: 1, - path: 'a' - } - }] - }], - [[{ a: 'b', c: 'c' }, { a: 'b', c: 'd' }], false, null, { - message: '"[1]" contains a duplicate value', - details: [{ - message: '"[1]" contains a duplicate value', - path: [1], - type: 'array.unique', - context: { - pos: 1, - value: { a: 'b', c: 'd' }, - dupePos: 0, - dupeValue: { a: 'b', c: 'c' }, - label: '[1]', - key: 1, - path: 'a' - } - }] - }] - ]); - }); - - it('fails with invalid configs', () => { - - expect(() => Joi.array().unique('id', 'invalid configs')).to.throw(Error, 'configs must be an object'); - expect(() => Joi.array().unique('id', {})).to.not.throw(); - }); - - it('fails with invalid comparator', () => { - - expect(() => Joi.array().unique({})).to.throw(Error, 'comparator must be a function or a string'); - }); - - it('handles period in key names', async () => { - - const schema = Joi.array().unique('a.b', { separator: false }); - - const test = [{ 'a.b': 1 }, { 'a.b': 2 }]; - expect(await schema.validate(test)).to.equal(test); - }); - }); - - describe('sparse()', () => { - - it('errors on undefined value', () => { - - const schema = Joi.array().items(Joi.number()); - - Helper.validate(schema, [ - [[undefined], false, null, { - message: '"[0]" must not be a sparse array item', - details: [{ - message: '"[0]" must not be a sparse array item', - path: [0], - type: 'array.sparse', - context: { label: '[0]', key: 0, path: [0], pos: 0, value: undefined } - }] - }], - [[2, undefined], false, null, { - message: '"[1]" must not be a sparse array item', - details: [{ - message: '"[1]" must not be a sparse array item', - path: [1], - type: 'array.sparse', - context: { label: '[1]', key: 1, path: [1], pos: 1, value: undefined } - }] - }] - ]); - }); - - it('errors on undefined value after validation', () => { - - const schema = Joi.array().items(Joi.object().empty({})); - - Helper.validate(schema, [ - [[{ a: 1 }, {}, { c: 3 }], false, null, { - message: '"[1]" must not be a sparse array item', - details: [{ - message: '"[1]" must not be a sparse array item', - path: [1], - type: 'array.sparse', - context: { label: '[1]', key: 1, path: [1], pos: 1, value: undefined } - }] - }] - ]); - }); - - it('errors on undefined value after validation with abortEarly false', () => { - - const schema = Joi.array().items(Joi.object().empty({})).prefs({ abortEarly: false }); - - Helper.validate(schema, [ - [[{ a: 1 }, {}, 3], false, null, { - message: '"[1]" must not be a sparse array item. "[2]" must be an object', - details: [ - { - message: '"[1]" must not be a sparse array item', - path: [1], - type: 'array.sparse', - context: { label: '[1]', key: 1, path: [1], pos: 1, value: undefined } - }, - { - message: '"[2]" must be an object', - path: [2], - type: 'object.base', - context: { label: '[2]', key: 2, value: 3 } - } - ] - }] - ]); - }); - - it('errors on undefined value after validation with required', () => { - - const schema = Joi.array().items(Joi.object().empty({}).required()); - - Helper.validate(schema, [ - [[{}, { c: 3 }], false, null, { - message: '"[0]" is required', - details: [{ - message: '"[0]" is required', - path: [0], - type: 'any.required', - context: { label: '[0]', key: 0 } - }] - }] - ]); - }); - - it('errors on undefined value after custom validation with required', () => { + it('errors on undefined value after custom validation with required', () => { const customJoi = Joi.extend({ name: 'myType', @@ -2170,485 +2116,833 @@ describe('array', () => { }); }); - describe('single()', () => { + describe('unique()', () => { - it('allows a single element', () => { + it('errors if duplicate numbers, strings, objects, binaries, functions, dates and booleans', () => { - const schema = Joi.array().items(Joi.number()).items(Joi.boolean().forbidden()).single(); + const buffer = Buffer.from('hello world'); + const func = function () { }; + const now = new Date(); + const schema = Joi.array().sparse().unique(); Helper.validate(schema, [ - [[1, 2, 3], true], - [1, true], - [['a'], false, null, { - message: '"[0]" must be a number', + [[2, 2], false, null, { + message: '"[1]" contains a duplicate value', details: [{ - message: '"[0]" must be a number', - path: [0], - type: 'number.base', - context: { label: '[0]', key: 0, value: 'a' } + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: 2, + dupePos: 0, + dupeValue: 2, + label: '[1]', + key: 1 + } }] }], - ['a', false, null, { - message: '"value" must be a number', + [[0x2, 2], false, null, { + message: '"[1]" contains a duplicate value', details: [{ - message: '"value" must be a number', - path: [], - type: 'number.base', - context: { label: 'value', value: 'a' } + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: 2, + dupePos: 0, + dupeValue: 0x2, + label: '[1]', + key: 1 + } }] }], - [true, false, null, { - message: '"value" contains an excluded value', + [['duplicate', 'duplicate'], false, null, { + message: '"[1]" contains a duplicate value', details: [{ - message: '"value" contains an excluded value', - path: [], - type: 'array.excludes', - context: { pos: 0, value: true, label: 'value' } + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: 'duplicate', + dupePos: 0, + dupeValue: 'duplicate', + label: '[1]', + key: 1 + } }] - }] - ]); - }); - - it('allows a single element with multiple types', () => { - - const schema = Joi.array().items(Joi.number(), Joi.string()).single(); - - Helper.validate(schema, [ - [[1, 2, 3], true], - [1, true], - [[1, 'a'], true], - ['a', true], - [true, false, null, { - message: '"value" does not match any of the allowed types', + }], + [[{ a: 'b' }, { a: 'b' }], false, null, { + message: '"[1]" contains a duplicate value', details: [{ - message: '"value" does not match any of the allowed types', - path: [], - type: 'array.includes', - context: { pos: 0, value: true, label: 'value' } + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: { a: 'b' }, + dupePos: 0, + dupeValue: { a: 'b' }, + label: '[1]', + key: 1 + } + }] + }], + [[buffer, buffer], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: buffer, + dupePos: 0, + dupeValue: buffer, + label: '[1]', + key: 1 + } + }] + }], + [[func, func], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: func, + dupePos: 0, + dupeValue: func, + label: '[1]', + key: 1 + } + }] + }], + [[now, now], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: now, + dupePos: 0, + dupeValue: now, + label: '[1]', + key: 1 + } + }] + }], + [[true, true], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: true, + dupePos: 0, + dupeValue: true, + label: '[1]', + key: 1 + } + }] + }], + [[undefined, undefined], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + dupePos: 0, + dupeValue: undefined, + label: '[1]', + key: 1, + value: undefined + } }] }] ]); }); - it('errors on single with array items', () => { + it('errors with the correct details', () => { - expect(() => Joi.array().items(Joi.array()).single()).to.throw('Cannot specify single rule when array has array items'); - expect(() => Joi.array().items(Joi.alternatives([Joi.array()])).single()).to.throw('Cannot specify single rule when array has array items'); + let error = Joi.array().items(Joi.number()).unique().validate([1, 2, 3, 1, 4]).error; + expect(error).to.be.an.error('"[3]" contains a duplicate value'); + expect(error.details).to.equal([{ + context: { + key: 3, + label: '[3]', + pos: 3, + value: 1, + dupePos: 0, + dupeValue: 1 + }, + message: '"[3]" contains a duplicate value', + path: [3], + type: 'array.unique' + }]); - expect(() => Joi.array().single().items(Joi.array())).to.throw('Cannot specify array item with single rule enabled'); - expect(() => Joi.array().single().items(Joi.alternatives([Joi.array()]))).to.throw('Cannot specify array item with single rule enabled'); + error = Joi.array().items(Joi.number()).unique((a, b) => a === b).validate([1, 2, 3, 1, 4]).error; + expect(error).to.be.an.error('"[3]" contains a duplicate value'); + expect(error.details).to.equal([{ + context: { + key: 3, + label: '[3]', + pos: 3, + value: 1, + dupePos: 0, + dupeValue: 1 + }, + message: '"[3]" contains a duplicate value', + path: [3], + type: 'array.unique' + }]); - expect(() => Joi.array().ordered(Joi.array()).single()).to.throw('Cannot specify single rule when array has array items'); - expect(() => Joi.array().ordered(Joi.alternatives([Joi.array()])).single()).to.throw('Cannot specify single rule when array has array items'); + error = Joi.object({ a: Joi.array().items(Joi.number()).unique() }).validate({ a: [1, 2, 3, 1, 4] }).error; + expect(error).to.be.an.error('"a[3]" contains a duplicate value'); + expect(error.details).to.equal([{ + context: { + key: 3, + label: 'a[3]', + pos: 3, + value: 1, + dupePos: 0, + dupeValue: 1 + }, + message: '"a[3]" contains a duplicate value', + path: ['a', 3], + type: 'array.unique' + }]); - expect(() => Joi.array().single().ordered(Joi.array())).to.throw('Cannot specify array item with single rule enabled'); - expect(() => Joi.array().single().ordered(Joi.alternatives([Joi.array()]))).to.throw('Cannot specify array item with single rule enabled'); + error = Joi.object({ a: Joi.array().items(Joi.number()).unique((a, b) => a === b) }).validate({ a: [1, 2, 3, 1, 4] }).error; + expect(error).to.be.an.error('"a[3]" contains a duplicate value'); + expect(error.details).to.equal([{ + context: { + key: 3, + label: 'a[3]', + pos: 3, + value: 1, + dupePos: 0, + dupeValue: 1 + }, + message: '"a[3]" contains a duplicate value', + path: ['a', 3], + type: 'array.unique' + }]); }); - it('switches the single flag with explicit value', () => { - - const schema = Joi.array().single(true); - const desc = schema.describe(); - expect(desc).to.equal({ - type: 'array', - flags: { sparse: false, single: true } - }); - }); + it('ignores duplicates if they are of different types', () => { - it('switches the single flag back', () => { + const schema = Joi.array().unique(); - const schema = Joi.array().single().single(false); - const desc = schema.describe(); - expect(desc).to.equal({ - type: 'array', - flags: { sparse: false, single: false } - }); + Helper.validate(schema, [ + [[2, '2'], true] + ]); }); - it('avoids unnecessary cloning when called twice', () => { + it('validates without duplicates', () => { - const schema = Joi.array().single(); - expect(schema.single()).to.shallow.equal(schema); + const buffer = Buffer.from('hello world'); + const buffer2 = Buffer.from('Hello world'); + const func = function () { }; + const func2 = function () { }; + const now = new Date(); + const now2 = new Date(+now + 100); + const schema = Joi.array().unique(); + + Helper.validate(schema, [ + [[1, 2], true], + [['s1', 's2'], true], + [[{ a: 'b' }, { a: 'c' }], true], + [[buffer, buffer2], true], + [[func, func2], true], + [[now, now2], true], + [[true, false], true] + ]); }); - }); - describe('options()', () => { + it('validates using a comparator', () => { - it('ignores stripUnknown when true', async () => { + const schema = Joi.array().unique((left, right) => left.a === right.a); - const schema = Joi.array().items(Joi.string()).prefs({ stripUnknown: true }); - await expect(schema.validate(['one', 'two', 3, 4, true, false])).to.reject('"[2]" must be a string'); + Helper.validate(schema, [ + [[{ a: 'b' }, { a: 'c' }], true], + [[{ a: 'b', c: 'd' }, { a: 'c', c: 'd' }], true], + [[{ a: 'b', c: 'd' }, { a: 'b', c: 'd' }], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: { a: 'b', c: 'd' }, + dupePos: 0, + dupeValue: { a: 'b', c: 'd' }, + label: '[1]', + key: 1 + } + }] + }], + [[{ a: 'b', c: 'c' }, { a: 'b', c: 'd' }], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: { a: 'b', c: 'd' }, + dupePos: 0, + dupeValue: { a: 'b', c: 'c' }, + label: '[1]', + key: 1 + } + }] + }] + ]); }); - it('respects stripUnknown (as an object)', async () => { + it('validates using a comparator with different types', () => { - const schema = Joi.array().items(Joi.string()).prefs({ stripUnknown: { arrays: true, objects: false } }); - const value = await schema.validate(['one', 'two', 3, 4, true, false]); - expect(value).to.equal(['one', 'two']); - }); - }); + const schema = Joi.array().items(Joi.string(), Joi.object({ a: Joi.string() })).unique((left, right) => { - describe('ordered()', () => { + if (typeof left === 'object') { + if (typeof right === 'object') { + return left.a === right.a; + } - it('shows path to errors in array ordered items', () => { + return left.a === right; + } - expect(() => { + if (typeof right === 'object') { + return left === right.a; + } - Joi.array().ordered({ - a: { - b: { - c: { - d: undefined - } + return left === right; + }); + + Helper.validate(schema, [ + [[{ a: 'b' }, { a: 'c' }], true], + [[{ a: 'b' }, 'c'], true], + [[{ a: 'b' }, 'c', { a: 'd' }, 'e'], true], + [[{ a: 'b' }, { a: 'b' }], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: { a: 'b' }, + dupePos: 0, + dupeValue: { a: 'b' }, + label: '[1]', + key: 1 } - } - }); - }).to.throw(Error, 'Invalid schema content: (0.a.b.c.d)'); + }] + }], + [[{ a: 'b' }, 'b'], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: 'b', + dupePos: 0, + dupeValue: { a: 'b' }, + label: '[1]', + key: 1 + } + }] + }] + ]); + }); - expect(() => { + it('validates using a path comparator', () => { - Joi.array().ordered({ foo: 'bar' }, undefined); - }).to.throw(Error, 'Invalid schema content: (1)'); - }); + let schema = Joi.array().items(Joi.object({ id: Joi.number() })).unique('id'); - it('validates input against items in order', async () => { + Helper.validate(schema, [ + [[{ id: 1 }, { id: 2 }, { id: 3 }], true], + [[{ id: 1 }, { id: 2 }, {}], true], + [[{ id: 1 }, { id: 2 }, { id: 1 }], false, null, { + message: '"[2]" contains a duplicate value', + details: [{ + context: { + dupePos: 0, + dupeValue: { id: 1 }, + key: 2, + label: '[2]', + path: 'id', + pos: 2, + value: { id: 1 } + }, + message: '"[2]" contains a duplicate value', + path: [2], + type: 'array.unique' + }] + }], + [[{ id: 1 }, { id: 2 }, {}, { id: 3 }, {}], false, null, { + message: '"[4]" contains a duplicate value', + details: [{ + context: { + dupePos: 2, + dupeValue: {}, + key: 4, + label: '[4]', + path: 'id', + pos: 4, + value: {} + }, + message: '"[4]" contains a duplicate value', + path: [4], + type: 'array.unique' + }] + }] + ]); - const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required()); - const input = ['s1', 2]; - const value = await schema.validate(input); - expect(value).to.equal(['s1', 2]); + schema = Joi.array().items(Joi.object({ nested: { id: Joi.number() } })).unique('nested.id'); + + Helper.validate(schema, [ + [[{ nested: { id: 1 } }, { nested: { id: 2 } }, { nested: { id: 3 } }], true], + [[{ nested: { id: 1 } }, { nested: { id: 2 } }, {}], true], + [[{ nested: { id: 1 } }, { nested: { id: 2 } }, { nested: { id: 1 } }], false, null, { + message: '"[2]" contains a duplicate value', + details: [{ + context: { + dupePos: 0, + dupeValue: { nested: { id: 1 } }, + key: 2, + label: '[2]', + path: 'nested.id', + pos: 2, + value: { nested: { id: 1 } } + }, + message: '"[2]" contains a duplicate value', + path: [2], + type: 'array.unique' + }] + }], + [[{ nested: { id: 1 } }, { nested: { id: 2 } }, {}, { nested: { id: 3 } }, {}], false, null, { + message: '"[4]" contains a duplicate value', + details: [{ + context: { + dupePos: 2, + dupeValue: {}, + key: 4, + label: '[4]', + path: 'nested.id', + pos: 4, + value: {} + }, + message: '"[4]" contains a duplicate value', + path: [4], + type: 'array.unique' + }] + }] + ]); + + schema = Joi.array().items(Joi.object({ nested: { id: Joi.number() } })).unique('nested'); + + Helper.validate(schema, [ + [[{ nested: { id: 1 } }, { nested: { id: 2 } }, { nested: { id: 3 } }], true], + [[{ nested: { id: 1 } }, { nested: { id: 2 } }, {}], true], + [[{ nested: { id: 1 } }, { nested: { id: 2 } }, { nested: { id: 1 } }], false, null, { + message: '"[2]" contains a duplicate value', + details: [{ + context: { + dupePos: 0, + dupeValue: { nested: { id: 1 } }, + key: 2, + label: '[2]', + path: 'nested', + pos: 2, + value: { nested: { id: 1 } } + }, + message: '"[2]" contains a duplicate value', + path: [2], + type: 'array.unique' + }] + }], + [[{ nested: { id: 1 } }, { nested: { id: 2 } }, {}, { nested: { id: 3 } }, {}], false, null, { + message: '"[4]" contains a duplicate value', + details: [{ + context: { + dupePos: 2, + dupeValue: {}, + key: 4, + label: '[4]', + path: 'nested', + pos: 4, + value: {} + }, + message: '"[4]" contains a duplicate value', + path: [4], + type: 'array.unique' + }] + }] + ]); }); - it('validates input with optional item', async () => { + it('ignores undefined value when ignoreUndefined is true', () => { - const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required(), Joi.number()); - const input = ['s1', 2, 3]; + const schema = Joi.array().unique('a', { ignoreUndefined: true }); - const value = await schema.validate(input); - expect(value).to.equal(['s1', 2, 3]); + Helper.validate(schema, [ + [[{ a: 'b' }, { a: 'c' }], true], + [[{ c: 'd' }, { c: 'd' }], true], + [[{ a: 'b', c: 'd' }, { a: 'b', c: 'd' }], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: { a: 'b', c: 'd' }, + dupePos: 0, + dupeValue: { a: 'b', c: 'd' }, + label: '[1]', + key: 1, + path: 'a' + } + }] + }], + [[{ a: 'b', c: 'c' }, { a: 'b', c: 'd' }], false, null, { + message: '"[1]" contains a duplicate value', + details: [{ + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique', + context: { + pos: 1, + value: { a: 'b', c: 'd' }, + dupePos: 0, + dupeValue: { a: 'b', c: 'c' }, + label: '[1]', + key: 1, + path: 'a' + } + }] + }] + ]); }); - it('validates input without optional item', async () => { - - const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required(), Joi.number()); - const input = ['s1', 2]; + it('fails with invalid configs', () => { - const value = await schema.validate(input); - expect(value).to.equal(['s1', 2]); + expect(() => Joi.array().unique('id', 'invalid configs')).to.throw(Error, 'configs must be an object'); + expect(() => Joi.array().unique('id', {})).to.not.throw(); }); - it('validates input without optional item', async () => { - - const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required(), Joi.number()).sparse(true); - const input = ['s1', 2, undefined]; + it('fails with invalid comparator', () => { - const value = await schema.validate(input); - expect(value).to.equal(['s1', 2, undefined]); + expect(() => Joi.array().unique({})).to.throw(Error, 'comparator must be a function or a string'); }); - it('validates input without optional item in a sparse array', async () => { + it('handles period in key names', async () => { - const schema = Joi.array().ordered(Joi.string().required(), Joi.number(), Joi.number().required()).sparse(true); - const input = ['s1', undefined, 3]; + const schema = Joi.array().unique('a.b', { separator: false }); - const value = await schema.validate(input); - expect(value).to.equal(['s1', undefined, 3]); + const test = [{ 'a.b': 1 }, { 'a.b': 2 }]; + expect(await schema.validate(test)).to.equal(test); }); + }); - it('validates when input matches ordered items and matches regular items', async () => { - - const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required()).items(Joi.number()); - const input = ['s1', 2, 3, 4, 5]; - const value = await schema.validate(input); - expect(value).to.equal(['s1', 2, 3, 4, 5]); - }); + describe('validate()', () => { - it('errors when input does not match ordered items', async () => { + it('should, by default, allow undefined, allow empty array', () => { - const schema = Joi.array().ordered(Joi.number().required(), Joi.string().required()); - const input = ['s1', 2]; - const err = await expect(schema.validate(input)).to.reject('"[0]" must be a number'); - expect(err.details).to.equal([{ - message: '"[0]" must be a number', - path: [0], - type: 'number.base', - context: { label: '[0]', key: 0, value: 's1' } - }]); + Helper.validate(Joi.array(), [ + [undefined, true], + [[], true] + ]); }); - it('errors when input has more items than ordered items', async () => { + it('should, when .required(), deny undefined', () => { - const schema = Joi.array().ordered(Joi.number().required(), Joi.string().required()); - const input = [1, 's2', 3]; - const err = await expect(schema.validate(input)).to.reject('"value" must contain at most 2 items'); - expect(err.details).to.equal([{ - message: '"value" must contain at most 2 items', - path: [], - type: 'array.orderedLength', - context: { pos: 2, limit: 2, label: 'value', value: input } - }]); + Helper.validate(Joi.array().required(), [ + [undefined, false, null, { + message: '"value" is required', + details: [{ + message: '"value" is required', + path: [], + type: 'any.required', + context: { label: 'value' } + }] + }] + ]); }); - it('errors when input has more items than ordered items with abortEarly = false', async () => { + it('allows empty arrays', () => { - const schema = Joi.array().ordered(Joi.string(), Joi.number()).prefs({ abortEarly: false }); - const input = [1, 2, 3, 4, 5]; - const err = await expect(schema.validate(input)).to.reject(); - expect(err).to.be.an.error('"[0]" must be a string. "value" must contain at most 2 items'); - expect(err.details).to.have.length(2); - expect(err.details).to.equal([ - { - message: '"[0]" must be a string', - path: [0], - type: 'string.base', - context: { value: 1, label: '[0]', key: 0 } - }, - { - message: '"value" must contain at most 2 items', - path: [], - type: 'array.orderedLength', - context: { pos: 2, limit: 2, label: 'value', value: input } - } + Helper.validate(Joi.array(), [ + [undefined, true], + [[], true] ]); }); - it('errors when input has less items than ordered items', async () => { + it('excludes values when items are forbidden', () => { - const schema = Joi.array().ordered(Joi.number().required(), Joi.string().required()); - const input = [1]; - const err = await expect(schema.validate(input)).to.reject('"value" does not contain 1 required value(s)'); - expect(err.details).to.equal([{ - message: '"value" does not contain 1 required value(s)', - path: [], - type: 'array.includesRequiredUnknowns', - context: { unknownMisses: 1, label: 'value', value: input } - }]); + Helper.validate(Joi.array().items(Joi.string().forbidden()), [ + [['2', '1'], false, null, { + message: '"[0]" contains an excluded value', + details: [{ + message: '"[0]" contains an excluded value', + path: [0], + type: 'array.excludes', + context: { pos: 0, value: '2', label: '[0]', key: 0 } + }] + }], + [['1'], false, null, { + message: '"[0]" contains an excluded value', + details: [{ + message: '"[0]" contains an excluded value', + path: [0], + type: 'array.excludes', + context: { pos: 0, value: '1', label: '[0]', key: 0 } + }] + }], + [[2], true] + ]); }); - it('errors when input matches ordered items but not matches regular items', async () => { - - const schema = Joi.array().ordered(Joi.string().required(), Joi.number().required()).items(Joi.number()).prefs({ abortEarly: false }); - const input = ['s1', 2, 3, 4, 's5']; - const err = await expect(schema.validate(input)).to.reject('"[4]" must be a number'); - expect(err.details).to.equal([{ - message: '"[4]" must be a number', - path: [4], - type: 'number.base', - context: { label: '[4]', key: 4, value: 's5' } - }]); - }); + it('allows types to be forbidden', async () => { - it('errors when input does not match ordered items but matches regular items', async () => { + const schema = Joi.array().items(Joi.number().forbidden()); - const schema = Joi.array().ordered(Joi.string(), Joi.number()).items(Joi.number()).prefs({ abortEarly: false }); - const input = [1, 2, 3, 4, 5]; - const err = await expect(schema.validate(input)).to.reject('"[0]" must be a string'); + const n = [1, 2, 'hippo']; + const err = await expect(schema.validate(n)).to.reject('"[0]" contains an excluded value'); expect(err.details).to.equal([{ - message: '"[0]" must be a string', + message: '"[0]" contains an excluded value', path: [0], - type: 'string.base', - context: { value: 1, label: '[0]', key: 0 } + type: 'array.excludes', + context: { pos: 0, value: 1, label: '[0]', key: 0 } }]); - }); - - it('errors when input does not match ordered items not matches regular items', async () => { - - const schema = Joi.array().ordered(Joi.string(), Joi.number()).items(Joi.string()).prefs({ abortEarly: false }); - const input = [1, 2, 3, 4, 5]; - const err = await expect(schema.validate(input)).to.reject(); - expect(err).to.be.an.error('"[0]" must be a string. "[2]" must be a string. "[3]" must be a string. "[4]" must be a string'); - expect(err.details).to.have.length(4); - expect(err.details).to.equal([ - { - message: '"[0]" must be a string', - path: [0], - type: 'string.base', - context: { value: 1, label: '[0]', key: 0 } - }, - { - message: '"[2]" must be a string', - path: [2], - type: 'string.base', - context: { value: 3, label: '[2]', key: 2 } - }, - { - message: '"[3]" must be a string', - path: [3], - type: 'string.base', - context: { value: 4, label: '[3]', key: 3 } - }, - { - message: '"[4]" must be a string', - path: [4], - type: 'string.base', - context: { value: 5, label: '[4]', key: 4 } - } - ]); - }); - - it('errors but continues when abortEarly is set to false', async () => { - const schema = Joi.array().ordered(Joi.number().required(), Joi.string().required()).prefs({ abortEarly: false }); - const input = ['s1', 2]; - const err = await expect(schema.validate(input)).to.reject(); - expect(err).to.be.an.error('"[0]" must be a number. "[1]" must be a string'); - expect(err.details).to.have.length(2); - expect(err.details).to.equal([ - { - message: '"[0]" must be a number', - path: [0], - type: 'number.base', - context: { label: '[0]', key: 0, value: 's1' } - }, - { - message: '"[1]" must be a string', - path: [1], - type: 'string.base', - context: { value: 2, label: '[1]', key: 1 } - } - ]); + const m = ['x', 'y', 'z']; + await schema.validate(m); }); - it('errors on sparse arrays and continues when abortEarly is set to false', () => { - - const schema = Joi.array().ordered( - Joi.number().min(0), - Joi.string().min(2), - Joi.number().max(0), - Joi.string().max(3) - ) - .prefs({ abortEarly: false }); + it('validates array of Numbers', () => { - Helper.validate(schema, [ - [[0, 'ab', 0, 'ab'], true], - [[undefined, 'foo', 2, 'bar'], false, null, { - message: '"[0]" must not be a sparse array item. "[2]" must be less than or equal to 0', - details: [{ - message: '"[0]" must not be a sparse array item', - path: [0], - type: 'array.sparse', - context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } - }, { - message: '"[2]" must be less than or equal to 0', - path: [2], - type: 'number.max', - context: { key: 2, label: '[2]', limit: 0, value: 2 } - }] - }], - [[undefined, 'foo', 2, undefined], false, null, { - message: '"[0]" must not be a sparse array item. "[2]" must be less than or equal to 0. "[3]" must not be a sparse array item', + Helper.validate(Joi.array().items(Joi.number()), [ + [[1, 2, 3], true], + [[50, 100, 1000], true], + [['a', 1, 2], false, null, { + message: '"[0]" must be a number', details: [{ - message: '"[0]" must not be a sparse array item', + message: '"[0]" must be a number', path: [0], - type: 'array.sparse', - context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } - }, { - message: '"[2]" must be less than or equal to 0', - path: [2], - type: 'number.max', - context: { key: 2, label: '[2]', limit: 0, value: 2 } - }, { - message: '"[3]" must not be a sparse array item', - path: [3], - type: 'array.sparse', - context: { key: 3, label: '[3]', path: [3], pos: 3, value: undefined } + type: 'number.base', + context: { label: '[0]', key: 0, value: 'a' } }] - }] + }], + [['1', '2', 4], true] ]); }); - it('errors on forbidden items and continues when abortEarly is set to false', () => { + it('validates array of mixed Numbers & Strings', () => { - const schema = Joi.array().items(Joi.bool().forbidden()).ordered( - Joi.number().min(0), - Joi.string().min(2), - Joi.number().max(0), - Joi.string().max(3) - ).prefs({ abortEarly: false }); + Helper.validate(Joi.array().items(Joi.number(), Joi.string()), [ + [[1, 2, 3], true], + [[50, 100, 1000], true], + [[1, 'a', 5, 10], true], + [['joi', 'everydaylowprices', 5000], true] + ]); + }); - Helper.validate(schema, [ - [[0, 'ab', 0, 'ab'], true], - [[undefined, 'foo', 2, 'bar'], false, null, { - message: '"[0]" must not be a sparse array item. "[2]" must be less than or equal to 0', + it('validates array of objects with schema', () => { + + Helper.validate(Joi.array().items(Joi.object({ h1: Joi.number().required() })), [ + [[{ h1: 1 }, { h1: 2 }, { h1: 3 }], true], + [[{ h2: 1, h3: 'somestring' }, { h1: 2 }, { h1: 3 }], false, null, { + message: '"[0].h1" is required', details: [{ - message: '"[0]" must not be a sparse array item', - path: [0], - type: 'array.sparse', - context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } - }, { - message: '"[2]" must be less than or equal to 0', - path: [2], - type: 'number.max', - context: { key: 2, label: '[2]', limit: 0, value: 2 } + message: '"[0].h1" is required', + path: [0, 'h1'], + type: 'any.required', + context: { label: '[0].h1', key: 'h1' } }] }], - [[undefined, 'foo', 2, undefined], false, null, { - message: '"[0]" must not be a sparse array item. "[2]" must be less than or equal to 0. "[3]" must not be a sparse array item', + [[1, 2, [1]], false, null, { + message: '"[0]" must be an object', details: [{ - message: '"[0]" must not be a sparse array item', + message: '"[0]" must be an object', path: [0], - type: 'array.sparse', - context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } - }, { - message: '"[2]" must be less than or equal to 0', - path: [2], - type: 'number.max', - context: { key: 2, label: '[2]', limit: 0, value: 2 } - }, { - message: '"[3]" must not be a sparse array item', - path: [3], - type: 'array.sparse', - context: { key: 3, label: '[3]', path: [3], pos: 3, value: undefined } + type: 'object.base', + context: { label: '[0]', key: 0, value: 1 } }] - }], - [[undefined, false, 2, undefined], false, null, { - message: '"[0]" must not be a sparse array item. "[1]" contains an excluded value. "[2]" must be less than or equal to 0. "[3]" must not be a sparse array item', + }] + ]); + }); + + it('errors on array of unallowed mixed types (Array)', () => { + + Helper.validate(Joi.array().items(Joi.number()), [ + [[1, 2, 3], true], + [[1, 2, [1]], false, null, { + message: '"[2]" must be a number', details: [{ - message: '"[0]" must not be a sparse array item', - path: [0], - type: 'array.sparse', - context: { key: 0, label: '[0]', path: [0], pos: 0, value: undefined } - }, { - message: '"[1]" contains an excluded value', - path: [1], - type: 'array.excludes', - context: { key: 1, label: '[1]', pos: 1, value: false } - }, { - message: '"[2]" must be less than or equal to 0', + message: '"[2]" must be a number', path: [2], - type: 'number.max', - context: { key: 2, label: '[2]', limit: 0, value: 2 } - }, { - message: '"[3]" must not be a sparse array item', - path: [3], - type: 'array.sparse', - context: { key: 3, label: '[3]', path: [3], pos: 3, value: undefined } + type: 'number.base', + context: { label: '[2]', key: 2, value: [1] } }] }] ]); }); - it('strips item', async () => { + it('errors on invalid number rule using includes', async () => { - const schema = Joi.array().ordered(Joi.string().required(), Joi.number().strip(), Joi.number().required()); - const input = ['s1', 2, 3]; - const value = await schema.validate(input); - expect(value).to.equal(['s1', 3]); + const schema = Joi.object({ + arr: Joi.array().items(Joi.number().integer()) + }); + + const input = { arr: [1, 2, 2.1] }; + const err = await expect(schema.validate(input)).to.reject('"arr[2]" must be an integer'); + expect(err.details).to.equal([{ + message: '"arr[2]" must be an integer', + path: ['arr', 2], + type: 'number.integer', + context: { value: 2.1, label: 'arr[2]', key: 2 } + }]); }); - it('strips multiple items', async () => { + it('validates an array within an object', () => { + + const schema = Joi.object({ + array: Joi.array().items(Joi.string().min(5), Joi.number().min(3)) + }).prefs({ convert: false }); + + Helper.validate(schema, [ + [{ array: ['12345'] }, true], + [{ array: ['1'] }, false, null, { + message: '"array[0]" does not match any of the allowed types', + details: [{ + message: '"array[0]" does not match any of the allowed types', + path: ['array', 0], + type: 'array.includes', + context: { pos: 0, value: '1', label: 'array[0]', key: 0 } + }] + }], + [{ array: [3] }, true], + [{ array: ['12345', 3] }, true] + ]); + }); + + it('should not change original value', async () => { + + const schema = Joi.array().items(Joi.number()).unique(); + const input = ['1', '2']; - const schema = Joi.array().ordered(Joi.string().strip(), Joi.number(), Joi.number().strip()); - const input = ['s1', 2, 3]; const value = await schema.validate(input); - expect(value).to.equal([2]); + expect(value).to.equal([1, 2]); + expect(input).to.equal(['1', '2']); }); - it('references array members', async () => { + it('returns multiple errors if abort early is false', async () => { - const schema = Joi.array().ordered(Joi.number(), Joi.number().greater(Joi.ref('..0'))); - expect(await schema.validate([1, 2])).to.equal([1, 2]); - await expect(schema.validate([1, 0])).to.reject(); + const schema = Joi.array().items(Joi.number(), Joi.object()).items(Joi.boolean().forbidden()); + const input = [1, undefined, true, 'a']; + + const err = await expect(Joi.validate(input, schema, { abortEarly: false })).to.reject(); + expect(err).to.be.an.error('"[1]" must not be a sparse array item. "[2]" contains an excluded value. "[3]" does not match any of the allowed types'); + expect(err.details).to.equal([{ + message: '"[1]" must not be a sparse array item', + path: [1], + type: 'array.sparse', + context: { + key: 1, + label: '[1]', + path: [1], + pos: 1, + value: undefined + } + }, { + message: '"[2]" contains an excluded value', + path: [2], + type: 'array.excludes', + context: { + pos: 2, + key: 2, + label: '[2]', + value: true + } + }, { + message: '"[3]" does not match any of the allowed types', + path: [3], + type: 'array.includes', + context: { + pos: 3, + key: 3, + label: '[3]', + value: 'a' + } + }]); + }); + + it('returns multiple errors if abort early is false across items() and unique()', async () => { + + const item = Joi.object({ + test: Joi.string(), + hello: Joi.string().required() + }); + + const schema = Joi.array().items(item).unique('test'); + + const input = [ + { + test: 'test', + hello: 'world' + }, + { + test: 'test' + } + ]; + + const err = await expect(Joi.validate(input, schema, { abortEarly: false })).to.reject('"[1].hello" is required. "[1]" contains a duplicate value'); + expect(err.details).to.equal([ + { + context: { + key: 'hello', + label: '[1].hello' + }, + message: '"[1].hello" is required', + path: [1, 'hello'], + type: 'any.required' + }, + { + context: { + dupePos: 0, + dupeValue: { + hello: 'world', + test: 'test' + }, + key: 1, + label: '[1]', + path: 'test', + pos: 1, + value: { + test: 'test' + } + }, + message: '"[1]" contains a duplicate value', + path: [1], + type: 'array.unique' + } + ]); }); }); });