@defra/forms-engine-plugin 4.11.3 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/.server/server/plugins/engine/beta/form-context.js +5 -4
  2. package/.server/server/plugins/engine/beta/form-context.js.map +1 -1
  3. package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -1
  4. package/.server/server/plugins/engine/form-availability.d.ts +16 -0
  5. package/.server/server/plugins/engine/form-availability.js +26 -0
  6. package/.server/server/plugins/engine/form-availability.js.map +1 -0
  7. package/.server/server/plugins/engine/models/unavailable-view-model.d.ts +8 -0
  8. package/.server/server/plugins/engine/models/unavailable-view-model.js +21 -0
  9. package/.server/server/plugins/engine/models/unavailable-view-model.js.map +1 -0
  10. package/.server/server/plugins/engine/plugin.js +6 -0
  11. package/.server/server/plugins/engine/plugin.js.map +1 -1
  12. package/.server/server/plugins/engine/unavailable-response.d.ts +9 -0
  13. package/.server/server/plugins/engine/unavailable-response.js +23 -0
  14. package/.server/server/plugins/engine/unavailable-response.js.map +1 -0
  15. package/.server/server/plugins/engine/views/unavailable.html +20 -0
  16. package/.server/typings/hapi/index.d.js.map +1 -1
  17. package/package.json +4 -4
  18. package/src/server/plugins/engine/beta/form-context.ts +6 -8
  19. package/src/server/plugins/engine/components/helpers/geospatial.ts +1 -1
  20. package/src/server/plugins/engine/form-availability.ts +31 -0
  21. package/src/server/plugins/engine/models/unavailable-view-model.ts +36 -0
  22. package/src/server/plugins/engine/plugin.ts +6 -0
  23. package/src/server/plugins/engine/unavailable-response.ts +29 -0
  24. package/src/server/plugins/engine/views/unavailable.html +20 -0
  25. package/src/typings/hapi/index.d.ts +1 -1
@@ -1,6 +1,7 @@
1
1
  import Boom from '@hapi/boom';
2
2
  import { isEqual } from 'date-fns';
3
3
  import { PREVIEW_PATH_PREFIX } from "../../../constants.js";
4
+ import { assertFormAvailable } from "../form-availability.js";
4
5
  import { checkEmailAddressForLiveFormSubmission, getCacheService } from "../helpers.js";
5
6
  import { FormModel } from "../models/index.js";
6
7
  import { TerminalPageController } from "../pageControllers/index.js";
