@dwp/govuk-casa 9.0.0 → 9.2.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 (199) hide show
  1. package/README.md +9 -9
  2. package/dist/assets/css/casa.css +1 -1
  3. package/dist/assets/css/casa.css.map +1 -1
  4. package/dist/casa.d.ts +122 -95
  5. package/dist/casa.js +119 -86
  6. package/dist/casa.js.map +1 -1
  7. package/dist/lib/CasaTemplateLoader.d.ts +4 -4
  8. package/dist/lib/CasaTemplateLoader.js +16 -16
  9. package/dist/lib/CasaTemplateLoader.js.map +1 -1
  10. package/dist/lib/JourneyContext.d.ts +58 -40
  11. package/dist/lib/JourneyContext.js +132 -75
  12. package/dist/lib/JourneyContext.js.map +1 -1
  13. package/dist/lib/MutableRouter.d.ts +40 -41
  14. package/dist/lib/MutableRouter.js +64 -71
  15. package/dist/lib/MutableRouter.js.map +1 -1
  16. package/dist/lib/Plan.d.ts +29 -26
  17. package/dist/lib/Plan.js +85 -71
  18. package/dist/lib/Plan.js.map +1 -1
  19. package/dist/lib/ValidationError.d.ts +16 -15
  20. package/dist/lib/ValidationError.js +21 -20
  21. package/dist/lib/ValidationError.js.map +1 -1
  22. package/dist/lib/ValidatorFactory.d.ts +15 -13
  23. package/dist/lib/ValidatorFactory.js +14 -12
  24. package/dist/lib/ValidatorFactory.js.map +1 -1
  25. package/dist/lib/configuration-ingestor.d.ts +37 -40
  26. package/dist/lib/configuration-ingestor.js +93 -93
  27. package/dist/lib/configuration-ingestor.js.map +1 -1
  28. package/dist/lib/configure.d.ts +6 -6
  29. package/dist/lib/configure.js +14 -12
  30. package/dist/lib/configure.js.map +1 -1
  31. package/dist/lib/constants.d.ts +1 -3
  32. package/dist/lib/constants.js +9 -11
  33. package/dist/lib/constants.js.map +1 -1
  34. package/dist/lib/context-id-generators.d.ts +3 -5
  35. package/dist/lib/context-id-generators.js +7 -6
  36. package/dist/lib/context-id-generators.js.map +1 -1
  37. package/dist/lib/end-session.d.ts +4 -4
  38. package/dist/lib/end-session.js +5 -5
  39. package/dist/lib/field.d.ts +20 -18
  40. package/dist/lib/field.js +35 -48
  41. package/dist/lib/field.js.map +1 -1
  42. package/dist/lib/index.d.ts +13 -13
  43. package/dist/lib/logger.d.ts +7 -6
  44. package/dist/lib/logger.js +7 -7
  45. package/dist/lib/logger.js.map +1 -1
  46. package/dist/lib/mount.d.ts +5 -5
  47. package/dist/lib/mount.js +11 -10
  48. package/dist/lib/mount.js.map +1 -1
  49. package/dist/lib/nunjucks-filters.d.ts +10 -12
  50. package/dist/lib/nunjucks-filters.js +35 -35
  51. package/dist/lib/nunjucks-filters.js.map +1 -1
  52. package/dist/lib/nunjucks.d.ts +7 -5
  53. package/dist/lib/nunjucks.js +10 -8
  54. package/dist/lib/nunjucks.js.map +1 -1
  55. package/dist/lib/utils.d.ts +19 -19
  56. package/dist/lib/utils.js +62 -55
  57. package/dist/lib/utils.js.map +1 -1
  58. package/dist/lib/validators/dateObject.d.ts +29 -22
  59. package/dist/lib/validators/dateObject.js +58 -49
  60. package/dist/lib/validators/dateObject.js.map +1 -1
  61. package/dist/lib/validators/email.d.ts +4 -4
  62. package/dist/lib/validators/email.js +4 -4
  63. package/dist/lib/validators/inArray.d.ts +4 -4
  64. package/dist/lib/validators/inArray.js +7 -8
  65. package/dist/lib/validators/inArray.js.map +1 -1
  66. package/dist/lib/validators/index.d.ts +10 -10
  67. package/dist/lib/validators/index.js +1 -3
  68. package/dist/lib/validators/index.js.map +1 -1
  69. package/dist/lib/validators/nino.d.ts +9 -8
  70. package/dist/lib/validators/nino.js +14 -10
  71. package/dist/lib/validators/nino.js.map +1 -1
  72. package/dist/lib/validators/postalAddressObject.d.ts +37 -24
  73. package/dist/lib/validators/postalAddressObject.js +65 -46
  74. package/dist/lib/validators/postalAddressObject.js.map +1 -1
  75. package/dist/lib/validators/range.d.ts +12 -8
  76. package/dist/lib/validators/range.js +11 -9
  77. package/dist/lib/validators/range.js.map +1 -1
  78. package/dist/lib/validators/regex.d.ts +4 -4
  79. package/dist/lib/validators/regex.js +5 -5
  80. package/dist/lib/validators/required.d.ts +6 -6
  81. package/dist/lib/validators/required.js +9 -11
  82. package/dist/lib/validators/required.js.map +1 -1
  83. package/dist/lib/validators/strlen.d.ts +12 -8
  84. package/dist/lib/validators/strlen.js +13 -11
  85. package/dist/lib/validators/strlen.js.map +1 -1
  86. package/dist/lib/validators/wordCount.d.ts +12 -8
  87. package/dist/lib/validators/wordCount.js +15 -11
  88. package/dist/lib/validators/wordCount.js.map +1 -1
  89. package/dist/lib/waypoint-url.d.ts +16 -13
  90. package/dist/lib/waypoint-url.js +39 -36
  91. package/dist/lib/waypoint-url.js.map +1 -1
  92. package/dist/middleware/body-parser.d.ts +1 -1
  93. package/dist/middleware/body-parser.js +6 -6
  94. package/dist/middleware/body-parser.js.map +1 -1
  95. package/dist/middleware/data.d.ts +1 -1
  96. package/dist/middleware/data.js +8 -7
  97. package/dist/middleware/data.js.map +1 -1
  98. package/dist/middleware/gather-fields.d.ts +2 -2
  99. package/dist/middleware/gather-fields.js +6 -4
  100. package/dist/middleware/gather-fields.js.map +1 -1
  101. package/dist/middleware/i18n.js +13 -15
  102. package/dist/middleware/i18n.js.map +1 -1
  103. package/dist/middleware/post.js +30 -18
  104. package/dist/middleware/post.js.map +1 -1
  105. package/dist/middleware/pre.d.ts +2 -2
  106. package/dist/middleware/pre.js +46 -27
  107. package/dist/middleware/pre.js.map +1 -1
  108. package/dist/middleware/progress-journey.d.ts +1 -1
  109. package/dist/middleware/progress-journey.js +5 -5
  110. package/dist/middleware/progress-journey.js.map +1 -1
  111. package/dist/middleware/sanitise-fields.d.ts +1 -1
  112. package/dist/middleware/sanitise-fields.js +13 -11
  113. package/dist/middleware/sanitise-fields.js.map +1 -1
  114. package/dist/middleware/serve-first-waypoint.d.ts +3 -3
  115. package/dist/middleware/serve-first-waypoint.js +8 -6
  116. package/dist/middleware/serve-first-waypoint.js.map +1 -1
  117. package/dist/middleware/session.js +14 -11
  118. package/dist/middleware/session.js.map +1 -1
  119. package/dist/middleware/skip-waypoint.d.ts +1 -1
  120. package/dist/middleware/skip-waypoint.js +5 -9
  121. package/dist/middleware/skip-waypoint.js.map +1 -1
  122. package/dist/middleware/steer-journey.d.ts +1 -1
  123. package/dist/middleware/steer-journey.js +16 -14
  124. package/dist/middleware/steer-journey.js.map +1 -1
  125. package/dist/middleware/strip-proxy-path.d.ts +1 -1
  126. package/dist/middleware/strip-proxy-path.js +3 -3
  127. package/dist/middleware/strip-proxy-path.js.map +1 -1
  128. package/dist/middleware/validate-fields.d.ts +1 -1
  129. package/dist/middleware/validate-fields.js +2 -5
  130. package/dist/middleware/validate-fields.js.map +1 -1
  131. package/dist/routes/ancillary.d.ts +3 -3
  132. package/dist/routes/ancillary.js +4 -4
  133. package/dist/routes/ancillary.js.map +1 -1
  134. package/dist/routes/journey.d.ts +2 -2
  135. package/dist/routes/journey.js +91 -39
  136. package/dist/routes/journey.js.map +1 -1
  137. package/dist/routes/static.d.ts +7 -5
  138. package/dist/routes/static.js +20 -19
  139. package/dist/routes/static.js.map +1 -1
  140. package/package.json +20 -19
  141. package/src/casa.js +133 -100
  142. package/src/lib/CasaTemplateLoader.js +24 -19
  143. package/src/lib/JourneyContext.js +190 -107
  144. package/src/lib/MutableRouter.js +72 -74
  145. package/src/lib/Plan.js +145 -97
  146. package/src/lib/ValidationError.js +25 -21
  147. package/src/lib/ValidatorFactory.js +17 -13
  148. package/src/lib/configuration-ingestor.js +147 -110
  149. package/src/lib/configure.js +34 -32
  150. package/src/lib/constants.js +9 -11
  151. package/src/lib/context-id-generators.js +40 -43
  152. package/src/lib/end-session.js +6 -6
  153. package/src/lib/field.js +69 -58
  154. package/src/lib/index.js +12 -12
  155. package/src/lib/logger.js +9 -9
  156. package/src/lib/mount.js +70 -74
  157. package/src/lib/nunjucks-filters.js +56 -59
  158. package/src/lib/nunjucks.js +23 -18
  159. package/src/lib/utils.js +78 -57
  160. package/src/lib/validators/dateObject.js +71 -60
  161. package/src/lib/validators/email.js +8 -8
  162. package/src/lib/validators/inArray.js +10 -11
  163. package/src/lib/validators/index.js +12 -14
  164. package/src/lib/validators/nino.js +29 -15
  165. package/src/lib/validators/postalAddressObject.js +87 -63
  166. package/src/lib/validators/range.js +14 -12
  167. package/src/lib/validators/regex.js +8 -8
  168. package/src/lib/validators/required.js +16 -16
  169. package/src/lib/validators/strlen.js +16 -14
  170. package/src/lib/validators/wordCount.js +22 -14
  171. package/src/lib/waypoint-url.js +64 -46
  172. package/src/middleware/body-parser.js +10 -10
  173. package/src/middleware/csrf.js +1 -1
  174. package/src/middleware/data.js +28 -24
  175. package/src/middleware/gather-fields.js +10 -9
  176. package/src/middleware/i18n.js +35 -37
  177. package/src/middleware/post.js +41 -21
  178. package/src/middleware/pre.js +62 -41
  179. package/src/middleware/progress-journey.js +32 -18
  180. package/src/middleware/sanitise-fields.js +43 -20
  181. package/src/middleware/serve-first-waypoint.js +14 -12
  182. package/src/middleware/session.js +74 -61
  183. package/src/middleware/skip-waypoint.js +8 -15
  184. package/src/middleware/steer-journey.js +40 -28
  185. package/src/middleware/strip-proxy-path.js +8 -7
  186. package/src/middleware/validate-fields.js +5 -12
  187. package/src/routes/ancillary.js +5 -7
  188. package/src/routes/journey.js +159 -85
  189. package/src/routes/static.js +62 -29
  190. package/views/casa/components/character-count/README.md +2 -2
  191. package/views/casa/components/checkboxes/README.md +6 -6
  192. package/views/casa/components/date-input/README.md +7 -7
  193. package/views/casa/components/input/README.md +2 -2
  194. package/views/casa/components/journey-form/README.md +33 -14
  195. package/views/casa/components/postal-address-object/README.md +4 -4
  196. package/views/casa/components/radios/README.md +6 -6
  197. package/views/casa/components/select/README.md +6 -6
  198. package/views/casa/components/textarea/README.md +2 -2
  199. package/views/casa/layouts/main.njk +2 -1
