@defra/forms-engine-plugin 4.11.2 → 4.12.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.
Files changed (30) hide show
  1. package/.public/javascripts/shared.min.js +1 -1
  2. package/.public/javascripts/shared.min.js.map +1 -1
  3. package/.server/client/javascripts/location-map.js +1 -1
  4. package/.server/client/javascripts/location-map.js.map +1 -1
  5. package/.server/server/plugins/engine/beta/form-context.js +5 -4
  6. package/.server/server/plugins/engine/beta/form-context.js.map +1 -1
  7. package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -1
  8. package/.server/server/plugins/engine/form-availability.d.ts +16 -0
  9. package/.server/server/plugins/engine/form-availability.js +26 -0
  10. package/.server/server/plugins/engine/form-availability.js.map +1 -0
  11. package/.server/server/plugins/engine/models/unavailable-view-model.d.ts +8 -0
  12. package/.server/server/plugins/engine/models/unavailable-view-model.js +21 -0
  13. package/.server/server/plugins/engine/models/unavailable-view-model.js.map +1 -0
  14. package/.server/server/plugins/engine/plugin.js +6 -0
  15. package/.server/server/plugins/engine/plugin.js.map +1 -1
  16. package/.server/server/plugins/engine/unavailable-response.d.ts +9 -0
  17. package/.server/server/plugins/engine/unavailable-response.js +23 -0
  18. package/.server/server/plugins/engine/unavailable-response.js.map +1 -0
  19. package/.server/server/plugins/engine/views/unavailable.html +20 -0
  20. package/.server/typings/hapi/index.d.js.map +1 -1
  21. package/package.json +4 -4
  22. package/src/client/javascripts/location-map.js +1 -1
  23. package/src/server/plugins/engine/beta/form-context.ts +6 -8
  24. package/src/server/plugins/engine/components/helpers/geospatial.ts +1 -1
  25. package/src/server/plugins/engine/form-availability.ts +31 -0
  26. package/src/server/plugins/engine/models/unavailable-view-model.ts +36 -0
  27. package/src/server/plugins/engine/plugin.ts +6 -0
  28. package/src/server/plugins/engine/unavailable-response.ts +29 -0
  29. package/src/server/plugins/engine/views/unavailable.html +20 -0
  30. package/src/typings/hapi/index.d.ts +1 -1
@@ -3,6 +3,7 @@ import { type Request, type Server } from '@hapi/hapi'
3
3
  import { isEqual } from 'date-fns'
4
4
 
5
5
  import { PREVIEW_PATH_PREFIX } from '../../../constants.js'
