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