@defra/forms-engine-plugin 2.1.9 → 3.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.
- package/.server/server/index.js +2 -1
- package/.server/server/index.js.map +1 -1
- package/.server/server/plugins/engine/README.md +2 -2
- package/.server/server/plugins/engine/components/helpers/components.js +1 -1
- package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.d.ts +2 -1
- package/.server/server/plugins/engine/configureEnginePlugin.js +4 -4
- package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
- package/.server/server/plugins/engine/helpers.d.ts +7 -11
- package/.server/server/plugins/engine/helpers.js +2 -2
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.js +1 -1
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
- package/.server/server/plugins/engine/options.js +3 -6
- package/.server/server/plugins/engine/options.js.map +1 -1
- package/.server/server/plugins/engine/options.test.js +2 -8
- package/.server/server/plugins/engine/options.test.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +5 -6
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/PageController.d.ts +6 -6
- package/.server/server/plugins/engine/pageControllers/PageController.js +4 -4
- package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +13 -13
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +12 -20
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/RepeatPageController.d.ts +7 -8
- package/.server/server/plugins/engine/pageControllers/RepeatPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -2
- package/.server/server/plugins/engine/pageControllers/StartPageController.js +1 -1
- package/.server/server/plugins/engine/pageControllers/StartPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StatusPageController.d.ts +3 -4
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js +1 -1
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +5 -4
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +12 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/TerminalPageController.d.ts +4 -4
- package/.server/server/plugins/engine/pageControllers/TerminalPageController.js +1 -1
- package/.server/server/plugins/engine/pageControllers/TerminalPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/__stubs__/server.d.ts +1 -1
- package/.server/server/plugins/engine/pageControllers/__stubs__/server.js +2 -6
- package/.server/server/plugins/engine/pageControllers/__stubs__/server.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +7 -12
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/routes/index.d.ts +5 -5
- package/.server/server/plugins/engine/routes/index.js.map +1 -1
- package/.server/server/plugins/engine/routes/questions.d.ts +4 -4
- package/.server/server/plugins/engine/routes/questions.js.map +1 -1
- package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -1
- package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -1
- package/.server/server/plugins/engine/types/index.d.ts +2 -2
- package/.server/server/plugins/engine/types/index.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +10 -11
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/views/partials/form.html +3 -3
- package/.server/server/plugins/engine/views/summary.html +21 -5
- package/.server/server/plugins/nunjucks/context.d.ts +5 -6
- package/.server/server/plugins/nunjucks/context.js +3 -3
- package/.server/server/plugins/nunjucks/context.js.map +1 -1
- package/.server/server/routes/types.d.ts +3 -2
- package/.server/server/routes/types.js +1 -1
- package/.server/server/routes/types.js.map +1 -1
- package/.server/server/schemas/index.js +1 -1
- package/.server/server/schemas/index.js.map +1 -1
- package/.server/server/services/cacheService.d.ts +11 -19
- package/.server/server/services/cacheService.js +9 -30
- package/.server/server/services/cacheService.js.map +1 -1
- package/.server/server/types.d.ts +4 -1
- package/.server/server/types.js.map +1 -1
- package/.server/typings/hapi/index.d.js.map +1 -1
- package/package.json +3 -1
- package/src/server/index.test.ts +0 -39
- package/src/server/index.ts +4 -1
- package/src/server/plugins/engine/README.md +2 -2
- package/src/server/plugins/engine/components/MultilineTextField.test.ts +11 -0
- package/src/server/plugins/engine/components/helpers/components.ts +1 -1
- package/src/server/plugins/engine/components/helpers/helpers.test.ts +1 -1
- package/src/server/plugins/engine/configureEnginePlugin.ts +15 -11
- package/src/server/plugins/engine/helpers.test.ts +3 -2
- package/src/server/plugins/engine/helpers.ts +6 -6
- package/src/server/plugins/engine/models/FormModel.test.ts +2 -2
- package/src/server/plugins/engine/models/FormModel.ts +1 -2
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +7 -7
- package/src/server/plugins/engine/models/SummaryViewModel.ts +1 -1
- package/src/server/plugins/engine/options.js +6 -6
- package/src/server/plugins/engine/options.test.js +2 -6
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +8 -10
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +9 -8
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +11 -8
- package/src/server/plugins/engine/pageControllers/PageController.ts +9 -15
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +35 -102
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +24 -36
- package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +4 -6
- package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +8 -11
- package/src/server/plugins/engine/pageControllers/StartPageController.test.ts +4 -4
- package/src/server/plugins/engine/pageControllers/StartPageController.ts +1 -1
- package/src/server/plugins/engine/pageControllers/StatusPageController.test.ts +4 -4
- package/src/server/plugins/engine/pageControllers/StatusPageController.ts +6 -4
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +15 -5
- package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +4 -4
- package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +7 -4
- package/src/server/plugins/engine/pageControllers/__stubs__/server.ts +5 -6
- package/src/server/plugins/engine/plugin.ts +7 -13
- package/src/server/plugins/engine/routes/index.ts +6 -11
- package/src/server/plugins/engine/routes/questions.test.ts +29 -53
- package/src/server/plugins/engine/routes/questions.ts +6 -8
- package/src/server/plugins/engine/routes/repeaters/item-delete.ts +5 -14
- package/src/server/plugins/engine/routes/repeaters/summary.ts +5 -14
- package/src/server/plugins/engine/types/index.ts +4 -1
- package/src/server/plugins/engine/types.ts +19 -13
- package/src/server/plugins/engine/views/partials/form.html +3 -3
- package/src/server/plugins/engine/views/summary.html +21 -5
- package/src/server/plugins/nunjucks/context.js +3 -3
- package/src/server/routes/types.ts +7 -2
- package/src/server/schemas/index.ts +1 -1
- package/src/server/services/cacheService.test.ts +1 -117
- package/src/server/services/cacheService.ts +22 -73
- package/src/server/types.ts +4 -1
- package/src/typings/hapi/index.d.ts +6 -7
- package/.server/server/plugins/engine/routes/exit.d.ts +0 -46
- package/.server/server/plugins/engine/routes/exit.js +0 -36
- package/.server/server/plugins/engine/routes/exit.js.map +0 -1
- package/src/server/plugins/engine/routes/exit.ts +0 -47
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
preventDoubleClick: true
|
|
14
14
|
}) }}
|
|
15
15
|
|
|
16
|
-
{% if
|
|
16
|
+
{% if allowSaveAndExit %}
|
|
17
17
|
{{ govukButton({
|
|
18
|
-
text: "Save and
|
|
18
|
+
text: "Save and exit",
|
|
19
19
|
classes: "govuk-button--secondary",
|
|
20
20
|
name: "action",
|
|
21
|
-
value: "save-and-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
100
|
+
* @import { AnyFormRequest } from '~/src/server/plugins/engine/types.js'
|
|
101
101
|
*/
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
48
|
+
SaveAndExit = 'save-and-exit'
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
export enum FormStatus {
|
|
@@ -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', () => {
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Server } from '@hapi/hapi'
|
|
2
2
|
import * as Hoek from '@hapi/hoek'
|
|
3
3
|
|
|
4
4
|
import { config } from '~/src/config/index.js'
|
|
5
5
|
import { type createServer } from '~/src/server/index.js'
|
|
6
6
|
import {
|
|
7
|
+
type AnyFormRequest,
|
|
8
|
+
type AnyRequest,
|
|
7
9
|
type FormPayload,
|
|
8
10
|
type FormState,
|
|
9
11
|
type FormSubmissionError,
|
|
10
12
|
type FormSubmissionState
|
|
11
13
|
} from '~/src/server/plugins/engine/types.js'
|
|
12
|
-
import {
|
|
13
|
-
type FormRequest,
|
|
14
|
-
type FormRequestPayload
|
|
15
|
-
} from '~/src/server/routes/types.js'
|
|
16
14
|
|
|
17
15
|
const partition = 'cache'
|
|
18
16
|
|
|
@@ -25,66 +23,28 @@ export class CacheService {
|
|
|
25
23
|
* This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}
|
|
26
24
|
*/
|
|
27
25
|
cache
|
|
28
|
-
generateKey?: (request: Request | FormRequest | FormRequestPayload) => string
|
|
29
|
-
customFetcher?: (
|
|
30
|
-
request: Request | FormRequest | FormRequestPayload
|
|
31
|
-
) => Promise<FormSubmissionState | null>
|
|
32
|
-
|
|
33
26
|
logger: Server['logger']
|
|
34
27
|
|
|
35
|
-
constructor({
|
|
36
|
-
server,
|
|
37
|
-
cacheName,
|
|
38
|
-
options
|
|
39
|
-
}: {
|
|
40
|
-
server: Server
|
|
41
|
-
cacheName?: string
|
|
42
|
-
options?: {
|
|
43
|
-
keyGenerator?: (
|
|
44
|
-
request: Request | FormRequest | FormRequestPayload
|
|
45
|
-
) => string
|
|
46
|
-
sessionHydrator?: (
|
|
47
|
-
request: Request | FormRequest | FormRequestPayload
|
|
48
|
-
) => Promise<FormSubmissionState | null>
|
|
49
|
-
}
|
|
50
|
-
}) {
|
|
51
|
-
const { keyGenerator, sessionHydrator } = options ?? {}
|
|
28
|
+
constructor({ server, cacheName }: { server: Server; cacheName?: string }) {
|
|
52
29
|
if (!cacheName) {
|
|
53
30
|
server.log(
|
|
54
31
|
'warn',
|
|
55
32
|
'You are using the default hapi cache. Please provide a cache name in plugin registration options.'
|
|
56
33
|
)
|
|
57
34
|
}
|
|
58
|
-
|
|
59
|
-
this.customFetcher = sessionHydrator ?? undefined
|
|
35
|
+
|
|
60
36
|
this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })
|
|
61
37
|
this.logger = server.logger
|
|
62
38
|
}
|
|
63
39
|
|
|
64
|
-
async getState(
|
|
65
|
-
request: Request | FormRequest | FormRequestPayload
|
|
66
|
-
): Promise<FormSubmissionState> {
|
|
40
|
+
async getState(request: AnyRequest): Promise<FormSubmissionState> {
|
|
67
41
|
const key = this.Key(request)
|
|
68
|
-
|
|
69
|
-
let cached = await this.cache.get(key)
|
|
70
|
-
|
|
71
|
-
// If nothing in Redis, attempt to rehydrate from backend DB
|
|
72
|
-
if (!cached && this.customFetcher) {
|
|
73
|
-
const rehydrated = await this.customFetcher(request)
|
|
74
|
-
|
|
75
|
-
if (rehydrated != null) {
|
|
76
|
-
await this.cache.set(key, rehydrated, config.get('sessionTimeout'))
|
|
77
|
-
cached = await this.getState(request)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
42
|
+
const cached = await this.cache.get(key)
|
|
80
43
|
|
|
81
44
|
return cached ?? {}
|
|
82
45
|
}
|
|
83
46
|
|
|
84
|
-
async setState(
|
|
85
|
-
request: FormRequest | FormRequestPayload,
|
|
86
|
-
state: FormSubmissionState
|
|
87
|
-
) {
|
|
47
|
+
async setState(request: AnyFormRequest, state: FormSubmissionState) {
|
|
88
48
|
const key = this.Key(request)
|
|
89
49
|
const ttl = config.get('sessionTimeout')
|
|
90
50
|
|
|
@@ -94,7 +54,7 @@ export class CacheService {
|
|
|
94
54
|
}
|
|
95
55
|
|
|
96
56
|
async getConfirmationState(
|
|
97
|
-
request:
|
|
57
|
+
request: AnyFormRequest
|
|
98
58
|
): Promise<{ confirmed?: true }> {
|
|
99
59
|
const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)
|
|
100
60
|
const value = await this.cache.get(key)
|
|
@@ -103,7 +63,7 @@ export class CacheService {
|
|
|
103
63
|
}
|
|
104
64
|
|
|
105
65
|
async setConfirmationState(
|
|
106
|
-
request:
|
|
66
|
+
request: AnyFormRequest,
|
|
107
67
|
confirmationState: { confirmed?: true }
|
|
108
68
|
) {
|
|
109
69
|
const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)
|
|
@@ -112,14 +72,14 @@ export class CacheService {
|
|
|
112
72
|
return this.cache.set(key, confirmationState, ttl)
|
|
113
73
|
}
|
|
114
74
|
|
|
115
|
-
async clearState(request:
|
|
75
|
+
async clearState(request: AnyFormRequest) {
|
|
116
76
|
if (request.yar.id) {
|
|
117
77
|
await this.cache.drop(this.Key(request))
|
|
118
78
|
}
|
|
119
79
|
}
|
|
120
80
|
|
|
121
81
|
getFlash(
|
|
122
|
-
request:
|
|
82
|
+
request: AnyFormRequest
|
|
123
83
|
): { errors: FormSubmissionError[] } | undefined {
|
|
124
84
|
const key = this.Key(request)
|
|
125
85
|
const messages = request.yar.flash(key.id)
|
|
@@ -130,7 +90,7 @@ export class CacheService {
|
|
|
130
90
|
}
|
|
131
91
|
|
|
132
92
|
setFlash(
|
|
133
|
-
request:
|
|
93
|
+
request: AnyFormRequest,
|
|
134
94
|
message: { errors: FormSubmissionError[] }
|
|
135
95
|
) {
|
|
136
96
|
const key = this.Key(request)
|
|
@@ -138,35 +98,24 @@ export class CacheService {
|
|
|
138
98
|
request.yar.flash(key.id, message)
|
|
139
99
|
}
|
|
140
100
|
|
|
141
|
-
private defaultKeyGenerator(
|
|
142
|
-
request: Request | FormRequest | FormRequestPayload
|
|
143
|
-
): string {
|
|
144
|
-
if (!request.yar.id) {
|
|
145
|
-
throw new Error('No session ID found')
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const state = (request.params.state as string) || ''
|
|
149
|
-
const slug = (request.params.slug as string) || ''
|
|
150
|
-
return `${request.yar.id}:${state}:${slug}:`
|
|
151
|
-
}
|
|
152
|
-
|
|
153
101
|
/**
|
|
154
102
|
* The key used to store user session data against.
|
|
155
103
|
* If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`
|
|
156
104
|
* @param request - hapi request object
|
|
157
105
|
* @param additionalIdentifier - appended to the id
|
|
158
106
|
*/
|
|
159
|
-
Key(
|
|
160
|
-
request
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
107
|
+
Key(request: AnyRequest, additionalIdentifier?: ADDITIONAL_IDENTIFIER) {
|
|
108
|
+
if (!request.yar.id) {
|
|
109
|
+
throw new Error('No session ID found')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const state = (request.params.state as string) || ''
|
|
113
|
+
const slug = (request.params.slug as string) || ''
|
|
114
|
+
const key = `${request.yar.id}:${state}:${slug}:`
|
|
166
115
|
|
|
167
116
|
return {
|
|
168
117
|
segment: partition,
|
|
169
|
-
id: `${
|
|
118
|
+
id: `${key}${additionalIdentifier ?? ''}`
|
|
170
119
|
}
|
|
171
120
|
}
|
|
172
121
|
}
|
package/src/server/types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type SubmitPayload,
|
|
5
5
|
type SubmitResponsePayload
|
|
6
6
|
} from '@defra/forms-model'
|
|
7
|
+
import { type Server } from '@hapi/hapi'
|
|
7
8
|
|
|
8
9
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
9
10
|
import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type FormRequestPayload,
|
|
19
20
|
type FormStatus
|
|
20
21
|
} from '~/src/server/routes/types.js'
|
|
22
|
+
import { type CacheService } from '~/src/server/services/cacheService.js'
|
|
21
23
|
|
|
22
24
|
export interface FormsService {
|
|
23
25
|
getFormMetadata: (slug: string) => Promise<FormMetadata>
|
|
@@ -49,7 +51,8 @@ export interface RouteConfig {
|
|
|
49
51
|
controllers?: Record<string, typeof PageController>
|
|
50
52
|
preparePageEventRequestOptions?: PreparePageEventRequestOptions
|
|
51
53
|
onRequest?: OnRequestCallback
|
|
52
|
-
|
|
54
|
+
saveAndExit?: PluginOptions['saveAndExit']
|
|
55
|
+
cacheServiceCreator?: (server: Server) => CacheService
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export interface OutputService {
|
|
@@ -5,11 +5,10 @@ import { type ServerYar, type Yar } from '@hapi/yar'
|
|
|
5
5
|
import { type Logger } from 'pino'
|
|
6
6
|
|
|
7
7
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
8
|
-
import { type PluginOptions } from '~/src/server/plugins/engine/types.ts'
|
|
9
8
|
import {
|
|
10
|
-
type
|
|
11
|
-
type
|
|
12
|
-
} from '~/src/server/
|
|
9
|
+
type AnyFormRequest,
|
|
10
|
+
type PluginOptions
|
|
11
|
+
} from '~/src/server/plugins/engine/types.ts'
|
|
13
12
|
import { type CacheService } from '~/src/server/services/index.js'
|
|
14
13
|
|
|
15
14
|
declare module '@hapi/hapi' {
|
|
@@ -17,15 +16,15 @@ declare module '@hapi/hapi' {
|
|
|
17
16
|
// props from plugins which doesn't export @types
|
|
18
17
|
interface PluginProperties {
|
|
19
18
|
crumb: {
|
|
20
|
-
generate?: (request:
|
|
19
|
+
generate?: (request: AnyRequest) => string
|
|
21
20
|
}
|
|
22
21
|
'forms-engine-plugin': {
|
|
23
22
|
baseLayoutPath: string
|
|
24
23
|
cacheService: CacheService
|
|
25
24
|
viewContext?: (
|
|
26
|
-
request:
|
|
25
|
+
request: AnyFormRequest | null
|
|
27
26
|
) => Record<string, unknown> | Promise<Record<string, unknown>>
|
|
28
|
-
|
|
27
|
+
saveAndExit?: PluginOptions['saveAndExit']
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { type ResponseToolkit, type RouteOptions } from '@hapi/hapi';
|
|
2
|
-
import Joi from 'joi';
|
|
3
|
-
import { type FormRequest, type FormRequestRefs } from '~/src/server/routes/types.js';
|
|
4
|
-
export declare function getRoutes(getRouteOptions: RouteOptions<FormRequestRefs>): {
|
|
5
|
-
method: string;
|
|
6
|
-
path: string;
|
|
7
|
-
handler: (request: FormRequest, h: Pick<ResponseToolkit, "redirect" | "view">) => import("@hapi/hapi").ResponseObject;
|
|
8
|
-
options: {
|
|
9
|
-
validate: {
|
|
10
|
-
params: Joi.ObjectSchema<any>;
|
|
11
|
-
};
|
|
12
|
-
auth?: false | string | import("@hapi/hapi").RouteOptionsAccess | undefined;
|
|
13
|
-
app?: import("@hapi/hapi").RouteOptionsApp | undefined;
|
|
14
|
-
bind?: object | null | undefined;
|
|
15
|
-
cache?: false | import("@hapi/hapi").RouteOptionsCache | undefined;
|
|
16
|
-
compression?: { [P in keyof import("@hapi/hapi").ContentEncoders]?: Parameters<import("@hapi/hapi").ContentEncoders[P]>[0]; } | undefined;
|
|
17
|
-
cors?: boolean | import("@hapi/hapi").RouteOptionsCors | undefined;
|
|
18
|
-
description?: string | undefined;
|
|
19
|
-
ext?: { [key in import("@hapi/hapi").RouteRequestExtType]?: import("@hapi/hapi").RouteExtObject | import("@hapi/hapi").RouteExtObject[] | undefined; } | undefined;
|
|
20
|
-
files?: {
|
|
21
|
-
relativeTo: string;
|
|
22
|
-
} | undefined;
|
|
23
|
-
handler?: object | import("@hapi/hapi").Lifecycle.Method<FormRequestRefs, import("@hapi/hapi").Lifecycle.ReturnValue<FormRequestRefs>> | undefined;
|
|
24
|
-
id?: string | undefined;
|
|
25
|
-
isInternal?: boolean | undefined;
|
|
26
|
-
json?: import("@hapi/hapi").Json.StringifyArguments | undefined;
|
|
27
|
-
log?: {
|
|
28
|
-
collect: boolean;
|
|
29
|
-
} | undefined;
|
|
30
|
-
notes?: string | string[] | undefined;
|
|
31
|
-
payload?: import("@hapi/hapi").RouteOptionsPayload | undefined;
|
|
32
|
-
plugins?: import("@hapi/hapi").PluginSpecificConfiguration | undefined;
|
|
33
|
-
pre?: import("@hapi/hapi").RouteOptionsPreArray<FormRequestRefs> | undefined;
|
|
34
|
-
response?: import("@hapi/hapi").RouteOptionsResponse | undefined;
|
|
35
|
-
security?: import("@hapi/hapi").RouteOptionsSecure | undefined;
|
|
36
|
-
state?: {
|
|
37
|
-
parse?: boolean | undefined;
|
|
38
|
-
failAction?: import("@hapi/hapi").Lifecycle.FailAction | undefined;
|
|
39
|
-
} | undefined;
|
|
40
|
-
tags?: string[] | undefined;
|
|
41
|
-
timeout?: {
|
|
42
|
-
server?: boolean | number | undefined;
|
|
43
|
-
socket?: boolean | number | undefined;
|
|
44
|
-
} | undefined;
|
|
45
|
-
};
|
|
46
|
-
}[];
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { slugSchema } from '@defra/forms-model';
|
|
2
|
-
import Boom from '@hapi/boom';
|
|
3
|
-
import Joi from 'joi';
|
|
4
|
-
export function getRoutes(getRouteOptions) {
|
|
5
|
-
return [{
|
|
6
|
-
method: 'get',
|
|
7
|
-
path: '/{slug}/exit',
|
|
8
|
-
handler: (request, h) => {
|
|
9
|
-
const {
|
|
10
|
-
app
|
|
11
|
-
} = request;
|
|
12
|
-
const {
|
|
13
|
-
model
|
|
14
|
-
} = app;
|
|
15
|
-
if (!model) {
|
|
16
|
-
throw Boom.notFound('No model found for exit page');
|
|
17
|
-
}
|
|
18
|
-
const returnUrl = request.query.returnUrl;
|
|
19
|
-
const exitViewModel = {
|
|
20
|
-
pageTitle: 'Your progress has been saved',
|
|
21
|
-
phaseTag: model.def.phaseBanner?.phase,
|
|
22
|
-
returnUrl
|
|
23
|
-
};
|
|
24
|
-
return h.view('exit', exitViewModel);
|
|
25
|
-
},
|
|
26
|
-
options: {
|
|
27
|
-
...getRouteOptions,
|
|
28
|
-
validate: {
|
|
29
|
-
params: Joi.object().keys({
|
|
30
|
-
slug: slugSchema
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}];
|
|
35
|
-
}
|
|
36
|
-
//# sourceMappingURL=exit.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"exit.js","names":["slugSchema","Boom","Joi","getRoutes","getRouteOptions","method","path","handler","request","h","app","model","notFound","returnUrl","query","exitViewModel","pageTitle","phaseTag","def","phaseBanner","phase","view","options","validate","params","object","keys","slug"],"sources":["../../../../../src/server/plugins/engine/routes/exit.ts"],"sourcesContent":["import { slugSchema } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'\nimport Joi from 'joi'\n\nimport {\n type FormRequest,\n type FormRequestRefs\n} from '~/src/server/routes/types.js'\n\nexport function getRoutes(getRouteOptions: RouteOptions<FormRequestRefs>) {\n return [\n {\n method: 'get',\n path: '/{slug}/exit',\n handler: (\n request: FormRequest,\n h: Pick<ResponseToolkit, 'redirect' | 'view'>\n ) => {\n const { app } = request\n const { model } = app\n\n if (!model) {\n throw Boom.notFound('No model found for exit page')\n }\n\n const returnUrl = request.query.returnUrl\n\n const exitViewModel = {\n pageTitle: 'Your progress has been saved',\n phaseTag: model.def.phaseBanner?.phase,\n returnUrl\n }\n\n return h.view('exit', exitViewModel)\n },\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema\n })\n }\n }\n }\n ]\n}\n"],"mappings":"AAAA,SAASA,UAAU,QAAQ,oBAAoB;AAC/C,OAAOC,IAAI,MAAM,YAAY;AAE7B,OAAOC,GAAG,MAAM,KAAK;AAOrB,OAAO,SAASC,SAASA,CAACC,eAA8C,EAAE;EACxE,OAAO,CACL;IACEC,MAAM,EAAE,KAAK;IACbC,IAAI,EAAE,cAAc;IACpBC,OAAO,EAAEA,CACPC,OAAoB,EACpBC,CAA6C,KAC1C;MACH,MAAM;QAAEC;MAAI,CAAC,GAAGF,OAAO;MACvB,MAAM;QAAEG;MAAM,CAAC,GAAGD,GAAG;MAErB,IAAI,CAACC,KAAK,EAAE;QACV,MAAMV,IAAI,CAACW,QAAQ,CAAC,8BAA8B,CAAC;MACrD;MAEA,MAAMC,SAAS,GAAGL,OAAO,CAACM,KAAK,CAACD,SAAS;MAEzC,MAAME,aAAa,GAAG;QACpBC,SAAS,EAAE,8BAA8B;QACzCC,QAAQ,EAAEN,KAAK,CAACO,GAAG,CAACC,WAAW,EAAEC,KAAK;QACtCP;MACF,CAAC;MAED,OAAOJ,CAAC,CAACY,IAAI,CAAC,MAAM,EAAEN,aAAa,CAAC;IACtC,CAAC;IACDO,OAAO,EAAE;MACP,GAAGlB,eAAe;MAClBmB,QAAQ,EAAE;QACRC,MAAM,EAAEtB,GAAG,CAACuB,MAAM,CAAC,CAAC,CAACC,IAAI,CAAC;UACxBC,IAAI,EAAE3B;QACR,CAAC;MACH;IACF;EACF,CAAC,CACF;AACH","ignoreList":[]}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { slugSchema } from '@defra/forms-model'
|
|
2
|
-
import Boom from '@hapi/boom'
|
|
3
|
-
import { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'
|
|
4
|
-
import Joi from 'joi'
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
type FormRequest,
|
|
8
|
-
type FormRequestRefs
|
|
9
|
-
} from '~/src/server/routes/types.js'
|
|
10
|
-
|
|
11
|
-
export function getRoutes(getRouteOptions: RouteOptions<FormRequestRefs>) {
|
|
12
|
-
return [
|
|
13
|
-
{
|
|
14
|
-
method: 'get',
|
|
15
|
-
path: '/{slug}/exit',
|
|
16
|
-
handler: (
|
|
17
|
-
request: FormRequest,
|
|
18
|
-
h: Pick<ResponseToolkit, 'redirect' | 'view'>
|
|
19
|
-
) => {
|
|
20
|
-
const { app } = request
|
|
21
|
-
const { model } = app
|
|
22
|
-
|
|
23
|
-
if (!model) {
|
|
24
|
-
throw Boom.notFound('No model found for exit page')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const returnUrl = request.query.returnUrl
|
|
28
|
-
|
|
29
|
-
const exitViewModel = {
|
|
30
|
-
pageTitle: 'Your progress has been saved',
|
|
31
|
-
phaseTag: model.def.phaseBanner?.phase,
|
|
32
|
-
returnUrl
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return h.view('exit', exitViewModel)
|
|
36
|
-
},
|
|
37
|
-
options: {
|
|
38
|
-
...getRouteOptions,
|
|
39
|
-
validate: {
|
|
40
|
-
params: Joi.object().keys({
|
|
41
|
-
slug: slugSchema
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
}
|