@defra/forms-engine-plugin 1.0.0 → 1.0.1

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.
@@ -85,3 +85,59 @@ There are a number of `LiquidJS` filters available to you from within the templa
85
85
  }
86
86
  ]
87
87
  ```
88
+
89
+ ## Session Rehydration
90
+
91
+ To support Save and Return functionality, this application now supports session rehydration. This allows user session state to be recovered across browser sessions or devices — even after the in-memory Redis session has expired.
92
+
93
+ ### How it works
94
+
95
+ To support session rehydration from a backend (e.g. for Save & Return), the consuming application must provide two functions when registering the DXT engine plugin:
96
+
97
+ ```
98
+ export interface PluginOptions {
99
+ ...
100
+ keyGenerator?: (request) => string
101
+ sessionHydrator?: (request) => Promise<FormSubmissionState | null>
102
+ ...
103
+ }
104
+
105
+ ```
106
+
107
+ 1. `keyGenerator(request)`
108
+
109
+ This generates a stable and consistent cache key used to store and retrieve user state. It should return a string based on persistent identifiers such as userId, businessId, and grantId — i.e., something like:
110
+
111
+ ```
112
+ const keyGenerator = request => {
113
+ const { userId, businessId, grantId } = request.app.userContext
114
+ return `${userId}:${businessId}:${grantId}`
115
+ }
116
+ ```
117
+
118
+ 2. `sessionHydrator(request, key)`
119
+
120
+ This function is called when no session state is found in Redis. It should fetch saved state (e.g., from an API) using the provided key and return it in the same structure expected by the form engine:
121
+
122
+ ```
123
+ const sessionHydrator = async (request, key) => {
124
+ const response = await fetch(`https://backend.api/state/${key}`)
125
+ if (!response.ok) return null
126
+ return await response.json() // Must match form engine state shape
127
+ }
128
+ ```
129
+
130
+ #### Session flow
131
+
132
+ - When user resumes a journey and Redis session data is missing or expired, DXT will use `keyGenerator` and `sessionHydrator` to fetch the saved state from an external API (e.g. `/state` endpoint).
133
+ - The fetched state is written back into Redis and used to continue the user journey.
134
+ - The rehydrated state must include enough information to satisfy schema validation on the current or next page.
135
+ - To properly resume a session, users should be redirected to the `/summary` page. This ensures the UI has all required answers preloaded and avoids invalid transitions from deep links.
136
+
137
+ ### Additional notes
138
+
139
+ Flash messaging and other ephemeral session data still rely on yar.id.
140
+
141
+ If the restored state does not satisfy the schema for the current page, the user will be redirected to the first incomplete step.
142
+
143
+ In development, a mock identity and /state response can be used to simulate a persisted session.
@@ -1,8 +1,9 @@
1
- import { type PluginProperties, type Server } from '@hapi/hapi';
1
+ import { type PluginProperties, type Request, type Server } from '@hapi/hapi';
2
2
  import { type Environment } from 'nunjucks';
3
3
  import { FormModel } from '~/src/server/plugins/engine/models/index.js';
4
4
  import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js';
5
- import { type FilterFunction } from '~/src/server/plugins/engine/types.js';
5
+ import { type FilterFunction, type FormSubmissionState } from '~/src/server/plugins/engine/types.js';
6
+ import { type FormRequest, type FormRequestPayload } from '~/src/server/routes/types.js';
6
7
  import { type Services } from '~/src/server/types.js';
7
8
  export declare function findPackageRoot(): string;
8
9
  export interface PluginOptions {
@@ -10,6 +11,8 @@ export interface PluginOptions {
10
11
  services?: Services;
11
12
  controllers?: Record<string, typeof PageController>;
12
13
  cacheName?: string;
14
+ keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string;
15
+ sessionHydrator?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState>;
13
16
  filters?: Record<string, FilterFunction>;
14
17
  pluginPath?: string;
15
18
  nunjucks: {
@@ -46,6 +46,8 @@ export const plugin = {
46
46
  services = defaultServices,
47
47
  controllers,
48
48
  cacheName,
49
+ keyGenerator,
50
+ sessionHydrator,
49
51
  filters,
50
52
  nunjucks: nunjucksOptions,
51
53
  viewContext
@@ -53,7 +55,14 @@ export const plugin = {
53
55
  const {
54
56
  formsService
55
57
  } = services;
56
- const cacheService = new CacheService(server, cacheName);
58
+ const cacheService = new CacheService({
59
+ server,
60
+ cacheName,
61
+ options: {
62
+ keyGenerator,
63
+ sessionHydrator
64
+ }
65
+ });
57
66
  const packageRoot = findPackageRoot();
58
67
  const govukFrontendPath = dirname(resolvePkg.sync('govuk-frontend/package.json'));
59
68
  const viewPathResolved = join(packageRoot, VIEW_PATH);
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":["existsSync","dirname","join","fileURLToPath","getErrorMessage","hasFormComponents","slugSchema","Boom","vision","isEqual","Joi","nunjucks","resolvePkg","PREVIEW_PATH_PREFIX","checkEmailAddressForLiveFormSubmission","checkFormStatus","findPage","getCacheService","getPage","getStartPath","normalisePath","proceed","redirectPath","VIEW_PATH","context","prepareNunjucksEnvironment","FormModel","SummaryViewModel","format","FileUploadPageController","RepeatPageController","getFormSubmissionData","generateUniqueReference","defaultServices","getUploadStatus","actionSchema","confirmSchema","crumbSchema","itemIdSchema","pathSchema","stateSchema","httpService","CacheService","findPackageRoot","currentFileName","import","meta","url","currentDirectoryName","dir","Error","plugin","name","dependencies","multiple","register","server","options","prefix","realm","modifiers","route","model","services","controllers","cacheName","filters","nunjucksOptions","viewContext","formsService","cacheService","packageRoot","govukFrontendPath","sync","viewPathResolved","paths","engines","html","compile","path","compileOptions","template","environment","render","prepare","next","configure","expose","baseLayoutPath","app","itemCache","Map","models","loadFormPreHandler","request","h","continue","params","slug","isPreview","state","formState","metadata","getFormMetadata","id","notFound","key","item","get","updatedAt","logger","info","definition","getFormDefinition","emailAddress","notificationEmail","outputEmail","basePath","substring","set","dispatchHandler","servicePath","redirectOrMakeHandler","makeHandler","page","getState","$$__referenceNumber","def","referenceNumberPrefix","badImplementation","referenceNumber","mergeState","flash","getFlash","getFormContext","errors","relevantPath","getRelevantPath","summaryPath","getSummaryPath","startsWith","isForceAccess","redirectTo","length","query","returnUrl","getHref","getHandler","events","onLoad","type","viewModel","items","details","payload","undefined","response","postJson","Object","assign","data","makeGetRouteHandler","postHandler","pageDef","href","makePostRouteHandler","dispatchRouteOptions","pre","method","handler","validate","object","keys","getRouteOptions","itemId","optional","postRouteOptions","parse","crumb","action","unknown","required","getListSummaryHandler","makeGetListSummaryRouteHandler","postListSummaryHandler","makePostListSummaryRouteHandler","getItemDeleteHandler","makeGetItemDeleteRouteHandler","postItemDeleteHandler","makePostItemDeleteRouteHandler","confirm","uploadId","status","error","code","errMsg","plugins","string","guid"],"sources":["../../../../src/server/plugins/engine/plugin.ts"],"sourcesContent":["import { existsSync } from 'fs'\nimport { dirname, join } from 'path'\nimport { fileURLToPath } from 'url'\n\nimport {\n getErrorMessage,\n hasFormComponents,\n slugSchema\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport {\n type Plugin,\n type PluginProperties,\n type ResponseObject,\n type ResponseToolkit,\n type RouteOptions,\n type Server\n} from '@hapi/hapi'\nimport vision from '@hapi/vision'\nimport { isEqual } from 'date-fns'\nimport Joi from 'joi'\nimport nunjucks, { type Environment } from 'nunjucks'\nimport resolvePkg from 'resolve'\n\nimport { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n checkFormStatus,\n findPage,\n getCacheService,\n getPage,\n getStartPath,\n normalisePath,\n proceed,\n redirectPath\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n VIEW_PATH,\n context,\n prepareNunjucksEnvironment\n} from '~/src/server/plugins/engine/index.js'\nimport {\n FormModel,\n SummaryViewModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport { format } from '~/src/server/plugins/engine/outputFormatters/machine/v1.js'\nimport { FileUploadPageController } from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'\nimport { getFormSubmissionData } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'\nimport { generateUniqueReference } from '~/src/server/plugins/engine/referenceNumbers.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport { getUploadStatus } from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n type FilterFunction,\n type FormContext\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\nimport {\n actionSchema,\n confirmSchema,\n crumbSchema,\n itemIdSchema,\n pathSchema,\n stateSchema\n} from '~/src/server/schemas/index.js'\nimport * as httpService from '~/src/server/services/httpService.js'\nimport { CacheService } from '~/src/server/services/index.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport function findPackageRoot() {\n const currentFileName = fileURLToPath(import.meta.url)\n const currentDirectoryName = dirname(currentFileName)\n\n let dir = currentDirectoryName\n while (dir !== '/') {\n if (existsSync(join(dir, 'package.json'))) {\n return dir\n }\n dir = dirname(dir)\n }\n\n throw new Error('package.json not found in parent directories')\n}\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n filters?: Record<string, FilterFunction>\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n}\n\nexport const plugin = {\n name: '@defra/forms-engine-plugin',\n dependencies: ['@hapi/crumb', '@hapi/yar', 'hapi-pino'],\n multiple: true,\n async register(server: Server, options: PluginOptions) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- hapi types are wrong\n const prefix = server.realm.modifiers.route.prefix ?? ''\n const {\n model,\n services = defaultServices,\n controllers,\n cacheName,\n filters,\n nunjucks: nunjucksOptions,\n viewContext\n } = options\n const { formsService } = services\n const cacheService = new CacheService(server, cacheName)\n\n const packageRoot = findPackageRoot()\n const govukFrontendPath = dirname(\n resolvePkg.sync('govuk-frontend/package.json')\n )\n\n const viewPathResolved = join(packageRoot, VIEW_PATH)\n\n const paths = [\n ...nunjucksOptions.paths,\n viewPathResolved,\n join(govukFrontendPath, 'dist')\n ]\n\n await server.register({\n plugin: vision,\n options: {\n engines: {\n html: {\n compile: (\n path: string,\n compileOptions: { environment: Environment }\n ) => {\n const template = nunjucks.compile(\n path,\n compileOptions.environment\n )\n\n return (context: object | undefined) => {\n return template.render(context)\n }\n },\n prepare: (\n options: EngineConfigurationObject,\n next: (err?: Error) => void\n ) => {\n // Nunjucks also needs an additional path configuration\n // to use the templates and macros from `govuk-frontend`\n const environment = nunjucks.configure(paths)\n\n // Applies custom filters and globals for nunjucks\n // that are required by the `forms-engine-plugin`\n prepareNunjucksEnvironment(environment, filters)\n\n options.compileOptions.environment = environment\n\n next()\n }\n }\n },\n path: paths,\n // Provides global context used with all templates\n context\n }\n })\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n\n server.app.model = model\n\n // In-memory cache of FormModel items, exposed\n // (for testing purposes) through `server.app.models`\n const itemCache = new Map<string, { model: FormModel; updatedAt: Date }>()\n server.app.models = itemCache\n\n const loadFormPreHandler = async (\n request: FormRequest | FormRequestPayload,\n h: Pick<ResponseToolkit, 'continue'>\n ) => {\n if (server.app.model) {\n request.app.model = server.app.model\n\n return h.continue\n }\n\n const { params } = request\n const { slug } = params\n const { isPreview, state: formState } = checkFormStatus(params)\n\n // Get the form metadata using the `slug` param\n const metadata = await formsService.getFormMetadata(slug)\n\n const { id, [formState]: state } = metadata\n\n // Check the metadata supports the requested state\n if (!state) {\n throw Boom.notFound(`No '${formState}' state for form metadata ${id}`)\n }\n\n // Cache the models based on id, state and whether\n // it's a preview or not. There could be up to 3 models\n // cached for a single form:\n // \"{id}_live_false\" (live/live)\n // \"{id}_live_true\" (live/preview)\n // \"{id}_draft_true\" (draft/preview)\n const key = `${id}_${formState}_${isPreview}`\n let item = itemCache.get(key)\n\n if (!item || !isEqual(item.updatedAt, state.updatedAt)) {\n server.logger.info(\n `Getting form definition ${id} (${slug}) ${formState}`\n )\n\n // Get the form definition using the `id` from the metadata\n const definition = await formsService.getFormDefinition(id, formState)\n\n if (!definition) {\n throw Boom.notFound(\n `No definition found for form metadata ${id} (${slug}) ${formState}`\n )\n }\n\n const emailAddress =\n metadata.notificationEmail ?? definition.outputEmail\n\n checkEmailAddressForLiveFormSubmission(emailAddress, isPreview)\n\n // Build the form model\n server.logger.info(\n `Building model for form definition ${id} (${slug}) ${formState}`\n )\n\n // Set up the basePath for the model\n const basePath = (\n isPreview\n ? `${prefix}${PREVIEW_PATH_PREFIX}/${formState}/${slug}`\n : `${prefix}/${slug}`\n ).substring(1)\n\n // Construct the form model\n const model = new FormModel(\n definition,\n { basePath },\n services,\n controllers\n )\n\n // Create new item and add it to the item cache\n item = { model, updatedAt: state.updatedAt }\n itemCache.set(key, item)\n }\n\n // Assign the model to the request data\n // for use in the downstream handler\n request.app.model = item.model\n\n return h.continue\n }\n\n const dispatchHandler = (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { model } = request.app\n\n const servicePath = model ? `/${model.basePath}` : ''\n return proceed(request, h, `${servicePath}${getStartPath(model)}`)\n }\n\n const redirectOrMakeHandler = async (\n request: FormRequest | FormRequestPayload,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>,\n makeHandler: (\n page: PageControllerClass,\n context: FormContext\n ) => ResponseObject | Promise<ResponseObject>\n ) => {\n const { app, params } = request\n const { model } = app\n\n if (!model) {\n throw Boom.notFound(`No model found for /${params.path}`)\n }\n\n const cacheService = getCacheService(request.server)\n const page = getPage(model, request)\n let state = await page.getState(request)\n\n if (!state.$$__referenceNumber) {\n const prefix = model.def.metadata?.referenceNumberPrefix ?? ''\n\n if (typeof prefix !== 'string') {\n throw Boom.badImplementation(\n 'Reference number prefix must be a string or undefined'\n )\n }\n\n const referenceNumber = generateUniqueReference(prefix)\n state = await page.mergeState(request, state, {\n $$__referenceNumber: referenceNumber\n })\n }\n\n const flash = cacheService.getFlash(request)\n const context = model.getFormContext(request, state, flash?.errors)\n const relevantPath = page.getRelevantPath(request, context)\n const summaryPath = page.getSummaryPath()\n\n // Return handler for relevant pages or preview URL direct access\n if (relevantPath.startsWith(page.path) || context.isForceAccess) {\n return makeHandler(page, context)\n }\n\n // Redirect back to last relevant page\n const redirectTo = findPage(model, relevantPath)\n\n // Set the return URL unless an exit page\n if (redirectTo?.next.length) {\n request.query.returnUrl = page.getHref(summaryPath)\n }\n\n return proceed(request, h, page.getHref(relevantPath))\n }\n\n const getHandler = (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n if (normalisePath(params.path) === '') {\n return dispatchHandler(request, h)\n }\n\n return redirectOrMakeHandler(request, h, async (page, context) => {\n // Check for a page onLoad HTTP event and if one exists,\n // call it and assign the response to the context data\n const { events } = page\n const { model } = request.app\n\n if (!model) {\n throw Boom.notFound(`No model found for /${params.path}`)\n }\n\n if (events?.onLoad && events.onLoad.type === 'http') {\n const { options } = events.onLoad\n const { url } = options\n\n // TODO: Update structured data POST payload with when helper\n // is updated to removing the dependency on `SummaryViewModel` etc.\n const viewModel = new SummaryViewModel(request, page, context)\n const items = getFormSubmissionData(\n viewModel.context,\n viewModel.details\n )\n\n // @ts-expect-error - function signature will be refactored in the next iteration of the formatter\n const payload = format(items, model, undefined, undefined)\n\n const { payload: response } = await httpService.postJson(url, {\n payload\n })\n\n Object.assign(context.data, response)\n }\n\n return page.makeGetRouteHandler()(request, context, h)\n })\n }\n\n const postHandler = (\n request: FormRequestPayload,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { query } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n const { pageDef } = page\n const { isForceAccess } = context\n\n // Redirect to GET for preview URL direct access\n if (isForceAccess && !hasFormComponents(pageDef)) {\n return proceed(request, h, redirectPath(page.href, query))\n }\n\n return page.makePostRouteHandler()(request, context, h)\n })\n }\n\n const dispatchRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method: loadFormPreHandler\n }\n ]\n }\n\n server.route({\n method: 'get',\n path: '/{slug}',\n handler: dispatchHandler,\n options: {\n ...dispatchRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema\n })\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/preview/{state}/{slug}',\n handler: dispatchHandler,\n options: {\n ...dispatchRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema\n })\n }\n }\n })\n\n const getRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method: loadFormPreHandler\n }\n ]\n }\n\n server.route({\n method: 'get',\n path: '/{slug}/{path}/{itemId?}',\n handler: getHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema.optional()\n })\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/preview/{state}/{slug}/{path}/{itemId?}',\n handler: getHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema.optional()\n })\n }\n }\n })\n\n const postRouteOptions: RouteOptions<FormRequestPayloadRefs> = {\n payload: {\n parse: true\n },\n pre: [{ method: loadFormPreHandler }]\n }\n\n server.route({\n method: 'post',\n path: '/{slug}/{path}/{itemId?}',\n handler: postHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema.optional()\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .unknown(true)\n .required()\n }\n }\n })\n\n server.route({\n method: 'post',\n path: '/preview/{state}/{slug}/{path}/{itemId?}',\n handler: postHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema.optional()\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .unknown(true)\n .required()\n }\n }\n })\n\n /**\n * \"AddAnother\" repeat routes\n */\n\n // List summary GET route\n const getListSummaryHandler = (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n if (!(page instanceof RepeatPageController)) {\n throw Boom.notFound(`No repeater page found for /${params.path}`)\n }\n\n return page.makeGetListSummaryRouteHandler()(request, context, h)\n })\n }\n\n server.route({\n method: 'get',\n path: '/{slug}/{path}/summary',\n handler: getListSummaryHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema\n })\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/preview/{state}/{slug}/{path}/summary',\n handler: getListSummaryHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema\n })\n }\n }\n })\n\n // List summary POST route\n const postListSummaryHandler = (\n request: FormRequestPayload,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n const { isForceAccess } = context\n\n if (isForceAccess || !(page instanceof RepeatPageController)) {\n throw Boom.notFound(`No repeater page found for /${params.path}`)\n }\n\n return page.makePostListSummaryRouteHandler()(request, context, h)\n })\n }\n\n server.route({\n method: 'post',\n path: '/{slug}/{path}/summary',\n handler: postListSummaryHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .required()\n }\n }\n })\n\n server.route({\n method: 'post',\n path: '/preview/{state}/{slug}/{path}/summary',\n handler: postListSummaryHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .required()\n }\n }\n })\n\n // Item delete GET route\n const getItemDeleteHandler = (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n if (\n !(\n page instanceof RepeatPageController ||\n page instanceof FileUploadPageController\n )\n ) {\n throw Boom.notFound(`No page found for /${params.path}`)\n }\n\n return page.makeGetItemDeleteRouteHandler()(request, context, h)\n })\n }\n\n server.route({\n method: 'get',\n path: '/{slug}/{path}/{itemId}/confirm-delete',\n handler: getItemDeleteHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema\n })\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/preview/{state}/{slug}/{path}/{itemId}/confirm-delete',\n handler: getItemDeleteHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema\n })\n }\n }\n })\n\n // Item delete POST route\n const postItemDeleteHandler = (\n request: FormRequestPayload,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n const { isForceAccess } = context\n\n if (\n isForceAccess ||\n !(\n page instanceof RepeatPageController ||\n page instanceof FileUploadPageController\n )\n ) {\n throw Boom.notFound(`No page found for /${params.path}`)\n }\n\n return page.makePostItemDeleteRouteHandler()(request, context, h)\n })\n }\n\n server.route({\n method: 'post',\n path: '/{slug}/{path}/{itemId}/confirm-delete',\n handler: postItemDeleteHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema,\n confirm: confirmSchema\n })\n .required()\n }\n }\n })\n\n server.route({\n method: 'post',\n path: '/preview/{state}/{slug}/{path}/{itemId}/confirm-delete',\n handler: postItemDeleteHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema,\n confirm: confirmSchema\n })\n .required()\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/upload-status/{uploadId}',\n handler: async (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'response'>\n ) => {\n const { uploadId } = request.params as unknown as {\n uploadId: string\n }\n try {\n const status = await getUploadStatus(uploadId)\n\n if (!status) {\n return h.response({ error: 'Status check failed' }).code(400)\n }\n\n return h.response(status)\n } catch (error) {\n const errMsg = getErrorMessage(error)\n request.logger.error(\n errMsg,\n `[uploadStatusFailed] Upload status check failed for uploadId: ${uploadId} - ${errMsg}`\n )\n return h.response({ error: 'Status check error' }).code(500)\n }\n },\n options: {\n plugins: {\n crumb: false\n },\n validate: {\n params: Joi.object().keys({\n uploadId: Joi.string().guid().required()\n })\n }\n }\n })\n }\n} satisfies Plugin<PluginOptions>\n\ninterface CompileOptions {\n environment: Environment\n}\n\nexport interface EngineConfigurationObject {\n compileOptions: CompileOptions\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"plugin.js","names":["existsSync","dirname","join","fileURLToPath","getErrorMessage","hasFormComponents","slugSchema","Boom","vision","isEqual","Joi","nunjucks","resolvePkg","PREVIEW_PATH_PREFIX","checkEmailAddressForLiveFormSubmission","checkFormStatus","findPage","getCacheService","getPage","getStartPath","normalisePath","proceed","redirectPath","VIEW_PATH","context","prepareNunjucksEnvironment","FormModel","SummaryViewModel","format","FileUploadPageController","RepeatPageController","getFormSubmissionData","generateUniqueReference","defaultServices","getUploadStatus","actionSchema","confirmSchema","crumbSchema","itemIdSchema","pathSchema","stateSchema","httpService","CacheService","findPackageRoot","currentFileName","import","meta","url","currentDirectoryName","dir","Error","plugin","name","dependencies","multiple","register","server","options","prefix","realm","modifiers","route","model","services","controllers","cacheName","keyGenerator","sessionHydrator","filters","nunjucksOptions","viewContext","formsService","cacheService","packageRoot","govukFrontendPath","sync","viewPathResolved","paths","engines","html","compile","path","compileOptions","template","environment","render","prepare","next","configure","expose","baseLayoutPath","app","itemCache","Map","models","loadFormPreHandler","request","h","continue","params","slug","isPreview","state","formState","metadata","getFormMetadata","id","notFound","key","item","get","updatedAt","logger","info","definition","getFormDefinition","emailAddress","notificationEmail","outputEmail","basePath","substring","set","dispatchHandler","servicePath","redirectOrMakeHandler","makeHandler","page","getState","$$__referenceNumber","def","referenceNumberPrefix","badImplementation","referenceNumber","mergeState","flash","getFlash","getFormContext","errors","relevantPath","getRelevantPath","summaryPath","getSummaryPath","startsWith","isForceAccess","redirectTo","length","query","returnUrl","getHref","getHandler","events","onLoad","type","viewModel","items","details","payload","undefined","response","postJson","Object","assign","data","makeGetRouteHandler","postHandler","pageDef","href","makePostRouteHandler","dispatchRouteOptions","pre","method","handler","validate","object","keys","getRouteOptions","itemId","optional","postRouteOptions","parse","crumb","action","unknown","required","getListSummaryHandler","makeGetListSummaryRouteHandler","postListSummaryHandler","makePostListSummaryRouteHandler","getItemDeleteHandler","makeGetItemDeleteRouteHandler","postItemDeleteHandler","makePostItemDeleteRouteHandler","confirm","uploadId","status","error","code","errMsg","plugins","string","guid"],"sources":["../../../../src/server/plugins/engine/plugin.ts"],"sourcesContent":["import { existsSync } from 'fs'\nimport { dirname, join } from 'path'\nimport { fileURLToPath } from 'url'\n\nimport {\n getErrorMessage,\n hasFormComponents,\n slugSchema\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport {\n type Plugin,\n type PluginProperties,\n type Request,\n type ResponseObject,\n type ResponseToolkit,\n type RouteOptions,\n type Server\n} from '@hapi/hapi'\nimport vision from '@hapi/vision'\nimport { isEqual } from 'date-fns'\nimport Joi from 'joi'\nimport nunjucks, { type Environment } from 'nunjucks'\nimport resolvePkg from 'resolve'\n\nimport { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n checkFormStatus,\n findPage,\n getCacheService,\n getPage,\n getStartPath,\n normalisePath,\n proceed,\n redirectPath\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n VIEW_PATH,\n context,\n prepareNunjucksEnvironment\n} from '~/src/server/plugins/engine/index.js'\nimport {\n FormModel,\n SummaryViewModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport { format } from '~/src/server/plugins/engine/outputFormatters/machine/v1.js'\nimport { FileUploadPageController } from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'\nimport { getFormSubmissionData } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'\nimport { generateUniqueReference } from '~/src/server/plugins/engine/referenceNumbers.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport { getUploadStatus } from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n type FilterFunction,\n type FormContext,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\nimport {\n actionSchema,\n confirmSchema,\n crumbSchema,\n itemIdSchema,\n pathSchema,\n stateSchema\n} from '~/src/server/schemas/index.js'\nimport * as httpService from '~/src/server/services/httpService.js'\nimport { CacheService } from '~/src/server/services/index.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport function findPackageRoot() {\n const currentFileName = fileURLToPath(import.meta.url)\n const currentDirectoryName = dirname(currentFileName)\n\n let dir = currentDirectoryName\n while (dir !== '/') {\n if (existsSync(join(dir, 'package.json'))) {\n return dir\n }\n dir = dirname(dir)\n }\n\n throw new Error('package.json not found in parent directories')\n}\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState>\n filters?: Record<string, FilterFunction>\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n}\n\nexport const plugin = {\n name: '@defra/forms-engine-plugin',\n dependencies: ['@hapi/crumb', '@hapi/yar', 'hapi-pino'],\n multiple: true,\n async register(server: Server, options: PluginOptions) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- hapi types are wrong\n const prefix = server.realm.modifiers.route.prefix ?? ''\n const {\n model,\n services = defaultServices,\n controllers,\n cacheName,\n keyGenerator,\n sessionHydrator,\n filters,\n nunjucks: nunjucksOptions,\n viewContext\n } = options\n const { formsService } = services\n const cacheService = new CacheService({\n server,\n cacheName,\n options: {\n keyGenerator,\n sessionHydrator\n }\n })\n\n const packageRoot = findPackageRoot()\n const govukFrontendPath = dirname(\n resolvePkg.sync('govuk-frontend/package.json')\n )\n\n const viewPathResolved = join(packageRoot, VIEW_PATH)\n\n const paths = [\n ...nunjucksOptions.paths,\n viewPathResolved,\n join(govukFrontendPath, 'dist')\n ]\n\n await server.register({\n plugin: vision,\n options: {\n engines: {\n html: {\n compile: (\n path: string,\n compileOptions: { environment: Environment }\n ) => {\n const template = nunjucks.compile(\n path,\n compileOptions.environment\n )\n\n return (context: object | undefined) => {\n return template.render(context)\n }\n },\n prepare: (\n options: EngineConfigurationObject,\n next: (err?: Error) => void\n ) => {\n // Nunjucks also needs an additional path configuration\n // to use the templates and macros from `govuk-frontend`\n const environment = nunjucks.configure(paths)\n\n // Applies custom filters and globals for nunjucks\n // that are required by the `forms-engine-plugin`\n prepareNunjucksEnvironment(environment, filters)\n\n options.compileOptions.environment = environment\n\n next()\n }\n }\n },\n path: paths,\n // Provides global context used with all templates\n context\n }\n })\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n\n server.app.model = model\n\n // In-memory cache of FormModel items, exposed\n // (for testing purposes) through `server.app.models`\n const itemCache = new Map<string, { model: FormModel; updatedAt: Date }>()\n server.app.models = itemCache\n\n const loadFormPreHandler = async (\n request: FormRequest | FormRequestPayload,\n h: Pick<ResponseToolkit, 'continue'>\n ) => {\n if (server.app.model) {\n request.app.model = server.app.model\n\n return h.continue\n }\n\n const { params } = request\n const { slug } = params\n const { isPreview, state: formState } = checkFormStatus(params)\n\n // Get the form metadata using the `slug` param\n const metadata = await formsService.getFormMetadata(slug)\n\n const { id, [formState]: state } = metadata\n\n // Check the metadata supports the requested state\n if (!state) {\n throw Boom.notFound(`No '${formState}' state for form metadata ${id}`)\n }\n\n // Cache the models based on id, state and whether\n // it's a preview or not. There could be up to 3 models\n // cached for a single form:\n // \"{id}_live_false\" (live/live)\n // \"{id}_live_true\" (live/preview)\n // \"{id}_draft_true\" (draft/preview)\n const key = `${id}_${formState}_${isPreview}`\n let item = itemCache.get(key)\n\n if (!item || !isEqual(item.updatedAt, state.updatedAt)) {\n server.logger.info(\n `Getting form definition ${id} (${slug}) ${formState}`\n )\n\n // Get the form definition using the `id` from the metadata\n const definition = await formsService.getFormDefinition(id, formState)\n\n if (!definition) {\n throw Boom.notFound(\n `No definition found for form metadata ${id} (${slug}) ${formState}`\n )\n }\n\n const emailAddress =\n metadata.notificationEmail ?? definition.outputEmail\n\n checkEmailAddressForLiveFormSubmission(emailAddress, isPreview)\n\n // Build the form model\n server.logger.info(\n `Building model for form definition ${id} (${slug}) ${formState}`\n )\n\n // Set up the basePath for the model\n const basePath = (\n isPreview\n ? `${prefix}${PREVIEW_PATH_PREFIX}/${formState}/${slug}`\n : `${prefix}/${slug}`\n ).substring(1)\n\n // Construct the form model\n const model = new FormModel(\n definition,\n { basePath },\n services,\n controllers\n )\n\n // Create new item and add it to the item cache\n item = { model, updatedAt: state.updatedAt }\n itemCache.set(key, item)\n }\n\n // Assign the model to the request data\n // for use in the downstream handler\n request.app.model = item.model\n\n return h.continue\n }\n\n const dispatchHandler = (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { model } = request.app\n\n const servicePath = model ? `/${model.basePath}` : ''\n return proceed(request, h, `${servicePath}${getStartPath(model)}`)\n }\n\n const redirectOrMakeHandler = async (\n request: FormRequest | FormRequestPayload,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>,\n makeHandler: (\n page: PageControllerClass,\n context: FormContext\n ) => ResponseObject | Promise<ResponseObject>\n ) => {\n const { app, params } = request\n const { model } = app\n\n if (!model) {\n throw Boom.notFound(`No model found for /${params.path}`)\n }\n\n const cacheService = getCacheService(request.server)\n const page = getPage(model, request)\n let state = await page.getState(request)\n\n if (!state.$$__referenceNumber) {\n const prefix = model.def.metadata?.referenceNumberPrefix ?? ''\n\n if (typeof prefix !== 'string') {\n throw Boom.badImplementation(\n 'Reference number prefix must be a string or undefined'\n )\n }\n\n const referenceNumber = generateUniqueReference(prefix)\n state = await page.mergeState(request, state, {\n $$__referenceNumber: referenceNumber\n })\n }\n\n const flash = cacheService.getFlash(request)\n const context = model.getFormContext(request, state, flash?.errors)\n const relevantPath = page.getRelevantPath(request, context)\n const summaryPath = page.getSummaryPath()\n\n // Return handler for relevant pages or preview URL direct access\n if (relevantPath.startsWith(page.path) || context.isForceAccess) {\n return makeHandler(page, context)\n }\n\n // Redirect back to last relevant page\n const redirectTo = findPage(model, relevantPath)\n\n // Set the return URL unless an exit page\n if (redirectTo?.next.length) {\n request.query.returnUrl = page.getHref(summaryPath)\n }\n\n return proceed(request, h, page.getHref(relevantPath))\n }\n\n const getHandler = (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n if (normalisePath(params.path) === '') {\n return dispatchHandler(request, h)\n }\n\n return redirectOrMakeHandler(request, h, async (page, context) => {\n // Check for a page onLoad HTTP event and if one exists,\n // call it and assign the response to the context data\n const { events } = page\n const { model } = request.app\n\n if (!model) {\n throw Boom.notFound(`No model found for /${params.path}`)\n }\n\n if (events?.onLoad && events.onLoad.type === 'http') {\n const { options } = events.onLoad\n const { url } = options\n\n // TODO: Update structured data POST payload with when helper\n // is updated to removing the dependency on `SummaryViewModel` etc.\n const viewModel = new SummaryViewModel(request, page, context)\n const items = getFormSubmissionData(\n viewModel.context,\n viewModel.details\n )\n\n // @ts-expect-error - function signature will be refactored in the next iteration of the formatter\n const payload = format(items, model, undefined, undefined)\n\n const { payload: response } = await httpService.postJson(url, {\n payload\n })\n\n Object.assign(context.data, response)\n }\n\n return page.makeGetRouteHandler()(request, context, h)\n })\n }\n\n const postHandler = (\n request: FormRequestPayload,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { query } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n const { pageDef } = page\n const { isForceAccess } = context\n\n // Redirect to GET for preview URL direct access\n if (isForceAccess && !hasFormComponents(pageDef)) {\n return proceed(request, h, redirectPath(page.href, query))\n }\n\n return page.makePostRouteHandler()(request, context, h)\n })\n }\n\n const dispatchRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method: loadFormPreHandler\n }\n ]\n }\n\n server.route({\n method: 'get',\n path: '/{slug}',\n handler: dispatchHandler,\n options: {\n ...dispatchRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema\n })\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/preview/{state}/{slug}',\n handler: dispatchHandler,\n options: {\n ...dispatchRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema\n })\n }\n }\n })\n\n const getRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method: loadFormPreHandler\n }\n ]\n }\n\n server.route({\n method: 'get',\n path: '/{slug}/{path}/{itemId?}',\n handler: getHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema.optional()\n })\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/preview/{state}/{slug}/{path}/{itemId?}',\n handler: getHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema.optional()\n })\n }\n }\n })\n\n const postRouteOptions: RouteOptions<FormRequestPayloadRefs> = {\n payload: {\n parse: true\n },\n pre: [{ method: loadFormPreHandler }]\n }\n\n server.route({\n method: 'post',\n path: '/{slug}/{path}/{itemId?}',\n handler: postHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema.optional()\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .unknown(true)\n .required()\n }\n }\n })\n\n server.route({\n method: 'post',\n path: '/preview/{state}/{slug}/{path}/{itemId?}',\n handler: postHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema.optional()\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .unknown(true)\n .required()\n }\n }\n })\n\n /**\n * \"AddAnother\" repeat routes\n */\n\n // List summary GET route\n const getListSummaryHandler = (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n if (!(page instanceof RepeatPageController)) {\n throw Boom.notFound(`No repeater page found for /${params.path}`)\n }\n\n return page.makeGetListSummaryRouteHandler()(request, context, h)\n })\n }\n\n server.route({\n method: 'get',\n path: '/{slug}/{path}/summary',\n handler: getListSummaryHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema\n })\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/preview/{state}/{slug}/{path}/summary',\n handler: getListSummaryHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema\n })\n }\n }\n })\n\n // List summary POST route\n const postListSummaryHandler = (\n request: FormRequestPayload,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n const { isForceAccess } = context\n\n if (isForceAccess || !(page instanceof RepeatPageController)) {\n throw Boom.notFound(`No repeater page found for /${params.path}`)\n }\n\n return page.makePostListSummaryRouteHandler()(request, context, h)\n })\n }\n\n server.route({\n method: 'post',\n path: '/{slug}/{path}/summary',\n handler: postListSummaryHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .required()\n }\n }\n })\n\n server.route({\n method: 'post',\n path: '/preview/{state}/{slug}/{path}/summary',\n handler: postListSummaryHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .required()\n }\n }\n })\n\n // Item delete GET route\n const getItemDeleteHandler = (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n if (\n !(\n page instanceof RepeatPageController ||\n page instanceof FileUploadPageController\n )\n ) {\n throw Boom.notFound(`No page found for /${params.path}`)\n }\n\n return page.makeGetItemDeleteRouteHandler()(request, context, h)\n })\n }\n\n server.route({\n method: 'get',\n path: '/{slug}/{path}/{itemId}/confirm-delete',\n handler: getItemDeleteHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema\n })\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/preview/{state}/{slug}/{path}/{itemId}/confirm-delete',\n handler: getItemDeleteHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema\n })\n }\n }\n })\n\n // Item delete POST route\n const postItemDeleteHandler = (\n request: FormRequestPayload,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, (page, context) => {\n const { isForceAccess } = context\n\n if (\n isForceAccess ||\n !(\n page instanceof RepeatPageController ||\n page instanceof FileUploadPageController\n )\n ) {\n throw Boom.notFound(`No page found for /${params.path}`)\n }\n\n return page.makePostItemDeleteRouteHandler()(request, context, h)\n })\n }\n\n server.route({\n method: 'post',\n path: '/{slug}/{path}/{itemId}/confirm-delete',\n handler: postItemDeleteHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema,\n confirm: confirmSchema\n })\n .required()\n }\n }\n })\n\n server.route({\n method: 'post',\n path: '/preview/{state}/{slug}/{path}/{itemId}/confirm-delete',\n handler: postItemDeleteHandler,\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema,\n itemId: itemIdSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema,\n confirm: confirmSchema\n })\n .required()\n }\n }\n })\n\n server.route({\n method: 'get',\n path: '/upload-status/{uploadId}',\n handler: async (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'response'>\n ) => {\n const { uploadId } = request.params as unknown as {\n uploadId: string\n }\n try {\n const status = await getUploadStatus(uploadId)\n\n if (!status) {\n return h.response({ error: 'Status check failed' }).code(400)\n }\n\n return h.response(status)\n } catch (error) {\n const errMsg = getErrorMessage(error)\n request.logger.error(\n errMsg,\n `[uploadStatusFailed] Upload status check failed for uploadId: ${uploadId} - ${errMsg}`\n )\n return h.response({ error: 'Status check error' }).code(500)\n }\n },\n options: {\n plugins: {\n crumb: false\n },\n validate: {\n params: Joi.object().keys({\n uploadId: Joi.string().guid().required()\n })\n }\n }\n })\n }\n} satisfies Plugin<PluginOptions>\n\ninterface CompileOptions {\n environment: Environment\n}\n\nexport interface EngineConfigurationObject {\n compileOptions: CompileOptions\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,7 +1,7 @@
1
1
  import { type Request, type Server } from '@hapi/hapi';
2
2
  import { type FormPayload, type FormState, type FormSubmissionError, type FormSubmissionState } from '~/src/server/plugins/engine/types.js';
3
3
  import { type FormRequest, type FormRequestPayload } from '~/src/server/routes/types.js';
4
- declare enum ADDITIONAL_IDENTIFIER {
4
+ export declare enum ADDITIONAL_IDENTIFIER {
5
5
  Confirmation = ":confirmation"
6
6
  }
7
7
  export declare class CacheService {
@@ -12,8 +12,17 @@ export declare class CacheService {
12
12
  cache: string | undefined;
13
13
  segment: string;
14
14
  }>;
15
+ generateKey?: (request: Request | FormRequest | FormRequestPayload) => string;
16
+ customFetcher?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState | null>;
15
17
  logger: Server['logger'];
16
- constructor(server: Server, cacheName?: string);
18
+ constructor({ server, cacheName, options }: {
19
+ server: Server;
20
+ cacheName?: string;
21
+ options?: {
22
+ keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string;
23
+ sessionHydrator?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState | null>;
24
+ };
25
+ });
17
26
  getState(request: Request | FormRequest | FormRequestPayload): Promise<FormSubmissionState>;
18
27
  setState(request: FormRequest | FormRequestPayload, state: FormSubmissionState): Promise<FormSubmissionState>;
19
28
  getConfirmationState(request: FormRequest | FormRequestPayload): Promise<{
@@ -29,6 +38,7 @@ export declare class CacheService {
29
38
  setFlash(request: FormRequest | FormRequestPayload, message: {
30
39
  errors: FormSubmissionError[];
31
40
  }): void;
41
+ private defaultKeyGenerator;
32
42
  /**
33
43
  * The key used to store user session data against.
34
44
  * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`
