@defra/forms-engine-plugin 2.1.10 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/.server/server/index.js +2 -1
  2. package/.server/server/index.js.map +1 -1
  3. package/.server/server/plugins/engine/README.md +2 -2
  4. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +2 -1
  5. package/.server/server/plugins/engine/configureEnginePlugin.js +4 -4
  6. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  7. package/.server/server/plugins/engine/helpers.d.ts +7 -11
  8. package/.server/server/plugins/engine/helpers.js +2 -2
  9. package/.server/server/plugins/engine/helpers.js.map +1 -1
  10. package/.server/server/plugins/engine/models/FormModel.d.ts +2 -0
  11. package/.server/server/plugins/engine/models/FormModel.js +5 -2
  12. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  13. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -1
  14. package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -1
  15. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  16. package/.server/server/plugins/engine/options.js +3 -6
  17. package/.server/server/plugins/engine/options.js.map +1 -1
  18. package/.server/server/plugins/engine/options.test.js +2 -8
  19. package/.server/server/plugins/engine/options.test.js.map +1 -1
  20. package/.server/server/plugins/engine/outputFormatters/adapter/v1.d.ts +4 -0
  21. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +25 -0
  22. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
  23. package/.server/server/plugins/engine/outputFormatters/machine/v2.js +7 -6
  24. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  25. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +5 -6
  26. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  27. package/.server/server/plugins/engine/pageControllers/PageController.d.ts +6 -6
  28. package/.server/server/plugins/engine/pageControllers/PageController.js +4 -4
  29. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  30. package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +13 -13
  31. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +12 -20
  32. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  33. package/.server/server/plugins/engine/pageControllers/RepeatPageController.d.ts +7 -8
  34. package/.server/server/plugins/engine/pageControllers/RepeatPageController.js.map +1 -1
  35. package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -2
  36. package/.server/server/plugins/engine/pageControllers/StartPageController.js +1 -1
  37. package/.server/server/plugins/engine/pageControllers/StartPageController.js.map +1 -1
  38. package/.server/server/plugins/engine/pageControllers/StatusPageController.d.ts +3 -4
  39. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +1 -1
  40. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  41. package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +5 -4
  42. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +12 -1
  43. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  44. package/.server/server/plugins/engine/pageControllers/TerminalPageController.d.ts +4 -4
  45. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js +1 -1
  46. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js.map +1 -1
  47. package/.server/server/plugins/engine/pageControllers/__stubs__/server.d.ts +1 -1
  48. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js +2 -6
  49. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js.map +1 -1
  50. package/.server/server/plugins/engine/plugin.js +7 -12
  51. package/.server/server/plugins/engine/plugin.js.map +1 -1
  52. package/.server/server/plugins/engine/routes/index.d.ts +5 -5
  53. package/.server/server/plugins/engine/routes/index.js +3 -1
  54. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  55. package/.server/server/plugins/engine/routes/questions.d.ts +4 -4
  56. package/.server/server/plugins/engine/routes/questions.js.map +1 -1
  57. package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -1
  58. package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -1
  59. package/.server/server/plugins/engine/types/index.d.ts +2 -2
  60. package/.server/server/plugins/engine/types/index.js.map +1 -1
  61. package/.server/server/plugins/engine/types/schema.js +3 -2
  62. package/.server/server/plugins/engine/types/schema.js.map +1 -1
  63. package/.server/server/plugins/engine/types.d.ts +13 -12
  64. package/.server/server/plugins/engine/types.js.map +1 -1
  65. package/.server/server/plugins/engine/views/partials/form.html +3 -3
  66. package/.server/server/plugins/engine/views/summary.html +21 -5
  67. package/.server/server/plugins/nunjucks/context.d.ts +5 -6
  68. package/.server/server/plugins/nunjucks/context.js +3 -3
  69. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  70. package/.server/server/routes/types.d.ts +3 -2
  71. package/.server/server/routes/types.js +1 -1
  72. package/.server/server/routes/types.js.map +1 -1
  73. package/.server/server/schemas/index.js +1 -1
  74. package/.server/server/schemas/index.js.map +1 -1
  75. package/.server/server/services/cacheService.d.ts +11 -19
  76. package/.server/server/services/cacheService.js +9 -30
  77. package/.server/server/services/cacheService.js.map +1 -1
  78. package/.server/server/types.d.ts +4 -1
  79. package/.server/server/types.js.map +1 -1
  80. package/.server/typings/hapi/index.d.js.map +1 -1
  81. package/package.json +4 -2
  82. package/src/server/index.test.ts +0 -39
  83. package/src/server/index.ts +4 -1
  84. package/src/server/plugins/engine/README.md +2 -2
  85. package/src/server/plugins/engine/components/helpers/helpers.test.ts +1 -1
  86. package/src/server/plugins/engine/configureEnginePlugin.ts +15 -11
  87. package/src/server/plugins/engine/helpers.test.ts +3 -2
  88. package/src/server/plugins/engine/helpers.ts +6 -6
  89. package/src/server/plugins/engine/models/FormModel.test.ts +66 -2
  90. package/src/server/plugins/engine/models/FormModel.ts +6 -4
  91. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +7 -7
  92. package/src/server/plugins/engine/models/SummaryViewModel.ts +1 -1
  93. package/src/server/plugins/engine/options.js +6 -6
  94. package/src/server/plugins/engine/options.test.js +2 -6
  95. package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +446 -13
  96. package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +37 -0
  97. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +8 -6
  98. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +8 -10
  99. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +9 -8
  100. package/src/server/plugins/engine/pageControllers/PageController.test.ts +11 -8
  101. package/src/server/plugins/engine/pageControllers/PageController.ts +9 -15
  102. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +35 -102
  103. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +24 -36
  104. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +4 -6
  105. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +8 -11
  106. package/src/server/plugins/engine/pageControllers/StartPageController.test.ts +4 -4
  107. package/src/server/plugins/engine/pageControllers/StartPageController.ts +1 -1
  108. package/src/server/plugins/engine/pageControllers/StatusPageController.test.ts +4 -4
  109. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +6 -4
  110. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +15 -5
  111. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +4 -4
  112. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +7 -4
  113. package/src/server/plugins/engine/pageControllers/__stubs__/server.ts +5 -6
  114. package/src/server/plugins/engine/plugin.ts +7 -13
  115. package/src/server/plugins/engine/routes/index.ts +9 -12
  116. package/src/server/plugins/engine/routes/questions.test.ts +29 -53
  117. package/src/server/plugins/engine/routes/questions.ts +6 -8
  118. package/src/server/plugins/engine/routes/repeaters/item-delete.ts +5 -14
  119. package/src/server/plugins/engine/routes/repeaters/summary.ts +5 -14
  120. package/src/server/plugins/engine/types/index.ts +4 -1
  121. package/src/server/plugins/engine/types/schema.test.ts +40 -0
  122. package/src/server/plugins/engine/types/schema.ts +3 -1
  123. package/src/server/plugins/engine/types.ts +22 -13
  124. package/src/server/plugins/engine/views/partials/form.html +3 -3
  125. package/src/server/plugins/engine/views/summary.html +21 -5
  126. package/src/server/plugins/nunjucks/context.js +3 -3
  127. package/src/server/routes/types.ts +7 -2
  128. package/src/server/schemas/index.ts +1 -1
  129. package/src/server/services/cacheService.test.ts +1 -117
  130. package/src/server/services/cacheService.ts +22 -73
  131. package/src/server/types.ts +4 -1
  132. package/src/typings/hapi/index.d.ts +6 -7
  133. package/.server/server/plugins/engine/routes/exit.d.ts +0 -46
  134. package/.server/server/plugins/engine/routes/exit.js +0 -36
  135. package/.server/server/plugins/engine/routes/exit.js.map +0 -1
  136. package/src/server/plugins/engine/routes/exit.ts +0 -47
