@becollective/utils 2.0.16 → 2.0.17
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/lib/forms.d.ts +1 -1
- package/lib/forms.js +35 -47
- package/package.json +1 -1
- package/src/forms.ts +39 -62
- package/tests/forms.test.ts +12 -0
package/lib/forms.d.ts
CHANGED
package/lib/forms.js
CHANGED
|
@@ -59,6 +59,34 @@ const isValidYear = (value) => {
|
|
|
59
59
|
const year = new Date(value).getFullYear();
|
|
60
60
|
return year < 9999 && year > 999;
|
|
61
61
|
};
|
|
62
|
+
// Form multi-choice widgets (dropdown / tickbox / radio / skills) store options as:
|
|
63
|
+
// items: { type: 'string', anyOf: [ { enum: ['Yes'] }, { enum: ['No'] } ] }
|
|
64
|
+
//
|
|
65
|
+
// The old validators passed `items: schema.items.anyOf` into AJV. In JSON Schema,
|
|
66
|
+
// `items` as an array is a positional *tuple* (index 0 matches schema 0, …), not
|
|
67
|
+
// "each value may match any of these options". AJV 8 strictTuples then logs/throws:
|
|
68
|
+
// strict mode: "items" is N-tuple, but minItems or maxItems/additionalItems
|
|
69
|
+
// are not specified or different at path "#"
|
|
70
|
+
// That started when @becollective/forms (AJV 6, no strictTuples) was replaced by
|
|
71
|
+
// @becollective/utils (AJV 8). AJV 6 silently accepted the illegal shape.
|
|
72
|
+
//
|
|
73
|
+
// Compile a single item schema instead. If a stored form still has a raw array on
|
|
74
|
+
// `items`, treat it as anyOf (the intended "one of these choices" meaning).
|
|
75
|
+
const itemSchemaForArray = (items) => {
|
|
76
|
+
if (Array.isArray(items)) {
|
|
77
|
+
return { anyOf: items };
|
|
78
|
+
}
|
|
79
|
+
return items;
|
|
80
|
+
};
|
|
81
|
+
const isValidArrayOfItems = (data, schema) => {
|
|
82
|
+
if (!schema || !schema.items)
|
|
83
|
+
return false;
|
|
84
|
+
return ajv.validate({
|
|
85
|
+
type: 'array',
|
|
86
|
+
items: itemSchemaForArray(schema.items),
|
|
87
|
+
minItems: 1,
|
|
88
|
+
}, data) === true;
|
|
89
|
+
};
|
|
62
90
|
const validateType = async (params) => {
|
|
63
91
|
const { type, data, schema, error } = params;
|
|
64
92
|
if (exports.validator.dataTypeIsValid[type](data, schema)) {
|
|
@@ -448,7 +476,9 @@ exports.validator = {
|
|
|
448
476
|
enum: {
|
|
449
477
|
type: 'array',
|
|
450
478
|
minItems: 1,
|
|
451
|
-
items: [{ type: 'string' }]
|
|
479
|
+
// Homogeneous string list — not `items: [{ type: 'string' }]`,
|
|
480
|
+
// which AJV 8 reads as a 1-tuple (same strictTuples error).
|
|
481
|
+
items: { type: 'string' },
|
|
452
482
|
},
|
|
453
483
|
},
|
|
454
484
|
},
|
|
@@ -576,52 +606,10 @@ exports.validator = {
|
|
|
576
606
|
phone: (data) => {
|
|
577
607
|
return phoneNumber.isValidNumber(data || '');
|
|
578
608
|
},
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
try {
|
|
584
|
-
hasAnyOf =
|
|
585
|
-
ajv.validate({
|
|
586
|
-
type: 'array',
|
|
587
|
-
items: schema.items.anyOf,
|
|
588
|
-
minItems: 1,
|
|
589
|
-
}, data) === true;
|
|
590
|
-
}
|
|
591
|
-
catch (e) { }
|
|
592
|
-
return hasAnyOf;
|
|
593
|
-
},
|
|
594
|
-
qualifications: (data, schema) => {
|
|
595
|
-
if (!schema || !schema.items)
|
|
596
|
-
return false;
|
|
597
|
-
return (ajv.validate({
|
|
598
|
-
type: 'array',
|
|
599
|
-
items: schema.items,
|
|
600
|
-
minItems: 1,
|
|
601
|
-
}, data) === true);
|
|
602
|
-
},
|
|
603
|
-
array: (data, schema) => {
|
|
604
|
-
if (!schema || !schema.items)
|
|
605
|
-
return false;
|
|
606
|
-
let hasAnyOf = false;
|
|
607
|
-
let hasItems = false;
|
|
608
|
-
try {
|
|
609
|
-
hasAnyOf =
|
|
610
|
-
ajv.validate({
|
|
611
|
-
type: 'array',
|
|
612
|
-
items: schema.items.anyOf,
|
|
613
|
-
minItems: 1,
|
|
614
|
-
}, data) === true;
|
|
615
|
-
hasItems =
|
|
616
|
-
ajv.validate({
|
|
617
|
-
type: 'array',
|
|
618
|
-
items: schema.items,
|
|
619
|
-
minItems: 1,
|
|
620
|
-
}, data) === true;
|
|
621
|
-
}
|
|
622
|
-
catch (e) { }
|
|
623
|
-
return hasItems || hasAnyOf;
|
|
624
|
-
},
|
|
609
|
+
// Do not pass `schema.items.anyOf` as AJV `items` (tuple). See itemSchemaForArray.
|
|
610
|
+
skills: (data, schema) => isValidArrayOfItems(data, schema),
|
|
611
|
+
qualifications: (data, schema) => isValidArrayOfItems(data, schema),
|
|
612
|
+
array: (data, schema) => isValidArrayOfItems(data, schema),
|
|
625
613
|
heading: () => true,
|
|
626
614
|
spacer: () => true,
|
|
627
615
|
location: (data) => {
|
package/package.json
CHANGED
package/src/forms.ts
CHANGED
|
@@ -71,6 +71,38 @@ const isValidYear = (value) => {
|
|
|
71
71
|
return year < 9999 && year > 999;
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
// Form multi-choice widgets (dropdown / tickbox / radio / skills) store options as:
|
|
75
|
+
// items: { type: 'string', anyOf: [ { enum: ['Yes'] }, { enum: ['No'] } ] }
|
|
76
|
+
//
|
|
77
|
+
// The old validators passed `items: schema.items.anyOf` into AJV. In JSON Schema,
|
|
78
|
+
// `items` as an array is a positional *tuple* (index 0 matches schema 0, …), not
|
|
79
|
+
// "each value may match any of these options". AJV 8 strictTuples then logs/throws:
|
|
80
|
+
// strict mode: "items" is N-tuple, but minItems or maxItems/additionalItems
|
|
81
|
+
// are not specified or different at path "#"
|
|
82
|
+
// That started when @becollective/forms (AJV 6, no strictTuples) was replaced by
|
|
83
|
+
// @becollective/utils (AJV 8). AJV 6 silently accepted the illegal shape.
|
|
84
|
+
//
|
|
85
|
+
// Compile a single item schema instead. If a stored form still has a raw array on
|
|
86
|
+
// `items`, treat it as anyOf (the intended "one of these choices" meaning).
|
|
87
|
+
const itemSchemaForArray = (items) => {
|
|
88
|
+
if (Array.isArray(items)) {
|
|
89
|
+
return { anyOf: items };
|
|
90
|
+
}
|
|
91
|
+
return items;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const isValidArrayOfItems = (data, schema) => {
|
|
95
|
+
if (!schema || !schema.items) return false;
|
|
96
|
+
return ajv.validate(
|
|
97
|
+
{
|
|
98
|
+
type: 'array',
|
|
99
|
+
items: itemSchemaForArray(schema.items),
|
|
100
|
+
minItems: 1,
|
|
101
|
+
},
|
|
102
|
+
data
|
|
103
|
+
) === true;
|
|
104
|
+
};
|
|
105
|
+
|
|
74
106
|
const validateType = async (params: { type: string, data: any, schema?: any, error?: string }) => {
|
|
75
107
|
const { type, data, schema, error } = params;
|
|
76
108
|
if (validator.dataTypeIsValid[type](data, schema)) {
|
|
@@ -496,7 +528,9 @@ export const validator = {
|
|
|
496
528
|
enum: {
|
|
497
529
|
type: 'array',
|
|
498
530
|
minItems: 1,
|
|
499
|
-
items: [{ type: 'string' }]
|
|
531
|
+
// Homogeneous string list — not `items: [{ type: 'string' }]`,
|
|
532
|
+
// which AJV 8 reads as a 1-tuple (same strictTuples error).
|
|
533
|
+
items: { type: 'string' },
|
|
500
534
|
},
|
|
501
535
|
},
|
|
502
536
|
},
|
|
@@ -628,67 +662,10 @@ export const validator = {
|
|
|
628
662
|
phone: (data) => {
|
|
629
663
|
return phoneNumber.isValidNumber(data || '');
|
|
630
664
|
},
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
try {
|
|
636
|
-
hasAnyOf =
|
|
637
|
-
ajv.validate(
|
|
638
|
-
{
|
|
639
|
-
type: 'array',
|
|
640
|
-
items: schema.items.anyOf,
|
|
641
|
-
minItems: 1,
|
|
642
|
-
},
|
|
643
|
-
data
|
|
644
|
-
) === true;
|
|
645
|
-
}
|
|
646
|
-
catch (e) {}
|
|
647
|
-
return hasAnyOf;
|
|
648
|
-
},
|
|
649
|
-
qualifications: (data, schema) => {
|
|
650
|
-
if (!schema || !schema.items) return false;
|
|
651
|
-
|
|
652
|
-
return (
|
|
653
|
-
ajv.validate(
|
|
654
|
-
{
|
|
655
|
-
type: 'array',
|
|
656
|
-
items: schema.items,
|
|
657
|
-
minItems: 1,
|
|
658
|
-
},
|
|
659
|
-
data
|
|
660
|
-
) === true
|
|
661
|
-
);
|
|
662
|
-
},
|
|
663
|
-
array: (data, schema) => {
|
|
664
|
-
if (!schema || !schema.items) return false;
|
|
665
|
-
let hasAnyOf = false;
|
|
666
|
-
let hasItems = false;
|
|
667
|
-
try {
|
|
668
|
-
hasAnyOf =
|
|
669
|
-
ajv.validate(
|
|
670
|
-
{
|
|
671
|
-
type: 'array',
|
|
672
|
-
items: schema.items.anyOf,
|
|
673
|
-
minItems: 1,
|
|
674
|
-
},
|
|
675
|
-
data
|
|
676
|
-
) === true;
|
|
677
|
-
|
|
678
|
-
hasItems =
|
|
679
|
-
ajv.validate(
|
|
680
|
-
{
|
|
681
|
-
type: 'array',
|
|
682
|
-
items: schema.items,
|
|
683
|
-
minItems: 1,
|
|
684
|
-
},
|
|
685
|
-
data
|
|
686
|
-
) === true;
|
|
687
|
-
}
|
|
688
|
-
catch (e) {}
|
|
689
|
-
|
|
690
|
-
return hasItems || hasAnyOf;
|
|
691
|
-
},
|
|
665
|
+
// Do not pass `schema.items.anyOf` as AJV `items` (tuple). See itemSchemaForArray.
|
|
666
|
+
skills: (data, schema) => isValidArrayOfItems(data, schema),
|
|
667
|
+
qualifications: (data, schema) => isValidArrayOfItems(data, schema),
|
|
668
|
+
array: (data, schema) => isValidArrayOfItems(data, schema),
|
|
692
669
|
heading: () => true,
|
|
693
670
|
spacer: () => true,
|
|
694
671
|
location: (data) => {
|
package/tests/forms.test.ts
CHANGED
|
@@ -638,6 +638,18 @@ describe('dataTypeIsValid', () => {
|
|
|
638
638
|
expect(isValid.array(['Soccer'], sport.schema)).toBe(true);
|
|
639
639
|
});
|
|
640
640
|
|
|
641
|
+
test('should not emit AJV strict-tuple errors for items.anyOf choice fields', () => {
|
|
642
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation();
|
|
643
|
+
const error = jest.spyOn(console, 'error').mockImplementation();
|
|
644
|
+
|
|
645
|
+
expect(isValid.array(['Soccer'], sport.schema)).toBe(true);
|
|
646
|
+
|
|
647
|
+
const messages = [...warn.mock.calls, ...error.mock.calls].flat().join(' ');
|
|
648
|
+
expect(messages).not.toMatch(/strict mode: "items" is \d+-tuple/);
|
|
649
|
+
warn.mockRestore();
|
|
650
|
+
error.mockRestore();
|
|
651
|
+
});
|
|
652
|
+
|
|
641
653
|
test('should return false when schema is an empty object', () => {
|
|
642
654
|
expect(isValid.array(['Soccer'], {})).toBe(false);
|
|
643
655
|
});
|