@@ -6,61 +6,61 @@
6
6
  * - Validation errors on that data
7
7
  * - Navigation information about how the user got where they are.
8
8
  */
9
- import lodash from 'lodash';
10
- import ValidationError from './ValidationError.js';
11
- import logger from './logger.js';
12
- import { notProto } from './utils.js';
13
- import { uuid as uuidGenerator } from './context-id-generators.js';
9
+ import lodash from "lodash";
10
+ import ValidationError from "./ValidationError.js";
11
+ import logger from "./logger.js";
12
+ import { notProto } from "./utils.js";
13
+ import { uuid as uuidGenerator } from "./context-id-generators.js";
14
14
 
15
- const {
16
- isPlainObject, isObject, has, isEqual,
17
- } = lodash; // CommonJS
15
+ const { isPlainObject, isObject, has, isEqual } = lodash; // CommonJS
18
16
 
19
- const log = logger('lib:journey-context');
17
+ const log = logger("lib:journey-context");
20
18
 
21
19
  const uuid = uuidGenerator();
22
20
 
23
21
  /**
22
+ * @typedef {import("../casa").ContextEventUserInfo} ContextEventUserInfo
24
23
  * @access private
25
- * @typedef {import('../casa').ContextEventUserInfo} ContextEventUserInfo
26
24
  */
