@bedrockio/yada 1.6.1 → 1.7.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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 1.7.0
2
+
3
+ - Partial revert of 1.6.0. Dot syntax expanding no longer default but enabled.
4
+ - Expanding flat syntax is now `expandFlatKeys`.
5
+ - Flat syntax can also now be allowed without being expanded with
6
+ `allowFlatKeys`.
7
+ - Checks on flat key syntax (array, object presence, etc).
8
+
1
9
  ## 1.6.1
2
10
 
3
11
  - Fixed empty string not allowed when enum provided.
package/README.md CHANGED
@@ -230,6 +230,10 @@ const schema = yd.string().options({
230
230
  });
231
231
  ```
232
232
 
233
+ ### String Schema Options
234
+
235
+ - `allowEmpty` - Allows empty strings.
236
+
233
237
  ### Number
234
238
 
235
239
  Numbers have a handful of useful valiadtions:
@@ -431,6 +435,13 @@ const newSchema = yd.object({
431
435
  });
432
436
  ```
433
437
 
438
+ ### Object Schema Options
439
+
440
+ - `stripEmpty` - Removes properties that are empty strings.
441
+ - `stripUnknown` - Removes properties not in the schema.
442
+ - `allowFlatKeys` - Allows "flat" keys like `profile.name`.
443
+ - `expendFlatKeys` - Expands "flat" keys into nested objects.
444
+
434
445
  ## Date
435
446
 
436
447
  Dates are similar to the basic types with the exception that in addition to date
@@ -777,12 +788,11 @@ await schema
777
788
 
778
789
  #### Options:
779
790
 
780
- - `stripUnknown` - Strips unknown fields on object schemas which otherwise would
781
- throw an error.
782
791
  - `cast` - Casts input to its associated type. For strings and numbers this
783
792
  performs a simple type coercion. For booleans `"true"` or `"1"` will be
784
793
  considered `true` and `"false"` or `"0"` will be considered `false`. This
785
794
  option is useful to convert query strings.
795
+ - [Object Schema Options](#object-schema-options)
786
796
 
787
797
  ## Error Messages
788
798
 
@@ -12,6 +12,7 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
12
12
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
13
13
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
14
14
  const APPEND_ASSERTION_TYPES = ['required', 'type', 'custom'];
15
+ const INTEGER_REG = /^\d+$/;
15
16
 
16
17
  /**
17
18
  * @typedef {{ [key: string]: Schema } | {}} SchemaMap
@@ -30,19 +31,15 @@ class ObjectSchema extends _TypeSchema.default {
30
31
  });
31
32
  this.transform((obj, options) => {
32
33
  const {
33
- fields,
34
34
  stripUnknown,
35
- stripEmpty,
36
- preserveKeys
35
+ stripEmpty
37
36
  } = options;
38
37
  if (obj) {
39
38
  const result = {};
40
- if (!preserveKeys) {
41
- obj = expandKeys(obj);
42
- }
39
+ obj = expandFlatSyntax(obj, options);
43
40
  for (let key of Object.keys(obj)) {
44
41
  const value = obj[key];
45
- const isUnknown = !!fields && !(key in fields);
42
+ const isUnknown = !isKnownKey(key, this, options);
46
43
  if (value === '' && stripEmpty || isUnknown && stripUnknown) {
47
44
  continue;
48
45
  } else if (isUnknown) {
@@ -262,12 +259,14 @@ class ObjectSchema extends _TypeSchema.default {
262
259
  /**
263
260
  * `stripEmpty` - Removes properties that are empty strings.
264
261
  * `stripUnknown` - Removes properties not in the schema.
265
- * `preserveKeys` - Prevents expansion of "flat" keys using dot syntax.
262
+ * `allowFlatKeys` - Allows "flat" keys like `profile.name`.
263
+ * `expendFlatKeys` - Expands "flat" keys into nested objects.
266
264
  *
267
265
  * @param {Object} [options]
268
266
  * @param {boolean} [options.stripEmpty]
269
267
  * @param {boolean} [options.stripUnknown]
270
- * @param {boolean} [options.preserveKeys]
268
+ * @param {boolean} [options.allowFlatKeys]
269
+ * @param {boolean} [options.expandFlatKeys]
271
270
  */
272
271
  options(options) {
273
272
  return super.options(options);
@@ -297,7 +296,10 @@ class ObjectSchema extends _TypeSchema.default {
297
296
  };
298
297
  }
299
298
  }
