@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,624 @@
1
+ import { Graph } from '@dagrejs/graphlib';
2
+ import JourneyContext from './JourneyContext.js';
3
+ import logger from './logger.js';
4
+
5
+ const log = logger('lib:plan');
6
+
7
+ /**
8
+ * @access private
9
+ * @typedef {import('../casa').PlanRoute} PlanRoute
10
+ */
11
+
12
+ /**
13
+ * @access private
14
+ * @typedef {import('../casa').PlanRouteCondition} PlanRouteCondition
15
+ */
16
+
17
+ /**
18
+ * @access private
19
+ * @typedef {import('../casa').PlanTraverseOptions} PlanTraverseOptions
20
+ */
21
+
22
+ /**
23
+ * @access private
24
+ * @typedef {import('../casa').PlanArbiter} PlanArbiter
25
+ */
26
+
27
+ /**
28
+ * @access private
29
+ * @typedef {import('@dagrejs/graphlib').Graph} Graph
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} PlanConstructorOptions
34
+ * @property {boolean} [validateBeforeRouteCondition=true] Check page validity before conditions
35
+ * @property {string|PlanArbiter} [arbiter=undefined] Arbitration mechanism
36
+ */
37
+
38
+ /**
39
+ * Will check if the source waypoint has specifically passed validation, i.e
40
+ * there is a "null" validation entry for the route source.
41
+ *
42
+ * @access private
43
+ * @type {PlanRouteCondition}
44
+ */
45
+ function defaultNextFollow(r, context) {
46
+ const { validation: v = {} } = context.toObject();
47
+ return Object.prototype.hasOwnProperty.call(v, r.source) && v[r.source] === null;
48
+ }
49
+
50
+ /**
51
+ * Will check if the target waypoint (the one we're moving back to) has
52
+ * specifically passed validation.
53
+ *
54
+ * @access private
55
+ * @type {PlanRouteCondition}
56
+ */
57
+ function defaultPrevFollow(r, context) {
58
+ const { validation: v = {} } = context.toObject();
59
+ return Object.prototype.hasOwnProperty.call(v, r.target) && v[r.target] === null;
60
+ }
61
+
62
+ /**
63
+ * Validate a given waypoint ID.
64
+ *
65
+ * @access private
66
+ * @param {string} val Waypoint ID
67
+ * @returns {void}
68
+ * @throws {TypeError} If waypoint ID is not a string
69
+ * @throws {SyntaxError} If waypoint ID is incorrectly formatted
70
+ */
71
+ function validateWaypointId(val) {
72
+ if (typeof val !== 'string') {
73
+ throw new TypeError(`Expected waypoint id to be a string, got ${typeof val}`);
74
+ }
75
+ if (val.substr(0, 6) === 'url://' && !val.endsWith('/')) {
76
+ throw new SyntaxError('url:// waypoints must include a trailing /')
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Validate a given route name.
82
+ *
83
+ * @access private
84
+ * @param {string} val Route name
85
+ * @returns {string} The route name
86
+ * @throws {TypeError} If route name is not a string
87
+ * @throws {ReferenceError} If route name is neither "next" or "prev"
88
+ */
89
+ function validateRouteName(val) {
90
+ if (typeof val !== 'string') {
91
+ throw new TypeError(`Expected route name to be a string, got ${typeof val}`);
92
+ } else if (!['next', 'prev'].includes(val)) {
93
+ throw new ReferenceError(`Expected route name to be one of next or prev. Got ${val}`)
94
+ }
95
+ return val;
96
+ }
97
+
98
+ /**
99
+ * Validate a given route condition.
100
+ *
101
+ * @access private
102
+ * @param {PlanRouteCondition} val The condition function
103
+ * @returns {void}
104
+ * @throws {TypeError} If condition is not a string
105
+ */
106
+ function validateRouteCondition(val) {
107
+ if (!(val instanceof Function)) {
108
+ throw new TypeError(`Expected route condition to be a function, got ${typeof val}`);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Creates a user friendly route structure from a given graph edge which will
114
+ * be used in userland. This is the object that will be passed into follow
115
+ * functions too as the "route" parameter.
116
+ *
117
+ * @access private
118
+ * @param {object} dgraph Directed graph instance.
119
+ * @param {object} edge Graph edge object.
120
+ * @returns {PlanRoute} Route.
121
+ */
122
+ const makeRouteObject = (dgraph, edge) => {
123
+ const label = dgraph.edge(edge) || {};
124
+ return {
125
+ source: edge.v,
126
+ target: edge.w,
127
+ name: edge.name,
128
+ // label: {},
129
+ label,
130
+ };
131
+ };
132
+
133
+ /**
134
+ * Exit nodes begin with a protocol format, such as `url://`, `http://`, etc
135
+ *
136
+ * @access private
137
+ */
138
+ const reExitNodeProtocol = /^[a-z]+:\/\//i;
139
+
140
+ /**
141
+ * For storing private properties for each instance
142
+ *
143
+ * @access private
144
+ * @todo Use property private class properties.
145
+ */
146
+ const priv = new WeakMap();
147
+
148
+ /**
149
+ * @memberof module:@dwp/govuk-casa
150
+ */
151
+ export default class Plan {
152
+ /**
153
+ * @type {string[]} These waypoints can be skipped
154
+ */
155
+ #skippableWaypoints;
156
+
157
+ /**
158
+ * Waypoints using the url:// protocol are known as "exit nodes" as they
159
+ * indicate an exit point to another Plan.
160
+ *
161
+ * @param {string} name Waypoint name
162
+ * @returns {boolean} True if the waypoint is a url:// type
163
+ */
164
+ static isExitNode(name) {
165
+ return reExitNodeProtocol.test(name);
166
+ }
167
+
168
+ /**
169
+ * Create a Plan.
170
+ *
171
+ * @param {PlanConstructorOptions} opts Options
172
+ */
173
+ constructor(opts = {}) {
174
+ // This is our directed, multigraph representation
175
+ const dgraph = new Graph({
176
+ directed: true,
177
+ multigraph: true,
178
+ compound: false,
179
+ });
180
+
181
+ // Gather options
182
+ const options = Object.assign(Object.create(null), {
183
+ // When true, the validation state of the source node must be `null` (i.e.
184
+ // no validation errors) before any custom route conditions are evaluated.
185
+ validateBeforeRouteCondition: true,
186
+
187
+ // Traversal arbitration
188
+ arbiter: undefined,
189
+ }, opts);
190
+ Object.freeze(options);
191
+
192
+ priv.set(this, {
193
+ dgraph,
194
+ follows: {
195
+ next: {},
196
+ prev: {},
197
+ },
198
+ options,
199
+ });
200
+
201
+ this.#skippableWaypoints = [];
202
+ }
203
+
204
+ /**
205
+ * Retrieve the options set on this Plan.
206
+ *
207
+ * @returns {PlanConstructorOptions} Options map
208
+ */
209
+ getOptions() {
210
+ return priv.get(this).options;
211
+ }
212
+
213
+ /**
214
+ * Retrieve the list of skippable waypoints.
215
+ *
216
+ * @returns {string[]} List of skippable waypoints
217
+ */
218
+ getSkippables() {
219
+ return this.#skippableWaypoints;
220
+ }
221
+
222
+ /**
223
+ * Add one or more skippable waypoints.
224
+ *
225
+ * @param {...string} waypoints Waypoints
226
+ * @returns {Plan} Chain
227
+ */
228
+ addSkippables(...waypoints) {
229
+ this.#skippableWaypoints = [...this.#skippableWaypoints, ...waypoints];
230
+ return this;
231
+ }
232
+
233
+ /**
234
+ * Check if the user can skip the named waypoint.
235
+ *
236
+ * @param {string} waypoint Waypoint
237
+ * @returns {boolean} True if waypoint can be skipped
238
+ */
239
+ isSkippable(waypoint) {
240
+ return this.#skippableWaypoints.indexOf(waypoint) > -1;
241
+ }
242
+
243
+ /**
244
+ * Retrieve all waypoints in this Plan (order is arbitrary).
245
+ *
246
+ * @returns {string[]} List of waypoints
247
+ */
248
+ getWaypoints() {
249
+ return priv.get(this).dgraph.nodes();
250
+ }
251
+
252
+ /**
253
+ * Determine if the given waypoint exists in this Plan.
254
+ *
255
+ * @param {string} waypoint Waypoint to search for
256
+ * @returns {boolean} Result
257
+ */
258
+ containsWaypoint(waypoint) {
259
+ return this.getWaypoints().includes(waypoint);
260
+ }
261
+
262
+ /**
263
+ * Get all route information.
264
+ *
265
+ * @returns {PlanRoute[]} Routes
266
+ */
267
+ getRoutes() {
268
+ const self = priv.get(this);
269
+ return self.dgraph.edges().map((edge) => makeRouteObject(self.dgraph, edge));
270
+ }
271
+
272
+ /**
273
+ * Get the condition function for the given parameters.
274
+ *
275
+ * @param {string} src Source waypoint
276
+ * @param {string} tgt Target waypoint
277
+ * @param {string} name Route name
278
+ * @returns {PlanRouteCondition} Route condition function
279
+ */
280
+ getRouteCondition(src, tgt, name) {
281
+ return priv.get(this).follows[validateRouteName(name)][`${src}/${tgt}`];
282
+ }
283
+
284
+ /**
285
+ * Return all outward routes (out-edges) from the given waypoint, to the
286
+ * optional target waypoint.
287
+ *
288
+ * @param {string} src Source waypoint.
289
+ * @param {string} [tgt] Target waypoint.
290
+ * @returns {PlanRoute[]} Route objects found.
291
+ */
292
+ getOutwardRoutes(src, tgt = null) {
293
+ const self = priv.get(this);
294
+ return self.dgraph.outEdges(src, tgt).map((e) => makeRouteObject(self.dgraph, e));
295
+ }
296
+
297
+ /**
298
+ * Return all outward routes (out-edges) from the given waypoint, to the
299
+ * optional target waypoint, matching the "prev" name.
300
+ *
301
+ * @param {string} src Source waypoint.
302
+ * @param {string} [tgt] Target waypoint.
303
+ * @returns {PlanRoute[]} Route objects found.
304
+ */
305
+ getPrevOutwardRoutes(src, tgt = null) {
306
+ return this.getOutwardRoutes(src, tgt).filter((r) => r.name === 'prev');
307
+ }
308
+
309
+ /**
310
+ * Add a sequence of waypoints that will follow on from each other, with no
311
+ * routing logic between them.
312
+ *
313
+ * @param {...string} waypoints Waypoints to add
314
+ * @returns {void}
315
+ */
316
+ addSequence(...waypoints) {
317
+ // Setup simple double routes (next/prev) between all waypoints in this list
318
+ for (let i = 0, l = waypoints.length - 1; i < l; i += 1) {
319
+ // ESLint disabled as `i` is an integer
320
+ /* eslint-disable-next-line security/detect-object-injection */
321
+ this.setRoute(waypoints[i], waypoints[i + 1]);
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Create a new directed route between two waypoints, labelled as "next".
327
+ *
328
+ * @param {string} src Source waypoint
329
+ * @param {string} tgt Target waypoint
330
+ * @param {PlanRouteCondition} follow Route condition function
331
+ * @returns {Plan} Chain
332
+ */
333
+ setNextRoute(src, tgt, follow) {
334
+ return this.setNamedRoute(src, tgt, 'next', follow);
335
+ }
336
+
337
+ /**
338
+ * Create a new directed route between two waypoints, labelled as "prev".
339
+ *
340
+ * @param {string} src Source waypoint
341
+ * @param {string} tgt Target waypoint
342
+ * @param {PlanRouteCondition} follow Route condition function
343
+ * @returns {Plan} Chain
344
+ */
345
+ setPrevRoute(src, tgt, follow) {
346
+ return this.setNamedRoute(src, tgt, 'prev', follow);
347
+ }
348
+
349
+ /**
350
+ * Adds both a "next" and "prev" route between the two waypoints.
351
+ *
352
+ * By default, the "prev" route will use the same "follow" condition as the
353
+ * "next" route. This makes sense in that in order to get to the target, the
354
+ * condition must be true, and so to reverse the direction we also need that
355
+ * same condition to be true.
356
+ *
357
+ * However, if the condition function uses the `source`/`target` property
358
+ * of the route in some way, then we must reverse these before passing to the
359
+ * condition on the "prev" route because `source` in the condition will almost
360
+ * certainly be referring to the source of the "next" route.
361
+ *
362
+ * If `tgt` is an egress node, do not create a `prev` route for it, because
363
+ * there's no way back from that point to this Plan.
364
+ *
365
+ * @param {string} src Source waypoint.
366
+ * @param {string} tgt Target waypoint.
367
+ * @param {PlanRouteCondition} [followNext] Follow test function.
368
+ * @param {PlanRouteCondition} [followPrev] Follow test function.
369
+ * @returns {Plan} Chain
370
+ */
371
+ setRoute(src, tgt, followNext = undefined, followPrev = undefined) {
372
+ this.setNamedRoute(src, tgt, 'next', followNext);
373
+
374
+ let followPrevious = followPrev;
375
+ if (followPrevious === undefined) {
376
+ followPrevious = followNext === undefined ? undefined : (r, c) => {
377
+ const invertedRoute = {
378
+ ...r,
379
+ source: r.target,
380
+ target: r.source,
381
+ };
382
+ return followNext(invertedRoute, c);
383
+ }
384
+ }
385
+
386
+ this.setNamedRoute(tgt, src, 'prev', followPrevious);
387
+
388
+ return this;
389
+ }
390
+
391
+ /**
392
+ * Create a named route between two waypoints, and give that route a function
393
+ * that determine whether it should be followed during traversal operations.
394
+ * Note that the source waypoint must be in a successful validation state
395
+ * to be considered for traversal, regardless of what the custom function
396
+ * determines.
397
+ *
398
+ * You may also define routes that take the user to any generic URL within the
399
+ * same domain by using the `url://` protocol. These are considered
400
+ * "exit nodes".
401
+ *
402
+ * setNamedRoute("my-waypoint", "url:///some/absolute/url");
403
+ *
404
+ * @param {string} src Source waypoint.
405
+ * @param {string} tgt Target waypoint.
406
+ * @param {string} name Name of the route (must be unique for this waypoint pairing).
407
+ * @param {PlanRouteCondition} follow Test function to determine if route can be followed.
408
+ * @returns {Plan} Chain
409
+ * @throws {Error} If attempting to create a "next" route from an exit node
410
+ */
411
+ setNamedRoute(src, tgt, name, follow) {
412
+ const self = priv.get(this);
413
+
414
+ // Validate
415
+ validateWaypointId(src);
416
+ validateWaypointId(tgt);
417
+ validateRouteName(name);
418
+ if (follow !== undefined) {
419
+ validateRouteCondition(follow);
420
+ }
421
+
422
+ // Get routing function name to label edge
423
+ const conditionName = follow && follow.name;
424
+
425
+ // Warn if we're overwriting an existing edge on the same name
426
+ if (self.dgraph.hasEdge(src, tgt, name)) {
427
+ log.warn('Setting a route that already exists (%s, %s, %s). Will be overridden', src, tgt, name);
428
+ }
429
+ self.dgraph.setEdge(src, tgt, { conditionName }, name);
430
+
431
+ // Determine which follow function to use
432
+ let followFunc;
433
+ if (follow) {
434
+ if (!self.options.validateBeforeRouteCondition) {
435
+ followFunc = follow;
436
+ } else if (name === 'next') {
437
+ // Retain the original function name of route condition
438
+ followFunc = {
439
+ [follow.name]: (r, c) => (defaultNextFollow(r, c) && follow(r, c)),
440
+ }[follow.name];
441
+ } else {
442
+ // Retain the original function name of route condition
443
+ followFunc = {
444
+ [follow.name]: (r, c) => (defaultPrevFollow(r, c) && follow(r, c)),
445
+ }[follow.name];
446
+ }
447
+ } else if (name === 'next') {
448
+ followFunc = defaultNextFollow;
449
+ } else {
450
+ followFunc = defaultPrevFollow;
451
+ }
452
+
453
+ // ESLint disabled as `name` has been validated further above
454
+ /* eslint-disable-next-line security/detect-object-injection */
455
+ self.follows[name][`${src}/${tgt}`] = followFunc;
456
+
457
+ return this;
458
+ }
459
+
460
+ /**
461
+ * This is a convenience method for traversing all "next" routes, and returning
462
+ * the IDs of all waypoints visited along the way.
463
+ *
464
+ * @param {JourneyContext} context Journey Context
465
+ * @param {PlanTraverseOptions} options Options
466
+ * @returns {string[]} List of traversed waypoints
467
+ */
468
+ traverse(context, options = {}) {
469
+ return this.traverseNextRoutes(context, options).map((e) => e.source);
470
+ }
471
+
472
+ /**
473
+ * Traverse the Plan by following all "next" routes, and returning the IDs of
474
+ * all waypoints visited along the way.
475
+ *
476
+ * @param {JourneyContext} context Journey Context
477
+ * @param {PlanTraverseOptions} options Options
478
+ * @returns {PlanRoute[]} List of traversed waypoints
479
+ */
480
+ traverseNextRoutes(context, options = {}) {
481
+ return this.traverseRoutes(context, { ...options, routeName: 'next' })
482
+ }
483
+
484
+ /**
485
+ * Traverse the Plan by following all "prev" routes, and returning the IDs of
486
+ * all waypoints visited along the way.
487
+ *
488
+ * @param {JourneyContext} context Journey Context
489
+ * @param {PlanTraverseOptions} options Options
490
+ * @returns {PlanRoute[]} List of traversed waypoints
491
+ */
492
+ traversePrevRoutes(context, options = {}) {
493
+ return this.traverseRoutes(context, { ...options, routeName: 'prev' })
494
+ }
495
+
496
+ /**
497
+ * Traverse through the plan from a particular starting waypoint. This is a
498
+ * non-exhaustive Graph Exploration.
499
+ *
500
+ * The last route in the list will contain the source of the last waypoint that
501
+ * can be reached, i.e. The waypoint that has no further satisfiable out-edges.
502
+ *
503
+ * If a cyclical set of routes are encountered, traversal will stop after
504
+ * reaching the first repeated waypoint.
505
+ *
506
+ * @param {JourneyContext} context Journey context
507
+ * @param {PlanTraverseOptions} options Options
508
+ * @returns {PlanRoute[]} Routes that were traversed
509
+ * @throws {TypeError} When context is not a JourneyContext
510
+ */
511
+ traverseRoutes(context, options = {}) {
512
+ if (!(context instanceof JourneyContext)) {
513
+ throw new TypeError(`Expected context to be an instance of JourneyContext, got ${typeof context}`);
514
+ }
515
+
516
+ const self = priv.get(this);
517
+
518
+ /** @type {PlanTraverseOptions} */
519
+ const {
520
+ startWaypoint = this.getWaypoints()[0],
521
+ stopCondition = () => (false),
522
+ arbiter = self.options.arbiter,
523
+ routeName,
524
+ } = options;
525
+
526
+ if (!self.dgraph.hasNode(startWaypoint)) {
527
+ throw new ReferenceError(`Plan does not contain waypoint '${startWaypoint}'`);
528
+ }
529
+
530
+ validateRouteName(routeName);
531
+
532
+ const history = new Map();
533
+
534
+ const traverse = (startWP) => {
535
+ let target = self.dgraph.outEdges(startWP).filter((e) => {
536
+ if (e.name !== routeName) {
537
+ return false;
538
+ }
539
+ const route = makeRouteObject(self.dgraph, e);
540
+ try {
541
+ // ESLint disabled as `routeName` has been validated further above
542
+ /* eslint-disable-next-line security/detect-object-injection */
543
+ return self.follows[routeName][`${e.v}/${e.w}`](route, context);
544
+ } catch (ex) {
545
+ log.warn('Route follow function threw an exception, "%s" (%s)', ex.message, `${e.v} -> ${e.w}`);
546
+ return false;
547
+ }
548
+ });
549
+
550
+ // When there's more than one candidate route to take, we need help to choose
551
+ if (target.length > 1) {
552
+ const satisfied = target.map((t) => `${t.v} -> ${t.w}`);
553
+ log.debug(`Multiple routes were satisfied for "${routeName}" from "${startWP}" (${satisfied.join(' / ')}). Deciding how to resolve ...`);
554
+
555
+ if (arbiter === 'auto') {
556
+ log.debug('Using automatic arbitration process');
557
+ const targetNames = target.map(({ w }) => w);
558
+ const forwardTraversal = this.traverseNextRoutes(context, {
559
+ stopCondition: ({ source }) => targetNames.includes(source),
560
+ });
561
+ const resolved = forwardTraversal.pop();
562
+ target = target.filter((t) => t.w === resolved.source);
563
+ } else if (arbiter instanceof Function) {
564
+ log.debug('Using custom arbitration process');
565
+ // Convert to routeObject and back to edge object so that only the
566
+ // routeObject is used in the public API
567
+ target = arbiter({
568
+ targets: target.map((t) => makeRouteObject(self.dgraph, t)),
569
+ journeyContext: context,
570
+ traverseOptions: options,
571
+ });
572
+ target = target.map((r) => ({ v: r.source, w: r.target, name: r.name }));
573
+ } else {
574
+ log.warn('Unable to arbitrate');
575
+ target = [];
576
+ }
577
+ }
578
+
579
+ if (target.length === 1) {
580
+ const route = makeRouteObject(self.dgraph, target[0]);
581
+ const routeHash = `${route.name}/${route.source}/${route.target}`;
582
+
583
+ if (stopCondition(route)) {
584
+ return [route];
585
+ }
586
+ if (!history.has(routeHash)) {
587
+ history.set(routeHash, null);
588
+ const traversed = traverse(target[0].w);
589
+ const totalTrav = traversed.length;
590
+ const results = new Array(totalTrav + 1);
591
+ results[0] = route;
592
+
593
+ for (let i = 0; i < totalTrav; i++) {
594
+ // ESLint disabled as `i` is an integer
595
+ /* eslint-disable-next-line security/detect-object-injection */
596
+ results[i + 1] = traversed[i];
597
+ }
598
+
599
+ return results;
600
+ }
601
+ log.debug('Encountered loop (%s). Stopping traversal.', `${route.source} -> ${route.target}`);
602
+ }
603
+
604
+ return [makeRouteObject(self.dgraph, {
605
+ v: startWP,
606
+ w: null,
607
+ name: routeName,
608
+ label: {},
609
+ })];
610
+ };
611
+
612
+ return traverse(startWaypoint);
613
+ }
614
+
615
+ /**
616
+ * Get raw graph data structure. This can be used with other libraries to
617
+ * generate graph visualisations, for example.
618
+ *
619
+ * @returns {Graph} Graph data structure.
620
+ */
621
+ getGraphStructure() {
622
+ return priv.get(this).dgraph;
623
+ }
624
+ }