@dwp/govuk-casa
Version:
A framework for building GOVUK Collect-And-Submit-Applications
839 lines • 32.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateObjectKey = validateObjectKey;
/**
* Represents the state of a user's journey through the Plan. It contains
* information about:
*
* - Data gathered during the journey
* - Validation errors on that data
* - Navigation information about how the user got where they are.
*/
const lodash_1 = __importDefault(require("lodash"));
const rfdc_1 = __importDefault(require("rfdc"));
const ValidationError_js_1 = __importDefault(require("./ValidationError.js"));
const logger_js_1 = __importDefault(require("./logger.js"));
const utils_js_1 = require("./utils.js");
const context_id_generators_js_1 = require("./context-id-generators.js");
const { isPlainObject, isObject, isEqual } = lodash_1.default; // CommonJS
const log = (0, logger_js_1.default)("lib:journey-context");
const uuid = (0, context_id_generators_js_1.uuid)();
const clone = (0, rfdc_1.default)({ proto: false });
/**
* @typedef {import("../casa").ContextEventUserInfo} ContextEventUserInfo
* @access private
*/
/**
* @typedef {import("../casa").Page} Page
* @access private
*/
/**
* @typedef {import("../casa").ContextEventHandler} ContextEventHandler
* @access private
*/
/**
* @typedef {import("../casa").ContextEvent} ContextEvent
* @access private
*/
/**
* @typedef {import("../casa").JourneyContextObject} JourneyContextObject
* @access private
*/
/**
* @typedef {import("express").Request} ExpressRequest
* @access private
*/
/**
* @param {any} key Object key to validate
* @returns {string} Validated key
*/
function validateObjectKey(key = "") {
const keyLower = String.prototype.toLowerCase.call(key);
if (keyLower === "prototype" ||
keyLower === "__proto__" ||
keyLower === "constructor") {
throw new SyntaxError(`Invalid object key used, ${key}`);
}
return String(key);
}
/** @memberof module:@dwp/govuk-casa */
class JourneyContext {
// Private properties
#data;
#validation;
#nav;
#identity;
#eventListeners;
#eventListenerPreState;
static DEFAULT_CONTEXT_ID = "default";
/** @type {symbol} */
static ID_GENERATOR_REQ_LOG = Symbol("generatedContextIds");
/** @type {symbol} */
static ID_GENERATOR_REQ_KEY = Symbol("generateContextId");
/**
* Constructor.
*
* `data` is the "single source of truth" for all data gathered during the
* user's journey. This is referred to as the "canonical data model".
* Page-specific "views" of this data are generated at runtime in order to
* populate/validate specific form fields.
*
* `validation` holds the results of form field validation carried out when
* page forms are POSTed. These results are mapped directly to per-page,
* per-field.
*
* `nav` holds information about the current navigation state. Currently this
* comprises of the language in which the user is navigating the service.
*
* `identity` holds information that helps uniquely identify this context
* among a group of contexts stored in the session.
*
* @param {Record<string, any>} data Entire journey data.
* @param {object} validation Page errors (indexed by waypoint id).
* @param {object} nav Navigation context.
* @param {object} identity Some metadata for identifying this context among
* others.
*/
constructor(data = {}, validation = {}, nav = {}, identity = {}) {
this.#data = data;
this.#validation = validation;
this.#nav = nav;
this.#identity = identity;
this.#eventListeners = [];
this.#eventListenerPreState = null;
}
/**
* Clone into an object that can be stringified.
*
* @returns {JourneyContextObject} Plain object.
*/
toObject() {
return Object.assign(Object.create(null), {
data: clone(this.#data),
validation: clone(this.#validation),
nav: clone(this.#nav),
identity: clone(this.#identity),
});
}
/**
* Create a new JourneyContext using the plain object.
*
* @param {JourneyContextObject} obj Object.
* @returns {JourneyContext} Instance.
*/
static fromObject({ data = Object.create(null), validation = Object.create(null), nav = Object.create(null), identity = Object.create(null), } = {}) {
// As we're constructing a JourneyContext from a plain JS object, we need to
// ensure any validation errors are instances of ValidationError.
const deserialisedValidation = Object.create(null);
for (const [waypoint, errors] of Object.entries(validation)) {
let dErrors = errors;
if (Array.isArray(errors)) {
dErrors = errors.map((e) => e instanceof ValidationError_js_1.default ? e : new ValidationError_js_1.default(e));
}
deserialisedValidation[(0, utils_js_1.notProto)(waypoint)] = dErrors;
}
return new JourneyContext(data, deserialisedValidation, nav, identity);
}
configureFromObject(object) {
const source = JourneyContext.fromObject(object);
this.#data = source.data;
this.#validation = source.validation;
this.#nav = source.nav;
}
get data() {
return this.#data;
}
set data(value) {
this.#data = value;
}
get validation() {
return this.#validation;
}
get nav() {
return this.#nav;
}
get identity() {
return this.#identity;
}
/**
* Get data context for a specific a specific page.
*
* @param {string | Page} page Page waypoint ID, or Page object.
* @returns {object} Page data.
* @throws {TypeError} When page is invalid.
*/
getDataForPage(page) {
if (typeof page === "string") {
return this.#data[validateObjectKey(page)];
}
if (isPlainObject(page)) {
return this.#data[validateObjectKey(page.waypoint)];
}
throw new TypeError(`Page must be a string or Page object. Got ${typeof page}`);
}
/**
* Get all data.
*
* @returns {object} Page data
*/
getData() {
return this.#data;
}
/**
* Overwrite the data context with a new object.
*
* @param {object} data Data that will overwrite all existing data.
* @returns {JourneyContext} Chain.
*/
setData(data) {
this.#data = data;
return this;
}
/**
* Write field form data from a page HTML form, into the `data` model.
*
* @param {string | Page} page Page waypoint ID, or Page object
* @param {object} webFormData Data to overwrite with
* @returns {JourneyContext} Chain
* @throws {TypeError} When page is invalid.
*/
setDataForPage(page, webFormData) {
if (typeof page === "string") {
this.#data[validateObjectKey(page)] = webFormData;
}
else if (isPlainObject(page)) {
this.#data[validateObjectKey(page.waypoint)] = webFormData;
}
else {
throw new TypeError(`Page must be a string or Page object. Got ${typeof page}`);
}
return this;
}
/**
* Return validation errors for all pages.
*
* @returns {object} All page validation errors.
*/
getValidationErrors() {
return this.#validation;
}
/**
* Removes any validation state for the given page. Clearing validation state
* completely will, by default, prevent onward traversal from this page. See
* the traversal logic in Plan class.
*
* @param {string} pageId Page ID.
* @returns {JourneyContext} Chain.
*/
removeValidationStateForPage(pageId) {
/* eslint-disable-next-line sonarjs/no-unused-vars,no-unused-vars */
const { [pageId]: dummy, ...remaining } = this.#validation;
this.#validation = { ...remaining };
return this;
}
/**
* Clear any validation errors for the given page. This effectively declares
* that this page has been successfully validated, and so can be traversed. If
* you want to remove any knowledge of validation success/failure, use
* `removeValidationStateForPage()` instead.
*
* @param {string} pageId Page ID.
* @returns {JourneyContext} Chain.
*/
clearValidationErrorsForPage(pageId) {
this.#validation[validateObjectKey(pageId)] = null;
return this;
}
/**
* Set validation errors for a page.
*
* @param {string} pageId Page ID.
* @param {ValidationError[]} errors Errors
* @returns {JourneyContext} Chain.
* @throws {SyntaxError} When errors are invalid.
*/
setValidationErrorsForPage(pageId, errors = []) {
if (!Array.isArray(errors)) {
throw new SyntaxError(`Errors must be an Array. Received ${Object.prototype.toString.call(errors)}`);
}
for (const error of errors) {
if (!(error instanceof ValidationError_js_1.default)) {
throw new SyntaxError("Field errors must be a ValidationError");
}
}
this.#validation[validateObjectKey(pageId)] = errors;
return this;
}
/**
* Return the validation errors associated with the page's currently held data
* context (if any).
*
* @param {string} pageId Page ID.
* @returns {ValidationError[]} An array of errors
*/
getValidationErrorsForPage(pageId) {
return this.#validation[validateObjectKey(pageId)] ?? [];
}
/**
* Same as `getValidationErrorsForPage()`, but the return value is an object
* whose keys are the field names, and values are the list of errors
* associated with that particular field.
*
* @param {string} pageId Page ID.
* @returns {object} Object indexed by field names; values containing list of
* errors
*/
getValidationErrorsForPageByField(pageId) {
const errors = this.getValidationErrorsForPage(pageId);
const obj = Object.create(null);
// ESLint disabled as `i` is an integer
/* eslint-disable security/detect-object-injection */
for (let i = 0, l = errors.length; i < l; i++) {
if (!obj[errors[i].field]) {
obj[errors[i].field] = [];
}
obj[errors[i].field].push(errors[i]);
}
/* eslint-enable security/detect-object-injection */
return obj;
}
/**
* Determine whether the specified page has any errors in its validation
* context.
*
* @param {string} pageId Page ID.
* @returns {boolean} Result.
*/
hasValidationErrorsForPage(pageId) {
return this.#validation?.[validateObjectKey(pageId)]?.length > 0;
}
/**
* Set language of the context.
*
* @param {string} language Language to set (ISO 639-1 2-letter code).
* @returns {JourneyContext} Chain.
*/
setNavigationLanguage(language = "en") {
this.#nav.language = language;
return this;
}
/**
* Convenience function to test if page is valid.
*
* @param {string} pageId Page ID.
* @returns {boolean} True if the page is valid.
*/
isPageValid(pageId) {
return this.#validation[validateObjectKey(pageId)] === null;
}
/**
* Remove information about these waypoints.
*
* @param {string[]} waypoints Waypoints to be removed
*/
purge(waypoints = []) {
const newData = Object.create(null);
const newValidation = Object.create(null);
const toKeep = Object.keys(this.#data).filter((w) => !waypoints.includes(w));
// ESLint disabled as `i` is an integer
/* eslint-disable security/detect-object-injection */
for (let i = 0, l = toKeep.length; i < l; i++) {
newData[toKeep[i]] = this.#data[toKeep[i]];
newValidation[toKeep[i]] = this.#validation[toKeep[i]];
}
/* eslint-enable security/detect-object-injection */
this.#data = { ...newData };
this.#validation = { ...newValidation };
}
/**
* Remove validation state from these waypoints. This is useful to quickly
* force the user to revisit some waypoints.
*
* @param {string[]} waypoints Waypoints to be invalidated
* @returns {void}
*/
invalidate(waypoints = []) {
for (let i = 0, l = waypoints.length; i < l; i++) {
// ESLint disabled as `i` is an integer
/* eslint-disable-next-line security/detect-object-injection */
this.removeValidationStateForPage(waypoints[i]);
}
}
/**
* Event listeners are transient. They are not stored in session, and
* generally only apply for the current request.
*
* They also only act on a fixed snapshot of this context's state, which is
* taken at the point of attaching the listeners (in the "data" middleware).
* This is important because JourneyContext.putContext()` could be called many
* times during a request, so the context will be constantly changing.
*
* @param {ContextEvent[]} events Event listeners
* @returns {JourneyContext} Chain
*/
addEventListeners(events) {
this.#eventListeners = events;
this.#eventListenerPreState = this.toObject();
return this;
}
/**
* Execute all listeners for the given event.
*
* The `userInfo` parameter is simply passed straight through to the event
* listeners.
*
* @param {object} params Params
* @param {string} params.event Event (waypoint-change | context-change)
* @param {object} params.session Session
* @param {ContextEventUserInfo | object} [params.userInfo] Pass-through info
* @returns {JourneyContext} Chain
*/
applyEventListeners({ event, session, userInfo }) {
if (!this.#eventListeners.length) {
return this;
}
const previousContext = JourneyContext.fromObject(this.#eventListenerPreState);
const listeners = this.#eventListeners.filter((l) => l.event === event);
// ESLint disabled as `listeners[i]` uses an integer key, and the other keys
// are derived from the list of `listeners`, which are not manipulated at
// runtime (only set by dev in code).
/* eslint-disable security/detect-object-injection */
for (let i = 0, l = listeners.length; i < l; i++) {
const { waypoint, field, handler } = listeners[i];
let logMessage;
let runHandler = false;
if (!waypoint && !field) {
logMessage = "Calling generic event handler";
runHandler = true;
}
else if (waypoint && !field) {
logMessage = `Calling waypoint-specific event handler on "${waypoint}"`;
runHandler =
previousContext.data?.[waypoint] !== undefined &&
!isEqual(this.data?.[waypoint], previousContext.data?.[waypoint]);
}
else if (waypoint && field) {
logMessage = `Calling field-specific event handler on "${waypoint} : ${field}"`;
runHandler =
previousContext.data?.[waypoint]?.[field] !== undefined &&
!isEqual(this.data?.[waypoint]?.[field], previousContext.data?.[waypoint]?.[field]);
}
if (runHandler) {
log.trace(logMessage);
handler({
journeyContext: this,
previousContext,
session,
userInfo,
});
}
}
/* eslint-enable security/detect-object-injection */
return this;
}
/* ----------------------------------------------- session context handling */
/**
* Construct a new ephemeral JourneyContext instance with a unique ID.
*
* Note: In later versions of CASA, the `req` property will be mandatory.
*
* @param {ExpressRequest} [req] Request session
* @returns {JourneyContext} Constructed JourneyContext instance
*/
static createEphemeralContext(req) {
return JourneyContext.fromObject({
identity: {
id: JourneyContext.generateContextId(req),
},
});
}
/**
* Construct a new JourneyContext instance from another instance.
*
* Note: In later versions of CASA, the `req` property will be mandatory.
*
* @param {JourneyContext} context Context to copy from
* @param {ExpressRequest} [req] Request
* @returns {JourneyContext} Constructed JourneyContext instance
* @throws {TypeError} When context is not a valid type
*/
static fromContext(context, req) {
if (!(context instanceof JourneyContext)) {
throw new TypeError("Source context must be a JourneyContext");
}
const newContextObj = context.toObject();
newContextObj.identity.id = JourneyContext.generateContextId(req);
return JourneyContext.fromObject(newContextObj);
}
/**
* Convenience method to determine if this is the default context.
*
* @returns {boolean} True if this is the "default" journey context
*/
isDefault() {
return this.#identity.id === JourneyContext.DEFAULT_CONTEXT_ID;
}
/**
* Initialise session with an empty entry for the "default" context.
*
* @param {object} session Request session
* @returns {void}
*/
static initContextStore(session) {
// For existing sessions that were created prior to `journeyContextList`
// being remodelled as an array, we need to convert the "legacy" structure
// into an equivalent array.
if (isPlainObject(session?.journeyContextList)) {
log.trace("Session context list already initialised as an object (legacy structure). Will convert from object to array.");
session.journeyContextList = Object.entries(session.journeyContextList);
}
// Initialise new context list in the session
if (!Object.hasOwn(session, "journeyContextList")) {
log.trace("Initialising session with a default journey context list");
session.journeyContextList = [];
const defaultContext = new JourneyContext();
defaultContext.identity.id = JourneyContext.DEFAULT_CONTEXT_ID;
JourneyContext.putContext(session, defaultContext);
}
}
/**
* Validate the format of a context ID:
*
* - Between 1 and 64 characters
* - Contain only the characters a-z, 0-9, -
*
* @param {string} id Context ID
* @returns {string} Original ID if it's valid
* @throws {TypeError} When id is not a valid type
* @throws {SyntaxError} When id is not a valid format
*/
static validateContextId(id) {
if (id === JourneyContext.DEFAULT_CONTEXT_ID) {
return JourneyContext.DEFAULT_CONTEXT_ID;
}
if (typeof id !== "string") {
throw new TypeError("Context ID must be a string");
}
else if (!id.match(/^[a-z0-9-]{1,64}$/)) {
throw new SyntaxError("Context ID is not in the correct format");
}
return id;
}
/**
* Generate a new context ID, validate it, and throw if the ID has already
* been generated during this request lifecycle. This may happen if an ID was
* generated, but never used to store a new context in the session. Therefore
* it is important for user code to always call `putContext()` before
* generating another ID.
*
* @param {ExpressRequest} [req] Request
* @returns {string} New ID
* @throws {Error} When generated ID has already been used
*/
static generateContextId(req) {
// Can't generate custom ID when no request object is provided, because the
// custom generator function itself exists on that object.
if (!req) {
throw new Error("Missing required request object.");
}
// Define a default context ID generator if required
if (!Object.hasOwn(req, JourneyContext.ID_GENERATOR_REQ_KEY)) {
log.warn("A context ID generator is not present in the request. Reverting to uuid().");
Object.defineProperty(req, JourneyContext.ID_GENERATOR_REQ_KEY, {
value: uuid,
enumerable: false,
writable: false,
});
}
// Collate a list of context IDs already in use, either from existing
// contexts in the session, or generated during this request lifecycle.
// We don't identify the source of each ID because the generator must not
// differentiate its behaviour on whether the ID exists in session or not.
const inSessionIds = JourneyContext.getContexts(req.session)
.map((c) => c.identity.id)
.filter((id) => id !== JourneyContext.DEFAULT_CONTEXT_ID);
const inRequestIds = req[JourneyContext.ID_GENERATOR_REQ_LOG] ?? [];
const reservedIds = Array.from(new Set([...inSessionIds, ...inRequestIds]).values());
// Generate and log the ID
const id = JourneyContext.validateContextId(req[JourneyContext.ID_GENERATOR_REQ_KEY].call(null, { req, reservedIds }));
if (reservedIds.includes(id)) {
throw new Error(`Regenerated a context ID, ${String(id)}. It has likely not yet been used to store a new context in session using JourneyContext.putContext().`);
}
if (!req[JourneyContext.ID_GENERATOR_REQ_LOG]) {
Object.defineProperty(req, JourneyContext.ID_GENERATOR_REQ_LOG, {
value: [],
enumerable: false,
writable: false,
});
}
req[JourneyContext.ID_GENERATOR_REQ_LOG].push(id);
return id;
}
/**
* Retrieve the default Journey Context. This is just a convenient wrapper
* around `getContextById()`.
*
* @param {object} session Request session
* @returns {JourneyContext} The default Journey Context
*/
static getDefaultContext(session) {
return JourneyContext.getContextById(session, JourneyContext.DEFAULT_CONTEXT_ID);
}
/**
* Lookup context from session using the ID.
*
* @param {object} session Request session
* @param {string} id Context ID
* @returns {JourneyContext} The discovered JourneyContext instance
*/
static getContextById(session, id) {
const list = new Map(session?.journeyContextList);
if (list.has(id)) {
// ESLint disabled as `id` has been verified as an "own" property
return JourneyContext.fromObject(list.get(id));
}
return undefined;
}
/**
* Lookup context from session using the name.
*
* @param {object} session Request session
* @param {string} name Context name
* @returns {JourneyContext} The discovered JourneyContext instance
*/
static getContextByName(session, name) {
if (session) {
const list = new Map(session?.journeyContextList);
const context = [...list.values()].find((c) => c.identity.name === name);
if (context) {
return JourneyContext.fromObject(context);
}
}
return undefined;
}
/**
* Lookup contexts from session using the tag.
*
* @param {object} session Request session
* @param {string} tag Context tag
* @returns {JourneyContext[]} The discovered JourneyContext instance
*/
static getContextsByTag(session, tag) {
if (session) {
const list = new Map(session?.journeyContextList);
return [...list.values()]
.filter((c) => c.identity.tags?.includes(tag))
.map((c) => JourneyContext.fromObject(c));
}
return undefined;
}
/**
* Return all contexts currently stored in the session.
*
* @param {object} session Request session
* @returns {Array} Array of contexts
*/
static getContexts(session) {
if (session && Object.hasOwn(session, "journeyContextList")) {
return session.journeyContextList.map(([, contextObj]) => JourneyContext.fromObject(contextObj));
}
return [];
}
/**
* Put context back into the session store.
*
* @param {object} session Request session
* @param {JourneyContext} context Context
* @param {object} options Options
* @param {ContextEventUserInfo | object} [options.userInfo] Pass-through
* event info
* @returns {void}
* @throws {TypeError} When session is not a valid type, or context has no ID
*/
static putContext(session, context, options = {}) {
if (!isObject(session)) {
throw new TypeError("Session must be an object");
}
else if (!(context instanceof JourneyContext)) {
throw new TypeError("Context must be a valid JourneyContext");
}
else if (context.identity.id === undefined) {
throw new TypeError("Context must have an ID before storing in session");
}
// Initialise the session if necessary
if (Object.hasOwn(session, "journeyContextList") === false) {
JourneyContext.initContextStore(session);
}
// Apply context events
const { userInfo = undefined } = options;
context.applyEventListeners({
event: "waypoint-change",
session,
userInfo,
});
context.applyEventListeners({
event: "context-change",
session,
userInfo,
});
const list = new Map(session.journeyContextList);
list.set(context.identity.id, context.toObject());
session.journeyContextList = [...list.entries()];
}
/**
* Remove a context from the session store.
*
* @param {object} session Request session
* @param {JourneyContext} context Context
* @returns {void}
*/
static removeContext(session, context) {
if (context instanceof JourneyContext) {
JourneyContext.removeContextById(session, context.identity.id);
}
}
/**
* Remove context from session using the ID.
*
* @param {object} session Request session
* @param {string} id Context ID
* @returns {void}
*/
static removeContextById(session, id) {
const index = (session?.journeyContextList ?? []).findIndex(([contextId]) => contextId === id);
if (index > -1) {
session.journeyContextList.splice(index, 1);
}
}
/**
* Remove context from session using the name.
*
* @param {object} session Request session
* @param {string} name Context name
* @returns {void}
*/
static removeContextByName(session, name) {
JourneyContext.removeContext(session, JourneyContext.getContextByName(session, name));
}
/**
* Remove context from session using the tag.
*
* @param {object} session Request session
* @param {string} tag Context tag
* @returns {void}
*/
static removeContextsByTag(session, tag) {
for (const c of JourneyContext.getContextsByTag(session, tag)) {
JourneyContext.removeContext(session, c);
}
}
/**
* Remove call contexts.
*
* @param {object} session Request session
* @returns {void}
*/
static removeContexts(session) {
for (const c of JourneyContext.getContexts(session)) {
JourneyContext.removeContext(session, c);
}
}
/**
* Extract the Journey Context referred to in the incoming request.
*
* This will look in `req.params`, `req.query` and `req.body` for a
* `contextid` parameter, and use that to load the correct Journey Context
* from the session.
*
* @param {ExpressRequest} req ExpressJS incoming request
* @returns {JourneyContext} The Journey Context
*/
static extractContextFromRequest(req) {
JourneyContext.initContextStore(req.session);
let contextId;
if (req.params && Object.hasOwn(req.params, "contextid")) {
log.trace("Context ID found in req.params.contextid");
contextId = String(req.params.contextid);
}
else if (req.query && Object.hasOwn(req.query, "contextid")) {
log.trace("Context ID found in req.query.contextid");
contextId = String(req.query.contextid);
}
else if (req.body && Object.hasOwn(req.body, "contextid")) {
log.trace("Context ID found in req.body.contextid");
contextId = String(req.body.contextid);
}
else {
log.trace("Context ID not specified or not found; will attempt to use default");
contextId = JourneyContext.DEFAULT_CONTEXT_ID;
}
try {
contextId = JourneyContext.validateContextId(contextId);
const context = JourneyContext.getContextById(req.session, contextId);
if (!context) {
throw new Error(`Could not find a context with id, ${contextId}`);
}
return context;
}
catch (err) {
log.debug(err.message);
log.trace("Falling back to default context");
return JourneyContext.getContextById(req.session, JourneyContext.DEFAULT_CONTEXT_ID);
}
}
/**
* Set page skipped status.
*
* @param {string} waypoint Waypoint to skip.
* @param {boolean | object} opts Is skipped flag or options.
* @param {string} opts.to Waypoint to skip to.
*/
setSkipped(waypoint, opts) {
/* eslint-disable security/detect-object-injection */
// Unset, with setSkipped(a, false)
if (opts === false) {
this.data[waypoint] ??= Object.create(null);
this.data[waypoint].__skipped__ = undefined;
this.data[waypoint].__skip__ = undefined;
}
// Set, with setSkipped(a, true) and clear data
else if (opts === true) {
this.data[waypoint] = Object.create(null);
this.data[waypoint].__skipped__ = true;
this.data[waypoint].__skip__ = { to: null };
}
// Set, with setSkipped(a, { to: b }) and clear data
else if (typeof opts?.to === "string") {
this.data[waypoint] = Object.create(null);
this.data[waypoint].__skipped__ = true;
this.data[waypoint].__skip__ = { to: opts.to };
}
else {
throw new TypeError(`setSkipped opts must be a boolean or object with a "to" prop of waypoint to skip to, got: ${typeof opts}`);
}
/* eslint-enable security/detect-object-injection */
}
/**
* Tests if a page has been skipped.
*
* @param {string} waypoint Page ID (waypoint).
* @param {object} opts Skip ptions.
* @param {string} opts.to Waypoint that should be skipped to.
* @returns {boolean} True if the page has been skipped, or if it has been
* skipped to a specific page.
*/
isSkipped(waypoint, opts) {
const wpData = this.data[String(waypoint)];
if (opts === undefined) {
return wpData?.__skipped__ === true || wpData?.__skip__ !== undefined;
}
else if (typeof opts.to === "string") {
return wpData?.__skip__?.to === opts.to;
}
}
}
exports.default = JourneyContext;
//# sourceMappingURL=JourneyContext.js.map