@@ -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 FormContext,\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 context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload,\n formMetadata?: FormMetadata\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'\nimport { type Server } from '@hapi/hapi'\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 FormContext,\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'\nimport { type CacheService } from '~/src/server/services/cacheService.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 saveAndExit?: PluginOptions['saveAndExit']\n cacheServiceCreator?: (server: Server) => CacheService\n}\n\nexport interface OutputService {\n submit: (\n context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload,\n formMetadata?: FormMetadata\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 { 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":[]}
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 AnyFormRequest,\n type PluginOptions\n} from '~/src/server/plugins/engine/types.ts'\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: 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 }\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": "2.1.10",
3
+ "version": "3.0.1",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -28,7 +28,9 @@
28
28
  "./services/*": "./.server/server/plugins/engine/services/*",
29
29
  "./engine/*": "./.server/server/plugins/engine/*",
30
30
  "./helpers.js": "./.server/server/plugins/engine/components/helpers.js",
31
+ "./schema.js": "./.server/server/schemas/index.js",
31
32
  "./templates/*": "./.server/server/plugins/engine/views/*",
33
+ "./cache-service.js": "./.server/server/services/cacheService.js",
32
34
  "./package.json": "./package.json"
33
35
  },
34
36
  "scripts": {
@@ -68,7 +70,7 @@
68
70
  },
