@defra/forms-engine-plugin 1.0.1 → 1.0.3

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 (47) hide show
  1. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +2 -1
  2. package/.server/server/plugins/engine/configureEnginePlugin.js +1 -1
  3. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  4. package/.server/server/plugins/engine/options.d.ts +7 -0
  5. package/.server/server/plugins/engine/options.js +36 -0
  6. package/.server/server/plugins/engine/options.js.map +1 -0
  7. package/.server/server/plugins/engine/options.test.js +33 -0
  8. package/.server/server/plugins/engine/options.test.js.map +1 -0
  9. package/.server/server/plugins/engine/plugin.d.ts +2 -30
  10. package/.server/server/plugins/engine/plugin.js +12 -596
  11. package/.server/server/plugins/engine/plugin.js.map +1 -1
  12. package/.server/server/plugins/engine/registrationOptions.d.ts +1 -0
  13. package/.server/server/plugins/engine/registrationOptions.js +2 -0
  14. package/.server/server/plugins/engine/registrationOptions.js.map +1 -0
  15. package/.server/server/plugins/engine/routes/file-upload.d.ts +4 -0
  16. package/.server/server/plugins/engine/routes/file-upload.js +41 -0
  17. package/.server/server/plugins/engine/routes/file-upload.js.map +1 -0
  18. package/.server/server/plugins/engine/routes/index.d.ts +7 -0
  19. package/.server/server/plugins/engine/routes/index.js +141 -0
  20. package/.server/server/plugins/engine/routes/index.js.map +1 -0
  21. package/.server/server/plugins/engine/routes/questions.d.ts +3 -0
  22. package/.server/server/plugins/engine/routes/questions.js +168 -0
  23. package/.server/server/plugins/engine/routes/questions.js.map +1 -0
  24. package/.server/server/plugins/engine/routes/repeaters/item-delete.d.ts +3 -0
  25. package/.server/server/plugins/engine/routes/repeaters/item-delete.js +106 -0
  26. package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -0
  27. package/.server/server/plugins/engine/routes/repeaters/summary.d.ts +3 -0
  28. package/.server/server/plugins/engine/routes/repeaters/summary.js +98 -0
  29. package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -0
  30. package/.server/server/plugins/engine/types.d.ts +19 -1
  31. package/.server/server/plugins/engine/types.js.map +1 -1
  32. package/.server/server/plugins/engine/vision.d.ts +12 -0
  33. package/.server/server/plugins/engine/vision.js +55 -0
  34. package/.server/server/plugins/engine/vision.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/server/plugins/engine/configureEnginePlugin.ts +3 -5
  37. package/src/server/plugins/engine/options.js +37 -0
  38. package/src/server/plugins/engine/options.test.js +34 -0
  39. package/src/server/plugins/engine/plugin.ts +30 -772
  40. package/src/server/plugins/engine/registrationOptions.ts +0 -0
  41. package/src/server/plugins/engine/routes/file-upload.ts +54 -0
  42. package/src/server/plugins/engine/routes/index.ts +187 -0
  43. package/src/server/plugins/engine/routes/questions.ts +208 -0
  44. package/src/server/plugins/engine/routes/repeaters/item-delete.ts +157 -0
  45. package/src/server/plugins/engine/routes/repeaters/summary.ts +137 -0
  46. package/src/server/plugins/engine/types.ts +26 -1
  47. package/src/server/plugins/engine/vision.ts +95 -0
@@ -1,132 +1,41 @@
1
- import { existsSync } from 'fs'
2
- import { dirname, join } from 'path'
3
- import { fileURLToPath } from 'url'
4
-
5
- import {
6
- getErrorMessage,
7
- hasFormComponents,
8
- slugSchema
9
- } from '@defra/forms-model'
10
- import Boom from '@hapi/boom'
11
1
  import {
2
+ type Lifecycle,
12
3
  type Plugin,
13
- type PluginProperties,
14
- type Request,
15
- type ResponseObject,
16
- type ResponseToolkit,
17
4
  type RouteOptions,
18
- type Server
5
+ type Server,
6
+ type ServerRoute
19
7
  } from '@hapi/hapi'
20
- import vision from '@hapi/vision'
21
- import { isEqual } from 'date-fns'
22
- import Joi from 'joi'
23
- import nunjucks, { type Environment } from 'nunjucks'
24
- import resolvePkg from 'resolve'
25
8
 
