@defra/forms-engine-plugin 1.4.0 → 2.0.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 (78) hide show
  1. package/.server/server/plugins/engine/README.md +2 -46
  2. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
  3. package/.server/server/plugins/engine/configureEnginePlugin.js +5 -2
  4. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  5. package/.server/server/plugins/engine/helpers.d.ts +11 -0
  6. package/.server/server/plugins/engine/helpers.js +7 -1
  7. package/.server/server/plugins/engine/helpers.js.map +1 -1
  8. package/.server/server/plugins/engine/models/FormModel.js +2 -2
  9. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  10. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -1
  11. package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -1
  12. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  13. package/.server/server/plugins/engine/options.js +5 -3
  14. package/.server/server/plugins/engine/options.js.map +1 -1
  15. package/.server/server/plugins/engine/options.test.js +18 -9
  16. package/.server/server/plugins/engine/options.test.js.map +1 -1
  17. package/.server/server/plugins/engine/pageControllers/PageController.d.ts +3 -1
  18. package/.server/server/plugins/engine/pageControllers/PageController.js +5 -1
  19. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  20. package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +1 -1
  21. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +2 -4
  22. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  23. package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -2
  24. package/.server/server/plugins/engine/pageControllers/StartPageController.js +1 -3
  25. package/.server/server/plugins/engine/pageControllers/StartPageController.js.map +1 -1
  26. package/.server/server/plugins/engine/pageControllers/StatusPageController.d.ts +1 -0
  27. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +1 -0
  28. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  29. package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +0 -1
  30. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +1 -4
  31. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  32. package/.server/server/plugins/engine/pageControllers/TerminalPageController.d.ts +1 -0
  33. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js +1 -0
  34. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js.map +1 -1
  35. package/.server/server/plugins/engine/pageControllers/__stubs__/request.d.ts +4 -0
  36. package/.server/server/plugins/engine/pageControllers/__stubs__/request.js +14 -0
  37. package/.server/server/plugins/engine/pageControllers/__stubs__/request.js.map +1 -0
  38. package/.server/server/plugins/engine/pageControllers/__stubs__/server.d.ts +3 -0
  39. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js +23 -0
  40. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js.map +1 -0
  41. package/.server/server/plugins/engine/plugin.js +5 -6
  42. package/.server/server/plugins/engine/plugin.js.map +1 -1
  43. package/.server/server/plugins/engine/types.d.ts +7 -5
  44. package/.server/server/plugins/engine/types.js.map +1 -1
  45. package/.server/server/types.d.ts +2 -1
  46. package/.server/server/types.js.map +1 -1
  47. package/.server/typings/hapi/index.d.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/server/plugins/engine/README.md +2 -46
  50. package/src/server/plugins/engine/configureEnginePlugin.ts +4 -2
  51. package/src/server/plugins/engine/helpers.test.ts +3 -2
  52. package/src/server/plugins/engine/helpers.ts +9 -1
  53. package/src/server/plugins/engine/models/FormModel.test.ts +96 -21
  54. package/src/server/plugins/engine/models/FormModel.ts +5 -2
  55. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +7 -5
  56. package/src/server/plugins/engine/models/SummaryViewModel.ts +1 -1
  57. package/src/server/plugins/engine/options.js +5 -3
  58. package/src/server/plugins/engine/options.test.js +22 -11
  59. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +3 -3
  60. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +12 -1
  61. package/src/server/plugins/engine/pageControllers/PageController.test.ts +9 -0
  62. package/src/server/plugins/engine/pageControllers/PageController.ts +10 -1
  63. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +34 -28
  64. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +2 -5
  65. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +19 -4
  66. package/src/server/plugins/engine/pageControllers/StartPageController.test.ts +32 -0
  67. package/src/server/plugins/engine/pageControllers/StartPageController.ts +2 -4
  68. package/src/server/plugins/engine/pageControllers/StatusPageController.test.ts +32 -0
  69. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +1 -0
  70. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +1 -5
  71. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +9 -0
  72. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +1 -0
  73. package/src/server/plugins/engine/pageControllers/__stubs__/request.ts +21 -0
  74. package/src/server/plugins/engine/pageControllers/__stubs__/server.ts +27 -0
  75. package/src/server/plugins/engine/plugin.ts +6 -6
  76. package/src/server/plugins/engine/types.ts +14 -9
  77. package/src/server/types.ts +2 -0
  78. package/src/typings/hapi/index.d.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"TerminalPageController.js","names":["Boom","QuestionPageController","TerminalPageController","makePostRouteHandler","methodNotAllowed"],"sources":["../../../../../src/server/plugins/engine/pageControllers/TerminalPageController.ts"],"sourcesContent":["import { type PageTerminal } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type ResponseObject, type ResponseToolkit } from '@hapi/hapi'\n\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport { type FormRequestPayload } from '~/src/server/routes/types.js'\n\nexport class TerminalPageController extends QuestionPageController {\n declare pageDef: PageTerminal\n\n makePostRouteHandler(): (\n request: FormRequestPayload,\n context: FormContext,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => Promise<ResponseObject> {\n throw Boom.methodNotAllowed('POST method not allowed for terminal pages')\n }\n}\n"],"mappings":"AACA,OAAOA,IAAI,MAAM,YAAY;AAG7B,SAASC,sBAAsB;AAI/B,OAAO,MAAMC,sBAAsB,SAASD,sBAAsB,CAAC;EAGjEE,oBAAoBA,CAAA,EAIS;IAC3B,MAAMH,IAAI,CAACI,gBAAgB,CAAC,4CAA4C,CAAC;EAC3E;AACF","ignoreList":[]}