69
71
  "license": "SEE LICENSE IN LICENSE",
70
72
  "dependencies": {
71
- "@defra/forms-model": "^3.0.506",
73
+ "@defra/forms-model": "^3.0.552",
72
74
  "@defra/hapi-tracing": "^1.26.0",
73
75
  "@elastic/ecs-pino-format": "^1.5.0",
74
76
  "@hapi/boom": "^10.0.1",
@@ -639,42 +639,3 @@ describe('prepareEnvironment', () => {
639
639
  )
640
640
  })
641
641
  })
642
-
643
- describe('Exit route handlers', () => {
644
- let server: Server
645
-
646
- beforeAll(async () => {
647
- server = await createServer({
648
- services: defaultServices
649
- })
650
- await server.initialize()
651
- })
652
-
653
- afterAll(async () => {
654
- await server.stop()
655
- })
656
-
657
- beforeEach(() => {
658
- jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata)
659
- server.app.models.clear()
660
- })
661
-
662
- test('GET /exit returns 200 with exit page content', async () => {
663
- jest.mocked(getFormMetadata).mockResolvedValueOnce({
664
- ...fixtures.form.metadata,
665
- live: fixtures.form.state
666
- })
667
-
668
- jest.mocked(getFormDefinition).mockResolvedValue(fixtures.form.definition)
669
-
670
- const options = {
671
- method: 'GET',
672
- url: `${FORM_PREFIX}/slug/exit`
673
- }
674
-
675
- const res = await server.inject(options)
676
-
677
- expect(res.statusCode).toBe(StatusCodes.OK)
678
- expect(res.result).toContain('Your progress has been saved')
679
- })
680
- })
@@ -82,8 +82,11 @@ export async function createServer(routeConfig?: RouteConfig) {
82
82
  prepareSecureContext(server)
83
83
  }
84
84
 
85
+ const cacheService = routeConfig?.cacheServiceCreator
86
+ ? routeConfig.cacheServiceCreator(server)
87
+ : undefined
85
88
  const pluginCrumb = configureCrumbPlugin(routeConfig)
86
- const pluginEngine = await configureEnginePlugin(routeConfig)
89
+ const pluginEngine = await configureEnginePlugin(routeConfig, cacheService)
87
90
 
88
91
  await server.register(pluginSession)
89
92
  await server.register(pluginPulse)
@@ -86,9 +86,9 @@ There are a number of `LiquidJS` filters available to you from within the templa
86
86
  ]
