@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.
Files changed (34) hide show
  1. package/.public/javascripts/application.min.js +1 -1
  2. package/.public/javascripts/application.min.js.map +1 -1
  3. package/.public/javascripts/shared.min.js +1 -1
  4. package/.public/javascripts/shared.min.js.map +1 -1
  5. package/.public/stylesheets/application.min.css +2 -2
  6. package/.public/stylesheets/application.min.css.map +1 -1
  7. package/.server/client/javascripts/file-upload.js +3 -3
  8. package/.server/client/javascripts/file-upload.js.map +1 -1
  9. package/.server/server/forms/page-events.yaml +87 -0
  10. package/.server/server/index.js +2 -1
  11. package/.server/server/index.js.map +1 -1
  12. package/.server/server/plugins/engine/routes/questions.d.ts +4 -2
  13. package/.server/server/plugins/engine/routes/questions.js +67 -43
  14. package/.server/server/plugins/engine/routes/questions.js.map +1 -1
  15. package/.server/server/plugins/engine/services/localFormsService.js +7 -9
  16. package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
  17. package/.server/server/routes/dummy-api.d.ts +38 -0
  18. package/.server/server/routes/dummy-api.js +33 -0
  19. package/.server/server/routes/dummy-api.js.map +1 -0
  20. package/.server/server/routes/index.d.ts +1 -0
  21. package/.server/server/routes/index.js +1 -0
  22. package/.server/server/routes/index.js.map +1 -1
  23. package/package.json +12 -10
  24. package/src/client/javascripts/file-upload.js +3 -3
  25. package/src/server/forms/page-events.yaml +87 -0
  26. package/src/server/index.ts +4 -2
  27. package/src/server/plugins/engine/routes/questions.test.ts +416 -0
  28. package/src/server/plugins/engine/routes/questions.ts +96 -40
  29. package/src/server/plugins/engine/services/localFormsService.js +7 -8
  30. package/src/server/routes/dummy-api.test.ts +96 -0
  31. package/src/server/routes/dummy-api.ts +62 -0
  32. package/src/server/routes/index.ts +1 -0
  33. package/.server/server/forms/register-as-a-unicorn-breeder.json +0 -393
  34. 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 { SummaryViewModel } from '~/src/server/plugins/engine/models/index.js'
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 { type PreparePageEventRequestOptions } from '~/src/server/plugins/engine/types.js'
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 makeGetHandler(
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
- const { options } = events.onLoad
63
- const { url } = options
64
-
65
- // TODO: Update structured data POST payload with when helper
66
- // is updated to removing the dependency on `SummaryViewModel` etc.
67
- const viewModel = new SummaryViewModel(request, page, context)
68
- const items = getFormSubmissionData(
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 postHandler(
92
- request: FormRequestPayload,
93
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
114
+ export function makePostHandler(
115
+ preparePageEventRequestOptions?: PreparePageEventRequestOptions
94
116
  ) {
95
- const { query } = request
117
+ return function postHandler(
118
+ request: FormRequestPayload,
119
+ h: Pick<ResponseToolkit, 'redirect' | 'view'>
120
+ ) {
121
+ const { query } = request
96
122
 
97
- return redirectOrMakeHandler(request, h, (page, context) => {
98
- const { pageDef } = page
99
- const { isForceAccess } = context
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
- // Redirect to GET for preview URL direct access
102
- if (isForceAccess && !hasFormComponents(pageDef)) {
103
- return proceed(request, h, redirectPath(page.href, query))
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
- return page.makePostRouteHandler()(request, context, h)
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: postHandler,
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: postHandler,
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 Json form
33
- await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.json', {
32
+ // Add a Yaml form
33
+ await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.yaml', {
34
34
  ...metadata,
35
- id: '95e92559-968d-44ae-8666-2b1ad3dffd31',
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
- // Add a Yaml form
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: '641aeafd-13dd-40fa-9186-001703800efb',
44
- title: 'Register as a unicorn breeder (yaml)',
45
- slug: 'register-as-a-unicorn-breeder-yaml' // if we needed to validate any JSON logic, make it available for convenience
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
+ ]
@@ -1 +1,2 @@
1
1
  export { default as publicRoutes } from '~/src/server/routes/public.js'
2
+ export { default as dummyApiRoutes } from '~/src/server/routes/dummy-api.js'