@defra/forms-engine-plugin 4.5.0 → 4.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.server/config/index.js +1 -1
  2. package/.server/config/index.js.map +1 -1
  3. package/.server/server/constants.d.ts +1 -0
  4. package/.server/server/constants.js +1 -0
  5. package/.server/server/constants.js.map +1 -1
  6. package/.server/server/plugins/engine/beta/form-context.d.ts +0 -1
  7. package/.server/server/plugins/engine/beta/form-context.js +4 -3
  8. package/.server/server/plugins/engine/beta/form-context.js.map +1 -1
  9. package/.server/server/plugins/engine/components/FileUploadField.js +2 -0
  10. package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
  11. package/.server/server/plugins/engine/helpers.d.ts +10 -8
  12. package/.server/server/plugins/engine/helpers.js +8 -23
  13. package/.server/server/plugins/engine/helpers.js.map +1 -1
  14. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +2 -1
  15. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
  16. package/.server/server/plugins/engine/routes/questions.js +2 -1
  17. package/.server/server/plugins/engine/routes/questions.js.map +1 -1
  18. package/.server/server/plugins/nunjucks/context.js +1 -2
  19. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  20. package/.server/server/plugins/nunjucks/context.test.js +0 -36
  21. package/.server/server/plugins/nunjucks/context.test.js.map +1 -1
  22. package/package.json +3 -3
  23. package/src/config/index.ts +1 -1
  24. package/src/server/constants.js +1 -0
  25. package/src/server/plugins/engine/beta/form-context.test.ts +22 -8
  26. package/src/server/plugins/engine/beta/form-context.ts +7 -6
  27. package/src/server/plugins/engine/components/FileUploadField.test.ts +21 -0
  28. package/src/server/plugins/engine/components/FileUploadField.ts +1 -0
  29. package/src/server/plugins/engine/helpers.test.ts +0 -74
  30. package/src/server/plugins/engine/helpers.ts +17 -27
  31. package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +54 -0
  32. package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +7 -5
  33. package/src/server/plugins/engine/routes/questions.ts +1 -1
  34. package/src/server/plugins/nunjucks/context.js +1 -3
  35. package/src/server/plugins/nunjucks/context.test.js +0 -37
  36. package/src/server/routes/dummy-api.test.ts +3 -1
