@defra/forms-engine-plugin 2.0.3 → 2.1.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.
- package/.public/javascripts/application.min.js +1 -1
- package/.public/javascripts/application.min.js.map +1 -1
- package/.public/javascripts/shared.min.js +1 -1
- package/.public/javascripts/shared.min.js.map +1 -1
- package/.public/stylesheets/application.min.css +2 -2
- package/.public/stylesheets/application.min.css.map +1 -1
- package/.server/client/javascripts/file-upload.js +3 -3
- package/.server/client/javascripts/file-upload.js.map +1 -1
- package/.server/server/forms/page-events.yaml +87 -0
- package/.server/server/index.js +2 -1
- package/.server/server/index.js.map +1 -1
- package/.server/server/plugins/engine/routes/questions.d.ts +4 -2
- package/.server/server/plugins/engine/routes/questions.js +67 -43
- package/.server/server/plugins/engine/routes/questions.js.map +1 -1
- package/.server/server/plugins/engine/services/localFormsService.js +7 -9
- package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
- package/.server/server/routes/dummy-api.d.ts +38 -0
- package/.server/server/routes/dummy-api.js +33 -0
- package/.server/server/routes/dummy-api.js.map +1 -0
- package/.server/server/routes/index.d.ts +1 -0
- package/.server/server/routes/index.js +1 -0
- package/.server/server/routes/index.js.map +1 -1
- package/package.json +12 -10
- package/src/client/javascripts/file-upload.js +3 -3
- package/src/server/forms/page-events.yaml +87 -0
- package/src/server/index.ts +4 -2
- package/src/server/plugins/engine/routes/questions.test.ts +416 -0
- package/src/server/plugins/engine/routes/questions.ts +96 -40
- package/src/server/plugins/engine/services/localFormsService.js +7 -8
- package/src/server/routes/dummy-api.test.ts +96 -0
- package/src/server/routes/dummy-api.ts +62 -0
- package/src/server/routes/index.ts +1 -0
- package/.server/server/forms/register-as-a-unicorn-breeder.json +0 -393
- package/src/server/forms/register-as-a-unicorn-breeder.json +0 -393
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { hasFormComponents, slugSchema } from '@defra/forms-model'
|
|
1
|
+
import { hasFormComponents, slugSchema, type Event } from '@defra/forms-model'
|
|
2
2
|
import Boom from '@hapi/boom'
|
|
3
3
|
import {
|
|
4
|
+
type ResponseObject,
|
|
4
5
|
type ResponseToolkit,
|
|
5
6
|
type RouteOptions,
|
|
6
7
|
type ServerRoute
|
|
@@ -12,14 +13,21 @@ import {
|
|
|
12
13
|
proceed,
|
|
13
14
|
redirectPath
|
|
14
15
|
} from '~/src/server/plugins/engine/helpers.js'
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
SummaryViewModel,
|
|
18
|
+
type FormModel
|
|
19
|
+
} from '~/src/server/plugins/engine/models/index.js'
|
|
16
20
|
import { format } from '~/src/server/plugins/engine/outputFormatters/machine/v1.js'
|
|
17
21
|
import { getFormSubmissionData } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
|
|
22
|
+
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'
|
|
18
23
|
import {
|
|
19
24
|
dispatchHandler,
|
|
20
25
|
redirectOrMakeHandler
|
|
21
26
|
} from '~/src/server/plugins/engine/routes/index.js'
|
|
22
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
type FormContext,
|
|
29
|
+
type PreparePageEventRequestOptions
|
|
30
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
23
31
|
import {
|
|
24
32
|
type FormRequest,
|
|
25
33
|
type FormRequestPayload,
|
|
@@ -35,7 +43,36 @@ import {
|
|
|
35
43
|
} from '~/src/server/schemas/index.js'
|
|
36
44
|
import * as httpService from '~/src/server/services/httpService.js'
|
|
37
45
|
|
|
38
|
-
function
|
|
46
|
+
async function handleHttpEvent(
|
|
47
|
+
request: FormRequest | FormRequestPayload,
|
|
48
|
+
page: PageControllerClass,
|
|
49
|
+
context: FormContext,
|
|
50
|
+
event: Event,
|
|
51
|
+
model: FormModel,
|
|
52
|
+
preparePageEventRequestOptions?: PreparePageEventRequestOptions
|
|
53
|
+
) {
|
|
54
|
+
const { options } = event
|
|
55
|
+
const { url } = options
|
|
56
|
+
|
|
57
|
+
// TODO: Update structured data POST payload with when helper
|
|
58
|
+
// is updated to removing the dependency on `SummaryViewModel` etc.
|
|
59
|
+
const viewModel = new SummaryViewModel(request, page, context)
|
|
60
|
+
const items = getFormSubmissionData(viewModel.context, viewModel.details)
|
|
61
|
+
|
|
62
|
+
// @ts-expect-error - function signature will be refactored in the next iteration of the formatter
|
|
63
|
+
const payload = format(context, items, model, undefined, undefined)
|
|
64
|
+
const opts = { payload }
|
|
65
|
+
|
|
66
|
+
if (preparePageEventRequestOptions) {
|
|
67
|
+
preparePageEventRequestOptions(opts, event, page, context)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { payload: response } = await httpService.postJson(url, opts)
|
|
71
|
+
|
|
72
|
+
Object.assign(context.data, response)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function makeGetHandler(
|
|
39
76
|
preparePageEventRequestOptions?: PreparePageEventRequestOptions
|
|
40
77
|
) {
|
|
41
78
|
return function getHandler(
|
|
@@ -59,28 +96,14 @@ function makeGetHandler(
|
|
|
59
96
|
}
|
|
60
97
|
|
|
61
98
|
if (events?.onLoad && events.onLoad.type === 'http') {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
viewModel.context,
|
|
70
|
-
viewModel.details
|
|
99
|
+
await handleHttpEvent(
|
|
100
|
+
request,
|
|
101
|
+
page,
|
|
102
|
+
context,
|
|
103
|
+
events.onLoad,
|
|
104
|
+
model,
|
|
105
|
+
preparePageEventRequestOptions
|
|
71
106
|
)
|
|
72
|
-
|
|
73
|
-
// @ts-expect-error - function signature will be refactored in the next iteration of the formatter
|
|
74
|
-
const payload = format(context, items, model, undefined, undefined)
|
|
75
|
-
const opts = { payload }
|
|
76
|
-
|
|
77
|
-
if (preparePageEventRequestOptions) {
|
|
78
|
-
preparePageEventRequestOptions(opts, events.onLoad, page, context)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const { payload: response } = await httpService.postJson(url, opts)
|
|
82
|
-
|
|
83
|
-
Object.assign(context.data, response)
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
return page.makeGetRouteHandler()(request, context, h)
|
|
@@ -88,23 +111,56 @@ function makeGetHandler(
|
|
|
88
111
|
}
|
|
89
112
|
}
|
|
90
113
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
h: Pick<ResponseToolkit, 'redirect' | 'view'>
|
|
114
|
+
export function makePostHandler(
|
|
115
|
+
preparePageEventRequestOptions?: PreparePageEventRequestOptions
|
|
94
116
|
) {
|
|
95
|
-
|
|
117
|
+
return function postHandler(
|
|
118
|
+
request: FormRequestPayload,
|
|
119
|
+
h: Pick<ResponseToolkit, 'redirect' | 'view'>
|
|
120
|
+
) {
|
|
121
|
+
const { query } = request
|
|
96
122
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
return redirectOrMakeHandler(request, h, async (page, context) => {
|
|
124
|
+
const { pageDef } = page
|
|
125
|
+
const { isForceAccess } = context
|
|
126
|
+
const { model } = request.app
|
|
127
|
+
const { events } = page
|
|
100
128
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
// Redirect to GET for preview URL direct access
|
|
130
|
+
if (isForceAccess && !hasFormComponents(pageDef)) {
|
|
131
|
+
return proceed(request, h, redirectPath(page.href, query))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!model) {
|
|
135
|
+
throw Boom.notFound(`No model found for /${request.params.path}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const response = await page.makePostRouteHandler()(request, context, h)
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
events?.onSave &&
|
|
142
|
+
events.onSave.type === 'http' &&
|
|
143
|
+
isSuccessful(response)
|
|
144
|
+
) {
|
|
145
|
+
await handleHttpEvent(
|
|
146
|
+
request,
|
|
147
|
+
page,
|
|
148
|
+
context,
|
|
149
|
+
events.onSave,
|
|
150
|
+
model,
|
|
151
|
+
preparePageEventRequestOptions
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return response
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isSuccessful(response: ResponseObject): boolean {
|
|
161
|
+
const { statusCode } = response
|
|
105
162
|
|
|
106
|
-
|
|
107
|
-
})
|
|
163
|
+
return !Boom.isBoom(response) && statusCode >= 200 && statusCode < 400
|
|
108
164
|
}
|
|
109
165
|
|
|
110
166
|
export function getRoutes(
|
|
@@ -174,7 +230,7 @@ export function getRoutes(
|
|
|
174
230
|
{
|
|
175
231
|
method: 'post',
|
|
176
232
|
path: '/{slug}/{path}/{itemId?}',
|
|
177
|
-
handler:
|
|
233
|
+
handler: makePostHandler(preparePageEventRequestOptions),
|
|
178
234
|
options: {
|
|
179
235
|
...postRouteOptions,
|
|
180
236
|
validate: {
|
|
@@ -196,7 +252,7 @@ export function getRoutes(
|
|
|
196
252
|
{
|
|
197
253
|
method: 'post',
|
|
198
254
|
path: '/preview/{state}/{slug}/{path}/{itemId?}',
|
|
199
|
-
handler:
|
|
255
|
+
handler: makePostHandler(preparePageEventRequestOptions),
|
|
200
256
|
options: {
|
|
201
257
|
...postRouteOptions,
|
|
202
258
|
validate: {
|
|
@@ -29,20 +29,19 @@ export const formsService = async () => {
|
|
|
29
29
|
// Instantiate the file loader form service
|
|
30
30
|
const loader = new FileFormService()
|
|
31
31
|
|
|
32
|
-
// Add a
|
|
33
|
-
await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.
|
|
32
|
+
// Add a Yaml form
|
|
33
|
+
await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.yaml', {
|
|
34
34
|
...metadata,
|
|
35
|
-
id: '
|
|
35
|
+
id: '641aeafd-13dd-40fa-9186-001703800efb',
|
|
36
36
|
title: 'Register as a unicorn breeder',
|
|
37
37
|
slug: 'register-as-a-unicorn-breeder'
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.yaml', {
|
|
40
|
+
await loader.addForm('src/server/forms/page-events.yaml', {
|
|
42
41
|
...metadata,
|
|
43
|
-
id: '
|
|
44
|
-
title: '
|
|
45
|
-
slug: '
|
|
42
|
+
id: '511db05e-ebbd-42e8-8270-5fe93f5c9762',
|
|
43
|
+
title: 'Page events demo',
|
|
44
|
+
slug: 'page-events-demo'
|
|
46
45
|
})
|
|
47
46
|
|
|
48
47
|
await loader.addForm('src/server/forms/components.json', {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type Server } from '@hapi/hapi'
|
|
2
|
+
// eslint-disable-next-line n/no-unpublished-import -- not sure why this is triggering, it's not a private module
|
|
3
|
+
import MockDate from 'mockdate'
|
|
4
|
+
|
|
5
|
+
import { createServer } from '~/src/server/index.js'
|
|
6
|
+
|
|
7
|
+
describe('Dummy API', () => {
|
|
8
|
+
let server: Server
|
|
9
|
+
|
|
10
|
+
const payload = {
|
|
11
|
+
meta: {
|
|
12
|
+
referenceNumber: 'FOO-BAR-123'
|
|
13
|
+
},
|
|
14
|
+
data: {
|
|
15
|
+
main: {
|
|
16
|
+
applicantFirstName: 'Joe',
|
|
17
|
+
applicantLastName: 'Bloggs',
|
|
18
|
+
dateOfBirth: '2020-01-01'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
MockDate.set('2025-01-01T00:00:00Z')
|
|
25
|
+
server = await createServer()
|
|
26
|
+
await server.initialize()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
await server.stop()
|
|
31
|
+
MockDate.reset()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should validate on-load-page POST response', async () => {
|
|
35
|
+
const res = await server.inject({
|
|
36
|
+
method: 'POST',
|
|
37
|
+
url: '/api/example/on-load-page',
|
|
38
|
+
payload
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(res.statusCode).toBe(200)
|
|
42
|
+
expect(res.result).toMatchObject({
|
|
43
|
+
submissionEvent: 'GET',
|
|
44
|
+
submissionReferenceNumber: payload.meta.referenceNumber
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
This is just dummy code that won't run in prod, we don't need a full suite of tests.
|
|
50
|
+
*/
|
|
51
|
+
it('should validate on-summary POST response', async () => {
|
|
52
|
+
const res = await server.inject({
|
|
53
|
+
method: 'POST',
|
|
54
|
+
url: '/api/example/on-summary',
|
|
55
|
+
payload
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(res.statusCode).toBe(200)
|
|
59
|
+
expect(res.result).toMatchObject({
|
|
60
|
+
calculatedAge: 5,
|
|
61
|
+
submissionEvent: 'POST',
|
|
62
|
+
submissionReferenceNumber: payload.meta.referenceNumber
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should calculate age correctly when birthday has not occurred yet this year', async () => {
|
|
67
|
+
// Set today's date to just before the birthday in 2025
|
|
68
|
+
MockDate.set('2025-01-01T00:00:00Z')
|
|
69
|
+
|
|
70
|
+
const payloadWithFutureBirthday = {
|
|
71
|
+
meta: {
|
|
72
|
+
referenceNumber: 'FOO-BAR-456'
|
|
73
|
+
},
|
|
74
|
+
data: {
|
|
75
|
+
main: {
|
|
76
|
+
applicantFirstName: 'Jane',
|
|
77
|
+
applicantLastName: 'Doe',
|
|
78
|
+
dateOfBirth: '2020-02-01' // Birthday is in February, today is January
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const res = await server.inject({
|
|
84
|
+
method: 'POST',
|
|
85
|
+
url: '/api/example/on-summary',
|
|
86
|
+
payload: payloadWithFutureBirthday
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(res.statusCode).toBe(200)
|
|
90
|
+
expect(res.result).toMatchObject({
|
|
91
|
+
calculatedAge: 4, // Should be 4, not 5, because birthday hasn't occurred yet
|
|
92
|
+
submissionEvent: 'POST',
|
|
93
|
+
submissionReferenceNumber: payloadWithFutureBirthday.meta.referenceNumber
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Request, type ResponseToolkit } from '@hapi/hapi'
|
|
2
|
+
|
|
3
|
+
function calculateAge(day: string, month: string, year: string) {
|
|
4
|
+
const dobDate = new Date(Number(day), Number(month) - 1, Number(year))
|
|
5
|
+
|
|
6
|
+
const today = new Date()
|
|
7
|
+
|
|
8
|
+
let age = today.getFullYear() - dobDate.getFullYear()
|
|
9
|
+
const m = today.getMonth() - dobDate.getMonth()
|
|
10
|
+
|
|
11
|
+
if (m < 0 || (m === 0 && today.getDate() < dobDate.getDate())) {
|
|
12
|
+
age--
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return age
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default [
|
|
19
|
+
{
|
|
20
|
+
method: 'POST',
|
|
21
|
+
path: '/api/example/on-load-page',
|
|
22
|
+
handler(
|
|
23
|
+
request: Request<{ Payload: { meta: { referenceNumber: string } } }>,
|
|
24
|
+
_h: ResponseToolkit
|
|
25
|
+
) {
|
|
26
|
+
return {
|
|
27
|
+
submissionEvent: 'GET',
|
|
28
|
+
submissionReferenceNumber: request.payload.meta.referenceNumber
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
method: 'POST',
|
|
34
|
+
path: '/api/example/on-summary',
|
|
35
|
+
handler(
|
|
36
|
+
request: Request<{
|
|
37
|
+
Payload: {
|
|
38
|
+
data: {
|
|
39
|
+
main: {
|
|
40
|
+
applicantFirstName: string
|
|
41
|
+
applicantLastName: string
|
|
42
|
+
dateOfBirth: string
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
meta: { event: string; referenceNumber: string }
|
|
46
|
+
}
|
|
47
|
+
}>,
|
|
48
|
+
_h: ResponseToolkit
|
|
49
|
+
) {
|
|
50
|
+
const [day, month, year] =
|
|
51
|
+
request.payload.data.main.dateOfBirth.split('-')
|
|
52
|
+
|
|
53
|
+
const age = calculateAge(day, month, year)
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
calculatedAge: age,
|
|
57
|
+
submissionEvent: 'POST',
|
|
58
|
+
submissionReferenceNumber: request.payload.meta.referenceNumber // example of receiving a payload from DXT
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]
|