@@ -14,6 +15,7 @@ export async function getFormModel(slug, state, options = {}) {
14
15
  const isPreview = isPreviewState(state, options);
15
16
  const formState = resolveState(state);
16
17
  const metadata = await formsService.getFormMetadata(slug);
18
+ assertFormAvailable(metadata);
17
19
  const definition = await formsService.getFormDefinition(metadata.id, formState);
18
20
  if (!definition) {
19
21
  throw Boom.notFound(`No definition found for form metadata ${metadata.id} (${slug}) ${state}`);
@@ -59,6 +61,7 @@ export async function resolveFormModel(server, slug, state, options = {}) {
59
61
  formsService
60
62
  } = services;
61
63
  const metadata = await formsService.getFormMetadata(slug);
64
+ assertFormAvailable(metadata);
62
65
  const formState = resolveState(state);
63
66
  const isPreview = options.isPreview ?? isPreviewState(state, options);
64
67
  const stateMetadata = metadata[formState];
@@ -67,10 +70,8 @@ export async function resolveFormModel(server, slug, state, options = {}) {
67
70
  }
68
71
 
69
72
  // The models cache is created lazily per server instance
70
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
71
- if (!server.app.models) {
72
- server.app.models = new Map();
73
- }
73
+
74
+ server.app.models ??= new Map();
74
75
  const cache = server.app.models;
75
76
  const cacheKey = `${metadata.id}_${formState}_${isPreview}`;
76
77
  let entry = cache.get(cacheKey);
@@ -1 +1 @@
1
- {"version":3,"file":"form-context.js","names":["Boom","isEqual","PREVIEW_PATH_PREFIX","checkEmailAddressForLiveFormSubmission","getCacheService","FormModel","TerminalPageController","defaultServices","FormStatus","getFormModel","slug","state","options","services","formsService","isPreview","isPreviewState","formState","resolveState","metadata","getFormMetadata","definition","getFormDefinition","id","notFound","basePath","buildBasePath","routePrefix","ordnanceSurveyApiKey","formId","controllers","getFormContext","server","yar","Live","formModel","resolveFormModel","cacheService","summaryRequest","app","method","params","path","query","url","URL","cachedState","getState","$$__referenceNumber","errors","stateMetadata","models","Map","cache","cacheKey","entry","get","updatedAt","notificationEmail","realm","modifiers","route","prefix","model","set","base","replace","startsWith","slice","getFirstJourneyPage","context","relevantPages","undefined","lastPageReached","at","penultimatePageReached"],"sources":["../../../../../src/server/plugins/engine/beta/form-context.ts"],"sourcesContent":["import Boom from '@hapi/boom'\nimport { type Request, type Server } from '@hapi/hapi'\nimport { isEqual } from 'date-fns'\n\nimport { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n getCacheService\n} from '~/src/server/plugins/engine/helpers.js'\nimport { FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { TerminalPageController } from '~/src/server/plugins/engine/pageControllers/index.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport {\n type AnyRequest,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport { FormStatus } from '~/src/server/routes/types.js'\nimport { type Services } from '~/src/server/types.js'\n\ntype JourneyState = FormStatus | 'preview'\n\nexport interface FormModelOptions {\n services?: Services\n controllers?: Record<string, typeof PageController>\n basePath?: string\n ordnanceSurveyApiKey?: string\n formId?: string\n routePrefix?: string\n isPreview?: boolean\n}\n\nexport interface FormContextOptions extends FormModelOptions {\n errors?: FormSubmissionError[]\n}\n\ntype SummaryRequest = FormContextRequest & {\n yar: Request['yar']\n}\n\nexport async function getFormModel(\n slug: string,\n state: JourneyState,\n options: FormModelOptions = {}\n) {\n const services = options.services ?? defaultServices\n const { formsService } = services\n const isPreview = isPreviewState(state, options)\n const formState = resolveState(state)\n\n const metadata = await formsService.getFormMetadata(slug)\n\n const definition = await formsService.getFormDefinition(\n metadata.id,\n formState\n )\n\n if (!definition) {\n throw Boom.notFound(\n `No definition found for form metadata ${metadata.id} (${slug}) ${state}`\n )\n }\n\n return new FormModel(\n definition,\n {\n basePath:\n options.basePath ??\n buildBasePath(options.routePrefix ?? '', slug, formState, isPreview),\n ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,\n formId: options.formId ?? metadata.id\n },\n services,\n options.controllers\n )\n}\n\nexport async function getFormContext(\n { server, yar }: Pick<Request, 'server' | 'yar'>,\n slug: string,\n state: JourneyState = FormStatus.Live,\n options: FormContextOptions = {}\n): Promise<FormContext> {\n const formModel = await resolveFormModel(server, slug, state, options)\n\n const cacheService = getCacheService(server)\n\n const summaryRequest: SummaryRequest = {\n app: {},\n method: 'get',\n params: {\n path: 'summary',\n slug,\n ...(isPreviewState(state, options) && {\n state: resolveState(state)\n })\n },\n path: `/${formModel.basePath}/summary`,\n query: {},\n url: new URL(\n `/${formModel.basePath}/summary`,\n 'https://form-context.local'\n ),\n server,\n yar\n }\n\n const cachedState = await cacheService.getState(\n summaryRequest as unknown as AnyRequest\n )\n\n const formState = {\n ...cachedState,\n $$__referenceNumber: cachedState.$$__referenceNumber\n } as unknown as FormSubmissionState\n\n return formModel.getFormContext(\n summaryRequest,\n formState,\n options.errors ?? []\n )\n}\n\nexport async function resolveFormModel(\n server: Server,\n slug: string,\n state: JourneyState,\n options: FormModelOptions = {}\n) {\n const services = options.services ?? defaultServices\n const { formsService } = services\n\n const metadata = await formsService.getFormMetadata(slug)\n const formState = resolveState(state)\n const isPreview = options.isPreview ?? isPreviewState(state, options)\n const stateMetadata = metadata[formState]\n\n if (!stateMetadata) {\n throw Boom.notFound(\n `No '${formState}' state for form metadata ${metadata.id}`\n )\n }\n\n // The models cache is created lazily per server instance\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!server.app.models) {\n server.app.models = new Map<string, { model: FormModel; updatedAt: Date }>()\n }\n\n const cache = server.app.models as Map<\n string,\n { model: FormModel; updatedAt: Date }\n >\n\n const cacheKey = `${metadata.id}_${formState}_${isPreview}`\n let entry = cache.get(cacheKey)\n\n if (!entry || !isEqual(entry.updatedAt, stateMetadata.updatedAt)) {\n const definition = await formsService.getFormDefinition(\n metadata.id,\n formState\n )\n\n if (!definition) {\n throw Boom.notFound(\n `No definition found for form metadata ${metadata.id} (${slug}) ${state}`\n )\n }\n\n checkEmailAddressForLiveFormSubmission(\n metadata.notificationEmail,\n isPreview\n )\n\n const routePrefix =\n options.routePrefix ?? server.realm.modifiers.route.prefix\n\n const model = new FormModel(\n definition,\n {\n basePath:\n options.basePath ??\n buildBasePath(routePrefix, slug, formState, isPreview),\n ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,\n formId: options.formId ?? metadata.id\n },\n services,\n options.controllers\n )\n\n entry = { model, updatedAt: stateMetadata.updatedAt }\n cache.set(cacheKey, entry)\n }\n\n return entry.model\n}\n\nfunction buildBasePath(\n routePrefix: string,\n slug: string,\n state: FormStatus,\n isPreview: boolean\n) {\n const base = (\n isPreview\n ? `${routePrefix}${PREVIEW_PATH_PREFIX}/${state}/${slug}`\n : `${routePrefix}/${slug}`\n ).replace(/\\/{2,}/g, '/')\n\n return base.startsWith('/') ? base.slice(1) : base\n}\n\nexport function getFirstJourneyPage(\n context?: Pick<FormContext, 'relevantPages'>\n) {\n if (!context?.relevantPages) {\n return undefined\n }\n\n const lastPageReached = context.relevantPages.at(-1)\n const penultimatePageReached = context.relevantPages.at(-2)\n\n if (\n lastPageReached instanceof TerminalPageController &&\n penultimatePageReached\n ) {\n return penultimatePageReached\n }\n\n return lastPageReached\n}\n\nfunction resolveState(state: JourneyState): FormStatus {\n return state === 'preview' ? FormStatus.Live : state\n}\n\nfunction isPreviewState(\n state: JourneyState,\n options: FormModelOptions = {}\n): boolean {\n return options.isPreview ?? state === 'preview'\n}\n"],"mappings":"AAAA,OAAOA,IAAI,MAAM,YAAY;AAE7B,SAASC,OAAO,QAAQ,UAAU;AAElC,SAASC,mBAAmB;AAC5B,SACEC,sCAAsC,EACtCC,eAAe;AAEjB,SAASC,SAAS;AAElB,SAASC,sBAAsB;AAC/B,OAAO,KAAKC,eAAe;AAQ3B,SAASC,UAAU;AAuBnB,OAAO,eAAeC,YAAYA,CAChCC,IAAY,EACZC,KAAmB,EACnBC,OAAyB,GAAG,CAAC,CAAC,EAC9B;EACA,MAAMC,QAAQ,GAAGD,OAAO,CAACC,QAAQ,IAAIN,eAAe;EACpD,MAAM;IAAEO;EAAa,CAAC,GAAGD,QAAQ;EACjC,MAAME,SAAS,GAAGC,cAAc,CAACL,KAAK,EAAEC,OAAO,CAAC;EAChD,MAAMK,SAAS,GAAGC,YAAY,CAACP,KAAK,CAAC;EAErC,MAAMQ,QAAQ,GAAG,MAAML,YAAY,CAACM,eAAe,CAACV,IAAI,CAAC;EAEzD,MAAMW,UAAU,GAAG,MAAMP,YAAY,CAACQ,iBAAiB,CACrDH,QAAQ,CAACI,EAAE,EACXN,SACF,CAAC;EAED,IAAI,CAACI,UAAU,EAAE;IACf,MAAMrB,IAAI,CAACwB,QAAQ,CACjB,yCAAyCL,QAAQ,CAACI,EAAE,KAAKb,IAAI,KAAKC,KAAK,EACzE,CAAC;EACH;EAEA,OAAO,IAAIN,SAAS,CAClBgB,UAAU,EACV;IACEI,QAAQ,EACNb,OAAO,CAACa,QAAQ,IAChBC,aAAa,CAACd,OAAO,CAACe,WAAW,IAAI,EAAE,EAAEjB,IAAI,EAAEO,SAAS,EAAEF,SAAS,CAAC;IACtEa,oBAAoB,EAAEhB,OAAO,CAACgB,oBAAoB;IAClDC,MAAM,EAAEjB,OAAO,CAACiB,MAAM,IAAIV,QAAQ,CAACI;EACrC,CAAC,EACDV,QAAQ,EACRD,OAAO,CAACkB,WACV,CAAC;AACH;AAEA,OAAO,eAAeC,cAAcA,CAClC;EAAEC,MAAM;EAAEC;AAAqC,CAAC,EAChDvB,IAAY,EACZC,KAAmB,GAAGH,UAAU,CAAC0B,IAAI,EACrCtB,OAA2B,GAAG,CAAC,CAAC,EACV;EACtB,MAAMuB,SAAS,GAAG,MAAMC,gBAAgB,CAACJ,MAAM,EAAEtB,IAAI,EAAEC,KAAK,EAAEC,OAAO,CAAC;EAEtE,MAAMyB,YAAY,GAAGjC,eAAe,CAAC4B,MAAM,CAAC;EAE5C,MAAMM,cAA8B,GAAG;IACrCC,GAAG,EAAE,CAAC,CAAC;IACPC,MAAM,EAAE,KAAK;IACbC,MAAM,EAAE;MACNC,IAAI,EAAE,SAAS;MACfhC,IAAI;MACJ,IAAIM,cAAc,CAACL,KAAK,EAAEC,OAAO,CAAC,IAAI;QACpCD,KAAK,EAAEO,YAAY,CAACP,KAAK;MAC3B,CAAC;IACH,CAAC;IACD+B,IAAI,EAAE,IAAIP,SAAS,CAACV,QAAQ,UAAU;IACtCkB,KAAK,EAAE,CAAC,CAAC;IACTC,GAAG,EAAE,IAAIC,GAAG,CACV,IAAIV,SAAS,CAACV,QAAQ,UAAU,EAChC,4BACF,CAAC;IACDO,MAAM;IACNC;EACF,CAAC;EAED,MAAMa,WAAW,GAAG,MAAMT,YAAY,CAACU,QAAQ,CAC7CT,cACF,CAAC;EAED,MAAMrB,SAAS,GAAG;IAChB,GAAG6B,WAAW;IACdE,mBAAmB,EAAEF,WAAW,CAACE;EACnC,CAAmC;EAEnC,OAAOb,SAAS,CAACJ,cAAc,CAC7BO,cAAc,EACdrB,SAAS,EACTL,OAAO,CAACqC,MAAM,IAAI,EACpB,CAAC;AACH;AAEA,OAAO,eAAeb,gBAAgBA,CACpCJ,MAAc,EACdtB,IAAY,EACZC,KAAmB,EACnBC,OAAyB,GAAG,CAAC,CAAC,EAC9B;EACA,MAAMC,QAAQ,GAAGD,OAAO,CAACC,QAAQ,IAAIN,eAAe;EACpD,MAAM;IAAEO;EAAa,CAAC,GAAGD,QAAQ;EAEjC,MAAMM,QAAQ,GAAG,MAAML,YAAY,CAACM,eAAe,CAACV,IAAI,CAAC;EACzD,MAAMO,SAAS,GAAGC,YAAY,CAACP,KAAK,CAAC;EACrC,MAAMI,SAAS,GAAGH,OAAO,CAACG,SAAS,IAAIC,cAAc,CAACL,KAAK,EAAEC,OAAO,CAAC;EACrE,MAAMsC,aAAa,GAAG/B,QAAQ,CAACF,SAAS,CAAC;EAEzC,IAAI,CAACiC,aAAa,EAAE;IAClB,MAAMlD,IAAI,CAACwB,QAAQ,CACjB,OAAOP,SAAS,6BAA6BE,QAAQ,CAACI,EAAE,EAC1D,CAAC;EACH;;EAEA;EACA;EACA,IAAI,CAACS,MAAM,CAACO,GAAG,CAACY,MAAM,EAAE;IACtBnB,MAAM,CAACO,GAAG,CAACY,MAAM,GAAG,IAAIC,GAAG,CAAgD,CAAC;EAC9E;EAEA,MAAMC,KAAK,GAAGrB,MAAM,CAACO,GAAG,CAACY,MAGxB;EAED,MAAMG,QAAQ,GAAG,GAAGnC,QAAQ,CAACI,EAAE,IAAIN,SAAS,IAAIF,SAAS,EAAE;EAC3D,IAAIwC,KAAK,GAAGF,KAAK,CAACG,GAAG,CAACF,QAAQ,CAAC;EAE/B,IAAI,CAACC,KAAK,IAAI,CAACtD,OAAO,CAACsD,KAAK,CAACE,SAAS,EAAEP,aAAa,CAACO,SAAS,CAAC,EAAE;IAChE,MAAMpC,UAAU,GAAG,MAAMP,YAAY,CAACQ,iBAAiB,CACrDH,QAAQ,CAACI,EAAE,EACXN,SACF,CAAC;IAED,IAAI,CAACI,UAAU,EAAE;MACf,MAAMrB,IAAI,CAACwB,QAAQ,CACjB,yCAAyCL,QAAQ,CAACI,EAAE,KAAKb,IAAI,KAAKC,KAAK,EACzE,CAAC;IACH;IAEAR,sCAAsC,CACpCgB,QAAQ,CAACuC,iBAAiB,EAC1B3C,SACF,CAAC;IAED,MAAMY,WAAW,GACff,OAAO,CAACe,WAAW,IAAIK,MAAM,CAAC2B,KAAK,CAACC,SAAS,CAACC,KAAK,CAACC,MAAM;IAE5D,MAAMC,KAAK,GAAG,IAAI1D,SAAS,CACzBgB,UAAU,EACV;MACEI,QAAQ,EACNb,OAAO,CAACa,QAAQ,IAChBC,aAAa,CAACC,WAAW,EAAEjB,IAAI,EAAEO,SAAS,EAAEF,SAAS,CAAC;MACxDa,oBAAoB,EAAEhB,OAAO,CAACgB,oBAAoB;MAClDC,MAAM,EAAEjB,OAAO,CAACiB,MAAM,IAAIV,QAAQ,CAACI;IACrC,CAAC,EACDV,QAAQ,EACRD,OAAO,CAACkB,WACV,CAAC;IAEDyB,KAAK,GAAG;MAAEQ,KAAK;MAAEN,SAAS,EAAEP,aAAa,CAACO;IAAU,CAAC;IACrDJ,KAAK,CAACW,GAAG,CAACV,QAAQ,EAAEC,KAAK,CAAC;EAC5B;EAEA,OAAOA,KAAK,CAACQ,KAAK;AACpB;AAEA,SAASrC,aAAaA,CACpBC,WAAmB,EACnBjB,IAAY,EACZC,KAAiB,EACjBI,SAAkB,EAClB;EACA,MAAMkD,IAAI,GAAG,CACXlD,SAAS,GACL,GAAGY,WAAW,GAAGzB,mBAAmB,IAAIS,KAAK,IAAID,IAAI,EAAE,GACvD,GAAGiB,WAAW,IAAIjB,IAAI,EAAE,EAC5BwD,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;EAEzB,OAAOD,IAAI,CAACE,UAAU,CAAC,GAAG,CAAC,GAAGF,IAAI,CAACG,KAAK,CAAC,CAAC,CAAC,GAAGH,IAAI;AACpD;AAEA,OAAO,SAASI,mBAAmBA,CACjCC,OAA4C,EAC5C;EACA,IAAI,CAACA,OAAO,EAAEC,aAAa,EAAE;IAC3B,OAAOC,SAAS;EAClB;EAEA,MAAMC,eAAe,GAAGH,OAAO,CAACC,aAAa,CAACG,EAAE,CAAC,CAAC,CAAC,CAAC;EACpD,MAAMC,sBAAsB,GAAGL,OAAO,CAACC,aAAa,CAACG,EAAE,CAAC,CAAC,CAAC,CAAC;EAE3D,IACED,eAAe,YAAYnE,sBAAsB,IACjDqE,sBAAsB,EACtB;IACA,OAAOA,sBAAsB;EAC/B;EAEA,OAAOF,eAAe;AACxB;AAEA,SAASvD,YAAYA,CAACP,KAAmB,EAAc;EACrD,OAAOA,KAAK,KAAK,SAAS,GAAGH,UAAU,CAAC0B,IAAI,GAAGvB,KAAK;AACtD;AAEA,SAASK,cAAcA,CACrBL,KAAmB,EACnBC,OAAyB,GAAG,CAAC,CAAC,EACrB;EACT,OAAOA,OAAO,CAACG,SAAS,IAAIJ,KAAK,KAAK,SAAS;AACjD","ignoreList":[]}
1
+ {"version":3,"file":"form-context.js","names":["Boom","isEqual","PREVIEW_PATH_PREFIX","assertFormAvailable","checkEmailAddressForLiveFormSubmission","getCacheService","FormModel","TerminalPageController","defaultServices","FormStatus","getFormModel","slug","state","options","services","formsService","isPreview","isPreviewState","formState","resolveState","metadata","getFormMetadata","definition","getFormDefinition","id","notFound","basePath","buildBasePath","routePrefix","ordnanceSurveyApiKey","formId","controllers","getFormContext","server","yar","Live","formModel","resolveFormModel","cacheService","summaryRequest","app","method","params","path","query","url","URL","cachedState","getState","$$__referenceNumber","errors","stateMetadata","models","Map","cache","cacheKey","entry","get","updatedAt","notificationEmail","realm","modifiers","route","prefix","model","set","base","replace","startsWith","slice","getFirstJourneyPage","context","relevantPages","undefined","lastPageReached","at","penultimatePageReached"],"sources":["../../../../../src/server/plugins/engine/beta/form-context.ts"],"sourcesContent":["import Boom from '@hapi/boom'\nimport { type Request, type Server } from '@hapi/hapi'\nimport { isEqual } from 'date-fns'\n\nimport { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'\nimport { assertFormAvailable } from '~/src/server/plugins/engine/form-availability.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n getCacheService\n} from '~/src/server/plugins/engine/helpers.js'\nimport { FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { TerminalPageController } from '~/src/server/plugins/engine/pageControllers/index.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport {\n type AnyRequest,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport { FormStatus } from '~/src/server/routes/types.js'\nimport { type Services } from '~/src/server/types.js'\n\ntype JourneyState = FormStatus | 'preview'\n\nexport interface FormModelOptions {\n services?: Services\n controllers?: Record<string, typeof PageController>\n basePath?: string\n ordnanceSurveyApiKey?: string\n formId?: string\n routePrefix?: string\n isPreview?: boolean\n}\n\nexport interface FormContextOptions extends FormModelOptions {\n errors?: FormSubmissionError[]\n}\n\ntype SummaryRequest = FormContextRequest & {\n yar: Request['yar']\n}\n\nexport async function getFormModel(\n slug: string,\n state: JourneyState,\n options: FormModelOptions = {}\n) {\n const services = options.services ?? defaultServices\n const { formsService } = services\n const isPreview = isPreviewState(state, options)\n const formState = resolveState(state)\n\n const metadata = await formsService.getFormMetadata(slug)\n assertFormAvailable(metadata)\n\n const definition = await formsService.getFormDefinition(\n metadata.id,\n formState\n )\n\n if (!definition) {\n throw Boom.notFound(\n `No definition found for form metadata ${metadata.id} (${slug}) ${state}`\n )\n }\n\n return new FormModel(\n definition,\n {\n basePath:\n options.basePath ??\n buildBasePath(options.routePrefix ?? '', slug, formState, isPreview),\n ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,\n formId: options.formId ?? metadata.id\n },\n services,\n options.controllers\n )\n}\n\nexport async function getFormContext(\n { server, yar }: Pick<Request, 'server' | 'yar'>,\n slug: string,\n state: JourneyState = FormStatus.Live,\n options: FormContextOptions = {}\n): Promise<FormContext> {\n const formModel = await resolveFormModel(server, slug, state, options)\n\n const cacheService = getCacheService(server)\n\n const summaryRequest: SummaryRequest = {\n app: {},\n method: 'get',\n params: {\n path: 'summary',\n slug,\n ...(isPreviewState(state, options) && {\n state: resolveState(state)\n })\n },\n path: `/${formModel.basePath}/summary`,\n query: {},\n url: new URL(\n `/${formModel.basePath}/summary`,\n 'https://form-context.local'\n ),\n server,\n yar\n }\n\n const cachedState = await cacheService.getState(\n summaryRequest as unknown as AnyRequest\n )\n\n const formState = {\n ...cachedState,\n $$__referenceNumber: cachedState.$$__referenceNumber\n } as unknown as FormSubmissionState\n\n return formModel.getFormContext(\n summaryRequest,\n formState,\n options.errors ?? []\n )\n}\n\nexport async function resolveFormModel(\n server: Server,\n slug: string,\n state: JourneyState,\n options: FormModelOptions = {}\n) {\n const services = options.services ?? defaultServices\n const { formsService } = services\n\n const metadata = await formsService.getFormMetadata(slug)\n assertFormAvailable(metadata)\n const formState = resolveState(state)\n const isPreview = options.isPreview ?? isPreviewState(state, options)\n const stateMetadata = metadata[formState]\n\n if (!stateMetadata) {\n throw Boom.notFound(\n `No '${formState}' state for form metadata ${metadata.id}`\n )\n }\n\n // The models cache is created lazily per server instance\n\n server.app.models ??= new Map<string, { model: FormModel; updatedAt: Date }>()\n\n const cache = server.app.models\n\n const cacheKey = `${metadata.id}_${formState}_${isPreview}`\n let entry = cache.get(cacheKey)\n\n if (!entry || !isEqual(entry.updatedAt, stateMetadata.updatedAt)) {\n const definition = await formsService.getFormDefinition(\n metadata.id,\n formState\n )\n\n if (!definition) {\n throw Boom.notFound(\n `No definition found for form metadata ${metadata.id} (${slug}) ${state}`\n )\n }\n\n checkEmailAddressForLiveFormSubmission(\n metadata.notificationEmail,\n isPreview\n )\n\n const routePrefix =\n options.routePrefix ?? server.realm.modifiers.route.prefix\n\n const model = new FormModel(\n definition,\n {\n basePath:\n options.basePath ??\n buildBasePath(routePrefix, slug, formState, isPreview),\n ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,\n formId: options.formId ?? metadata.id\n },\n services,\n options.controllers\n )\n\n entry = { model, updatedAt: stateMetadata.updatedAt }\n cache.set(cacheKey, entry)\n }\n\n return entry.model\n}\n\nfunction buildBasePath(\n routePrefix: string,\n slug: string,\n state: FormStatus,\n isPreview: boolean\n) {\n const base = (\n isPreview\n ? `${routePrefix}${PREVIEW_PATH_PREFIX}/${state}/${slug}`\n : `${routePrefix}/${slug}`\n ).replace(/\\/{2,}/g, '/')\n\n return base.startsWith('/') ? base.slice(1) : base\n}\n\nexport function getFirstJourneyPage(\n context?: Pick<FormContext, 'relevantPages'>\n) {\n if (!context?.relevantPages) {\n return undefined\n }\n\n const lastPageReached = context.relevantPages.at(-1)\n const penultimatePageReached = context.relevantPages.at(-2)\n\n if (\n lastPageReached instanceof TerminalPageController &&\n penultimatePageReached\n ) {\n return penultimatePageReached\n }\n\n return lastPageReached\n}\n\nfunction resolveState(state: JourneyState): FormStatus {\n return state === 'preview' ? FormStatus.Live : state\n}\n\nfunction isPreviewState(\n state: JourneyState,\n options: FormModelOptions = {}\n): boolean {\n return options.isPreview ?? state === 'preview'\n}\n"],"mappings":"AAAA,OAAOA,IAAI,MAAM,YAAY;AAE7B,SAASC,OAAO,QAAQ,UAAU;AAElC,SAASC,mBAAmB;AAC5B,SAASC,mBAAmB;AAC5B,SACEC,sCAAsC,EACtCC,eAAe;AAEjB,SAASC,SAAS;AAElB,SAASC,sBAAsB;AAC/B,OAAO,KAAKC,eAAe;AAQ3B,SAASC,UAAU;AAuBnB,OAAO,eAAeC,YAAYA,CAChCC,IAAY,EACZC,KAAmB,EACnBC,OAAyB,GAAG,CAAC,CAAC,EAC9B;EACA,MAAMC,QAAQ,GAAGD,OAAO,CAACC,QAAQ,IAAIN,eAAe;EACpD,MAAM;IAAEO;EAAa,CAAC,GAAGD,QAAQ;EACjC,MAAME,SAAS,GAAGC,cAAc,CAACL,KAAK,EAAEC,OAAO,CAAC;EAChD,MAAMK,SAAS,GAAGC,YAAY,CAACP,KAAK,CAAC;EAErC,MAAMQ,QAAQ,GAAG,MAAML,YAAY,CAACM,eAAe,CAACV,IAAI,CAAC;EACzDR,mBAAmB,CAACiB,QAAQ,CAAC;EAE7B,MAAME,UAAU,GAAG,MAAMP,YAAY,CAACQ,iBAAiB,CACrDH,QAAQ,CAACI,EAAE,EACXN,SACF,CAAC;EAED,IAAI,CAACI,UAAU,EAAE;IACf,MAAMtB,IAAI,CAACyB,QAAQ,CACjB,yCAAyCL,QAAQ,CAACI,EAAE,KAAKb,IAAI,KAAKC,KAAK,EACzE,CAAC;EACH;EAEA,OAAO,IAAIN,SAAS,CAClBgB,UAAU,EACV;IACEI,QAAQ,EACNb,OAAO,CAACa,QAAQ,IAChBC,aAAa,CAACd,OAAO,CAACe,WAAW,IAAI,EAAE,EAAEjB,IAAI,EAAEO,SAAS,EAAEF,SAAS,CAAC;IACtEa,oBAAoB,EAAEhB,OAAO,CAACgB,oBAAoB;IAClDC,MAAM,EAAEjB,OAAO,CAACiB,MAAM,IAAIV,QAAQ,CAACI;EACrC,CAAC,EACDV,QAAQ,EACRD,OAAO,CAACkB,WACV,CAAC;AACH;AAEA,OAAO,eAAeC,cAAcA,CAClC;EAAEC,MAAM;EAAEC;AAAqC,CAAC,EAChDvB,IAAY,EACZC,KAAmB,GAAGH,UAAU,CAAC0B,IAAI,EACrCtB,OAA2B,GAAG,CAAC,CAAC,EACV;EACtB,MAAMuB,SAAS,GAAG,MAAMC,gBAAgB,CAACJ,MAAM,EAAEtB,IAAI,EAAEC,KAAK,EAAEC,OAAO,CAAC;EAEtE,MAAMyB,YAAY,GAAGjC,eAAe,CAAC4B,MAAM,CAAC;EAE5C,MAAMM,cAA8B,GAAG;IACrCC,GAAG,EAAE,CAAC,CAAC;IACPC,MAAM,EAAE,KAAK;IACbC,MAAM,EAAE;MACNC,IAAI,EAAE,SAAS;MACfhC,IAAI;MACJ,IAAIM,cAAc,CAACL,KAAK,EAAEC,OAAO,CAAC,IAAI;QACpCD,KAAK,EAAEO,YAAY,CAACP,KAAK;MAC3B,CAAC;IACH,CAAC;IACD+B,IAAI,EAAE,IAAIP,SAAS,CAACV,QAAQ,UAAU;IACtCkB,KAAK,EAAE,CAAC,CAAC;IACTC,GAAG,EAAE,IAAIC,GAAG,CACV,IAAIV,SAAS,CAACV,QAAQ,UAAU,EAChC,4BACF,CAAC;IACDO,MAAM;IACNC;EACF,CAAC;EAED,MAAMa,WAAW,GAAG,MAAMT,YAAY,CAACU,QAAQ,CAC7CT,cACF,CAAC;EAED,MAAMrB,SAAS,GAAG;IAChB,GAAG6B,WAAW;IACdE,mBAAmB,EAAEF,WAAW,CAACE;EACnC,CAAmC;EAEnC,OAAOb,SAAS,CAACJ,cAAc,CAC7BO,cAAc,EACdrB,SAAS,EACTL,OAAO,CAACqC,MAAM,IAAI,EACpB,CAAC;AACH;AAEA,OAAO,eAAeb,gBAAgBA,CACpCJ,MAAc,EACdtB,IAAY,EACZC,KAAmB,EACnBC,OAAyB,GAAG,CAAC,CAAC,EAC9B;EACA,MAAMC,QAAQ,GAAGD,OAAO,CAACC,QAAQ,IAAIN,eAAe;EACpD,MAAM;IAAEO;EAAa,CAAC,GAAGD,QAAQ;EAEjC,MAAMM,QAAQ,GAAG,MAAML,YAAY,CAACM,eAAe,CAACV,IAAI,CAAC;EACzDR,mBAAmB,CAACiB,QAAQ,CAAC;EAC7B,MAAMF,SAAS,GAAGC,YAAY,CAACP,KAAK,CAAC;EACrC,MAAMI,SAAS,GAAGH,OAAO,CAACG,SAAS,IAAIC,cAAc,CAACL,KAAK,EAAEC,OAAO,CAAC;EACrE,MAAMsC,aAAa,GAAG/B,QAAQ,CAACF,SAAS,CAAC;EAEzC,IAAI,CAACiC,aAAa,EAAE;IAClB,MAAMnD,IAAI,CAACyB,QAAQ,CACjB,OAAOP,SAAS,6BAA6BE,QAAQ,CAACI,EAAE,EAC1D,CAAC;EACH;;EAEA;;EAEAS,MAAM,CAACO,GAAG,CAACY,MAAM,KAAK,IAAIC,GAAG,CAAgD,CAAC;EAE9E,MAAMC,KAAK,GAAGrB,MAAM,CAACO,GAAG,CAACY,MAAM;EAE/B,MAAMG,QAAQ,GAAG,GAAGnC,QAAQ,CAACI,EAAE,IAAIN,SAAS,IAAIF,SAAS,EAAE;EAC3D,IAAIwC,KAAK,GAAGF,KAAK,CAACG,GAAG,CAACF,QAAQ,CAAC;EAE/B,IAAI,CAACC,KAAK,IAAI,CAACvD,OAAO,CAACuD,KAAK,CAACE,SAAS,EAAEP,aAAa,CAACO,SAAS,CAAC,EAAE;IAChE,MAAMpC,UAAU,GAAG,MAAMP,YAAY,CAACQ,iBAAiB,CACrDH,QAAQ,CAACI,EAAE,EACXN,SACF,CAAC;IAED,IAAI,CAACI,UAAU,EAAE;MACf,MAAMtB,IAAI,CAACyB,QAAQ,CACjB,yCAAyCL,QAAQ,CAACI,EAAE,KAAKb,IAAI,KAAKC,KAAK,EACzE,CAAC;IACH;IAEAR,sCAAsC,CACpCgB,QAAQ,CAACuC,iBAAiB,EAC1B3C,SACF,CAAC;IAED,MAAMY,WAAW,GACff,OAAO,CAACe,WAAW,IAAIK,MAAM,CAAC2B,KAAK,CAACC,SAAS,CAACC,KAAK,CAACC,MAAM;IAE5D,MAAMC,KAAK,GAAG,IAAI1D,SAAS,CACzBgB,UAAU,EACV;MACEI,QAAQ,EACNb,OAAO,CAACa,QAAQ,IAChBC,aAAa,CAACC,WAAW,EAAEjB,IAAI,EAAEO,SAAS,EAAEF,SAAS,CAAC;MACxDa,oBAAoB,EAAEhB,OAAO,CAACgB,oBAAoB;MAClDC,MAAM,EAAEjB,OAAO,CAACiB,MAAM,IAAIV,QAAQ,CAACI;IACrC,CAAC,EACDV,QAAQ,EACRD,OAAO,CAACkB,WACV,CAAC;IAEDyB,KAAK,GAAG;MAAEQ,KAAK;MAAEN,SAAS,EAAEP,aAAa,CAACO;IAAU,CAAC;IACrDJ,KAAK,CAACW,GAAG,CAACV,QAAQ,EAAEC,KAAK,CAAC;EAC5B;EAEA,OAAOA,KAAK,CAACQ,KAAK;AACpB;AAEA,SAASrC,aAAaA,CACpBC,WAAmB,EACnBjB,IAAY,EACZC,KAAiB,EACjBI,SAAkB,EAClB;EACA,MAAMkD,IAAI,GAAG,CACXlD,SAAS,GACL,GAAGY,WAAW,GAAG1B,mBAAmB,IAAIU,KAAK,IAAID,IAAI,EAAE,GACvD,GAAGiB,WAAW,IAAIjB,IAAI,EAAE,EAC5BwD,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;EAEzB,OAAOD,IAAI,CAACE,UAAU,CAAC,GAAG,CAAC,GAAGF,IAAI,CAACG,KAAK,CAAC,CAAC,CAAC,GAAGH,IAAI;AACpD;AAEA,OAAO,SAASI,mBAAmBA,CACjCC,OAA4C,EAC5C;EACA,IAAI,CAACA,OAAO,EAAEC,aAAa,EAAE;IAC3B,OAAOC,SAAS;EAClB;EAEA,MAAMC,eAAe,GAAGH,OAAO,CAACC,aAAa,CAACG,EAAE,CAAC,CAAC,CAAC,CAAC;EACpD,MAAMC,sBAAsB,GAAGL,OAAO,CAACC,aAAa,CAACG,EAAE,CAAC,CAAC,CAAC,CAAC;EAE3D,IACED,eAAe,YAAYnE,sBAAsB,IACjDqE,sBAAsB,EACtB;IACA,OAAOA,sBAAsB;EAC/B;EAEA,OAAOF,eAAe;AACxB;AAEA,SAASvD,YAAYA,CAACP,KAAmB,EAAc;EACrD,OAAOA,KAAK,KAAK,SAAS,GAAGH,UAAU,CAAC0B,IAAI,GAAGvB,KAAK;AACtD;AAEA,SAASK,cAAcA,CACrBL,KAAmB,EACnBC,OAAyB,GAAG,CAAC,CAAC,EACrB;EACT,OAAOA,OAAO,CAACG,SAAS,IAAIJ,KAAK,KAAK,SAAS;AACjD","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"geospatial.js","names":["GeospatialFieldOptionsCountryEnum","Bourne","booleanWithin","JoiBase","countries","countriesDesc","England","NorthernIreland","Scotland","Wales","Joi","extend","type","base","array","messages","coerce","from","method","value","helpers","trim","undefined","parse","result","errors","error","coordinatesSchema","items","number","required","featurePropertiesSchema","object","keys","description","string","coordinateGridReference","centroidGridReference","featureGeometrySchema","valid","coordinates","when","switch","is","then","min","featureSchema","id","properties","geometry","geospatialSchema","unique","getGeospatialSchema","country","validateCountryBounds","countryFeature","features","find","feature","custom"],"sources":["../../../../../../src/server/plugins/engine/components/helpers/geospatial.ts"],"sourcesContent":["import {\n GeospatialFieldOptionsCountryEnum,\n type GeospatialFieldOptionsCountry\n} from '@defra/forms-model'\nimport Bourne from '@hapi/bourne'\nimport { booleanWithin } from '@turf/boolean-within'\nimport JoiBase, { type CustomValidator } from 'joi'\n\nimport {\n type Coordinates,\n type Feature,\n type FeatureProperties,\n type Geometry\n} from '~/src/server/plugins/engine/types.js'\nimport { countries } from '~/src/server/plugins/map/routes/index.js'\n\nconst countriesDesc: Record<GeospatialFieldOptionsCountryEnum, string> = {\n [GeospatialFieldOptionsCountryEnum.England]: 'England',\n [GeospatialFieldOptionsCountryEnum.NorthernIreland]: 'Northern Ireland',\n [GeospatialFieldOptionsCountryEnum.Scotland]: 'Scotland',\n [GeospatialFieldOptionsCountryEnum.Wales]: 'Wales'\n}\n\nconst Joi = JoiBase.extend({\n type: 'array',\n base: JoiBase.array(),\n messages: {\n 'object.invalidjson': '{{#label}} must be a valid json array string'\n },\n coerce: {\n from: 'string',\n method(value, helpers) {\n if (typeof value === 'string') {\n if (value.trim() === '') {\n return {\n value: undefined\n }\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n return { value: Bourne.parse(value) }\n } catch {\n const result = {\n value,\n errors: [helpers.error('object.invalidjson')]\n }\n\n return result\n }\n } else {\n return {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n value\n }\n }\n }\n }\n}) as JoiBase.Root\n\nconst coordinatesSchema = Joi.array<Coordinates[]>()\n .items(Joi.number().required(), Joi.number().required())\n .required()\n\nconst featurePropertiesSchema = Joi.object<FeatureProperties>()\n .keys({\n description: Joi.string().required(),\n coordinateGridReference: Joi.string().required(),\n centroidGridReference: Joi.string().required()\n })\n .required()\n\nconst featureGeometrySchema = Joi.object<Geometry>().keys({\n type: Joi.string().valid('Point', 'LineString', 'Polygon').required(),\n coordinates: Joi.array()\n .when('type', {\n switch: [\n { is: 'Point', then: coordinatesSchema },\n {\n is: 'LineString',\n then: Joi.array().items(coordinatesSchema).min(2)\n },\n {\n is: 'Polygon',\n then: Joi.array().items(Joi.array().items(coordinatesSchema).min(3))\n }\n ]\n })\n .required()\n})\n\nconst featureSchema = Joi.object<Feature>().keys({\n id: Joi.string().required(),\n type: Joi.string().valid('Feature').required(),\n properties: featurePropertiesSchema,\n geometry: featureGeometrySchema\n})\n\nconst geospatialSchema = Joi.array<Feature[]>()\n .items(featureSchema)\n .unique('id')\n .required()\n\nexport function getGeospatialSchema(country?: GeospatialFieldOptionsCountry) {\n if (!country) {\n return geospatialSchema\n }\n\n const validateCountryBounds: CustomValidator = (value, helpers) => {\n const countryFeature = countries.features.find(\n (feature) => feature.id === country\n )\n\n if (!countryFeature) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return value\n }\n\n const result = booleanWithin(value, countryFeature)\n\n if (!result) {\n return helpers.error('any.custom', {\n country: countriesDesc[country as GeospatialFieldOptionsCountryEnum]\n })\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return value\n }\n\n return Joi.array<Feature[]>()\n .items(featureSchema.custom(validateCountryBounds))\n .unique('id')\n .required()\n}\n\n/**\n * @import { CustomHelpers } from 'joi'\n */\n"],"mappings":"AAAA,SACEA,iCAAiC,QAE5B,oBAAoB;AAC3B,OAAOC,MAAM,MAAM,cAAc;AACjC,SAASC,aAAa,QAAQ,sBAAsB;AACpD,OAAOC,OAAO,MAAgC,KAAK;AAQnD,SAASC,SAAS;AAElB,MAAMC,aAAgE,GAAG;EACvE,CAACL,iCAAiC,CAACM,OAAO,GAAG,SAAS;EACtD,CAACN,iCAAiC,CAACO,eAAe,GAAG,kBAAkB;EACvE,CAACP,iCAAiC,CAACQ,QAAQ,GAAG,UAAU;EACxD,CAACR,iCAAiC,CAACS,KAAK,GAAG;AAC7C,CAAC;AAED,MAAMC,GAAG,GAAGP,OAAO,CAACQ,MAAM,CAAC;EACzBC,IAAI,EAAE,OAAO;EACbC,IAAI,EAAEV,OAAO,CAACW,KAAK,CAAC,CAAC;EACrBC,QAAQ,EAAE;IACR,oBAAoB,EAAE;EACxB,CAAC;EACDC,MAAM,EAAE;IACNC,IAAI,EAAE,QAAQ;IACdC,MAAMA,CAACC,KAAK,EAAEC,OAAO,EAAE;MACrB,IAAI,OAAOD,KAAK,KAAK,QAAQ,EAAE;QAC7B,IAAIA,KAAK,CAACE,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;UACvB,OAAO;YACLF,KAAK,EAAEG;UACT,CAAC;QACH;QAEA,IAAI;UACF;UACA,OAAO;YAAEH,KAAK,EAAElB,MAAM,CAACsB,KAAK,CAACJ,KAAK;UAAE,CAAC;QACvC,CAAC,CAAC,MAAM;UACN,MAAMK,MAAM,GAAG;YACbL,KAAK;YACLM,MAAM,EAAE,CAACL,OAAO,CAACM,KAAK,CAAC,oBAAoB,CAAC;UAC9C,CAAC;UAED,OAAOF,MAAM;QACf;MACF,CAAC,MAAM;QACL,OAAO;UACL;UACAL;QACF,CAAC;MACH;IACF;EACF;AACF,CAAC,CAAiB;AAElB,MAAMQ,iBAAiB,GAAGjB,GAAG,CAACI,KAAK,CAAgB,CAAC,CACjDc,KAAK,CAAClB,GAAG,CAACmB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC,EAAEpB,GAAG,CAACmB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC,CAAC,CACvDA,QAAQ,CAAC,CAAC;AAEb,MAAMC,uBAAuB,GAAGrB,GAAG,CAACsB,MAAM,CAAoB,CAAC,CAC5DC,IAAI,CAAC;EACJC,WAAW,EAAExB,GAAG,CAACyB,MAAM,CAAC,CAAC,CAACL,QAAQ,CAAC,CAAC;EACpCM,uBAAuB,EAAE1B,GAAG,CAACyB,MAAM,CAAC,CAAC,CAACL,QAAQ,CAAC,CAAC;EAChDO,qBAAqB,EAAE3B,GAAG,CAACyB,MAAM,CAAC,CAAC,CAACL,QAAQ,CAAC;AAC/C,CAAC,CAAC,CACDA,QAAQ,CAAC,CAAC;AAEb,MAAMQ,qBAAqB,GAAG5B,GAAG,CAACsB,MAAM,CAAW,CAAC,CAACC,IAAI,CAAC;EACxDrB,IAAI,EAAEF,GAAG,CAACyB,MAAM,CAAC,CAAC,CAACI,KAAK,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,CAAC,CAACT,QAAQ,CAAC,CAAC;EACrEU,WAAW,EAAE9B,GAAG,CAACI,KAAK,CAAC,CAAC,CACrB2B,IAAI,CAAC,MAAM,EAAE;IACZC,MAAM,EAAE,CACN;MAAEC,EAAE,EAAE,OAAO;MAAEC,IAAI,EAAEjB;IAAkB,CAAC,EACxC;MACEgB,EAAE,EAAE,YAAY;MAChBC,IAAI,EAAElC,GAAG,CAACI,KAAK,CAAC,CAAC,CAACc,KAAK,CAACD,iBAAiB,CAAC,CAACkB,GAAG,CAAC,CAAC;IAClD,CAAC,EACD;MACEF,EAAE,EAAE,SAAS;MACbC,IAAI,EAAElC,GAAG,CAACI,KAAK,CAAC,CAAC,CAACc,KAAK,CAAClB,GAAG,CAACI,KAAK,CAAC,CAAC,CAACc,KAAK,CAACD,iBAAiB,CAAC,CAACkB,GAAG,CAAC,CAAC,CAAC;IACrE,CAAC;EAEL,CAAC,CAAC,CACDf,QAAQ,CAAC;AACd,CAAC,CAAC;AAEF,MAAMgB,aAAa,GAAGpC,GAAG,CAACsB,MAAM,CAAU,CAAC,CAACC,IAAI,CAAC;EAC/Cc,EAAE,EAAErC,GAAG,CAACyB,MAAM,CAAC,CAAC,CAACL,QAAQ,CAAC,CAAC;EAC3BlB,IAAI,EAAEF,GAAG,CAACyB,MAAM,CAAC,CAAC,CAACI,KAAK,CAAC,SAAS,CAAC,CAACT,QAAQ,CAAC,CAAC;EAC9CkB,UAAU,EAAEjB,uBAAuB;EACnCkB,QAAQ,EAAEX;AACZ,CAAC,CAAC;AAEF,MAAMY,gBAAgB,GAAGxC,GAAG,CAACI,KAAK,CAAY,CAAC,CAC5Cc,KAAK,CAACkB,aAAa,CAAC,CACpBK,MAAM,CAAC,IAAI,CAAC,CACZrB,QAAQ,CAAC,CAAC;AAEb,OAAO,SAASsB,mBAAmBA,CAACC,OAAuC,EAAE;EAC3E,IAAI,CAACA,OAAO,EAAE;IACZ,OAAOH,gBAAgB;EACzB;EAEA,MAAMI,qBAAsC,GAAGA,CAACnC,KAAK,EAAEC,OAAO,KAAK;IACjE,MAAMmC,cAAc,GAAGnD,SAAS,CAACoD,QAAQ,CAACC,IAAI,CAC3CC,OAAO,IAAKA,OAAO,CAACX,EAAE,KAAKM,OAC9B,CAAC;IAED,IAAI,CAACE,cAAc,EAAE;MACnB;MACA,OAAOpC,KAAK;IACd;IAEA,MAAMK,MAAM,GAAGtB,aAAa,CAACiB,KAAK,EAAEoC,cAAc,CAAC;IAEnD,IAAI,CAAC/B,MAAM,EAAE;MACX,OAAOJ,OAAO,CAACM,KAAK,CAAC,YAAY,EAAE;QACjC2B,OAAO,EAAEhD,aAAa,CAACgD,OAAO;MAChC,CAAC,CAAC;IACJ;;IAEA;IACA,OAAOlC,KAAK;EACd,CAAC;EAED,OAAOT,GAAG,CAACI,KAAK,CAAY,CAAC,CAC1Bc,KAAK,CAACkB,aAAa,CAACa,MAAM,CAACL,qBAAqB,CAAC,CAAC,CAClDH,MAAM,CAAC,IAAI,CAAC,CACZrB,QAAQ,CAAC,CAAC;AACf;;AAEA;AACA;AACA","ignoreList":[]}
1
+ {"version":3,"file":"geospatial.js","names":["GeospatialFieldOptionsCountryEnum","Bourne","booleanWithin","JoiBase","countries","countriesDesc","England","NorthernIreland","Scotland","Wales","Joi","extend","type","base","array","messages","coerce","from","method","value","helpers","trim","undefined","parse","result","errors","error","coordinatesSchema","items","number","required","featurePropertiesSchema","object","keys","description","string","coordinateGridReference","centroidGridReference","featureGeometrySchema","valid","coordinates","when","switch","is","then","min","featureSchema","id","properties","geometry","geospatialSchema","unique","getGeospatialSchema","country","validateCountryBounds","countryFeature","features","find","feature","custom"],"sources":["../../../../../../src/server/plugins/engine/components/helpers/geospatial.ts"],"sourcesContent":["import {\n GeospatialFieldOptionsCountryEnum,\n type GeospatialFieldOptionsCountry\n} from '@defra/forms-model'\nimport Bourne from '@hapi/bourne'\nimport { booleanWithin } from '@turf/boolean-within'\nimport JoiBase, { type CustomValidator } from 'joi'\n\nimport {\n type Coordinates,\n type Feature,\n type FeatureProperties,\n type Geometry\n} from '~/src/server/plugins/engine/types.js'\nimport { countries } from '~/src/server/plugins/map/routes/index.js'\n\nconst countriesDesc: Record<GeospatialFieldOptionsCountryEnum, string> = {\n [GeospatialFieldOptionsCountryEnum.England]: 'England',\n [GeospatialFieldOptionsCountryEnum.NorthernIreland]: 'Northern Ireland',\n [GeospatialFieldOptionsCountryEnum.Scotland]: 'Scotland',\n [GeospatialFieldOptionsCountryEnum.Wales]: 'Wales'\n}\n\nconst Joi = JoiBase.extend({\n type: 'array',\n base: JoiBase.array(),\n messages: {\n 'object.invalidjson': '{{#label}} must be a valid json array string'\n },\n coerce: {\n from: 'string',\n method(value, helpers) {\n if (typeof value === 'string') {\n if (value.trim() === '') {\n return {\n value: undefined\n }\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n return { value: Bourne.parse(value) }\n } catch {\n const result = {\n value,\n errors: [helpers.error('object.invalidjson')]\n }\n\n return result\n }\n } else {\n return {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n value\n }\n }\n }\n }\n}) as JoiBase.Root\n\nconst coordinatesSchema = Joi.array<Coordinates[]>()\n .items(Joi.number().required(), Joi.number().required())\n .required()\n\nconst featurePropertiesSchema = Joi.object<FeatureProperties>()\n .keys({\n description: Joi.string().required(),\n coordinateGridReference: Joi.string().required(),\n centroidGridReference: Joi.string().required()\n })\n .required()\n\nconst featureGeometrySchema = Joi.object<Geometry>().keys({\n type: Joi.string().valid('Point', 'LineString', 'Polygon').required(),\n coordinates: Joi.array()\n .when('type', {\n switch: [\n { is: 'Point', then: coordinatesSchema },\n {\n is: 'LineString',\n then: Joi.array().items(coordinatesSchema).min(2)\n },\n {\n is: 'Polygon',\n then: Joi.array().items(Joi.array().items(coordinatesSchema).min(3))\n }\n ]\n })\n .required()\n})\n\nconst featureSchema = Joi.object<Feature>().keys({\n id: Joi.string().required(),\n type: Joi.string().valid('Feature').required(),\n properties: featurePropertiesSchema,\n geometry: featureGeometrySchema\n})\n\nconst geospatialSchema = Joi.array<Feature[]>()\n .items(featureSchema)\n .unique('id')\n .required()\n\nexport function getGeospatialSchema(country?: GeospatialFieldOptionsCountry) {\n if (!country) {\n return geospatialSchema\n }\n\n const validateCountryBounds: CustomValidator = (value, helpers) => {\n const countryFeature = countries.features.find(\n (feature) => feature.id === country\n )\n\n if (!countryFeature) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return value\n }\n\n const result = booleanWithin(value as Geometry | Feature, countryFeature)\n\n if (!result) {\n return helpers.error('any.custom', {\n country: countriesDesc[country as GeospatialFieldOptionsCountryEnum]\n })\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return value\n }\n\n return Joi.array<Feature[]>()\n .items(featureSchema.custom(validateCountryBounds))\n .unique('id')\n .required()\n}\n\n/**\n * @import { CustomHelpers } from 'joi'\n */\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,16 @@
1
+ import { type FormMetadata } from '@defra/forms-model';
2
+ import Boom from '@hapi/boom';
3
+ export interface OfflineBoomData {
4
+ offline: true;
5
+ metadata: FormMetadata;
6
+ }
7
+ /**
8
+ * Throws when the form has been taken offline. The plugin's
9
+ * unavailable-response extension catches the marker and renders the
10
+ * "Sorry, this form is unavailable" view at HTTP 200.
11
+ */
12
+ export declare function assertFormAvailable(metadata: FormMetadata): void;
13
+ /** Type guard for the offline Boom marker. */
14
+ export declare function isOfflineBoom(err: unknown): err is Boom.Boom<OfflineBoomData> & {
15
+ data: OfflineBoomData;
16
+ };
@@ -0,0 +1,26 @@
1
+ import Boom from '@hapi/boom';
2
+ /**
3
+ * Throws when the form has been taken offline. The plugin's
4
+ * unavailable-response extension catches the marker and renders the
5
+ * "Sorry, this form is unavailable" view at HTTP 200.
6
+ */
7
+ export function assertFormAvailable(metadata) {
8
+ if (metadata.offline === true) {
9
+ const data = {
10
+ offline: true,
11
+ metadata
12
+ };
13
+ throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), {
14
+ statusCode: 503,
15
+ data
16
+ });
17
+ }
18
+ }
19
+
20
+ /** Type guard for the offline Boom marker. */
21
+ export function isOfflineBoom(err) {
22
+ if (!Boom.isBoom(err)) return false;
23
+ const data = err.data;
24
+ return data?.offline === true && !!data.metadata;
25
+ }
26
+ //# sourceMappingURL=form-availability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-availability.js","names":["Boom","assertFormAvailable","metadata","offline","data","boomify","Error","slug","statusCode","isOfflineBoom","err","isBoom"],"sources":["../../../../src/server/plugins/engine/form-availability.ts"],"sourcesContent":["import { type FormMetadata } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\n\nexport interface OfflineBoomData {\n offline: true\n metadata: FormMetadata\n}\n\n/**\n * Throws when the form has been taken offline. The plugin's\n * unavailable-response extension catches the marker and renders the\n * \"Sorry, this form is unavailable\" view at HTTP 200.\n */\nexport function assertFormAvailable(metadata: FormMetadata): void {\n if (metadata.offline === true) {\n const data: OfflineBoomData = { offline: true, metadata }\n throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), {\n statusCode: 503,\n data\n })\n }\n}\n\n/** Type guard for the offline Boom marker. */\nexport function isOfflineBoom(\n err: unknown\n): err is Boom.Boom<OfflineBoomData> & { data: OfflineBoomData } {\n if (!Boom.isBoom(err)) return false\n const data = err.data as Partial<OfflineBoomData> | null | undefined\n return data?.offline === true && !!data.metadata\n}\n"],"mappings":"AACA,OAAOA,IAAI,MAAM,YAAY;AAO7B;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,mBAAmBA,CAACC,QAAsB,EAAQ;EAChE,IAAIA,QAAQ,CAACC,OAAO,KAAK,IAAI,EAAE;IAC7B,MAAMC,IAAqB,GAAG;MAAED,OAAO,EAAE,IAAI;MAAED;IAAS,CAAC;IACzD,MAAMF,IAAI,CAACK,OAAO,CAAC,IAAIC,KAAK,CAAC,QAAQJ,QAAQ,CAACK,IAAI,aAAa,CAAC,EAAE;MAChEC,UAAU,EAAE,GAAG;MACfJ;IACF,CAAC,CAAC;EACJ;AACF;;AAEA;AACA,OAAO,SAASK,aAAaA,CAC3BC,GAAY,EACmD;EAC/D,IAAI,CAACV,IAAI,CAACW,MAAM,CAACD,GAAG,CAAC,EAAE,OAAO,KAAK;EACnC,MAAMN,IAAI,GAAGM,GAAG,CAACN,IAAmD;EACpE,OAAOA,IAAI,EAAED,OAAO,KAAK,IAAI,IAAI,CAAC,CAACC,IAAI,CAACF,QAAQ;AAClD","ignoreList":[]}
@@ -0,0 +1,8 @@
1
+ import { type FormMetadata } from '@defra/forms-model';
2
+ export interface UnavailableViewModel {
3
+ pageTitle: string;
4
+ formTitle: string;
5
+ organisationName: string;
6
+ phoneLines?: string[];
7
+ }
8
+ export declare function unavailableViewModel(metadata: FormMetadata): UnavailableViewModel;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Defra organisations carry an abbreviation suffix on the enum value, e.g.
3
+ * "Rural Payments Agency – RPA". The unavailable page reads cleanly without it.
4
+ */
5
+ function stripOrgSuffix(organisation) {
6
+ return organisation.split(' – ')[0];
7
+ }
8
+ function splitPhoneLines(phone) {
9
+ if (!phone) return undefined;
10
+ const lines = phone.split('\n').map(line => line.trim()).filter(line => line.length > 0);
11
+ return lines.length > 0 ? lines : undefined;
12
+ }
13
+ export function unavailableViewModel(metadata) {
14
+ return {
15
+ pageTitle: 'Sorry, this form is unavailable',
16
+ formTitle: metadata.title,
17
+ organisationName: stripOrgSuffix(metadata.organisation),
18
+ phoneLines: splitPhoneLines(metadata.contact?.phone)
19
+ };
20
+ }
21
+ //# sourceMappingURL=unavailable-view-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unavailable-view-model.js","names":["stripOrgSuffix","organisation","split","splitPhoneLines","phone","undefined","lines","map","line","trim","filter","length","unavailableViewModel","metadata","pageTitle","formTitle","title","organisationName","phoneLines","contact"],"sources":["../../../../../src/server/plugins/engine/models/unavailable-view-model.ts"],"sourcesContent":["import { type FormMetadata } from '@defra/forms-model'\n\nexport interface UnavailableViewModel {\n pageTitle: string\n formTitle: string\n organisationName: string\n phoneLines?: string[]\n}\n\n/**\n * Defra organisations carry an abbreviation suffix on the enum value, e.g.\n * \"Rural Payments Agency – RPA\". The unavailable page reads cleanly without it.\n */\nfunction stripOrgSuffix(organisation: string) {\n return organisation.split(' – ')[0]\n}\n\nfunction splitPhoneLines(phone: string | undefined) {\n if (!phone) return undefined\n const lines = phone\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0)\n return lines.length > 0 ? lines : undefined\n}\n\nexport function unavailableViewModel(\n metadata: FormMetadata\n): UnavailableViewModel {\n return {\n pageTitle: 'Sorry, this form is unavailable',\n formTitle: metadata.title,\n organisationName: stripOrgSuffix(metadata.organisation),\n phoneLines: splitPhoneLines(metadata.contact?.phone)\n }\n}\n"],"mappings":"AASA;AACA;AACA;AACA;AACA,SAASA,cAAcA,CAACC,YAAoB,EAAE;EAC5C,OAAOA,YAAY,CAACC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACrC;AAEA,SAASC,eAAeA,CAACC,KAAyB,EAAE;EAClD,IAAI,CAACA,KAAK,EAAE,OAAOC,SAAS;EAC5B,MAAMC,KAAK,GAAGF,KAAK,CAChBF,KAAK,CAAC,IAAI,CAAC,CACXK,GAAG,CAAEC,IAAI,IAAKA,IAAI,CAACC,IAAI,CAAC,CAAC,CAAC,CAC1BC,MAAM,CAAEF,IAAI,IAAKA,IAAI,CAACG,MAAM,GAAG,CAAC,CAAC;EACpC,OAAOL,KAAK,CAACK,MAAM,GAAG,CAAC,GAAGL,KAAK,GAAGD,SAAS;AAC7C;AAEA,OAAO,SAASO,oBAAoBA,CAClCC,QAAsB,EACA;EACtB,OAAO;IACLC,SAAS,EAAE,iCAAiC;IAC5CC,SAAS,EAAEF,QAAQ,CAACG,KAAK;IACzBC,gBAAgB,EAAEjB,cAAc,CAACa,QAAQ,CAACZ,YAAY,CAAC;IACvDiB,UAAU,EAAEf,eAAe,CAACU,QAAQ,CAACM,OAAO,EAAEf,KAAK;EACrD,CAAC;AACH","ignoreList":[]}
@@ -5,6 +5,7 @@ import { getRoutes as getPaymentRoutes } from "./routes/payment.js";
5
5
  import { getRoutes as getQuestionRoutes } from "./routes/questions.js";
6
6
  import { getRoutes as getRepeaterItemDeleteRoutes } from "./routes/repeaters/item-delete.js";
7
7
  import { getRoutes as getRepeaterSummaryRoutes } from "./routes/repeaters/summary.js";
8
+ import { registerUnavailableResponse } from "./unavailable-response.js";
8
9
  import { registerVision } from "./vision.js";
9
10
  import { mapPlugin } from "../map/index.js";
10
11
  import { postcodeLookupPlugin } from "../postcode-lookup/index.js";
@@ -82,6 +83,11 @@ export const plugin = {
82
83
  };
83
84
  const routes = [...getPaymentRoutes(), ...getFileUploadStatusRoutes(), ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest), ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions, onRequest), ...getQuestionRoutes(getRouteOptions, postRouteOptions, preparePageEventRequestOptions, onRequest)];
84
85
  server.route(routes); // TODO
86
+
87
+ // Registration order is important: must be registered after the engine's
88
+ // routes so it sees their responses, but before any global error-page
89
+ // handler that would re-shape Boom errors.
90
+ registerUnavailableResponse(server);
85
91
  }
