@planning-inspectorate/dynamic-forms 3.13.1 → 3.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/dynamic-forms",
3
- "version": "3.13.1",
3
+ "version": "3.14.0",
4
4
  "description": "This package is for building GDS forms using a configuration-based approach.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -1,5 +1,4 @@
1
1
  import { body } from 'express-validator';
2
-
3
2
  import { isAfter, isBefore, isValid, parse } from 'date-fns';
4
3
  import { enGB } from 'date-fns/locale';
5
4
 
@@ -12,8 +11,9 @@ import { endOfDay, parseDateInput, startOfDay } from '../lib/date-utils.js';
12
11
 
13
12
  /**
14
13
  * @typedef {Object} DateValidationSettings
15
- * @property {Boolean} ensureFuture
16
- * @property {Boolean} ensurePast
14
+ * @property {Boolean} [ensureFuture]
15
+ * @property {Boolean} [ensurePast]
16
+ * @property {Boolean} [optional]
17
17
  */
18
18
 
19
19
  /**
@@ -34,7 +34,8 @@ export class DateValidator extends BaseValidator {
34
34
  inputLabel,
35
35
  dateValidationSettings = {
36
36
  ensureFuture: false,
37
- ensurePast: false
37
+ ensurePast: false,
38
+ optional: false
38
39
  },
39
40
  errorMessages
40
41
  ) {
@@ -63,7 +64,7 @@ export class DateValidator extends BaseValidator {
63
64
  }
64
65
 
65
66
  isRequired() {
66
- return true;
67
+ return !this.dateValidationSettings.optional;
67
68
  }
68
69
 
69
70
  /**
@@ -76,9 +77,21 @@ export class DateValidator extends BaseValidator {
76
77
  const monthInput = `${fieldName}_month`;
77
78
  const yearInput = `${fieldName}_year`;
78
79
 
80
+ /**
81
+ * Run before every check, if the correct param set then skip the validation
82
+ * when there is no data entered.
83
+ */
84
+ const shouldValidate = (value, { req }) => {
85
+ if (this.dateValidationSettings.optional === true) {
86
+ return !!(req.body[dayInput] || req.body[monthInput] || req.body[yearInput]);
87
+ }
88
+ return true;
89
+ };
90
+
79
91
  const rules = [
80
92
  // check all or some date inputs are not empty
81
93
  body(dayInput)
94
+ .if(shouldValidate)
82
95
  .notEmpty()
83
96
  .withMessage((_, { req }) => {
84
97
  if (!req.body[monthInput] && !req.body[yearInput]) {
@@ -96,6 +109,7 @@ export class DateValidator extends BaseValidator {
96
109
  return this.noDayErrorMessage;
97
110
  }),
98
111
  body(monthInput)
112
+ .if(shouldValidate)
99
113
  .notEmpty()
100
114
  .withMessage((_, { req }) => {
101
115
  if (req.body[dayInput] && !req.body[yearInput]) {
@@ -108,6 +122,7 @@ export class DateValidator extends BaseValidator {
108
122
  // empty error message returned
109
123
  }),
110
124
  body(yearInput)
125
+ .if(shouldValidate)
111
126
  .notEmpty()
112
127
  .withMessage((_, { req }) => {
113
128
  if (req.body[dayInput] && req.body[monthInput]) return this.noYearErrorMessage;
@@ -117,6 +132,7 @@ export class DateValidator extends BaseValidator {
117
132
 
118
133
  // check date values entered are within valid ranges
119
134
  body(dayInput)
135
+ .if(shouldValidate)
120
136
  .isInt({ min: 1, max: 31 })
121
137
  .withMessage(this.invalidDateErrorMessage)
122
138
  .bail()
@@ -135,38 +151,42 @@ export class DateValidator extends BaseValidator {
135
151
 
136
152
  return true;
137
153
  }),
138
- body(monthInput).isInt({ min: 1, max: 12 }).withMessage(this.invalidMonthErrorMessage),
139
- body(yearInput).isInt({ min: 1000, max: 9999 }).withMessage(this.invalidYearErrorMessage)
154
+ body(monthInput).if(shouldValidate).isInt({ min: 1, max: 12 }).withMessage(this.invalidMonthErrorMessage),
155
+ body(yearInput).if(shouldValidate).isInt({ min: 1000, max: 9999 }).withMessage(this.invalidYearErrorMessage)
140
156
  ];
141
157
 
142
158
  if (this.dateValidationSettings.ensurePast === true) {
143
159
  rules.push(
144
- body(dayInput).custom((value, { req }) => {
145
- const inputDate = parseDateInput({ day: value, month: req.body[monthInput], year: req.body[yearInput] });
160
+ body(dayInput)
161
+ .if(shouldValidate)
162
+ .custom((value, { req }) => {
163
+ const inputDate = parseDateInput({ day: value, month: req.body[monthInput], year: req.body[yearInput] });
146
164
 
147
- const today = endOfDay();
165
+ const today = endOfDay();
148
166
 
149
- if (isAfter(inputDate, today)) {
150
- throw new Error(this.futureDateErrorMessage);
151
- }
167
+ if (isAfter(inputDate, today)) {
168
+ throw new Error(this.futureDateErrorMessage);
169
+ }
152
170
 
153
- return true;
154
- })
171
+ return true;
172
+ })
155
173
  );
156
174
  }
157
175
 
158
176
  if (this.dateValidationSettings.ensureFuture === true) {
159
177
  rules.push(
160
- body(dayInput).custom((value, { req }) => {
161
- const inputDate = parseDateInput({ day: value, month: req.body[monthInput], year: req.body[yearInput] });
162
- const today = startOfDay();
163
-
164
- if (isBefore(inputDate, today)) {
165
- throw new Error(this.pastDateErrorMessage);
166
- }
167
-
168
- return true;
169
- })
178
+ body(dayInput)
179
+ .if(shouldValidate)
180
+ .custom((value, { req }) => {
181
+ const inputDate = parseDateInput({ day: value, month: req.body[monthInput], year: req.body[yearInput] });
182
+ const today = startOfDay();
183
+
184
+ if (isBefore(inputDate, today)) {
185
+ throw new Error(this.pastDateErrorMessage);
186
+ }
187
+
188
+ return true;
189
+ })
170
190
  );
171
191
  }
172
192
 
@@ -389,6 +389,109 @@ describe('./src/dynamic-forms/validator/date-validator.js', () => {
389
389
  assert.strictEqual(errors[`${question.fieldName}_month`].msg, errorMessages.invalidMonthErrorMessage);
390
390
  assert.strictEqual(errors[`${question.fieldName}_year`].msg, errorMessages.invalidYearErrorMessage);
391
391
  });
392
+
393
+ it('passes validation if optional is true and no fields are provided (undefined)', async () => {
394
+ const req = {
395
+ body: {
396
+ ['date-question_day']: undefined,
397
+ ['date-question_month']: undefined,
398
+ ['date-question_year']: undefined
399
+ }
400
+ };
401
+
402
+ const question = {
403
+ fieldName: 'date-question'
404
+ };
405
+
406
+ const errors = await _validationMappedErrors(req, question, 'the required date', undefined, {
407
+ optional: true
408
+ });
409
+
410
+ assert.strictEqual(Object.keys(errors).length, 0);
411
+ });
412
+
413
+ it('passes validation if optional is true and fields are empty strings', async () => {
414
+ const req = {
415
+ body: {
416
+ ['date-question_day']: '',
417
+ ['date-question_month']: '',
418
+ ['date-question_year']: ''
419
+ }
420
+ };
421
+
422
+ const question = {
423
+ fieldName: 'date-question'
424
+ };
425
+
426
+ const errors = await _validationMappedErrors(req, question, 'the required date', undefined, {
427
+ optional: true
428
+ });
429
+
430
+ assert.strictEqual(Object.keys(errors).length, 0);
431
+ });
432
+
433
+ it('throws error if optional is true but date is partially filled (missing day)', async () => {
434
+ const req = {
435
+ body: {
436
+ ['date-question_day']: '',
437
+ ['date-question_month']: '10',
438
+ ['date-question_year']: '2023'
439
+ }
440
+ };
441
+
442
+ const question = {
443
+ fieldName: 'date-question'
444
+ };
445
+
446
+ const errors = await _validationMappedErrors(req, question, 'the required date', undefined, {
447
+ optional: true
448
+ });
449
+
450
+ assert.strictEqual(Object.keys(errors).length, 1);
451
+ assert.strictEqual(errors[`${question.fieldName}_day`].msg, 'The required date must include a day');
452
+ });
453
+
454
+ it('throws error if optional is true but invalid values are provided', async () => {
455
+ const req = {
456
+ body: {
457
+ ['date-question_day']: '35',
458
+ ['date-question_month']: '10',
459
+ ['date-question_year']: '2023'
460
+ }
461
+ };
462
+
463
+ const question = {
464
+ fieldName: 'date-question'
465
+ };
466
+
467
+ const errors = await _validationMappedErrors(req, question, 'the required date', undefined, {
468
+ optional: true
469
+ });
470
+
471
+ assert.strictEqual(Object.keys(errors).length, 1);
472
+ assert.strictEqual(errors[`${question.fieldName}_day`].msg, 'The required date day must be a real day');
473
+ });
474
+
475
+ it('throws error if an object is passed without the optional key (undefined) and nothing is entered', async () => {
476
+ const req = {
477
+ body: {
478
+ ['date-question_day']: undefined,
479
+ ['date-question_month']: undefined,
480
+ ['date-question_year']: undefined
481
+ }
482
+ };
483
+
484
+ const question = {
485
+ fieldName: 'date-question'
486
+ };
487
+
488
+ const errors = await _validationMappedErrors(req, question, 'the required date', undefined, {
489
+ randomKey: true
490
+ });
491
+
492
+ assert.strictEqual(Object.keys(errors).length, 3);
493
+ assert.strictEqual(errors[`${question.fieldName}_day`].msg, 'Enter the required date');
494
+ });
392
495
  });
393
496
  });
394
497
 
@@ -3,8 +3,9 @@
3
3
  */
4
4
  /**
5
5
  * @typedef {Object} DateValidationSettings
6
- * @property {Boolean} ensureFuture
7
- * @property {Boolean} ensurePast
6
+ * @property {Boolean} [ensureFuture]
7
+ * @property {Boolean} [ensurePast]
8
+ * @property {Boolean} [optional]
8
9
  */
9
10
  /**
10
11
  * enforces a user has entered a valid date
@@ -42,8 +43,9 @@ export class DateValidator extends BaseValidator {
42
43
  export default DateValidator;
43
44
  export type DateQuestion = typeof import("../components/date/question.js");
44
45
  export type DateValidationSettings = {
45
- ensureFuture: boolean;
46
- ensurePast: boolean;
46
+ ensureFuture?: boolean | undefined;
47
+ ensurePast?: boolean | undefined;
48
+ optional?: boolean | undefined;
47
49
  };
48
50
  import BaseValidator from './base-validator.js';
49
51
  //# sourceMappingURL=date-validator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"date-validator.d.ts","sourceRoot":"","sources":["../../../src/validator/date-validator.js"],"names":[],"mappings":"AAQA;;GAEG;AAEH;;;;GAIG;AAEH;;;GAGG;AACH;IAIC;;;;;OAKG;IACH,wBAJW,MAAM,2BACN,sBAAsB,kBACtB,MAAM,EAgChB;IAvCD,qCAAqC;IACrC,wBADW,sBAAsB,CACV;IAoBtB,uBAAmG;IACnG,uBAAmG;IACnG,yBAAyG;IACzG,wBAAsG;IACtG,4BAAkH;IAClH,2BAA+G;IAC/G,6BACuF;IACvF,6BACuF;IACvF,8BACyF;IACzF,6BACuF;IACvF,4BAAkH;IAClH,0BAA4G;IAS7G;;;OAGG;IACH,sBAFW,YAAY,iDAuGtB;;CA6BD;;;;;;;0BArMyB,qBAAqB"}
1
+ {"version":3,"file":"date-validator.d.ts","sourceRoot":"","sources":["../../../src/validator/date-validator.js"],"names":[],"mappings":"AAOA;;GAEG;AAEH;;;;;GAKG;AAEH;;;GAGG;AACH;IAIC;;;;;OAKG;IACH,wBAJW,MAAM,2BACN,sBAAsB,kBACtB,MAAM,EAiChB;IAxCD,qCAAqC;IACrC,wBADW,sBAAsB,CACV;IAqBtB,uBAAmG;IACnG,uBAAmG;IACnG,yBAAyG;IACzG,wBAAsG;IACtG,4BAAkH;IAClH,2BAA+G;IAC/G,6BACuF;IACvF,6BACuF;IACvF,8BACyF;IACzF,6BACuF;IACvF,4BAAkH;IAClH,0BAA4G;IAS7G;;;OAGG;IACH,sBAFW,YAAY,iDA0HtB;;CA6BD;;;;;;;;0BA1NyB,qBAAqB"}