@bedrockio/yada 1.0.39 → 1.1.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/README.md CHANGED
@@ -19,12 +19,13 @@ Concepts
19
19
  - [Object](#object)
20
20
  - [Date](#date)
21
21
  - [Common Methods](#common-methods)
22
- - [Allow](#allow)
23
- - [Reject](#reject)
24
- - [Append](#append)
25
- - [Custom](#custom)
26
- - [Default](#default)
27
- - [Strip](#strip)
22
+ - [allow](#allow)
23
+ - [reject](#reject)
24
+ - [append](#append)
25
+ - [custom](#custom)
26
+ - [default](#default)
27
+ - [strip](#strip)
28
+ - [message](#message)
28
29
  - [Validation Options](#validation-options)
29
30
  - [Error Messages](#error-messages)
30
31
  - [Localization](#localization)
@@ -552,6 +553,65 @@ const schema = yd.object({
552
553
  Arguments are identical to those passed to [custom](#custom). The field will be
553
554
  stripped out if the function returns a truthy value.
554
555
 
556
+ ### Message
557
+
558
+ The `message` method allows adding a custom message to schema or field. Note
559
+ that when using with [`getFullMessage`](#error-messages) the custom message will
560
+ not include the nested field name by default:
561
+
562
+ ```js
563
+ const schema = yd.object({
564
+ name: yd.string().required().message('Please provide your full name.'),
565
+ });
566
+ try {
567
+ await schema.validate({});
568
+ } catch (error) {
569
+ console.log(error.getFullMessage());
570
+ // -> Please provide your full name.
571
+ }
572
+ ```
573
+
574
+ To include the field name, the `{field}` token may be used in the message:
575
+
576
+ ```js
577
+ const schema = yd.object({
578
+ name: yd
579
+ .string()
580
+ .match(/^[A-Z]/)
581
+ .message('{field} must start with an uppercase letter.'),
582
+ });
583
+ try {
584
+ await schema.validate({
585
+ name: 'frank',
586
+ });
587
+ } catch (error) {
588
+ console.log(error.getFullMessage());
589
+ // -> "name" must start with an uppercase letter.
590
+ }
591
+ ```
592
+
593
+ The `{field}` token may also be used when throwing custom errors:
594
+
595
+ ```js
596
+ const schema = yd.object({
597
+ profile: yd.object({
598
+ name: yd.custom((value) => {
599
+ if (value !== 'Frank') {
600
+ throw new Error('{field} must be "Frank".');
601
+ }
602
+ }),
603
+ }),
604
+ });
605
+ try {
606
+ await schema.validate({
607
+ name: 'Bob',
608
+ });
609
+ } catch (error) {
610
+ console.log(error.getFullMessage());
611
+ // -> "name" must be "Frank".
612
+ }
613
+ ```
614
+
555
615
  ## Validation Options
556
616
 
557
617
  Validation options in Yada can be passed at runtime on validation or baked into
@@ -634,7 +694,6 @@ which returns that map:
634
694
  ```js
635
695
  yd.useLocalizer({
636
696
  'Must be at least {length} character{s}.': '{length}文字以上入力して下さい。',
637
- 'Object failed validation.': '不正な入力がありました。',
638
697
  });
639
698
  ```
640
699
 
@@ -657,13 +716,11 @@ allow quick discovery of strings that have not yet been localized:
657
716
  ```js
658
717
  yd.useLocalizer({
659
718
  'Must be at least {length} character{s}.': '{length}文字以上入力して下さい。',
660
- 'Object failed validation.': '不正な入力がありました。',
661
719
  });
662
720
  // Error validation occuring here
663
721
  yd.getLocalizedMessages();
664
722
  // {
665
723
  // 'Must be at least {length} character{s}.': '{length}文字以上入力して下さい。',
666
- // 'Object failed validation.': '不正な入力がありました。',
667
724
  // 'Value must be a string.': 'Value must be a string.',
668
725
  // ...etc
669
726
  // }
@@ -13,7 +13,6 @@ const REQUIRED_TYPES = ['default', 'required'];
13
13
  /**
14
14
  * @typedef {[fn: Function] | [type: string, fn: Function]} CustomSignature
15
15
  */
16
-
17
16
  class Schema {
18
17
  constructor(meta = {}) {
19
18
  this.assertions = [];
@@ -157,8 +156,15 @@ class Schema {
157
156
  value = result;
158
157
  }
159
158
  } catch (error) {
160
- if (error instanceof _errors.ArrayError) {
161
- details = [...details, ...error.details];
159
+ const {
160
+ type
161
+ } = assertion;
162
+ if (type === 'type') {
163
+ details.push(new _errors.TypeError(error, this.meta.type));
164
+ } else if (type === 'format') {
165
+ details.push(new _errors.FormatError(error, this.meta.format));
166
+ } else if (!error.type) {
167
+ details.push(new _errors.AssertionError(error, type));
162
168
  } else {
163
169
  details.push(error);
164
170
  }
@@ -168,10 +174,7 @@ class Schema {
168
174
  }
169
175
  }
170
176
  if (details.length) {
171
- const {
172
- message = 'Input failed validation.'
173
- } = this.meta;
174
- throw new _errors.ValidationError(message, details);
177
+ throw new _errors.ValidationError(this.meta.message, details);
175
178
  }
176
179
  return value;
177
180
  }
@@ -266,7 +269,7 @@ class Schema {
266
269
  }
267
270
  return el;
268
271
  });
269
- const msg = `${allow ? 'Must' : 'Must not'} be one of [{types}].`;
272
+ const message = `${allow ? 'Must' : 'Must not'} be one of [{types}].`;
270
273
  return this.clone({
271
274
  enum: set
272
275
  }).assert('enum', async (val, options) => {
@@ -279,14 +282,23 @@ class Schema {
279
282
  // example allowing a string or array of strings.
280
283
  options = (0, _utils.omit)(options, 'cast');
281
284
  return await el.validate(val, options);
282
- } catch (error) {
283
- continue;
285
+ } catch (err) {
286
+ const [first] = err.details;
287
+ if (first instanceof _errors.TypeError) {
288
+ // If the error is a simple type error then continue
289
+ // to show more meaningful messages for simple enums.
290
+ continue;
291
+ } else {
292
+ // Otherwise throw the error to surface messages on
293
+ // more complex schemas.
294
+ throw err;
295
+ }
284
296
  }
285
297
  } else if (el === val === allow) {
286
298
  return;
287
299
  }
288
300
  }
289
- throw new _errors.LocalizedError(options.message || msg, {
301
+ throw new _errors.LocalizedError(message, {
290
302
  types: types.join(', ')
291
303
  });
292
304
  }
@@ -329,17 +341,9 @@ class Schema {
329
341
  }
330
342
  async runAssertion(assertion, value, options = {}) {
331
343
  const {
332
- type,
333
344
  fn
334
345
  } = assertion;
335
- try {
336
- return await fn(value, options);
337
- } catch (error) {
338
- if ((0, _errors.isSchemaError)(error)) {
339
- throw error;
340
- }
341
- throw new _errors.AssertionError(error.message, error.type || type, error);
342
- }
346
+ return await fn(value, options);
343
347
  }
344
348
  enumToOpenApi() {
345
349
  const {
@@ -10,8 +10,8 @@ class TypeSchema extends _Schema.default {
10
10
  constructor(Class, meta) {
11
11
  const type = Class.name.toLowerCase();
12
12
  super({
13
- type,
14
- ...meta
13
+ ...meta,
14
+ type
15
15
  });
16
16
  }
17
17
  format(name, fn) {
package/dist/cjs/array.js CHANGED
@@ -5,13 +5,13 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = _default;
7
7
  var _Schema = _interopRequireDefault(require("./Schema"));
8
+ var _TypeSchema = _interopRequireDefault(require("./TypeSchema"));
8
9
  var _errors = require("./errors");
9
10
  var _utils = require("./utils");
10
11
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
- class ArraySchema extends _Schema.default {
12
+ class ArraySchema extends _TypeSchema.default {
12
13
  constructor(schemas) {
13
- super({
14
- message: 'Array failed validation.',
14
+ super(Array, {
15
15
  schemas
16
16
  });
17
17
  this.setup();
@@ -22,8 +22,7 @@ class ArraySchema extends _Schema.default {
22
22
  */
23
23
  setup() {
24
24
  const {
25
- schemas,
26
- message
25
+ schemas
27
26
  } = this.meta;
28
27
  const schema = schemas.length > 1 ? new _Schema.default().allow(schemas) : schemas[0];
29
28
  this.assert('type', (val, options) => {
@@ -36,6 +35,9 @@ class ArraySchema extends _Schema.default {
36
35
  return val;
37
36
  });
38
37
  if (schema) {
38
+ const {
39
+ message
40
+ } = schema.meta;
39
41
  this.assert('elements', async (arr, options) => {
40
42
  const errors = [];
41
43
  const result = [];
@@ -47,7 +49,7 @@ class ArraySchema extends _Schema.default {
47
49
  options = (0, _utils.omit)(options, 'message');
48
50
  result.push(await schema.validate(el, options));
49
51
  } catch (error) {
50
- errors.push(new _errors.ElementError('Element failed validation.', i, error.original, error.details));
52
+ errors.push(new _errors.ElementError(message, i, error.details));
51
53
  }
52
54
  }
53
55
  if (errors.length) {
@@ -3,55 +3,38 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.ValidationError = exports.LocalizedError = exports.FieldError = exports.ElementError = exports.AssertionError = exports.ArrayError = void 0;
6
+ exports.ValidationError = exports.TypeError = exports.LocalizedError = exports.FormatError = exports.FieldError = exports.ElementError = exports.AssertionError = exports.ArrayError = void 0;
7
7
  exports.isSchemaError = isSchemaError;
8
8
  var _messages = require("./messages");
9
9
  var _localization = require("./localization");
10
10
  class LocalizedError extends Error {
11
- constructor(message, values) {
11
+ constructor(message, values = {}) {
12
12
  super((0, _localization.localize)(message, values));
13
- this.values = values;
14
- }
15
- get type() {
16
- return this.values?.type;
17
13
  }
18
14
  }
19
15
  exports.LocalizedError = LocalizedError;
20
16
  class ValidationError extends Error {
21
- constructor(message, details = [], type = 'validation') {
22
- super((0, _localization.localize)(message));
17
+ constructor(arg, details = []) {
18
+ super(getLocalizedMessage(arg));
19
+ this.type = 'validation';
23
20
  this.details = details;
24
- this.type = type;
25
21
  }
26
22
  toJSON() {
27
- if (this.canRollup()) {
28
- const [first] = this.details;
29
- return {
30
- type: this.type,
31
- message: first.message
32
- };
33
- } else {
34
- return {
35
- type: this.type,
36
- message: this.message,
37
- details: this.details.map(error => {
38
- return error.toJSON();
39
- })
40
- };
41
- }
42
- }
43
- canRollup() {
44
23
  const {
24
+ message,
45
25
  details
46
26
  } = this;
47
- if (this.isFieldType() && details.length === 1) {
48
- return !details[0].isFieldType?.();
49
- } else {
50
- return false;
51
- }
52
- }
53
- isFieldType() {
54
- return this.type === 'field' || this.type === 'element';
27
+ return {
28
+ type: this.type,
29
+ ...(message && {
30
+ message
31
+ }),
32
+ ...(details.length && {
33
+ details: details.map(error => {
34
+ return error.toJSON();
35
+ })
36
+ })
37
+ };
55
38
  }
56
39
  getFullMessage(options) {
57
40
  return (0, _messages.getFullMessage)(this, {
@@ -61,12 +44,46 @@ class ValidationError extends Error {
61
44
  }
62
45
  }
63
46
  exports.ValidationError = ValidationError;
47
+ class AssertionError extends ValidationError {
48
+ constructor(message, type = 'assertion') {
49
+ super(message);
50
+ this.type = type;
51
+ }
52
+ }
53
+ exports.AssertionError = AssertionError;
54
+ class TypeError extends ValidationError {
55
+ constructor(message, kind) {
56
+ super(message);
57
+ this.type = 'type';
58
+ this.kind = kind;
59
+ }
60
+ toJSON() {
61
+ return {
62
+ ...super.toJSON(),
63
+ kind: this.kind
64
+ };
65
+ }
66
+ }
67
+ exports.TypeError = TypeError;
68
+ class FormatError extends ValidationError {
69
+ constructor(message, format) {
70
+ super(message);
71
+ this.type = 'format';
72
+ this.format = format;
73
+ }
74
+ toJSON() {
75
+ return {
76
+ ...super.toJSON(),
77
+ format: this.format
78
+ };
79
+ }
80
+ }
81
+ exports.FormatError = FormatError;
64
82
  class FieldError extends ValidationError {
65
- constructor(message, field, original, details) {
66
- super(message, details, 'field');
83
+ constructor(message, field, details) {
84
+ super(message, details);
85
+ this.type = 'field';
67
86
  this.field = field;
68
- this.original = original;
69
- this.details = details;
70
87
  }
71
88
  toJSON() {
72
89
  return {
@@ -77,11 +94,10 @@ class FieldError extends ValidationError {
77
94
  }
78
95
  exports.FieldError = FieldError;
79
96
  class ElementError extends ValidationError {
80
- constructor(message, index, original, details) {
81
- super(message, details, 'element');
97
+ constructor(message, index, details) {
98
+ super(message, details);
99
+ this.type = 'element';
82
100
  this.index = index;
83
- this.original = original;
84
- this.details = details;
85
101
  }
86
102
  toJSON() {
87
103
  return {
@@ -91,27 +107,23 @@ class ElementError extends ValidationError {
91
107
  }
92
108
  }
93
109
  exports.ElementError = ElementError;
94
- class AssertionError extends Error {
95
- constructor(message, type, original) {
96
- super(message);
97
- this.type = type;
98
- this.original = original;
99
- }
100
- toJSON() {
101
- return {
102
- type: this.type,
103
- message: this.message
104
- };
105
- }
106
- }
107
- exports.AssertionError = AssertionError;
108
- class ArrayError extends Error {
110
+ class ArrayError extends ValidationError {
109
111
  constructor(message, details) {
110
112
  super(message);
113
+ this.type = 'array';
111
114
  this.details = details;
112
115
  }
113
116
  }
114
117
  exports.ArrayError = ArrayError;
115
118
  function isSchemaError(arg) {
116
- return arg instanceof ValidationError || arg instanceof AssertionError || arg instanceof ArrayError;
119
+ return arg instanceof ValidationError;
120
+ }
121
+ function getLocalizedMessage(arg) {
122
+ if (arg instanceof LocalizedError) {
123
+ return arg.message;
124
+ } else if (arg instanceof Error) {
125
+ return (0, _localization.localize)(arg.message);
126
+ } else {
127
+ return (0, _localization.localize)(arg);
128
+ }
117
129
  }
@@ -32,6 +32,9 @@ function getLocalized(message) {
32
32
  }
33
33
  }
34
34
  function localize(message, values = {}) {
35
+ if (!message) {
36
+ return;
37
+ }
35
38
  let str = message;
36
39
  if (str) {
37
40
  let localized = getLocalized(message);
@@ -4,37 +4,29 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.getFullMessage = getFullMessage;
7
- var _errors = require("./errors");
8
7
  var _localization = require("./localization");
9
8
  function getFullMessage(error, options) {
10
9
  const {
11
10
  delimiter = '\n'
12
11
  } = options;
13
- if (error.details) {
12
+ if (error.message) {
13
+ return getLabeledMessage(error, options);
14
+ } else if (error.details?.length) {
14
15
  return error.details.map(error => {
15
- if ((0, _errors.isSchemaError)(error)) {
16
- return getFullMessage(error, {
17
- ...options,
18
- path: getInnerPath(error, options)
19
- });
20
- } else {
21
- return error.message;
22
- }
16
+ return getFullMessage(error, {
17
+ ...options,
18
+ path: getInnerPath(error, options)
19
+ });
23
20
  }).join(delimiter);
24
- } else {
25
- return getLabeledMessage(error, options);
26
21
  }
27
22
  }
28
23
  function getInnerPath(error, options) {
29
- const {
30
- type
31
- } = error;
32
24
  const {
33
25
  path = []
34
26
  } = options;
35
- if (type === 'field' && error.field) {
27
+ if (error.field) {
36
28
  return [...path, error.field];
37
- } else if (type === 'element') {
29
+ } else if (error.index != null) {
38
30
  return [...path, error.index];
39
31
  } else {
40
32
  return path;
@@ -48,15 +40,28 @@ function getLabeledMessage(error, options) {
48
40
  path = []
49
41
  } = options;
50
42
  const base = getBase(error.message);
51
- if (type !== 'custom' && path.length) {
52
- const msg = `{field} ${downcase(base)}`;
53
- return (0, _localization.localize)(msg, {
43
+ let template;
44
+ if (base.includes('{field}')) {
45
+ template = base;
46
+ } else if (canAutoAddField(type, path)) {
47
+ template = `{field} ${downcase(base)}`;
48
+ }
49
+ if (template) {
50
+ return (0, _localization.localize)(template, {
54
51
  field: getFieldLabel(options)
55
52
  });
56
53
  } else {
57
54
  return (0, _localization.localize)(base);
58
55
  }
59
56
  }
57
+ const DISALLOWED_TYPES = ['field', 'element', 'array', 'custom'];
58
+
59
+ // Error types that have custom messages should not add the field
60
+ // names automatically. Instead the custom messages can include
61
+ // the {field} token to allow it to be interpolated if required.
62
+ function canAutoAddField(type, path) {
63
+ return path.length && !DISALLOWED_TYPES.includes(type);
64
+ }
60
65
  function getFieldLabel(options) {
61
66
  const {
62
67
  path = [],
@@ -21,8 +21,7 @@ class ObjectSchema extends _TypeSchema.default {
21
21
  constructor(fields, meta) {
22
22
  super(Object, {
23
23
  ...meta,
24
- fields,
25
- message: 'Object failed validation.'
24
+ fields
26
25
  });
27
26
  this.setup();
28
27
  }
@@ -82,6 +81,9 @@ class ObjectSchema extends _TypeSchema.default {
82
81
  return;
83
82
  }
84
83
  try {
84
+ // Do not pass down message into validators
85
+ // to allow custom messages to take precedence.
86
+ options = (0, _utils.omit)(options, 'message');
85
87
  const result = await schema.validate(val, options);
86
88
  if (result !== undefined) {
87
89
  return {
@@ -90,7 +92,10 @@ class ObjectSchema extends _TypeSchema.default {
90
92
  };
91
93
  }
92
94
  } catch (error) {
93
- throw new _errors.FieldError('Field failed validation.', key, error.original, error.details);
95
+ const {
96
+ message
97
+ } = schema.meta;
98
+ throw new _errors.FieldError(message, key, error.details);
94
99
  }
95
100
  }
96
101
  });
@@ -16,11 +16,17 @@ class StringSchema extends _TypeSchema.default {
16
16
  constructor() {
17
17
  super(String);
18
18
  this.assert('type', (val, options) => {
19
- if (typeof val !== 'string' && options.cast) {
19
+ const {
20
+ cast,
21
+ allowEmpty
22
+ } = options;
23
+ if (cast && typeof val !== 'string') {
20
24
  val = String(val);
21
25
  }
22
26
  if (typeof val !== 'string') {
23
27
  throw new _errors.LocalizedError('Must be a string.');
28
+ } else if (!allowEmpty && val === '') {
29
+ throw new _errors.LocalizedError('String may not be empty.');
24
30
  }
25
31
  return val;
26
32
  });
@@ -35,12 +41,19 @@ class StringSchema extends _TypeSchema.default {
35
41
  }).assert('required', val => {
36
42
  if (val == null) {
37
43
  throw new _errors.LocalizedError('Value is required.');
38
- } else if (val === '') {
39
- throw new _errors.LocalizedError('String may not be empty.');
40
44
  }
41
45
  });
42
46
  }
43
47
 
48
+ /**
49
+ * @returns {this}
50
+ */
51
+ allowEmpty() {
52
+ return this.options({
53
+ allowEmpty: true
54
+ });
55
+ }
56
+
44
57
  /**
45
58
  * @param {number} length
46
59
  */
package/dist/cjs/tuple.js CHANGED
@@ -10,7 +10,6 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
10
10
  class TupleSchema extends _Schema.default {
11
11
  constructor(schemas) {
12
12
  super({
13
- message: 'Tuple failed validation.',
14
13
  schemas
15
14
  });
16
15
  this.setup();
@@ -21,7 +20,8 @@ class TupleSchema extends _Schema.default {
21
20
  */
22
21
  setup() {
23
22
  const {
24
- schemas
23
+ schemas,
24
+ message
25
25
  } = this.meta;
26
26
  this.assert('type', (val, options) => {
27
27
  if (typeof val === 'string' && options.cast) {
@@ -63,11 +63,10 @@ class TupleSchema extends _Schema.default {
63
63
  }
64
64
  result.push(await schema.validate(el, options));
65
65
  } catch (error) {
66
- if (error.details?.length === 1) {
67
- errors.push(new _errors.ElementError(error.details[0].message, i));
68
- } else {
69
- errors.push(new _errors.ElementError('Element failed validation.', i, error.details));
70
- }
66
+ const {
67
+ message
68
+ } = schema.meta;
69
+ errors.push(new _errors.ElementError(message, i, error.details));
71
70
  }
72
71
  }
73
72
  if (errors.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrockio/yada",
3
- "version": "1.0.39",
3
+ "version": "1.1.0",
4
4
  "description": "Validation library inspired by Joi.",
5
5
  "scripts": {
6
6
  "test": "jest",