86
92
  };
87
93
  //# sourceMappingURL=plugin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":["validatePluginOptions","getRoutes","getFileUploadStatusRoutes","makeLoadFormPreHandler","getPaymentRoutes","getQuestionRoutes","getRepeaterItemDeleteRoutes","getRepeaterSummaryRoutes","registerVision","mapPlugin","postcodeLookupPlugin","CacheService","plugin","name","dependencies","multiple","register","server","options","model","cache","saveAndExit","nunjucks","nunjucksOptions","viewContext","preparePageEventRequestOptions","onRequest","ordnanceSurveyApiKey","baseUrl","ordnanceSurveyApiSecret","services","cacheService","cacheName","expose","baseLayoutPath","app","itemCache","Map","models","loadFormPreHandler","getRouteOptions","pre","method","postRouteOptions","payload","parse","routes","route"],"sources":["../../../../src/server/plugins/engine/plugin.ts"],"sourcesContent":["import {\n type Lifecycle,\n type Plugin,\n type RouteOptions,\n type Server,\n type ServerRoute\n} from '@hapi/hapi'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { validatePluginOptions } from '~/src/server/plugins/engine/options.js'\nimport { getRoutes as getFileUploadStatusRoutes } from '~/src/server/plugins/engine/routes/file-upload.js'\nimport { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js'\nimport { getRoutes as getPaymentRoutes } from '~/src/server/plugins/engine/routes/payment.js'\nimport { getRoutes as getQuestionRoutes } from '~/src/server/plugins/engine/routes/questions.js'\nimport { getRoutes as getRepeaterItemDeleteRoutes } from '~/src/server/plugins/engine/routes/repeaters/item-delete.js'\nimport { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engine/routes/repeaters/summary.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport { registerVision } from '~/src/server/plugins/engine/vision.js'\nimport { mapPlugin } from '~/src/server/plugins/map/index.js'\nimport { postcodeLookupPlugin } from '~/src/server/plugins/postcode-lookup/index.js'\nimport {\n type FormRequestPayloadRefs,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\nimport { CacheService } from '~/src/server/services/index.js'\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 options = validatePluginOptions(options)\n\n const {\n model,\n cache,\n saveAndExit,\n nunjucks: nunjucksOptions,\n viewContext,\n preparePageEventRequestOptions,\n onRequest,\n ordnanceSurveyApiKey,\n baseUrl,\n ordnanceSurveyApiSecret,\n services\n } = options\n\n const cacheService =\n typeof cache === 'string'\n ? new CacheService({ server, cacheName: cache })\n : cache\n\n await registerVision(server, options)\n\n // Register the postcode lookup plugin only if we have an OS api key\n if (ordnanceSurveyApiKey) {\n await server.register({\n plugin: postcodeLookupPlugin,\n options: {\n ordnanceSurveyApiKey\n }\n })\n }\n\n // Register the maps plugin only if we have an OS api key & secret\n if (ordnanceSurveyApiKey && ordnanceSurveyApiSecret) {\n await server.register({\n plugin: mapPlugin,\n options: {\n ordnanceSurveyApiKey,\n ordnanceSurveyApiSecret\n }\n })\n }\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n server.expose('saveAndExit', saveAndExit)\n server.expose('baseUrl', baseUrl)\n server.expose('services', services)\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 = makeLoadFormPreHandler(server, options)\n\n const getRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestRefs>\n }\n ]\n }\n\n const postRouteOptions: RouteOptions<FormRequestPayloadRefs> = {\n payload: {\n parse: true\n },\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestPayloadRefs>\n }\n ]\n }\n\n const routes = [\n ...getPaymentRoutes(),\n ...getFileUploadStatusRoutes(),\n ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest),\n ...getRepeaterItemDeleteRoutes(\n getRouteOptions,\n postRouteOptions,\n onRequest\n ),\n\n ...getQuestionRoutes(\n getRouteOptions,\n postRouteOptions,\n preparePageEventRequestOptions,\n onRequest\n )\n ]\n\n server.route(routes as unknown as ServerRoute[]) // TODO\n }\n} satisfies Plugin<PluginOptions>\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"plugin.js","names":["validatePluginOptions","getRoutes","getFileUploadStatusRoutes","makeLoadFormPreHandler","getPaymentRoutes","getQuestionRoutes","getRepeaterItemDeleteRoutes","getRepeaterSummaryRoutes","registerUnavailableResponse","registerVision","mapPlugin","postcodeLookupPlugin","CacheService","plugin","name","dependencies","multiple","register","server","options","model","cache","saveAndExit","nunjucks","nunjucksOptions","viewContext","preparePageEventRequestOptions","onRequest","ordnanceSurveyApiKey","baseUrl","ordnanceSurveyApiSecret","services","cacheService","cacheName","expose","baseLayoutPath","app","itemCache","Map","models","loadFormPreHandler","getRouteOptions","pre","method","postRouteOptions","payload","parse","routes","route"],"sources":["../../../../src/server/plugins/engine/plugin.ts"],"sourcesContent":["import {\n type Lifecycle,\n type Plugin,\n type RouteOptions,\n type Server,\n type ServerRoute\n} from '@hapi/hapi'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { validatePluginOptions } from '~/src/server/plugins/engine/options.js'\nimport { getRoutes as getFileUploadStatusRoutes } from '~/src/server/plugins/engine/routes/file-upload.js'\nimport { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js'\nimport { getRoutes as getPaymentRoutes } from '~/src/server/plugins/engine/routes/payment.js'\nimport { getRoutes as getQuestionRoutes } from '~/src/server/plugins/engine/routes/questions.js'\nimport { getRoutes as getRepeaterItemDeleteRoutes } from '~/src/server/plugins/engine/routes/repeaters/item-delete.js'\nimport { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engine/routes/repeaters/summary.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport { registerUnavailableResponse } from '~/src/server/plugins/engine/unavailable-response.js'\nimport { registerVision } from '~/src/server/plugins/engine/vision.js'\nimport { mapPlugin } from '~/src/server/plugins/map/index.js'\nimport { postcodeLookupPlugin } from '~/src/server/plugins/postcode-lookup/index.js'\nimport {\n type FormRequestPayloadRefs,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\nimport { CacheService } from '~/src/server/services/index.js'\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 options = validatePluginOptions(options)\n\n const {\n model,\n cache,\n saveAndExit,\n nunjucks: nunjucksOptions,\n viewContext,\n preparePageEventRequestOptions,\n onRequest,\n ordnanceSurveyApiKey,\n baseUrl,\n ordnanceSurveyApiSecret,\n services\n } = options\n\n const cacheService =\n typeof cache === 'string'\n ? new CacheService({ server, cacheName: cache })\n : cache\n\n await registerVision(server, options)\n\n // Register the postcode lookup plugin only if we have an OS api key\n if (ordnanceSurveyApiKey) {\n await server.register({\n plugin: postcodeLookupPlugin,\n options: {\n ordnanceSurveyApiKey\n }\n })\n }\n\n // Register the maps plugin only if we have an OS api key & secret\n if (ordnanceSurveyApiKey && ordnanceSurveyApiSecret) {\n await server.register({\n plugin: mapPlugin,\n options: {\n ordnanceSurveyApiKey,\n ordnanceSurveyApiSecret\n }\n })\n }\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n server.expose('saveAndExit', saveAndExit)\n server.expose('baseUrl', baseUrl)\n server.expose('services', services)\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 = makeLoadFormPreHandler(server, options)\n\n const getRouteOptions: RouteOptions<FormRequestRefs> = {\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestRefs>\n }\n ]\n }\n\n const postRouteOptions: RouteOptions<FormRequestPayloadRefs> = {\n payload: {\n parse: true\n },\n pre: [\n {\n method:\n loadFormPreHandler as unknown as Lifecycle.Method<FormRequestPayloadRefs>\n }\n ]\n }\n\n const routes = [\n ...getPaymentRoutes(),\n ...getFileUploadStatusRoutes(),\n ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest),\n ...getRepeaterItemDeleteRoutes(\n getRouteOptions,\n postRouteOptions,\n onRequest\n ),\n\n ...getQuestionRoutes(\n getRouteOptions,\n postRouteOptions,\n preparePageEventRequestOptions,\n onRequest\n )\n ]\n\n server.route(routes as unknown as ServerRoute[]) // TODO\n\n // Registration order is important: must be registered after the engine's\n // routes so it sees their responses, but before any global error-page\n // handler that would re-shape Boom errors.\n registerUnavailableResponse(server)\n }\n} satisfies Plugin<PluginOptions>\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,9 @@