27
25
 
28
26
  /**
27
+ * @typedef {import("../casa").Page} Page
29
28
  * @access private
30
- * @typedef {import('../casa').Page} Page
31
29
  */
32
30
 
33
31
  /**
32
+ * @typedef {import("../casa").ContextEventHandler} ContextEventHandler
34
33
  * @access private
35
- * @typedef {import('../casa').ContextEventHandler} ContextEventHandler
36
34
  */
37
35
 
38
36
  /**
37
+ * @typedef {import("../casa").ContextEvent} ContextEvent
39
38
  * @access private
40
- * @typedef {import('../casa').ContextEvent} ContextEvent
41
39
  */
42
40
 
43
41
  /**
42
+ * @typedef {import("../casa").JourneyContextObject} JourneyContextObject
44
43
  * @access private
45
- * @typedef {import('../casa').JourneyContextObject} JourneyContextObject
46
44
  */
47
45
 
48
46
  /**
47
+ * @typedef {import("express").Request} ExpressRequest
49
48
  * @access private
50
- * @typedef {import('express').Request} ExpressRequest
51
49
  */
52
50
 
53
- export function validateObjectKey(key = '') {
51
+ export function validateObjectKey(key = "") {
54
52
  const keyLower = String.prototype.toLowerCase.call(key);
55
- if (keyLower === 'prototype' || keyLower === '__proto__' || keyLower === 'constructor') {
53
+ if (
54
+ keyLower === "prototype" ||
55
+ keyLower === "__proto__" ||
56
+ keyLower === "constructor"
57
+ ) {
56
58
  throw new SyntaxError(`Invalid object key used, ${key}`);
57
59
  }
58
60
  return String(key);
59
61
  }
60
62
 
61
- /**
62
- * @memberof module:@dwp/govuk-casa
63
- */
63
+ /** @memberof module:@dwp/govuk-casa */
64
64
  export default class JourneyContext {
65
65
  // Private properties
66
66
  #data;
@@ -75,24 +75,20 @@ export default class JourneyContext {
75
75
 
76
76
  #eventListenerPreState;
77
77
 
78
- static DEFAULT_CONTEXT_ID = 'default';
78
+ static DEFAULT_CONTEXT_ID = "default";
79
79
 
80
- /**
81
- * @type {symbol}
82
- */
83
- static ID_GENERATOR_REQ_LOG = Symbol('generatedContextIds');
80
+ /** @type {symbol} */
81
+ static ID_GENERATOR_REQ_LOG = Symbol("generatedContextIds");
84
82
 
85
- /**
86
- * @type {symbol}
87
- */
88
- static ID_GENERATOR_REQ_KEY = Symbol('generateContextId');
83
+ /** @type {symbol} */
84
+ static ID_GENERATOR_REQ_KEY = Symbol("generateContextId");
89
85
 
90
86
  /**
91
87
  * Constructor.
92
88
  *
93
89
  * `data` is the "single source of truth" for all data gathered during the
94
90
  * user's journey. This is referred to as the "canonical data model".
95
- * Page-specific "views" of this data are generated at runtime in order to
91
+ * Page-specific "views" of this data are generated at runtime in order to
96
92
  * populate/validate specific form fields.
97
93
  *
98
94
  * `validation` holds the results of form field validation carried out when
@@ -105,10 +101,11 @@ export default class JourneyContext {
105
101
  * `identity` holds information that helps uniquely identify this context
106
102
  * among a group of contexts stored in the session.
107
103
  *
108
- * @param {Record<string,any>} data Entire journey data.
104
+ * @param {Record<string, any>} data Entire journey data.
109
105
  * @param {object} validation Page errors (indexed by waypoint id).
110
106
  * @param {object} nav Navigation context.
111
- * @param {object} identity Some metadata for identifying this context among others.
107
+ * @param {object} identity Some metadata for identifying this context among
108
+ * others.
112
109
  */
113
110
  constructor(data = {}, validation = {}, nav = {}, identity = {}) {
114
111
  this.#data = data;
@@ -152,7 +149,9 @@ export default class JourneyContext {
152
149
  let dErrors = errors;
153
150
 
154
151
  if (Array.isArray(errors)) {
155
- dErrors = errors.map((e) => (e instanceof ValidationError ? e : new ValidationError(e)));
152
+ dErrors = errors.map((e) =>
153
+ e instanceof ValidationError ? e : new ValidationError(e),
154
+ );
156
155
  }
157
156
 
158
157
  deserialisedValidation[notProto(waypoint)] = dErrors;
@@ -184,18 +183,20 @@ export default class JourneyContext {
184
183
  /**
185
184
  * Get data context for a specific a specific page.
186
185
  *
187
- * @param {string | Page} page Page waypoint ID, or Page object.
186
+ * @param {string | Page} page Page waypoint ID, or Page object.
188
187
  * @returns {object} Page data.
189
188
  * @throws {TypeError} When page is invalid.
190
189
  */
191
190
  getDataForPage(page) {
192
- if (typeof page === 'string') {
191
+ if (typeof page === "string") {
193
192
  return this.#data[validateObjectKey(page)];
194
193
  }
195
194
  if (isPlainObject(page)) {
196
195
  return this.#data[validateObjectKey(page.waypoint)];
197
196
  }
198
- throw new TypeError(`Page must be a string or Page object. Got ${typeof page}`);
197
+ throw new TypeError(
198
+ `Page must be a string or Page object. Got ${typeof page}`,
199
+ );
199
200
  }
200
201
 
201
202
  /**
@@ -227,12 +228,14 @@ export default class JourneyContext {
227
228
  * @throws {TypeError} When page is invalid.
228
229
  */
229
230
  setDataForPage(page, webFormData) {
230
- if (typeof page === 'string') {
231
+ if (typeof page === "string") {
231
232
  this.#data[validateObjectKey(page)] = webFormData;
232
233
  } else if (isPlainObject(page)) {
233
234
  this.#data[validateObjectKey(page.waypoint)] = webFormData;
234
235
  } else {
235
- throw new TypeError(`Page must be a string or Page object. Got ${typeof page}`)
236
+ throw new TypeError(
237
+ `Page must be a string or Page object. Got ${typeof page}`,
238
+ );
236
239
  }
237
240
 
238
241
  return this;
@@ -286,12 +289,14 @@ export default class JourneyContext {
286
289
  */
287
290
  setValidationErrorsForPage(pageId, errors = []) {
288
291
  if (!Array.isArray(errors)) {
289
- throw new SyntaxError(`Errors must be an Array. Received ${Object.prototype.toString.call(errors)}`);
292
+ throw new SyntaxError(
293
+ `Errors must be an Array. Received ${Object.prototype.toString.call(errors)}`,
294
+ );
290
295
  }
291
296
 
292
297
  errors.forEach((error) => {
293
298
  if (!(error instanceof ValidationError)) {
294
- throw new SyntaxError('Field errors must be a ValidationError');
299
+ throw new SyntaxError("Field errors must be a ValidationError");
295
300
  }
296
301
  });
297
302
 
@@ -312,12 +317,13 @@ export default class JourneyContext {
312
317
  }
313
318
 
314
319
  /**
315
- * Same as `getValidationErrorsForPage()`, but the return value is
316
- * an object whose keys are the field names, and values are the list of errors
320
+ * Same as `getValidationErrorsForPage()`, but the return value is an object
321
+ * whose keys are the field names, and values are the list of errors
317
322
  * associated with that particular field.
318
323
  *
319
324
  * @param {string} pageId Page ID.
320
- * @returns {object} Object indexed by field names; values containing list of errors
325
+ * @returns {object} Object indexed by field names; values containing list of
326
+ * errors
321
327
  */
322
328
  getValidationErrorsForPageByField(pageId) {
323
329
  const errors = this.getValidationErrorsForPage(pageId);
@@ -353,7 +359,7 @@ export default class JourneyContext {
353
359
  * @param {string} language Language to set (ISO 639-1 2-letter code).
354
360
  * @returns {JourneyContext} Chain.
355
361
  */
356
- setNavigationLanguage(language = 'en') {
362
+ setNavigationLanguage(language = "en") {
357
363
  this.#nav.language = language;
358
364
  return this;
359
365
  }
@@ -376,7 +382,9 @@ export default class JourneyContext {
376
382
  purge(waypoints = []) {
377
383
  const newData = Object.create(null);
378
384
  const newValidation = Object.create(null);
379
- const toKeep = Object.keys(this.#data).filter((w) => !waypoints.includes(w));
385
+ const toKeep = Object.keys(this.#data).filter(
386
+ (w) => !waypoints.includes(w),
387
+ );
380
388
 
381
389
  // ESLint disabled as `i` is an integer
382
390
  /* eslint-disable security/detect-object-injection */
@@ -405,8 +413,8 @@ export default class JourneyContext {
405
413
  }
406
414
 
407
415
  /**
408
- * Event listeners are transient. They are not stored in session, and generally
409
- * only apply for the current request.
416
+ * Event listeners are transient. They are not stored in session, and
417
+ * generally only apply for the current request.
410
418
  *
411
419
  * They also only act on a fixed snapshot of this context's state, which is
412
420
  * taken at the point of attaching the listeners (in the "data" middleware).
@@ -431,7 +439,7 @@ export default class JourneyContext {
431
439
  * @param {object} params Params
432
440
  * @param {string} params.event Event (waypoint-change | context-change)
433
441
  * @param {object} params.session Session
434
- * @param {ContextEventUserInfo|object} [params.userInfo] Pass-through info
442
+ * @param {ContextEventUserInfo | object} [params.userInfo] Pass-through info
435
443
  * @returns {JourneyContext} Chain
436
444
  */
437
445
  applyEventListeners({ event, session, userInfo }) {
@@ -439,7 +447,9 @@ export default class JourneyContext {
439
447
  return this;
440
448
  }
441
449
 
442
- const previousContext = JourneyContext.fromObject(this.#eventListenerPreState);
450
+ const previousContext = JourneyContext.fromObject(
451
+ this.#eventListenerPreState,
452
+ );
443
453
  const listeners = this.#eventListeners.filter((l) => l.event === event);
444
454
 
445
455
  // ESLint disabled as `listeners[i]` uses an integer key, and the other keys
@@ -453,20 +463,21 @@ export default class JourneyContext {
453
463
  let runHandler = false;
454
464
 
455
465
  if (!waypoint && !field) {
456
- logMessage = 'Calling generic event handler';
466
+ logMessage = "Calling generic event handler";
457
467
  runHandler = true;
458
468
  } else if (waypoint && !field) {
459
469
  logMessage = `Calling waypoint-specific event handler on "${waypoint}"`;
460
- runHandler = previousContext.data?.[waypoint] !== undefined && !isEqual(
461
- this.data?.[waypoint],
462
- previousContext.data?.[waypoint],
463
- );
470
+ runHandler =
471
+ previousContext.data?.[waypoint] !== undefined &&
472
+ !isEqual(this.data?.[waypoint], previousContext.data?.[waypoint]);
464
473
  } else if (waypoint && field) {
465
474
  logMessage = `Calling field-specific event handler on "${waypoint} : ${field}"`;
466
- runHandler = previousContext.data?.[waypoint]?.[field] !== undefined && !isEqual(
467
- this.data?.[waypoint]?.[field],
468
- previousContext.data?.[waypoint]?.[field],
469
- );
475
+ runHandler =
476
+ previousContext.data?.[waypoint]?.[field] !== undefined &&
477
+ !isEqual(
478
+ this.data?.[waypoint]?.[field],
479
+ previousContext.data?.[waypoint]?.[field],
480
+ );
470
481
  }
471
482
 
472
483
  if (runHandler) {
@@ -514,7 +525,7 @@ export default class JourneyContext {
514
525
  */
515
526
  static fromContext(context, req) {
516
527
  if (!(context instanceof JourneyContext)) {
517
- throw new TypeError('Source context must be a JourneyContext');
528
+ throw new TypeError("Source context must be a JourneyContext");
518
529
  }
519
530
 
520
531
  const newContextObj = context.toObject();
@@ -543,14 +554,16 @@ export default class JourneyContext {
543
554
  // being remodelled as an array, we need to convert the "legacy" structure
544
555
  // into an equivalent array.
545
556
  if (isPlainObject(session?.journeyContextList)) {
546
- log.trace('Session context list already initialised as an object (legacy structure). Will convert from object to array.');
557
+ log.trace(
558
+ "Session context list already initialised as an object (legacy structure). Will convert from object to array.",
559
+ );
547
560
  /* eslint-disable-next-line no-param-reassign */
548
561
  session.journeyContextList = Object.entries(session.journeyContextList);
549
562
  }
550
563
 
551
564
  // Initialise new context list in the session
552
- if (!has(session, 'journeyContextList')) {
553
- log.trace('Initialising session with a default journey context list');
565
+ if (!has(session, "journeyContextList")) {
566
+ log.trace("Initialising session with a default journey context list");
554
567
  /* eslint-disable-next-line no-param-reassign */
555
568
  session.journeyContextList = [];
556
569
 
@@ -562,6 +575,7 @@ export default class JourneyContext {
562
575
 
563
576
  /**
564
577
  * Validate the format of a context ID:
578
+ *
565
579
  * - Between 1 and 64 characters
566
580
  * - Contain only the characters a-z, 0-9, -
567
581
  *
@@ -575,18 +589,18 @@ export default class JourneyContext {
575
589
  return JourneyContext.DEFAULT_CONTEXT_ID;
576
590
  }
577
591
 
578
- if (typeof id !== 'string') {
579
- throw new TypeError('Context ID must be a string');
592
+ if (typeof id !== "string") {
593
+ throw new TypeError("Context ID must be a string");
580
594
  } else if (!id.match(/^[a-z0-9-]{1,64}$/)) {
581
- throw new SyntaxError('Context ID is not in the correct format');
595
+ throw new SyntaxError("Context ID is not in the correct format");
582
596
  }
583
597
 
584
598
  return id;
585
599
  }
586
600
 
587
601
  /**
588
- * Generate a new context ID, validate it, and throw if the ID has already been
589
- * generated during this request lifecycle. This may happen if an ID was
602
+ * Generate a new context ID, validate it, and throw if the ID has already
603
+ * been generated during this request lifecycle. This may happen if an ID was
590
604
  * generated, but never used to store a new context in the session. Therefore
591
605
  * it is important for user code to always call `putContext()` before
592
606
  * generating another ID.
@@ -599,12 +613,14 @@ export default class JourneyContext {
599
613
  // Can't generate custom ID when no request object is provided, because the
600
614
  // custom generator function itself exists on that object.
601
615
  if (!req) {
602
- throw new Error('Missing required request object.')
616
+ throw new Error("Missing required request object.");
603
617
  }
604
618
 
605
619
  // Define a default context ID generator if required
606
620
  if (!Object.hasOwn(req, JourneyContext.ID_GENERATOR_REQ_KEY)) {
607
- log.warn('A context ID generator is not present in the request. Reverting to uuid().');
621
+ log.warn(
622
+ "A context ID generator is not present in the request. Reverting to uuid().",
623
+ );
608
624
  Object.defineProperty(req, JourneyContext.ID_GENERATOR_REQ_KEY, {
609
625
  value: uuid,
610
626
  enumerable: false,
@@ -620,14 +636,18 @@ export default class JourneyContext {
620
636
  .map((c) => c.identity.id)
621
637
  .filter((id) => id !== JourneyContext.DEFAULT_CONTEXT_ID);
622
638
  const inRequestIds = req[JourneyContext.ID_GENERATOR_REQ_LOG] ?? [];
623
- const reservedIds = Array.from(new Set([...inSessionIds, ...inRequestIds]).values());
639
+ const reservedIds = Array.from(
640
+ new Set([...inSessionIds, ...inRequestIds]).values(),
641
+ );
624
642
 
625
643
  // Generate and log the ID
626
644
  const id = JourneyContext.validateContextId(
627
645
  req[JourneyContext.ID_GENERATOR_REQ_KEY].call(null, { req, reservedIds }),
628
646
  );
629
647
  if (reservedIds.includes(id)) {
630
- throw new Error(`Regenerated a context ID, ${String(id)}. It has likely not yet been used to store a new context in session using JourneyContext.putContext().`);
648
+ throw new Error(
649
+ `Regenerated a context ID, ${String(id)}. It has likely not yet been used to store a new context in session using JourneyContext.putContext().`,
650
+ );
631
651
  }
632
652
 
633
653
  if (!req[JourneyContext.ID_GENERATOR_REQ_LOG]) {
@@ -650,7 +670,10 @@ export default class JourneyContext {
650
670
  * @returns {JourneyContext} The default Journey Context
651
671
  */
652
672
  static getDefaultContext(session) {
653
- return JourneyContext.getContextById(session, JourneyContext.DEFAULT_CONTEXT_ID);
673
+ return JourneyContext.getContextById(
674
+ session,
675
+ JourneyContext.DEFAULT_CONTEXT_ID,
676
+ );
654
677
  }
655
678
 
656
679
  /**
@@ -681,9 +704,7 @@ export default class JourneyContext {
681
704
  static getContextByName(session, name) {
682
705
  if (session) {
683
706
  const list = new Map(session?.journeyContextList);
684
- const context = [...list.values()].find(
685
- (c) => (c.identity.name === name),
686
- );
707
+ const context = [...list.values()].find((c) => c.identity.name === name);
687
708
  if (context) {
688
709
  return JourneyContext.fromObject(context);
689
710
  }
@@ -697,14 +718,14 @@ export default class JourneyContext {
697
718
  *
698
719
  * @param {object} session Request session
699
720
  * @param {string} tag Context tag
700
- * @returns {Array<JourneyContext>} The discovered JourneyContext instance
721
+ * @returns {JourneyContext[]} The discovered JourneyContext instance
701
722
  */
702
723
  static getContextsByTag(session, tag) {
703
724
  if (session) {
704
725
  const list = new Map(session?.journeyContextList);
705
- return [...list.values()].filter(
706
- (c) => (c.identity.tags?.includes(tag)),
707
- ).map((c) => (JourneyContext.fromObject(c)));
726
+ return [...list.values()]
727
+ .filter((c) => c.identity.tags?.includes(tag))
728
+ .map((c) => JourneyContext.fromObject(c));
708
729
  }
709
730
 
710
731
  return undefined;
@@ -717,10 +738,10 @@ export default class JourneyContext {
717
738
  * @returns {Array} Array of contexts
718
739
  */
719
740
  static getContexts(session) {
720
- if (has(session, 'journeyContextList')) {
721
- return session.journeyContextList.map(([, contextObj]) => (
722
- JourneyContext.fromObject(contextObj)
723
- ));
741
+ if (has(session, "journeyContextList")) {
742
+ return session.journeyContextList.map(([, contextObj]) =>
743
+ JourneyContext.fromObject(contextObj),
744
+ );
724
745
  }
725
746
 
726
747
  return [];
@@ -732,21 +753,22 @@ export default class JourneyContext {
732
753
  * @param {object} session Request session
733
754
  * @param {JourneyContext} context Context
734
755
  * @param {object} options Options
735
- * @param {ContextEventUserInfo|object} [options.userInfo] Pass-through event info
756
+ * @param {ContextEventUserInfo | object} [options.userInfo] Pass-through
757
+ * event info
736
758
  * @returns {void}
737
759
  * @throws {TypeError} When session is not a valid type, or context has no ID
738
760
  */
739
761
  static putContext(session, context, options = {}) {
740
762
  if (!isObject(session)) {
741
- throw new TypeError('Session must be an object');
763
+ throw new TypeError("Session must be an object");
742
764
  } else if (!(context instanceof JourneyContext)) {
743
- throw new TypeError('Context must be a valid JourneyContext');
765
+ throw new TypeError("Context must be a valid JourneyContext");
744
766
  } else if (context.identity.id === undefined) {
745
- throw new TypeError('Context must have an ID before storing in session');
767
+ throw new TypeError("Context must have an ID before storing in session");
746
768
  }
747
769
 
748
770
  // Initialise the session if necessary
749
- if (!has(session, 'journeyContextList')) {
771
+ if (!has(session, "journeyContextList")) {
750
772
  JourneyContext.initContextStore(session);
751
773
  }
752
774
 
@@ -754,13 +776,13 @@ export default class JourneyContext {
754
776
  const { userInfo = undefined } = options;
755
777
 
756
778
  context.applyEventListeners({
757
- event: 'waypoint-change',
779
+ event: "waypoint-change",
758
780
  session,
759
781
  userInfo,
760
782
  });
761
783
 
762
784
  context.applyEventListeners({
763
- event: 'context-change',
785
+ event: "context-change",
764
786
  session,
765
787
  userInfo,
766
788
  });
@@ -792,7 +814,9 @@ export default class JourneyContext {
792
814
  * @returns {void}
793
815
  */
794
816
  static removeContextById(session, id) {
795
- const index = (session?.journeyContextList ?? []).findIndex(([contextId]) => contextId === id);
817
+ const index = (session?.journeyContextList ?? []).findIndex(
818
+ ([contextId]) => contextId === id,
819
+ );
796
820
  if (index > -1) {
797
821
  session.journeyContextList.splice(index, 1);
798
822
  }
@@ -820,8 +844,8 @@ export default class JourneyContext {
820
844
  * @returns {void}
821
845
  */
822
846
  static removeContextsByTag(session, tag) {
823
- JourneyContext.getContextsByTag(session, tag).forEach(
824
- (c) => JourneyContext.removeContext(session, c),
847
+ JourneyContext.getContextsByTag(session, tag).forEach((c) =>
848
+ JourneyContext.removeContext(session, c),
825
849
  );
826
850
  }
827
851
 
@@ -832,15 +856,17 @@ export default class JourneyContext {
832
856
  * @returns {void}
833
857
  */
834
858
  static removeContexts(session) {
835
- JourneyContext.getContexts(session).forEach((c) => JourneyContext.removeContext(session, c));
859
+ JourneyContext.getContexts(session).forEach((c) =>
860
+ JourneyContext.removeContext(session, c),
861
+ );
836
862
  }
837
863
 
838
864
  /**
839
865
  * Extract the Journey Context referred to in the incoming request.
840
866
  *
841
- * This will look in `req.params`, `req.query` and
842
- * `req.body` for a `contextid` parameter, and use that
843
- * to load the correct Journey Context from the session.
867
+ * This will look in `req.params`, `req.query` and `req.body` for a
868
+ * `contextid` parameter, and use that to load the correct Journey Context
869
+ * from the session.
844
870
  *
845
871
  * @param {ExpressRequest} req ExpressJS incoming request
846
872
  * @returns {JourneyContext} The Journey Context
@@ -849,17 +875,19 @@ export default class JourneyContext {
849
875
  JourneyContext.initContextStore(req.session);
850
876
 
851
877
  let contextId;
852
- if (has(req?.params, 'contextid')) {
853
- log.trace('Context ID found in req.params.contextid');
878
+ if (has(req?.params, "contextid")) {
879
+ log.trace("Context ID found in req.params.contextid");
854
880
  contextId = String(req.params.contextid);
855
- } else if (has(req.query, 'contextid')) {
856
- log.trace('Context ID found in req.query.contextid');
881
+ } else if (has(req.query, "contextid")) {
882
+ log.trace("Context ID found in req.query.contextid");
857
883
  contextId = String(req.query.contextid);
858
- } else if (has(req?.body, 'contextid')) {
859
- log.trace('Context ID found in req.body.contextid');
884
+ } else if (has(req?.body, "contextid")) {
885
+ log.trace("Context ID found in req.body.contextid");
860
886
  contextId = String(req.body.contextid);
861
887
  } else {
862
- log.trace('Context ID not specified or not found; will attempt to use default');
888
+ log.trace(
889
+ "Context ID not specified or not found; will attempt to use default",
890
+ );
863
891
  contextId = JourneyContext.DEFAULT_CONTEXT_ID;
864
892
  }
865
893
 
@@ -867,13 +895,68 @@ export default class JourneyContext {
867
895
  contextId = JourneyContext.validateContextId(contextId);
868
896
  const context = JourneyContext.getContextById(req.session, contextId);
869
897
  if (!context) {
870
- throw (new Error(`Could not find a context with id, ${contextId}`));
898
+ throw new Error(`Could not find a context with id, ${contextId}`);
871
899
  }
872
900
  return context;
873
901
  } catch (err) {
874
902
  log.debug(err.message);
875
- log.trace('Falling back to default context');
876
- return JourneyContext.getContextById(req.session, JourneyContext.DEFAULT_CONTEXT_ID);
903
+ log.trace("Falling back to default context");
904
+ return JourneyContext.getContextById(
905
+ req.session,
906
+ JourneyContext.DEFAULT_CONTEXT_ID,
907
+ );
908
+ }
909
+ }
910
+
911
+ /**
912
+ * Set page skipped status.
913
+ *
914
+ * @param {string} waypoint Waypoint to skip.
915
+ * @param {boolean | object} opts Is skipped flag or options.
916
+ * @param {string} opts.to Waypoint to skip to.
917
+ */
918
+ setSkipped(waypoint, opts) {
919
+ // Unset, with setSkipped(a, false)
920
+ if (opts === false) {
921
+ this.data[waypoint] ??= Object.create(null);
922
+ this.data[waypoint].__skipped__ = undefined;
923
+ this.data[waypoint].__skip__ = undefined;
924
+ }
925
+ // Set, with setSkipped(a, true) and clear data
926
+ else if (opts === true) {
927
+ this.data[waypoint] = Object.create(null);
928
+ this.data[waypoint].__skipped__ = true;
929
+ this.data[waypoint].__skip__ = { to: null };
930
+ }
931
+ // Set, with setSkipped(a, { to: b }) and clear data
932
+ else if (typeof opts?.to === "string") {
933
+ this.data[waypoint] = Object.create(null);
934
+ this.data[waypoint].__skipped__ = true;
935
+ this.data[waypoint].__skip__ = { to: opts.to };
936
+ } else {
937
+ throw new TypeError(
938
+ `setSkipped opts must be a boolean or object with a "to" prop of waypoint to skip to, got: ${typeof opts}`,
939
+ );
940
+ }
941
+ }
942
+
943
+ /**
944
+ * Tests if a page has been skipped.
945
+ *
946
+ * @param {string} page Page ID (waypoint).
947
+ * @param {object} opts Skip ptions.
948
+ * @param {string} opts.to Waypoint that should be skipped to.
949
+ * @returns {boolean} True if the page has been skipped, or if it has been
950
+ * skipped to a specific page.
951
+ */
952
+ isSkipped(waypoint, opts) {
953
+ if (opts === undefined) {
954
+ return (
955
+ this.data[waypoint]?.__skipped__ === true ||
956
+ this.data[waypoint]?.__skip__ !== undefined
957
+ );
958
+ } else if (typeof opts.to === "string") {
959
+ return this.data[waypoint]?.__skip__?.to === opts.to;
877
960
  }
878
961
  }
879
962
  }