@dwp/govuk-casa 8.7.9 → 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.
- package/README.md +0 -5
- package/dist/casa.d.ts +1 -1
- package/dist/casa.js +3 -2
- package/dist/casa.js.map +1 -0
- package/dist/lib/CasaTemplateLoader.d.ts +1 -7
- package/dist/lib/CasaTemplateLoader.js +6 -1
- package/dist/lib/CasaTemplateLoader.js.map +1 -0
- package/dist/lib/JourneyContext.d.ts +1 -1
- package/dist/lib/JourneyContext.js +2 -1
- package/dist/lib/JourneyContext.js.map +1 -0
- package/dist/lib/MutableRouter.js +1 -0
- package/dist/lib/MutableRouter.js.map +1 -0
- package/dist/lib/Plan.d.ts +2 -0
- package/dist/lib/Plan.js +9 -4
- package/dist/lib/Plan.js.map +1 -0
- package/dist/lib/ValidationError.js +2 -1
- package/dist/lib/ValidationError.js.map +1 -0
- package/dist/lib/ValidatorFactory.d.ts +2 -2
- package/dist/lib/ValidatorFactory.js +3 -2
- package/dist/lib/ValidatorFactory.js.map +1 -0
- package/dist/lib/configuration-ingestor.d.ts +1 -1
- package/dist/lib/configuration-ingestor.js +2 -1
- package/dist/lib/configuration-ingestor.js.map +1 -0
- package/dist/lib/configure.js +2 -1
- package/dist/lib/configure.js.map +1 -0
- package/dist/lib/end-session.js +1 -0
- package/dist/lib/end-session.js.map +1 -0
- package/dist/lib/field.js +1 -0
- package/dist/lib/field.js.map +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/mount.js +3 -2
- package/dist/lib/mount.js.map +1 -0
- package/dist/lib/nunjucks-filters.js +28 -1
- package/dist/lib/nunjucks-filters.js.map +1 -0
- package/dist/lib/nunjucks.js +1 -0
- package/dist/lib/nunjucks.js.map +1 -0
- package/dist/lib/utils.d.ts +46 -28
- package/dist/lib/utils.js +105 -67
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/validators/dateObject.js +5 -4
- package/dist/lib/validators/dateObject.js.map +1 -0
- package/dist/lib/validators/email.js +1 -0
- package/dist/lib/validators/email.js.map +1 -0
- package/dist/lib/validators/inArray.js +1 -0
- package/dist/lib/validators/inArray.js.map +1 -0
- package/dist/lib/validators/index.js +1 -0
- package/dist/lib/validators/index.js.map +1 -0
- package/dist/lib/validators/nino.js +1 -0
- package/dist/lib/validators/nino.js.map +1 -0
- package/dist/lib/validators/postalAddressObject.d.ts +2 -2
- package/dist/lib/validators/postalAddressObject.js +2 -1
- package/dist/lib/validators/postalAddressObject.js.map +1 -0
- package/dist/lib/validators/regex.js +1 -0
- package/dist/lib/validators/regex.js.map +1 -0
- package/dist/lib/validators/required.d.ts +1 -1
- package/dist/lib/validators/required.js +2 -1
- package/dist/lib/validators/required.js.map +1 -0
- package/dist/lib/validators/strlen.js +1 -0
- package/dist/lib/validators/strlen.js.map +1 -0
- package/dist/lib/validators/wordCount.js +1 -0
- package/dist/lib/validators/wordCount.js.map +1 -0
- package/dist/lib/waypoint-url.js +1 -0
- package/dist/lib/waypoint-url.js.map +1 -0
- package/dist/middleware/body-parser.js +1 -0
- package/dist/middleware/body-parser.js.map +1 -0
- package/dist/middleware/csrf.js +1 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/data.js +1 -0
- package/dist/middleware/data.js.map +1 -0
- package/dist/middleware/gather-fields.js +2 -1
- package/dist/middleware/gather-fields.js.map +1 -0
- package/dist/middleware/i18n.js +1 -0
- package/dist/middleware/i18n.js.map +1 -0
- package/dist/middleware/post.js +1 -0
- package/dist/middleware/post.js.map +1 -0
- package/dist/middleware/pre.js +1 -0
- package/dist/middleware/pre.js.map +1 -0
- package/dist/middleware/progress-journey.js +1 -0
- package/dist/middleware/progress-journey.js.map +1 -0
- package/dist/middleware/sanitise-fields.js +1 -0
- package/dist/middleware/sanitise-fields.js.map +1 -0
- package/dist/middleware/serve-first-waypoint.js +1 -0
- package/dist/middleware/serve-first-waypoint.js.map +1 -0
- package/dist/middleware/session.js +1 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/skip-waypoint.js +1 -0
- package/dist/middleware/skip-waypoint.js.map +1 -0
- package/dist/middleware/steer-journey.js +1 -0
- package/dist/middleware/steer-journey.js.map +1 -0
- package/dist/middleware/strip-proxy-path.js +2 -1
- package/dist/middleware/strip-proxy-path.js.map +1 -0
- package/dist/middleware/validate-fields.js +2 -1
- package/dist/middleware/validate-fields.js.map +1 -0
- package/dist/mjs/esm-wrapper.js +10 -15
- package/dist/routes/ancillary.js +1 -0
- package/dist/routes/ancillary.js.map +1 -0
- package/dist/routes/journey.js +1 -0
- package/dist/routes/journey.js.map +1 -0
- package/dist/routes/static.js +1 -0
- package/dist/routes/static.js.map +1 -0
- package/locales/cy/error.json +1 -1
- package/locales/en/error.json +1 -1
- package/package.json +20 -19
- package/src/casa.js +320 -0
- package/src/lib/CasaTemplateLoader.js +104 -0
- package/src/lib/JourneyContext.js +783 -0
- package/src/lib/MutableRouter.js +310 -0
- package/src/lib/Plan.js +624 -0
- package/src/lib/ValidationError.js +163 -0
- package/src/lib/ValidatorFactory.js +105 -0
- package/src/lib/configuration-ingestor.js +457 -0
- package/src/lib/configure.js +202 -0
- package/src/lib/dirname.cjs +1 -0
- package/src/lib/end-session.js +45 -0
- package/src/lib/field.js +456 -0
- package/src/lib/index.js +33 -0
- package/src/lib/logger.js +16 -0
- package/src/lib/mount.js +127 -0
- package/src/lib/nunjucks-filters.js +150 -0
- package/src/lib/nunjucks.js +53 -0
- package/src/lib/utils.js +232 -0
- package/src/lib/validators/dateObject.js +169 -0
- package/src/lib/validators/email.js +55 -0
- package/src/lib/validators/inArray.js +81 -0
- package/src/lib/validators/index.js +24 -0
- package/src/lib/validators/nino.js +57 -0
- package/src/lib/validators/postalAddressObject.js +162 -0
- package/src/lib/validators/regex.js +48 -0
- package/src/lib/validators/required.js +74 -0
- package/src/lib/validators/strlen.js +66 -0
- package/src/lib/validators/wordCount.js +70 -0
- package/src/lib/waypoint-url.js +93 -0
- package/src/middleware/body-parser.js +31 -0
- package/src/middleware/csrf.js +29 -0
- package/src/middleware/data.js +105 -0
- package/src/middleware/dirname.cjs +1 -0
- package/src/middleware/gather-fields.js +51 -0
- package/src/middleware/i18n.js +106 -0
- package/src/middleware/post.js +61 -0
- package/src/middleware/pre.js +91 -0
- package/src/middleware/progress-journey.js +92 -0
- package/src/middleware/sanitise-fields.js +58 -0
- package/src/middleware/serve-first-waypoint.js +28 -0
- package/src/middleware/session.js +129 -0
- package/src/middleware/skip-waypoint.js +46 -0
- package/src/middleware/steer-journey.js +78 -0
- package/src/middleware/strip-proxy-path.js +56 -0
- package/src/middleware/validate-fields.js +84 -0
- package/src/routes/ancillary.js +29 -0
- package/src/routes/dirname.cjs +1 -0
- package/src/routes/journey.js +212 -0
- package/src/routes/static.js +77 -0
- package/views/casa/components/character-count/README.md +10 -0
- package/views/casa/components/character-count/template.njk +6 -2
- package/views/casa/components/checkboxes/README.md +45 -37
- package/views/casa/components/checkboxes/template.njk +8 -7
- package/views/casa/components/date-input/README.md +15 -3
- package/views/casa/components/date-input/template.njk +6 -4
- package/views/casa/components/input/README.md +9 -0
- package/views/casa/components/input/template.njk +6 -2
- package/views/casa/components/journey-form/README.md +3 -2
- package/views/casa/components/postal-address-object/README.md +10 -0
- package/views/casa/components/postal-address-object/template.njk +20 -5
- package/views/casa/components/radios/README.md +51 -26
- package/views/casa/components/radios/template.njk +6 -3
- package/views/casa/components/select/README.md +65 -0
- package/views/casa/components/select/macro.njk +3 -0
- package/views/casa/components/select/template.njk +49 -0
- package/views/casa/components/textarea/README.md +9 -0
- package/views/casa/components/textarea/template.njk +6 -2
- package/views/casa/layouts/journey.njk +1 -1
- package/views/casa/layouts/main.njk +1 -1
package/src/lib/Plan.js
ADDED
|
@@ -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
|
+
}
|