@@ -24,7 +24,8 @@ async function handleHttpEvent(request, page, context, event, model, preparePage
24
24
  // @ts-expect-error - function signature will be refactored in the next iteration of the formatter
25
25
  const payload = format(context, items, model, undefined, undefined);
26
26
  const opts = {
27
- payload
27
+ payload,
28
+ timeout: 5000
28
29
  };
29
30
  if (preparePageEventRequestOptions) {
30
31
  preparePageEventRequestOptions(opts, event, page, context);
@@ -1 +1 @@
1
- {"version":3,"file":"questions.js","names":["hasFormComponents","slugSchema","Boom","Joi","normalisePath","proceed","redirectPath","SummaryViewModel","format","getFormSubmissionData","dispatchHandler","redirectOrMakeHandler","actionSchema","crumbSchema","itemIdSchema","pathSchema","stateSchema","httpService","handleHttpEvent","request","page","context","event","model","preparePageEventRequestOptions","options","url","viewModel","items","details","payload","undefined","opts","response","postJson","Object","assign","data","makeGetHandler","onRequest","getHandler","h","params","path","events","app","notFound","onLoad","type","makeGetRouteHandler","makePostHandler","postHandler","query","pageDef","isForceAccess","href","makePostRouteHandler","onSave","isSuccessful","statusCode","isBoom","getRoutes","getRouteOptions","postRouteOptions","method","handler","validate","object","keys","slug","state","itemId","optional","crumb","action","unknown","required"],"sources":["../../../../../src/server/plugins/engine/routes/questions.ts"],"sourcesContent":["import { hasFormComponents, slugSchema, type Event } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport {\n type ResponseObject,\n type RouteOptions,\n type ServerRoute\n} from '@hapi/hapi'\nimport Joi from 'joi'\n\nimport {\n normalisePath,\n proceed,\n redirectPath\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n SummaryViewModel,\n type FormModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport { format } from '~/src/server/plugins/engine/outputFormatters/machine/v1.js'\nimport { getFormSubmissionData } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n dispatchHandler,\n redirectOrMakeHandler\n} from '~/src/server/plugins/engine/routes/index.js'\nimport {\n type AnyFormRequest,\n type FormContext,\n type OnRequestCallback,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormRequestRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\nimport {\n actionSchema,\n crumbSchema,\n itemIdSchema,\n pathSchema,\n stateSchema\n} from '~/src/server/schemas/index.js'\nimport * as httpService from '~/src/server/services/httpService.js'\n\nasync function handleHttpEvent(\n request: AnyFormRequest,\n page: PageControllerClass,\n context: FormContext,\n event: Event,\n model: FormModel,\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n) {\n const { options } = event\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(viewModel.context, viewModel.details)\n\n // @ts-expect-error - function signature will be refactored in the next iteration of the formatter\n const payload = format(context, items, model, undefined, undefined)\n const opts = { payload }\n\n if (preparePageEventRequestOptions) {\n preparePageEventRequestOptions(opts, event, page, context)\n }\n\n const { payload: response } = await httpService.postJson(url, opts)\n\n Object.assign(context.data, response)\n}\n\nexport function makeGetHandler(\n preparePageEventRequestOptions?: PreparePageEventRequestOptions,\n onRequest?: OnRequestCallback\n) {\n return function getHandler(request: FormRequest, h: FormResponseToolkit) {\n const { params } = request\n\n if (normalisePath(params.path) === '') {\n return dispatchHandler(request, h)\n }\n\n return redirectOrMakeHandler(\n request,\n h,\n onRequest,\n 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?.type === 'http') {\n await handleHttpEvent(\n request,\n page,\n context,\n events.onLoad,\n model,\n preparePageEventRequestOptions\n )\n }\n\n return page.makeGetRouteHandler()(request, context, h)\n }\n )\n }\n}\n\nexport function makePostHandler(\n preparePageEventRequestOptions?: PreparePageEventRequestOptions,\n onRequest?: OnRequestCallback\n) {\n return function postHandler(\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const { query } = request\n\n return redirectOrMakeHandler(\n request,\n h,\n onRequest,\n async (page, context) => {\n const { pageDef } = page\n const { isForceAccess } = context\n const { model } = request.app\n const { events } = page\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 if (!model) {\n throw Boom.notFound(`No model found for /${request.params.path}`)\n }\n\n const response = await page.makePostRouteHandler()(request, context, h)\n\n if (events?.onSave?.type === 'http' && isSuccessful(response)) {\n await handleHttpEvent(\n request,\n page,\n context,\n events.onSave,\n model,\n preparePageEventRequestOptions\n )\n }\n\n return response\n }\n )\n }\n}\n\nfunction isSuccessful(response: ResponseObject): boolean {\n const { statusCode } = response\n\n return !Boom.isBoom(response) && statusCode >= 200 && statusCode < 400\n}\n\nexport function getRoutes(\n getRouteOptions: RouteOptions<FormRequestRefs>,\n postRouteOptions: RouteOptions<FormRequestPayloadRefs>,\n preparePageEventRequestOptions?: PreparePageEventRequestOptions,\n onRequest?: OnRequestCallback\n): (ServerRoute<FormRequestRefs> | ServerRoute<FormRequestPayloadRefs>)[] {\n return [\n {\n method: 'get',\n path: '/{slug}',\n handler: makeGetHandler(preparePageEventRequestOptions, onRequest),\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema\n })\n }\n }\n },\n {\n method: 'get',\n path: '/preview/{state}/{slug}',\n handler: dispatchHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema\n })\n }\n }\n },\n {\n method: 'get',\n path: '/{slug}/{path}/{itemId?}',\n handler: makeGetHandler(preparePageEventRequestOptions, onRequest),\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 method: 'get',\n path: '/preview/{state}/{slug}/{path}/{itemId?}',\n handler: makeGetHandler(preparePageEventRequestOptions, onRequest),\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 method: 'post',\n path: '/{slug}/{path}/{itemId?}',\n handler: makePostHandler(preparePageEventRequestOptions, onRequest),\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 method: 'post',\n path: '/preview/{state}/{slug}/{path}/{itemId?}',\n handler: makePostHandler(preparePageEventRequestOptions, onRequest),\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"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"questions.js","names":["hasFormComponents","slugSchema","Boom","Joi","normalisePath","proceed","redirectPath","SummaryViewModel","format","getFormSubmissionData","dispatchHandler","redirectOrMakeHandler","actionSchema","crumbSchema","itemIdSchema","pathSchema","stateSchema","httpService","handleHttpEvent","request","page","context","event","model","preparePageEventRequestOptions","options","url","viewModel","items","details","payload","undefined","opts","timeout","response","postJson","Object","assign","data","makeGetHandler","onRequest","getHandler","h","params","path","events","app","notFound","onLoad","type","makeGetRouteHandler","makePostHandler","postHandler","query","pageDef","isForceAccess","href","makePostRouteHandler","onSave","isSuccessful","statusCode","isBoom","getRoutes","getRouteOptions","postRouteOptions","method","handler","validate","object","keys","slug","state","itemId","optional","crumb","action","unknown","required"],"sources":["../../../../../src/server/plugins/engine/routes/questions.ts"],"sourcesContent":["import { hasFormComponents, slugSchema, type Event } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport {\n type ResponseObject,\n type RouteOptions,\n type ServerRoute\n} from '@hapi/hapi'\nimport Joi from 'joi'\n\nimport {\n normalisePath,\n proceed,\n redirectPath\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n SummaryViewModel,\n type FormModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport { format } from '~/src/server/plugins/engine/outputFormatters/machine/v1.js'\nimport { getFormSubmissionData } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n dispatchHandler,\n redirectOrMakeHandler\n} from '~/src/server/plugins/engine/routes/index.js'\nimport {\n type AnyFormRequest,\n type FormContext,\n type OnRequestCallback,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormRequestRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\nimport {\n actionSchema,\n crumbSchema,\n itemIdSchema,\n pathSchema,\n stateSchema\n} from '~/src/server/schemas/index.js'\nimport * as httpService from '~/src/server/services/httpService.js'\n\nasync function handleHttpEvent(\n request: AnyFormRequest,\n page: PageControllerClass,\n context: FormContext,\n event: Event,\n model: FormModel,\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n) {\n const { options } = event\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(viewModel.context, viewModel.details)\n\n // @ts-expect-error - function signature will be refactored in the next iteration of the formatter\n const payload = format(context, items, model, undefined, undefined)\n const opts: httpService.RequestOptions = { payload, timeout: 5000 }\n\n if (preparePageEventRequestOptions) {\n preparePageEventRequestOptions(opts, event, page, context)\n }\n\n const { payload: response } = await httpService.postJson(url, opts)\n\n Object.assign(context.data, response)\n}\n\nexport function makeGetHandler(\n preparePageEventRequestOptions?: PreparePageEventRequestOptions,\n onRequest?: OnRequestCallback\n) {\n return function getHandler(request: FormRequest, h: FormResponseToolkit) {\n const { params } = request\n\n if (normalisePath(params.path) === '') {\n return dispatchHandler(request, h)\n }\n\n return redirectOrMakeHandler(\n request,\n h,\n onRequest,\n 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?.type === 'http') {\n await handleHttpEvent(\n request,\n page,\n context,\n events.onLoad,\n model,\n preparePageEventRequestOptions\n )\n }\n\n return page.makeGetRouteHandler()(request, context, h)\n }\n )\n }\n}\n\nexport function makePostHandler(\n preparePageEventRequestOptions?: PreparePageEventRequestOptions,\n onRequest?: OnRequestCallback\n) {\n return function postHandler(\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const { query } = request\n\n return redirectOrMakeHandler(\n request,\n h,\n onRequest,\n async (page, context) => {\n const { pageDef } = page\n const { isForceAccess } = context\n const { model } = request.app\n const { events } = page\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 if (!model) {\n throw Boom.notFound(`No model found for /${request.params.path}`)\n }\n\n const response = await page.makePostRouteHandler()(request, context, h)\n\n if (events?.onSave?.type === 'http' && isSuccessful(response)) {\n await handleHttpEvent(\n request,\n page,\n context,\n events.onSave,\n model,\n preparePageEventRequestOptions\n )\n }\n\n return response\n }\n )\n }\n}\n\nfunction isSuccessful(response: ResponseObject): boolean {\n const { statusCode } = response\n\n return !Boom.isBoom(response) && statusCode >= 200 && statusCode < 400\n}\n\nexport function getRoutes(\n getRouteOptions: RouteOptions<FormRequestRefs>,\n postRouteOptions: RouteOptions<FormRequestPayloadRefs>,\n preparePageEventRequestOptions?: PreparePageEventRequestOptions,\n onRequest?: OnRequestCallback\n): (ServerRoute<FormRequestRefs> | ServerRoute<FormRequestPayloadRefs>)[] {\n return [\n {\n method: 'get',\n path: '/{slug}',\n handler: makeGetHandler(preparePageEventRequestOptions, onRequest),\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema\n })\n }\n }\n },\n {\n method: 'get',\n path: '/preview/{state}/{slug}',\n handler: dispatchHandler,\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema\n })\n }\n }\n },\n {\n method: 'get',\n path: '/{slug}/{path}/{itemId?}',\n handler: makeGetHandler(preparePageEventRequestOptions, onRequest),\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 method: 'get',\n path: '/preview/{state}/{slug}/{path}/{itemId?}',\n handler: makeGetHandler(preparePageEventRequestOptions, onRequest),\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 method: 'post',\n path: '/{slug}/{path}/{itemId?}',\n handler: makePostHandler(preparePageEventRequestOptions, onRequest),\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 method: 'post',\n path: '/preview/{state}/{slug}/{path}/{itemId?}',\n handler: makePostHandler(preparePageEventRequestOptions, onRequest),\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"],"mappings":"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","ignoreList":[]}
@@ -4,7 +4,7 @@ import Boom from '@hapi/boom';
4
4
  import { StatusCodes } from 'http-status-codes';
5
5
  import { config } from "../../../config/index.js";
6
6
  import { createLogger } from "../../common/helpers/logging/logger.js";
7
- import { checkFormStatus, encodeUrl, safeGenerateCrumb } from "../engine/helpers.js";
7
+ import { checkFormStatus, encodeUrl } from "../engine/helpers.js";
8
8
  const logger = createLogger();
9
9
 
10
10
  /** @type {Record<string, string> | undefined} */
@@ -43,7 +43,6 @@ export async function context(request) {
43
43
  // take consumers props first so we can override it
44
44
  ...consumerViewContext,
45
45
  baseLayoutPath: pluginStorage.baseLayoutPath,
46
- crumb: safeGenerateCrumb(request),
47
46
  currentPath: `${request.path}${request.url.search}`,
48
47
  previewMode: isPreviewMode ? formState : undefined,
49
48
  slug: isResponseOK ? params?.slug : undefined
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":["readFileSync","basename","join","Boom","StatusCodes","config","createLogger","checkFormStatus","encodeUrl","safeGenerateCrumb","logger","webpackManifest","context","request","params","response","isPreview","isPreviewMode","state","formState","isResponseOK","isBoom","statusCode","OK","pluginStorage","server","plugins","consumerViewContext","Error","baseLayoutPath","viewContext","ctx","crumb","currentPath","path","url","search","previewMode","undefined","slug","devtoolContext","_request","manifestPath","get","JSON","parse","info","cdpEnvironment","designerUrl","feedbackLink","phaseTag","serviceName","serviceVersion","assetPath","getDxtAssetPath","asset"],"sources":["../../../../src/server/plugins/nunjucks/context.js"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport Boom from '@hapi/boom'\nimport { StatusCodes } from 'http-status-codes'\n\nimport { config } from '~/src/config/index.js'\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport {\n checkFormStatus,\n encodeUrl,\n safeGenerateCrumb\n} from '~/src/server/plugins/engine/helpers.js'\n\nconst logger = createLogger()\n\n/** @type {Record<string, string> | undefined} */\nlet webpackManifest\n\n/**\n * @param {AnyFormRequest | null} request\n */\nexport async function context(request) {\n const { params, response } = request ?? {}\n\n const { isPreview: isPreviewMode, state: formState } = checkFormStatus(params)\n\n // Only add the slug in to the context if the response is OK.\n // Footer meta links are not rendered when the slug is missing.\n const isResponseOK =\n !Boom.isBoom(response) && response?.statusCode === StatusCodes.OK\n\n const pluginStorage = request?.server.plugins['forms-engine-plugin']\n\n let consumerViewContext = {}\n\n if (!pluginStorage) {\n throw Error('context called before plugin registered')\n }\n\n if (!pluginStorage.baseLayoutPath) {\n throw Error('Missing baseLayoutPath in plugin.options.nunjucks')\n }\n\n if (typeof pluginStorage.viewContext === 'function') {\n consumerViewContext = await pluginStorage.viewContext(request)\n }\n\n /** @type {ViewContext} */\n const ctx = {\n // take consumers props first so we can override it\n ...consumerViewContext,\n baseLayoutPath: pluginStorage.baseLayoutPath,\n crumb: safeGenerateCrumb(request),\n currentPath: `${request.path}${request.url.search}`,\n previewMode: isPreviewMode ? formState : undefined,\n slug: isResponseOK ? params?.slug : undefined\n }\n\n return ctx\n}\n\n/**\n * Returns the context for the devtool. Consumers won't have access to this.\n * @param {AnyFormRequest | null} _request\n * @returns {Record<string, unknown> & { assetPath: string, getDxtAssetPath: (asset: string) => string }}\n */\nexport function devtoolContext(_request) {\n const manifestPath = join(config.get('publicDir'), 'assets-manifest.json')\n\n if (!webpackManifest) {\n try {\n // eslint-disable-next-line -- Allow JSON type 'any'\n webpackManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))\n } catch {\n logger.info(\n `[webpackManifestMissing] Webpack ${basename(manifestPath)} not found - running without asset manifest`\n )\n }\n }\n\n return {\n config: {\n cdpEnvironment: config.get('cdpEnvironment'),\n designerUrl: config.get('designerUrl'),\n feedbackLink: encodeUrl(config.get('feedbackLink')),\n phaseTag: config.get('phaseTag'),\n serviceName: config.get('serviceName'),\n serviceVersion: config.get('serviceVersion')\n },\n assetPath: '/assets',\n getDxtAssetPath: (asset = '') => {\n return `/${webpackManifest?.[asset] ?? asset}`\n }\n }\n}\n\n/**\n * @import { ViewContext } from '~/src/server/plugins/nunjucks/types.js'\n * @import { AnyFormRequest } from '~/src/server/plugins/engine/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"context.js","names":["readFileSync","basename","join","Boom","StatusCodes","config","createLogger","checkFormStatus","encodeUrl","logger","webpackManifest","context","request","params","response","isPreview","isPreviewMode","state","formState","isResponseOK","isBoom","statusCode","OK","pluginStorage","server","plugins","consumerViewContext","Error","baseLayoutPath","viewContext","ctx","currentPath","path","url","search","previewMode","undefined","slug","devtoolContext","_request","manifestPath","get","JSON","parse","info","cdpEnvironment","designerUrl","feedbackLink","phaseTag","serviceName","serviceVersion","assetPath","getDxtAssetPath","asset"],"sources":["../../../../src/server/plugins/nunjucks/context.js"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport Boom from '@hapi/boom'\nimport { StatusCodes } from 'http-status-codes'\n\nimport { config } from '~/src/config/index.js'\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport {\n checkFormStatus,\n encodeUrl\n} from '~/src/server/plugins/engine/helpers.js'\n\nconst logger = createLogger()\n\n/** @type {Record<string, string> | undefined} */\nlet webpackManifest\n\n/**\n * @param {AnyFormRequest | null} request\n */\nexport async function context(request) {\n const { params, response } = request ?? {}\n\n const { isPreview: isPreviewMode, state: formState } = checkFormStatus(params)\n\n // Only add the slug in to the context if the response is OK.\n // Footer meta links are not rendered when the slug is missing.\n const isResponseOK =\n !Boom.isBoom(response) && response?.statusCode === StatusCodes.OK\n\n const pluginStorage = request?.server.plugins['forms-engine-plugin']\n\n let consumerViewContext = {}\n\n if (!pluginStorage) {\n throw Error('context called before plugin registered')\n }\n\n if (!pluginStorage.baseLayoutPath) {\n throw Error('Missing baseLayoutPath in plugin.options.nunjucks')\n }\n\n if (typeof pluginStorage.viewContext === 'function') {\n consumerViewContext = await pluginStorage.viewContext(request)\n }\n\n /** @type {ViewContext} */\n const ctx = {\n // take consumers props first so we can override it\n ...consumerViewContext,\n baseLayoutPath: pluginStorage.baseLayoutPath,\n currentPath: `${request.path}${request.url.search}`,\n previewMode: isPreviewMode ? formState : undefined,\n slug: isResponseOK ? params?.slug : undefined\n }\n\n return ctx\n}\n\n/**\n * Returns the context for the devtool. Consumers won't have access to this.\n * @param {AnyFormRequest | null} _request\n * @returns {Record<string, unknown> & { assetPath: string, getDxtAssetPath: (asset: string) => string }}\n */\nexport function devtoolContext(_request) {\n const manifestPath = join(config.get('publicDir'), 'assets-manifest.json')\n\n if (!webpackManifest) {\n try {\n // eslint-disable-next-line -- Allow JSON type 'any'\n webpackManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))\n } catch {\n logger.info(\n `[webpackManifestMissing] Webpack ${basename(manifestPath)} not found - running without asset manifest`\n )\n }\n }\n\n return {\n config: {\n cdpEnvironment: config.get('cdpEnvironment'),\n designerUrl: config.get('designerUrl'),\n feedbackLink: encodeUrl(config.get('feedbackLink')),\n phaseTag: config.get('phaseTag'),\n serviceName: config.get('serviceName'),\n serviceVersion: config.get('serviceVersion')\n },\n assetPath: '/assets',\n getDxtAssetPath: (asset = '') => {\n return `/${webpackManifest?.[asset] ?? asset}`\n }\n }\n}\n\n/**\n * @import { ViewContext } from '~/src/server/plugins/nunjucks/types.js'\n * @import { AnyFormRequest } from '~/src/server/plugins/engine/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
@@ -92,42 +92,6 @@ describe('Nunjucks context', () => {
92
92
  expect(crumb).toBeUndefined();
93
93
  expect(malformedRequest.server.plugins.crumb.generate).not.toHaveBeenCalled();
94
94
  });
95
- it('should generate crumb when state exists', async () => {
96
- const mockCrumb = 'generated-crumb-value';
97
- const validRequest = /** @type {FormRequest} */
98
- /** @type {unknown} */{
99
- server: {
100
- plugins: {
101
- crumb: {
102
- generate: jest.fn().mockReturnValue(mockCrumb)
103
- },
104
- 'forms-engine-plugin': {
105
- baseLayoutPath: 'randomValue'
106
- }
107
- }
108
- },
109
- plugins: {},
110
- route: {
111
- settings: {
112
- plugins: {}
113
- }
114
- },
115
- path: '/test',
116
- url: {
117
- search: ''
118
- },
119
- state: {},
120
- yar: {
121
- flash: jest.fn().mockReturnValue([]),
122
- commit: jest.fn()
123
- }
124
- };
125
- const {
126
- crumb
127
- } = await context(validRequest);
128
- expect(crumb).toBe(mockCrumb);
129
- expect(validRequest.server.plugins.crumb.generate).toHaveBeenCalledWith(validRequest);
130
- });
131
95
  });
132
96
  });