1
+ import { type Server } from '@hapi/hapi';
2
+ /**
3
+ * Registers a server-wide onPreResponse extension that intercepts the offline
4
+ * Boom thrown and renders the unavailable view.
5
+ *
6
+ * Must be registered after the engine's routes so it sees their responses,
7
+ * but before any global error-page handler that would re-shape Boom errors.
8
+ */
9
+ export declare function registerUnavailableResponse(server: Server): void;
@@ -0,0 +1,23 @@
1
+ import { isOfflineBoom } from "./form-availability.js";
2
+ import { unavailableViewModel } from "./models/unavailable-view-model.js";
3
+
4
+ /**
5
+ * Registers a server-wide onPreResponse extension that intercepts the offline
6
+ * Boom thrown and renders the unavailable view.
7
+ *
8
+ * Must be registered after the engine's routes so it sees their responses,
9
+ * but before any global error-page handler that would re-shape Boom errors.
10
+ */
11
+ export function registerUnavailableResponse(server) {
12
+ server.ext('onPreResponse', (request, h) => {
13
+ const response = request.response;
14
+ if (!isOfflineBoom(response)) {
15
+ return h.continue;
16
+ }
17
+ const {
18
+ metadata
19
+ } = response.data;
20
+ return h.view('unavailable', unavailableViewModel(metadata)).header('Cache-Control', 'no-store, no-cache, must-revalidate').header('X-Robots-Tag', 'noindex, nofollow').code(200).takeover();
21
+ });
22
+ }
23
+ //# sourceMappingURL=unavailable-response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unavailable-response.js","names":["isOfflineBoom","unavailableViewModel","registerUnavailableResponse","server","ext","request","h","response","continue","metadata","data","view","header","code","takeover"],"sources":["../../../../src/server/plugins/engine/unavailable-response.ts"],"sourcesContent":["import { type Request, type ResponseToolkit, type Server } from '@hapi/hapi'\n\nimport { isOfflineBoom } from '~/src/server/plugins/engine/form-availability.js'\nimport { unavailableViewModel } from '~/src/server/plugins/engine/models/unavailable-view-model.js'\n\n/**\n * Registers a server-wide onPreResponse extension that intercepts the offline\n * Boom thrown and renders the unavailable view.\n *\n * Must be registered after the engine's routes so it sees their responses,\n * but before any global error-page handler that would re-shape Boom errors.\n */\nexport function registerUnavailableResponse(server: Server) {\n server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {\n const response = request.response\n if (!isOfflineBoom(response)) {\n return h.continue\n }\n\n const { metadata } = response.data\n\n return h\n .view('unavailable', unavailableViewModel(metadata))\n .header('Cache-Control', 'no-store, no-cache, must-revalidate')\n .header('X-Robots-Tag', 'noindex, nofollow')\n .code(200)\n .takeover()\n })\n}\n"],"mappings":"AAEA,SAASA,aAAa;AACtB,SAASC,oBAAoB;;AAE7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,2BAA2BA,CAACC,MAAc,EAAE;EAC1DA,MAAM,CAACC,GAAG,CAAC,eAAe,EAAE,CAACC,OAAgB,EAAEC,CAAkB,KAAK;IACpE,MAAMC,QAAQ,GAAGF,OAAO,CAACE,QAAQ;IACjC,IAAI,CAACP,aAAa,CAACO,QAAQ,CAAC,EAAE;MAC5B,OAAOD,CAAC,CAACE,QAAQ;IACnB;IAEA,MAAM;MAAEC;IAAS,CAAC,GAAGF,QAAQ,CAACG,IAAI;IAElC,OAAOJ,CAAC,CACLK,IAAI,CAAC,aAAa,EAAEV,oBAAoB,CAACQ,QAAQ,CAAC,CAAC,CACnDG,MAAM,CAAC,eAAe,EAAE,qCAAqC,CAAC,CAC9DA,MAAM,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAC3CC,IAAI,CAAC,GAAG,CAAC,CACTC,QAAQ,CAAC,CAAC;EACf,CAAC,CAAC;AACJ","ignoreList":[]}
@@ -0,0 +1,20 @@
1
+ {% extends baseLayoutPath %}
2
+
3
+ {% block content %}
4
+ <div class="govuk-grid-row">
5
+ <div class="govuk-grid-column-two-thirds">
6
+ <h1 class="govuk-heading-l">Sorry, this form is unavailable</h1>
7
+ <p class="govuk-body">'{{ formTitle }}' has been archived and is no longer available.</p>
8
+ <p class="govuk-body">Contact the {{ organisationName }}.</p>
9
+
10
+ {% if phoneLines %}
11
+ <ul class="govuk-list govuk-list--bullet">
12
+ {% for line in phoneLines %}
13
+ <li>{{ line }}</li>
14
+ {% endfor %}
15
+ </ul>
16
+ <p class="govuk-body"><a href="https://www.gov.uk/call-charges" class="govuk-link govuk-link--no-visited-state">Find out about call charges</a></p>
17
+ {% endif %}
18
+ </div>
19
+ </div>
20
+ {% endblock %}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.js","names":[],"sources":["../../../src/typings/hapi/index.d.ts"],"sourcesContent":["import { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport {\n type COMPONENT_STATE_ERROR,\n type EXTERNAL_STATE_APPENDAGE,\n type EXTERNAL_STATE_PAYLOAD\n} from '~/src/server/constants.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type AnyFormRequest,\n type FormSubmissionError,\n type PluginOptions\n} from '~/src/server/plugins/engine/types.ts'\nimport { type CacheService } from '~/src/server/services/index.js'\n\ndeclare module '@hapi/yar' {\n interface YarFlashes {\n [EXTERNAL_STATE_APPENDAGE]: object\n [EXTERNAL_STATE_PAYLOAD]: object\n [COMPONENT_STATE_ERROR]: string\n [key: string]: { errors: FormSubmissionError[] }\n }\n}\n\ndeclare module '@hapi/hapi' {\n // Here we are decorating Hapi interface types with\n // props from plugins which doesn't export @types\n interface PluginProperties {\n crumb: {\n generate?: (request: AnyRequest) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: AnyFormRequest | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\n saveAndExit?: PluginOptions['saveAndExit']\n baseUrl: string\n services: PluginOptions['services']\n }\n }\n\n interface Request {\n logger: Logger\n yar: Yar\n }\n\n interface RequestApplicationState {\n model?: FormModel\n }\n\n interface Server {\n logger: Logger\n yar: ServerYar\n }\n\n interface ServerApplicationState {\n model?: FormModel\n models: Map<string, { model: FormModel; updatedAt: Date }>\n }\n}\n\ndeclare module 'blankie' {\n declare const blankie: {\n plugin: Plugin<Record<string, boolean | string | string[]>>\n }\n\n export = blankie\n}\n\ndeclare module 'blipp' {\n declare const blipp: {\n plugin: Plugin\n }\n\n export = blipp\n}\n\ndeclare module 'hapi-pulse' {\n declare const hapiPulse: {\n plugin: Plugin<{\n timeout: number\n }>\n }\n\n export = hapiPulse\n}\n"],"mappings":"","ignoreList":[]}
1
+ {"version":3,"file":"index.d.js","names":[],"sources":["../../../src/typings/hapi/index.d.ts"],"sourcesContent":["import { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport {\n type COMPONENT_STATE_ERROR,\n type EXTERNAL_STATE_APPENDAGE,\n type EXTERNAL_STATE_PAYLOAD\n} from '~/src/server/constants.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type AnyFormRequest,\n type FormSubmissionError,\n type PluginOptions\n} from '~/src/server/plugins/engine/types.ts'\nimport { type CacheService } from '~/src/server/services/index.js'\n\ndeclare module '@hapi/yar' {\n interface YarFlashes {\n [EXTERNAL_STATE_APPENDAGE]: object\n [EXTERNAL_STATE_PAYLOAD]: object\n [COMPONENT_STATE_ERROR]: string\n [key: string]: { errors: FormSubmissionError[] }\n }\n}\n\ndeclare module '@hapi/hapi' {\n // Here we are decorating Hapi interface types with\n // props from plugins which doesn't export @types\n interface PluginProperties {\n crumb: {\n generate?: (request: AnyRequest) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: AnyFormRequest | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\n saveAndExit?: PluginOptions['saveAndExit']\n baseUrl: string\n services: PluginOptions['services']\n }\n }\n\n interface Request {\n logger: Logger\n yar: Yar\n }\n\n interface RequestApplicationState {\n model?: FormModel\n }\n\n interface Server {\n logger: Logger\n yar: ServerYar\n }\n\n interface ServerApplicationState {\n model?: FormModel\n models?: Map<string, { model: FormModel; updatedAt: Date }>\n }\n}\n\ndeclare module 'blankie' {\n declare const blankie: {\n plugin: Plugin<Record<string, boolean | string | string[]>>\n }\n\n export = blankie\n}\n\ndeclare module 'blipp' {\n declare const blipp: {\n plugin: Plugin\n }\n\n export = blipp\n}\n\ndeclare module 'hapi-pulse' {\n declare const hapiPulse: {\n plugin: Plugin<{\n timeout: number\n }>\n }\n\n export = hapiPulse\n}\n"],"mappings":"","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.11.3",
3
+ "version": "4.12.0",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -44,10 +44,10 @@
44
44
  "dev:debug": "concurrently \"npm run client:watch\" \"npm run server:watch:debug\" --kill-others --names \"client,server\" --prefix-colors \"red.dim,blue.dim\"",
45
45
  "format": "npm run format:check -- --write",
46
46
  "format:check": "prettier --cache --cache-location .cache/prettier --cache-strategy content --check \"**/*.{cjs,js,json,md,mjs,scss,ts}\"",
47
- "predocs:dev": "node scripts/generate-schema-docs.js && node scripts/generate-component-docs.js",
47
+ "predocs:dev": "node scripts/generate-schema-docs.js && tsx scripts/generate-component-docs.js",
48
48
  "docs:dev": "BROWSERSLIST_ENV=javascripts docusaurus start --host 0.0.0.0",
49
49
  "docs:build": "BROWSERSLIST_ENV=javascripts docusaurus build",
50
- "docs:build:all": "node scripts/generate-schema-docs.js && node scripts/generate-component-docs.js && npm run docs:build",
50
+ "docs:build:all": "node scripts/generate-schema-docs.js && tsx scripts/generate-component-docs.js && npm run docs:build",
51
51
  "docs:serve": "docusaurus serve --host 0.0.0.0",
52
52
  "docs:clear": "docusaurus clear",
53
53
  "generate-schema-docs": "node scripts/generate-schema-docs.js",
@@ -87,7 +87,7 @@
87
87
  },
