@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.
- package/.server/server/plugins/engine/configureEnginePlugin.d.ts +2 -1
- package/.server/server/plugins/engine/configureEnginePlugin.js +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
- package/.server/server/plugins/engine/options.d.ts +7 -0
- package/.server/server/plugins/engine/options.js +36 -0
- package/.server/server/plugins/engine/options.js.map +1 -0
- package/.server/server/plugins/engine/options.test.js +33 -0
- package/.server/server/plugins/engine/options.test.js.map +1 -0
- package/.server/server/plugins/engine/plugin.d.ts +2 -30
- package/.server/server/plugins/engine/plugin.js +12 -596
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/registrationOptions.d.ts +1 -0
- package/.server/server/plugins/engine/registrationOptions.js +2 -0
- package/.server/server/plugins/engine/registrationOptions.js.map +1 -0
- package/.server/server/plugins/engine/routes/file-upload.d.ts +4 -0
- package/.server/server/plugins/engine/routes/file-upload.js +41 -0
- package/.server/server/plugins/engine/routes/file-upload.js.map +1 -0
- package/.server/server/plugins/engine/routes/index.d.ts +7 -0
- package/.server/server/plugins/engine/routes/index.js +141 -0
- package/.server/server/plugins/engine/routes/index.js.map +1 -0
- package/.server/server/plugins/engine/routes/questions.d.ts +3 -0
- package/.server/server/plugins/engine/routes/questions.js +168 -0
- package/.server/server/plugins/engine/routes/questions.js.map +1 -0
- package/.server/server/plugins/engine/routes/repeaters/item-delete.d.ts +3 -0
- package/.server/server/plugins/engine/routes/repeaters/item-delete.js +106 -0
- package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -0
- package/.server/server/plugins/engine/routes/repeaters/summary.d.ts +3 -0
- package/.server/server/plugins/engine/routes/repeaters/summary.js +98 -0
- package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -0
- package/.server/server/plugins/engine/types.d.ts +19 -1
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/vision.d.ts +12 -0
- package/.server/server/plugins/engine/vision.js +55 -0
- package/.server/server/plugins/engine/vision.js.map +1 -0
- package/package.json +1 -1
- package/src/server/plugins/engine/configureEnginePlugin.ts +3 -5
- package/src/server/plugins/engine/options.js +37 -0
- package/src/server/plugins/engine/options.test.js +34 -0
- package/src/server/plugins/engine/plugin.ts +30 -772
- package/src/server/plugins/engine/registrationOptions.ts +0 -0
- package/src/server/plugins/engine/routes/file-upload.ts +54 -0
- package/src/server/plugins/engine/routes/index.ts +187 -0
- package/src/server/plugins/engine/routes/questions.ts +208 -0
- package/src/server/plugins/engine/routes/repeaters/item-delete.ts +157 -0
- package/src/server/plugins/engine/routes/repeaters/summary.ts +137 -0
- package/src/server/plugins/engine/types.ts +26 -1
- 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 {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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: [
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
-
}
|