@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,10 +1,6 @@
1
1
  import { slugSchema } from '@defra/forms-model'
2
2
  import Boom from '@hapi/boom'
3
- import {
4
- type ResponseToolkit,
5
- type RouteOptions,
6
- type ServerRoute
7
- } from '@hapi/hapi'
3
+ import { type RouteOptions, type ServerRoute } from '@hapi/hapi'
8
4
  import Joi from 'joi'
9
5
 
10
6
  import { FileUploadPageController } from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'
@@ -14,7 +10,8 @@ import {
14
10
  type FormRequest,
15
11
  type FormRequestPayload,
16
12
  type FormRequestPayloadRefs,
17
- type FormRequestRefs
13
+ type FormRequestRefs,
14
+ type FormResponseToolkit
18
15
  } from '~/src/server/routes/types.js'
19
16
  import {
20
17
  actionSchema,
@@ -26,10 +23,7 @@ import {
26
23
  } from '~/src/server/schemas/index.js'
27
24
 
28
25
  // Item delete GET route
29
- function getHandler(
30
- request: FormRequest,
31
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
32
- ) {
26
+ function getHandler(request: FormRequest, h: FormResponseToolkit) {
33
27
  const { params } = request
34
28
 
35
29
  return redirectOrMakeHandler(request, h, (page, context) => {
@@ -46,10 +40,7 @@ function getHandler(
46
40
  })
47
41
  }
48
42
 
49
- function postHandler(
50
- request: FormRequestPayload,
51
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
52
- ) {
43
+ function postHandler(request: FormRequestPayload, h: FormResponseToolkit) {
53
44
  const { params } = request
54
45
 
55
46
  return redirectOrMakeHandler(request, h, (page, context) => {
@@ -1,11 +1,7 @@
1
1
  // List summary GET route
2
2
  import { slugSchema } from '@defra/forms-model'
3
3
  import Boom from '@hapi/boom'
4
- import {
5
- type ResponseToolkit,
6
- type RouteOptions,
7
- type ServerRoute
8
- } from '@hapi/hapi'
4
+ import { type RouteOptions, type ServerRoute } from '@hapi/hapi'
9
5
  import Joi from 'joi'
10
6
 
11
7
  import { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'
@@ -14,7 +10,8 @@ import {
14
10
  type FormRequest,
15
11
  type FormRequestPayload,
16
12
  type FormRequestPayloadRefs,
17
- type FormRequestRefs
13
+ type FormRequestRefs,
14
+ type FormResponseToolkit
18
15
  } from '~/src/server/routes/types.js'
19
16
  import {
20
17
  actionSchema,
@@ -23,10 +20,7 @@ import {
23
20
  stateSchema
24
21
  } from '~/src/server/schemas/index.js'
25
22
 
26
- function getHandler(
27
- request: FormRequest,
28
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
29
- ) {
23
+ function getHandler(request: FormRequest, h: FormResponseToolkit) {
30
24
  const { params } = request
31
25
 
32
26
  return redirectOrMakeHandler(request, h, (page, context) => {
@@ -38,10 +32,7 @@ function getHandler(
38
32
  })
39
33
  }
40
34
 
41
- function postHandler(
42
- request: FormRequestPayload,
43
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
44
- ) {
35
+ function postHandler(request: FormRequestPayload, h: FormResponseToolkit) {
45
36
  const { params } = request
46
37
 
47
38
  return redirectOrMakeHandler(request, h, (page, context) => {
@@ -1,4 +1,6 @@
1
1
  export type {
2
+ AnyFormRequest,
3
+ AnyRequest,
2
4
  CheckAnswers,
3
5
  ErrorMessageTemplate,
4
6
  ErrorMessageTemplateList,
@@ -74,7 +76,8 @@ export type {
74
76
  FormRequest,
75
77
  FormRequestPayload,
76
78
  FormRequestPayloadRefs,
77
- FormRequestRefs
79
+ FormRequestRefs,
80
+ FormResponseToolkit
78
81
  } from '~/src/server/routes/types.js'
79
82
 
80
83
  export { FormAction, FormStatus } from '~/src/server/routes/types.js'
@@ -156,5 +156,45 @@ describe('Schema validation', () => {
156
156
  formAdapterSubmissionMessagePayloadSchema.validate(payloadWithoutData)
157
157
  expect(error).toBeDefined()
158
158
  })
159
+
160
+ it('should validate payload with versionMetadata', () => {
161
+ const payloadWithVersion = {
162
+ ...validPayload,
163
+ meta: {
164
+ ...validPayload.meta,
165
+ versionMetadata: {
166
+ versionNumber: 19,
167
+ createdAt: new Date('2025-09-08T09:28:15.576Z')
168
+ }
169
+ }
170
+ }
171
+ const { error } =
172
+ formAdapterSubmissionMessagePayloadSchema.validate(payloadWithVersion)
173
+ expect(error).toBeUndefined()
174
+ })
175
+
176
+ it('should validate payload without versionMetadata', () => {
177
+ const { error } =
178
+ formAdapterSubmissionMessagePayloadSchema.validate(validPayload)
179
+ expect(error).toBeUndefined()
180
+ })
181
+
182
+ it('should reject invalid versionMetadata', () => {
183
+ const payloadWithInvalidVersion = {
184
+ ...validPayload,
185
+ meta: {
186
+ ...validPayload.meta,
187
+ versionMetadata: {
188
+ versionNumber: 'not-a-number', // Invalid - should be number
189
+ createdAt: new Date('2025-09-08T09:28:15.576Z')
190
+ }
191
+ }
192
+ }
193
+ const { error } = formAdapterSubmissionMessagePayloadSchema.validate(
194
+ payloadWithInvalidVersion
195
+ )
196
+ expect(error).toBeDefined()
197
+ expect(error?.message).toContain('must be a number')
198
+ })
159
199
  })
160
200
  })
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  FormStatus,
3
+ formVersionMetadataSchema,
3
4
  idSchema,
4
5
  notificationEmailAddressSchema,
5
6
  slugSchema,
@@ -29,7 +30,8 @@ export const formAdapterSubmissionMessageMetaSchema =
29
30
  .valid(...Object.values(FormStatus))
30
31
  .required(),
31
32
  isPreview: Joi.boolean().required(),
32
- notificationEmail: notificationEmailAddressSchema.required()
33
+ notificationEmail: notificationEmailAddressSchema.required(),
34
+ versionMetadata: formVersionMetadataSchema.optional()
33
35
  })
34
36
 
35
37
  export const formAdapterSubmissionMessageDataSchema =
@@ -3,11 +3,16 @@ import {
3
3
  type Event,
4
4
  type FormDefinition,
5
5
  type FormMetadata,
6
+ type FormVersionMetadata,
6
7
  type Item,
7
8
  type List,
8
9
  type Page
9
10
  } from '@defra/forms-model'
10
- import { type PluginProperties, type Request } from '@hapi/hapi'
11
+ import {
12
+ type PluginProperties,
13
+ type Request,
14
+ type ResponseObject
15
+ } from '@hapi/hapi'
11
16
  import { type JoiExpression, type ValidationErrorItem } from 'joi'
12
17
 
13
18
  import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
@@ -36,12 +41,15 @@ import {
36
41
  type FormParams,
37
42
  type FormRequest,
38
43
  type FormRequestPayload,
44
+ type FormResponseToolkit,
39
45
  type FormStatus
40
46
  } from '~/src/server/routes/types.js'
47
+ import { type CacheService } from '~/src/server/services/cacheService.js'
41
48
  import { type RequestOptions } from '~/src/server/services/httpService.js'
42
49
  import { type Services } from '~/src/server/types.js'
43
50
 
44
- type RequestType = Request | FormRequest | FormRequestPayload
51
+ export type AnyFormRequest = FormRequest | FormRequestPayload
52
+ export type AnyRequest = Request | AnyFormRequest
45
53
 
46
54
  /**
47
55
  * Form submission state stores the following in Redis:
@@ -172,6 +180,7 @@ export interface FormContext {
172
180
  pageMap: Map<string, PageControllerClass>
173
181
  componentMap: Map<string, Component>
174
182
  referenceNumber: string
183
+ submittedVersionNumber?: number
175
184
  }
176
185
 
177
186
  export type FormContextRequest = (
@@ -312,7 +321,7 @@ export interface FormPageViewModel extends PageViewModelBase {
312
321
  context: FormContext
313
322
  errors?: FormSubmissionError[]
314
323
  hasMissingNotificationEmail?: boolean
315
- allowSaveAndReturn: boolean
324
+ allowSaveAndExit: boolean
316
325
  }
317
326
 
318
327
  export interface RepeaterSummaryPageViewModel extends PageViewModelBase {
@@ -357,27 +366,26 @@ export type PreparePageEventRequestOptions = (
357
366
  ) => void
358
367
 
359
368
  export type OnRequestCallback = (
360
- request: FormRequest | FormRequestPayload,
369
+ request: AnyFormRequest,
361
370
  params: FormParams,
362
371
  definition: FormDefinition,
363
372
  metadata: FormMetadata
364
373
  ) => void
365
374
 
375
+ export type SaveAndExitHandler = (
376
+ request: FormRequestPayload,
377
+ h: FormResponseToolkit,
378
+ context: FormContext
379
+ ) => ResponseObject
380
+
366
381
  export interface PluginOptions {
367
382
  model?: FormModel
368
383
  services?: Services
369
384
  controllers?: Record<string, typeof PageController>
370
- cacheName?: string
385
+ cache?: CacheService | string
371
386
  globals?: Record<string, GlobalFunction>
372
387
  filters?: Record<string, FilterFunction>
373
- saveAndReturn?: {
374
- keyGenerator: (request: RequestType) => string
375
- sessionHydrator: (request: RequestType) => Promise<FormSubmissionState>
376
- sessionPersister: (
377
- state: FormSubmissionState,
378
- request: RequestType
379
- ) => Promise<void>
380
- }
388
+ saveAndExit?: SaveAndExitHandler
381
389
  pluginPath?: string
382
390
  nunjucks: {
383
391
  baseLayoutPath: string
@@ -399,6 +407,7 @@ export interface FormAdapterSubmissionMessageMeta {
399
407
  status: FormStatus
400
408
  isPreview: boolean
401
409
  notificationEmail: string
410
+ versionMetadata?: FormVersionMetadata
402
411
  }
403
412
 
404
413
  export type FormAdapterSubmissionMessageMetaSerialised = Omit<
@@ -13,12 +13,12 @@
13
13
  preventDoubleClick: true
14
14
  }) }}
15
15
 
16
- {% if allowSaveAndReturn %}
16
+ {% if allowSaveAndExit %}
17
17
  {{ govukButton({
18
- text: "Save and return",
18
+ text: "Save and exit",
19
19
  classes: "govuk-button--secondary",
20
20
  name: "action",
21
- value: "save-and-return",
21
+ value: "save-and-exit",
22
22
  preventDoubleClick: true
23
23
  }) }}
24
24
  {% endif %}
@@ -2,6 +2,7 @@
2
2
 
3
3
  {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
4
4
  {% from "govuk/components/summary-list/macro.njk" import govukSummaryList %}
5
+ {% from "govuk/components/button/macro.njk" import govukButton %}
5
6
  {% from "partials/components.html" import componentList with context %}
6
7
 
7
8
  {% block content %}
@@ -31,7 +32,6 @@
31
32
 
32
33
  <form method="post" novalidate>
33
34
  <input type="hidden" name="crumb" value="{{ crumb }}">
34
- <input type="hidden" name="action" value="send">
35
35
 
36
36
  {% if declaration %}
37
37
  <h2 class="govuk-heading-m" id="declaration">Declaration</h2>
@@ -42,10 +42,26 @@
42
42
 
43
43
  {{ componentList(components) }}
44
44
 
45
- {% set isDeclaration = declaration or components | length %}
46
- <button data-prevent-double-click="true" class="govuk-button" data-module="govuk-button">
47
- {{ "Accept and send" if isDeclaration else "Send" }}
48
- </button>
45
+ <div class="govuk-button-group">
46
+ {% set isDeclaration = declaration or components | length %}
47
+
48
+ {{ govukButton({
49
+ text: "Accept and send" if isDeclaration else "Send",
50
+ name: "action",
51
+ value: "send",
52
+ preventDoubleClick: true
53
+ }) }}
54
+
55
+ {% if allowSaveAndExit %}
56
+ {{ govukButton({
57
+ text: "Save and exit",
58
+ classes: "govuk-button--secondary",
59
+ name: "action",
60
+ value: "save-and-exit",
61
+ preventDoubleClick: true
62
+ }) }}
63
+ {% endif %}
64
+ </div>
49
65
  </form>
50
66
  </div>
51
67
  </div>
@@ -18,7 +18,7 @@ const logger = createLogger()
18
18
  let webpackManifest
19
19
 
20
20
  /**
21
- * @param {FormRequest | FormRequestPayload | null} request
21
+ * @param {AnyFormRequest | null} request
22
22
  */
23
23
  export async function context(request) {
24
24
  const { params, response } = request ?? {}
@@ -62,7 +62,7 @@ export async function context(request) {
62
62
 
63
63
  /**
64
64
  * Returns the context for the devtool. Consumers won't have access to this.
65
- * @param {FormRequest | FormRequestPayload | null} _request
65
+ * @param {AnyFormRequest | null} _request
66
66
  * @returns {Record<string, unknown> & { assetPath: string, getDxtAssetPath: (asset: string) => string }}
67
67
  */
68
68
  export function devtoolContext(_request) {
@@ -97,5 +97,5 @@ export function devtoolContext(_request) {
97
97
 
98
98
  /**
99
99
  * @import { ViewContext } from '~/src/server/plugins/nunjucks/types.js'
100
- * @import { FormRequest, FormRequestPayload } from '~/src/server/routes/types.js'
100
+ * @import { AnyFormRequest } from '~/src/server/plugins/engine/types.js'
101
101
  */
@@ -1,4 +1,8 @@
1
- import { type ReqRefDefaults, type Request } from '@hapi/hapi'
1
+ import {
2
+ type ReqRefDefaults,
3
+ type Request,
4
+ type ResponseToolkit
5
+ } from '@hapi/hapi'
2
6
 
3
7
  import { type FormPayload } from '~/src/server/plugins/engine/types.js'
4
8
 
@@ -33,6 +37,7 @@ export interface FormRequestPayloadRefs extends FormRequestRefs {
33
37
 
34
38
  export type FormRequest = Request<FormRequestRefs>
35
39
  export type FormRequestPayload = Request<FormRequestPayloadRefs>
40
+ export type FormResponseToolkit = Pick<ResponseToolkit, 'redirect' | 'view'>
36
41
 
37
42
  export enum FormAction {
38
43
  Continue = 'continue',
@@ -40,7 +45,7 @@ export enum FormAction {
40
45
  Delete = 'delete',
41
46
  AddAnother = 'add-another',
42
47
  Send = 'send',
43
- SaveAndReturn = 'save-and-return'
48
+ SaveAndExit = 'save-and-exit'
44
49
  }
45
50
 
46
51
  export enum FormStatus {
@@ -14,7 +14,7 @@ export const actionSchema = Joi.string<FormAction>()
14
14
  FormAction.Delete,
15
15
  FormAction.AddAnother,
16
16
  FormAction.Send,
17
- FormAction.SaveAndReturn
17
+ FormAction.SaveAndExit
18
18
  )
19
19
  .default(FormAction.Validate)
20
20
  .optional()
@@ -2,11 +2,7 @@ import { type Request, type Server } from '@hapi/hapi'
2
2
 
3
3
  import { config } from '~/src/config/index.js'
4
4
  import { type FormRequest } from '~/src/server/routes/types.js'
5
- import {
6
- ADDITIONAL_IDENTIFIER,
7
- CacheService,
8
- merge
9
- } from '~/src/server/services/cacheService.js'
5
+ import { CacheService, merge } from '~/src/server/services/cacheService.js'
10
6
 
11
7
  describe('CacheService', () => {
12
8
  let mockServer: Partial<Server>
@@ -76,63 +72,6 @@ describe('CacheService', () => {
76
72
  expect(result).toEqual({})
77
73
  })
78
74
  })
79
-
80
- it('should rehydrate state using custom fetcher when cache is missed', async () => {
81
- const rehydratedState = { rehydrated: true }
82
-
83
- const customFetcher = jest.fn().mockResolvedValue(rehydratedState)
84
-
85
- cacheService = new CacheService({
86
- server: mockServer as Server,
87
- cacheName: 'test-cache',
88
- options: { sessionHydrator: customFetcher }
89
- })
90
-
91
- const mockRequest = {
92
- yar: { id: 'session-id' },
93
- params: { state: 's', slug: 'p' }
94
- } as unknown as FormRequest
95
-
96
- mockCache.get
97
- .mockResolvedValueOnce(null)
98
- .mockResolvedValueOnce(rehydratedState)
99
-
100
- const result = await cacheService.getState(mockRequest)
101
-
102
- expect(customFetcher).toHaveBeenCalledWith(mockRequest)
103
- expect(mockCache.set).toHaveBeenCalledWith(
104
- expect.objectContaining({
105
- segment: 'cache',
106
- id: expect.stringContaining('session-id')
107
- }),
108
- rehydratedState,
109
- config.get('sessionTimeout')
110
- )
111
- expect(result).toEqual(rehydratedState)
112
- })
113
-
114
- it('should return empty object when custom fetcher returns null', async () => {
115
- const customFetcher = jest.fn().mockResolvedValue(null)
116
-
117
- cacheService = new CacheService({
118
- server: mockServer as Server,
119
- cacheName: 'test-cache',
120
- options: { sessionHydrator: customFetcher }
121
- })
122
-
123
- const mockRequest = {
124
- yar: { id: 'session-id' },
125
- params: { state: 's', slug: 'p' }
126
- } as unknown as FormRequest
127
-
128
- mockCache.get.mockResolvedValue(null)
129
-
130
- const result = await cacheService.getState(mockRequest)
131
-
132
- expect(customFetcher).toHaveBeenCalledWith(mockRequest)
133
- expect(mockCache.set).not.toHaveBeenCalled()
134
- expect(result).toEqual({})
135
- })
136
75
  })
137
76
 
138
77
  describe('setState', () => {
@@ -178,61 +117,6 @@ describe('CacheService', () => {
178
117
  )
179
118
  })
180
119
  })
181
-
182
- it('should use custom key generator if provided', async () => {
183
- const customKey = 'my-custom-key'
184
- const customKeyGenerator = jest.fn().mockReturnValue(customKey)
185
-
186
- cacheService = new CacheService({
187
- server: mockServer as Server,
188
- cacheName: 'test-cache',
189
- options: { keyGenerator: customKeyGenerator }
190
- })
191
-
192
- const mockRequest = {
193
- yar: { id: 'some-session' },
194
- params: { state: 'form1', slug: 'page1' }
195
- } as unknown as FormRequest
196
-
197
- await cacheService.setState(mockRequest, { test: 'value' })
198
-
199
- expect(mockCache.set).toHaveBeenCalledWith(
200
- {
201
- segment: 'cache',
202
- id: 'my-custom-key'
203
- },
204
- { test: 'value' },
205
- expect.any(Number)
206
- )
207
- })
208
-
209
- it('should append additionalIdentifier to custom key', () => {
210
- const customKey = 'custom:key:base:'
211
- const customKeyGenerator = jest.fn().mockReturnValue(customKey)
212
-
213
- cacheService = new CacheService({
214
- server: mockServer as Server,
215
- cacheName: 'test-cache',
216
- options: { keyGenerator: customKeyGenerator }
217
- })
218
-
219
- const mockRequest = {
220
- yar: { id: 'session-id' },
221
- params: { state: 'formA', slug: 'step1' }
222
- } as unknown as FormRequest
223
-
224
- const result = cacheService.Key(
225
- mockRequest,
226
- ADDITIONAL_IDENTIFIER.Confirmation
227
- )
228
-
229
- expect(result).toEqual({
230
- segment: 'cache',
231
- id: 'custom:key:base::confirmation'
232
- })
233
-
234
- expect(customKeyGenerator).toHaveBeenCalledWith(mockRequest)
235
- })
236
120
  })
237
121
 
238
122
  describe('merge', () => {