@dwp/govuk-casa 8.15.0 → 9.1.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 +9 -9
- package/dist/assets/css/casa.css +2 -1
- package/dist/assets/css/casa.css.map +1 -0
- package/dist/casa.d.ts +122 -99
- package/dist/casa.js +120 -88
- package/dist/casa.js.map +1 -1
- package/dist/lib/CasaTemplateLoader.d.ts +4 -4
- package/dist/lib/CasaTemplateLoader.js +16 -16
- package/dist/lib/CasaTemplateLoader.js.map +1 -1
- package/dist/lib/JourneyContext.d.ts +38 -40
- package/dist/lib/JourneyContext.js +89 -75
- package/dist/lib/JourneyContext.js.map +1 -1
- package/dist/lib/MutableRouter.d.ts +40 -41
- package/dist/lib/MutableRouter.js +64 -71
- package/dist/lib/MutableRouter.js.map +1 -1
- package/dist/lib/Plan.d.ts +29 -26
- package/dist/lib/Plan.js +85 -71
- package/dist/lib/Plan.js.map +1 -1
- package/dist/lib/ValidationError.d.ts +16 -15
- package/dist/lib/ValidationError.js +21 -20
- package/dist/lib/ValidationError.js.map +1 -1
- package/dist/lib/ValidatorFactory.d.ts +15 -13
- package/dist/lib/ValidatorFactory.js +14 -12
- package/dist/lib/ValidatorFactory.js.map +1 -1
- package/dist/lib/configuration-ingestor.d.ts +37 -40
- package/dist/lib/configuration-ingestor.js +93 -93
- package/dist/lib/configuration-ingestor.js.map +1 -1
- package/dist/lib/configure.d.ts +6 -6
- package/dist/lib/configure.js +14 -12
- package/dist/lib/configure.js.map +1 -1
- package/dist/lib/constants.d.ts +1 -3
- package/dist/lib/constants.js +9 -11
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/context-id-generators.d.ts +3 -5
- package/dist/lib/context-id-generators.js +7 -6
- package/dist/lib/context-id-generators.js.map +1 -1
- package/dist/lib/end-session.d.ts +4 -4
- package/dist/lib/end-session.js +5 -5
- package/dist/lib/field.d.ts +24 -29
- package/dist/lib/field.js +41 -70
- package/dist/lib/field.js.map +1 -1
- package/dist/lib/index.d.ts +13 -13
- package/dist/lib/logger.d.ts +7 -6
- package/dist/lib/logger.js +7 -7
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/mount.d.ts +5 -5
- package/dist/lib/mount.js +12 -17
- package/dist/lib/mount.js.map +1 -1
- package/dist/lib/nunjucks-filters.d.ts +10 -12
- package/dist/lib/nunjucks-filters.js +35 -35
- package/dist/lib/nunjucks-filters.js.map +1 -1
- package/dist/lib/nunjucks.d.ts +7 -5
- package/dist/lib/nunjucks.js +10 -8
- package/dist/lib/nunjucks.js.map +1 -1
- package/dist/lib/utils.d.ts +19 -19
- package/dist/lib/utils.js +62 -55
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/validators/dateObject.d.ts +29 -22
- package/dist/lib/validators/dateObject.js +58 -49
- package/dist/lib/validators/dateObject.js.map +1 -1
- package/dist/lib/validators/email.d.ts +4 -4
- package/dist/lib/validators/email.js +4 -4
- package/dist/lib/validators/inArray.d.ts +4 -4
- package/dist/lib/validators/inArray.js +7 -8
- package/dist/lib/validators/inArray.js.map +1 -1
- package/dist/lib/validators/index.d.ts +10 -10
- package/dist/lib/validators/index.js +1 -3
- package/dist/lib/validators/index.js.map +1 -1
- package/dist/lib/validators/nino.d.ts +9 -8
- package/dist/lib/validators/nino.js +14 -10
- package/dist/lib/validators/nino.js.map +1 -1
- package/dist/lib/validators/postalAddressObject.d.ts +37 -24
- package/dist/lib/validators/postalAddressObject.js +65 -46
- package/dist/lib/validators/postalAddressObject.js.map +1 -1
- package/dist/lib/validators/range.d.ts +12 -8
- package/dist/lib/validators/range.js +11 -9
- package/dist/lib/validators/range.js.map +1 -1
- package/dist/lib/validators/regex.d.ts +4 -4
- package/dist/lib/validators/regex.js +5 -5
- package/dist/lib/validators/required.d.ts +6 -6
- package/dist/lib/validators/required.js +9 -11
- package/dist/lib/validators/required.js.map +1 -1
- package/dist/lib/validators/strlen.d.ts +12 -8
- package/dist/lib/validators/strlen.js +13 -11
- package/dist/lib/validators/strlen.js.map +1 -1
- package/dist/lib/validators/wordCount.d.ts +12 -8
- package/dist/lib/validators/wordCount.js +15 -11
- package/dist/lib/validators/wordCount.js.map +1 -1
- package/dist/lib/waypoint-url.d.ts +16 -13
- package/dist/lib/waypoint-url.js +39 -36
- package/dist/lib/waypoint-url.js.map +1 -1
- package/dist/middleware/body-parser.d.ts +1 -1
- package/dist/middleware/body-parser.js +6 -6
- package/dist/middleware/body-parser.js.map +1 -1
- package/dist/middleware/data.d.ts +1 -1
- package/dist/middleware/data.js +8 -7
- package/dist/middleware/data.js.map +1 -1
- package/dist/middleware/gather-fields.d.ts +2 -2
- package/dist/middleware/gather-fields.js +6 -4
- package/dist/middleware/gather-fields.js.map +1 -1
- package/dist/middleware/i18n.js +13 -15
- package/dist/middleware/i18n.js.map +1 -1
- package/dist/middleware/post.js +30 -18
- package/dist/middleware/post.js.map +1 -1
- package/dist/middleware/pre.d.ts +2 -2
- package/dist/middleware/pre.js +46 -26
- package/dist/middleware/pre.js.map +1 -1
- package/dist/middleware/progress-journey.d.ts +1 -1
- package/dist/middleware/progress-journey.js +5 -5
- package/dist/middleware/progress-journey.js.map +1 -1
- package/dist/middleware/sanitise-fields.d.ts +1 -1
- package/dist/middleware/sanitise-fields.js +13 -11
- package/dist/middleware/sanitise-fields.js.map +1 -1
- package/dist/middleware/serve-first-waypoint.d.ts +3 -3
- package/dist/middleware/serve-first-waypoint.js +8 -6
- package/dist/middleware/serve-first-waypoint.js.map +1 -1
- package/dist/middleware/session.js +14 -11
- package/dist/middleware/session.js.map +1 -1
- package/dist/middleware/skip-waypoint.d.ts +1 -1
- package/dist/middleware/skip-waypoint.js +3 -3
- package/dist/middleware/skip-waypoint.js.map +1 -1
- package/dist/middleware/steer-journey.d.ts +1 -1
- package/dist/middleware/steer-journey.js +16 -14
- package/dist/middleware/steer-journey.js.map +1 -1
- package/dist/middleware/strip-proxy-path.d.ts +1 -1
- package/dist/middleware/strip-proxy-path.js +3 -3
- package/dist/middleware/strip-proxy-path.js.map +1 -1
- package/dist/middleware/validate-fields.d.ts +1 -1
- package/dist/middleware/validate-fields.js +2 -5
- package/dist/middleware/validate-fields.js.map +1 -1
- package/dist/routes/ancillary.d.ts +3 -3
- package/dist/routes/ancillary.js +4 -4
- package/dist/routes/ancillary.js.map +1 -1
- package/dist/routes/journey.d.ts +2 -2
- package/dist/routes/journey.js +91 -39
- package/dist/routes/journey.js.map +1 -1
- package/dist/routes/static.d.ts +7 -5
- package/dist/routes/static.js +20 -20
- package/dist/routes/static.js.map +1 -1
- package/package.json +17 -16
- package/src/casa.js +134 -102
- package/src/lib/CasaTemplateLoader.js +24 -19
- package/src/lib/JourneyContext.js +147 -107
- package/src/lib/MutableRouter.js +72 -74
- package/src/lib/Plan.js +145 -97
- package/src/lib/ValidationError.js +25 -21
- package/src/lib/ValidatorFactory.js +17 -13
- package/src/lib/configuration-ingestor.js +147 -110
- package/src/lib/configure.js +34 -32
- package/src/lib/constants.js +9 -11
- package/src/lib/context-id-generators.js +40 -43
- package/src/lib/end-session.js +6 -6
- package/src/lib/field.js +74 -78
- package/src/lib/index.js +12 -12
- package/src/lib/logger.js +9 -9
- package/src/lib/mount.js +70 -80
- package/src/lib/nunjucks-filters.js +56 -59
- package/src/lib/nunjucks.js +23 -18
- package/src/lib/utils.js +78 -57
- package/src/lib/validators/dateObject.js +71 -60
- package/src/lib/validators/email.js +8 -8
- package/src/lib/validators/inArray.js +10 -11
- package/src/lib/validators/index.js +12 -14
- package/src/lib/validators/nino.js +29 -15
- package/src/lib/validators/postalAddressObject.js +87 -63
- package/src/lib/validators/range.js +14 -12
- package/src/lib/validators/regex.js +8 -8
- package/src/lib/validators/required.js +16 -16
- package/src/lib/validators/strlen.js +16 -14
- package/src/lib/validators/wordCount.js +22 -14
- package/src/lib/waypoint-url.js +64 -46
- package/src/middleware/body-parser.js +10 -10
- package/src/middleware/csrf.js +1 -1
- package/src/middleware/data.js +28 -24
- package/src/middleware/gather-fields.js +10 -9
- package/src/middleware/i18n.js +35 -37
- package/src/middleware/post.js +41 -21
- package/src/middleware/pre.js +62 -40
- package/src/middleware/progress-journey.js +32 -18
- package/src/middleware/sanitise-fields.js +43 -20
- package/src/middleware/serve-first-waypoint.js +14 -12
- package/src/middleware/session.js +74 -61
- package/src/middleware/skip-waypoint.js +7 -9
- package/src/middleware/steer-journey.js +40 -28
- package/src/middleware/strip-proxy-path.js +8 -7
- package/src/middleware/validate-fields.js +5 -12
- package/src/routes/ancillary.js +5 -7
- package/src/routes/journey.js +159 -85
- package/src/routes/static.js +62 -30
- package/views/casa/components/character-count/README.md +2 -2
- package/views/casa/components/checkboxes/README.md +6 -6
- package/views/casa/components/date-input/README.md +7 -7
- package/views/casa/components/input/README.md +2 -2
- package/views/casa/components/journey-form/README.md +33 -14
- package/views/casa/components/postal-address-object/README.md +4 -4
- package/views/casa/components/radios/README.md +6 -6
- package/views/casa/components/select/README.md +6 -6
- package/views/casa/components/textarea/README.md +2 -2
- package/views/casa/partials/scripts.njk +5 -3
- package/views/casa/partials/styles.njk +1 -4
- package/dist/assets/css/casa-ie8.css +0 -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
|
|
10
|
-
import ValidationError from
|
|
11
|
-
import logger from
|
|
12
|
-
import { notProto } from
|
|
13
|
-
import { uuid as uuidGenerator } from
|
|
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(
|
|
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 (
|
|
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 =
|
|
78
|
+
static DEFAULT_CONTEXT_ID = "default";
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
*/
|
|
83
|
-
static ID_GENERATOR_REQ_LOG = Symbol('generatedContextIds');
|
|
80
|
+
/** @type {symbol} */
|
|
81
|
+
static ID_GENERATOR_REQ_LOG = Symbol("generatedContextIds");
|
|
84
82
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
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
|
-
*
|
|
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
|
|
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) =>
|
|
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
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
461
|
-
|
|
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 =
|
|
467
|
-
|
|
468
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
553
|
-
log.trace(
|
|
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 !==
|
|
579
|
-
throw new TypeError(
|
|
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(
|
|
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
|
|
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,8 +613,19 @@ 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
|
-
|
|
603
|
-
|
|
616
|
+
throw new Error("Missing required request object.");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Define a default context ID generator if required
|
|
620
|
+
if (!Object.hasOwn(req, JourneyContext.ID_GENERATOR_REQ_KEY)) {
|
|
621
|
+
log.warn(
|
|
622
|
+
"A context ID generator is not present in the request. Reverting to uuid().",
|
|
623
|
+
);
|
|
624
|
+
Object.defineProperty(req, JourneyContext.ID_GENERATOR_REQ_KEY, {
|
|
625
|
+
value: uuid,
|
|
626
|
+
enumerable: false,
|
|
627
|
+
writable: false,
|
|
628
|
+
});
|
|
604
629
|
}
|
|
605
630
|
|
|
606
631
|
// Collate a list of context IDs already in use, either from existing
|
|
@@ -611,14 +636,18 @@ export default class JourneyContext {
|
|
|
611
636
|
.map((c) => c.identity.id)
|
|
612
637
|
.filter((id) => id !== JourneyContext.DEFAULT_CONTEXT_ID);
|
|
613
638
|
const inRequestIds = req[JourneyContext.ID_GENERATOR_REQ_LOG] ?? [];
|
|
614
|
-
const reservedIds = Array.from(
|
|
639
|
+
const reservedIds = Array.from(
|
|
640
|
+
new Set([...inSessionIds, ...inRequestIds]).values(),
|
|
641
|
+
);
|
|
615
642
|
|
|
616
643
|
// Generate and log the ID
|
|
617
644
|
const id = JourneyContext.validateContextId(
|
|
618
645
|
req[JourneyContext.ID_GENERATOR_REQ_KEY].call(null, { req, reservedIds }),
|
|
619
646
|
);
|
|
620
647
|
if (reservedIds.includes(id)) {
|
|
621
|
-
throw new Error(
|
|
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
|
+
);
|
|
622
651
|
}
|
|
623
652
|
|
|
624
653
|
if (!req[JourneyContext.ID_GENERATOR_REQ_LOG]) {
|
|
@@ -641,7 +670,10 @@ export default class JourneyContext {
|
|
|
641
670
|
* @returns {JourneyContext} The default Journey Context
|
|
642
671
|
*/
|
|
643
672
|
static getDefaultContext(session) {
|
|
644
|
-
return JourneyContext.getContextById(
|
|
673
|
+
return JourneyContext.getContextById(
|
|
674
|
+
session,
|
|
675
|
+
JourneyContext.DEFAULT_CONTEXT_ID,
|
|
676
|
+
);
|
|
645
677
|
}
|
|
646
678
|
|
|
647
679
|
/**
|
|
@@ -672,9 +704,7 @@ export default class JourneyContext {
|
|
|
672
704
|
static getContextByName(session, name) {
|
|
673
705
|
if (session) {
|
|
674
706
|
const list = new Map(session?.journeyContextList);
|
|
675
|
-
const context = [...list.values()].find(
|
|
676
|
-
(c) => (c.identity.name === name),
|
|
677
|
-
);
|
|
707
|
+
const context = [...list.values()].find((c) => c.identity.name === name);
|
|
678
708
|
if (context) {
|
|
679
709
|
return JourneyContext.fromObject(context);
|
|
680
710
|
}
|
|
@@ -688,14 +718,14 @@ export default class JourneyContext {
|
|
|
688
718
|
*
|
|
689
719
|
* @param {object} session Request session
|
|
690
720
|
* @param {string} tag Context tag
|
|
691
|
-
* @returns {
|
|
721
|
+
* @returns {JourneyContext[]} The discovered JourneyContext instance
|
|
692
722
|
*/
|
|
693
723
|
static getContextsByTag(session, tag) {
|
|
694
724
|
if (session) {
|
|
695
725
|
const list = new Map(session?.journeyContextList);
|
|
696
|
-
return [...list.values()]
|
|
697
|
-
(c) =>
|
|
698
|
-
|
|
726
|
+
return [...list.values()]
|
|
727
|
+
.filter((c) => c.identity.tags?.includes(tag))
|
|
728
|
+
.map((c) => JourneyContext.fromObject(c));
|
|
699
729
|
}
|
|
700
730
|
|
|
701
731
|
return undefined;
|
|
@@ -708,10 +738,10 @@ export default class JourneyContext {
|
|
|
708
738
|
* @returns {Array} Array of contexts
|
|
709
739
|
*/
|
|
710
740
|
static getContexts(session) {
|
|
711
|
-
if (has(session,
|
|
712
|
-
return session.journeyContextList.map(([, contextObj]) =>
|
|
713
|
-
JourneyContext.fromObject(contextObj)
|
|
714
|
-
)
|
|
741
|
+
if (has(session, "journeyContextList")) {
|
|
742
|
+
return session.journeyContextList.map(([, contextObj]) =>
|
|
743
|
+
JourneyContext.fromObject(contextObj),
|
|
744
|
+
);
|
|
715
745
|
}
|
|
716
746
|
|
|
717
747
|
return [];
|
|
@@ -723,21 +753,22 @@ export default class JourneyContext {
|
|
|
723
753
|
* @param {object} session Request session
|
|
724
754
|
* @param {JourneyContext} context Context
|
|
725
755
|
* @param {object} options Options
|
|
726
|
-
* @param {ContextEventUserInfo|object} [options.userInfo] Pass-through
|
|
756
|
+
* @param {ContextEventUserInfo | object} [options.userInfo] Pass-through
|
|
757
|
+
* event info
|
|
727
758
|
* @returns {void}
|
|
728
759
|
* @throws {TypeError} When session is not a valid type, or context has no ID
|
|
729
760
|
*/
|
|
730
761
|
static putContext(session, context, options = {}) {
|
|
731
762
|
if (!isObject(session)) {
|
|
732
|
-
throw new TypeError(
|
|
763
|
+
throw new TypeError("Session must be an object");
|
|
733
764
|
} else if (!(context instanceof JourneyContext)) {
|
|
734
|
-
throw new TypeError(
|
|
765
|
+
throw new TypeError("Context must be a valid JourneyContext");
|
|
735
766
|
} else if (context.identity.id === undefined) {
|
|
736
|
-
throw new TypeError(
|
|
767
|
+
throw new TypeError("Context must have an ID before storing in session");
|
|
737
768
|
}
|
|
738
769
|
|
|
739
770
|
// Initialise the session if necessary
|
|
740
|
-
if (!has(session,
|
|
771
|
+
if (!has(session, "journeyContextList")) {
|
|
741
772
|
JourneyContext.initContextStore(session);
|
|
742
773
|
}
|
|
743
774
|
|
|
@@ -745,13 +776,13 @@ export default class JourneyContext {
|
|
|
745
776
|
const { userInfo = undefined } = options;
|
|
746
777
|
|
|
747
778
|
context.applyEventListeners({
|
|
748
|
-
event:
|
|
779
|
+
event: "waypoint-change",
|
|
749
780
|
session,
|
|
750
781
|
userInfo,
|
|
751
782
|
});
|
|
752
783
|
|
|
753
784
|
context.applyEventListeners({
|
|
754
|
-
event:
|
|
785
|
+
event: "context-change",
|
|
755
786
|
session,
|
|
756
787
|
userInfo,
|
|
757
788
|
});
|
|
@@ -783,7 +814,9 @@ export default class JourneyContext {
|
|
|
783
814
|
* @returns {void}
|
|
784
815
|
*/
|
|
785
816
|
static removeContextById(session, id) {
|
|
786
|
-
const index = (session?.journeyContextList ?? []).findIndex(
|
|
817
|
+
const index = (session?.journeyContextList ?? []).findIndex(
|
|
818
|
+
([contextId]) => contextId === id,
|
|
819
|
+
);
|
|
787
820
|
if (index > -1) {
|
|
788
821
|
session.journeyContextList.splice(index, 1);
|
|
789
822
|
}
|
|
@@ -811,8 +844,8 @@ export default class JourneyContext {
|
|
|
811
844
|
* @returns {void}
|
|
812
845
|
*/
|
|
813
846
|
static removeContextsByTag(session, tag) {
|
|
814
|
-
JourneyContext.getContextsByTag(session, tag).forEach(
|
|
815
|
-
|
|
847
|
+
JourneyContext.getContextsByTag(session, tag).forEach((c) =>
|
|
848
|
+
JourneyContext.removeContext(session, c),
|
|
816
849
|
);
|
|
817
850
|
}
|
|
818
851
|
|
|
@@ -823,15 +856,17 @@ export default class JourneyContext {
|
|
|
823
856
|
* @returns {void}
|
|
824
857
|
*/
|
|
825
858
|
static removeContexts(session) {
|
|
826
|
-
JourneyContext.getContexts(session).forEach((c) =>
|
|
859
|
+
JourneyContext.getContexts(session).forEach((c) =>
|
|
860
|
+
JourneyContext.removeContext(session, c),
|
|
861
|
+
);
|
|
827
862
|
}
|
|
828
863
|
|
|
829
864
|
/**
|
|
830
865
|
* Extract the Journey Context referred to in the incoming request.
|
|
831
866
|
*
|
|
832
|
-
* This will look in `req.params`, `req.query` and
|
|
833
|
-
* `
|
|
834
|
-
*
|
|
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.
|
|
835
870
|
*
|
|
836
871
|
* @param {ExpressRequest} req ExpressJS incoming request
|
|
837
872
|
* @returns {JourneyContext} The Journey Context
|
|
@@ -840,17 +875,19 @@ export default class JourneyContext {
|
|
|
840
875
|
JourneyContext.initContextStore(req.session);
|
|
841
876
|
|
|
842
877
|
let contextId;
|
|
843
|
-
if (has(req?.params,
|
|
844
|
-
log.trace(
|
|
878
|
+
if (has(req?.params, "contextid")) {
|
|
879
|
+
log.trace("Context ID found in req.params.contextid");
|
|
845
880
|
contextId = String(req.params.contextid);
|
|
846
|
-
} else if (has(req.query,
|
|
847
|
-
log.trace(
|
|
881
|
+
} else if (has(req.query, "contextid")) {
|
|
882
|
+
log.trace("Context ID found in req.query.contextid");
|
|
848
883
|
contextId = String(req.query.contextid);
|
|
849
|
-
} else if (has(req?.body,
|
|
850
|
-
log.trace(
|
|
884
|
+
} else if (has(req?.body, "contextid")) {
|
|
885
|
+
log.trace("Context ID found in req.body.contextid");
|
|
851
886
|
contextId = String(req.body.contextid);
|
|
852
887
|
} else {
|
|
853
|
-
log.trace(
|
|
888
|
+
log.trace(
|
|
889
|
+
"Context ID not specified or not found; will attempt to use default",
|
|
890
|
+
);
|
|
854
891
|
contextId = JourneyContext.DEFAULT_CONTEXT_ID;
|
|
855
892
|
}
|
|
856
893
|
|
|
@@ -858,13 +895,16 @@ export default class JourneyContext {
|
|
|
858
895
|
contextId = JourneyContext.validateContextId(contextId);
|
|
859
896
|
const context = JourneyContext.getContextById(req.session, contextId);
|
|
860
897
|
if (!context) {
|
|
861
|
-
throw
|
|
898
|
+
throw new Error(`Could not find a context with id, ${contextId}`);
|
|
862
899
|
}
|
|
863
900
|
return context;
|
|
864
901
|
} catch (err) {
|
|
865
902
|
log.debug(err.message);
|
|
866
|
-
log.trace(
|
|
867
|
-
return JourneyContext.getContextById(
|
|
903
|
+
log.trace("Falling back to default context");
|
|
904
|
+
return JourneyContext.getContextById(
|
|
905
|
+
req.session,
|
|
906
|
+
JourneyContext.DEFAULT_CONTEXT_ID,
|
|
907
|
+
);
|
|
868
908
|
}
|
|
869
909
|
}
|
|
870
910
|
}
|