@defra/forms-engine-plugin 2.1.3 → 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/models/FormModel.js +1 -1
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- 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/models/FormModel.test.ts +64 -0
- package/src/server/plugins/engine/models/FormModel.ts +5 -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
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FormStatus,
|
|
3
|
+
idSchema,
|
|
4
|
+
notificationEmailAddressSchema,
|
|
5
|
+
slugSchema,
|
|
6
|
+
titleSchema
|
|
7
|
+
} from '@defra/forms-model'
|
|
8
|
+
import Joi from 'joi'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
FormAdapterSubmissionSchemaVersion,
|
|
12
|
+
type FormAdapterSubmissionMessageData,
|
|
13
|
+
type FormAdapterSubmissionMessageMeta,
|
|
14
|
+
type FormAdapterSubmissionMessagePayload
|
|
15
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
16
|
+
|
|
17
|
+
export const formAdapterSubmissionMessageMetaSchema =
|
|
18
|
+
Joi.object<FormAdapterSubmissionMessageMeta>().keys({
|
|
19
|
+
schemaVersion: Joi.string().valid(
|
|
20
|
+
...Object.values(FormAdapterSubmissionSchemaVersion)
|
|
21
|
+
),
|
|
22
|
+
timestamp: Joi.date().required(),
|
|
23
|
+
referenceNumber: Joi.string().required(),
|
|
24
|
+
formName: titleSchema,
|
|
25
|
+
formId: idSchema,
|
|
26
|
+
formSlug: slugSchema,
|
|
27
|
+
status: Joi.string()
|
|
28
|
+
.valid(...Object.values(FormStatus))
|
|
29
|
+
.required(),
|
|
30
|
+
isPreview: Joi.boolean().required(),
|
|
31
|
+
notificationEmail: notificationEmailAddressSchema.required()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const formAdapterSubmissionMessageDataSchema =
|
|
35
|
+
Joi.object<FormAdapterSubmissionMessageData>().keys({
|
|
36
|
+
main: Joi.object(),
|
|
37
|
+
repeaters: Joi.object(),
|
|
38
|
+
files: Joi.object()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export const formAdapterSubmissionMessagePayloadSchema =
|
|
42
|
+
Joi.object<FormAdapterSubmissionMessagePayload>().keys({
|
|
43
|
+
meta: formAdapterSubmissionMessageMetaSchema.required(),
|
|
44
|
+
data: formAdapterSubmissionMessageDataSchema.required()
|
|
45
|
+
})
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type ComponentViewModel
|
|
19
19
|
} from '~/src/server/plugins/engine/components/types.js'
|
|
20
20
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
21
|
+
import { type RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
|
|
21
22
|
import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
|
|
22
23
|
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'
|
|
23
24
|
import { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'
|
|
@@ -25,7 +26,8 @@ import {
|
|
|
25
26
|
type FormAction,
|
|
26
27
|
type FormParams,
|
|
27
28
|
type FormRequest,
|
|
28
|
-
type FormRequestPayload
|
|
29
|
+
type FormRequestPayload,
|
|
30
|
+
type FormStatus
|
|
29
31
|
} from '~/src/server/routes/types.js'
|
|
30
32
|
import { type RequestOptions } from '~/src/server/services/httpService.js'
|
|
31
33
|
import { type Services } from '~/src/server/types.js'
|
|
@@ -384,3 +386,51 @@ export interface PluginOptions {
|
|
|
384
386
|
onRequest?: OnRequestCallback
|
|
385
387
|
baseUrl: string // base URL of the application, protocol and hostname e.g. "https://myapp.com"
|
|
386
388
|
}
|
|
389
|
+
|
|
390
|
+
export interface FormAdapterSubmissionMessageMeta {
|
|
391
|
+
schemaVersion: FormAdapterSubmissionSchemaVersion
|
|
392
|
+
timestamp: Date
|
|
393
|
+
referenceNumber: string
|
|
394
|
+
formName: string
|
|
395
|
+
formId: string
|
|
396
|
+
formSlug: string
|
|
397
|
+
status: FormStatus
|
|
398
|
+
isPreview: boolean
|
|
399
|
+
notificationEmail: string
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export type FormAdapterSubmissionMessageMetaSerialised = Omit<
|
|
403
|
+
FormAdapterSubmissionMessageMeta,
|
|
404
|
+
'schemaVersion' | 'timestamp' | 'status'
|
|
405
|
+
> & {
|
|
406
|
+
schemaVersion: string
|
|
407
|
+
status: string
|
|
408
|
+
timestamp: string
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface FormAdapterSubmissionMessageData {
|
|
412
|
+
main: Record<string, RichFormValue>
|
|
413
|
+
repeaters: Record<string, Record<string, RichFormValue>[]>
|
|
414
|
+
files: Record<string, Record<string, string>[]>
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export enum FormAdapterSubmissionSchemaVersion {
|
|
418
|
+
V1 = 1
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface FormAdapterSubmissionMessagePayload {
|
|
422
|
+
meta: FormAdapterSubmissionMessageMeta
|
|
423
|
+
data: FormAdapterSubmissionMessageData
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export interface FormAdapterSubmissionMessage
|
|
427
|
+
extends FormAdapterSubmissionMessagePayload {
|
|
428
|
+
messageId: string
|
|
429
|
+
recordCreatedAt: Date
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export interface FormAdapterSubmissionService {
|
|
433
|
+
handleFormSubmission: (
|
|
434
|
+
submissionMessage: FormAdapterSubmissionMessage
|
|
435
|
+
) => unknown
|
|
436
|
+
}
|
package/src/server/types.ts
CHANGED