@becollective/utils 2.0.15 → 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/date-time.js CHANGED
@@ -68,8 +68,11 @@ const getShiftText = (from, to, timezone) => {
68
68
  try {
69
69
  if (!from || !to || !timezone)
70
70
  return '';
71
- const startToTimezone = luxon_1.DateTime.fromJSDate(from).setZone(timezone);
72
- const endToTimezone = luxon_1.DateTime.fromJSDate(to).setZone(timezone);
71
+ // GraphQL / JSON often yield ISO strings; fromJSDate alone rejects those as Invalid.
72
+ const startToTimezone = luxon_1.DateTime.fromJSDate(new Date(from)).setZone(timezone);
73
+ const endToTimezone = luxon_1.DateTime.fromJSDate(new Date(to)).setZone(timezone);
74
+ if (!startToTimezone.isValid || !endToTimezone.isValid)
75
+ return '';
73
76
  return `${startToTimezone.toFormat('cccc, d MMMM yyyy, h:mma')}-${endToTimezone.toFormat('h:mma')}`;
74
77
  }
75
78
  catch (e) {
package/lib/forms.d.ts CHANGED
@@ -188,7 +188,7 @@ export declare const validator: {
188
188
  minItems: number;
189
189
  items: {
190
190
  type: string;
191
- }[];
191
+ };
192
192
  };
193
193
  };
194
194
  };
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
- skills: (data, schema) => {
580
- if (!schema || !schema.items)
581
- return false;
582
- let hasAnyOf = false;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@becollective/utils",
3
- "version": "2.0.15",
3
+ "version": "2.0.17",
4
4
  "description": "Common utilities",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
package/src/date-time.ts CHANGED
@@ -65,8 +65,10 @@ export const datesByThemselves = (a, b) => {
65
65
  export const getShiftText = (from, to, timezone) => {
66
66
  try {
67
67
  if (!from || !to || !timezone) return '';
68
- const startToTimezone = DateTime.fromJSDate(from).setZone(timezone);
69
- const endToTimezone = DateTime.fromJSDate(to).setZone(timezone);
68
+ // GraphQL / JSON often yield ISO strings; fromJSDate alone rejects those as Invalid.
69
+ const startToTimezone = DateTime.fromJSDate(new Date(from)).setZone(timezone);
70
+ const endToTimezone = DateTime.fromJSDate(new Date(to)).setZone(timezone);
71
+ if (!startToTimezone.isValid || !endToTimezone.isValid) return '';
70
72
  return `${startToTimezone.toFormat('cccc, d MMMM yyyy, h:mma')}-${endToTimezone.toFormat('h:mma')}`;
71
73
  }
72
74
  catch (e) {
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
- skills: (data, schema) => {
632
- if (!schema || !schema.items) return false;
633
-
634
- let hasAnyOf = false;
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) => {
@@ -150,6 +150,11 @@ describe('getShiftText', () => {
150
150
  test('get shift text with auckland timezone', () => {
151
151
  expect(getShiftText(from, to, auckland)).toBe('Thursday, 30 July 2020, 12:00PM-5:30PM');
152
152
  });
153
+ test('accepts ISO strings from GraphQL', () => {
154
+ expect(getShiftText(from.toISOString(), to.toISOString(), melbourne)).toBe(
155
+ 'Thursday, 30 July 2020, 10:00AM-3:30PM',
156
+ );
157
+ });
153
158
  // Will not execute due to TypeScript
154
159
  // test('with empty data', () => {
155
160
  // expect(getShiftText()).toBe('');
@@ -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
  });