@dwp/govuk-casa 8.7.9 → 8.8.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 +0 -5
- package/dist/casa.d.ts +1 -1
- package/dist/casa.js +3 -2
- package/dist/casa.js.map +1 -0
- package/dist/lib/CasaTemplateLoader.d.ts +1 -7
- package/dist/lib/CasaTemplateLoader.js +6 -1
- package/dist/lib/CasaTemplateLoader.js.map +1 -0
- package/dist/lib/JourneyContext.d.ts +1 -1
- package/dist/lib/JourneyContext.js +2 -1
- package/dist/lib/JourneyContext.js.map +1 -0
- package/dist/lib/MutableRouter.js +1 -0
- package/dist/lib/MutableRouter.js.map +1 -0
- package/dist/lib/Plan.d.ts +2 -0
- package/dist/lib/Plan.js +9 -4
- package/dist/lib/Plan.js.map +1 -0
- package/dist/lib/ValidationError.js +2 -1
- package/dist/lib/ValidationError.js.map +1 -0
- package/dist/lib/ValidatorFactory.d.ts +2 -2
- package/dist/lib/ValidatorFactory.js +3 -2
- package/dist/lib/ValidatorFactory.js.map +1 -0
- package/dist/lib/configuration-ingestor.d.ts +1 -1
- package/dist/lib/configuration-ingestor.js +2 -1
- package/dist/lib/configuration-ingestor.js.map +1 -0
- package/dist/lib/configure.js +2 -1
- package/dist/lib/configure.js.map +1 -0
- package/dist/lib/end-session.js +1 -0
- package/dist/lib/end-session.js.map +1 -0
- package/dist/lib/field.js +1 -0
- package/dist/lib/field.js.map +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/mount.js +3 -2
- package/dist/lib/mount.js.map +1 -0
- package/dist/lib/nunjucks-filters.js +28 -1
- package/dist/lib/nunjucks-filters.js.map +1 -0
- package/dist/lib/nunjucks.js +1 -0
- package/dist/lib/nunjucks.js.map +1 -0
- package/dist/lib/utils.d.ts +46 -28
- package/dist/lib/utils.js +105 -67
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/validators/dateObject.js +5 -4
- package/dist/lib/validators/dateObject.js.map +1 -0
- package/dist/lib/validators/email.js +1 -0
- package/dist/lib/validators/email.js.map +1 -0
- package/dist/lib/validators/inArray.js +1 -0
- package/dist/lib/validators/inArray.js.map +1 -0
- package/dist/lib/validators/index.js +1 -0
- package/dist/lib/validators/index.js.map +1 -0
- package/dist/lib/validators/nino.js +1 -0
- package/dist/lib/validators/nino.js.map +1 -0
- package/dist/lib/validators/postalAddressObject.d.ts +2 -2
- package/dist/lib/validators/postalAddressObject.js +2 -1
- package/dist/lib/validators/postalAddressObject.js.map +1 -0
- package/dist/lib/validators/regex.js +1 -0
- package/dist/lib/validators/regex.js.map +1 -0
- package/dist/lib/validators/required.d.ts +1 -1
- package/dist/lib/validators/required.js +2 -1
- package/dist/lib/validators/required.js.map +1 -0
- package/dist/lib/validators/strlen.js +1 -0
- package/dist/lib/validators/strlen.js.map +1 -0
- package/dist/lib/validators/wordCount.js +1 -0
- package/dist/lib/validators/wordCount.js.map +1 -0
- package/dist/lib/waypoint-url.js +1 -0
- package/dist/lib/waypoint-url.js.map +1 -0
- package/dist/middleware/body-parser.js +1 -0
- package/dist/middleware/body-parser.js.map +1 -0
- package/dist/middleware/csrf.js +1 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/data.js +1 -0
- package/dist/middleware/data.js.map +1 -0
- package/dist/middleware/gather-fields.js +2 -1
- package/dist/middleware/gather-fields.js.map +1 -0
- package/dist/middleware/i18n.js +1 -0
- package/dist/middleware/i18n.js.map +1 -0
- package/dist/middleware/post.js +1 -0
- package/dist/middleware/post.js.map +1 -0
- package/dist/middleware/pre.js +1 -0
- package/dist/middleware/pre.js.map +1 -0
- package/dist/middleware/progress-journey.js +1 -0
- package/dist/middleware/progress-journey.js.map +1 -0
- package/dist/middleware/sanitise-fields.js +1 -0
- package/dist/middleware/sanitise-fields.js.map +1 -0
- package/dist/middleware/serve-first-waypoint.js +1 -0
- package/dist/middleware/serve-first-waypoint.js.map +1 -0
- package/dist/middleware/session.js +1 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/skip-waypoint.js +1 -0
- package/dist/middleware/skip-waypoint.js.map +1 -0
- package/dist/middleware/steer-journey.js +1 -0
- package/dist/middleware/steer-journey.js.map +1 -0
- package/dist/middleware/strip-proxy-path.js +2 -1
- package/dist/middleware/strip-proxy-path.js.map +1 -0
- package/dist/middleware/validate-fields.js +2 -1
- package/dist/middleware/validate-fields.js.map +1 -0
- package/dist/mjs/esm-wrapper.js +10 -15
- package/dist/routes/ancillary.js +1 -0
- package/dist/routes/ancillary.js.map +1 -0
- package/dist/routes/journey.js +1 -0
- package/dist/routes/journey.js.map +1 -0
- package/dist/routes/static.js +1 -0
- package/dist/routes/static.js.map +1 -0
- package/locales/cy/error.json +1 -1
- package/locales/en/error.json +1 -1
- package/package.json +20 -19
- package/src/casa.js +320 -0
- package/src/lib/CasaTemplateLoader.js +104 -0
- package/src/lib/JourneyContext.js +783 -0
- package/src/lib/MutableRouter.js +310 -0
- package/src/lib/Plan.js +624 -0
- package/src/lib/ValidationError.js +163 -0
- package/src/lib/ValidatorFactory.js +105 -0
- package/src/lib/configuration-ingestor.js +457 -0
- package/src/lib/configure.js +202 -0
- package/src/lib/dirname.cjs +1 -0
- package/src/lib/end-session.js +45 -0
- package/src/lib/field.js +456 -0
- package/src/lib/index.js +33 -0
- package/src/lib/logger.js +16 -0
- package/src/lib/mount.js +127 -0
- package/src/lib/nunjucks-filters.js +150 -0
- package/src/lib/nunjucks.js +53 -0
- package/src/lib/utils.js +232 -0
- package/src/lib/validators/dateObject.js +169 -0
- package/src/lib/validators/email.js +55 -0
- package/src/lib/validators/inArray.js +81 -0
- package/src/lib/validators/index.js +24 -0
- package/src/lib/validators/nino.js +57 -0
- package/src/lib/validators/postalAddressObject.js +162 -0
- package/src/lib/validators/regex.js +48 -0
- package/src/lib/validators/required.js +74 -0
- package/src/lib/validators/strlen.js +66 -0
- package/src/lib/validators/wordCount.js +70 -0
- package/src/lib/waypoint-url.js +93 -0
- package/src/middleware/body-parser.js +31 -0
- package/src/middleware/csrf.js +29 -0
- package/src/middleware/data.js +105 -0
- package/src/middleware/dirname.cjs +1 -0
- package/src/middleware/gather-fields.js +51 -0
- package/src/middleware/i18n.js +106 -0
- package/src/middleware/post.js +61 -0
- package/src/middleware/pre.js +91 -0
- package/src/middleware/progress-journey.js +92 -0
- package/src/middleware/sanitise-fields.js +58 -0
- package/src/middleware/serve-first-waypoint.js +28 -0
- package/src/middleware/session.js +129 -0
- package/src/middleware/skip-waypoint.js +46 -0
- package/src/middleware/steer-journey.js +78 -0
- package/src/middleware/strip-proxy-path.js +56 -0
- package/src/middleware/validate-fields.js +84 -0
- package/src/routes/ancillary.js +29 -0
- package/src/routes/dirname.cjs +1 -0
- package/src/routes/journey.js +212 -0
- package/src/routes/static.js +77 -0
- package/views/casa/components/character-count/README.md +10 -0
- package/views/casa/components/character-count/template.njk +6 -2
- package/views/casa/components/checkboxes/README.md +45 -37
- package/views/casa/components/checkboxes/template.njk +8 -7
- package/views/casa/components/date-input/README.md +15 -3
- package/views/casa/components/date-input/template.njk +6 -4
- package/views/casa/components/input/README.md +9 -0
- package/views/casa/components/input/template.njk +6 -2
- package/views/casa/components/journey-form/README.md +3 -2
- package/views/casa/components/postal-address-object/README.md +10 -0
- package/views/casa/components/postal-address-object/template.njk +20 -5
- package/views/casa/components/radios/README.md +51 -26
- package/views/casa/components/radios/template.njk +6 -3
- package/views/casa/components/select/README.md +65 -0
- package/views/casa/components/select/macro.njk +3 -0
- package/views/casa/components/select/template.njk +49 -0
- package/views/casa/components/textarea/README.md +9 -0
- package/views/casa/components/textarea/template.njk +6 -2
- package/views/casa/layouts/journey.njk +1 -1
- package/views/casa/layouts/main.njk +1 -1
package/src/lib/field.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import lodash from 'lodash';
|
|
2
|
+
import { isEmpty } from './utils.js';
|
|
3
|
+
import logger from './logger.js';
|
|
4
|
+
|
|
5
|
+
const log = logger('lib:field');
|
|
6
|
+
const { isFunction } = lodash;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @access private
|
|
10
|
+
* @typedef {import('./index').JourneyContext} JourneyContext
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @access private
|
|
15
|
+
* @typedef {import('../casa').Validator} Validator
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @access private
|
|
20
|
+
* @typedef {import('../casa').ValidateContext} ValidateContext
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @access private
|
|
25
|
+
* @typedef {import('../casa').ValidatorConditionFunction} ValidatorConditionFunction
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @access private
|
|
30
|
+
* @typedef {import('../casa').FieldProcessorFunction} FieldProcessorFunction
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @access private
|
|
35
|
+
* @typedef {import('./index').ValidationError} ValidationError
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// Quick check to see if the field name corresponds to a non-primitive complex
|
|
39
|
+
// type. For example, `my_field[nested]`.
|
|
40
|
+
const reComplexType = /^([^[]+)\[([^\]]+)\]/;
|
|
41
|
+
|
|
42
|
+
const reInvalidName = /[^a-z0-9_.\-[\]]/i;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This class is not exposed via the public API. Instances should instead be
|
|
46
|
+
* instantiated through the `field()` factory function.
|
|
47
|
+
*
|
|
48
|
+
* @class
|
|
49
|
+
*/
|
|
50
|
+
export class PageField {
|
|
51
|
+
/**
|
|
52
|
+
* @type {string}
|
|
53
|
+
*/
|
|
54
|
+
#name;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @type {FieldProcessorFunction[]}
|
|
58
|
+
*/
|
|
59
|
+
#processors;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @type {Validator[]}
|
|
63
|
+
*/
|
|
64
|
+
#validators;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @type {ValidatorConditionFunction[]}
|
|
68
|
+
*/
|
|
69
|
+
#conditions;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @type {object}
|
|
73
|
+
*/
|
|
74
|
+
#meta;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a field.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} name Field name
|
|
80
|
+
* @param {object} [opts] Options
|
|
81
|
+
* @param {boolean} [opts.optional=false] Whether this field is optional
|
|
82
|
+
* @param {boolean} [opts.persist=true] Whether this field will persist in `req.body`
|
|
83
|
+
*/
|
|
84
|
+
constructor(name, { optional = false, persist = true } = Object.create(null)) {
|
|
85
|
+
if (!name) {
|
|
86
|
+
throw new SyntaxError('A name for this field is required, i.e. "field(\'myField\')".');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.#name = undefined;
|
|
90
|
+
this.#validators = [];
|
|
91
|
+
this.#processors = [];
|
|
92
|
+
this.#conditions = [];
|
|
93
|
+
|
|
94
|
+
this.#meta = {
|
|
95
|
+
optional,
|
|
96
|
+
persist,
|
|
97
|
+
complex: undefined,
|
|
98
|
+
complexFieldName: undefined,
|
|
99
|
+
complexFieldProperty: undefined,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Apply name
|
|
103
|
+
/* eslint-disable-next-line security/detect-non-literal-fs-filename */
|
|
104
|
+
this.rename(name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clone this field.
|
|
109
|
+
*
|
|
110
|
+
* @returns {PageField} Cloned field
|
|
111
|
+
*/
|
|
112
|
+
clone() {
|
|
113
|
+
const clone = new PageField(this.#name, {
|
|
114
|
+
optional: this.#meta.optional,
|
|
115
|
+
persist: this.#meta.persist,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (this.getValidators()) {
|
|
119
|
+
clone.validators(this.getValidators());
|
|
120
|
+
}
|
|
121
|
+
if (this.getConditions()) {
|
|
122
|
+
clone.conditions(this.getConditions());
|
|
123
|
+
}
|
|
124
|
+
if (this.getProcessors()) {
|
|
125
|
+
clone.processors(this.getProcessors());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return clone;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract this field's value from the given object.
|
|
133
|
+
*
|
|
134
|
+
* For complex fields, we may need to drill into an object to extract the
|
|
135
|
+
* value.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} obj Object from which to extract the value
|
|
138
|
+
* @returns {any} Value extracted from object
|
|
139
|
+
*/
|
|
140
|
+
getValue(obj = Object.create(null)) {
|
|
141
|
+
if (this.#meta.complex) {
|
|
142
|
+
return obj[this.#meta.complexFieldName]?.[this.#meta.complexFieldProperty];
|
|
143
|
+
}
|
|
144
|
+
return obj[this.#name];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Store this field's value in the given object, using its name as the key.
|
|
149
|
+
*
|
|
150
|
+
* For complex fields, the field object will be created if it does not yet
|
|
151
|
+
* exist, before then storing the property within that object.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} obj Object from which to extract the value
|
|
154
|
+
* @param {any} value Value to be stored
|
|
155
|
+
* @returns {any} Value extracted from object
|
|
156
|
+
*/
|
|
157
|
+
putValue(obj = Object.create(null), value = undefined) {
|
|
158
|
+
if (this.#meta.complex) {
|
|
159
|
+
/* eslint-disable-next-line no-param-reassign */
|
|
160
|
+
obj[this.#meta.complexFieldName] = {
|
|
161
|
+
...(obj[this.#meta.complexFieldName] ?? {}),
|
|
162
|
+
[this.#meta.complexFieldProperty]: value,
|
|
163
|
+
};
|
|
164
|
+
} else {
|
|
165
|
+
/* eslint-disable-next-line no-param-reassign */
|
|
166
|
+
obj[this.#name] = value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* -------------------------------------------------------------- configure */
|
|
173
|
+
|
|
174
|
+
get name() {
|
|
175
|
+
return this.#name;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get meta() {
|
|
179
|
+
return this.#meta;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Rename this field.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} name New name to be applied
|
|
186
|
+
* @returns {PageField} Chain
|
|
187
|
+
* @throws {SyntaxError} When the name is invalid in some way
|
|
188
|
+
*/
|
|
189
|
+
rename(name) {
|
|
190
|
+
if (reInvalidName.test(String(name))) {
|
|
191
|
+
throw new SyntaxError(`Field '${String(name)}' name contains invalid characters.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Complex names are only supported to one level deep. For example,
|
|
195
|
+
// `field[prop]` is supported, whilst `field[prop][subprop]` is not. Throw
|
|
196
|
+
// early to aid developer.
|
|
197
|
+
const isComplex = reComplexType.test(name);
|
|
198
|
+
if (isComplex && name.match(/\[/g).length > 1) {
|
|
199
|
+
throw new SyntaxError('Complex field names are only supported to 1 property depth. E.g. a[b] is ok, a[b][c] is not');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.#name = String(name);
|
|
203
|
+
this.#meta.complex = isComplex;
|
|
204
|
+
|
|
205
|
+
// Extract the field name and property from a complex type for later use
|
|
206
|
+
if (isComplex) {
|
|
207
|
+
const parts = name.match(reComplexType);
|
|
208
|
+
[, this.#meta.complexFieldName, this.#meta.complexFieldProperty] = parts;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get validators
|
|
216
|
+
*
|
|
217
|
+
* @returns {Validator[]} A list containing all validators.
|
|
218
|
+
*/
|
|
219
|
+
getValidators() {
|
|
220
|
+
return this.#validators;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Add/get value validators
|
|
225
|
+
* Some validators will include a `sanitise()` method which will be run at the
|
|
226
|
+
* same time as other "processors".
|
|
227
|
+
*
|
|
228
|
+
* @param {Validator[]} items Validation functions
|
|
229
|
+
* @returns {PageField} Chain - Deprecated: this currently gets all validators if
|
|
230
|
+
* empty or missing, in v9 this functionality will removed in favour of the
|
|
231
|
+
* function getValidators().
|
|
232
|
+
*/
|
|
233
|
+
validators(items = []) {
|
|
234
|
+
if (!items.length) {
|
|
235
|
+
log.warn('Calling validators() to get all validators is deprecated, please use getValidators()');
|
|
236
|
+
return this.getValidators();
|
|
237
|
+
}
|
|
238
|
+
this.#validators = [...this.#validators, ...(items.flat())];
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get processors
|
|
244
|
+
*
|
|
245
|
+
* @returns {FieldProcessorFunction[]} A list containing all processors.
|
|
246
|
+
*/
|
|
247
|
+
getProcessors() {
|
|
248
|
+
return this.#processors;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Add/get value pre-processors
|
|
253
|
+
* This is most often used to sanitise values to a particular data type.
|
|
254
|
+
*
|
|
255
|
+
* @param {FieldProcessorFunction[]} items Processor functions
|
|
256
|
+
* @returns {PageField} Chain - Deprecated: this currently gets all processors if
|
|
257
|
+
* empty or missing, in v9 this functionality will removed in favour of the
|
|
258
|
+
* function getProcessors().
|
|
259
|
+
*/
|
|
260
|
+
processors(items = []) {
|
|
261
|
+
if (!items.length) {
|
|
262
|
+
log.warn('Calling processors() to get all processors is deprecated, please use getProcessors()');
|
|
263
|
+
return this.getProcessors();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.#processors = [...this.#processors, ...(items.flat())];
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get conditions
|
|
272
|
+
*
|
|
273
|
+
* @returns {ValidatorConditionFunction[]} A list containing all conditions.
|
|
274
|
+
*/
|
|
275
|
+
getConditions() {
|
|
276
|
+
return this.#conditions;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Add/get conditions
|
|
281
|
+
* All conditions must be met in order for this field to be considered
|
|
282
|
+
* "actionable".
|
|
283
|
+
*
|
|
284
|
+
* @param {ValidatorConditionFunction[]} items Condition functions
|
|
285
|
+
* @returns {PageField} Chain - Deprecated: this currently gets all conditions if
|
|
286
|
+
* empty or missing, in v9 this functionality will removed in favour of the
|
|
287
|
+
* function getConditions().
|
|
288
|
+
*/
|
|
289
|
+
conditions(items = []) {
|
|
290
|
+
if (!items.length) {
|
|
291
|
+
log.warn('Calling conditions() to get all conditions is deprecated, please use getConditions()');
|
|
292
|
+
return this.getConditions();
|
|
293
|
+
}
|
|
294
|
+
this.#conditions = [...this.#conditions, ...(items.flat())];
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* ---------------------------------------------------------------- execute */
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Run all validators and return array of errors, if applicable.
|
|
302
|
+
*
|
|
303
|
+
* @param {any} value Value to validate
|
|
304
|
+
* @param {ValidateContext} context Contextual validation information
|
|
305
|
+
* @returns {ValidationError[]} Errors, or an empty array if all valid
|
|
306
|
+
* @throws {TypeError} If validator does not return an array
|
|
307
|
+
*/
|
|
308
|
+
runValidators(value, context = Object.create(null)) {
|
|
309
|
+
// Skip validation if the field is empty and optional
|
|
310
|
+
if (this.#meta.optional && isEmpty(value)) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Skip validation if conditions are not met
|
|
315
|
+
// We duplicate value in context.fieldValue for historical reasons
|
|
316
|
+
// @todo explain these historical reasons! And deprecate the need for
|
|
317
|
+
// `value` altogether
|
|
318
|
+
context.fieldValue = context.fieldValue ?? value;
|
|
319
|
+
if (!this.testConditions(context)) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let errors = [];
|
|
324
|
+
for (let i = 0, l = this.#validators.length; i < l; i++) {
|
|
325
|
+
// ESLint disabled as `i` is an integer
|
|
326
|
+
/* eslint-disable security/detect-object-injection */
|
|
327
|
+
// TODO: Replace `value` with `context.fieldValue` here
|
|
328
|
+
let fieldErrors = this.#validators[i].validate(value, context)
|
|
329
|
+
if (!Array.isArray(fieldErrors)) {
|
|
330
|
+
// Friendly message for developer
|
|
331
|
+
throw new TypeError(`The validator at index ${i} (name: ${this.#validators[i].name || 'unknown'}) for field '${this.#name}' did not return an array`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fieldErrors = fieldErrors.map((e) => e.withContext({
|
|
335
|
+
...context,
|
|
336
|
+
validator: this.#validators[i].name,
|
|
337
|
+
}));
|
|
338
|
+
/* eslint-enable security/detect-object-injection */
|
|
339
|
+
|
|
340
|
+
errors = [
|
|
341
|
+
...errors,
|
|
342
|
+
...(fieldErrors ?? []),
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return errors;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Apply all the processors to the given value.
|
|
351
|
+
*
|
|
352
|
+
* @param {any} value Value to process
|
|
353
|
+
* @returns {any} Processed value
|
|
354
|
+
*/
|
|
355
|
+
applyProcessors(value) {
|
|
356
|
+
let processedValue = value;
|
|
357
|
+
|
|
358
|
+
// Some of the validators may have their own "sanitise()" methods. These
|
|
359
|
+
// should be run before any other processors
|
|
360
|
+
// ESLint disabled as `i` is an integer
|
|
361
|
+
/* eslint-disable security/detect-object-injection */
|
|
362
|
+
for (let i = 0, l = this.#validators.length; i < l; i++) {
|
|
363
|
+
if (isFunction(this.#validators[i].sanitise)) {
|
|
364
|
+
processedValue = this.#validators[i].sanitise(processedValue);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/* eslint-enable security/detect-object-injection */
|
|
368
|
+
|
|
369
|
+
for (let i = 0, l = this.#processors.length; i < l; i++) {
|
|
370
|
+
// ESLint disabled as `i` is an integer
|
|
371
|
+
/* eslint-disable-next-line security/detect-object-injection */
|
|
372
|
+
processedValue = this.#processors[i](processedValue);
|
|
373
|
+
}
|
|
374
|
+
return processedValue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* All conditions must return true to be considered a successful test.
|
|
379
|
+
*
|
|
380
|
+
* @param {ValidateContext} context Contextual validation information
|
|
381
|
+
* @returns {boolean} True if all conditions pass
|
|
382
|
+
*/
|
|
383
|
+
testConditions({ fieldValue, waypoint, journeyContext }) {
|
|
384
|
+
const context = {
|
|
385
|
+
fieldName: this.#name,
|
|
386
|
+
fieldValue,
|
|
387
|
+
waypoint,
|
|
388
|
+
waypointId: waypoint, // [DEPRECATED] for backwards compatibility with v7
|
|
389
|
+
journeyContext,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
let result = true;
|
|
393
|
+
for (let i = 0, l = this.#conditions.length; i < l; i++) {
|
|
394
|
+
// ESLint disabled as `i` is an integer
|
|
395
|
+
/* eslint-disable-next-line security/detect-object-injection */
|
|
396
|
+
result = result && this.#conditions[i](context);
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* ---------------------------------------------------------------- aliases */
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Add a single validator.
|
|
405
|
+
*
|
|
406
|
+
* @param {Validator} validator Validation function
|
|
407
|
+
* @returns {PageField} Chain
|
|
408
|
+
*/
|
|
409
|
+
validator(validator) {
|
|
410
|
+
return this.validators([validator]);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Add a single pre-processors
|
|
415
|
+
*
|
|
416
|
+
* @param {FieldProcessorFunction} processor Processor function
|
|
417
|
+
* @returns {PageField} Chain
|
|
418
|
+
*/
|
|
419
|
+
processor(processor) {
|
|
420
|
+
return this.processors([processor]);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Add a single condition.
|
|
425
|
+
*
|
|
426
|
+
* @param {ValidatorConditionFunction} condition Condition function
|
|
427
|
+
* @returns {PageField} Chain
|
|
428
|
+
*/
|
|
429
|
+
condition(condition) {
|
|
430
|
+
return this.conditions([condition]);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Alias for `conditions()`.
|
|
435
|
+
*
|
|
436
|
+
* @param {...ValidatorConditionFunction} args Condition functions
|
|
437
|
+
* @returns {PageField} Chain
|
|
438
|
+
*/
|
|
439
|
+
if(...args) {
|
|
440
|
+
return this.conditions(args);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Factory for creating PageField instances.
|
|
446
|
+
*
|
|
447
|
+
* @memberof module:@dwp/govuk-casa
|
|
448
|
+
* @param {string} name Field name
|
|
449
|
+
* @param {object} [opts] Options
|
|
450
|
+
* @param {boolean} [opts.optional=false] Whether this field is optional
|
|
451
|
+
* @param {boolean} [opts.persist=true] Whether this field will persist in `req.body`
|
|
452
|
+
* @returns {PageField} A PageField
|
|
453
|
+
*/
|
|
454
|
+
export default function field(name, opts) {
|
|
455
|
+
return new PageField(name, opts);
|
|
456
|
+
}
|
package/src/lib/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Just used to collate type information for intellisense. This should not get
|
|
3
|
+
* imported anywhere in code, other than JSDoc references.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import CasaTemplateLoader from './CasaTemplateLoader.js';
|
|
7
|
+
import configure from './configure.js';
|
|
8
|
+
import configurationIngestor from './configuration-ingestor.js';
|
|
9
|
+
import endSession from './end-session.js';
|
|
10
|
+
import field, { PageField } from './field.js';
|
|
11
|
+
import JourneyContext from './JourneyContext.js';
|
|
12
|
+
import MutableRouter from './MutableRouter.js';
|
|
13
|
+
import Plan from './Plan.js';
|
|
14
|
+
import ValidationError from './ValidationError.js';
|
|
15
|
+
import ValidatorFactory from './ValidatorFactory.js';
|
|
16
|
+
import waypointUrl from './waypoint-url.js';
|
|
17
|
+
import * as utils from './utils.js';
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
CasaTemplateLoader,
|
|
21
|
+
configure,
|
|
22
|
+
configurationIngestor,
|
|
23
|
+
endSession,
|
|
24
|
+
field,
|
|
25
|
+
PageField,
|
|
26
|
+
JourneyContext,
|
|
27
|
+
MutableRouter,
|
|
28
|
+
Plan,
|
|
29
|
+
utils,
|
|
30
|
+
ValidationError,
|
|
31
|
+
ValidatorFactory,
|
|
32
|
+
waypointUrl,
|
|
33
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
|
|
3
|
+
const casaDebugger = debug('casa');
|
|
4
|
+
|
|
5
|
+
export default (namespace) => {
|
|
6
|
+
const logger = casaDebugger.extend(namespace);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
trace: logger.extend('trace'),
|
|
10
|
+
debug: logger.extend('debug'),
|
|
11
|
+
info: logger.extend('info'),
|
|
12
|
+
warn: logger.extend('warn'),
|
|
13
|
+
error: logger.extend('error'),
|
|
14
|
+
fatal: logger.extend('fatal'),
|
|
15
|
+
};
|
|
16
|
+
}
|
package/src/lib/mount.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { pathToRegexp } from 'path-to-regexp';
|
|
3
|
+
|
|
4
|
+
import stripProxyPathMiddlewareFactory from '../middleware/strip-proxy-path.js';
|
|
5
|
+
import serveFirstWaypointMiddlewareFactory from '../middleware/serve-first-waypoint.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @access private
|
|
9
|
+
* @typedef {import('nunjucks').Environment} NunjucksEnvironment
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @access private
|
|
14
|
+
* @typedef {import('express').RequestHandler} ExpressRequestHandler
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @access private
|
|
19
|
+
* @typedef {import('../casa').Mounter} Mounter
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @access private
|
|
24
|
+
* @typedef {import('../casa').Plan} Plan
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @access private
|
|
29
|
+
* @typedef {import('../casa').MutableRouter} MutableRouter
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Mounting function factory.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} args Arguments
|
|
36
|
+
* @param {NunjucksEnvironment} args.nunjucksEnv Pre-configured Nunjucks environment
|
|
37
|
+
* @param {string} [args.mountUrl] Mount URL
|
|
38
|
+
* @param {Plan} [args.plan] CASA Plan
|
|
39
|
+
* @param {MutableRouter} args.staticRouter Router for all static assets
|
|
40
|
+
* @param {MutableRouter} args.ancillaryRouter Router for all ancillary routes
|
|
41
|
+
* @param {MutableRouter} args.journeyRouter Router for all waypoints
|
|
42
|
+
* @param {ExpressRequestHandler[]} args.preMiddleware Middleware
|
|
43
|
+
* @param {ExpressRequestHandler[]} args.sessionMiddleware Middleware
|
|
44
|
+
* @param {ExpressRequestHandler[]} args.i18nMiddleware Middleware
|
|
45
|
+
* @param {ExpressRequestHandler[]} args.bodyParserMiddleware Middleware
|
|
46
|
+
* @param {ExpressRequestHandler[]} args.dataMiddleware Middleware
|
|
47
|
+
* @param {ExpressRequestHandler[]} args.postMiddleware Middleware
|
|
48
|
+
* @returns {Mounter} mount
|
|
49
|
+
*/
|
|
50
|
+
export default ({
|
|
51
|
+
nunjucksEnv,
|
|
52
|
+
mountUrl,
|
|
53
|
+
plan,
|
|
54
|
+
staticRouter,
|
|
55
|
+
ancillaryRouter,
|
|
56
|
+
journeyRouter,
|
|
57
|
+
preMiddleware,
|
|
58
|
+
sessionMiddleware,
|
|
59
|
+
i18nMiddleware,
|
|
60
|
+
bodyParserMiddleware,
|
|
61
|
+
dataMiddleware,
|
|
62
|
+
postMiddleware,
|
|
63
|
+
}) => (
|
|
64
|
+
app,
|
|
65
|
+
{
|
|
66
|
+
route = '/',
|
|
67
|
+
serveFirstWaypoint = false,
|
|
68
|
+
} = {},
|
|
69
|
+
) => {
|
|
70
|
+
nunjucksEnv.express(app);
|
|
71
|
+
app.set('view engine', 'njk');
|
|
72
|
+
|
|
73
|
+
// If a `mountUrl` has been defined, then we're potentially in "proxy mode",
|
|
74
|
+
// in which we strip the proxy path prefix from the incoming request URLs.
|
|
75
|
+
if (mountUrl) {
|
|
76
|
+
app.use(stripProxyPathMiddlewareFactory({ mountUrl }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Attach a handler to redirect requests for `/` to the first waypoint in
|
|
80
|
+
// the plan
|
|
81
|
+
if (serveFirstWaypoint && plan) {
|
|
82
|
+
const re = pathToRegexp(`${route}`.replace(/\/+/g, '/'));
|
|
83
|
+
app.use(re, serveFirstWaypointMiddlewareFactory({ plan }));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Capture the mount path of this CASA app, before any parameterised path
|
|
87
|
+
// segments exert influence over `req.baseUrl` in the `router` further below.
|
|
88
|
+
// This can later be used by middleware that wants to use the
|
|
89
|
+
// "unparameterised" version of the request's `baseUrl`, such as the static
|
|
90
|
+
// router's middleware.
|
|
91
|
+
app.use((req, res, next) => {
|
|
92
|
+
req.unparameterisedBaseUrl = req.baseUrl;
|
|
93
|
+
next();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Serve static assets from the `app` rather than the `router`. The router
|
|
97
|
+
// may contain paramaterised path segments which would mean serving static
|
|
98
|
+
// assets over a dynamic URL each time, thus causing lots of cache misses on
|
|
99
|
+
// the browser.
|
|
100
|
+
const sealedStaticRouter = staticRouter.seal();
|
|
101
|
+
app.use(preMiddleware);
|
|
102
|
+
app.use(sealedStaticRouter);
|
|
103
|
+
|
|
104
|
+
const router = Router({
|
|
105
|
+
// Required so that any parameters in the URL are propagated to middleware
|
|
106
|
+
mergeParams: true,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
router.use(preMiddleware);
|
|
110
|
+
// !!! DEPRECATE in v9 !!! For performance reasons, static assets will
|
|
111
|
+
// always be handled via the `app` middleware rather than `router`.
|
|
112
|
+
// Anywhere `mountUrl` is used in templates to service static assets must be
|
|
113
|
+
// changed to use `staticMountUrl`.
|
|
114
|
+
// TASK: remove this line below
|
|
115
|
+
router.use(sealedStaticRouter);
|
|
116
|
+
router.use(sessionMiddleware);
|
|
117
|
+
router.use(i18nMiddleware);
|
|
118
|
+
router.use(bodyParserMiddleware);
|
|
119
|
+
router.use(dataMiddleware);
|
|
120
|
+
router.use(ancillaryRouter.seal());
|
|
121
|
+
router.use(journeyRouter.seal());
|
|
122
|
+
router.use(postMiddleware);
|
|
123
|
+
|
|
124
|
+
app.use(route, router);
|
|
125
|
+
|
|
126
|
+
return app;
|
|
127
|
+
};
|