@bedrockio/yada 1.2.7 → 1.2.9

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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ ## 1.2.9
2
+
3
+ - Fixed field missing on empty strings.
4
+
5
+ ## 1.2.8
6
+
7
+ - Added "missing" which runs a custom validator on undefined.
8
+
9
+ ## 1.2.7
10
+
11
+ - Removed private fields due to typescript bug
12
+
13
+ ## 1.2.6
14
+
15
+ - Changed types config
16
+
17
+ ## 1.2.5 bumped node version
package/README.md CHANGED
@@ -27,6 +27,7 @@ Concepts
27
27
  - [strip](#strip)
28
28
  - [nullable](#nullable)
29
29
  - [message](#message)
30
+ - [missing](#missing)
30
31
  - [Validation Options](#validation-options)
31
32
  - [Error Messages](#error-messages)
32
33
  - [Localization](#localization)
@@ -60,9 +61,9 @@ The schema will now validate input with `validate`:
60
61
  ```js
61
62
  await schema.validate({
62
63
  name: 'Jules',
63
- email: 'jules@gmail.com
64
+ email: 'jules@gmail.com',
64
65
  age: 25,
65
- })
66
+ });
66
67
  ```
67
68
 
68
69
  Note that the `validate` method is asynchronous so must be awaited with `await`
@@ -220,6 +221,15 @@ There are also transform methods that will transform input:
220
221
  - `uppercase` - Transforms input into upper case. Passing `true` as the first
221
222
  argument will instead error if input is not upper case.
222
223
 
224
+ Note that empty strings are allowed by default unless `required`. If you need an
225
+ optional string field that may not be empty, then use `options`:
226
+
227
+ ```js
228
+ const schema = yd.string().options({
229
+ allowEmpty: false,
230
+ });
231
+ ```
232
+
223
233
  ### Number
224
234
 
225
235
  Numbers have a handful of useful valiadtions:
@@ -444,6 +454,10 @@ The `custom` schema allows for custom validations expressed in code. A custom
444
454
  schema may either throw an error or transform the input by returning a value
445
455
  that is not `undefined`.
446
456
 
457
+ > [!WARNING] Note that `custom` will only be run if a value is passed other than
458
+ > `undefined`. To run more complex validations on optional fields, use the
459
+ > [`missing`](#missing) method.
460
+
447
461
  ```js
448
462
  const schema = yd.custom((val) => {
449
463
  if (val === 'foo') {
@@ -636,6 +650,29 @@ try {
636
650
  }
637
651
  ```
638
652
 
653
+ ### Missing
654
+
655
+ Allows custom validations when no value is passed:
656
+
657
+ ```js
658
+ const schema = yd.object({
659
+ type: yd.string().allow('email', 'phone'),
660
+ email: yd.string.email(),
661
+ phone: yd
662
+ .string()
663
+ .phone()
664
+ .missing(({ root }) => {
665
+ // Run this assertion when "phone" is undefined.
666
+ // Contrast this with "custom" which will never be run
667
+ // here as it is part of the valiation chain and bails
668
+ // after the string validation cannot be fulfilled.
669
+ if (root.type === 'phone') {
670
+ throw new Error('"phone" must be passed when "type" is "phone"');
671
+ }
672
+ }),
673
+ });
674
+ ```
675
+
639
676
  ## Validation Options
640
677
 
641
678
  Validation options in Yada can be passed at runtime on validation or baked into
@@ -8,7 +8,7 @@ exports.isSchema = isSchema;
8
8
  var _errors = require("./errors");
9
9
  var _utils = require("./utils");
10
10
  const INITIAL_TYPES = ['default', 'required', 'type', 'transform', 'empty'];
11
- const REQUIRED_TYPES = ['default', 'required'];
11
+ const REQUIRED_TYPES = ['default', 'required', 'missing'];
12
12
  class Schema {
13
13
  constructor(meta = {}) {
14
14
  this.assertions = [];
@@ -59,6 +59,20 @@ class Schema {
59
59
  });
60
60
  }
61
61
 
62
+ /**
63
+ * Validate by a custom function when no value passed. [Link](https://github.com/bedrockio/yada#missing)
64
+ * @param {Function} fn
65
+ * @returns {this}
66
+ */
67
+ missing(fn) {
68
+ if (!fn) {
69
+ throw new Error('Assertion function required.');
70
+ }
71
+ return this.clone().assert('missing', async (val, options) => {
72
+ return await fn(val, options);
73
+ });
74
+ }
75
+
62
76
  /**
63
77
  * Conditionally exclude fields inside an object schema.
64
78
  * [Link](https://github.com/bedrockio/yada#strip)
@@ -143,16 +157,11 @@ class Schema {
143
157
  original: value
144
158
  };
145
159
  for (let assertion of this.assertions) {
146
- if (value === undefined && !assertion.required) {
147
- break;
148
- } else if (value === null && options.nullable) {
149
- break;
160
+ if (this.canSkipAssertion(value, assertion, options)) {
161
+ continue;
150
162
  }
151
163
  try {
152
- const result = await this.runAssertion(assertion, value, options);
153
- if (result !== undefined) {
154
- value = result;
155
- }
164
+ value = await this.runAssertion(value, assertion, options);
156
165
  } catch (error) {
157
166
  const {
158
167
  type
@@ -340,6 +349,15 @@ class Schema {
340
349
  return this.getSortIndex(a.type) - this.getSortIndex(b.type);
341
350
  });
342
351
  }
352
+ canSkipAssertion(value, assertion, options) {
353
+ if (value === undefined) {
354
+ return !assertion.required;
355
+ } else if (value === null) {
356
+ return options.nullable;
357
+ } else {
358
+ return assertion.type === 'missing';
359
+ }
360
+ }
343
361
 
344
362
  /**
345
363
  * @returns {this}
@@ -356,11 +374,21 @@ class Schema {
356
374
  const index = INITIAL_TYPES.indexOf(type);
357
375
  return index === -1 ? INITIAL_TYPES.length : index;
358
376
  }
359
- async runAssertion(assertion, value, options = {}) {
377
+ async runAssertion(value, assertion, options = {}) {
360
378
  const {
379
+ type,
361
380
  fn
362
381
  } = assertion;
363
- return await fn(value, options);
382
+ let result;
383
+ if (type === 'missing') {
384
+ result = await fn(options);
385
+ } else {
386
+ result = await fn(value, options);
387
+ }
388
+ if (result !== undefined) {
389
+ return result;
390
+ }
391
+ return value;
364
392
  }
365
393
  enumToOpenApi() {
366
394
  const {
@@ -44,15 +44,21 @@ function getInnerPath(error, options) {
44
44
  }
45
45
  }
46
46
  function getLabeledMessage(error, options) {
47
+ const {
48
+ message
49
+ } = error;
47
50
  const {
48
51
  path = []
49
52
  } = options;
50
- const base = getBase(error.message);
51
53
  let template;
52
- if (base.includes('{field}')) {
53
- template = base;
54
- } else if (canAutoAddField(path, base)) {
55
- template = `{field} ${downcase(base)}`;
54
+ if (message.includes('{field}')) {
55
+ template = message;
56
+ } else if (canAutoAddField(path, message)) {
57
+ if (message === 'Value is required.') {
58
+ template = '{field} is required.';
59
+ } else {
60
+ template = `{field} ${downcase(message)}`;
61
+ }
56
62
  } else {
57
63
  template = error.message;
58
64
  }
@@ -61,10 +67,10 @@ function getLabeledMessage(error, options) {
61
67
  field: getFieldLabel(options)
62
68
  });
63
69
  } else {
64
- return (0, _localization.localize)(base);
70
+ return (0, _localization.localize)(message);
65
71
  }
66
72
  }
67
- const GENERIC_MESSAGE_REG = /^Must|is required\.?$/;
73
+ const GENERIC_MESSAGE_REG = /^(Must|Value is required\.)/;
68
74
 
69
75
  // Only "generic" error messages should automatically add the field.
70
76
  // A custom error message may be "Please verify you are human" which
@@ -84,13 +90,6 @@ function getFieldLabel(options) {
84
90
  return `"${path.join('.')}"`;
85
91
  }
86
92
  }
87
- function getBase(str) {
88
- if (str === 'Value is required.') {
89
- return 'is required.';
90
- } else {
91
- return str;
92
- }
93
- }
94
93
  function naturalize(str) {
95
94
  const first = str.slice(0, 1).toUpperCase();
96
95
  let rest = str.slice(1);
@@ -87,7 +87,12 @@ class ObjectSchema extends _TypeSchema.default {
87
87
  // Do not pass down message into validators
88
88
  // to allow custom messages to take precedence.
89
89
  options = (0, _utils.omit)(options, 'message');
90
- const result = await schema.validate(val, options);
90
+ const result = await schema.validate(val, {
91
+ ...options,
92
+ // Re-pass the object as root here as its fields
93
+ // may have been transformed by defaults.
94
+ root: obj
95
+ });
91
96
  if (result !== undefined) {
92
97
  return {
93
98
  ...obj,
@@ -35,7 +35,7 @@ class StringSchema extends _TypeSchema.default {
35
35
  allowEmpty
36
36
  } = options;
37
37
  if (val === '' && (required || allowEmpty === false)) {
38
- throw new _errors.LocalizedError('String may not be empty.');
38
+ throw new _errors.LocalizedError('Value is required.');
39
39
  }
40
40
  return val;
41
41
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrockio/yada",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Validation library inspired by Joi.",
5
5
  "scripts": {
6
6
  "test": "jest",
package/src/Schema.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  import { omit } from './utils';
10
10
 
11
11
  const INITIAL_TYPES = ['default', 'required', 'type', 'transform', 'empty'];
12
- const REQUIRED_TYPES = ['default', 'required'];
12
+ const REQUIRED_TYPES = ['default', 'required', 'missing'];
13
13
 
14
14
  export default class Schema {
15
15
  constructor(meta = {}) {
@@ -57,6 +57,20 @@ export default class Schema {
57
57
  });
58
58
  }
59
59
 
60
+ /**
61
+ * Validate by a custom function when no value passed. [Link](https://github.com/bedrockio/yada#missing)
62
+ * @param {Function} fn
63
+ * @returns {this}
64
+ */
65
+ missing(fn) {
66
+ if (!fn) {
67
+ throw new Error('Assertion function required.');
68
+ }
69
+ return this.clone().assert('missing', async (val, options) => {
70
+ return await fn(val, options);
71
+ });
72
+ }
73
+
60
74
  /**
61
75
  * Conditionally exclude fields inside an object schema.
62
76
  * [Link](https://github.com/bedrockio/yada#strip)
@@ -136,17 +150,12 @@ export default class Schema {
136
150
  };
137
151
 
138
152
  for (let assertion of this.assertions) {
139
- if (value === undefined && !assertion.required) {
140
- break;
141
- } else if (value === null && options.nullable) {
142
- break;
153
+ if (this.canSkipAssertion(value, assertion, options)) {
154
+ continue;
143
155
  }
144
156
 
145
157
  try {
146
- const result = await this.runAssertion(assertion, value, options);
147
- if (result !== undefined) {
148
- value = result;
149
- }
158
+ value = await this.runAssertion(value, assertion, options);
150
159
  } catch (error) {
151
160
  const { type } = assertion;
152
161
  const { message } = error;
@@ -322,6 +331,16 @@ export default class Schema {
322
331
  });
323
332
  }
324
333
 
334
+ canSkipAssertion(value, assertion, options) {
335
+ if (value === undefined) {
336
+ return !assertion.required;
337
+ } else if (value === null) {
338
+ return options.nullable;
339
+ } else {
340
+ return assertion.type === 'missing';
341
+ }
342
+ }
343
+
325
344
  /**
326
345
  * @returns {this}
327
346
  */
@@ -339,9 +358,18 @@ export default class Schema {
339
358
  return index === -1 ? INITIAL_TYPES.length : index;
340
359
  }
341
360
 
342
- async runAssertion(assertion, value, options = {}) {
343
- const { fn } = assertion;
344
- return await fn(value, options);
361
+ async runAssertion(value, assertion, options = {}) {
362
+ const { type, fn } = assertion;
363
+ let result;
364
+ if (type === 'missing') {
365
+ result = await fn(options);
366
+ } else {
367
+ result = await fn(value, options);
368
+ }
369
+ if (result !== undefined) {
370
+ return result;
371
+ }
372
+ return value;
345
373
  }
346
374
 
347
375
  enumToOpenApi() {
package/src/messages.js CHANGED
@@ -40,14 +40,18 @@ function getInnerPath(error, options) {
40
40
  }
41
41
 
42
42
  function getLabeledMessage(error, options) {
43
+ const { message } = error;
43
44
  const { path = [] } = options;
44
- const base = getBase(error.message);
45
45
 
46
46
  let template;
47
- if (base.includes('{field}')) {
48
- template = base;
49
- } else if (canAutoAddField(path, base)) {
50
- template = `{field} ${downcase(base)}`;
47
+ if (message.includes('{field}')) {
48
+ template = message;
49
+ } else if (canAutoAddField(path, message)) {
50
+ if (message === 'Value is required.') {
51
+ template = '{field} is required.';
52
+ } else {
53
+ template = `{field} ${downcase(message)}`;
54
+ }
51
55
  } else {
52
56
  template = error.message;
53
57
  }
@@ -57,11 +61,11 @@ function getLabeledMessage(error, options) {
57
61
  field: getFieldLabel(options),
58
62
  });
59
63
  } else {
60
- return localize(base);
64
+ return localize(message);
61
65
  }
62
66
  }
63
67
 
64
- const GENERIC_MESSAGE_REG = /^Must|is required\.?$/;
68
+ const GENERIC_MESSAGE_REG = /^(Must|Value is required\.)/;
65
69
 
66
70
  // Only "generic" error messages should automatically add the field.
67
71
  // A custom error message may be "Please verify you are human" which
@@ -80,14 +84,6 @@ function getFieldLabel(options) {
80
84
  }
81
85
  }
82
86
 
83
- function getBase(str) {
84
- if (str === 'Value is required.') {
85
- return 'is required.';
86
- } else {
87
- return str;
88
- }
89
- }
90
-
91
87
  function naturalize(str) {
92
88
  const first = str.slice(0, 1).toUpperCase();
93
89
  let rest = str.slice(1);
package/src/object.js CHANGED
@@ -70,7 +70,12 @@ class ObjectSchema extends TypeSchema {
70
70
  // Do not pass down message into validators
71
71
  // to allow custom messages to take precedence.
72
72
  options = omit(options, 'message');
73
- const result = await schema.validate(val, options);
73
+ const result = await schema.validate(val, {
74
+ ...options,
75
+ // Re-pass the object as root here as its fields
76
+ // may have been transformed by defaults.
77
+ root: obj,
78
+ });
74
79
  if (result !== undefined) {
75
80
  return {
76
81
  ...obj,
package/src/string.js CHANGED
@@ -37,7 +37,7 @@ class StringSchema extends TypeSchema {
37
37
  this.assert('empty', (val, options) => {
38
38
  const { required, allowEmpty } = options;
39
39
  if (val === '' && (required || allowEmpty === false)) {
40
- throw new LocalizedError('String may not be empty.');
40
+ throw new LocalizedError('Value is required.');
41
41
  }
42
42
  return val;
43
43
  });
package/types/Schema.d.ts CHANGED
@@ -18,6 +18,12 @@ export default class Schema {
18
18
  * @returns {this}
19
19
  */
20
20
  custom(fn: Function): this;
21
+ /**
22
+ * Validate by a custom function when no value passed. [Link](https://github.com/bedrockio/yada#missing)
23
+ * @param {Function} fn
24
+ * @returns {this}
25
+ */
26
+ missing(fn: Function): this;
21
27
  /**
22
28
  * Conditionally exclude fields inside an object schema.
23
29
  * [Link](https://github.com/bedrockio/yada#strip)
@@ -88,12 +94,13 @@ export default class Schema {
88
94
  */
89
95
  assert(type: any, fn: any): this;
90
96
  pushAssertion(assertion: any): void;
97
+ canSkipAssertion(value: any, assertion: any, options: any): any;
91
98
  /**
92
99
  * @returns {this}
93
100
  */
94
101
  transform(fn: any): this;
95
102
  getSortIndex(type: any): number;
96
- runAssertion(assertion: any, value: any, options?: {}): Promise<any>;
103
+ runAssertion(value: any, assertion: any, options?: {}): Promise<any>;
97
104
  enumToOpenApi(): any;
98
105
  }
99
106
  //# sourceMappingURL=Schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Schema.d.ts","sourceRoot":"","sources":["../src/Schema.js"],"names":[],"mappings":"AAmYA,kDAEC;AAxXD;IACE,uBAGC;IAFC,kBAAoB;IACpB,SAAgB;IAKlB;;OAEG;IACH,YAFa,IAAI,CAQhB;IAED;;;OAGG;IACH,mBAFa,IAAI,CAShB;IAED;;;;OAIG;IACH,sBAFa,IAAI,CAShB;IAED;;;;OAIG;IACH,mBAFa,IAAI,CAIhB;IAED;;;OAGG;IACH,sBAFa,IAAI,CAIhB;IAED;;;OAGG;IACH,uBAFa,IAAI,CAIhB;IAED;;;OAGG;IACH,YAFa,IAAI,CAIhB;IAED;;OAEG;IACH,uBAFa,IAAI,CAIhB;IAED;;OAEG;IACH,gBAFa,IAAI,CAShB;IAED;;OAEG;IACH,+BAFa,IAAI,CAMhB;IAED;;OAEG;IACH,uBAFa,IAAI,CAIhB;IAED,iDA8CC;IAED;;OAEG;IACH,kBAFa,IAAI,CAOhB;IAED;;;OAGG;IACH,qBAFa,IAAI,CAMhB;IAED,2BAYC;IAED;;MAOC;IAED;;;;MASC;IAED;;MAOC;IAED,4BAMC;IAED,kBAEC;IAID;;OAEG;IACH,kCAFa,IAAI,CAiDhB;IAED;;OAEG;IACH,4BAFa,IAAI,CAUhB;IAED,oCAKC;IAED;;OAEG;IACH,oBAFa,IAAI,CAShB;IAED,gCAGC;IAED,qEAGC;IAED,qBAsCC;CACF"}
1
+ {"version":3,"file":"Schema.d.ts","sourceRoot":"","sources":["../src/Schema.js"],"names":[],"mappings":"AA+ZA,kDAEC;AApZD;IACE,uBAGC;IAFC,kBAAoB;IACpB,SAAgB;IAKlB;;OAEG;IACH,YAFa,IAAI,CAQhB;IAED;;;OAGG;IACH,mBAFa,IAAI,CAShB;IAED;;;;OAIG;IACH,sBAFa,IAAI,CAShB;IAED;;;;OAIG;IACH,uBAFa,IAAI,CAShB;IAED;;;;OAIG;IACH,mBAFa,IAAI,CAIhB;IAED;;;OAGG;IACH,sBAFa,IAAI,CAIhB;IAED;;;OAGG;IACH,uBAFa,IAAI,CAIhB;IAED;;;OAGG;IACH,YAFa,IAAI,CAIhB;IAED;;OAEG;IACH,uBAFa,IAAI,CAIhB;IAED;;OAEG;IACH,gBAFa,IAAI,CAShB;IAED;;OAEG;IACH,+BAFa,IAAI,CAMhB;IAED;;OAEG;IACH,uBAFa,IAAI,CAIhB;IAED,iDAyCC;IAED;;OAEG;IACH,kBAFa,IAAI,CAOhB;IAED;;;OAGG;IACH,qBAFa,IAAI,CAMhB;IAED,2BAYC;IAED;;MAOC;IAED;;;;MASC;IAED;;MAOC;IAED,4BAMC;IAED,kBAEC;IAID;;OAEG;IACH,kCAFa,IAAI,CAiDhB;IAED;;OAEG;IACH,4BAFa,IAAI,CAUhB;IAED,oCAKC;IAED,gEAQC;IAED;;OAEG;IACH,oBAFa,IAAI,CAShB;IAED,gCAGC;IAED,qEAYC;IAED,qBAsCC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../src/object.js"],"names":[],"mappings":"AAiMA;;;;;;GAMG;AACH,uCAJW,SAAS,gBAMnB;wBAlMY;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,EAAE;AAD3C;;GAEG;AAEH;IAME,cAqEC;IAED,iBAEC;IAED;;OAEG;IAEH,YAHW,SAAS,GAAC,MAAM,gBAkC1B;IAED;;OAEG;IACH,gBAFc,MAAM,EAAA,gBAWnB;IAED;;OAEG;IACH,gBAFc,MAAM,EAAA,gBAWnB;CAcF;mBAtKgC,UAAU;uBAHpB,cAAc"}
1
+ {"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../src/object.js"],"names":[],"mappings":"AAsMA;;;;;;GAMG;AACH,uCAJW,SAAS,gBAMnB;wBAvMY;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,EAAE;AAD3C;;GAEG;AAEH;IAME,cA0EC;IAED,iBAEC;IAED;;OAEG;IAEH,YAHW,SAAS,GAAC,MAAM,gBAkC1B;IAED;;OAEG;IACH,gBAFc,MAAM,EAAA,gBAWnB;IAED;;OAEG;IACH,gBAFc,MAAM,EAAA,gBAWnB;CAcF;mBA3KgC,UAAU;uBAHpB,cAAc"}