@bedrockio/yada 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +13 -0
- package/README.md +750 -0
- package/babel.config.json +17 -0
- package/jest.config.json +5 -0
- package/package.json +38 -0
- package/scripts/build +4 -0
- package/src/Schema.js +262 -0
- package/src/TypeSchema.js +23 -0
- package/src/__tests__/index.js +1855 -0
- package/src/array.js +131 -0
- package/src/boolean.js +25 -0
- package/src/date.js +130 -0
- package/src/errors.js +92 -0
- package/src/index.js +51 -0
- package/src/localization.js +38 -0
- package/src/messages.js +72 -0
- package/src/number.js +84 -0
- package/src/object.js +102 -0
- package/src/password.js +61 -0
- package/src/string.js +303 -0
- package/src/utils.js +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
# Yada
|
|
2
|
+
|
|
3
|
+
Yada is a validation library inspired by [Joi](https://joi.dev/). The name
|
|
4
|
+
derives from a
|
|
5
|
+
[Japanese slang phrase](https://jisho.org/word/%E3%82%84%E3%81%A0) that
|
|
6
|
+
expresses rejection. Its goal is to provide an API for validation that is simple
|
|
7
|
+
and composable.
|
|
8
|
+
|
|
9
|
+
Concepts
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Getting Started](#getting-started)
|
|
13
|
+
- [Types](#types)
|
|
14
|
+
- [String](#string)
|
|
15
|
+
- [Number](#number)
|
|
16
|
+
- [Boolean](#boolean)
|
|
17
|
+
- [Array](#array)
|
|
18
|
+
- [Object](#object)
|
|
19
|
+
- [Date](#date)
|
|
20
|
+
- [Common Methods](#common-methods)
|
|
21
|
+
- [Allow](#allow)
|
|
22
|
+
- [Reject](#reject)
|
|
23
|
+
- [Custom](#custom)
|
|
24
|
+
- [Default](#default)
|
|
25
|
+
- [Validation Options](#validation-options)
|
|
26
|
+
- [Error Messages](#error-messages)
|
|
27
|
+
- [Localization](#localization)
|
|
28
|
+
- [OpenApi](#openapi)
|
|
29
|
+
- [Utils](#utils)
|
|
30
|
+
- [Differences with Joi](#differences-with-joi)
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```shell
|
|
35
|
+
yarn install @bedrockio/yada
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Getting Started
|
|
39
|
+
|
|
40
|
+
Validations are created by utility methods that build up a schema:\
|
|
41
|
+
(Note that the import `yd` is used here and below for brevity.)
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import yd from '@bedrockio/yada';
|
|
45
|
+
|
|
46
|
+
const schema = yd.object({
|
|
47
|
+
name: yd.string().required(),
|
|
48
|
+
email: yd.string().required(),
|
|
49
|
+
age: yd.number(),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The schema will now validate input with `validate`:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
await schema.validate({
|
|
57
|
+
name: 'Jules',
|
|
58
|
+
email: 'jules@gmail.com
|
|
59
|
+
age: 25,
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Note that the `validate` method is asynchronous so must be awaited with `await`
|
|
64
|
+
or `then/catch`. A schema that fails validation will throw an error:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
try {
|
|
68
|
+
await schema.validate({
|
|
69
|
+
name: 'Jules',
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Error is thrown as `email` is required.
|
|
73
|
+
console.info(error.details);
|
|
74
|
+
// [
|
|
75
|
+
// {
|
|
76
|
+
// field: 'email',
|
|
77
|
+
// message: '"email" is required.',
|
|
78
|
+
// },
|
|
79
|
+
// ],
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The thrown error object exposes a `details` property that describes validation
|
|
84
|
+
failures. [Type validations](#types) and `required()` will be run first and halt
|
|
85
|
+
execution. All other validations will be run sequentially. This means that all
|
|
86
|
+
validation issues will be identified up front:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const schema = yd.object({
|
|
90
|
+
password: yd
|
|
91
|
+
.string()
|
|
92
|
+
.custom((val) => {
|
|
93
|
+
if (val.length < 12) {
|
|
94
|
+
throw new Error('Must be 12 characters or more.');
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.custom((val) => {
|
|
98
|
+
if (!val.match(/[0-9]/)) {
|
|
99
|
+
throw new Error('Must contain at least 1 number.');
|
|
100
|
+
}
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
await schema.validate({
|
|
105
|
+
password: 'password',
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// error.details: [
|
|
109
|
+
// {
|
|
110
|
+
// ...
|
|
111
|
+
// field: 'password',
|
|
112
|
+
// details: [
|
|
113
|
+
// {
|
|
114
|
+
// type: 'custom',
|
|
115
|
+
// message: 'Must be 12 characters or more.',
|
|
116
|
+
// },
|
|
117
|
+
// {
|
|
118
|
+
// type: 'custom',
|
|
119
|
+
// message: 'Must contain at least 1 number.',
|
|
120
|
+
// }
|
|
121
|
+
// [
|
|
122
|
+
// },
|
|
123
|
+
// ],
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A validation that passes will return either the initial input or any
|
|
128
|
+
transformations that may apply to the schema:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
const schema = yd.date();
|
|
132
|
+
const date = await schema.validate('2020-01-01');
|
|
133
|
+
console.log(date instanceof Date); // true
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Custom validations can transform data simply by returning a value that is not
|
|
137
|
+
`undefined` in the validation function:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const schema = yd.string().custom((val) => {
|
|
141
|
+
return val.split(',');
|
|
142
|
+
});
|
|
143
|
+
const arr = await schema.validate('a,b,c');
|
|
144
|
+
console.log(arr); // ['a','b','c']
|
|
145
|
+
console.log(Array.isArray(arr)); // true
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Types
|
|
149
|
+
|
|
150
|
+
The core validation types are:
|
|
151
|
+
|
|
152
|
+
- [String](#string)
|
|
153
|
+
- [Number](#number)
|
|
154
|
+
- [Boolean](#boolean)
|
|
155
|
+
- [Array](#array)
|
|
156
|
+
- [Object](#object)
|
|
157
|
+
- [Date](#date)
|
|
158
|
+
|
|
159
|
+
These perform an initial type check on validation and will halt execution of any
|
|
160
|
+
further validations (except `required` which is run up front). The above types
|
|
161
|
+
are all "basic" types as they can be serialized as JSON with the exception of
|
|
162
|
+
[dates](#date).
|
|
163
|
+
|
|
164
|
+
### String
|
|
165
|
+
|
|
166
|
+
Strings are the most basic validation type. They are simple but come in many
|
|
167
|
+
different formats that can be validated against. Most string validations use
|
|
168
|
+
[validator](https://www.npmjs.com/package/validator) under the hood. Validations
|
|
169
|
+
that allow options will be passed through to that library.
|
|
170
|
+
|
|
171
|
+
#### Methods:
|
|
172
|
+
|
|
173
|
+
- `ascii` - Must be ASCII characters.
|
|
174
|
+
- `base64` - Must be base64 encoded. Allows options.
|
|
175
|
+
- `btc` - Must be a valid BTC address.
|
|
176
|
+
- `country` - Must be a valid
|
|
177
|
+
[ISO 3166-1 alpha 2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) code.
|
|
178
|
+
- `creditCard` - Must be a valid credit card number.
|
|
179
|
+
- `domain` - Must be a fully qualified domain name. Allows options.
|
|
180
|
+
- `email` - Must be a valid email address.
|
|
181
|
+
- `eth` - Must be a valid ETH address. Does not validate checksums.
|
|
182
|
+
- `hex` - Must be valid hexadecimal input.
|
|
183
|
+
- `ip` - Must be a valid IP address. Allows v4 or v6.
|
|
184
|
+
- `jwt` - Must be a valid JWT token.
|
|
185
|
+
- `latlng` - Must be a valid string representation of a lat/lng coordinate (ie.
|
|
186
|
+
`"35.686648,139.6975552"`).
|
|
187
|
+
- `locale` - Must be a valid locale code.
|
|
188
|
+
- `match` - Must match the regex pattern passed as an argument.
|
|
189
|
+
- `max` - Length must be less than equal to `n` characters, passed as an
|
|
190
|
+
argument.
|
|
191
|
+
- `md5` - Must be an MD5 hash.
|
|
192
|
+
- `min` - Length must be more than or equal to `n` characters, passed as an
|
|
193
|
+
argument.
|
|
194
|
+
- `mongo` - Must be a valid hexadecimal representation of a
|
|
195
|
+
[Mongo ObjectId](https://www.mongodb.com/docs/manual/reference/bson-types/#std-label-objectid).
|
|
196
|
+
- `password` - Must be a valid password as defined by the options object passed
|
|
197
|
+
as an argument:
|
|
198
|
+
- `minLength`
|
|
199
|
+
- `minLowercase`
|
|
200
|
+
- `minUppercase`
|
|
201
|
+
- `minNumbers`
|
|
202
|
+
- `minSymbols`
|
|
203
|
+
- `postalCode` - Must be a valid postal code. Accepts a `locale`.
|
|
204
|
+
- `sha1` - Must be a SHA1 hash.
|
|
205
|
+
- `slug` - Must be a URL slug, ie. a-z, numbers, or hyphen.
|
|
206
|
+
- `swift` - Must be a valid SWIFT (Bank Identification) code.
|
|
207
|
+
- `url` - Must be a valid URL. Allows options.
|
|
208
|
+
- `uuid` - Must be a valid UUID. Allows a `version` string.
|
|
209
|
+
|
|
210
|
+
There are also transform methods that will transform input:
|
|
211
|
+
|
|
212
|
+
- `trim` - Trims input with `String#trim`.
|
|
213
|
+
- `lowercase` - Transforms input into lower case. Passing `true` as the first
|
|
214
|
+
argument will instead error if input is not lower case.
|
|
215
|
+
- `uppercase` - Transforms input into upper case. Passing `true` as the first
|
|
216
|
+
argument will instead error if input is not upper case.
|
|
217
|
+
|
|
218
|
+
### Number
|
|
219
|
+
|
|
220
|
+
Numbers have a handful of useful valiadtions:
|
|
221
|
+
|
|
222
|
+
#### Methods:
|
|
223
|
+
|
|
224
|
+
- `integer` - Must be an integer.
|
|
225
|
+
- `max` - Must be less than or equal to `n`, passed as an argument.
|
|
226
|
+
- `min` - Must be greater than or equal to `n`, passed as an argument.
|
|
227
|
+
- `multiple` - Must be a multiple of `n`, passed as an argument.
|
|
228
|
+
- `negative` - Must be a negative number.
|
|
229
|
+
- `positive` - Must be a positive number.
|
|
230
|
+
|
|
231
|
+
### Boolean
|
|
232
|
+
|
|
233
|
+
`.boolean()` simply validates that input is a boolean value. There are no
|
|
234
|
+
further validation methods. Note that when `.required()` is used the input must
|
|
235
|
+
be a boolean, either `true` or `false`.
|
|
236
|
+
|
|
237
|
+
### Array
|
|
238
|
+
|
|
239
|
+
Array schemas validate that input is an array. By default elements may be of any
|
|
240
|
+
type, however however other schemas may be passed in as arguments:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
// Allows arrays containing either strings or numbers.
|
|
244
|
+
const schema = yd.array(yd.string(), yd.number());
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
A single array may also be passed in place of enumerated arguments:
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// Allows arrays containing either strings or numbers.
|
|
251
|
+
const schema = yd.array([schemas]);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### Methods:
|
|
255
|
+
|
|
256
|
+
- `max` - Length must be less than equal to `n` characters, passed as an
|
|
257
|
+
argument.
|
|
258
|
+
- `min` - Length must be more than or equal to `n` characters, passed as an
|
|
259
|
+
- `latlng` - Must be a 2 element array of numbers that represent latitude (`-90`
|
|
260
|
+
to `90`) and longitude(`-180` to `180`) coordinates.
|
|
261
|
+
|
|
262
|
+
## Object
|
|
263
|
+
|
|
264
|
+
Object schemas validate that input is an object and further validate individual
|
|
265
|
+
fields:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
const schema = yd.object({
|
|
269
|
+
name: yd.string().required(),
|
|
270
|
+
dob: yd.date(),
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Note that schema fields will only be validated if input exists:
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
// No object passed so no error thrown.
|
|
278
|
+
await schema.validate();
|
|
279
|
+
|
|
280
|
+
// Object passed but required field did
|
|
281
|
+
// not pass validation so error is thrown.
|
|
282
|
+
await schema.validate({
|
|
283
|
+
dob: '2020-01-01',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Passes
|
|
287
|
+
await schema.validate({
|
|
288
|
+
name: 'Jules',
|
|
289
|
+
dob: '2020-01-01',
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
To ensure that the object exists, simply use `.required()`.
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
const schema = yd
|
|
297
|
+
.object({
|
|
298
|
+
// ...
|
|
299
|
+
})
|
|
300
|
+
.required();
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### Methods:
|
|
304
|
+
|
|
305
|
+
- `append` - Appends the passed argument to create a new object schema. Accepts
|
|
306
|
+
either an object or another `.object()` schema.
|
|
307
|
+
|
|
308
|
+
## Date
|
|
309
|
+
|
|
310
|
+
Dates are similar to the basic types with the exception that in addition to date
|
|
311
|
+
objects they will also accept input that can resolve into a date. By default
|
|
312
|
+
this will include any string input or a number representing a timestamp in
|
|
313
|
+
milliseconds.
|
|
314
|
+
|
|
315
|
+
#### Methods:
|
|
316
|
+
|
|
317
|
+
- `after` - Input must resolve to a date after the supplied argument. Compare to
|
|
318
|
+
`min` which allows the date to be the same moment.
|
|
319
|
+
- `before` - Input must resolve to a date before the supplied argument. Compare
|
|
320
|
+
to `max` which allows the date to be the same moment.
|
|
321
|
+
- `future` - Input must resolve to a date in the future.
|
|
322
|
+
- `iso` - Input must be a string in
|
|
323
|
+
[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format which will resolve
|
|
324
|
+
into a date. Note that this format can reprsent both a date (`2020-01-01`) as
|
|
325
|
+
well as a datetime (`2020-01-01T00:00:00.000Z`).
|
|
326
|
+
- `max` - Input must resolve to a date before or equal to the supplied argument.
|
|
327
|
+
Compare to `before` which does not allow the input to be the same moment.
|
|
328
|
+
- `min` - Input must resolve to a date after or equal to the supplied argument.
|
|
329
|
+
Compare to `after` which does not allow the input to be the same moment.
|
|
330
|
+
- `past` - Input must resolve to a date in the past.
|
|
331
|
+
- `timestamp` - Input must be a number in milliseconds which will resolve into a
|
|
332
|
+
date.
|
|
333
|
+
- `unix - Input must be a number in seconds which will resolve into a date.
|
|
334
|
+
|
|
335
|
+
## Common Methods
|
|
336
|
+
|
|
337
|
+
The following methods are common to all schemas.
|
|
338
|
+
|
|
339
|
+
### Allow
|
|
340
|
+
|
|
341
|
+
Alternates are validated with the `allow` schema. This may be used to validate
|
|
342
|
+
literals as enums:
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
const schema = yd.string().allow('foo', 'bar');
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Or to allow alternates matching different schemas:
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
const schema = yd.allow(yd.string(), yd.number());
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
An array may also be passed here as the first argument:
|
|
355
|
+
|
|
356
|
+
```js
|
|
357
|
+
const schema = yd.object({
|
|
358
|
+
countries: yd.string.allow(countries),
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Reject
|
|
363
|
+
|
|
364
|
+
The `reject` schema is simply an inverse of `allow`. Anything that does not
|
|
365
|
+
match will be allowed through:
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
const schema = yd.string().reject('foo', 'bar');
|
|
369
|
+
await schema.validate('baz'); // pass
|
|
370
|
+
await schema.validate('foo'); // error!
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
const schema = yd.reject(yd.string(), yd.number());
|
|
375
|
+
await schema.validate(true); // pass
|
|
376
|
+
await schema.validate('true'); // error!
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Custom
|
|
380
|
+
|
|
381
|
+
The `custom` schema allows for custom validations expressed in code. A custom
|
|
382
|
+
schema may either throw an error or transform the input by returning a value
|
|
383
|
+
that is not `undefined`.
|
|
384
|
+
|
|
385
|
+
```js
|
|
386
|
+
const schema = yd.custom((val) => {
|
|
387
|
+
if (val === 'foo') {
|
|
388
|
+
throw new Error('"foo" is not allowed!');
|
|
389
|
+
} else if (val.startsWith('f')) {
|
|
390
|
+
return 'b' + val.slice(1);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
console.info(await schema.validate('fad')); // "bad"
|
|
394
|
+
console.info(await schema.validate('far')); // "bar"
|
|
395
|
+
console.info(await schema.validate('foo')); // error thrown: "foo" is not allowed!
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### Arguments
|
|
399
|
+
|
|
400
|
+
Custom validators are passed two arguments: the initial `value` and an `options`
|
|
401
|
+
object that contains meta information that can be helpful:
|
|
402
|
+
|
|
403
|
+
- `root`: The root value passed into the validation. This is helpful to create
|
|
404
|
+
complex validations that depend on other fields, etc.
|
|
405
|
+
- `original`: The original input passed in before transformation.
|
|
406
|
+
- `...rest`: Other fields specific to the schema as well as any options passed
|
|
407
|
+
into the schema when validating.
|
|
408
|
+
|
|
409
|
+
#### Examples
|
|
410
|
+
|
|
411
|
+
Validating input that depends on another field in an object schema:
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
const schema = yd.object({
|
|
415
|
+
age: yd.number(),
|
|
416
|
+
parentConsent: yd.boolean().custom((val, { root }) => {
|
|
417
|
+
if (!val && root.age < 13) {
|
|
418
|
+
throw new Error('Parent consent must be given if you are under 13.');
|
|
419
|
+
}
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Validating input that depends on options passed in at runtime:
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
const schema = yd.custom((val, options) => {
|
|
428
|
+
if (options.foo) {
|
|
429
|
+
// Do something
|
|
430
|
+
} else {
|
|
431
|
+
// Do soemthing else
|
|
432
|
+
}
|
|
433
|
+
}));
|
|
434
|
+
|
|
435
|
+
await schema.validate('test', {
|
|
436
|
+
foo: 'bar'
|
|
437
|
+
})
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Asynchronous validation from an external service:
|
|
441
|
+
|
|
442
|
+
```js
|
|
443
|
+
const schema = yd.object({
|
|
444
|
+
numbers: yd.string().custom(async (val, { root }) => {
|
|
445
|
+
try {
|
|
446
|
+
const info = await validateCreditCard({
|
|
447
|
+
number: val,
|
|
448
|
+
cvc: root.cvc,
|
|
449
|
+
});
|
|
450
|
+
// Return a transformed object that represents the credit card.
|
|
451
|
+
return info;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throw new Error('Could not validate credit card number');
|
|
454
|
+
}
|
|
455
|
+
}),
|
|
456
|
+
cvc: yd.string().required(),
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Default
|
|
461
|
+
|
|
462
|
+
The `default` method will return a default value if one is not passed:
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
const schema = yd.string().default('hi!');
|
|
466
|
+
console.log(await schema.validate()); // "hi!"
|
|
467
|
+
console.log(await schema.validate('hello!')); // "hello!"
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Validation Options
|
|
471
|
+
|
|
472
|
+
Validation options in Yada can be passed at runtime on validation or baked into
|
|
473
|
+
the schema with the `options` method. The following can be considered
|
|
474
|
+
equivalent:
|
|
475
|
+
|
|
476
|
+
```js
|
|
477
|
+
await schema.validate(input, {
|
|
478
|
+
stripUnknown: true,
|
|
479
|
+
});
|
|
480
|
+
await schema
|
|
481
|
+
.options({
|
|
482
|
+
stripUnknown: true,
|
|
483
|
+
})
|
|
484
|
+
.validate(input);
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
#### Options:
|
|
488
|
+
|
|
489
|
+
- `stripUnknown` - Strips unknown fields on object schemas which otherwise would
|
|
490
|
+
throw an error.
|
|
491
|
+
- `cast` - Casts input to its associated type. For strings and numbers this
|
|
492
|
+
performs a simple type coercion. For booleans `"true"` or `"1"` will be
|
|
493
|
+
considered `true` and `"false"` or `"0"` will be considered `false`. This
|
|
494
|
+
option is useful to convert query strings.
|
|
495
|
+
|
|
496
|
+
## Error Messages
|
|
497
|
+
|
|
498
|
+
Error objects thrown on validation expose details about the validations and
|
|
499
|
+
fields which have errored. The `getFullMessage` method aggregates these as a
|
|
500
|
+
single message separated by a space:
|
|
501
|
+
|
|
502
|
+
```js
|
|
503
|
+
const schema = yd.object({
|
|
504
|
+
email: yd.string().required(),
|
|
505
|
+
firstName: yd.string().required(),
|
|
506
|
+
lastName: yd.string().required(),
|
|
507
|
+
});
|
|
508
|
+
try {
|
|
509
|
+
await schema.validate({
|
|
510
|
+
firstName: 'Jules',
|
|
511
|
+
});
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.info(error.getFullMessage());
|
|
514
|
+
// "email" is required "lastName" is required
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The delimiter can be customized with the `delimiter` option:
|
|
519
|
+
|
|
520
|
+
```js
|
|
521
|
+
console.info(
|
|
522
|
+
error.getFullMessage({
|
|
523
|
+
delimiter: '\n',
|
|
524
|
+
})
|
|
525
|
+
);
|
|
526
|
+
// "email" is required
|
|
527
|
+
// "lastName" is required
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Further the `natural` option will capitalize field names to correspond with form
|
|
531
|
+
labels:
|
|
532
|
+
|
|
533
|
+
```js
|
|
534
|
+
console.info(
|
|
535
|
+
error.getFullMessage({
|
|
536
|
+
natural: true,
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
// "Email" is required
|
|
540
|
+
// "Last Name" is required
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Localization
|
|
544
|
+
|
|
545
|
+
Any error message can be localized with the `useLocalizer` method. This method
|
|
546
|
+
accepts either a map of error messages to their localized strings or a function
|
|
547
|
+
which returns that map:
|
|
548
|
+
|
|
549
|
+
```js
|
|
550
|
+
yd.useLocalizer({
|
|
551
|
+
'Must be at least {length} character{s}.': '{length}文字以上入力して下さい。',
|
|
552
|
+
'Object failed validation.': '不正な入力がありました。',
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
or
|
|
557
|
+
|
|
558
|
+
```js
|
|
559
|
+
yd.useLocalizer((template) => {
|
|
560
|
+
// Where "getMessages" could be a function
|
|
561
|
+
// that returns messages for a specific locale.
|
|
562
|
+
return getMessages()[template];
|
|
563
|
+
});
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Any validation message matching the keys passed to the map will result in the
|
|
567
|
+
localized message instead. The curly braces allow for variable substitution.
|
|
568
|
+
|
|
569
|
+
A special method `getLocalizerTemplates` will aggregte used error messages to
|
|
570
|
+
allow quick discovery of strings that have not yet been localized:
|
|
571
|
+
|
|
572
|
+
```js
|
|
573
|
+
yd.useLocalizer({
|
|
574
|
+
'Must be at least {length} character{s}.': '{length}文字以上入力して下さい。',
|
|
575
|
+
'Object failed validation.': '不正な入力がありました。',
|
|
576
|
+
});
|
|
577
|
+
// Error validation occuring here
|
|
578
|
+
yd.getLocalizedTemplates();
|
|
579
|
+
// {
|
|
580
|
+
// 'Must be at least {length} character{s}.': '{length}文字以上入力して下さい。',
|
|
581
|
+
// 'Object failed validation.': '不正な入力がありました。',
|
|
582
|
+
// 'Value must be a string.': 'Value must be a string.',
|
|
583
|
+
// ...etc
|
|
584
|
+
// }
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Finally an exported error object `LocalizedError` allows you to throw custom
|
|
588
|
+
localized error messages. Template substitution can be used here as well:
|
|
589
|
+
|
|
590
|
+
```js
|
|
591
|
+
import { default as yd, LocalizedError } from '@bedrockio/yada';
|
|
592
|
+
|
|
593
|
+
const max = 5;
|
|
594
|
+
const schema = yd.custom((val) => {
|
|
595
|
+
if (val > chars) {
|
|
596
|
+
throw new LocalizedError('Cannot be greater than {max}.', {
|
|
597
|
+
max,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## OpenApi
|
|
604
|
+
|
|
605
|
+
One of the benefits of Yada is built-in OpenApi integration. Any schema can
|
|
606
|
+
describe itself in OpenApi format including complex nested schemas.
|
|
607
|
+
|
|
608
|
+
```js
|
|
609
|
+
yd.string().allow('foo', 'bar').toOpenApi();
|
|
610
|
+
// {
|
|
611
|
+
// type: 'string',
|
|
612
|
+
// enum: ['foo', 'bar'],
|
|
613
|
+
// }
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
The `tag` method allows tagging custom OpenApi fields:
|
|
617
|
+
|
|
618
|
+
```js
|
|
619
|
+
yd.string().tag({
|
|
620
|
+
description: 'my description!',
|
|
621
|
+
x-field: 'my custom field!',
|
|
622
|
+
}).toOpenApi();
|
|
623
|
+
// {
|
|
624
|
+
// type: 'string',
|
|
625
|
+
// description: 'my description!',
|
|
626
|
+
// x-field: 'my custom field!!',
|
|
627
|
+
// }
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
The `description` method is a shortcut:
|
|
631
|
+
|
|
632
|
+
```js
|
|
633
|
+
yd.string().description('my description!').toOpenApi();
|
|
634
|
+
// {
|
|
635
|
+
// type: 'string',
|
|
636
|
+
// description: 'my description!',
|
|
637
|
+
// }
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## Utils
|
|
641
|
+
|
|
642
|
+
Basic utility methods:
|
|
643
|
+
|
|
644
|
+
- `isSchema`: returns `true` if the object passed is a schema.
|
|
645
|
+
- `isSchemaError`: returns `true` if the error object is a schema error.
|
|
646
|
+
- `useLocalizer`: Allows [localization](#localization) of error messages.
|
|
647
|
+
- `getLocalizerTemplates`: Allows discovery of messages for
|
|
648
|
+
[localization](#localization).
|
|
649
|
+
- `LocalizedError`: An error object that can be thrown in custom validations to
|
|
650
|
+
allow [localization](#localization).
|
|
651
|
+
|
|
652
|
+
## Differences with Joi
|
|
653
|
+
|
|
654
|
+
Yada is intentionally kept extremely simple. Complex validations are expressed
|
|
655
|
+
in code rather than requiring complex API knowledge. For example (from the Joi
|
|
656
|
+
docs):
|
|
657
|
+
|
|
658
|
+
```js
|
|
659
|
+
// Joi schema
|
|
660
|
+
const schema = Joi.object({
|
|
661
|
+
a: Joi.any()
|
|
662
|
+
.valid('x')
|
|
663
|
+
.when('b', {
|
|
664
|
+
is: Joi.exist(),
|
|
665
|
+
then: Joi.valid('y'),
|
|
666
|
+
otherwise: Joi.valid('z'),
|
|
667
|
+
})
|
|
668
|
+
.when('c', { is: Joi.number().min(10), then: Joi.forbidden() }),
|
|
669
|
+
b: Joi.any(),
|
|
670
|
+
c: Joi.number(),
|
|
671
|
+
});
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
```js
|
|
675
|
+
// equivalent yada schema
|
|
676
|
+
const schema = yd.object({
|
|
677
|
+
a: yd
|
|
678
|
+
.custom((val, { root }) => {
|
|
679
|
+
if (root.b && val !== 'y') {
|
|
680
|
+
throw new Error('x must be y when b is passed');
|
|
681
|
+
} else if (val !== 'z') {
|
|
682
|
+
throw new Error('x must be z when b is not passed');
|
|
683
|
+
}
|
|
684
|
+
})
|
|
685
|
+
.custom((val, { root }) => {
|
|
686
|
+
if (typeof root.c > 10 && val) {
|
|
687
|
+
throw new Error('x may not be passed when c is more than 10');
|
|
688
|
+
}
|
|
689
|
+
}),
|
|
690
|
+
b: yd.string(),
|
|
691
|
+
c: yd.number(),
|
|
692
|
+
});
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Note: yada has no "any" equivalent. schemas must either define their types or
|
|
696
|
+
use "allow" for alternate types.
|
|
697
|
+
|
|
698
|
+
For a few more lines of code the same schema can be defined simply as readable
|
|
699
|
+
Javascript, complete with custom error messages.
|
|
700
|
+
|
|
701
|
+
Additionally, schemas may be validated inside other custom schemas, as they are
|
|
702
|
+
simply functions that throw an error or (optionally) return a transformed value.
|
|
703
|
+
This makes even custom schema composition simple:
|
|
704
|
+
|
|
705
|
+
```js
|
|
706
|
+
// Joi schema
|
|
707
|
+
const states = Joi.valid(UsStates);
|
|
708
|
+
const provinces = Joi.valid(JapanProvinces);
|
|
709
|
+
const schema = Joi.object({
|
|
710
|
+
region: Joi.string().when('country', {
|
|
711
|
+
is: 'japan',
|
|
712
|
+
then: provinces,
|
|
713
|
+
otherwise: states,
|
|
714
|
+
}),
|
|
715
|
+
});
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
```js
|
|
719
|
+
// equivalent yada schema
|
|
720
|
+
const states = yd.allow(UsStates);
|
|
721
|
+
const provinces = yd.allow(JapanProvinces);
|
|
722
|
+
const schema = yd.object({
|
|
723
|
+
region: yd.string().custom(async (val, { root }) => {
|
|
724
|
+
const schema = root.country === 'japan' ? provinces : states;
|
|
725
|
+
await schema.validate(val);
|
|
726
|
+
}),
|
|
727
|
+
});
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
Since all custom validators are simply functions that throw errors, the above
|
|
731
|
+
can also use a simple `try/catch` to throw custom error messages:
|
|
732
|
+
|
|
733
|
+
```js
|
|
734
|
+
// equivalent yada schema
|
|
735
|
+
const states = yd.allow(UsStates);
|
|
736
|
+
const provinces = yd.allow(JapanProvinces);
|
|
737
|
+
const schema = yd.object({
|
|
738
|
+
region: yd.string().custom(async (val, { root }) => {
|
|
739
|
+
const schema = root.country === 'japan' ? provinces : states;
|
|
740
|
+
try {
|
|
741
|
+
await schema.validate(val);
|
|
742
|
+
} catch {
|
|
743
|
+
throw new Error('"region" is invalid.');
|
|
744
|
+
}
|
|
745
|
+
}),
|
|
746
|
+
});
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
This can be useful either to add context or to strip it away, for example to
|
|
750
|
+
hide allowed values if they are private.
|