@defra/forms-engine-plugin 2.1.4 → 2.1.5
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/plugins/engine/outputFormatters/adapter/v1.d.ts +6 -0
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +23 -0
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -0
- package/.server/server/plugins/engine/outputFormatters/human/v1.d.ts +2 -2
- package/.server/server/plugins/engine/outputFormatters/human/v1.js +1 -1
- package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/index.d.ts +4 -3
- package/.server/server/plugins/engine/outputFormatters/index.js +4 -0
- package/.server/server/plugins/engine/outputFormatters/index.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v1.d.ts +2 -2
- package/.server/server/plugins/engine/outputFormatters/machine/v1.js +1 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v1.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +4 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +5 -4
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/services/localFormsService.d.ts +1 -1
- package/.server/server/plugins/engine/services/notifyService.d.ts +7 -2
- package/.server/server/plugins/engine/services/notifyService.js +11 -2
- package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
- package/.server/server/plugins/engine/types/index.d.ts +10 -0
- package/.server/server/plugins/engine/types/index.js +4 -0
- package/.server/server/plugins/engine/types/index.js.map +1 -0
- package/.server/server/plugins/engine/types/schema.d.ts +5 -0
- package/.server/server/plugins/engine/types/schema.js +24 -0
- package/.server/server/plugins/engine/types/schema.js.map +1 -0
- package/.server/server/plugins/engine/types.d.ts +37 -1
- package/.server/server/plugins/engine/types.js +4 -0
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
- package/.server/server/plugins/nunjucks/filters/page.d.ts +1 -1
- package/.server/server/types.d.ts +1 -1
- package/.server/server/types.js.map +1 -1
- package/package.json +6 -1
- package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +506 -0
- package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +53 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.ts +6 -2
- package/src/server/plugins/engine/outputFormatters/index.ts +11 -3
- package/src/server/plugins/engine/outputFormatters/machine/v1.ts +6 -2
- package/src/server/plugins/engine/outputFormatters/machine/v2.ts +1 -1
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +15 -4
- package/src/server/plugins/engine/services/notifyService.test.ts +156 -1
- package/src/server/plugins/engine/services/notifyService.ts +24 -3
- package/src/server/plugins/engine/types/index.ts +96 -0
- package/src/server/plugins/engine/types/schema.test.ts +152 -0
- package/src/server/plugins/engine/types/schema.ts +45 -0
- package/src/server/plugins/engine/types.ts +51 -1
- package/src/server/types.ts +2 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FormMetadata,
|
|
3
|
+
type SubmitResponsePayload
|
|
4
|
+
} from '@defra/forms-model'
|
|
5
|
+
|
|
6
|
+
import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
|
|
7
|
+
import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
8
|
+
import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
|
|
9
|
+
import { format as machineV2 } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
|
|
10
|
+
import {
|
|
11
|
+
FormAdapterSubmissionSchemaVersion,
|
|
12
|
+
type FormAdapterSubmissionMessageData,
|
|
13
|
+
type FormAdapterSubmissionMessagePayload,
|
|
14
|
+
type FormContext
|
|
15
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
16
|
+
import { FormStatus } from '~/src/server/routes/types.js'
|
|
17
|
+
|
|
18
|
+
export function format(
|
|
19
|
+
context: FormContext,
|
|
20
|
+
items: DetailItem[],
|
|
21
|
+
model: FormModel,
|
|
22
|
+
submitResponse: SubmitResponsePayload,
|
|
23
|
+
formStatus: ReturnType<typeof checkFormStatus>,
|
|
24
|
+
formMetadata?: FormMetadata
|
|
25
|
+
): string {
|
|
26
|
+
const v2DataString = machineV2(
|
|
27
|
+
context,
|
|
28
|
+
items,
|
|
29
|
+
model,
|
|
30
|
+
submitResponse,
|
|
31
|
+
formStatus
|
|
32
|
+
)
|
|
33
|
+
const v2DataParsed = JSON.parse(v2DataString) as {
|
|
34
|
+
data: FormAdapterSubmissionMessageData
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const payload: FormAdapterSubmissionMessagePayload = {
|
|
38
|
+
meta: {
|
|
39
|
+
schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
|
|
40
|
+
timestamp: new Date(),
|
|
41
|
+
referenceNumber: context.referenceNumber,
|
|
42
|
+
formName: model.name,
|
|
43
|
+
formId: formMetadata?.id ?? '',
|
|
44
|
+
formSlug: formMetadata?.slug ?? '',
|
|
45
|
+
status: formStatus.isPreview ? FormStatus.Draft : FormStatus.Live,
|
|
46
|
+
isPreview: formStatus.isPreview,
|
|
47
|
+
notificationEmail: formMetadata?.notificationEmail ?? ''
|
|
48
|
+
},
|
|
49
|
+
data: v2DataParsed.data
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return JSON.stringify(payload)
|
|
53
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type FormMetadata,
|
|
3
|
+
type SubmitResponsePayload
|
|
4
|
+
} from '@defra/forms-model'
|
|
2
5
|
import { addDays, format as dateFormat } from 'date-fns'
|
|
3
6
|
|
|
4
7
|
import { config } from '~/src/config/index.js'
|
|
@@ -18,7 +21,8 @@ export function format(
|
|
|
18
21
|
items: DetailItem[],
|
|
19
22
|
model: FormModel,
|
|
20
23
|
submitResponse: SubmitResponsePayload,
|
|
21
|
-
formStatus: ReturnType<typeof checkFormStatus
|
|
24
|
+
formStatus: ReturnType<typeof checkFormStatus>,
|
|
25
|
+
_formMetadata?: FormMetadata
|
|
22
26
|
) {
|
|
23
27
|
const { files } = submitResponse.result
|
|
24
28
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type FormMetadata,
|
|
3
|
+
type SubmitResponsePayload
|
|
4
|
+
} from '@defra/forms-model'
|
|
2
5
|
|
|
3
6
|
import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
|
|
4
7
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
5
8
|
import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
|
|
9
|
+
import { format as formatAdapterV1 } from '~/src/server/plugins/engine/outputFormatters/adapter/v1.js'
|
|
6
10
|
import { format as formatHumanV1 } from '~/src/server/plugins/engine/outputFormatters/human/v1.js'
|
|
7
11
|
import { format as formatMachineV1 } from '~/src/server/plugins/engine/outputFormatters/machine/v1.js'
|
|
8
12
|
import { format as formatMachineV2 } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
|
|
@@ -13,12 +17,13 @@ type Formatter = (
|
|
|
13
17
|
items: DetailItem[],
|
|
14
18
|
model: FormModel,
|
|
15
19
|
submitResponse: SubmitResponsePayload,
|
|
16
|
-
formStatus: ReturnType<typeof checkFormStatus
|
|
20
|
+
formStatus: ReturnType<typeof checkFormStatus>,
|
|
21
|
+
formMetadata?: FormMetadata
|
|
17
22
|
) => string
|
|
18
23
|
|
|
19
24
|
const formatters: Record<
|
|
20
25
|
string,
|
|
21
|
-
Record<string, Formatter | undefined> | undefined
|
|
26
|
+
Record<string, Formatter | typeof formatAdapterV1 | undefined> | undefined
|
|
22
27
|
> = {
|
|
23
28
|
human: {
|
|
24
29
|
'1': formatHumanV1
|
|
@@ -26,6 +31,9 @@ const formatters: Record<
|
|
|
26
31
|
machine: {
|
|
27
32
|
'1': formatMachineV1,
|
|
28
33
|
'2': formatMachineV2
|
|
34
|
+
},
|
|
35
|
+
adapter: {
|
|
36
|
+
'1': formatAdapterV1
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type FormMetadata,
|
|
3
|
+
type SubmitResponsePayload
|
|
4
|
+
} from '@defra/forms-model'
|
|
2
5
|
|
|
3
6
|
import { config } from '~/src/config/index.js'
|
|
4
7
|
import { getAnswer } from '~/src/server/plugins/engine/components/helpers.js'
|
|
@@ -19,7 +22,8 @@ export function format(
|
|
|
19
22
|
items: DetailItem[],
|
|
20
23
|
model: FormModel,
|
|
21
24
|
_submitResponse: SubmitResponsePayload,
|
|
22
|
-
_formStatus: ReturnType<typeof checkFormStatus
|
|
25
|
+
_formStatus: ReturnType<typeof checkFormStatus>,
|
|
26
|
+
_formMetadata?: FormMetadata
|
|
23
27
|
) {
|
|
24
28
|
const now = new Date()
|
|
25
29
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
hasComponentsEvenIfNoNext,
|
|
3
|
+
type FormMetadata,
|
|
3
4
|
type Page,
|
|
4
5
|
type SubmitPayload
|
|
5
6
|
} from '@defra/forms-model'
|
|
@@ -111,7 +112,8 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
111
112
|
const { getFormMetadata } = formsService
|
|
112
113
|
|
|
113
114
|
// Get the form metadata using the `slug` param
|
|
114
|
-
const
|
|
115
|
+
const formMetadata = await getFormMetadata(params.slug)
|
|
116
|
+
const { notificationEmail } = formMetadata
|
|
115
117
|
const { isPreview } = checkFormStatus(request.params)
|
|
116
118
|
const emailAddress = notificationEmail ?? this.model.def.outputEmail
|
|
117
119
|
|
|
@@ -120,7 +122,14 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
120
122
|
// Send submission email
|
|
121
123
|
if (emailAddress) {
|
|
122
124
|
const viewModel = this.getSummaryViewModel(request, context)
|
|
123
|
-
await submitForm(
|
|
125
|
+
await submitForm(
|
|
126
|
+
context,
|
|
127
|
+
request,
|
|
128
|
+
viewModel,
|
|
129
|
+
model,
|
|
130
|
+
emailAddress,
|
|
131
|
+
formMetadata
|
|
132
|
+
)
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
await cacheService.setConfirmationState(request, { confirmed: true })
|
|
@@ -150,7 +159,8 @@ async function submitForm(
|
|
|
150
159
|
request: FormRequestPayload,
|
|
151
160
|
summaryViewModel: SummaryViewModel,
|
|
152
161
|
model: FormModel,
|
|
153
|
-
emailAddress: string
|
|
162
|
+
emailAddress: string,
|
|
163
|
+
formMetadata: FormMetadata
|
|
154
164
|
) {
|
|
155
165
|
await extendFileRetention(model, context.state, emailAddress)
|
|
156
166
|
|
|
@@ -184,7 +194,8 @@ async function submitForm(
|
|
|
184
194
|
model,
|
|
185
195
|
emailAddress,
|
|
186
196
|
items,
|
|
187
|
-
submitResponse
|
|
197
|
+
submitResponse,
|
|
198
|
+
formMetadata
|
|
188
199
|
)
|
|
189
200
|
}
|
|
190
201
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type FormMetadata } from '@defra/forms-model'
|
|
2
|
+
|
|
3
|
+
import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers.js'
|
|
1
4
|
import { checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
|
|
2
5
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
3
6
|
import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
|
|
@@ -13,6 +16,7 @@ import { sendNotification } from '~/src/server/utils/notify.js'
|
|
|
13
16
|
jest.mock('~/src/server/utils/notify')
|
|
14
17
|
jest.mock('~/src/server/plugins/engine/helpers')
|
|
15
18
|
jest.mock('~/src/server/plugins/engine/outputFormatters/index')
|
|
19
|
+
jest.mock('~/src/server/plugins/engine/components/helpers')
|
|
16
20
|
|
|
17
21
|
describe('notifyService', () => {
|
|
18
22
|
const submitResponse = {
|
|
@@ -32,7 +36,8 @@ describe('notifyService', () => {
|
|
|
32
36
|
const mockRequest: FormRequestPayload = jest.mocked<FormRequestPayload>({
|
|
33
37
|
path: 'test',
|
|
34
38
|
logger: {
|
|
35
|
-
info: jest.fn()
|
|
39
|
+
info: jest.fn(),
|
|
40
|
+
error: jest.fn()
|
|
36
41
|
}
|
|
37
42
|
} as unknown as FormRequestPayload)
|
|
38
43
|
let model: FormModel
|
|
@@ -41,6 +46,7 @@ describe('notifyService', () => {
|
|
|
41
46
|
|
|
42
47
|
beforeEach(() => {
|
|
43
48
|
jest.resetAllMocks()
|
|
49
|
+
jest.mocked(escapeMarkdown).mockImplementation((text) => text)
|
|
44
50
|
})
|
|
45
51
|
|
|
46
52
|
it('creates a subject line for real forms', async () => {
|
|
@@ -152,4 +158,153 @@ describe('notifyService', () => {
|
|
|
152
158
|
})
|
|
153
159
|
)
|
|
154
160
|
})
|
|
161
|
+
|
|
162
|
+
it('calls outputFormatter with all correct arguments', async () => {
|
|
163
|
+
const mockFormatter = jest.fn().mockReturnValue('formatted-output')
|
|
164
|
+
jest.mocked(getFormatter).mockReturnValue(mockFormatter)
|
|
165
|
+
|
|
166
|
+
const formMetadata: FormMetadata = {
|
|
167
|
+
id: 'form-id',
|
|
168
|
+
slug: 'form-slug',
|
|
169
|
+
title: 'Form Title'
|
|
170
|
+
} as FormMetadata
|
|
171
|
+
|
|
172
|
+
const formStatus = {
|
|
173
|
+
isPreview: false,
|
|
174
|
+
state: FormStatus.Live
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
jest.mocked(checkFormStatus).mockReturnValue(formStatus)
|
|
178
|
+
|
|
179
|
+
model = {
|
|
180
|
+
name: 'foobar',
|
|
181
|
+
def: {
|
|
182
|
+
output: {
|
|
183
|
+
audience: 'human',
|
|
184
|
+
version: '1'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} as FormModel
|
|
188
|
+
|
|
189
|
+
await submit(
|
|
190
|
+
formContext,
|
|
191
|
+
mockRequest,
|
|
192
|
+
model,
|
|
193
|
+
'test@defra.gov.uk',
|
|
194
|
+
items,
|
|
195
|
+
submitResponse,
|
|
196
|
+
formMetadata
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
expect(getFormatter).toHaveBeenCalledWith('human', '1')
|
|
200
|
+
|
|
201
|
+
expect(mockFormatter).toHaveBeenCalledWith(
|
|
202
|
+
formContext,
|
|
203
|
+
items,
|
|
204
|
+
model,
|
|
205
|
+
submitResponse,
|
|
206
|
+
formStatus,
|
|
207
|
+
formMetadata
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
expect(sendNotificationMock).toHaveBeenCalledWith(
|
|
211
|
+
expect.objectContaining({
|
|
212
|
+
personalisation: {
|
|
213
|
+
subject: 'Form submission: foobar',
|
|
214
|
+
body: 'formatted-output'
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('calls outputFormatter without formMetadata when not provided', async () => {
|
|
221
|
+
const mockFormatter = jest.fn().mockReturnValue('formatted-output')
|
|
222
|
+
jest.mocked(getFormatter).mockReturnValue(mockFormatter)
|
|
223
|
+
|
|
224
|
+
const formStatus = {
|
|
225
|
+
isPreview: true,
|
|
226
|
+
state: FormStatus.Draft
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
jest.mocked(checkFormStatus).mockReturnValue(formStatus)
|
|
230
|
+
|
|
231
|
+
model = {
|
|
232
|
+
name: 'foobar',
|
|
233
|
+
def: {
|
|
234
|
+
output: {
|
|
235
|
+
audience: 'machine',
|
|
236
|
+
version: '2'
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} as FormModel
|
|
240
|
+
|
|
241
|
+
await submit(
|
|
242
|
+
formContext,
|
|
243
|
+
mockRequest,
|
|
244
|
+
model,
|
|
245
|
+
'test@defra.gov.uk',
|
|
246
|
+
items,
|
|
247
|
+
submitResponse
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
expect(getFormatter).toHaveBeenCalledWith('machine', '2')
|
|
251
|
+
|
|
252
|
+
expect(mockFormatter).toHaveBeenCalledWith(
|
|
253
|
+
formContext,
|
|
254
|
+
items,
|
|
255
|
+
model,
|
|
256
|
+
submitResponse,
|
|
257
|
+
formStatus,
|
|
258
|
+
undefined
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
expect(sendNotificationMock).toHaveBeenCalledWith(
|
|
262
|
+
expect.objectContaining({
|
|
263
|
+
personalisation: {
|
|
264
|
+
subject: 'TEST FORM SUBMISSION: foobar',
|
|
265
|
+
body: Buffer.from('formatted-output').toString('base64')
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('should handle sendNotification errors and rethrow', async () => {
|
|
272
|
+
const mockFormatter = jest.fn().mockReturnValue('formatted-output')
|
|
273
|
+
jest.mocked(getFormatter).mockReturnValue(mockFormatter)
|
|
274
|
+
jest.mocked(checkFormStatus).mockReturnValue({
|
|
275
|
+
isPreview: false,
|
|
276
|
+
state: FormStatus.Live
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const testError = new Error('Notification service unavailable')
|
|
280
|
+
sendNotificationMock.mockRejectedValue(testError)
|
|
281
|
+
|
|
282
|
+
model = {
|
|
283
|
+
name: 'foobar',
|
|
284
|
+
def: {
|
|
285
|
+
output: {
|
|
286
|
+
audience: 'human',
|
|
287
|
+
version: '1'
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} as FormModel
|
|
291
|
+
|
|
292
|
+
await expect(
|
|
293
|
+
submit(
|
|
294
|
+
formContext,
|
|
295
|
+
mockRequest,
|
|
296
|
+
model,
|
|
297
|
+
'test@defra.gov.uk',
|
|
298
|
+
items,
|
|
299
|
+
submitResponse
|
|
300
|
+
)
|
|
301
|
+
).rejects.toThrow('Notification service unavailable')
|
|
302
|
+
|
|
303
|
+
expect(mockRequest.logger.error).toHaveBeenCalledWith(
|
|
304
|
+
'Notification service unavailable',
|
|
305
|
+
expect.stringContaining(
|
|
306
|
+
'[emailSendFailed] Error sending notification email'
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
})
|
|
155
310
|
})
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getErrorMessage,
|
|
3
|
+
type FormMetadata,
|
|
4
|
+
type SubmitResponsePayload
|
|
5
|
+
} from '@defra/forms-model'
|
|
2
6
|
|
|
3
7
|
import { config } from '~/src/config/index.js'
|
|
4
8
|
import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers.js'
|
|
@@ -12,14 +16,24 @@ import { sendNotification } from '~/src/server/utils/notify.js'
|
|
|
12
16
|
|
|
13
17
|
const templateId = config.get('notifyTemplateId')
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Optional GOV.UK Notify service for consumers who want email notifications
|
|
21
|
+
* Can be disabled by not providing notifyTemplateId in config
|
|
22
|
+
* Can be overridden by providing a custom outputService in the services config
|
|
23
|
+
*/
|
|
15
24
|
export async function submit(
|
|
16
25
|
context: FormContext,
|
|
17
26
|
request: FormRequestPayload,
|
|
18
27
|
model: FormModel,
|
|
19
28
|
emailAddress: string,
|
|
20
29
|
items: DetailItem[],
|
|
21
|
-
submitResponse: SubmitResponsePayload
|
|
30
|
+
submitResponse: SubmitResponsePayload,
|
|
31
|
+
formMetadata?: FormMetadata
|
|
22
32
|
) {
|
|
33
|
+
if (!templateId) {
|
|
34
|
+
return Promise.resolve()
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
const logTags = ['submit', 'email']
|
|
24
38
|
const formStatus = checkFormStatus(request.params)
|
|
25
39
|
|
|
@@ -35,7 +49,14 @@ export async function submit(
|
|
|
35
49
|
const outputVersion = model.def.output?.version ?? '1'
|
|
36
50
|
|
|
37
51
|
const outputFormatter = getFormatter(outputAudience, outputVersion)
|
|
38
|
-
let body = outputFormatter(
|
|
52
|
+
let body = outputFormatter(
|
|
53
|
+
context,
|
|
54
|
+
items,
|
|
55
|
+
model,
|
|
56
|
+
submitResponse,
|
|
57
|
+
formStatus,
|
|
58
|
+
formMetadata
|
|
59
|
+
)
|
|
39
60
|
|
|
40
61
|
// GOV.UK Notify transforms quotes into curly quotes, so we can't just send the raw payload
|
|
41
62
|
// This is logic specific to Notify, so we include the logic here rather than in the formatter
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
CheckAnswers,
|
|
3
|
+
ErrorMessageTemplate,
|
|
4
|
+
ErrorMessageTemplateList,
|
|
5
|
+
FeaturedFormPageViewModel,
|
|
6
|
+
FileState,
|
|
7
|
+
FilterFunction,
|
|
8
|
+
FormAdapterSubmissionMessage,
|
|
9
|
+
FormAdapterSubmissionMessageData,
|
|
10
|
+
FormAdapterSubmissionMessageMeta,
|
|
11
|
+
FormAdapterSubmissionMessageMetaSerialised,
|
|
12
|
+
FormAdapterSubmissionMessagePayload,
|
|
13
|
+
FormAdapterSubmissionService,
|
|
14
|
+
FormContext,
|
|
15
|
+
FormContextRequest,
|
|
16
|
+
FormPageViewModel,
|
|
17
|
+
FormPayload,
|
|
18
|
+
FormPayloadParams,
|
|
19
|
+
FormState,
|
|
20
|
+
FormStateValue,
|
|
21
|
+
FormSubmissionError,
|
|
22
|
+
FormSubmissionState,
|
|
23
|
+
FormValidationResult,
|
|
24
|
+
FormValue,
|
|
25
|
+
GlobalFunction,
|
|
26
|
+
ItemDeletePageViewModel,
|
|
27
|
+
OnRequestCallback,
|
|
28
|
+
PageViewModel,
|
|
29
|
+
PageViewModelBase,
|
|
30
|
+
PluginOptions,
|
|
31
|
+
PreparePageEventRequestOptions,
|
|
32
|
+
RepeatItemState,
|
|
33
|
+
RepeatListState,
|
|
34
|
+
RepeaterSummaryPageViewModel,
|
|
35
|
+
SummaryList,
|
|
36
|
+
SummaryListAction,
|
|
37
|
+
SummaryListRow,
|
|
38
|
+
TempFileState,
|
|
39
|
+
UploadInitiateResponse,
|
|
40
|
+
UploadStatusFileResponse,
|
|
41
|
+
UploadStatusResponse
|
|
42
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
FileStatus,
|
|
46
|
+
FormAdapterSubmissionSchemaVersion,
|
|
47
|
+
UploadStatus
|
|
48
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
49
|
+
|
|
50
|
+
export type {
|
|
51
|
+
Detail,
|
|
52
|
+
DetailItem,
|
|
53
|
+
DetailItemBase,
|
|
54
|
+
DetailItemField,
|
|
55
|
+
DetailItemRepeat,
|
|
56
|
+
ExecutableCondition
|
|
57
|
+
} from '~/src/server/plugins/engine/models/types.js'
|
|
58
|
+
|
|
59
|
+
export type {
|
|
60
|
+
BackLink,
|
|
61
|
+
ComponentText,
|
|
62
|
+
ComponentViewModel,
|
|
63
|
+
Content,
|
|
64
|
+
DateInputItem,
|
|
65
|
+
DatePartsState,
|
|
66
|
+
Label,
|
|
67
|
+
ListItem,
|
|
68
|
+
ListItemLabel,
|
|
69
|
+
MonthYearState,
|
|
70
|
+
ViewModel
|
|
71
|
+
} from '~/src/server/plugins/engine/components/types.js'
|
|
72
|
+
|
|
73
|
+
export type { UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'
|
|
74
|
+
|
|
75
|
+
export type {
|
|
76
|
+
FormParams,
|
|
77
|
+
FormQuery,
|
|
78
|
+
FormRequest,
|
|
79
|
+
FormRequestPayload,
|
|
80
|
+
FormRequestPayloadRefs,
|
|
81
|
+
FormRequestRefs
|
|
82
|
+
} from '~/src/server/routes/types.js'
|
|
83
|
+
|
|
84
|
+
export { FormAction, FormStatus } from '~/src/server/routes/types.js'
|
|
85
|
+
|
|
86
|
+
export type {
|
|
87
|
+
FormSubmissionService,
|
|
88
|
+
FormsService,
|
|
89
|
+
OutputService,
|
|
90
|
+
RouteConfig,
|
|
91
|
+
Services
|
|
92
|
+
} from '~/src/server/types.js'
|
|
93
|
+
|
|
94
|
+
export type { RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
|
|
95
|
+
|
|
96
|
+
export * from '~/src/server/plugins/engine/types/schema.js'
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { FormStatus } from '@defra/forms-model'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
formAdapterSubmissionMessageDataSchema,
|
|
5
|
+
formAdapterSubmissionMessageMetaSchema,
|
|
6
|
+
formAdapterSubmissionMessagePayloadSchema
|
|
7
|
+
} from '~/src/server/plugins/engine/types/schema.js'
|
|
8
|
+
import {
|
|
9
|
+
FormAdapterSubmissionSchemaVersion,
|
|
10
|
+
type FormAdapterSubmissionMessageData,
|
|
11
|
+
type FormAdapterSubmissionMessageMeta,
|
|
12
|
+
type FormAdapterSubmissionMessagePayload
|
|
13
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
14
|
+
|
|
15
|
+
describe('Schema validation', () => {
|
|
16
|
+
describe('formAdapterSubmissionMessageMetaSchema', () => {
|
|
17
|
+
const validMeta: FormAdapterSubmissionMessageMeta = {
|
|
18
|
+
schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
|
|
19
|
+
timestamp: new Date('2025-08-22T18:15:10.785Z'),
|
|
20
|
+
referenceNumber: '576-225-943',
|
|
21
|
+
formName: 'Order a pizza',
|
|
22
|
+
formId: '68a8b0449ab460290c28940a',
|
|
23
|
+
formSlug: 'order-a-pizza',
|
|
24
|
+
status: FormStatus.Live,
|
|
25
|
+
isPreview: false,
|
|
26
|
+
notificationEmail: 'info@example.com'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it('should validate valid meta object', () => {
|
|
30
|
+
const { error } =
|
|
31
|
+
formAdapterSubmissionMessageMetaSchema.validate(validMeta)
|
|
32
|
+
expect(error).toBeUndefined()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should reject invalid schema version', () => {
|
|
36
|
+
const invalidMeta = { ...validMeta, schemaVersion: 'invalid' }
|
|
37
|
+
const { error } =
|
|
38
|
+
formAdapterSubmissionMessageMetaSchema.validate(invalidMeta)
|
|
39
|
+
expect(error).toBeDefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should reject missing required fields', () => {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
44
|
+
const { timestamp: _, ...metaWithoutTimestamp } = validMeta
|
|
45
|
+
const { error } =
|
|
46
|
+
formAdapterSubmissionMessageMetaSchema.validate(metaWithoutTimestamp)
|
|
47
|
+
expect(error).toBeDefined()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('formAdapterSubmissionMessageDataSchema', () => {
|
|
52
|
+
const validData: FormAdapterSubmissionMessageData = {
|
|
53
|
+
main: {
|
|
54
|
+
QMwMir: 'Roman Pizza',
|
|
55
|
+
duOEvZ: 'Small',
|
|
56
|
+
DzEODf: ['Mozzarella'],
|
|
57
|
+
juiCfC: ['Pepperoni', 'Sausage', 'Onions', 'Basil'],
|
|
58
|
+
YEpypP: 'None',
|
|
59
|
+
JumNVc: 'Joe Bloggs',
|
|
60
|
+
ALNehP: '+441234567890',
|
|
61
|
+
vAqTmg: {
|
|
62
|
+
addressLine1: '1 Anywhere Street',
|
|
63
|
+
town: 'Anywhereville',
|
|
64
|
+
postcode: 'AN1 2WH'
|
|
65
|
+
},
|
|
66
|
+
IbXVGY: {
|
|
67
|
+
day: 22,
|
|
68
|
+
month: 8,
|
|
69
|
+
year: 2025
|
|
70
|
+
},
|
|
71
|
+
HGBWLt: ['Garlic sauce']
|
|
72
|
+
},
|
|
73
|
+
repeaters: {},
|
|
74
|
+
files: {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
it('should validate valid data object', () => {
|
|
78
|
+
const { error } =
|
|
79
|
+
formAdapterSubmissionMessageDataSchema.validate(validData)
|
|
80
|
+
expect(error).toBeUndefined()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should validate empty objects', () => {
|
|
84
|
+
const emptyData = { main: {}, repeaters: {}, files: {} }
|
|
85
|
+
const { error } =
|
|
86
|
+
formAdapterSubmissionMessageDataSchema.validate(emptyData)
|
|
87
|
+
expect(error).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('formAdapterSubmissionMessagePayloadSchema', () => {
|
|
92
|
+
const validPayload: FormAdapterSubmissionMessagePayload = {
|
|
93
|
+
meta: {
|
|
94
|
+
schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
|
|
95
|
+
timestamp: new Date('2025-08-22T18:15:10.785Z'),
|
|
96
|
+
referenceNumber: '576-225-943',
|
|
97
|
+
formName: 'Order a pizza',
|
|
98
|
+
formId: '68a8b0449ab460290c28940a',
|
|
99
|
+
formSlug: 'order-a-pizza',
|
|
100
|
+
status: FormStatus.Live,
|
|
101
|
+
isPreview: false,
|
|
102
|
+
notificationEmail: 'info@example.com'
|
|
103
|
+
},
|
|
104
|
+
data: {
|
|
105
|
+
main: {
|
|
106
|
+
QMwMir: 'Roman Pizza',
|
|
107
|
+
duOEvZ: 'Small',
|
|
108
|
+
DzEODf: ['Mozzarella'],
|
|
109
|
+
juiCfC: ['Pepperoni', 'Sausage', 'Onions', 'Basil'],
|
|
110
|
+
YEpypP: 'None',
|
|
111
|
+
JumNVc: 'Joe Bloggs',
|
|
112
|
+
ALNehP: '+441234567890',
|
|
113
|
+
vAqTmg: {
|
|
114
|
+
addressLine1: '1 Anywhere Street',
|
|
115
|
+
town: 'Anywhereville',
|
|
116
|
+
postcode: 'AN1 2WH'
|
|
117
|
+
},
|
|
118
|
+
IbXVGY: {
|
|
119
|
+
day: 22,
|
|
120
|
+
month: 8,
|
|
121
|
+
year: 2025
|
|
122
|
+
},
|
|
123
|
+
HGBWLt: ['Garlic sauce']
|
|
124
|
+
},
|
|
125
|
+
repeaters: {},
|
|
126
|
+
files: {}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
it('should validate complete payload', () => {
|
|
131
|
+
const { error } =
|
|
132
|
+
formAdapterSubmissionMessagePayloadSchema.validate(validPayload)
|
|
133
|
+
expect(error).toBeUndefined()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should reject payload without meta', () => {
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
138
|
+
const { meta: _, ...payloadWithoutMeta } = validPayload
|
|
139
|
+
const { error } =
|
|
140
|
+
formAdapterSubmissionMessagePayloadSchema.validate(payloadWithoutMeta)
|
|
141
|
+
expect(error).toBeDefined()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should reject payload without data', () => {
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
146
|
+
const { data: _, ...payloadWithoutData } = validPayload
|
|
147
|
+
const { error } =
|
|
148
|
+
formAdapterSubmissionMessagePayloadSchema.validate(payloadWithoutData)
|
|
149
|
+
expect(error).toBeDefined()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
})
|