@dwp/govuk-casa
Version:
Framework for creating basic GOVUK Collect-And-Submit-Applications
163 lines (142 loc) • 7.09 kB
JavaScript
const createLogger = require('../../lib/Logger.js');
const { executeHook } = require('./utils.js');
const { parseRequest, createGetRequest } = require('../../lib/utils/index.js');
module.exports = (pageMeta = {}, mountUrl = '/', useStickyEdit = false) => (req, res, next) => {
const logger = createLogger('page.journey-continue');
logger.setSessionId(req.session.id);
const pageId = pageMeta.id;
req.casa = req.casa || Object.create(null);
// If the page has errors, traversal must stop here until those errors are
// resolved. It is the responsibility of the next middleware to deal with
// these errors (usually `middleware/page/render.js`)
if (req.casa.journeyContext.hasValidationErrorsForPage(pageId)) {
logger.trace('Page %s has errors, not progressing journey. Passthrough to next middleware', pageId);
return next();
}
const { journeyOrigin, plan: journey } = req.casa;
const commonRedirectAttributes = {
// undefined because we're including the mountUrl in the waypoint portion of the request builder
mountUrl: undefined,
editOrigin: req.editOriginUrl,
contextId: req.casa.journeyContext.identity.id,
};
function calculateNextWaypoint() {
let nextWaypoint;
if (req.inEditMode) {
// Extract just the waypoint portion of the editOriginUrl, for comparison
// to waypoint during traversal. This will include the mountUrl.
const editOriginWaypoint = req.inEditMode && req.editOriginUrl ? `/${parseRequest({
method: req.method,
url: req.editOriginUrl,
query: {},
body: {},
}).waypoint}` : '';
// When in edit mode, the user should be redirected back to
// `req.editOriginUrl` after submitting their update unless - due to the
// journey data being altered - the waypoints along the journey have
// changed. If the user hasn't yet reached the edit origin url, the
// 'rails' middleware will ensure they are redirected back to the
// correct next waypoint.
let nextOrigin = journeyOrigin.originId || '';
nextWaypoint = editOriginWaypoint;
logger.trace('Comparing pre-gather traversal snapshot (starting from origin %s)', nextOrigin);
// Grab the list of traversed waypoints as it was before gathering, and
// generate a new list of traversed waypoints based on the new context.
const { preGatherTraversalSnapshot = [] } = req.casa || Object.create(null);
const currentTraversalSnapshot = journey.traverseNextRoutes(req.casa.journeyContext, {
startWaypoint: journeyOrigin.waypoint,
});
// Compare the two snapshots. Some rules:
// `a, b, c` vs `a, b, x, c` <- should stop at `x` (`x` was inserted) <- [..++..]
// `a, b, c, d` vs `a, b, d` <- should stop at `d` (`c` was removed) <- [..-.]
// `a, b, c, d` vs `a, c, d` <- should stop at `d` (`b` was removed) <- [.-..]
// `a, b, c` vs `a, c, d` <- should stop at `d` (`b` was removed, `d` was added) <- [.-..+]
let compareIndex = 0;
const compareIndexMax = preGatherTraversalSnapshot.length - 1;
for (let i = 0, l = currentTraversalSnapshot.length; i < l; i++) {
// Build waypoint URL for the current waypoint
const waypointUrl = `${mountUrl}/${currentTraversalSnapshot[i].label.sourceOrigin || nextOrigin}/${currentTraversalSnapshot[i].source}`.replace(/\/+/g, '/');
// Stop testing if we've arrived at the edit origin waypoint
if (editOriginWaypoint === waypointUrl) {
nextWaypoint = editOriginWaypoint;
break;
}
// Find a match for the current waypoint in the previous snapshot.
// And track a change in origin, assuming that all subsequent matches
// (until the next change of origin) are accessed from that origin.
while (compareIndex <= compareIndexMax) {
nextWaypoint = waypointUrl;
nextOrigin = currentTraversalSnapshot[i].label.targetOrigin || nextOrigin;
if (currentTraversalSnapshot[i].source === preGatherTraversalSnapshot[compareIndex++]) {
// The current snapshot may include more waypoints than than the
// previous. In this case, if we've exhausted the list of previous
// waypoints, with the last one being a match, we must leave the user on
// the next waypoint in the current snapshot. Otherwise, the user will
// be left on the same after submitting the form.
if ((compareIndex > compareIndexMax) && (i < l - 1)) {
nextWaypoint = `${mountUrl}/${nextOrigin}/${currentTraversalSnapshot[i + 1].source}`.replace(/\/+/g, '/');
nextOrigin = currentTraversalSnapshot[i + 1].label.targetOrigin || nextOrigin;
}
break;
}
}
// If we've exhausted all comparisons, we're done
if (compareIndex > compareIndexMax && nextWaypoint !== editOriginWaypoint) {
break;
}
}
// Ensure the user remains in edit mode after redirecting, unless they're
// being sent back to the edit origin anyway
nextWaypoint = createGetRequest({
...commonRedirectAttributes,
waypoint: nextWaypoint,
editMode: useStickyEdit && nextWaypoint !== editOriginWaypoint,
});
} else if (journey.containsWaypoint(pageId)) {
logger.trace('Check waypoint %s can be reached (journey guid = %s)', pageId, journeyOrigin.originId);
const routes = journey.traverseNextRoutes(req.casa.journeyContext, {
startWaypoint: journeyOrigin.waypoint,
});
const waypoints = routes.map((e) => e.source);
const positionInJourney = Math.min(
waypoints.indexOf(pageId),
waypoints.length - 2,
);
if (positionInJourney > -1) {
const route = routes[positionInJourney];
nextWaypoint = `${mountUrl}/${route.label.targetOrigin || journeyOrigin.originId || ''}/${waypoints[positionInJourney + 1]}`.replace(/\/+/g, '/');
} else {
nextWaypoint = req.originalUrl;
}
nextWaypoint = createGetRequest({
...commonRedirectAttributes,
waypoint: nextWaypoint,
editMode: false,
});
} else {
logger.trace('Waypoint %s not in journey %s. Returning to original url', pageId, journeyOrigin.originId);
nextWaypoint = req.originalUrl;
}
return nextWaypoint;
}
function redirect(url) {
// Because the hash fragment persists over a redirect, we reset it here.
// Session does not reliably persist when issuing a redirect, so here we
// save explicitly.
req.session.save((err) => {
if (err) {
logger.error('Failed to save session prior to redirect. %s', err.message);
next(err);
} else {
logger.trace('Redirect: %s -> %s', pageId, url);
res.status(302).redirect(`${url}#`);
}
});
}
return executeHook(logger, req, res, pageMeta, 'preredirect')
.then(calculateNextWaypoint)
.then(redirect)
.catch((err) => {
next(err);
});
}