6
+ import { assertFormAvailable } from '../form-availability.js'
6
7
  import {
7
8
  checkEmailAddressForLiveFormSubmission,
8
9
  getCacheService
@@ -52,6 +53,7 @@ export async function getFormModel(
52
53
  const formState = resolveState(state)
53
54
 
54
55
  const metadata = await formsService.getFormMetadata(slug)
56
+ assertFormAvailable(metadata)
55
57
 
56
58
  const definition = await formsService.getFormDefinition(
57
59
  metadata.id,
@@ -134,6 +136,7 @@ export async function resolveFormModel(
134
136
  const { formsService } = services
135
137
 
136
138
  const metadata = await formsService.getFormMetadata(slug)
139
+ assertFormAvailable(metadata)
137
140
  const formState = resolveState(state)
138
141
  const isPreview = options.isPreview ?? isPreviewState(state, options)
139
142
  const stateMetadata = metadata[formState]
@@ -145,15 +148,10 @@ export async function resolveFormModel(
145
148
  }
146
149
 
147
150
  // The models cache is created lazily per server instance
148
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
149
- if (!server.app.models) {
150
- server.app.models = new Map<string, { model: FormModel; updatedAt: Date }>()
151
- }
152
151
 
153
- const cache = server.app.models as Map<
154
- string,
155
- { model: FormModel; updatedAt: Date }
156
- >
152
+ server.app.models ??= new Map<string, { model: FormModel; updatedAt: Date }>()
153
+
154
+ const cache = server.app.models
157
155
 
158
156
  const cacheKey = `${metadata.id}_${formState}_${isPreview}`
159
157
  let entry = cache.get(cacheKey)
@@ -116,7 +116,7 @@ export function getGeospatialSchema(country?: GeospatialFieldOptionsCountry) {
116
116
  return value
117
117
  }
118
118
 
119
- const result = booleanWithin(value, countryFeature)
119
+ const result = booleanWithin(value as Geometry | Feature, countryFeature)
120
120
 
121
121
  if (!result) {
122
122
  return helpers.error('any.custom', {
@@ -0,0 +1,31 @@
1
+ import { type FormMetadata } from '@defra/forms-model'
2
+ import Boom from '@hapi/boom'
3
+
4
+ export interface OfflineBoomData {
5
+ offline: true
6
+ metadata: FormMetadata
7
+ }
8
+
9
+ /**
10
+ * Throws when the form has been taken offline. The plugin's
11
+ * unavailable-response extension catches the marker and renders the
12
+ * "Sorry, this form is unavailable" view at HTTP 200.
13
+ */
14
+ export function assertFormAvailable(metadata: FormMetadata): void {
15
+ if (metadata.offline === true) {
16
+ const data: OfflineBoomData = { offline: true, metadata }
17
+ throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), {
18
+ statusCode: 503,
19
+ data
20
+ })
21
+ }
22
+ }
23
+
24
+ /** Type guard for the offline Boom marker. */
25
+ export function isOfflineBoom(
26
+ err: unknown
27
+ ): err is Boom.Boom<OfflineBoomData> & { data: OfflineBoomData } {
28
+ if (!Boom.isBoom(err)) return false
29
+ const data = err.data as Partial<OfflineBoomData> | null | undefined
30
+ return data?.offline === true && !!data.metadata
31
+ }
@@ -0,0 +1,36 @@
1
+ import { type FormMetadata } from '@defra/forms-model'
2
+
3
+ export interface UnavailableViewModel {
4
+ pageTitle: string
5
+ formTitle: string
6
+ organisationName: string
7
+ phoneLines?: string[]
8
+ }
9
+
10
+ /**
11
+ * Defra organisations carry an abbreviation suffix on the enum value, e.g.
12
+ * "Rural Payments Agency – RPA". The unavailable page reads cleanly without it.
13
+ */
14
+ function stripOrgSuffix(organisation: string) {
15
+ return organisation.split(' – ')[0]
16
+ }
17
+
18
+ function splitPhoneLines(phone: string | undefined) {
19
+ if (!phone) return undefined
20
+ const lines = phone
21
+ .split('\n')
22
+ .map((line) => line.trim())
23
+ .filter((line) => line.length > 0)
24
+ return lines.length > 0 ? lines : undefined
25
+ }
26
+
27
+ export function unavailableViewModel(
28
+ metadata: FormMetadata
29
+ ): UnavailableViewModel {
30
+ return {
31
+ pageTitle: 'Sorry, this form is unavailable',
32
+ formTitle: metadata.title,
33
+ organisationName: stripOrgSuffix(metadata.organisation),
34
+ phoneLines: splitPhoneLines(metadata.contact?.phone)
35
+ }
36
+ }
@@ -15,6 +15,7 @@ import { getRoutes as getQuestionRoutes } from './routes/questions.js'
15
15
  import { getRoutes as getRepeaterItemDeleteRoutes } from './routes/repeaters/item-delete.js'
16
16
  import { getRoutes as getRepeaterSummaryRoutes } from './routes/repeaters/summary.js'
17
17
  import { type PluginOptions } from './types.js'
18
+ import { registerUnavailableResponse } from './unavailable-response.js'
18
19
  import { registerVision } from './vision.js'
19
20
  import { mapPlugin } from '../map/index.js'
20
21
  import { postcodeLookupPlugin } from '../postcode-lookup/index.js'
@@ -129,5 +130,10 @@ export const plugin = {
129
130
  ]
130
131
 
131
132
  server.route(routes as unknown as ServerRoute[]) // TODO
133
+
134
+ // Registration order is important: must be registered after the engine's
135
+ // routes so it sees their responses, but before any global error-page
136
+ // handler that would re-shape Boom errors.
137
+ registerUnavailableResponse(server)
132
138
  }
133
139
  } satisfies Plugin<PluginOptions>
@@ -0,0 +1,29 @@
1
+ import { type Request, type ResponseToolkit, type Server } from '@hapi/hapi'
2
+
3
+ import { isOfflineBoom } from './form-availability.js'
4
+ import { unavailableViewModel } from './models/unavailable-view-model.js'
5
+
6
+ /**
7
+ * Registers a server-wide onPreResponse extension that intercepts the offline
8
+ * Boom thrown and renders the unavailable view.
9
+ *
10
+ * Must be registered after the engine's routes so it sees their responses,
11
+ * but before any global error-page handler that would re-shape Boom errors.
12
+ */
13
+ export function registerUnavailableResponse(server: Server) {
14
+ server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
15
+ const response = request.response
16
+ if (!isOfflineBoom(response)) {
17
+ return h.continue
18
+ }
19
+
20
+ const { metadata } = response.data
21
+
22
+ return h
23
+ .view('unavailable', unavailableViewModel(metadata))
24
+ .header('Cache-Control', 'no-store, no-cache, must-revalidate')
25
+ .header('X-Robots-Tag', 'noindex, nofollow')
26
+ .code(200)
27
+ .takeover()
28
+ })
29
+ }
@@ -0,0 +1,20 @@
1
+ {% extends baseLayoutPath %}
2
+
3
+ {% block content %}
4
+ <div class="govuk-grid-row">
5
+ <div class="govuk-grid-column-two-thirds">
6
+ <h1 class="govuk-heading-l">Sorry, this form is unavailable</h1>
7
+ <p class="govuk-body">'{{ formTitle }}' has been archived and is no longer available.</p>
8
+ <p class="govuk-body">Contact the {{ organisationName }}.</p>
9
+
10
+ {% if phoneLines %}
11
+ <ul class="govuk-list govuk-list--bullet">
12
+ {% for line in phoneLines %}
13
+ <li>{{ line }}</li>
14
+ {% endfor %}
15
+ </ul>
16
+ <p class="govuk-body"><a href="https://www.gov.uk/call-charges" class="govuk-link govuk-link--no-visited-state">Find out about call charges</a></p>
17
+ {% endif %}
18
+ </div>
19
+ </div>
20
+ {% endblock %}
@@ -59,7 +59,7 @@ declare module '@hapi/hapi' {
59
59
 
60
60
  interface ServerApplicationState {
61
61
  model?: FormModel
62
- models: Map<string, { model: FormModel; updatedAt: Date }>
62
+ models?: Map<string, { model: FormModel; updatedAt: Date }>
63
63
  }
64
64
  }
65
65