@@ -46,4 +56,3 @@ export declare class CacheService {
46
56
  * 2. Overwrites arrays
47
57
  */
48
58
  export declare function merge<StateType extends FormState | FormPayload>(state: StateType, update: object): StateType;
49
- export {};
@@ -1,20 +1,32 @@
1
1
  import * as Hoek from '@hapi/hoek';
2
2
  import { config } from "../../config/index.js";
3
3
  const partition = 'cache';
4
- var ADDITIONAL_IDENTIFIER = /*#__PURE__*/function (ADDITIONAL_IDENTIFIER) {
4
+ export let ADDITIONAL_IDENTIFIER = /*#__PURE__*/function (ADDITIONAL_IDENTIFIER) {
5
5
  ADDITIONAL_IDENTIFIER["Confirmation"] = ":confirmation";
6
6
  return ADDITIONAL_IDENTIFIER;
7
- }(ADDITIONAL_IDENTIFIER || {});
7
+ }({});
8
8
  export class CacheService {
9
9
  /**
10
10
  * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}
11
11
  */
12
12
  cache;
13
+ generateKey;
14
+ customFetcher;
13
15
  logger;
14
- constructor(server, cacheName) {
16
+ constructor({
17
+ server,
18
+ cacheName,
19
+ options
20
+ }) {
21
+ const {
22
+ keyGenerator,
23
+ sessionHydrator
24
+ } = options ?? {};
15
25
  if (!cacheName) {
16
26
  server.log('warn', 'You are using the default hapi cache. Please provide a cache name in plugin registration options.');
17
27
  }
28
+ this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this);
29
+ this.customFetcher = sessionHydrator ?? undefined;
18
30
  this.cache = server.cache({
19
31
  cache: cacheName,
20
32
  segment: 'formSubmission'
@@ -22,7 +34,16 @@ export class CacheService {
22
34
  this.logger = server.logger;
23
35
  }
24
36
  async getState(request) {
25
- const cached = await this.cache.get(this.Key(request));
37
+ let cached = await this.cache.get(this.Key(request));
38
+
39
+ // If nothing in Redis, attempt to rehydrate from backend DB
40
+ if (!cached && this.customFetcher) {
41
+ const rehydrated = await this.customFetcher(request);
42
+ if (rehydrated != null) {
43
+ await this.cache.set(this.Key(request), rehydrated, config.get('sessionTimeout'));
44
+ cached = await this.getState(request);
45
+ }
46
+ }
26
47
  return cached ?? {};
27
48
  }
28
49
  async setState(request, state) {
@@ -57,6 +78,14 @@ export class CacheService {
57
78
  const key = this.Key(request);
58
79
  request.yar.flash(key.id, message);
59
80
  }
81
+ defaultKeyGenerator(request) {
82
+ if (!request.yar.id) {
83
+ throw new Error('No session ID found');
84
+ }
85
+ const state = request.params.state ?? '';
86
+ const slug = request.params.slug ?? '';
87
+ return `${request.yar.id}:${state}:${slug}:`;
88
+ }
60
89
 
61
90
  /**
62
91
  * The key used to store user session data against.
@@ -65,12 +94,10 @@ export class CacheService {
65
94
  * @param additionalIdentifier - appended to the id
66
95
  */
67
96
  Key(request, additionalIdentifier) {
68
- if (!request.yar.id) {
69
- throw Error('No session ID found');
70
- }
97
+ const baseKey = this.generateKey ? this.generateKey(request) : this.defaultKeyGenerator(request);
71
98
  return {
72
99
  segment: partition,
73
- id: `${request.yar.id}:${request.params.state ?? ''}:${request.params.slug ?? ''}:${additionalIdentifier ?? ''}`
100
+ id: `${baseKey}${additionalIdentifier ?? ''}`
74
101
  };
75
102
  }
76
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cacheService.js","names":["Hoek","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","logger","constructor","server","cacheName","log","segment","getState","request","cached","get","Key","setState","state","key","ttl","set","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","additionalIdentifier","Error","params","slug","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Request, type Server } from '@hapi/hapi'\nimport * as Hoek from '@hapi/hoek'\n\nimport { config } from '~/src/config/index.js'\nimport { type createServer } from '~/src/server/index.js'\nimport {\n type FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\n\nconst partition = 'cache'\n\nenum ADDITIONAL_IDENTIFIER {\n Confirmation = ':confirmation'\n}\n\nexport class CacheService {\n /**\n * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}\n */\n cache\n logger: Server['logger']\n\n constructor(server: Server, cacheName?: string) {\n if (!cacheName) {\n server.log(\n 'warn',\n 'You are using the default hapi cache. Please provide a cache name in plugin registration options.'\n )\n }\n\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(\n request: Request | FormRequest | FormRequestPayload\n ): Promise<FormSubmissionState> {\n const cached = await this.cache.get(this.Key(request))\n\n return cached ?? {}\n }\n\n async setState(\n request: FormRequest | FormRequestPayload,\n state: FormSubmissionState\n ) {\n const key = this.Key(request)\n const ttl = config.get('sessionTimeout')\n\n await this.cache.set(key, state, ttl)\n return this.getState(request)\n }\n\n async getConfirmationState(\n request: FormRequest | FormRequestPayload\n ): Promise<{ confirmed?: true }> {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const value = await this.cache.get(key)\n\n return value ?? {}\n }\n\n async setConfirmationState(\n request: FormRequest | FormRequestPayload,\n confirmationState: { confirmed?: true }\n ) {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const ttl = config.get('confirmationSessionTimeout')\n\n return this.cache.set(key, confirmationState, ttl)\n }\n\n async clearState(request: FormRequest | FormRequestPayload) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: FormRequest | FormRequestPayload\n ): { errors: FormSubmissionError[] } | undefined {\n const key = this.Key(request)\n const messages = request.yar.flash(key.id)\n\n if (Array.isArray(messages) && messages.length) {\n return messages.at(0) as { errors: FormSubmissionError[] }\n }\n }\n\n setFlash(\n request: FormRequest | FormRequestPayload,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n /**\n * The key used to store user session data against.\n * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`\n * @param request - hapi request object\n * @param additionalIdentifier - appended to the id\n */\n Key(\n request: Request | FormRequest | FormRequestPayload,\n additionalIdentifier?: ADDITIONAL_IDENTIFIER\n ) {\n if (!request.yar.id) {\n throw Error('No session ID found')\n }\n return {\n segment: partition,\n id: `${request.yar.id}:${request.params.state ?? ''}:${request.params.slug ?? ''}:${additionalIdentifier ?? ''}`\n }\n }\n}\n\n/**\n * State merge helper\n * 1. Merges objects (form fields)\n * 2. Overwrites arrays\n */\nexport function merge<StateType extends FormState | FormPayload>(\n state: StateType,\n update: object\n): StateType {\n return Hoek.merge(state, update, {\n mergeArrays: false\n })\n}\n"],"mappings":"AACA,OAAO,KAAKA,IAAI,MAAM,YAAY;AAElC,SAASC,MAAM;AAaf,MAAMC,SAAS,GAAG,OAAO;AAAA,IAEpBC,qBAAqB,0BAArBA,qBAAqB;EAArBA,qBAAqB;EAAA,OAArBA,qBAAqB;AAAA,EAArBA,qBAAqB;AAI1B,OAAO,MAAMC,YAAY,CAAC;EACxB;AACF;AACA;EACEC,KAAK;EACLC,MAAM;EAENC,WAAWA,CAACC,MAAc,EAAEC,SAAkB,EAAE;IAC9C,IAAI,CAACA,SAAS,EAAE;MACdD,MAAM,CAACE,GAAG,CACR,MAAM,EACN,mGACF,CAAC;IACH;IAEA,IAAI,CAACL,KAAK,GAAGG,MAAM,CAACH,KAAK,CAAC;MAAEA,KAAK,EAAEI,SAAS;MAAEE,OAAO,EAAE;IAAiB,CAAC,CAAC;IAC1E,IAAI,CAACL,MAAM,GAAGE,MAAM,CAACF,MAAM;EAC7B;EAEA,MAAMM,QAAQA,CACZC,OAAmD,EACrB;IAC9B,MAAMC,MAAM,GAAG,MAAM,IAAI,CAACT,KAAK,CAACU,GAAG,CAAC,IAAI,CAACC,GAAG,CAACH,OAAO,CAAC,CAAC;IAEtD,OAAOC,MAAM,IAAI,CAAC,CAAC;EACrB;EAEA,MAAMG,QAAQA,CACZJ,OAAyC,EACzCK,KAA0B,EAC1B;IACA,MAAMC,GAAG,GAAG,IAAI,CAACH,GAAG,CAACH,OAAO,CAAC;IAC7B,MAAMO,GAAG,GAAGnB,MAAM,CAACc,GAAG,CAAC,gBAAgB,CAAC;IAExC,MAAM,IAAI,CAACV,KAAK,CAACgB,GAAG,CAACF,GAAG,EAAED,KAAK,EAAEE,GAAG,CAAC;IACrC,OAAO,IAAI,CAACR,QAAQ,CAACC,OAAO,CAAC;EAC/B;EAEA,MAAMS,oBAAoBA,CACxBT,OAAyC,EACV;IAC/B,MAAMM,GAAG,GAAG,IAAI,CAACH,GAAG,CAACH,OAAO,EAAEV,qBAAqB,CAACoB,YAAY,CAAC;IACjE,MAAMC,KAAK,GAAG,MAAM,IAAI,CAACnB,KAAK,CAACU,GAAG,CAACI,GAAG,CAAC;IAEvC,OAAOK,KAAK,IAAI,CAAC,CAAC;EACpB;EAEA,MAAMC,oBAAoBA,CACxBZ,OAAyC,EACzCa,iBAAuC,EACvC;IACA,MAAMP,GAAG,GAAG,IAAI,CAACH,GAAG,CAACH,OAAO,EAAEV,qBAAqB,CAACoB,YAAY,CAAC;IACjE,MAAMH,GAAG,GAAGnB,MAAM,CAACc,GAAG,CAAC,4BAA4B,CAAC;IAEpD,OAAO,IAAI,CAACV,KAAK,CAACgB,GAAG,CAACF,GAAG,EAAEO,iBAAiB,EAAEN,GAAG,CAAC;EACpD;EAEA,MAAMO,UAAUA,CAACd,OAAyC,EAAE;IAC1D,IAAIA,OAAO,CAACe,GAAG,CAACC,EAAE,EAAE;MAClB,MAAM,IAAI,CAACxB,KAAK,CAACyB,IAAI,CAAC,IAAI,CAACd,GAAG,CAACH,OAAO,CAAC,CAAC;IAC1C;EACF;EAEAkB,QAAQA,CACNlB,OAAyC,EACM;IAC/C,MAAMM,GAAG,GAAG,IAAI,CAACH,GAAG,CAACH,OAAO,CAAC;IAC7B,MAAMmB,QAAQ,GAAGnB,OAAO,CAACe,GAAG,CAACK,KAAK,CAACd,GAAG,CAACU,EAAE,CAAC;IAE1C,IAAIK,KAAK,CAACC,OAAO,CAACH,QAAQ,CAAC,IAAIA,QAAQ,CAACI,MAAM,EAAE;MAC9C,OAAOJ,QAAQ,CAACK,EAAE,CAAC,CAAC,CAAC;IACvB;EACF;EAEAC,QAAQA,CACNzB,OAAyC,EACzC0B,OAA0C,EAC1C;IACA,MAAMpB,GAAG,GAAG,IAAI,CAACH,GAAG,CAACH,OAAO,CAAC;IAE7BA,OAAO,CAACe,GAAG,CAACK,KAAK,CAACd,GAAG,CAACU,EAAE,EAAEU,OAAO,CAAC;EACpC;;EAEA;AACF;AACA;AACA;AACA;AACA;EACEvB,GAAGA,CACDH,OAAmD,EACnD2B,oBAA4C,EAC5C;IACA,IAAI,CAAC3B,OAAO,CAACe,GAAG,CAACC,EAAE,EAAE;MACnB,MAAMY,KAAK,CAAC,qBAAqB,CAAC;IACpC;IACA,OAAO;MACL9B,OAAO,EAAET,SAAS;MAClB2B,EAAE,EAAE,GAAGhB,OAAO,CAACe,GAAG,CAACC,EAAE,IAAIhB,OAAO,CAAC6B,MAAM,CAACxB,KAAK,IAAI,EAAE,IAAIL,OAAO,CAAC6B,MAAM,CAACC,IAAI,IAAI,EAAE,IAAIH,oBAAoB,IAAI,EAAE;IAChH,CAAC;EACH;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASI,KAAKA,CACnB1B,KAAgB,EAChB2B,MAAc,EACH;EACX,OAAO7C,IAAI,CAAC4C,KAAK,CAAC1B,KAAK,EAAE2B,MAAM,EAAE;IAC/BC,WAAW,EAAE;EACf,CAAC,CAAC;AACJ","ignoreList":[]}
1
+ {"version":3,"file":"cacheService.js","names":["Hoek","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","generateKey","customFetcher","logger","constructor","server","cacheName","options","keyGenerator","sessionHydrator","log","defaultKeyGenerator","bind","undefined","segment","getState","request","cached","get","Key","rehydrated","set","setState","state","key","ttl","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","Error","params","slug","additionalIdentifier","baseKey","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Request, type Server } from '@hapi/hapi'\nimport * as Hoek from '@hapi/hoek'\n\nimport { config } from '~/src/config/index.js'\nimport { type createServer } from '~/src/server/index.js'\nimport {\n type FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\n\nconst partition = 'cache'\n\nexport enum ADDITIONAL_IDENTIFIER {\n Confirmation = ':confirmation'\n}\n\nexport class CacheService {\n /**\n * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}\n */\n cache\n generateKey?: (request: Request | FormRequest | FormRequestPayload) => string\n customFetcher?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n\n logger: Server['logger']\n\n constructor({\n server,\n cacheName,\n options\n }: {\n server: Server\n cacheName?: string\n options?: {\n keyGenerator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n }\n }) {\n const { keyGenerator, sessionHydrator } = options ?? {}\n if (!cacheName) {\n server.log(\n 'warn',\n 'You are using the default hapi cache. Please provide a cache name in plugin registration options.'\n )\n }\n this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this)\n this.customFetcher = sessionHydrator ?? undefined\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(\n request: Request | FormRequest | FormRequestPayload\n ): Promise<FormSubmissionState> {\n let cached = await this.cache.get(this.Key(request))\n\n // If nothing in Redis, attempt to rehydrate from backend DB\n if (!cached && this.customFetcher) {\n const rehydrated = await this.customFetcher(request)\n\n if (rehydrated != null) {\n await this.cache.set(\n this.Key(request),\n rehydrated,\n config.get('sessionTimeout')\n )\n cached = await this.getState(request)\n }\n }\n\n return cached ?? {}\n }\n\n async setState(\n request: FormRequest | FormRequestPayload,\n state: FormSubmissionState\n ) {\n const key = this.Key(request)\n const ttl = config.get('sessionTimeout')\n\n await this.cache.set(key, state, ttl)\n return this.getState(request)\n }\n\n async getConfirmationState(\n request: FormRequest | FormRequestPayload\n ): Promise<{ confirmed?: true }> {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const value = await this.cache.get(key)\n\n return value ?? {}\n }\n\n async setConfirmationState(\n request: FormRequest | FormRequestPayload,\n confirmationState: { confirmed?: true }\n ) {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const ttl = config.get('confirmationSessionTimeout')\n\n return this.cache.set(key, confirmationState, ttl)\n }\n\n async clearState(request: FormRequest | FormRequestPayload) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: FormRequest | FormRequestPayload\n ): { errors: FormSubmissionError[] } | undefined {\n const key = this.Key(request)\n const messages = request.yar.flash(key.id)\n\n if (Array.isArray(messages) && messages.length) {\n return messages.at(0) as { errors: FormSubmissionError[] }\n }\n }\n\n setFlash(\n request: FormRequest | FormRequestPayload,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n private defaultKeyGenerator(\n request: Request | FormRequest | FormRequestPayload\n ): string {\n if (!request.yar.id) {\n throw new Error('No session ID found')\n }\n\n const state = request.params.state ?? ''\n const slug = request.params.slug ?? ''\n return `${request.yar.id}:${state}:${slug}:`\n }\n\n /**\n * The key used to store user session data against.\n * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`\n * @param request - hapi request object\n * @param additionalIdentifier - appended to the id\n */\n Key(\n request: Request | FormRequest | FormRequestPayload,\n additionalIdentifier?: ADDITIONAL_IDENTIFIER\n ) {\n const baseKey = this.generateKey\n ? this.generateKey(request)\n : this.defaultKeyGenerator(request)\n\n return {\n segment: partition,\n id: `${baseKey}${additionalIdentifier ?? ''}`\n }\n }\n}\n\n/**\n * State merge helper\n * 1. Merges objects (form fields)\n * 2. Overwrites arrays\n */\nexport function merge<StateType extends FormState | FormPayload>(\n state: StateType,\n update: object\n): StateType {\n return Hoek.merge(state, update, {\n mergeArrays: false\n })\n}\n"],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "repository": {
57
57
  "type": "git",
58
- "url": "https://github.com/DEFRA/forms-engine-plugin"
58
+ "url": "git+https://github.com/DEFRA/forms-engine-plugin.git"
59
59
  },
60
60
  "engines": {
61
61
  "node": "^22.11.0",
@@ -106,7 +106,7 @@
106
106
  "pino-pretty": "^13.0.0",
107
107
  "proxy-agent": "^6.5.0",
108
108
  "resolve": "^1.22.10",
109
- "yaml": "^2.7.1"
109
+ "yaml": "^2.8.0"
110
110
  },
111
111
  "devDependencies": {
112
112
  "@babel/cli": "^7.28.0",
@@ -85,3 +85,59 @@ There are a number of `LiquidJS` filters available to you from within the templa
85
85
  }
86
86
  ]
87
87
  ```
88
+
89
+ ## Session Rehydration
90
+
91
+ To support Save and Return functionality, this application now supports session rehydration. This allows user session state to be recovered across browser sessions or devices — even after the in-memory Redis session has expired.
92
+
93
+ ### How it works
94
+
95
+ To support session rehydration from a backend (e.g. for Save & Return), the consuming application must provide two functions when registering the DXT engine plugin:
96
+
97
+ ```
98
+ export interface PluginOptions {
99
+ ...
100
+ keyGenerator?: (request) => string
101
+ sessionHydrator?: (request) => Promise<FormSubmissionState | null>
102
+ ...
103
+ }
104
+
105
+ ```
106
+
107
+ 1. `keyGenerator(request)`
108
+
109
+ This generates a stable and consistent cache key used to store and retrieve user state. It should return a string based on persistent identifiers such as userId, businessId, and grantId — i.e., something like:
110
+
111
+ ```
112
+ const keyGenerator = request => {
113
+ const { userId, businessId, grantId } = request.app.userContext
114
+ return `${userId}:${businessId}:${grantId}`
115
+ }
116
+ ```
117
+
118
+ 2. `sessionHydrator(request, key)`
119
+
120
+ This function is called when no session state is found in Redis. It should fetch saved state (e.g., from an API) using the provided key and return it in the same structure expected by the form engine:
121
+
122
+ ```
123
+ const sessionHydrator = async (request, key) => {
124
+ const response = await fetch(`https://backend.api/state/${key}`)
125
+ if (!response.ok) return null
126
+ return await response.json() // Must match form engine state shape
127
+ }
128
+ ```
129
+
130
+ #### Session flow
131
+
132
+ - When user resumes a journey and Redis session data is missing or expired, DXT will use `keyGenerator` and `sessionHydrator` to fetch the saved state from an external API (e.g. `/state` endpoint).
133
+ - The fetched state is written back into Redis and used to continue the user journey.
134
+ - The rehydrated state must include enough information to satisfy schema validation on the current or next page.
135
+ - To properly resume a session, users should be redirected to the `/summary` page. This ensures the UI has all required answers preloaded and avoids invalid transitions from deep links.
136
+
137
+ ### Additional notes
138
+
139
+ Flash messaging and other ephemeral session data still rely on yar.id.
140
+
141
+ If the restored state does not satisfy the schema for the current page, the user will be redirected to the first incomplete step.
142
+
143
+ In development, a mock identity and /state response can be used to simulate a persisted session.
@@ -11,6 +11,7 @@ import Boom from '@hapi/boom'
11
11
  import {
12
12
  type Plugin,
13
13
  type PluginProperties,
14
+ type Request,
14
15
  type ResponseObject,
15
16
  type ResponseToolkit,
16
17
  type RouteOptions,
@@ -54,7 +55,8 @@ import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
54
55
  import { getUploadStatus } from '~/src/server/plugins/engine/services/uploadService.js'
55
56
  import {
56
57
  type FilterFunction,
57
- type FormContext
58
+ type FormContext,
59
+ type FormSubmissionState
58
60
  } from '~/src/server/plugins/engine/types.js'
59
61
  import {
60
62
  type FormRequest,
@@ -93,6 +95,10 @@ export interface PluginOptions {
93
95
  services?: Services
94
96
  controllers?: Record<string, typeof PageController>
95
97
  cacheName?: string
98
+ keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string
99
+ sessionHydrator?: (
100
+ request: Request | FormRequest | FormRequestPayload
101
+ ) => Promise<FormSubmissionState>
96
102
  filters?: Record<string, FilterFunction>
97
103
  pluginPath?: string
98
104
  nunjucks: {
@@ -114,12 +120,21 @@ export const plugin = {
114
120
  services = defaultServices,
115
121
  controllers,
116
122
  cacheName,
123
+ keyGenerator,
124
+ sessionHydrator,
117
125
  filters,
118
126
  nunjucks: nunjucksOptions,
119
127
  viewContext
120
128
  } = options
121
129
  const { formsService } = services
122
- const cacheService = new CacheService(server, cacheName)
130
+ const cacheService = new CacheService({
131
+ server,
132
+ cacheName,
133
+ options: {
134
+ keyGenerator,
135
+ sessionHydrator
136
+ }
137
+ })
123
138
 
124
139
  const packageRoot = findPackageRoot()
125
140
  const govukFrontendPath = dirname(
@@ -2,7 +2,11 @@ import { type Request, type Server } from '@hapi/hapi'
2
2
 
3
3
  import { config } from '~/src/config/index.js'
4
4
  import { type FormRequest } from '~/src/server/routes/types.js'
5
- import { CacheService, merge } from '~/src/server/services/cacheService.js'
5
+ import {
6
+ ADDITIONAL_IDENTIFIER,
7
+ CacheService,
8
+ merge
9
+ } from '~/src/server/services/cacheService.js'
6
10
 
7
11
  describe('CacheService', () => {
8
12
  let mockServer: Partial<Server>
@@ -31,7 +35,10 @@ describe('CacheService', () => {
31
35
  } as unknown as Server['logger']
32
36
  }
33
37
 
34
- cacheService = new CacheService(mockServer as Server)
38
+ cacheService = new CacheService({
39
+ server: mockServer as Server,
40
+ cacheName: 'test-cache'
41
+ })
35
42
  })
36
43
 
37
44
  describe('getState', () => {
@@ -69,6 +76,40 @@ describe('CacheService', () => {
69
76
  expect(result).toEqual({})
70
77
  })
71
78
  })
79
+
80
+ it('should rehydrate state using custom fetcher when cache is missed', async () => {
81
+ const rehydratedState = { rehydrated: true }
82
+
83
+ const customFetcher = jest.fn().mockResolvedValue(rehydratedState)
84
+
85
+ cacheService = new CacheService({
86
+ server: mockServer as Server,
87
+ cacheName: 'test-cache',
88
+ options: { sessionHydrator: customFetcher }
89
+ })
90
+
91
+ const mockRequest = {
92
+ yar: { id: 'session-id' },
93
+ params: { state: 's', slug: 'p' }
94
+ } as unknown as FormRequest
95
+
96
+ mockCache.get
97
+ .mockResolvedValueOnce(null)
98
+ .mockResolvedValueOnce(rehydratedState)
99
+
100
+ const result = await cacheService.getState(mockRequest)
101
+
102
+ expect(customFetcher).toHaveBeenCalledWith(mockRequest)
103
+ expect(mockCache.set).toHaveBeenCalledWith(
104
+ expect.objectContaining({
105
+ segment: 'cache',
106
+ id: expect.stringContaining('session-id')
107
+ }),
108
+ rehydratedState,
109
+ config.get('sessionTimeout')
110
+ )
111
+ expect(result).toEqual(rehydratedState)
112
+ })
72
113
  })
73
114
 
74
115
  describe('setState', () => {
@@ -108,6 +149,61 @@ describe('CacheService', () => {
108
149
  )
109
150
  })
110
151
  })
152
+
153
+ it('should use custom key generator if provided', async () => {
154
+ const customKey = 'my-custom-key'
155
+ const customKeyGenerator = jest.fn().mockReturnValue(customKey)
156
+
157
+ cacheService = new CacheService({
158
+ server: mockServer as Server,
159
+ cacheName: 'test-cache',
160
+ options: { keyGenerator: customKeyGenerator }
161
+ })
162
+
163
+ const mockRequest = {
164
+ yar: { id: 'some-session' },
165
+ params: { state: 'form1', slug: 'page1' }
166
+ } as unknown as FormRequest
167
+
168
+ await cacheService.setState(mockRequest, { test: 'value' })
169
+
170
+ expect(mockCache.set).toHaveBeenCalledWith(
171
+ {
172
+ segment: 'cache',
173
+ id: 'my-custom-key'
174
+ },
175
+ { test: 'value' },
176
+ expect.any(Number)
177
+ )
178
+ })
179
+
180
+ it('should append additionalIdentifier to custom key', () => {
181
+ const customKey = 'custom:key:base:'
182
+ const customKeyGenerator = jest.fn().mockReturnValue(customKey)
183
+
184
+ cacheService = new CacheService({
185
+ server: mockServer as Server,
186
+ cacheName: 'test-cache',
187
+ options: { keyGenerator: customKeyGenerator }
188
+ })
189
+
190
+ const mockRequest = {
191
+ yar: { id: 'session-id' },
192
+ params: { state: 'formA', slug: 'step1' }
193
+ } as unknown as FormRequest
194
+
195
+ const result = cacheService.Key(
196
+ mockRequest,
197
+ ADDITIONAL_IDENTIFIER.Confirmation
198
+ )
199
+
200
+ expect(result).toEqual({
201
+ segment: 'cache',
202
+ id: 'custom:key:base::confirmation'
203
+ })
204
+
205
+ expect(customKeyGenerator).toHaveBeenCalledWith(mockRequest)
206
+ })
111
207
  })
112
208
 
113
209
  describe('merge', () => {
@@ -16,7 +16,7 @@ import {
16
16
 
17
17
  const partition = 'cache'
18
18
 
19
- enum ADDITIONAL_IDENTIFIER {
19
+ export enum ADDITIONAL_IDENTIFIER {
20
20
  Confirmation = ':confirmation'
21
21
  }
22
22
 
@@ -25,16 +25,38 @@ export class CacheService {
25
25
  * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}
26
26
  */
27
27
  cache
28
+ generateKey?: (request: Request | FormRequest | FormRequestPayload) => string
29
+ customFetcher?: (
30
+ request: Request | FormRequest | FormRequestPayload
31
+ ) => Promise<FormSubmissionState | null>
32
+
28
33
  logger: Server['logger']
29
34
 
30
- constructor(server: Server, cacheName?: string) {
35
+ constructor({
36
+ server,
37
+ cacheName,
38
+ options
39
+ }: {
40
+ server: Server
41
+ cacheName?: string
42
+ options?: {
43
+ keyGenerator?: (
44
+ request: Request | FormRequest | FormRequestPayload
45
+ ) => string
46
+ sessionHydrator?: (
47
+ request: Request | FormRequest | FormRequestPayload
48
+ ) => Promise<FormSubmissionState | null>
49
+ }
50
+ }) {
51
+ const { keyGenerator, sessionHydrator } = options ?? {}
31
52
  if (!cacheName) {
32
53
  server.log(
33
54
  'warn',
34
55
  'You are using the default hapi cache. Please provide a cache name in plugin registration options.'
35
56
  )
36
57
  }
37
-
58
+ this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this)
59
+ this.customFetcher = sessionHydrator ?? undefined
38
60
  this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })
39
61
  this.logger = server.logger
40
62
  }
@@ -42,7 +64,21 @@ export class CacheService {
42
64
  async getState(
43
65
  request: Request | FormRequest | FormRequestPayload
44
66
  ): Promise<FormSubmissionState> {
45
- const cached = await this.cache.get(this.Key(request))
67
+ let cached = await this.cache.get(this.Key(request))
68
+
69
+ // If nothing in Redis, attempt to rehydrate from backend DB
70
+ if (!cached && this.customFetcher) {
71
+ const rehydrated = await this.customFetcher(request)
72
+
73
+ if (rehydrated != null) {
74
+ await this.cache.set(
75
+ this.Key(request),
76
+ rehydrated,
77
+ config.get('sessionTimeout')
78
+ )
79
+ cached = await this.getState(request)
80
+ }
81
+ }
46
82
 
47
83
  return cached ?? {}
48
84
  }
@@ -103,6 +139,18 @@ export class CacheService {
103
139
  request.yar.flash(key.id, message)
104
140
  }
105
141
 
142
+ private defaultKeyGenerator(
143
+ request: Request | FormRequest | FormRequestPayload
144
+ ): string {
145
+ if (!request.yar.id) {
146
+ throw new Error('No session ID found')
147
+ }
148
+
149
+ const state = request.params.state ?? ''
150
+ const slug = request.params.slug ?? ''
151
+ return `${request.yar.id}:${state}:${slug}:`
152
+ }
153
+
106
154
  /**
107
155
  * The key used to store user session data against.
108
156
  * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`
@@ -113,12 +161,13 @@ export class CacheService {
113
161
  request: Request | FormRequest | FormRequestPayload,
114
162
  additionalIdentifier?: ADDITIONAL_IDENTIFIER
115
163
  ) {
116
- if (!request.yar.id) {
117
- throw Error('No session ID found')
118
- }
164
+ const baseKey = this.generateKey
165
+ ? this.generateKey(request)
166
+ : this.defaultKeyGenerator(request)
167
+
119
168
  return {
120
169
  segment: partition,
121
- id: `${request.yar.id}:${request.params.state ?? ''}:${request.params.slug ?? ''}:${additionalIdentifier ?? ''}`
170
+ id: `${baseKey}${additionalIdentifier ?? ''}`
122
171
  }
123
172
  }
124
173
  }