133
97
 
@@ -1 +1 @@
1
- {"version":3,"file":"context.test.js","names":["tmpdir","context","devtoolContext","describe","beforeEach","jest","resetModules","it","assetPath","expect","toBe","getDxtAssetPath","isolateModulesAsync","config","set","rejects","toThrow","malformedRequest","server","plugins","crumb","generate","fn","baseLayoutPath","route","settings","path","url","search","yar","flash","mockReturnValue","commit","toBeUndefined","not","toHaveBeenCalled","mockCrumb","validRequest","state","toHaveBeenCalledWith"],"sources":["../../../../src/server/plugins/nunjucks/context.test.js"],"sourcesContent":["import { tmpdir } from 'node:os'\n\nimport {\n context,\n devtoolContext\n} from '~/src/server/plugins/nunjucks/context.js'\n\ndescribe('Nunjucks context', () => {\n beforeEach(() => jest.resetModules())\n\n describe('Asset path', () => {\n it(\"should include 'assetPath' for GOV.UK Frontend icons\", () => {\n const { assetPath } = devtoolContext(null)\n expect(assetPath).toBe('/assets')\n })\n })\n\n describe('Asset helper', () => {\n it(\"should locate 'assets-manifest.json' assets\", () => {\n const { getDxtAssetPath } = devtoolContext(null)\n\n expect(getDxtAssetPath('example.scss')).toBe(\n '/stylesheets/example.xxxxxxx.min.css'\n )\n\n expect(getDxtAssetPath('example.mjs')).toBe(\n '/javascripts/example.xxxxxxx.min.js'\n )\n })\n\n it(\"should return path when 'assets-manifest.json' is missing\", async () => {\n await jest.isolateModulesAsync(async () => {\n const { config } = await import('~/src/config/index.js')\n\n // Import when isolated to avoid cache\n const { devtoolContext } =\n await import('~/src/server/plugins/nunjucks/context.js')\n\n // Update config for missing manifest\n config.set('publicDir', tmpdir())\n const { getDxtAssetPath } = devtoolContext(null)\n\n // Uses original paths when missing\n expect(getDxtAssetPath('example.scss')).toBe('/example.scss')\n expect(getDxtAssetPath('example.mjs')).toBe('/example.mjs')\n })\n })\n\n it('should return path to unknown assets', () => {\n const { getDxtAssetPath } = devtoolContext(null)\n\n expect(getDxtAssetPath('')).toBe('/')\n expect(getDxtAssetPath('example.jpg')).toBe('/example.jpg')\n expect(getDxtAssetPath('example.gif')).toBe('/example.gif')\n })\n })\n\n describe('Config', () => {\n it('should include environment, phase tag and service info', async () => {\n await expect(context(null)).rejects.toThrow(\n 'context called before plugin registered'\n )\n })\n })\n\n describe('Crumb', () => {\n it('should handle malformed requests with missing state', async () => {\n // While state should always exist in a valid Hapi request (it holds cookies),\n // we've seen malformed requests in production where it's missing\n const malformedRequest = /** @type {FormRequest} */ (\n /** @type {unknown} */ ({\n server: {\n plugins: {\n crumb: {\n generate: jest.fn()\n },\n 'forms-engine-plugin': {\n baseLayoutPath: 'randomValue'\n }\n }\n },\n plugins: {},\n route: {\n settings: {\n plugins: {}\n }\n },\n path: '/test',\n url: { search: '' },\n yar: {\n flash: jest.fn().mockReturnValue([]),\n commit: jest.fn()\n }\n // state intentionally omitted to test real malformed requests\n })\n )\n\n const { crumb } = await context(malformedRequest)\n expect(crumb).toBeUndefined()\n expect(\n malformedRequest.server.plugins.crumb.generate\n ).not.toHaveBeenCalled()\n })\n\n it('should generate crumb when state exists', async () => {\n const mockCrumb = 'generated-crumb-value'\n const validRequest = /** @type {FormRequest} */ (\n /** @type {unknown} */ ({\n server: {\n plugins: {\n crumb: {\n generate: jest.fn().mockReturnValue(mockCrumb)\n },\n 'forms-engine-plugin': {\n baseLayoutPath: 'randomValue'\n }\n }\n },\n plugins: {},\n route: {\n settings: {\n plugins: {}\n }\n },\n path: '/test',\n url: { search: '' },\n state: {},\n yar: {\n flash: jest.fn().mockReturnValue([]),\n commit: jest.fn()\n }\n })\n )\n\n const { crumb } = await context(validRequest)\n expect(crumb).toBe(mockCrumb)\n expect(validRequest.server.plugins.crumb.generate).toHaveBeenCalledWith(\n validRequest\n )\n })\n })\n})\n\n/**\n * @import { FormRequest } from '~/src/server/routes/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"context.test.js","names":["tmpdir","context","devtoolContext","describe","beforeEach","jest","resetModules","it","assetPath","expect","toBe","getDxtAssetPath","isolateModulesAsync","config","set","rejects","toThrow","malformedRequest","server","plugins","crumb","generate","fn","baseLayoutPath","route","settings","path","url","search","yar","flash","mockReturnValue","commit","toBeUndefined","not","toHaveBeenCalled"],"sources":["../../../../src/server/plugins/nunjucks/context.test.js"],"sourcesContent":["import { tmpdir } from 'node:os'\n\nimport {\n context,\n devtoolContext\n} from '~/src/server/plugins/nunjucks/context.js'\n\ndescribe('Nunjucks context', () => {\n beforeEach(() => jest.resetModules())\n\n describe('Asset path', () => {\n it(\"should include 'assetPath' for GOV.UK Frontend icons\", () => {\n const { assetPath } = devtoolContext(null)\n expect(assetPath).toBe('/assets')\n })\n })\n\n describe('Asset helper', () => {\n it(\"should locate 'assets-manifest.json' assets\", () => {\n const { getDxtAssetPath } = devtoolContext(null)\n\n expect(getDxtAssetPath('example.scss')).toBe(\n '/stylesheets/example.xxxxxxx.min.css'\n )\n\n expect(getDxtAssetPath('example.mjs')).toBe(\n '/javascripts/example.xxxxxxx.min.js'\n )\n })\n\n it(\"should return path when 'assets-manifest.json' is missing\", async () => {\n await jest.isolateModulesAsync(async () => {\n const { config } = await import('~/src/config/index.js')\n\n // Import when isolated to avoid cache\n const { devtoolContext } =\n await import('~/src/server/plugins/nunjucks/context.js')\n\n // Update config for missing manifest\n config.set('publicDir', tmpdir())\n const { getDxtAssetPath } = devtoolContext(null)\n\n // Uses original paths when missing\n expect(getDxtAssetPath('example.scss')).toBe('/example.scss')\n expect(getDxtAssetPath('example.mjs')).toBe('/example.mjs')\n })\n })\n\n it('should return path to unknown assets', () => {\n const { getDxtAssetPath } = devtoolContext(null)\n\n expect(getDxtAssetPath('')).toBe('/')\n expect(getDxtAssetPath('example.jpg')).toBe('/example.jpg')\n expect(getDxtAssetPath('example.gif')).toBe('/example.gif')\n })\n })\n\n describe('Config', () => {\n it('should include environment, phase tag and service info', async () => {\n await expect(context(null)).rejects.toThrow(\n 'context called before plugin registered'\n )\n })\n })\n\n describe('Crumb', () => {\n it('should handle malformed requests with missing state', async () => {\n // While state should always exist in a valid Hapi request (it holds cookies),\n // we've seen malformed requests in production where it's missing\n const malformedRequest = /** @type {FormRequest} */ (\n /** @type {unknown} */ ({\n server: {\n plugins: {\n crumb: {\n generate: jest.fn()\n },\n 'forms-engine-plugin': {\n baseLayoutPath: 'randomValue'\n }\n }\n },\n plugins: {},\n route: {\n settings: {\n plugins: {}\n }\n },\n path: '/test',\n url: { search: '' },\n yar: {\n flash: jest.fn().mockReturnValue([]),\n commit: jest.fn()\n }\n // state intentionally omitted to test real malformed requests\n })\n )\n\n const { crumb } = await context(malformedRequest)\n expect(crumb).toBeUndefined()\n expect(\n malformedRequest.server.plugins.crumb.generate\n ).not.toHaveBeenCalled()\n })\n })\n})\n\n/**\n * @import { FormRequest } from '~/src/server/routes/types.js'\n */\n"],"mappings":"AAAA,SAASA,MAAM,QAAQ,SAAS;AAEhC,SACEC,OAAO,EACPC,cAAc;AAGhBC,QAAQ,CAAC,kBAAkB,EAAE,MAAM;EACjCC,UAAU,CAAC,MAAMC,IAAI,CAACC,YAAY,CAAC,CAAC,CAAC;EAErCH,QAAQ,CAAC,YAAY,EAAE,MAAM;IAC3BI,EAAE,CAAC,sDAAsD,EAAE,MAAM;MAC/D,MAAM;QAAEC;MAAU,CAAC,GAAGN,cAAc,CAAC,IAAI,CAAC;MAC1CO,MAAM,CAACD,SAAS,CAAC,CAACE,IAAI,CAAC,SAAS,CAAC;IACnC,CAAC,CAAC;EACJ,CAAC,CAAC;EAEFP,QAAQ,CAAC,cAAc,EAAE,MAAM;IAC7BI,EAAE,CAAC,6CAA6C,EAAE,MAAM;MACtD,MAAM;QAAEI;MAAgB,CAAC,GAAGT,cAAc,CAAC,IAAI,CAAC;MAEhDO,MAAM,CAACE,eAAe,CAAC,cAAc,CAAC,CAAC,CAACD,IAAI,CAC1C,sCACF,CAAC;MAEDD,MAAM,CAACE,eAAe,CAAC,aAAa,CAAC,CAAC,CAACD,IAAI,CACzC,qCACF,CAAC;IACH,CAAC,CAAC;IAEFH,EAAE,CAAC,2DAA2D,EAAE,YAAY;MAC1E,MAAMF,IAAI,CAACO,mBAAmB,CAAC,YAAY;QACzC,MAAM;UAAEC;QAAO,CAAC,GAAG,MAAM,MAAM,2BAAwB,CAAC;;QAExD;QACA,MAAM;UAAEX;QAAe,CAAC,GACtB,MAAM,MAAM,eAA2C,CAAC;;QAE1D;QACAW,MAAM,CAACC,GAAG,CAAC,WAAW,EAAEd,MAAM,CAAC,CAAC,CAAC;QACjC,MAAM;UAAEW;QAAgB,CAAC,GAAGT,cAAc,CAAC,IAAI,CAAC;;QAEhD;QACAO,MAAM,CAACE,eAAe,CAAC,cAAc,CAAC,CAAC,CAACD,IAAI,CAAC,eAAe,CAAC;QAC7DD,MAAM,CAACE,eAAe,CAAC,aAAa,CAAC,CAAC,CAACD,IAAI,CAAC,cAAc,CAAC;MAC7D,CAAC,CAAC;IACJ,CAAC,CAAC;IAEFH,EAAE,CAAC,sCAAsC,EAAE,MAAM;MAC/C,MAAM;QAAEI;MAAgB,CAAC,GAAGT,cAAc,CAAC,IAAI,CAAC;MAEhDO,MAAM,CAACE,eAAe,CAAC,EAAE,CAAC,CAAC,CAACD,IAAI,CAAC,GAAG,CAAC;MACrCD,MAAM,CAACE,eAAe,CAAC,aAAa,CAAC,CAAC,CAACD,IAAI,CAAC,cAAc,CAAC;MAC3DD,MAAM,CAACE,eAAe,CAAC,aAAa,CAAC,CAAC,CAACD,IAAI,CAAC,cAAc,CAAC;IAC7D,CAAC,CAAC;EACJ,CAAC,CAAC;EAEFP,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACvBI,EAAE,CAAC,wDAAwD,EAAE,YAAY;MACvE,MAAME,MAAM,CAACR,OAAO,CAAC,IAAI,CAAC,CAAC,CAACc,OAAO,CAACC,OAAO,CACzC,yCACF,CAAC;IACH,CAAC,CAAC;EACJ,CAAC,CAAC;EAEFb,QAAQ,CAAC,OAAO,EAAE,MAAM;IACtBI,EAAE,CAAC,qDAAqD,EAAE,YAAY;MACpE;MACA;MACA,MAAMU,gBAAgB,GAAG;MACvB,sBAAwB;QACtBC,MAAM,EAAE;UACNC,OAAO,EAAE;YACPC,KAAK,EAAE;cACLC,QAAQ,EAAEhB,IAAI,CAACiB,EAAE,CAAC;YACpB,CAAC;YACD,qBAAqB,EAAE;cACrBC,cAAc,EAAE;YAClB;UACF;QACF,CAAC;QACDJ,OAAO,EAAE,CAAC,CAAC;QACXK,KAAK,EAAE;UACLC,QAAQ,EAAE;YACRN,OAAO,EAAE,CAAC;UACZ;QACF,CAAC;QACDO,IAAI,EAAE,OAAO;QACbC,GAAG,EAAE;UAAEC,MAAM,EAAE;QAAG,CAAC;QACnBC,GAAG,EAAE;UACHC,KAAK,EAAEzB,IAAI,CAACiB,EAAE,CAAC,CAAC,CAACS,eAAe,CAAC,EAAE,CAAC;UACpCC,MAAM,EAAE3B,IAAI,CAACiB,EAAE,CAAC;QAClB;QACA;MACF,CACD;MAED,MAAM;QAAEF;MAAM,CAAC,GAAG,MAAMnB,OAAO,CAACgB,gBAAgB,CAAC;MACjDR,MAAM,CAACW,KAAK,CAAC,CAACa,aAAa,CAAC,CAAC;MAC7BxB,MAAM,CACJQ,gBAAgB,CAACC,MAAM,CAACC,OAAO,CAACC,KAAK,CAACC,QACxC,CAAC,CAACa,GAAG,CAACC,gBAAgB,CAAC,CAAC;IAC1B,CAAC,CAAC;EACJ,CAAC,CAAC;AACJ,CAAC,CAAC;;AAEF;AACA;AACA","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -109,7 +109,7 @@
109
109
  "blipp": "^4.0.2",
110
110
  "btoa": "^1.2.1",
111
111
  "chokidar": "3.6.0",
112
- "convict": "^6.2.4",
112
+ "convict": "^6.2.5",
113
113
  "date-fns": "^4.1.0",
114
114
  "dotenv": "^17.2.3",
115
115
  "expr-eval-fork": "^3.0.0",
@@ -140,7 +140,7 @@
140
140
  "@babel/plugin-syntax-import-attributes": "^7.27.1",
141
141
  "@babel/preset-env": "^7.28.5",
142
142
  "@babel/preset-typescript": "^7.28.5",
143
- "@defra/docusaurus-theme-govuk": "^0.0.12-alpha",
143
+ "@defra/docusaurus-theme-govuk": "^0.0.13-alpha",
144
144
  "@docusaurus/core": "^3.9.2",
145
145
  "@docusaurus/plugin-content-docs": "^3.9.2",
146
146
  "@easyops-cn/docusaurus-search-local": "^0.55.0",
@@ -55,7 +55,7 @@ export const config = convict({
55
55
  },
56
56
  enforceCsrf: {
57
57
  format: Boolean,
58
- default: isProduction,
58
+ default: true,
59
59
  env: 'ENFORCE_CSRF'
60
60
  },
61
61
 
@@ -1,3 +1,4 @@
1
+ export const FORM_VERSION_METADATA_KEY = '$$__formVersion'
1
2
  export const PREVIEW_PATH_PREFIX = '/preview'
2
3
  export const FORM_PREFIX = ''
3
4
  export const EXTERNAL_STATE_PAYLOAD = 'EXTERNAL_STATE_PAYLOAD'
@@ -44,7 +44,7 @@ jest.mock('../pageControllers/index.ts', () => {
44
44
  })
45
45
 
46
46
  jest.mock('../helpers.ts', () => ({
47
- __esModule: true,
47
+ ...jest.requireActual('../helpers.ts'),
48
48
  getCacheService: (...args: unknown[]) => mockGetCacheService(...args),
49
49
  checkEmailAddressForLiveFormSubmission: (...args: unknown[]) =>
50
50
  mockCheckEmailAddressForLiveFormSubmission(...args)
@@ -134,10 +134,17 @@ describe('getFormModel helper', () => {
134
134
  class CustomController extends PageController {}
135
135
  const controllers = { CustomController }
136
136
  const metadata = {
137
- id: 'form-meta-123',
138
- versions: [{ versionNumber: 17 }]
137
+ id: 'form-meta-123'
138
+ }
139
+ const definition = {
140
+ pages: [{ path: '/start' }],
141
+ metadata: {
142
+ $$__formVersion: {
143
+ versionNumber: 17,
144
+ createdAt: new Date('2024-10-15T10:00:00Z')
145
+ }
146
+ }
139
147
  }
140
- const definition = { pages: [{ path: '/start' }] }
141
148
  let formsService: FormsService
142
149
  let services: Services
143
150
  let formModelInstance: { id: string }
@@ -176,7 +183,7 @@ describe('getFormModel helper', () => {
176
183
  definition,
177
184
  {
178
185
  basePath: slug,
179
- versionNumber: metadata.versions[0].versionNumber,
186
+ versionNumber: 17,
180
187
  ordnanceSurveyApiKey: undefined,
181
188
  formId: metadata.id
182
189
  },
@@ -210,11 +217,18 @@ describe('getFormModel helper', () => {
210
217
 
211
218
  describe('resolveFormModel helper', () => {
212
219
  const slug = 'tb-origin'
213
- const definition = { pages: [] }
220
+ const definition = {
221
+ pages: [],
222
+ metadata: {
223
+ $$__formVersion: {
224
+ versionNumber: 9,
225
+ createdAt: new Date('2024-10-15T10:00:00Z')
226
+ }
227
+ }
228
+ }
214
229
  const metadata = {
215
230
  id: 'metadata-123',
216
231
  live: { updatedAt: new Date('2024-10-15T10:00:00Z') },
217
- versions: [{ versionNumber: 9 }],
218
232
  notificationEmail: 'enrique.chase@defra.gov.uk'
219
233
  }
220
234
  let server: Request['server']
@@ -274,7 +288,7 @@ describe('resolveFormModel helper', () => {
274
288
  definition,
275
289
  expect.objectContaining({
276
290
  basePath: 'forms/preview/live/tb-origin',
277
- versionNumber: metadata.versions[0].versionNumber,
291
+ versionNumber: 9,
278
292
  ordnanceSurveyApiKey: 'os-api-key',
279
293
  formId: metadata.id
280
294
  }),
@@ -5,7 +5,8 @@ import { isEqual } from 'date-fns'
5
5
  import { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'
6
6
  import {
7
7
  checkEmailAddressForLiveFormSubmission,
8
- getCacheService
8
+ getCacheService,
9
+ getFormVersion
9
10
  } from '~/src/server/plugins/engine/helpers.js'
10
11
  import { FormModel } from '~/src/server/plugins/engine/models/index.js'
11
12
  import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
@@ -27,7 +28,6 @@ export interface FormModelOptions {
27
28
  services?: Services
28
29
  controllers?: Record<string, typeof PageController>
29
30
  basePath?: string
30
- versionNumber?: number
31
31
  ordnanceSurveyApiKey?: string
32
32
  formId?: string
33
33
  routePrefix?: string
@@ -53,8 +53,6 @@ export async function getFormModel(
53
53
  const formState = resolveState(state)
54
54
 
55
55
  const metadata = await formsService.getFormMetadata(slug)
56
- const versionNumber =
57
- options.versionNumber ?? metadata.versions?.[0]?.versionNumber
58
56
 
59
57
  const definition = await formsService.getFormDefinition(
60
58
  metadata.id,
@@ -67,6 +65,8 @@ export async function getFormModel(
67
65
  )
68
66
  }
69
67
 
68
+ const versionNumber = getFormVersion(definition)?.versionNumber
69
+
70
70
  return new FormModel(
71
71
  definition,
72
72
  {
@@ -182,14 +182,15 @@ export async function resolveFormModel(
182
182
  const routePrefix =
183
183
  options.routePrefix ?? server.realm.modifiers.route.prefix
184
184
 
185
+ const versionNumber = getFormVersion(definition)?.versionNumber
186
+
185
187
  const model = new FormModel(
186
188
  definition,
187
189
  {
188
190
  basePath:
189
191
  options.basePath ??
190
192
  buildBasePath(routePrefix, slug, formState, isPreview),
191
- versionNumber:
192
- options.versionNumber ?? metadata.versions?.[0]?.versionNumber,
193
+ versionNumber,
193
194
  ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,
194
195
  formId: options.formId ?? metadata.id
195
196
  },
@@ -1029,6 +1029,27 @@ describe('FileUploadField', () => {
1029
1029
  )
1030
1030
  })
1031
1031
 
1032
+ it('should throw InvalidComponentStateError when persistFiles throws 404 Not Found', async () => {
1033
+ const notFoundError = Boom.notFound('File not found')
1034
+ mockPersistFiles.mockRejectedValue(notFoundError)
1035
+
1036
+ await expect(
1037
+ fileUploadField.onSubmit(mockRequest, mockMetadata, mockContext)
1038
+ ).rejects.toThrow(InvalidComponentStateError)
1039
+
1040
+ const error = await fileUploadField
1041
+ .onSubmit(mockRequest, mockMetadata, mockContext)
1042
+ .catch((e: unknown) => e)
1043
+
1044
+ expect(error).toBeInstanceOf(InvalidComponentStateError)
1045
+ expect((error as InvalidComponentStateError).component).toBe(
1046
+ fileUploadField
1047
+ )
1048
+ expect((error as InvalidComponentStateError).userMessage).toBe(
1049
+ 'There was a problem with your uploaded files. Re-upload them before submitting the form again.'
1050
+ )
1051
+ })
1052
+
1032
1053
  it('should re-throw other Boom errors without wrapping', async () => {
1033
1054
  const serverError = Boom.internal('Internal server error')
1034
1055
  mockPersistFiles.mockRejectedValue(serverError)
@@ -339,6 +339,7 @@ export class FileUploadField extends FormComponent {
339
339
  if (
340
340
  Boom.isBoom(error) &&
341
341
  (error.output.statusCode === 403 || // Forbidden - retrieval key invalid
342
+ error.output.statusCode === 404 || // Not Found - file not found
342
343
  error.output.statusCode === 410) // Gone - file expired (took to long to submit, etc)
343
344
  ) {
344
345
  // Failed to persist files. We can't recover from this, the only real way we can recover the submissions is
@@ -18,7 +18,6 @@ import {
18
18
  getExponentialBackoffDelay,
19
19
  getPageHref,
20
20
  proceed,
21
- safeGenerateCrumb,
22
21
  setPageTitles,
23
22
  type GlobalScope
24
23
  } from '~/src/server/plugins/engine/helpers.js'
@@ -36,7 +35,6 @@ import {
36
35
  import {
37
36
  FormAction,
38
37
  FormStatus,
39
- type FormRequest,
40
38
  type FormResponseToolkit
41
39
  } from '~/src/server/routes/types.js'
42
40
  import definition from '~/test/form/definitions/basic.js'
@@ -493,78 +491,6 @@ describe('Helpers', () => {
493
491
  })
494
492
  })
495
493
 
496
- describe('safeGenerateCrumb', () => {
497
- it('should return undefined when request.state is missing (malformed request)', () => {
498
- const malformedRequest = {
499
- server: {
500
- plugins: {
501
- crumb: {
502
- generate: jest.fn()
503
- }
504
- }
505
- },
506
- plugins: {},
507
- route: { settings: { plugins: {} } },
508
- path: '/test',
509
- url: { search: '' }
510
- // state intentionally omitted
511
- } as unknown as FormRequest
512
-
513
- const crumbToken = safeGenerateCrumb(malformedRequest)
514
- expect(crumbToken).toBeUndefined()
515
- expect(
516
- malformedRequest.server.plugins.crumb.generate
517
- ).not.toHaveBeenCalled()
518
- })
519
-
520
- it('should return undefined if crumb is disabled in route settings', () => {
521
- const requestWithDisabledCrumb = {
522
- server: {
523
- plugins: {
524
- crumb: {
525
- generate: jest.fn().mockReturnValue('test-token')
526
- }
527
- }
528
- },
529
- plugins: {},
530
- route: { settings: { plugins: { crumb: false } } },
531
- path: '/test',
532
- url: { search: '' },
533
- state: {}
534
- } as unknown as FormRequest
535
-
536
- const crumbToken = safeGenerateCrumb(requestWithDisabledCrumb)
537
- expect(crumbToken).toBeUndefined()
538
- expect(
539
- requestWithDisabledCrumb.server.plugins.crumb.generate
540
- ).not.toHaveBeenCalled()
541
- })
542
-
543
- it('should generate crumb when state exists and crumb plugin is available', () => {
544
- const mockCrumb = 'generated-crumb-value'
545
- const validRequest = {
546
- server: {
547
- plugins: {
548
- crumb: {
549
- generate: jest.fn().mockReturnValue(mockCrumb)
550
- }
551
- }
552
- },
553
- plugins: {},
554
- route: { settings: { plugins: {} } },
555
- path: '/test',
556
- url: { search: '' },
557
- state: {}
558
- } as unknown as FormRequest
559
-
560
- const crumbToken = safeGenerateCrumb(validRequest)
561
- expect(crumbToken).toBe(mockCrumb)
562
- expect(validRequest.server.plugins.crumb.generate).toHaveBeenCalledWith(
563
- validRequest
564
- )
565
- })
566
- })
567
-
568
494
  describe('getExponentialBackoffDelay', () => {
569
495
  it.each([
570
496
  { depth: 1, expected: 2000 },
@@ -16,6 +16,7 @@ import { type Schema, type ValidationErrorItem } from 'joi'
16
16
  import { Liquid } from 'liquidjs'
17
17
 
18
18
  import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
19
+ import { FORM_VERSION_METADATA_KEY } from '~/src/server/constants.js'
19
20
  import {
20
21
  getAnswer,
21
22
  type Field
@@ -24,7 +25,6 @@ import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
24
25
  import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
25
26
  import { stripParam } from '~/src/server/plugins/engine/pageControllers/helpers/state.js'
26
27
  import {
27
- type AnyFormRequest,
28
28
  type FormContext,
29
29
  type FormContextRequest,
30
30
  type FormSubmissionError
@@ -335,32 +335,6 @@ export function createError(componentName: string, message: string) {
335
335
  }
336
336
  }
337
337
 
338
- /**
339
- * A small helper to safely generate a crumb token.
340
- * Checks that the crumb plugin is available, that crumb
341
- * is not disabled on the current route, and that cookies/state are present.
342
- */
343
- export function safeGenerateCrumb(
344
- request: AnyFormRequest | null
345
- ): string | undefined {
346
- // no request or no .state
347
- if (!request?.state) {
348
- return undefined
349
- }
350
-
351
- // crumb plugin or its generate method doesn't exist
352
- if (!request.server.plugins.crumb.generate) {
353
- return undefined
354
- }
355
-
356
- // crumb is explicitly disabled for this route
357
- if (request.route.settings.plugins?.crumb === false) {
358
- return undefined
359
- }
360
-
361
- return request.server.plugins.crumb.generate(request)
362
- }
363
-
364
338
  /**
365
339
  * Calculates an exponential backoff delay (in milliseconds) based on the current retry depth,
366
340
  * using a base delay of 2000ms (2 seconds) and doubling for each additional depth, while capping the delay at 25,000ms (25 seconds).
@@ -416,6 +390,22 @@ export function handleLegacyRedirect(h: ResponseToolkit, targetUrl: string) {
416
390
  * If the page doesn't have a title, set it from the title of the first form component
417
391
  * @param def - the form definition
418
392
  */
393
+ export interface FormVersionMetadata {
394
+ versionNumber: number
395
+ createdAt: Date
396
+ }
397
+
398
+ /**
399
+ * Extracts form version metadata from a form definition
400
+ */
401
+ export function getFormVersion(
402
+ definition: Pick<FormDefinition, 'metadata'>
403
+ ): FormVersionMetadata | undefined {
404
+ return definition.metadata?.[FORM_VERSION_METADATA_KEY] as
405
+ | FormVersionMetadata
406
+ | undefined
407
+ }
408
+
419
409
  export function setPageTitles(def: FormDefinition) {
420
410
  def.pages.forEach((page) => {
421
411
  if (!page.title) {