1
+ {"version":3,"file":"TerminalPageController.js","names":["Boom","QuestionPageController","TerminalPageController","allowSaveAndReturn","makePostRouteHandler","methodNotAllowed"],"sources":["../../../../../src/server/plugins/engine/pageControllers/TerminalPageController.ts"],"sourcesContent":["import { type PageTerminal } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type ResponseObject, type ResponseToolkit } from '@hapi/hapi'\n\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport { type FormRequestPayload } from '~/src/server/routes/types.js'\n\nexport class TerminalPageController extends QuestionPageController {\n declare pageDef: PageTerminal\n allowSaveAndReturn = false\n\n makePostRouteHandler(): (\n request: FormRequestPayload,\n context: FormContext,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => Promise<ResponseObject> {\n throw Boom.methodNotAllowed('POST method not allowed for terminal pages')\n }\n}\n"],"mappings":"AACA,OAAOA,IAAI,MAAM,YAAY;AAG7B,SAASC,sBAAsB;AAI/B,OAAO,MAAMC,sBAAsB,SAASD,sBAAsB,CAAC;EAEjEE,kBAAkB,GAAG,KAAK;EAE1BC,oBAAoBA,CAAA,EAIS;IAC3B,MAAMJ,IAAI,CAACK,gBAAgB,CAAC,4CAA4C,CAAC;EAC3E;AACF","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ import { type FormContextRequest } from '~/src/server/plugins/engine/types.js';
2
+ import { type FormRequest } from '~/src/server/routes/types.js';
3
+ export declare function buildFormRequest(request: Omit<FormRequest, 'server'>): FormRequest;
4
+ export declare function buildFormContextRequest(request: Omit<FormContextRequest, 'server'>): FormContextRequest;
@@ -0,0 +1,14 @@
1
+ import { server } from "./server.js";
2
+ export function buildFormRequest(request) {
3
+ return {
4
+ ...request,
5
+ server
6
+ };
7
+ }
8
+ export function buildFormContextRequest(request) {
9
+ return {
10
+ ...request,
11
+ server
12
+ };
13
+ }
14
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","names":["server","buildFormRequest","request","buildFormContextRequest"],"sources":["../../../../../../src/server/plugins/engine/pageControllers/__stubs__/request.ts"],"sourcesContent":["import { server } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'\nimport { type FormContextRequest } from '~/src/server/plugins/engine/types.js'\nimport { type FormRequest } from '~/src/server/routes/types.js'\n\nexport function buildFormRequest(\n request: Omit<FormRequest, 'server'>\n): FormRequest {\n return {\n ...request,\n server\n } as FormRequest\n}\n\nexport function buildFormContextRequest(\n request: Omit<FormContextRequest, 'server'>\n): FormContextRequest {\n return {\n ...request,\n server\n } as FormContextRequest\n}\n"],"mappings":"AAAA,SAASA,MAAM;AAIf,OAAO,SAASC,gBAAgBA,CAC9BC,OAAoC,EACvB;EACb,OAAO;IACL,GAAGA,OAAO;IACVF;EACF,CAAC;AACH;AAEA,OAAO,SAASG,uBAAuBA,CACrCD,OAA2C,EACvB;EACpB,OAAO;IACL,GAAGA,OAAO;IACVF;EACF,CAAC;AACH","ignoreList":[]}
@@ -0,0 +1,3 @@
1
+ import { type Server } from '@hapi/hapi';
2
+ export declare const server: Server;
3
+ export declare const serverWithSaveAndReturn: Server;
@@ -0,0 +1,23 @@
1
+ export const server = {
2
+ plugins: {
3
+ 'forms-engine-plugin': {
4
+ baseLayoutPath: '',
5
+ cacheService: {}
6
+ }
7
+ }
8
+ }; // only mocking out properties we care about;
9
+
10
+ export const serverWithSaveAndReturn = {
11
+ plugins: {
12
+ ...server.plugins,
13
+ 'forms-engine-plugin': {
14
+ ...server.plugins['forms-engine-plugin'],
15
+ saveAndReturn: {
16
+ keyGenerator: jest.fn().mockReturnValue('foobar'),
17
+ sessionHydrator: jest.fn().mockReturnValue({}),
18
+ sessionPersister: jest.fn().mockImplementation(() => Promise.resolve())
19
+ }
20
+ }
21
+ }
22
+ }; // only mocking out properties we care about
23
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":["server","plugins","baseLayoutPath","cacheService","serverWithSaveAndReturn","saveAndReturn","keyGenerator","jest","fn","mockReturnValue","sessionHydrator","sessionPersister","mockImplementation","Promise","resolve"],"sources":["../../../../../../src/server/plugins/engine/pageControllers/__stubs__/server.ts"],"sourcesContent":["import { type Server } from '@hapi/hapi'\n\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport { type CacheService } from '~/src/server/services/index.js'\n\nexport const server: Server = {\n plugins: {\n 'forms-engine-plugin': {\n baseLayoutPath: '',\n cacheService: {} as CacheService\n }\n }\n} as Server // only mocking out properties we care about;\n\nexport const serverWithSaveAndReturn: Server = {\n plugins: {\n ...server.plugins,\n 'forms-engine-plugin': {\n ...server.plugins['forms-engine-plugin'],\n saveAndReturn: {\n keyGenerator: jest.fn().mockReturnValue('foobar'),\n sessionHydrator: jest.fn().mockReturnValue({}),\n sessionPersister: jest.fn().mockImplementation(() => Promise.resolve())\n } as Pick<PluginOptions, 'saveAndReturn'>\n }\n }\n} as Server // only mocking out properties we care about\n"],"mappings":"AAKA,OAAO,MAAMA,MAAc,GAAG;EAC5BC,OAAO,EAAE;IACP,qBAAqB,EAAE;MACrBC,cAAc,EAAE,EAAE;MAClBC,YAAY,EAAE,CAAC;IACjB;EACF;AACF,CAAW,EAAC;;AAEZ,OAAO,MAAMC,uBAA+B,GAAG;EAC7CH,OAAO,EAAE;IACP,GAAGD,MAAM,CAACC,OAAO;IACjB,qBAAqB,EAAE;MACrB,GAAGD,MAAM,CAACC,OAAO,CAAC,qBAAqB,CAAC;MACxCI,aAAa,EAAE;QACbC,YAAY,EAAEC,IAAI,CAACC,EAAE,CAAC,CAAC,CAACC,eAAe,CAAC,QAAQ,CAAC;QACjDC,eAAe,EAAEH,IAAI,CAACC,EAAE,CAAC,CAAC,CAACC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC9CE,gBAAgB,EAAEJ,IAAI,CAACC,EAAE,CAAC,CAAC,CAACI,kBAAkB,CAAC,MAAMC,OAAO,CAACC,OAAO,CAAC,CAAC;MACxE;IACF;EACF;AACF,CAAW,EAAC","ignoreList":[]}
@@ -16,9 +16,7 @@ export const plugin = {
16
16
  const {
17
17
  model,
18
18
  cacheName,
19
- keyGenerator,
20
- sessionHydrator,
21
- sessionPersister,
19
+ saveAndReturn,
22
20
  nunjucks: nunjucksOptions,
23
21
  viewContext,
24
22
  preparePageEventRequestOptions
@@ -27,15 +25,16 @@ export const plugin = {
27
25
  server,
28
26
  cacheName,
29
27
  options: {
30
- keyGenerator,
31
- sessionHydrator,
32
- sessionPersister
28
+ keyGenerator: saveAndReturn?.keyGenerator,
29
+ sessionHydrator: saveAndReturn?.sessionHydrator,
30
+ sessionPersister: saveAndReturn?.sessionPersister
33
31
  }
34
32
  });
35
33
  await registerVision(server, options);
36
34
  server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath);
37
35
  server.expose('viewContext', viewContext);
38
36
  server.expose('cacheService', cacheService);
37
+ server.expose('saveAndReturn', saveAndReturn);
39
38
  server.app.model = model;
40
39
 
41
40
  // In-memory cache of FormModel items, exposed
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","names":["validatePluginOptions","getRoutes","getSaveAndReturnExitRoutes","getFileUploadStatusRoutes","makeLoadFormPreHandler","getQuestionRoutes","getRepeaterItemDeleteRoutes","getRepeaterSummaryRoutes","registerVision","CacheService","plugin","name","dependencies","multiple","register","server","options","model","cacheName","keyGenerator","sessionHydrator","sessionPersister","nunjucks","nunjucksOptions","viewContext","preparePageEventRequestOptions","cacheService","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 getSaveAndReturnExitRoutes } from '~/src/server/plugins/engine/routes/exit.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 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 {\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 cacheName,\n keyGenerator,\n sessionHydrator,\n sessionPersister,\n nunjucks: nunjucksOptions,\n viewContext,\n preparePageEventRequestOptions\n } = options\n const cacheService = new CacheService({\n server,\n cacheName,\n options: {\n keyGenerator,\n sessionHydrator,\n sessionPersister\n }\n })\n\n await registerVision(server, options)\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n\n server.app.model = model\n\n // In-memory cache of FormModel items, exposed\n // (for testing purposes) through `server.app.models`\n const itemCache = new Map<string, { model: FormModel; updatedAt: Date }>()\n server.app.models = itemCache\n\n const loadFormPreHandler = 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 ...getQuestionRoutes(\n getRouteOptions,\n postRouteOptions,\n preparePageEventRequestOptions\n ),\n ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions),\n ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions),\n ...getSaveAndReturnExitRoutes(getRouteOptions),\n ...getFileUploadStatusRoutes()\n ]\n\n server.route(routes as unknown as ServerRoute[]) // TODO\n }\n} satisfies Plugin<PluginOptions>\n"],"mappings":"AASA,SAASA,qBAAqB;AAC9B,SAASC,SAAS,IAAIC,0BAA0B;AAChD,SAASD,SAAS,IAAIE,yBAAyB;AAC/C,SAASC,sBAAsB;AAC/B,SAASH,SAAS,IAAII,iBAAiB;AACvC,SAASJ,SAAS,IAAIK,2BAA2B;AACjD,SAASL,SAAS,IAAIM,wBAAwB;AAE9C,SAASC,cAAc;AAKvB,SAASC,YAAY;AAErB,OAAO,MAAMC,MAAM,GAAG;EACpBC,IAAI,EAAE,4BAA4B;EAClCC,YAAY,EAAE,CAAC,aAAa,EAAE,WAAW,EAAE,WAAW,CAAC;EACvDC,QAAQ,EAAE,IAAI;EACd,MAAMC,QAAQA,CAACC,MAAc,EAAEC,OAAsB,EAAE;IACrDA,OAAO,GAAGhB,qBAAqB,CAACgB,OAAO,CAAC;IAExC,MAAM;MACJC,KAAK;MACLC,SAAS;MACTC,YAAY;MACZC,eAAe;MACfC,gBAAgB;MAChBC,QAAQ,EAAEC,eAAe;MACzBC,WAAW;MACXC;IACF,CAAC,GAAGT,OAAO;IACX,MAAMU,YAAY,GAAG,IAAIjB,YAAY,CAAC;MACpCM,MAAM;MACNG,SAAS;MACTF,OAAO,EAAE;QACPG,YAAY;QACZC,eAAe;QACfC;MACF;IACF,CAAC,CAAC;IAEF,MAAMb,cAAc,CAACO,MAAM,EAAEC,OAAO,CAAC;IAErCD,MAAM,CAACY,MAAM,CAAC,gBAAgB,EAAEJ,eAAe,CAACK,cAAc,CAAC;IAC/Db,MAAM,CAACY,MAAM,CAAC,aAAa,EAAEH,WAAW,CAAC;IACzCT,MAAM,CAACY,MAAM,CAAC,cAAc,EAAED,YAAY,CAAC;IAE3CX,MAAM,CAACc,GAAG,CAACZ,KAAK,GAAGA,KAAK;;IAExB;IACA;IACA,MAAMa,SAAS,GAAG,IAAIC,GAAG,CAAgD,CAAC;IAC1EhB,MAAM,CAACc,GAAG,CAACG,MAAM,GAAGF,SAAS;IAE7B,MAAMG,kBAAkB,GAAG7B,sBAAsB,CAACW,MAAM,EAAEC,OAAO,CAAC;IAElE,MAAMkB,eAA8C,GAAG;MACrDC,GAAG,EAAE,CACH;QACEC,MAAM,EACJH;MACJ,CAAC;IAEL,CAAC;IAED,MAAMI,gBAAsD,GAAG;MAC7DC,OAAO,EAAE;QACPC,KAAK,EAAE;MACT,CAAC;MACDJ,GAAG,EAAE,CACH;QACEC,MAAM,EACJH;MACJ,CAAC;IAEL,CAAC;IAED,MAAMO,MAAM,GAAG,CACb,GAAGnC,iBAAiB,CAClB6B,eAAe,EACfG,gBAAgB,EAChBZ,8BACF,CAAC,EACD,GAAGlB,wBAAwB,CAAC2B,eAAe,EAAEG,gBAAgB,CAAC,EAC9D,GAAG/B,2BAA2B,CAAC4B,eAAe,EAAEG,gBAAgB,CAAC,EACjE,GAAGnC,0BAA0B,CAACgC,eAAe,CAAC,EAC9C,GAAG/B,yBAAyB,CAAC,CAAC,CAC/B;IAEDY,MAAM,CAAC0B,KAAK,CAACD,MAAkC,CAAC,EAAC;EACnD;AACF,CAAiC","ignoreList":[]}
1
+ {"version":3,"file":"plugin.js","names":["validatePluginOptions","getRoutes","getSaveAndReturnExitRoutes","getFileUploadStatusRoutes","makeLoadFormPreHandler","getQuestionRoutes","getRepeaterItemDeleteRoutes","getRepeaterSummaryRoutes","registerVision","CacheService","plugin","name","dependencies","multiple","register","server","options","model","cacheName","saveAndReturn","nunjucks","nunjucksOptions","viewContext","preparePageEventRequestOptions","cacheService","keyGenerator","sessionHydrator","sessionPersister","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 getSaveAndReturnExitRoutes } from '~/src/server/plugins/engine/routes/exit.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 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 {\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 cacheName,\n saveAndReturn,\n nunjucks: nunjucksOptions,\n viewContext,\n preparePageEventRequestOptions\n } = options\n\n const cacheService = new CacheService({\n server,\n cacheName,\n options: {\n keyGenerator: saveAndReturn?.keyGenerator,\n sessionHydrator: saveAndReturn?.sessionHydrator,\n sessionPersister: saveAndReturn?.sessionPersister\n }\n })\n\n await registerVision(server, options)\n\n server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)\n server.expose('viewContext', viewContext)\n server.expose('cacheService', cacheService)\n server.expose('saveAndReturn', saveAndReturn)\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 ...getQuestionRoutes(\n getRouteOptions,\n postRouteOptions,\n preparePageEventRequestOptions\n ),\n ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions),\n ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions),\n ...getSaveAndReturnExitRoutes(getRouteOptions),\n ...getFileUploadStatusRoutes()\n ]\n\n server.route(routes as unknown as ServerRoute[]) // TODO\n }\n} satisfies Plugin<PluginOptions>\n"],"mappings":"AASA,SAASA,qBAAqB;AAC9B,SAASC,SAAS,IAAIC,0BAA0B;AAChD,SAASD,SAAS,IAAIE,yBAAyB;AAC/C,SAASC,sBAAsB;AAC/B,SAASH,SAAS,IAAII,iBAAiB;AACvC,SAASJ,SAAS,IAAIK,2BAA2B;AACjD,SAASL,SAAS,IAAIM,wBAAwB;AAE9C,SAASC,cAAc;AAKvB,SAASC,YAAY;AAErB,OAAO,MAAMC,MAAM,GAAG;EACpBC,IAAI,EAAE,4BAA4B;EAClCC,YAAY,EAAE,CAAC,aAAa,EAAE,WAAW,EAAE,WAAW,CAAC;EACvDC,QAAQ,EAAE,IAAI;EACd,MAAMC,QAAQA,CAACC,MAAc,EAAEC,OAAsB,EAAE;IACrDA,OAAO,GAAGhB,qBAAqB,CAACgB,OAAO,CAAC;IAExC,MAAM;MACJC,KAAK;MACLC,SAAS;MACTC,aAAa;MACbC,QAAQ,EAAEC,eAAe;MACzBC,WAAW;MACXC;IACF,CAAC,GAAGP,OAAO;IAEX,MAAMQ,YAAY,GAAG,IAAIf,YAAY,CAAC;MACpCM,MAAM;MACNG,SAAS;MACTF,OAAO,EAAE;QACPS,YAAY,EAAEN,aAAa,EAAEM,YAAY;QACzCC,eAAe,EAAEP,aAAa,EAAEO,eAAe;QAC/CC,gBAAgB,EAAER,aAAa,EAAEQ;MACnC;IACF,CAAC,CAAC;IAEF,MAAMnB,cAAc,CAACO,MAAM,EAAEC,OAAO,CAAC;IAErCD,MAAM,CAACa,MAAM,CAAC,gBAAgB,EAAEP,eAAe,CAACQ,cAAc,CAAC;IAC/Dd,MAAM,CAACa,MAAM,CAAC,aAAa,EAAEN,WAAW,CAAC;IACzCP,MAAM,CAACa,MAAM,CAAC,cAAc,EAAEJ,YAAY,CAAC;IAC3CT,MAAM,CAACa,MAAM,CAAC,eAAe,EAAET,aAAa,CAAC;IAE7CJ,MAAM,CAACe,GAAG,CAACb,KAAK,GAAGA,KAAK;;IAExB;IACA;IACA,MAAMc,SAAS,GAAG,IAAIC,GAAG,CAAgD,CAAC;IAC1EjB,MAAM,CAACe,GAAG,CAACG,MAAM,GAAGF,SAAS;IAE7B,MAAMG,kBAAkB,GAAG9B,sBAAsB,CAACW,MAAM,EAAEC,OAAO,CAAC;IAElE,MAAMmB,eAA8C,GAAG;MACrDC,GAAG,EAAE,CACH;QACEC,MAAM,EACJH;MACJ,CAAC;IAEL,CAAC;IAED,MAAMI,gBAAsD,GAAG;MAC7DC,OAAO,EAAE;QACPC,KAAK,EAAE;MACT,CAAC;MACDJ,GAAG,EAAE,CACH;QACEC,MAAM,EACJH;MACJ,CAAC;IAEL,CAAC;IAED,MAAMO,MAAM,GAAG,CACb,GAAGpC,iBAAiB,CAClB8B,eAAe,EACfG,gBAAgB,EAChBf,8BACF,CAAC,EACD,GAAGhB,wBAAwB,CAAC4B,eAAe,EAAEG,gBAAgB,CAAC,EAC9D,GAAGhC,2BAA2B,CAAC6B,eAAe,EAAEG,gBAAgB,CAAC,EACjE,GAAGpC,0BAA0B,CAACiC,eAAe,CAAC,EAC9C,GAAGhC,yBAAyB,CAAC,CAAC,CAC/B;IAEDY,MAAM,CAAC2B,KAAK,CAACD,MAAkC,CAAC,EAAC;EACnD;AACF,CAAiC","ignoreList":[]}
@@ -125,7 +125,7 @@ export type FormContextRequest = ({
125
125
  } | {
126
126
  method: FormRequest['method'];
127
127
  payload?: object | undefined;
128
- }) & Pick<FormRequest, 'app' | 'method' | 'params' | 'path' | 'query' | 'url'>;
128
+ }) & Pick<FormRequest, 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'>;
129
129
  export interface UploadInitiateResponse {
130
130
  uploadId: string;
131
131
  uploadUrl: string;
@@ -235,7 +235,7 @@ export interface FormPageViewModel extends PageViewModelBase {
235
235
  context: FormContext;
236
236
  errors?: FormSubmissionError[];
237
237
  hasMissingNotificationEmail?: boolean;
238
- allowSaveAndReturn?: boolean;
238
+ allowSaveAndReturn: boolean;
239
239
  }
240
240
  export interface RepeaterSummaryPageViewModel extends PageViewModelBase {
241
241
  context: FormContext;
@@ -270,9 +270,11 @@ export interface PluginOptions {
270
270
  cacheName?: string;
271
271
  globals?: Record<string, GlobalFunction>;
272
272
  filters?: Record<string, FilterFunction>;
273
- keyGenerator?: (request: RequestType) => string;
274
- sessionHydrator?: (request: RequestType) => Promise<FormSubmissionState>;
275
- sessionPersister?: (key: string, state: FormSubmissionState, request: RequestType) => Promise<void>;
273
+ saveAndReturn?: {
274
+ keyGenerator: (request: RequestType) => string;
275
+ sessionHydrator: (request: RequestType) => Promise<FormSubmissionState>;
276
+ sessionPersister: (key: string, state: FormSubmissionState, request: RequestType) => Promise<void>;
277
+ };
276
278
  pluginPath?: string;
277
279
  nunjucks: {
278
280
  baseLayoutPath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":["UploadStatus","FileStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { type PluginProperties, type Request } from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\ntype RequestType = Request | FormRequest | FormRequestPayload\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<FormRequest, 'app' | 'method' | 'params' | 'path' | 'query' | 'url'>\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport enum UploadStatus {\n initiated = 'initiated',\n pending = 'pending',\n ready = 'ready'\n}\n\nexport enum FileStatus {\n complete = 'complete',\n rejected = 'rejected',\n pending = 'pending'\n}\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndReturn?: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: FormRequest | FormRequestPayload,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n keyGenerator?: (request: RequestType) => string\n sessionHydrator?: (request: RequestType) => Promise<FormSubmissionState>\n sessionPersister?: (\n key: string,\n state: FormSubmissionState,\n request: RequestType\n ) => Promise<void>\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n"],"mappings":"AAkCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAkGA,WAAYA,YAAY,0BAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAA,OAAZA,YAAY;AAAA;AAMxB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":["UploadStatus","FileStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { type PluginProperties, type Request } from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\ntype RequestType = Request | FormRequest | FormRequestPayload\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport enum UploadStatus {\n initiated = 'initiated',\n pending = 'pending',\n ready = 'ready'\n}\n\nexport enum FileStatus {\n complete = 'complete',\n rejected = 'rejected',\n pending = 'pending'\n}\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndReturn: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: FormRequest | FormRequestPayload,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndReturn?: {\n keyGenerator: (request: RequestType) => string\n sessionHydrator: (request: RequestType) => Promise<FormSubmissionState>\n sessionPersister: (\n key: string,\n state: FormSubmissionState,\n request: RequestType\n ) => Promise<void>\n }\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n"],"mappings":"AAkCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAqGA,WAAYA,YAAY,0BAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAA,OAAZA,YAAY;AAAA;AAMxB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
@@ -2,7 +2,7 @@ import { type FormDefinition, type FormMetadata, type SubmitPayload, type Submit
2
2
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js';
3
3
  import { type DetailItem } from '~/src/server/plugins/engine/models/types.js';
4
4
  import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js';
5
- import { type OnRequestCallback, type PreparePageEventRequestOptions } from '~/src/server/plugins/engine/types.js';
5
+ import { type OnRequestCallback, type PluginOptions, type PreparePageEventRequestOptions } from '~/src/server/plugins/engine/types.js';
6
6
  import { type FormRequestPayload, type FormStatus } from '~/src/server/routes/types.js';
7
7
  export interface FormsService {
8
8
  getFormMetadata: (slug: string) => Promise<FormMetadata>;
@@ -28,6 +28,7 @@ export interface RouteConfig {
28
28
  controllers?: Record<string, typeof PageController>;
29
29
  preparePageEventRequestOptions?: PreparePageEventRequestOptions;
30
30
  onRequest?: OnRequestCallback;
31
+ saveAndReturn?: PluginOptions['saveAndReturn'];
31
32
  }
32
33
  export interface OutputService {
33
34
  submit: (request: FormRequestPayload, model: FormModel, emailAddress: string, items: DetailItem[], submitResponse: SubmitResponsePayload) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../src/server/types.ts"],"sourcesContent":["import {\n type FormDefinition,\n type FormMetadata,\n type SubmitPayload,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n type OnRequestCallback,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequestPayload,\n type FormStatus\n} from '~/src/server/routes/types.js'\n\nexport interface FormsService {\n getFormMetadata: (slug: string) => Promise<FormMetadata>\n getFormDefinition: (\n id: string,\n state: FormStatus\n ) => Promise<FormDefinition | undefined>\n}\n\nexport interface FormSubmissionService {\n persistFiles: (\n files: { fileId: string; initiatedRetrievalKey: string }[],\n persistedRetrievalKey: string\n ) => Promise<object>\n submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>\n}\n\nexport interface Services {\n formsService: FormsService\n formSubmissionService: FormSubmissionService\n outputService: OutputService\n}\n\nexport interface RouteConfig {\n formFileName?: string\n formFilePath?: string\n enforceCsrf?: boolean\n services?: Services\n controllers?: Record<string, typeof PageController>\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n}\n\nexport interface OutputService {\n submit: (\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload\n ) => Promise<void>\n}\n"],"mappings":"","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/server/types.ts"],"sourcesContent":["import {\n type FormDefinition,\n type FormMetadata,\n type SubmitPayload,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n type OnRequestCallback,\n type PluginOptions,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequestPayload,\n type FormStatus\n} from '~/src/server/routes/types.js'\n\nexport interface FormsService {\n getFormMetadata: (slug: string) => Promise<FormMetadata>\n getFormDefinition: (\n id: string,\n state: FormStatus\n ) => Promise<FormDefinition | undefined>\n}\n\nexport interface FormSubmissionService {\n persistFiles: (\n files: { fileId: string; initiatedRetrievalKey: string }[],\n persistedRetrievalKey: string\n ) => Promise<object>\n submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>\n}\n\nexport interface Services {\n formsService: FormsService\n formSubmissionService: FormSubmissionService\n outputService: OutputService\n}\n\nexport interface RouteConfig {\n formFileName?: string\n formFilePath?: string\n enforceCsrf?: boolean\n services?: Services\n controllers?: Record<string, typeof PageController>\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n saveAndReturn?: PluginOptions['saveAndReturn']\n}\n\nexport interface OutputService {\n submit: (\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload\n ) => Promise<void>\n}\n"],"mappings":"","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.js","names":[],"sources":["../../../src/typings/hapi/index.d.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/unified-signatures */\n\nimport { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/index.js'\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: Request | FormRequest | FormRequestPayload) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: FormRequest | FormRequestPayload | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\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 '@hapi/scooter' {\n declare const hapiScooter: {\n plugin: Plugin\n }\n\n export = hapiScooter\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":["/* eslint-disable @typescript-eslint/unified-signatures */\n\nimport { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.ts'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/index.js'\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: Request | FormRequest | FormRequestPayload) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: FormRequest | FormRequestPayload | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\n saveAndReturn?: PluginOptions['saveAndReturn']\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 '@hapi/scooter' {\n declare const hapiScooter: {\n plugin: Plugin\n }\n\n export = hapiScooter\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": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -86,53 +86,9 @@ There are a number of `LiquidJS` filters available to you from within the templa
86
86
  ]
87
87
  ```
88
88
 
89
- ## Session Rehydration
89
+ ### Save and return
90
90
 
91
- To support Save and Return functionality, this application now supports session rehydration. This allows user session state to be recovered across browser sessions or devices — even after the in-memory Redis session has expired.
92
-
93
- ### How it works
94
-
95
- To support session rehydration from a backend (e.g. for Save & Return), the consuming application must provide two functions when registering the DXT engine plugin:
96
-
97
- ```
98
- export interface PluginOptions {
99
- ...
100
- keyGenerator?: (request) => string
101
- sessionHydrator?: (request) => Promise<FormSubmissionState | null>
102
- ...
103
- }
104
-
105
- ```
106
-
107
- 1. `keyGenerator(request)`
108
-
109
- This generates a stable and consistent cache key used to store and retrieve user state. It should return a string based on persistent identifiers such as userId, businessId, and grantId — i.e., something like:
110
-
111
- ```
112
- const keyGenerator = request => {
113
- const { userId, businessId, grantId } = request.app.userContext
114
- return `${userId}:${businessId}:${grantId}`
115
- }
116
- ```
117
-
118
- 2. `sessionHydrator(request, key)`
119
-
120
- This function is called when no session state is found in Redis. It should fetch saved state (e.g., from an API) using the provided key and return it in the same structure expected by the form engine:
121
-
122
- ```
123
- const sessionHydrator = async (request, key) => {
124
- const response = await fetch(`https://backend.api/state/${key}`)
125
- if (!response.ok) return null
126
- return await response.json() // Must match form engine state shape
127
- }
128
- ```
129
-
130
- #### Session flow
131
-
132
- - When user resumes a journey and Redis session data is missing or expired, DXT will use `keyGenerator` and `sessionHydrator` to fetch the saved state from an external API (e.g. `/state` endpoint).
133
- - The fetched state is written back into Redis and used to continue the user journey.
134
- - The rehydrated state must include enough information to satisfy schema validation on the current or next page.
135
- - To properly resume a session, users should be redirected to the `/summary` page. This ensures the UI has all required answers preloaded and avoids invalid transitions from deep links.
91
+ See [our save and return feature page](/docs/features/code-based/SAVE_AND_RETURN.md).
136
92
 
137
93
  ### Additional notes
138
94
 
@@ -18,7 +18,8 @@ export const configureEnginePlugin = async ({
18
18
  services,
19
19
  controllers,
20
20
  preparePageEventRequestOptions,
21
- onRequest
21
+ onRequest,
22
+ saveAndReturn
22
23
  }: RouteConfig = {}): Promise<{
23
24
  plugin: typeof plugin
24
25
  options: PluginOptions
@@ -57,7 +58,8 @@ export const configureEnginePlugin = async ({
57
58
  viewContext: devtoolContext,
58
59
  preparePageEventRequestOptions,
59
60
  onRequest,
60
- baseUrl: 'http://localhost:3009' // always runs locally
61
+ baseUrl: 'http://localhost:3009', // always runs locally
62
+ saveAndReturn
61
63
  }
62
64
  }
63
65
  }
@@ -18,6 +18,7 @@ import {
18
18
  } from '~/src/server/plugins/engine/helpers.js'
19
19
  import { handleLegacyRedirect } from '~/src/server/plugins/engine/helpers.js'
20
20
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
21
+ import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
21
22
  import {
22
23
  createPage,
23
24
  type PageControllerClass
@@ -56,7 +57,7 @@ describe('Helpers', () => {
56
57
  page = createPage(model, definition.pages[0])
57
58
  const pageUrl = new URL(page.href, 'http://example.com')
58
59
 
59
- request = {
60
+ request = buildFormContextRequest({
60
61
  method: 'get',
61
62
  url: pageUrl,
62
63
  path: pageUrl.pathname,
@@ -66,7 +67,7 @@ describe('Helpers', () => {
66
67
  },
67
68
  query: {},
68
69
  app: { model }
69
- }
70
+ })
70
71
 
71
72
  const response = {
72
73
  code: jest.fn().mockImplementation(() => response)
@@ -377,7 +377,15 @@ export function evaluateTemplate(
377
377
  }
378
378
 
379
379
  export function getCacheService(server: Server) {
380
- return server.plugins['forms-engine-plugin'].cacheService
380
+ return getPluginOptions(server).cacheService
381
+ }
382
+
383
+ export function getSaveAndReturnHelpers(server: Server) {
384
+ return getPluginOptions(server).saveAndReturn
385
+ }
386
+
387
+ export function getPluginOptions(server: Server) {
388
+ return server.plugins['forms-engine-plugin']
381
389
  }
382
390
 
383
391
  /**