300
- function expandKeys(obj) {
299
+ function expandFlatSyntax(obj, options) {
300
+ if (!options.expandFlatKeys) {
301
+ return obj;
302
+ }
301
303
  const result = {
302
304
  ...obj
303
305
  };
@@ -309,6 +311,66 @@ function expandKeys(obj) {
309
311
  }
310
312
  return result;
311
313
  }
314
+ function isKnownKey(key, schema, options) {
315
+ const {
316
+ fields
317
+ } = schema.meta;
318
+ const {
319
+ allowFlatKeys
320
+ } = options;
321
+ if (!fields) {
322
+ // No fields defined -> all keys are "known".
323
+ return true;
324
+ } else if (key in fields) {
325
+ // Exact match -> key is known.
326
+ return true;
327
+ } else if (allowFlatKeys && key.includes('.')) {
328
+ // Flat syntax "foo.bar".
329
+ const [base, ...rest] = key.split('.');
330
+ let subschema = fields[base];
331
+ if (!subschema) {
332
+ return false;
333
+ }
334
+ const {
335
+ type,
336
+ schemas
337
+ } = subschema.meta;
338
+ let subkey;
339
+ if (type === 'array') {
340
+ // If the subschema is an array then take the first of
341
+ // its defined schemas as we can safely assume that an
342
+ // array of objects will be defined as a single element
343
+ // or multiple schemas will only set the base property.
344
+ // Test that the element key is valid and take any
345
+ // further properties as the subkey. Examples:
346
+ // - profiles.0.name (array of objects)
347
+ // - profiles.0 (array of stringsmk)
348
+ const [index, ...other] = rest;
349
+ if (!INTEGER_REG.test(index)) {
350
+ return false;
351
+ }
352
+ subschema = schemas[0];
353
+ subkey = other.join('.');
354
+ } else if (type === 'object') {
355
+ // If the subschema is an object then simply take any
356
+ // further properties as the subkey. Example:
357
+ // - profile.name
358
+ subkey = rest.join('.');
359
+ } else {
360
+ // If the subschema is anything else then disallow it
361
+ // further properties as the subkey. Example:
362
+ // - profile.name
363
+ return false;
364
+ }
365
+ if (subschema.meta.type === 'object') {
366
+ return isKnownKey(subkey, subschema, options);
367
+ } else {
368
+ return !subkey;
369
+ }
370
+ } else {
371
+ return false;
372
+ }
373
+ }
312
374
  function mergeFields(aFields, bFields) {
313
375
  if (!aFields || !bFields) {
314
376
  return aFields || bFields;
package/dist/cjs/utils.js CHANGED
@@ -8,7 +8,7 @@ function canAllowEmptyString(options) {
8
8
  const {
9
9
  type,
10
10
  required,
11
- allowEmpty
11
+ allowEmpty = true
12
12
  } = options;
13
- return type === 'string' && !required && allowEmpty !== false;
13
+ return type === 'string' && !required && allowEmpty;
14
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrockio/yada",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Validation library inspired by Joi.",
5
5
  "scripts": {
6
6
  "test": "jest",
package/src/object.js CHANGED
@@ -6,6 +6,8 @@ import { FieldError, LocalizedError } from './errors';
6
6
 
7
7
  const APPEND_ASSERTION_TYPES = ['required', 'type', 'custom'];
8
8
 
9
+ const INTEGER_REG = /^\d+$/;
10
+
9
11
  /**
10
12
  * @typedef {{ [key: string]: Schema } | {}} SchemaMap
11
13
  */
@@ -23,15 +25,15 @@ class ObjectSchema extends TypeSchema {
23
25
  }
24
26
  });
25
27
  this.transform((obj, options) => {
26
- const { fields, stripUnknown, stripEmpty, preserveKeys } = options;
28
+ const { stripUnknown, stripEmpty } = options;
27
29
  if (obj) {
28
30
  const result = {};
29
- if (!preserveKeys) {
30
- obj = expandKeys(obj);
31
- }
31
+
32
+ obj = expandFlatSyntax(obj, options);
33
+
32
34
  for (let key of Object.keys(obj)) {
33
35
  const value = obj[key];
34
- const isUnknown = !!fields && !(key in fields);
36
+ const isUnknown = !isKnownKey(key, this, options);
35
37
 
36
38
  if ((value === '' && stripEmpty) || (isUnknown && stripUnknown)) {
37
39
  continue;
@@ -253,12 +255,14 @@ class ObjectSchema extends TypeSchema {
253
255
  /**
254
256
  * `stripEmpty` - Removes properties that are empty strings.
255
257
  * `stripUnknown` - Removes properties not in the schema.
256
- * `preserveKeys` - Prevents expansion of "flat" keys using dot syntax.
258
+ * `allowFlatKeys` - Allows "flat" keys like `profile.name`.
259
+ * `expendFlatKeys` - Expands "flat" keys into nested objects.
257
260
  *
258
261
  * @param {Object} [options]
259
262
  * @param {boolean} [options.stripEmpty]
260
263
  * @param {boolean} [options.stripUnknown]
261
- * @param {boolean} [options.preserveKeys]
264
+ * @param {boolean} [options.allowFlatKeys]
265
+ * @param {boolean} [options.expandFlatKeys]
262
266
  */
263
267
  options(options) {
264
268
  return super.options(options);
@@ -288,7 +292,11 @@ class ObjectSchema extends TypeSchema {
288
292
  }
289
293
  }
290
294
 
291
- function expandKeys(obj) {
295
+ function expandFlatSyntax(obj, options) {
296
+ if (!options.expandFlatKeys) {
297
+ return obj;
298
+ }
299
+
292
300
  const result = { ...obj };
293
301
  for (let [key, value] of Object.entries(result)) {
294
302
  if (key.includes('.')) {
@@ -299,6 +307,65 @@ function expandKeys(obj) {
299
307
  return result;
300
308
  }
301
309
 
310
+ function isKnownKey(key, schema, options) {
311
+ const { fields } = schema.meta;
312
+ const { allowFlatKeys } = options;
313
+ if (!fields) {
314
+ // No fields defined -> all keys are "known".
315
+ return true;
316
+ } else if (key in fields) {
317
+ // Exact match -> key is known.
318
+ return true;
319
+ } else if (allowFlatKeys && key.includes('.')) {
320
+ // Flat syntax "foo.bar".
321
+ const [base, ...rest] = key.split('.');
322
+ let subschema = fields[base];
323
+
324
+ if (!subschema) {
325
+ return false;
326
+ }
327
+
328
+ const { type, schemas } = subschema.meta;
329
+
330
+ let subkey;
331
+
332
+ if (type === 'array') {
333
+ // If the subschema is an array then take the first of
334
+ // its defined schemas as we can safely assume that an
335
+ // array of objects will be defined as a single element
336
+ // or multiple schemas will only set the base property.
337
+ // Test that the element key is valid and take any
338
+ // further properties as the subkey. Examples:
339
+ // - profiles.0.name (array of objects)
340
+ // - profiles.0 (array of stringsmk)
341
+ const [index, ...other] = rest;
342
+ if (!INTEGER_REG.test(index)) {
343
+ return false;
344
+ }
345
+ subschema = schemas[0];
346
+ subkey = other.join('.');
347
+ } else if (type === 'object') {
348
+ // If the subschema is an object then simply take any
349
+ // further properties as the subkey. Example:
350
+ // - profile.name
351
+ subkey = rest.join('.');
352
+ } else {
353
+ // If the subschema is anything else then disallow it
354
+ // further properties as the subkey. Example:
355
+ // - profile.name
356
+ return false;
357
+ }
358
+
359
+ if (subschema.meta.type === 'object') {
360
+ return isKnownKey(subkey, subschema, options);
361
+ } else {
362
+ return !subkey;
363
+ }
364
+ } else {
365
+ return false;
366
+ }
367
+ }
368
+
302
369
  function mergeFields(aFields, bFields) {
303
370
  if (!aFields || !bFields) {
304
371
  return aFields || bFields;
package/src/utils.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export function canAllowEmptyString(options) {
2
- const { type, required, allowEmpty } = options;
3
- return type === 'string' && !required && allowEmpty !== false;
2
+ const { type, required, allowEmpty = true } = options;
3
+ return type === 'string' && !required && allowEmpty;
4
4
  }
package/types/object.d.ts CHANGED
@@ -74,17 +74,20 @@ declare class ObjectSchema extends TypeSchema {
74
74
  /**
75
75
  * `stripEmpty` - Removes properties that are empty strings.
76
76
  * `stripUnknown` - Removes properties not in the schema.
77
- * `preserveKeys` - Prevents expansion of "flat" keys using dot syntax.
77
+ * `allowFlatKeys` - Allows "flat" keys like `profile.name`.
78
+ * `expendFlatKeys` - Expands "flat" keys into nested objects.
78
79
  *
79
80
  * @param {Object} [options]
80
81
  * @param {boolean} [options.stripEmpty]
81
82
  * @param {boolean} [options.stripUnknown]
82
- * @param {boolean} [options.preserveKeys]
83
+ * @param {boolean} [options.allowFlatKeys]
84
+ * @param {boolean} [options.expandFlatKeys]
83
85
  */
84
86
  options(options?: {
85
87
  stripEmpty?: boolean;
86
88
  stripUnknown?: boolean;
87
- preserveKeys?: boolean;
89
+ allowFlatKeys?: boolean;
90
+ expandFlatKeys?: boolean;
88
91
  }): this;
89
92
  }
90
93
  import Schema from './Schema';
@@ -1 +1 @@
1
- {"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../src/object.js"],"names":[],"mappings":"AA+TA;;;;;;GAMG;AACH,uCAJW,SAAS,gBAQnB;wBAjUY;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,EAAE;AAD3C;;GAEG;AAEH;IACE,uBAGC;IAED,cA8EC;IAED;;;;;;OAMG;IACH,WAFW,MAAM,GAAC,KAAK,CAAC,MAAM,CAAC,OAsB9B;IAED;;;;;;OAMG;IACH,cAFW,MAAM,GAAC,KAAK,CAAC,MAAM,CAAC,OAY9B;IAED;;;;OAIG;IACH,gBAFc,MAAM,EAAA,gBASnB;IAED;;;;OAIG;IACH,gBAFc,MAAM,EAAA,gBASnB;IAED;;;;;;;OAOG;IACH,mBAHc,MAAM,EAAA,gBAmBnB;IAED;;;;;;OAMG;IACH,cAEC;IAED;;;;;;;;;OASG;IACH,YAFW,SAAS,GAAC,MAAM,gBA+B1B;IAED;;;;;;;;;OASG;IACH,kBAJG;QAA0B,UAAU,GAA5B,OAAO;QACW,YAAY,GAA9B,OAAO;QACW,YAAY,GAA9B,OAAO;KAAwB,QAIzC;CAwBF;mBA9RgC,UAAU;uBACpB,cAAc"}
1
+ {"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../src/object.js"],"names":[],"mappings":"AAkYA;;;;;;GAMG;AACH,uCAJW,SAAS,gBAQnB;wBAlYY;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,EAAE;AAD3C;;GAEG;AAEH;IACE,uBAGC;IAED,cA8EC;IAED;;;;;;OAMG;IACH,WAFW,MAAM,GAAC,KAAK,CAAC,MAAM,CAAC,OAsB9B;IAED;;;;;;OAMG;IACH,cAFW,MAAM,GAAC,KAAK,CAAC,MAAM,CAAC,OAY9B;IAED;;;;OAIG;IACH,gBAFc,MAAM,EAAA,gBASnB;IAED;;;;OAIG;IACH,gBAFc,MAAM,EAAA,gBASnB;IAED;;;;;;;OAOG;IACH,mBAHc,MAAM,EAAA,gBAmBnB;IAED;;;;;;OAMG;IACH,cAEC;IAED;;;;;;;;;OASG;IACH,YAFW,SAAS,GAAC,MAAM,gBA+B1B;IAED;;;;;;;;;;;OAWG;IACH,kBALG;QAA0B,UAAU,GAA5B,OAAO;QACW,YAAY,GAA9B,OAAO;QACW,aAAa,GAA/B,OAAO;QACW,cAAc,GAAhC,OAAO;KAA0B,QAI3C;CAwBF;mBAlSgC,UAAU;uBACpB,cAAc"}
package/types/utils.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export function canAllowEmptyString(options: any): boolean;
1
+ export function canAllowEmptyString(options: any): any;
2
2
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AAAA,2DAGC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AAAA,uDAGC"}