87
87
  ```
88
88
 
89
- ### Save and return
89
+ ### Save and exit
90
90
 
91
- See [our save and return feature page](/docs/features/code-based/SAVE_AND_RETURN.md).
91
+ See [our save and exit feature page](/docs/features/code-based/SAVE_AND_EXIT.md).
92
92
 
93
93
  ### Additional notes
94
94
 
@@ -25,7 +25,7 @@ describe('helpers tests', () => {
25
25
  })
26
26
 
27
27
  describe('ComponentBase tests', () => {
28
- test('should handle save and return functionality', () => {
28
+ test('should handle save and exit functionality', () => {
29
29
  const mockComponentDef = {
30
30
  type: 'TextField',
31
31
  name: 'testField',
@@ -10,17 +10,21 @@ import { formsService } from '~/src/server/plugins/engine/services/localFormsSer
10
10
  import { type PluginOptions } from '~/src/server/plugins/engine/types.js'
11
11
  import { findPackageRoot } from '~/src/server/plugins/engine/vision.js'
12
12
  import { devtoolContext } from '~/src/server/plugins/nunjucks/context.js'
13
+ import { type CacheService } from '~/src/server/services/cacheService.js'
13
14
  import { type RouteConfig } from '~/src/server/types.js'
14
15
 
15
- export const configureEnginePlugin = async ({
16
- formFileName,
17
- formFilePath,
18
- services,
19
- controllers,
20
- preparePageEventRequestOptions,
21
- onRequest,
22
- saveAndReturn
23
- }: RouteConfig = {}): Promise<{
16
+ export const configureEnginePlugin = async (
17
+ {
18
+ formFileName,
19
+ formFilePath,
20
+ services,
21
+ controllers,
22
+ preparePageEventRequestOptions,
23
+ onRequest,
24
+ saveAndExit
25
+ }: RouteConfig = {},
26
+ cache?: CacheService
27
+ ): Promise<{
24
28
  plugin: typeof plugin
25
29
  options: PluginOptions
26
30
  }> => {
@@ -50,7 +54,7 @@ export const configureEnginePlugin = async ({
50
54
  formsService: await formsService()
51
55
  },
52
56
  controllers,
53
- cacheName: 'session',
57
+ cache: cache ?? 'session',
54
58
  nunjucks: {
55
59
  baseLayoutPath: 'dxt-devtool-baselayout.html',
56
60
  paths: [join(findPackageRoot(), 'src/server/devserver')] // custom layout to make it really clear this is not the same as the runner
@@ -59,7 +63,7 @@ export const configureEnginePlugin = async ({
59
63
  preparePageEventRequestOptions,
60
64
  onRequest,
61
65
  baseUrl: 'http://localhost:3009', // always runs locally
62
- saveAndReturn
66
+ saveAndExit
63
67
  }
64
68
  }
65
69
  }
@@ -30,7 +30,8 @@ import {
30
30
  import {
31
31
  FormAction,
32
32
  FormStatus,
33
- type FormRequest
33
+ type FormRequest,
34
+ type FormResponseToolkit
34
35
  } from '~/src/server/routes/types.js'
35
36
  import definition from '~/test/form/definitions/basic.js'
36
37
  import templateDefinition from '~/test/form/definitions/templates.js'
@@ -47,7 +48,7 @@ type HrefFilter = (this: NunjucksContext, path: string) => string | undefined
47
48
  describe('Helpers', () => {
48
49
  let page: PageControllerClass
49
50
  let request: FormContextRequest
50
- let h: Pick<ResponseToolkit, 'redirect' | 'view'>
51
+ let h: FormResponseToolkit
51
52
 
52
53
  beforeEach(() => {
53
54
  const model = new FormModel(definition, {
@@ -23,6 +23,7 @@ import {
23
23
  import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
24
24
  import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
25
25
  import {
26
+ type AnyFormRequest,
26
27
  type FormContext,
27
28
  type FormContextRequest,
28
29
  type FormSubmissionError
@@ -32,8 +33,7 @@ import {
32
33
  FormStatus,
33
34
  type FormParams,
34
35
  type FormQuery,
35
- type FormRequest,
36
- type FormRequestPayload
36
+ type FormResponseToolkit
37
37
  } from '~/src/server/routes/types.js'
38
38
 
39
39
  const logger = createLogger()
@@ -117,7 +117,7 @@ engine.registerFilter('answer', function (name: string) {
117
117
 
118
118
  export function proceed(
119
119
  request: Pick<FormContextRequest, 'method' | 'payload' | 'query'>,
120
- h: Pick<ResponseToolkit, 'redirect' | 'view'>,
120
+ h: FormResponseToolkit,
121
121
  nextUrl: string
122
122
  ) {
123
123
  const { method, payload, query } = request
@@ -327,7 +327,7 @@ export function getError(detail: ValidationErrorItem): FormSubmissionError {
327
327
  * is not disabled on the current route, and that cookies/state are present.
328
328
  */
329
329
  export function safeGenerateCrumb(
330
- request: FormRequest | FormRequestPayload | null
330
+ request: AnyFormRequest | null
331
331
  ): string | undefined {
332
332
  // no request or no .state
333
333
  if (!request?.state) {
@@ -380,8 +380,8 @@ export function getCacheService(server: Server) {
380
380
  return getPluginOptions(server).cacheService
381
381
  }
382
382
 
383
- export function getSaveAndReturnHelpers(server: Server) {
384
- return getPluginOptions(server).saveAndReturn
383
+ export function getSaveAndExitHelpers(server: Server) {
384
+ return getPluginOptions(server).saveAndExit
385
385
  }
386
386
 
387
387
  export function getPluginOptions(server: Server) {
@@ -141,6 +141,21 @@ describe('FormModel', () => {
141
141
  expect(model.schemaVersion).toBe(SchemaVersion.V1)
142
142
  })
143
143
 
144
+ it('sets versionNumber from options', () => {
145
+ const model = new FormModel(definition, {
146
+ basePath: 'test',
147
+ versionNumber: 42
148
+ })
149
+
150
+ expect(model.versionNumber).toBe(42)
151
+ })
152
+
153
+ it('sets versionNumber to undefined when not provided', () => {
154
+ const model = new FormModel(definition, { basePath: 'test' })
155
+
156
+ expect(model.versionNumber).toBeUndefined()
157
+ })
158
+
144
159
  it.each([
145
160
  {
146
161
  input: undefined,
@@ -185,7 +200,7 @@ describe('FormModel', () => {
185
200
  })
186
201
 
187
202
  describe('getFormContext', () => {
188
- it.each([FormAction.Validate, FormAction.SaveAndReturn, undefined])(
203
+ it.each([FormAction.Validate, FormAction.SaveAndExit, undefined])(
189
204
  'returns a form context with the correct payload and state when action is %s',
190
205
  (action) => {
191
206
  const formModel = new FormModel(fieldsRequiredDefinition, {
@@ -219,7 +234,7 @@ describe('FormModel', () => {
219
234
  }
220
235
  )
221
236
 
222
- it('returns without updating the state when the action is not validate or saveAndReturn', () => {
237
+ it('returns without updating the state when the action is not validate or saveAndExit', () => {
223
238
  const formModel = new FormModel(fieldsRequiredDefinition, {
224
239
  basePath: '/components'
225
240
  })
@@ -329,6 +344,55 @@ describe('FormModel', () => {
329
344
  )
330
345
  })
331
346
 
347
+ it('includes submittedVersionNumber in context when versionNumber is set', () => {
348
+ const formModel = new FormModel(fieldsRequiredDefinition, {
349
+ basePath: '/components',
350
+ versionNumber: 123
351
+ })
352
+
353
+ const state = {
354
+ $$__referenceNumber: 'foobar'
355
+ }
356
+ const pageUrl = new URL('http://example.com/components/fields-required')
357
+
358
+ const request: FormContextRequest = buildFormContextRequest({
359
+ method: 'get',
360
+ query: {},
361
+ path: pageUrl.pathname,
362
+ params: { path: 'components', slug: 'fields-required' },
363
+ url: pageUrl,
364
+ app: { model: formModel }
365
+ })
366
+
367
+ const context = formModel.getFormContext(request, state)
368
+
369
+ expect(context.submittedVersionNumber).toBe(123)
370
+ })
371
+
372
+ it('sets submittedVersionNumber to undefined when versionNumber is not set', () => {
373
+ const formModel = new FormModel(fieldsRequiredDefinition, {
374
+ basePath: '/components'
375
+ })
376
+
377
+ const state = {
378
+ $$__referenceNumber: 'foobar'
379
+ }
380
+ const pageUrl = new URL('http://example.com/components/fields-required')
381
+
382
+ const request: FormContextRequest = buildFormContextRequest({
383
+ method: 'get',
384
+ query: {},
385
+ path: pageUrl.pathname,
386
+ params: { path: 'components', slug: 'fields-required' },
387
+ url: pageUrl,
388
+ app: { model: formModel }
389
+ })
390
+
391
+ const context = formModel.getFormContext(request, state)
392
+
393
+ expect(context.submittedVersionNumber).toBeUndefined()
394
+ })
395
+
332
396
  it('redirects to the page if the list field (radio) is invalidated due to list item conditions', () => {
333
397
  const formModel = new FormModel(conditionsListDefinition, {
334
398
  basePath: '/conditional-list-items'
@@ -76,6 +76,7 @@ export class FormModel {
76
76
  name: string
77
77
  values: FormDefinition
78
78
  basePath: string
79
+ versionNumber?: number
79
80
  conditions: Partial<Record<string, ExecutableCondition>>
80
81
  pages: PageControllerClass[]
81
82
  services: Services
@@ -94,7 +95,7 @@ export class FormModel {
94
95
 
95
96
  constructor(
96
97
  def: typeof this.def,
97
- options: { basePath: string },
98
+ options: { basePath: string; versionNumber?: number },
98
99
  services: Services = defaultServices,
99
100
  controllers?: Record<string, typeof PageController>
100
101
  ) {
@@ -148,6 +149,7 @@ export class FormModel {
148
149
  this.name = def.name ?? ''
149
150
  this.values = result.value
150
151
  this.basePath = options.basePath
152
+ this.versionNumber = options.versionNumber
151
153
  this.conditions = {}
152
154
  this.services = services
153
155
  this.controllers = controllers
@@ -344,7 +346,8 @@ export class FormModel {
344
346
  componentDefMap: this.componentDefMap,
345
347
  pageMap: this.pageMap,
346
348
  componentMap: this.componentMap,
347
- referenceNumber: getReferenceNumber(state)
349
+ referenceNumber: getReferenceNumber(state),
350
+ submittedVersionNumber: this.versionNumber
348
351
  }
349
352
 
350
353
  // Validate current page
@@ -543,8 +546,7 @@ function validateFormPayload(
543
546
  // Skip validation GET requests or other actions
544
547
  if (
545
548
  !request.payload ||
546
- (action &&
547
- ![FormAction.Validate, FormAction.SaveAndReturn].includes(action))
549
+ (action && ![FormAction.Validate, FormAction.SaveAndExit].includes(action))
548
550
  ) {
549
551
  return context
550
552
  }
@@ -5,7 +5,7 @@ import {
5
5
  } from '~/src/server/plugins/engine/models/index.js'
6
6
  import { SummaryPageController } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
7
7
  import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
8
- import { serverWithSaveAndReturn } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
8
+ import { serverWithSaveAndExit } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
9
9
  import {
10
10
  createPage,
11
11
  type PageControllerClass
@@ -274,16 +274,16 @@ describe('SummaryPageController', () => {
274
274
  },
275
275
  query: {},
276
276
  app: { model },
277
- server: serverWithSaveAndReturn
277
+ server: serverWithSaveAndExit
278
278
  }
279
279
  })
280
280
 
281
- describe('Save and Return functionality', () => {
282
- it('should show save and return button on summary page', () => {
283
- expect(controller.shouldShowSaveAndReturn(request.server)).toBe(true)
281
+ describe('Save and Exit functionality', () => {
282
+ it('should show save and exit button on summary page', () => {
283
+ expect(controller.shouldShowSaveAndExit(request.server)).toBe(true)
284
284
  })
285
285
 
286
- it('should handle save and return from summary page', () => {
286
+ it('should handle save and exit from summary page', () => {
287
287
  const state: FormState = {
288
288
  $$__referenceNumber: 'foobar',
289
289
  orderType: 'collection',
@@ -293,7 +293,7 @@ describe('SummaryPageController', () => {
293
293
  const context = model.getFormContext(request, state)
294
294
  const viewModel = controller.getViewModel(request, context)
295
295
 
296
- expect(viewModel).toHaveProperty('allowSaveAndReturn', true)
296
+ expect(viewModel).toHaveProperty('allowSaveAndExit', true)
297
297
  })
298
298
 
299
299
  it('should display correct page title', () => {
@@ -51,7 +51,7 @@ export class SummaryViewModel {
51
51
  serviceUrl: string
52
52
  hasMissingNotificationEmail?: boolean
53
53
  components?: ComponentViewModel[]
54
- allowSaveAndReturn = false
54
+ allowSaveAndExit = false
55
55
 
56
56
  constructor(
57
57
  request: FormContextRequest,
@@ -1,6 +1,7 @@
1
1
  import Joi from 'joi'
2
2
 
3
3
  import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
4
+ import { CacheService } from '~/src/server/services/index.js'
4
5
 
5
6
  const logger = createLogger()
6
7
 
@@ -8,7 +9,10 @@ const pluginRegistrationOptionsSchema = Joi.object({
8
9
  model: Joi.object().optional(),
9
10
  services: Joi.object().optional(),
10
11
  controllers: Joi.object().pattern(Joi.string(), Joi.any()).optional(),
11
- cacheName: Joi.string().optional(),
12
+ cache: Joi.alternatives().try(
13
+ Joi.object().instance(CacheService),
14
+ Joi.string()
15
+ ),
12
16
  globals: Joi.object().pattern(Joi.string(), Joi.any()).optional(),
13
17
  filters: Joi.object().pattern(Joi.string(), Joi.any()).optional(),
14
18
  pluginPath: Joi.string().optional(),
@@ -20,11 +24,7 @@ const pluginRegistrationOptionsSchema = Joi.object({
20
24
  preparePageEventRequestOptions: Joi.function().optional(),
21
25
  onRequest: Joi.function().optional(),
22
26
  baseUrl: Joi.string().uri().required(),
23
- saveAndReturn: Joi.object({
24
- keyGenerator: Joi.function(),
25
- sessionHydrator: Joi.function(),
26
- sessionPersister: Joi.function()
27
- }).optional()
27
+ saveAndExit: Joi.function().optional()
28
28
  })
29
29
 
30
30
  /**
@@ -19,7 +19,7 @@ describe('validatePluginOptions', () => {
19
19
  expect(validatePluginOptions(validOptions)).toEqual(validOptions)
20
20
  })
21
21
 
22
- it('accepts optional properties keyGenerator, sessionHydrator, and sessionPersister', () => {
22
+ it('accepts optional property saveAndExit', () => {
23
23
  /**
24
24
  * @type {PluginOptions}
25
25
  */
@@ -32,11 +32,7 @@ describe('validatePluginOptions', () => {
32
32
  return { hello: 'world' }
33
33
  },
34
34
  baseUrl: 'http://localhost:3009',
35
- saveAndReturn: {
36
- keyGenerator: () => 'test-key',
37
- sessionHydrator: () => Promise.resolve({ someState: 'value' }),
38
- sessionPersister: () => Promise.resolve(undefined)
39
- }
35
+ saveAndExit: (request, h) => h.redirect('/save-and-exit')
40
36
  }
41
37
 
42
38
  expect(validatePluginOptions(validOptionsWithOptionals)).toEqual(