26
- import { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'
27
- import {
28
- checkEmailAddressForLiveFormSubmission,
29
- checkFormStatus,
30
- findPage,
31
- getCacheService,
32
- getPage,
33
- getStartPath,
34
- normalisePath,
35
- proceed,
36
- redirectPath
37
- } from '~/src/server/plugins/engine/helpers.js'
38
- import {
39
- VIEW_PATH,
40
- context,
41
- prepareNunjucksEnvironment
42
- } from '~/src/server/plugins/engine/index.js'
9
+ import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
10
+ import { validatePluginOptions } from '~/src/server/plugins/engine/options.js'
11
+ import { getRoutes as getFileUploadStatusRoutes } from '~/src/server/plugins/engine/routes/file-upload.js'
12
+ import { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js'
13
+ import { getRoutes as getQuestionRoutes } from '~/src/server/plugins/engine/routes/questions.js'
14
+ import { getRoutes as getRepeaterItemDeleteRoutes } from '~/src/server/plugins/engine/routes/repeaters/item-delete.js'
15
+ import { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engine/routes/repeaters/summary.js'
16
+ import { type PluginOptions } from '~/src/server/plugins/engine/types.js'
17
+ import { registerVision } from '~/src/server/plugins/engine/vision.js'
43
18
  import {
44
- FormModel,
45
- SummaryViewModel
46
- } from '~/src/server/plugins/engine/models/index.js'
47
- import { format } from '~/src/server/plugins/engine/outputFormatters/machine/v1.js'
48
- import { FileUploadPageController } from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'
49
- import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
50
- import { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'
51
- import { getFormSubmissionData } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
52
- import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'
53
- import { generateUniqueReference } from '~/src/server/plugins/engine/referenceNumbers.js'
54
- import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
55
- import { getUploadStatus } from '~/src/server/plugins/engine/services/uploadService.js'
56
- import {
57
- type FilterFunction,
58
- type FormContext,
59
- type FormSubmissionState
60
- } from '~/src/server/plugins/engine/types.js'
61
- import {
62
- type FormRequest,
63
- type FormRequestPayload,
64
19
  type FormRequestPayloadRefs,
65
20
  type FormRequestRefs
66
21
  } from '~/src/server/routes/types.js'
67
- import {
68
- actionSchema,
69
- confirmSchema,
70
- crumbSchema,
71
- itemIdSchema,
72
- pathSchema,
73
- stateSchema
74
- } from '~/src/server/schemas/index.js'
75
- import * as httpService from '~/src/server/services/httpService.js'
76
22
  import { CacheService } from '~/src/server/services/index.js'
77
- import { type Services } from '~/src/server/types.js'
78
-
79
- export function findPackageRoot() {
80
- const currentFileName = fileURLToPath(import.meta.url)
81
- const currentDirectoryName = dirname(currentFileName)
82
-
83
- let dir = currentDirectoryName
84
- while (dir !== '/') {
85
- if (existsSync(join(dir, 'package.json'))) {
86
- return dir
87
- }
88
- dir = dirname(dir)
89
- }
90
-
91
- throw new Error('package.json not found in parent directories')
92
- }
93
- export interface PluginOptions {
94
- model?: FormModel
95
- services?: Services
96
- controllers?: Record<string, typeof PageController>
97
- cacheName?: string
98
- keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string
99
- sessionHydrator?: (
100
- request: Request | FormRequest | FormRequestPayload
101
- ) => Promise<FormSubmissionState>
102
- filters?: Record<string, FilterFunction>
103
- pluginPath?: string
104
- nunjucks: {
105
- baseLayoutPath: string
106
- paths: string[]
107
- }
108
- viewContext: PluginProperties['forms-engine-plugin']['viewContext']
109
- }
110
23
 
111
24
  export const plugin = {
112
25
  name: '@defra/forms-engine-plugin',
113
26
  dependencies: ['@hapi/crumb', '@hapi/yar', 'hapi-pino'],
114
27
  multiple: true,
115
28
  async register(server: Server, options: PluginOptions) {
116
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- hapi types are wrong
117
- const prefix = server.realm.modifiers.route.prefix ?? ''
29
+ options = validatePluginOptions(options)
30
+
118
31
  const {
119
32
  model,
120
- services = defaultServices,
121
- controllers,
122
33
  cacheName,
123
34
  keyGenerator,
124
35
  sessionHydrator,
125
- filters,
126
36
  nunjucks: nunjucksOptions,
127
37
  viewContext
128
38
  } = options
129
- const { formsService } = services
130
39
  const cacheService = new CacheService({
131
40
  server,
132
41
  cacheName,
@@ -136,60 +45,7 @@ export const plugin = {
136
45
  }
137
46
  })
138
47
 
139
- const packageRoot = findPackageRoot()
140
- const govukFrontendPath = dirname(
141
- resolvePkg.sync('govuk-frontend/package.json')
142
- )
143
-
144
- const viewPathResolved = join(packageRoot, VIEW_PATH)
145
-
146
- const paths = [
147
- ...nunjucksOptions.paths,
148
- viewPathResolved,
149
- join(govukFrontendPath, 'dist')
150
- ]
151
-
152
- await server.register({
153
- plugin: vision,
154
- options: {
155
- engines: {
156
- html: {
157
- compile: (
158
- path: string,
159
- compileOptions: { environment: Environment }
160
- ) => {
161
- const template = nunjucks.compile(
162
- path,
163
- compileOptions.environment
164
- )
165
-
166
- return (context: object | undefined) => {
167
- return template.render(context)
168
- }
169
- },
170
- prepare: (
171
- options: EngineConfigurationObject,
172
- next: (err?: Error) => void
173
- ) => {
174
- // Nunjucks also needs an additional path configuration
175
- // to use the templates and macros from `govuk-frontend`
176
- const environment = nunjucks.configure(paths)
177
-
178
- // Applies custom filters and globals for nunjucks
179
- // that are required by the `forms-engine-plugin`
180
- prepareNunjucksEnvironment(environment, filters)
181
-
182
- options.compileOptions.environment = environment
183
-
184
- next()
185
- }
186
- }
187
- },
188
- path: paths,
189
- // Provides global context used with all templates
190
- context
191
- }
192
- })
48
+ await registerVision(server, options)
193
49
 
194
50
  server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)
195
51
  server.expose('viewContext', viewContext)
@@ -202,634 +58,36 @@ export const plugin = {
202
58
  const itemCache = new Map<string, { model: FormModel; updatedAt: Date }>()
203
59
  server.app.models = itemCache
204
60
 
205
- const loadFormPreHandler = async (
206
- request: FormRequest | FormRequestPayload,
207
- h: Pick<ResponseToolkit, 'continue'>
208
- ) => {
209
- if (server.app.model) {
210
- request.app.model = server.app.model
211
-
212
- return h.continue
213
- }
214
-
215
- const { params } = request
216
- const { slug } = params
217
- const { isPreview, state: formState } = checkFormStatus(params)
218
-
219
- // Get the form metadata using the `slug` param
220
- const metadata = await formsService.getFormMetadata(slug)
221
-
222
- const { id, [formState]: state } = metadata
223
-
224
- // Check the metadata supports the requested state
225
- if (!state) {
226
- throw Boom.notFound(`No '${formState}' state for form metadata ${id}`)
227
- }
228
-
229
- // Cache the models based on id, state and whether
230
- // it's a preview or not. There could be up to 3 models
231
- // cached for a single form:
232
- // "{id}_live_false" (live/live)
233
- // "{id}_live_true" (live/preview)
234
- // "{id}_draft_true" (draft/preview)
235
- const key = `${id}_${formState}_${isPreview}`
236
- let item = itemCache.get(key)
237
-
238
- if (!item || !isEqual(item.updatedAt, state.updatedAt)) {
239
- server.logger.info(
240
- `Getting form definition ${id} (${slug}) ${formState}`
241
- )
242
-
243
- // Get the form definition using the `id` from the metadata
244
- const definition = await formsService.getFormDefinition(id, formState)
245
-
246
- if (!definition) {
247
- throw Boom.notFound(
248
- `No definition found for form metadata ${id} (${slug}) ${formState}`
249
- )
250
- }
251
-
252
- const emailAddress =
253
- metadata.notificationEmail ?? definition.outputEmail
254
-
255
- checkEmailAddressForLiveFormSubmission(emailAddress, isPreview)
256
-
257
- // Build the form model
258
- server.logger.info(
259
- `Building model for form definition ${id} (${slug}) ${formState}`
260
- )
261
-
262
- // Set up the basePath for the model
263
- const basePath = (
264
- isPreview
265
- ? `${prefix}${PREVIEW_PATH_PREFIX}/${formState}/${slug}`
266
- : `${prefix}/${slug}`
267
- ).substring(1)
268
-
269
- // Construct the form model
270
- const model = new FormModel(
271
- definition,
272
- { basePath },
273
- services,
274
- controllers
275
- )
276
-
277
- // Create new item and add it to the item cache
278
- item = { model, updatedAt: state.updatedAt }
279
- itemCache.set(key, item)
280
- }
281
-
282
- // Assign the model to the request data
283
- // for use in the downstream handler
284
- request.app.model = item.model
285
-
286
- return h.continue
287
- }
288
-
289
- const dispatchHandler = (
290
- request: FormRequest,
291
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
292
- ) => {
293
- const { model } = request.app
294
-
295
- const servicePath = model ? `/${model.basePath}` : ''
296
- return proceed(request, h, `${servicePath}${getStartPath(model)}`)
297
- }
298
-
299
- const redirectOrMakeHandler = async (
300
- request: FormRequest | FormRequestPayload,
301
- h: Pick<ResponseToolkit, 'redirect' | 'view'>,
302
- makeHandler: (
303
- page: PageControllerClass,
304
- context: FormContext
305
- ) => ResponseObject | Promise<ResponseObject>
306
- ) => {
307
- const { app, params } = request
308
- const { model } = app
309
-
310
- if (!model) {
311
- throw Boom.notFound(`No model found for /${params.path}`)
312
- }
313
-
314
- const cacheService = getCacheService(request.server)
315
- const page = getPage(model, request)
316
- let state = await page.getState(request)
317
-
318
- if (!state.$$__referenceNumber) {
319
- const prefix = model.def.metadata?.referenceNumberPrefix ?? ''
320
-
321
- if (typeof prefix !== 'string') {
322
- throw Boom.badImplementation(
323
- 'Reference number prefix must be a string or undefined'
324
- )
325
- }
326
-
327
- const referenceNumber = generateUniqueReference(prefix)
328
- state = await page.mergeState(request, state, {
329
- $$__referenceNumber: referenceNumber
330
- })
331
- }
332
-
333
- const flash = cacheService.getFlash(request)
334
- const context = model.getFormContext(request, state, flash?.errors)
335
- const relevantPath = page.getRelevantPath(request, context)
336
- const summaryPath = page.getSummaryPath()
337
-
338
- // Return handler for relevant pages or preview URL direct access
339
- if (relevantPath.startsWith(page.path) || context.isForceAccess) {
340
- return makeHandler(page, context)
341
- }
342
-
343
- // Redirect back to last relevant page
344
- const redirectTo = findPage(model, relevantPath)
345
-
346
- // Set the return URL unless an exit page
347
- if (redirectTo?.next.length) {
348
- request.query.returnUrl = page.getHref(summaryPath)
349
- }
350
-
351
- return proceed(request, h, page.getHref(relevantPath))
352
- }
353
-
354
- const getHandler = (
355
- request: FormRequest,
356
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
357
- ) => {
358
- const { params } = request
359
-
360
- if (normalisePath(params.path) === '') {
361
- return dispatchHandler(request, h)
362
- }
363
-
364
- return redirectOrMakeHandler(request, h, async (page, context) => {
365
- // Check for a page onLoad HTTP event and if one exists,
366
- // call it and assign the response to the context data
367
- const { events } = page
368
- const { model } = request.app
369
-
370
- if (!model) {
371
- throw Boom.notFound(`No model found for /${params.path}`)
372
- }
373
-
374
- if (events?.onLoad && events.onLoad.type === 'http') {
375
- const { options } = events.onLoad
376
- const { url } = options
377
-
378
- // TODO: Update structured data POST payload with when helper
379
- // is updated to removing the dependency on `SummaryViewModel` etc.
380
- const viewModel = new SummaryViewModel(request, page, context)
381
- const items = getFormSubmissionData(
382
- viewModel.context,
383
- viewModel.details
384
- )
385
-
386
- // @ts-expect-error - function signature will be refactored in the next iteration of the formatter
387
- const payload = format(items, model, undefined, undefined)
388
-
389
- const { payload: response } = await httpService.postJson(url, {
390
- payload
391
- })
392
-
393
- Object.assign(context.data, response)
394
- }
395
-
396
- return page.makeGetRouteHandler()(request, context, h)
397
- })
398
- }
399
-
400
- const postHandler = (
401
- request: FormRequestPayload,
402
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
403
- ) => {
404
- const { query } = request
405
-
406
- return redirectOrMakeHandler(request, h, (page, context) => {
407
- const { pageDef } = page
408
- const { isForceAccess } = context
409
-
410
- // Redirect to GET for preview URL direct access
411
- if (isForceAccess && !hasFormComponents(pageDef)) {
412
- return proceed(request, h, redirectPath(page.href, query))
413
- }
414
-
415
- return page.makePostRouteHandler()(request, context, h)
416
- })
417
- }
418
-
419
- const dispatchRouteOptions: RouteOptions<FormRequestRefs> = {
420
- pre: [
421
- {
422
- method: loadFormPreHandler
423
- }
424
- ]
425
- }
426
-
427
- server.route({
428
- method: 'get',
429
- path: '/{slug}',
430
- handler: dispatchHandler,
431
- options: {
432
- ...dispatchRouteOptions,
433
- validate: {
434
- params: Joi.object().keys({
435
- slug: slugSchema
436
- })
437
- }
438
- }
439
- })
440
-
441
- server.route({
442
- method: 'get',
443
- path: '/preview/{state}/{slug}',
444
- handler: dispatchHandler,
445
- options: {
446
- ...dispatchRouteOptions,
447
- validate: {
448
- params: Joi.object().keys({
449
- state: stateSchema,
450
- slug: slugSchema
451
- })
452
- }
453
- }
454
- })
61
+ const loadFormPreHandler = makeLoadFormPreHandler(server, options)
455
62
 
456
63
  const getRouteOptions: RouteOptions<FormRequestRefs> = {
457
64
  pre: [
458
65
  {
459
- method: loadFormPreHandler
66
+ method:
67
+ loadFormPreHandler as unknown as Lifecycle.Method<FormRequestRefs>
460
68
  }
461
69
  ]
462
70
  }
463
71
 
464
- server.route({
465
- method: 'get',
466
- path: '/{slug}/{path}/{itemId?}',
467
- handler: getHandler,
468
- options: {
469
- ...getRouteOptions,
470
- validate: {
471
- params: Joi.object().keys({
472
- slug: slugSchema,
473
- path: pathSchema,
474
- itemId: itemIdSchema.optional()
475
- })
476
- }
477
- }
478
- })
479
-
480
- server.route({
481
- method: 'get',
482
- path: '/preview/{state}/{slug}/{path}/{itemId?}',
483
- handler: getHandler,
484
- options: {
485
- ...getRouteOptions,
486
- validate: {
487
- params: Joi.object().keys({
488
- state: stateSchema,
489
- slug: slugSchema,
490
- path: pathSchema,
491
- itemId: itemIdSchema.optional()
492
- })
493
- }
494
- }
495
- })
496
-
497
72
  const postRouteOptions: RouteOptions<FormRequestPayloadRefs> = {
498
73
  payload: {
499
74
  parse: true
500
75
  },
501
- pre: [{ method: loadFormPreHandler }]
502
- }
503
-
504
- server.route({
505
- method: 'post',
506
- path: '/{slug}/{path}/{itemId?}',
507
- handler: postHandler,
508
- options: {
509
- ...postRouteOptions,
510
- validate: {
511
- params: Joi.object().keys({
512
- slug: slugSchema,
513
- path: pathSchema,
514
- itemId: itemIdSchema.optional()
515
- }),
516
- payload: Joi.object()
517
- .keys({
518
- crumb: crumbSchema,
519
- action: actionSchema
520
- })
521
- .unknown(true)
522
- .required()
523
- }
524
- }
525
- })
526
-
527
- server.route({
528
- method: 'post',
529
- path: '/preview/{state}/{slug}/{path}/{itemId?}',
530
- handler: postHandler,
531
- options: {
532
- ...postRouteOptions,
533
- validate: {
534
- params: Joi.object().keys({
535
- state: stateSchema,
536
- slug: slugSchema,
537
- path: pathSchema,
538
- itemId: itemIdSchema.optional()
539
- }),
540
- payload: Joi.object()
541
- .keys({
542
- crumb: crumbSchema,
543
- action: actionSchema
544
- })
545
- .unknown(true)
546
- .required()
547
- }
548
- }
549
- })
550
-
551
- /**
552
- * "AddAnother" repeat routes
553
- */
554
-
555
- // List summary GET route
556
- const getListSummaryHandler = (
557
- request: FormRequest,
558
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
559
- ) => {
560
- const { params } = request
561
-
562
- return redirectOrMakeHandler(request, h, (page, context) => {
563
- if (!(page instanceof RepeatPageController)) {
564
- throw Boom.notFound(`No repeater page found for /${params.path}`)
565
- }
566
-
567
- return page.makeGetListSummaryRouteHandler()(request, context, h)
568
- })
569
- }
570
-
571
- server.route({
572
- method: 'get',
573
- path: '/{slug}/{path}/summary',
574
- handler: getListSummaryHandler,
575
- options: {
576
- ...getRouteOptions,
577
- validate: {
578
- params: Joi.object().keys({
579
- slug: slugSchema,
580
- path: pathSchema
581
- })
582
- }
583
- }
584
- })
585
-
586
- server.route({
587
- method: 'get',
588
- path: '/preview/{state}/{slug}/{path}/summary',
589
- handler: getListSummaryHandler,
590
- options: {
591
- ...getRouteOptions,
592
- validate: {
593
- params: Joi.object().keys({
594
- state: stateSchema,
595
- slug: slugSchema,
596
- path: pathSchema
597
- })
598
- }
599
- }
600
- })
601
-
602
- // List summary POST route
603
- const postListSummaryHandler = (
604
- request: FormRequestPayload,
605
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
606
- ) => {
607
- const { params } = request
608
-
609
- return redirectOrMakeHandler(request, h, (page, context) => {
610
- const { isForceAccess } = context
611
-
612
- if (isForceAccess || !(page instanceof RepeatPageController)) {
613
- throw Boom.notFound(`No repeater page found for /${params.path}`)
614
- }
615
-
616
- return page.makePostListSummaryRouteHandler()(request, context, h)
617
- })
618
- }
619
-
620
- server.route({
621
- method: 'post',
622
- path: '/{slug}/{path}/summary',
623
- handler: postListSummaryHandler,
624
- options: {
625
- ...postRouteOptions,
626
- validate: {
627
- params: Joi.object().keys({
628
- slug: slugSchema,
629
- path: pathSchema
630
- }),
631
- payload: Joi.object()
632
- .keys({
633
- crumb: crumbSchema,
634
- action: actionSchema
635
- })
636
- .required()
637
- }
638
- }
639
- })
640
-
641
- server.route({
642
- method: 'post',
643
- path: '/preview/{state}/{slug}/{path}/summary',
644
- handler: postListSummaryHandler,
645
- options: {
646
- ...postRouteOptions,
647
- validate: {
648
- params: Joi.object().keys({
649
- state: stateSchema,
650
- slug: slugSchema,
651
- path: pathSchema
652
- }),
653
- payload: Joi.object()
654
- .keys({
655
- crumb: crumbSchema,
656
- action: actionSchema
657
- })
658
- .required()
659
- }
660
- }
661
- })
662
-
663
- // Item delete GET route
664
- const getItemDeleteHandler = (
665
- request: FormRequest,
666
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
667
- ) => {
668
- const { params } = request
669
-
670
- return redirectOrMakeHandler(request, h, (page, context) => {
671
- if (
672
- !(
673
- page instanceof RepeatPageController ||
674
- page instanceof FileUploadPageController
675
- )
676
- ) {
677
- throw Boom.notFound(`No page found for /${params.path}`)
678
- }
679
-
680
- return page.makeGetItemDeleteRouteHandler()(request, context, h)
681
- })
682
- }
683
-
684
- server.route({
685
- method: 'get',
686
- path: '/{slug}/{path}/{itemId}/confirm-delete',
687
- handler: getItemDeleteHandler,
688
- options: {
689
- ...getRouteOptions,
690
- validate: {
691
- params: Joi.object().keys({
692
- slug: slugSchema,
693
- path: pathSchema,
694
- itemId: itemIdSchema
695
- })
696
- }
697
- }
698
- })
699
-
700
- server.route({
701
- method: 'get',
702
- path: '/preview/{state}/{slug}/{path}/{itemId}/confirm-delete',
703
- handler: getItemDeleteHandler,
704
- options: {
705
- ...getRouteOptions,
706
- validate: {
707
- params: Joi.object().keys({
708
- state: stateSchema,
709
- slug: slugSchema,
710
- path: pathSchema,
711
- itemId: itemIdSchema
712
- })
713
- }
714
- }
715
- })
716
-
717
- // Item delete POST route
718
- const postItemDeleteHandler = (
719
- request: FormRequestPayload,
720
- h: Pick<ResponseToolkit, 'redirect' | 'view'>
721
- ) => {
722
- const { params } = request
723
-
724
- return redirectOrMakeHandler(request, h, (page, context) => {
725
- const { isForceAccess } = context
726
-
727
- if (
728
- isForceAccess ||
729
- !(
730
- page instanceof RepeatPageController ||
731
- page instanceof FileUploadPageController
732
- )
733
- ) {
734
- throw Boom.notFound(`No page found for /${params.path}`)
76
+ pre: [
77
+ {
78
+ method:
79
+ loadFormPreHandler as unknown as Lifecycle.Method<FormRequestPayloadRefs>
735
80
  }
736
-
737
- return page.makePostItemDeleteRouteHandler()(request, context, h)
738
- })
81
+ ]
739
82
  }
740
83
 
741
- server.route({
742
- method: 'post',
743
- path: '/{slug}/{path}/{itemId}/confirm-delete',
744
- handler: postItemDeleteHandler,
745
- options: {
746
- ...postRouteOptions,
747
- validate: {
748
- params: Joi.object().keys({
749
- slug: slugSchema,
750
- path: pathSchema,
751
- itemId: itemIdSchema
752
- }),
753
- payload: Joi.object()
754
- .keys({
755
- crumb: crumbSchema,
756
- action: actionSchema,
757
- confirm: confirmSchema
758
- })
759
- .required()
760
- }
761
- }
762
- })
763
-
764
- server.route({
765
- method: 'post',
766
- path: '/preview/{state}/{slug}/{path}/{itemId}/confirm-delete',
767
- handler: postItemDeleteHandler,
768
- options: {
769
- ...postRouteOptions,
770
- validate: {
771
- params: Joi.object().keys({
772
- state: stateSchema,
773
- slug: slugSchema,
774
- path: pathSchema,
775
- itemId: itemIdSchema
776
- }),
777
- payload: Joi.object()
778
- .keys({
779
- crumb: crumbSchema,
780
- action: actionSchema,
781
- confirm: confirmSchema
782
- })
783
- .required()
784
- }
785
- }
786
- })
787
-
788
- server.route({
789
- method: 'get',
790
- path: '/upload-status/{uploadId}',
791
- handler: async (
792
- request: FormRequest,
793
- h: Pick<ResponseToolkit, 'response'>
794
- ) => {
795
- const { uploadId } = request.params as unknown as {
796
- uploadId: string
797
- }
798
- try {
799
- const status = await getUploadStatus(uploadId)
800
-
801
- if (!status) {
802
- return h.response({ error: 'Status check failed' }).code(400)
803
- }
84
+ const routes = [
85
+ ...getQuestionRoutes(getRouteOptions, postRouteOptions),
86
+ ...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions),
87
+ ...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions),
88
+ ...getFileUploadStatusRoutes()
89
+ ]
804
90
 
805
- return h.response(status)
806
- } catch (error) {
807
- const errMsg = getErrorMessage(error)
808
- request.logger.error(
809
- errMsg,
810
- `[uploadStatusFailed] Upload status check failed for uploadId: ${uploadId} - ${errMsg}`
811
- )
812
- return h.response({ error: 'Status check error' }).code(500)
813
- }
814
- },
815
- options: {
816
- plugins: {
817
- crumb: false
818
- },
819
- validate: {
820
- params: Joi.object().keys({
821
- uploadId: Joi.string().guid().required()
822
- })
823
- }
824
- }
825
- })
91
+ server.route(routes as unknown as ServerRoute[]) // TODO
826
92
  }
827
93
  } satisfies Plugin<PluginOptions>
828
-
829
- interface CompileOptions {
830
- environment: Environment
831
- }
832
-
833
- export interface EngineConfigurationObject {
834
- compileOptions: CompileOptions
835
- }