88
88
  "license": "SEE LICENSE IN LICENSE",
89
89
  "dependencies": {
90
- "@defra/forms-model": "^3.0.655",
90
+ "@defra/forms-model": "^3.0.663",
91
91
  "@defra/hapi-tracing": "^1.29.0",
92
92
  "@defra/interactive-map": "^0.0.22-alpha",
93
93
  "@elastic/ecs-pino-format": "^1.5.0",
@@ -3,6 +3,7 @@ import { type Request, type Server } from '@hapi/hapi'
3
3
  import { isEqual } from 'date-fns'
4
4
 
5
5
  import { PREVIEW_PATH_PREFIX } from '../../../constants.js'
6
+ import { assertFormAvailable } from '../form-availability.js'
6
7
  import {
7
8
  checkEmailAddressForLiveFormSubmission,
8
9
  getCacheService
@@ -52,6 +53,7 @@ export async function getFormModel(
52
53
  const formState = resolveState(state)
53
54
 
54
55
  const metadata = await formsService.getFormMetadata(slug)
56
+ assertFormAvailable(metadata)
55
57
 
56
58
  const definition = await formsService.getFormDefinition(
57
59
  metadata.id,
@@ -134,6 +136,7 @@ export async function resolveFormModel(
134
136
  const { formsService } = services
135
137
 
136
138
  const metadata = await formsService.getFormMetadata(slug)
139
+ assertFormAvailable(metadata)
137
140
  const formState = resolveState(state)
138
141
  const isPreview = options.isPreview ?? isPreviewState(state, options)
139
142
  const stateMetadata = metadata[formState]
@@ -145,15 +148,10 @@ export async function resolveFormModel(
145
148
  }
146
149
 
147
150
  // The models cache is created lazily per server instance
148
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
149
- if (!server.app.models) {
150
- server.app.models = new Map<string, { model: FormModel; updatedAt: Date }>()
151
- }
152
151
 
153
- const cache = server.app.models as Map<
154
- string,
155
- { model: FormModel; updatedAt: Date }
156
- >
152
+ server.app.models ??= new Map<string, { model: FormModel; updatedAt: Date }>()
153
+
154
+ const cache = server.app.models
157
155
 
158
156
  const cacheKey = `${metadata.id}_${formState}_${isPreview}`
159
157
  let entry = cache.get(cacheKey)
@@ -116,7 +116,7 @@ export function getGeospatialSchema(country?: GeospatialFieldOptionsCountry) {
116
116
  return value
117
117
  }
118
118
 
119
- const result = booleanWithin(value, countryFeature)
119
+ const result = booleanWithin(value as Geometry | Feature, countryFeature)
120
120
 
121
121
  if (!result) {
122
122
  return helpers.error('any.custom', {
@@ -0,0 +1,31 @@
1
+ import { type FormMetadata } from '@defra/forms-model'
2
+ import Boom from '@hapi/boom'
3
+
4
+ export interface OfflineBoomData {
5
+ offline: true
6
+ metadata: FormMetadata
7
+ }
8
+
9
+ /**
10
+ * Throws when the form has been taken offline. The plugin's
11
+ * unavailable-response extension catches the marker and renders the
12
+ * "Sorry, this form is unavailable" view at HTTP 200.
13
+ */
14
+ export function assertFormAvailable(metadata: FormMetadata): void {
15
+ if (metadata.offline === true) {
16
+ const data: OfflineBoomData = { offline: true, metadata }
17
+ throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), {
18
+ statusCode: 503,
19
+ data
20
+ })
21
+ }
22
+ }
23
+
24
+ /** Type guard for the offline Boom marker. */
25
+ export function isOfflineBoom(
26
+ err: unknown
27
+ ): err is Boom.Boom<OfflineBoomData> & { data: OfflineBoomData } {
28
+ if (!Boom.isBoom(err)) return false
29
+ const data = err.data as Partial<OfflineBoomData> | null | undefined
30
+ return data?.offline === true && !!data.metadata
31
+ }
@@ -0,0 +1,36 @@
1
+ import { type FormMetadata } from '@defra/forms-model'
2
+
3
+ export interface UnavailableViewModel {
4
+ pageTitle: string
5
+ formTitle: string
6
+ organisationName: string
7
+ phoneLines?: string[]
8
+ }
9
+
10
+ /**
11
+ * Defra organisations carry an abbreviation suffix on the enum value, e.g.
12
+ * "Rural Payments Agency – RPA". The unavailable page reads cleanly without it.
13
+ */
14
+ function stripOrgSuffix(organisation: string) {
15
+ return organisation.split(' – ')[0]
16
+ }
17
+
18
+ function splitPhoneLines(phone: string | undefined) {
19
+ if (!phone) return undefined
20
+ const lines = phone
21
+ .split('\n')
22
+ .map((line) => line.trim())
23
+ .filter((line) => line.length > 0)
24
+ return lines.length > 0 ? lines : undefined
25
+ }
26
+
27
+ export function unavailableViewModel(
28
+ metadata: FormMetadata
29
+ ): UnavailableViewModel {
30
+ return {
31
+ pageTitle: 'Sorry, this form is unavailable',
32
+ formTitle: metadata.title,
33
+ organisationName: stripOrgSuffix(metadata.organisation),
34
+ phoneLines: splitPhoneLines(metadata.contact?.phone)
35
+ }
36
+ }
@@ -15,6 +15,7 @@ import { getRoutes as getQuestionRoutes } from './routes/questions.js'
15
15
  import { getRoutes as getRepeaterItemDeleteRoutes } from './routes/repeaters/item-delete.js'
16
16
  import { getRoutes as getRepeaterSummaryRoutes } from './routes/repeaters/summary.js'
17
17
  import { type PluginOptions } from './types.js'
18
+ import { registerUnavailableResponse } from './unavailable-response.js'
18
19
  import { registerVision } from './vision.js'
19
20
  import { mapPlugin } from '../map/index.js'
20
21
  import { postcodeLookupPlugin } from '../postcode-lookup/index.js'
@@ -129,5 +130,10 @@ export const plugin = {
129
130
  ]
130
131
 
131
132
  server.route(routes as unknown as ServerRoute[]) // TODO
133
+
134
+ // Registration order is important: must be registered after the engine's
135
+ // routes so it sees their responses, but before any global error-page
136
+ // handler that would re-shape Boom errors.
137
+ registerUnavailableResponse(server)
132
138
  }
133
139
  } satisfies Plugin<PluginOptions>
@@ -0,0 +1,29 @@
1
+ import { type Request, type ResponseToolkit, type Server } from '@hapi/hapi'
2
+
3
+ import { isOfflineBoom } from './form-availability.js'
4
+ import { unavailableViewModel } from './models/unavailable-view-model.js'
5
+
6
+ /**
7
+ * Registers a server-wide onPreResponse extension that intercepts the offline
8
+ * Boom thrown and renders the unavailable view.
9
+ *
10
+ * Must be registered after the engine's routes so it sees their responses,
11
+ * but before any global error-page handler that would re-shape Boom errors.
12
+ */
13
+ export function registerUnavailableResponse(server: Server) {
14
+ server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
15
+ const response = request.response
16
+ if (!isOfflineBoom(response)) {
17
+ return h.continue
18
+ }
19
+
20
+ const { metadata } = response.data
21
+
22
+ return h
23
+ .view('unavailable', unavailableViewModel(metadata))
24
+ .header('Cache-Control', 'no-store, no-cache, must-revalidate')
25
+ .header('X-Robots-Tag', 'noindex, nofollow')
26
+ .code(200)
27
+ .takeover()
28
+ })
29
+ }
@@ -0,0 +1,20 @@
1
+ {% extends baseLayoutPath %}
2
+
3
+ {% block content %}
4
+ <div class="govuk-grid-row">
5
+ <div class="govuk-grid-column-two-thirds">
6
+ <h1 class="govuk-heading-l">Sorry, this form is unavailable</h1>
7
+ <p class="govuk-body">'{{ formTitle }}' has been archived and is no longer available.</p>
8
+ <p class="govuk-body">Contact the {{ organisationName }}.</p>
9
+
10
+ {% if phoneLines %}
11
+ <ul class="govuk-list govuk-list--bullet">
12
+ {% for line in phoneLines %}
13
+ <li>{{ line }}</li>
14
+ {% endfor %}
15
+ </ul>
16
+ <p class="govuk-body"><a href="https://www.gov.uk/call-charges" class="govuk-link govuk-link--no-visited-state">Find out about call charges</a></p>
17
+ {% endif %}
18
+ </div>
19
+ </div>
20
+ {% endblock %}
@@ -59,7 +59,7 @@ declare module '@hapi/hapi' {
59
59
 
60
60
  interface ServerApplicationState {
61
61
  model?: FormModel
62
- models: Map<string, { model: FormModel; updatedAt: Date }>
62
+ models?: Map<string, { model: FormModel; updatedAt: Date }>
63
63
  }
64
64
  }
65
65