@dwp/govuk-casa 8.7.12 → 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.
Files changed (168) hide show
  1. package/dist/casa.js +2 -1
  2. package/dist/casa.js.map +1 -0
  3. package/dist/lib/CasaTemplateLoader.js +1 -0
  4. package/dist/lib/CasaTemplateLoader.js.map +1 -0
  5. package/dist/lib/JourneyContext.d.ts +1 -1
  6. package/dist/lib/JourneyContext.js +2 -1
  7. package/dist/lib/JourneyContext.js.map +1 -0
  8. package/dist/lib/MutableRouter.js +1 -0
  9. package/dist/lib/MutableRouter.js.map +1 -0
  10. package/dist/lib/Plan.d.ts +2 -1
  11. package/dist/lib/Plan.js +4 -3
  12. package/dist/lib/Plan.js.map +1 -0
  13. package/dist/lib/ValidationError.js +1 -0
  14. package/dist/lib/ValidationError.js.map +1 -0
  15. package/dist/lib/ValidatorFactory.d.ts +2 -2
  16. package/dist/lib/ValidatorFactory.js +3 -2
  17. package/dist/lib/ValidatorFactory.js.map +1 -0
  18. package/dist/lib/configuration-ingestor.js +1 -0
  19. package/dist/lib/configuration-ingestor.js.map +1 -0
  20. package/dist/lib/configure.js +2 -1
  21. package/dist/lib/configure.js.map +1 -0
  22. package/dist/lib/end-session.js +1 -0
  23. package/dist/lib/end-session.js.map +1 -0
  24. package/dist/lib/field.js +1 -0
  25. package/dist/lib/field.js.map +1 -0
  26. package/dist/lib/index.js +1 -0
  27. package/dist/lib/index.js.map +1 -0
  28. package/dist/lib/logger.js +1 -0
  29. package/dist/lib/logger.js.map +1 -0
  30. package/dist/lib/mount.js +3 -2
  31. package/dist/lib/mount.js.map +1 -0
  32. package/dist/lib/nunjucks-filters.js +1 -0
  33. package/dist/lib/nunjucks-filters.js.map +1 -0
  34. package/dist/lib/nunjucks.js +1 -0
  35. package/dist/lib/nunjucks.js.map +1 -0
  36. package/dist/lib/utils.d.ts +45 -27
  37. package/dist/lib/utils.js +105 -67
  38. package/dist/lib/utils.js.map +1 -0
  39. package/dist/lib/validators/dateObject.js +4 -3
  40. package/dist/lib/validators/dateObject.js.map +1 -0
  41. package/dist/lib/validators/email.js +1 -0
  42. package/dist/lib/validators/email.js.map +1 -0
  43. package/dist/lib/validators/inArray.js +1 -0
  44. package/dist/lib/validators/inArray.js.map +1 -0
  45. package/dist/lib/validators/index.js +1 -0
  46. package/dist/lib/validators/index.js.map +1 -0
  47. package/dist/lib/validators/nino.js +1 -0
  48. package/dist/lib/validators/nino.js.map +1 -0
  49. package/dist/lib/validators/postalAddressObject.d.ts +2 -2
  50. package/dist/lib/validators/postalAddressObject.js +2 -1
  51. package/dist/lib/validators/postalAddressObject.js.map +1 -0
  52. package/dist/lib/validators/regex.js +1 -0
  53. package/dist/lib/validators/regex.js.map +1 -0
  54. package/dist/lib/validators/required.js +1 -0
  55. package/dist/lib/validators/required.js.map +1 -0
  56. package/dist/lib/validators/strlen.js +1 -0
  57. package/dist/lib/validators/strlen.js.map +1 -0
  58. package/dist/lib/validators/wordCount.js +1 -0
  59. package/dist/lib/validators/wordCount.js.map +1 -0
  60. package/dist/lib/waypoint-url.js +1 -0
  61. package/dist/lib/waypoint-url.js.map +1 -0
  62. package/dist/middleware/body-parser.js +1 -0
  63. package/dist/middleware/body-parser.js.map +1 -0
  64. package/dist/middleware/csrf.js +1 -0
  65. package/dist/middleware/csrf.js.map +1 -0
  66. package/dist/middleware/data.js +1 -0
  67. package/dist/middleware/data.js.map +1 -0
  68. package/dist/middleware/gather-fields.js +1 -0
  69. package/dist/middleware/gather-fields.js.map +1 -0
  70. package/dist/middleware/i18n.js +1 -0
  71. package/dist/middleware/i18n.js.map +1 -0
  72. package/dist/middleware/post.js +1 -0
  73. package/dist/middleware/post.js.map +1 -0
  74. package/dist/middleware/pre.js +1 -0
  75. package/dist/middleware/pre.js.map +1 -0
  76. package/dist/middleware/progress-journey.js +1 -0
  77. package/dist/middleware/progress-journey.js.map +1 -0
  78. package/dist/middleware/sanitise-fields.js +1 -0
  79. package/dist/middleware/sanitise-fields.js.map +1 -0
  80. package/dist/middleware/serve-first-waypoint.js +1 -0
  81. package/dist/middleware/serve-first-waypoint.js.map +1 -0
  82. package/dist/middleware/session.js +1 -0
  83. package/dist/middleware/session.js.map +1 -0
  84. package/dist/middleware/skip-waypoint.js +1 -0
  85. package/dist/middleware/skip-waypoint.js.map +1 -0
  86. package/dist/middleware/steer-journey.js +1 -0
  87. package/dist/middleware/steer-journey.js.map +1 -0
  88. package/dist/middleware/strip-proxy-path.js +1 -0
  89. package/dist/middleware/strip-proxy-path.js.map +1 -0
  90. package/dist/middleware/validate-fields.js +1 -0
  91. package/dist/middleware/validate-fields.js.map +1 -0
  92. package/dist/mjs/esm-wrapper.js +10 -15
  93. package/dist/routes/ancillary.js +1 -0
  94. package/dist/routes/ancillary.js.map +1 -0
  95. package/dist/routes/journey.js +1 -0
  96. package/dist/routes/journey.js.map +1 -0
  97. package/dist/routes/static.js +1 -0
  98. package/dist/routes/static.js.map +1 -0
  99. package/locales/cy/error.json +1 -1
  100. package/locales/en/error.json +1 -1
  101. package/package.json +16 -15
  102. package/src/casa.js +320 -0
  103. package/src/lib/CasaTemplateLoader.js +104 -0
  104. package/src/lib/JourneyContext.js +783 -0
  105. package/src/lib/MutableRouter.js +310 -0
  106. package/src/lib/Plan.js +624 -0
  107. package/src/lib/ValidationError.js +163 -0
  108. package/src/lib/ValidatorFactory.js +105 -0
  109. package/src/lib/configuration-ingestor.js +457 -0
  110. package/src/lib/configure.js +202 -0
  111. package/src/lib/dirname.cjs +1 -0
  112. package/src/lib/end-session.js +45 -0
  113. package/src/lib/field.js +456 -0
  114. package/src/lib/index.js +33 -0
  115. package/src/lib/logger.js +16 -0
  116. package/src/lib/mount.js +127 -0
  117. package/src/lib/nunjucks-filters.js +150 -0
  118. package/src/lib/nunjucks.js +53 -0
  119. package/src/lib/utils.js +232 -0
  120. package/src/lib/validators/dateObject.js +169 -0
  121. package/src/lib/validators/email.js +55 -0
  122. package/src/lib/validators/inArray.js +81 -0
  123. package/src/lib/validators/index.js +24 -0
  124. package/src/lib/validators/nino.js +57 -0
  125. package/src/lib/validators/postalAddressObject.js +162 -0
  126. package/src/lib/validators/regex.js +48 -0
  127. package/src/lib/validators/required.js +74 -0
  128. package/src/lib/validators/strlen.js +66 -0
  129. package/src/lib/validators/wordCount.js +70 -0
  130. package/src/lib/waypoint-url.js +93 -0
  131. package/src/middleware/body-parser.js +31 -0
  132. package/src/middleware/csrf.js +29 -0
  133. package/src/middleware/data.js +105 -0
  134. package/src/middleware/dirname.cjs +1 -0
  135. package/src/middleware/gather-fields.js +51 -0
  136. package/src/middleware/i18n.js +106 -0
  137. package/src/middleware/post.js +61 -0
  138. package/src/middleware/pre.js +91 -0
  139. package/src/middleware/progress-journey.js +92 -0
  140. package/src/middleware/sanitise-fields.js +58 -0
  141. package/src/middleware/serve-first-waypoint.js +28 -0
  142. package/src/middleware/session.js +129 -0
  143. package/src/middleware/skip-waypoint.js +46 -0
  144. package/src/middleware/steer-journey.js +78 -0
  145. package/src/middleware/strip-proxy-path.js +56 -0
  146. package/src/middleware/validate-fields.js +84 -0
  147. package/src/routes/ancillary.js +29 -0
  148. package/src/routes/dirname.cjs +1 -0
  149. package/src/routes/journey.js +212 -0
  150. package/src/routes/static.js +77 -0
  151. package/views/casa/components/character-count/README.md +10 -0
  152. package/views/casa/components/character-count/template.njk +6 -2
  153. package/views/casa/components/checkboxes/README.md +43 -34
  154. package/views/casa/components/checkboxes/template.njk +8 -7
  155. package/views/casa/components/date-input/README.md +11 -1
  156. package/views/casa/components/date-input/template.njk +6 -4
  157. package/views/casa/components/input/README.md +9 -0
  158. package/views/casa/components/input/template.njk +6 -2
  159. package/views/casa/components/postal-address-object/README.md +10 -0
  160. package/views/casa/components/postal-address-object/template.njk +20 -5
  161. package/views/casa/components/radios/README.md +49 -24
  162. package/views/casa/components/radios/template.njk +6 -3
  163. package/views/casa/components/select/README.md +65 -0
  164. package/views/casa/components/select/macro.njk +3 -0
  165. package/views/casa/components/select/template.njk +49 -0
  166. package/views/casa/components/textarea/README.md +9 -0
  167. package/views/casa/components/textarea/template.njk +6 -2
  168. package/views/casa/layouts/journey.njk +1 -1
@@ -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
+ }
@@ -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
+ }
@@ -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
+ };