Skip to content

Commit eba9841

Browse files
committed
Build schemas on server creation to support custom actions
1 parent 5f5df92 commit eba9841

10 files changed

Lines changed: 86 additions & 20 deletions

File tree

src/server/plugins/engine/configureEnginePlugin.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { join, parse } from 'node:path'
33
import { type FormDefinition } from '@defra/forms-model'
44

55
import { FORM_PREFIX } from '~/src/server/constants.js'
6+
import { getCacheService } from '~/src/server/plugins/engine/helpers.js'
67
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
78
import { plugin } from '~/src/server/plugins/engine/plugin.js'
89
import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
@@ -59,7 +60,22 @@ export const configureEnginePlugin = async ({
5960
preparePageEventRequestOptions,
6061
onRequest,
6162
baseUrl: 'http://localhost:3009', // always runs locally
62-
saveAndReturn
63+
saveAndReturn,
64+
buttons: [
65+
{
66+
text: 'My custom submit button'
67+
},
68+
{
69+
text: 'Withdraw button',
70+
action: 'withdraw-submission'
71+
}
72+
],
73+
actionHandlers: {
74+
'withdraw-submission': async (request, _) => {
75+
await getCacheService(request.server).clearState(request)
76+
return '/summary'
77+
}
78+
}
6379
}
6480

6581
/*

src/server/plugins/engine/helpers.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Boom from '@hapi/boom'
1212
import { type ResponseToolkit, type Server } from '@hapi/hapi'
1313
import { format, parseISO } from 'date-fns'
1414
import { StatusCodes } from 'http-status-codes'
15-
import { type Schema, type ValidationErrorItem } from 'joi'
15+
import Joi, { type Schema, type ValidationErrorItem } from 'joi'
1616
import { Liquid } from 'liquidjs'
1717

1818
import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
@@ -376,6 +376,18 @@ export function evaluateTemplate(
376376
})
377377
}
378378

379+
export function getSchemas(server?: Server) {
380+
if (!server) {
381+
// only used in debug UI helper where validation isn't really important
382+
return {
383+
actionSchema: Joi.any().required(),
384+
paramsSchema: Joi.any().required()
385+
}
386+
}
387+
388+
return getPluginOptions(server).schemas
389+
}
390+
379391
export function getCacheService(server: Server) {
380392
return getPluginOptions(server).cacheService
381393
}

src/server/plugins/engine/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ const pluginRegistrationOptionsSchema = Joi.object({
3232
action: Joi.string().optional()
3333
})
3434
)
35-
.optional()
35+
.optional(),
36+
actionHandlers: Joi.object().pattern(Joi.string(), Joi.function())
3637
})
3738

3839
/**

src/server/plugins/engine/pageControllers/QuestionPageController.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import {
1010
} from '@defra/forms-model'
1111
// import Boom from '@hapi/boom' // No longer needed
1212
import { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'
13-
import { type ValidationErrorItem } from 'joi'
13+
import Joi, { type ValidationErrorItem } from 'joi'
1414

1515
import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
1616
import { optionalText } from '~/src/server/plugins/engine/components/constants.js'
1717
import { type BackLink } from '~/src/server/plugins/engine/components/types.js'
1818
import {
1919
getCacheService,
2020
getErrors,
21+
getSchemas,
2122
normalisePath,
2223
proceed
2324
} from '~/src/server/plugins/engine/helpers.js'
@@ -39,11 +40,7 @@ import {
3940
type FormRequestPayloadRefs,
4041
type FormRequestRefs
4142
} from '~/src/server/routes/types.js'
42-
import {
43-
actionSchema,
44-
crumbSchema,
45-
paramsSchema
46-
} from '~/src/server/schemas/index.js'
43+
import { crumbSchema } from '~/src/server/schemas/index.js'
4744
import { merge } from '~/src/server/services/cacheService.js'
4845

4946
export class QuestionPageController extends PageController {
@@ -62,7 +59,7 @@ export class QuestionPageController extends PageController {
6259

6360
this.collection.formSchema = this.collection.formSchema.keys({
6461
crumb: crumbSchema,
65-
action: actionSchema
62+
action: Joi.string()
6663
})
6764
}
6865

@@ -274,7 +271,7 @@ export class QuestionPageController extends PageController {
274271
getFormParams(request?: FormContextRequest): FormPayloadParams {
275272
const { payload } = request ?? {}
276273

277-
const result = paramsSchema.validate(payload, {
274+
const result = getSchemas(request?.server).paramsSchema.validate(payload, {
278275
abortEarly: false,
279276
stripUnknown: true
280277
})

src/server/plugins/engine/plugin.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import {
2222
type FormRequestPayloadRefs,
2323
type FormRequestRefs
2424
} from '~/src/server/routes/types.js'
25+
import {
26+
buildActionSchema,
27+
buildParamsSchema
28+
} from '~/src/server/schemas/index.js'
2529
import { CacheService } from '~/src/server/services/index.js'
2630

2731
export const plugin = {
@@ -51,12 +55,20 @@ export const plugin = {
5155

5256
await registerVision(server, options)
5357

58+
const buttons = getButtons(options)
59+
const actionHandlers = getActionHandlers(options)
60+
const customActions = Object.keys(actionHandlers)
61+
5462
server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)
5563
server.expose('viewContext', viewContext)
5664
server.expose('cacheService', cacheService)
5765
server.expose('saveAndReturn', saveAndReturn)
58-
server.expose('buttons', getButtons(options))
59-
server.expose('actionHandlers', getActionHandlers(options))
66+
server.expose('buttons', buttons)
67+
server.expose('actionHandlers', actionHandlers)
68+
server.expose('schemas', {
69+
actionSchema: buildActionSchema(customActions),
70+
paramsSchema: buildParamsSchema(customActions)
71+
})
6072

6173
server.app.model = model
6274

@@ -90,12 +102,13 @@ export const plugin = {
90102

91103
const routes = [
92104
...getQuestionRoutes(
105+
server,
93106
getRouteOptions,
94107
postRouteOptions,
95108
preparePageEventRequestOptions
96109
),
97-
...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions),
98-
...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions),
110+
...getRepeaterSummaryRoutes(server, getRouteOptions, postRouteOptions),
111+
...getRepeaterItemDeleteRoutes(server, getRouteOptions, postRouteOptions),
99112
...getSaveAndReturnExitRoutes(getRouteOptions),
100113
...getFileUploadStatusRoutes()
101114
]

src/server/plugins/engine/routes/questions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import {
44
type ResponseObject,
55
type ResponseToolkit,
66
type RouteOptions,
7+
type Server,
78
type ServerRoute
89
} from '@hapi/hapi'
910
import Joi from 'joi'
1011

1112
import {
13+
getSchemas,
1214
normalisePath,
1315
proceed,
1416
redirectPath
@@ -35,7 +37,6 @@ import {
3537
type FormRequestRefs
3638
} from '~/src/server/routes/types.js'
3739
import {
38-
actionSchema,
3940
crumbSchema,
4041
itemIdSchema,
4142
pathSchema,
@@ -164,10 +165,13 @@ function isSuccessful(response: ResponseObject): boolean {
164165
}
165166

166167
export function getRoutes(
168+
server: Server,
167169
getRouteOptions: RouteOptions<FormRequestRefs>,
168170
postRouteOptions: RouteOptions<FormRequestPayloadRefs>,
169171
preparePageEventRequestOptions?: PreparePageEventRequestOptions
170172
): (ServerRoute<FormRequestRefs> | ServerRoute<FormRequestPayloadRefs>)[] {
173+
const { actionSchema } = getSchemas(server)
174+
171175
return [
172176
{
173177
method: 'get',

src/server/plugins/engine/routes/repeaters/item-delete.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import Boom from '@hapi/boom'
33
import {
44
type ResponseToolkit,
55
type RouteOptions,
6+
type Server,
67
type ServerRoute
78
} from '@hapi/hapi'
89
import Joi from 'joi'
910

11+
import { getSchemas } from '~/src/server/plugins/engine/helpers.js'
1012
import { FileUploadPageController } from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'
1113
import { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'
1214
import { redirectOrMakeHandler } from '~/src/server/plugins/engine/routes/index.js'
@@ -17,7 +19,6 @@ import {
1719
type FormRequestRefs
1820
} from '~/src/server/routes/types.js'
1921
import {
20-
actionSchema,
2122
confirmSchema,
2223
crumbSchema,
2324
itemIdSchema,
@@ -70,9 +71,12 @@ function postHandler(
7071
}
7172

7273
export function getRoutes(
74+
server: Server,
7375
getRouteOptions: RouteOptions<FormRequestRefs>,
7476
postRouteOptions: RouteOptions<FormRequestPayloadRefs>
7577
): (ServerRoute<FormRequestRefs> | ServerRoute<FormRequestPayloadRefs>)[] {
78+
const { actionSchema } = getSchemas(server)
79+
7680
return [
7781
{
7882
method: 'get',

src/server/plugins/engine/routes/repeaters/summary.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import Boom from '@hapi/boom'
44
import {
55
type ResponseToolkit,
66
type RouteOptions,
7+
type Server,
78
type ServerRoute
89
} from '@hapi/hapi'
910
import Joi from 'joi'
1011

12+
import { getSchemas } from '~/src/server/plugins/engine/helpers.js'
1113
import { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'
1214
import { redirectOrMakeHandler } from '~/src/server/plugins/engine/routes/index.js'
1315
import {
@@ -17,7 +19,6 @@ import {
1719
type FormRequestRefs
1820
} from '~/src/server/routes/types.js'
1921
import {
20-
actionSchema,
2122
crumbSchema,
2223
pathSchema,
2324
stateSchema
@@ -56,9 +57,12 @@ function postHandler(
5657
}
5758

5859
export function getRoutes(
60+
server: Server,
5961
getRouteOptions: RouteOptions<FormRequestRefs>,
6062
postRouteOptions: RouteOptions<FormRequestPayloadRefs>
6163
): (ServerRoute<FormRequestRefs> | ServerRoute<FormRequestPayloadRefs>)[] {
64+
const { actionSchema } = getSchemas(server)
65+
6266
return [
6367
{
6468
method: 'get',

src/server/schemas/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const stateSchema = Joi.string<FormStatus>()
77
.valid(FormStatus.Draft, FormStatus.Live)
88
.required()
99

10-
export const actionSchema = Joi.string<FormAction>()
10+
const actionSchema = Joi.string<FormAction>()
1111
.valid(
1212
FormAction.Continue,
1313
FormAction.Validate,
@@ -24,7 +24,7 @@ export const itemIdSchema = Joi.string().uuid().required()
2424
export const crumbSchema = Joi.string().optional().allow('')
2525
export const confirmSchema = Joi.boolean().empty(false)
2626

27-
export const paramsSchema = Joi.object<FormPayloadParams>()
27+
const paramsSchema = Joi.object<FormPayloadParams>()
2828
.keys({
2929
action: actionSchema,
3030
confirm: confirmSchema,
@@ -33,3 +33,13 @@ export const paramsSchema = Joi.object<FormPayloadParams>()
3333
})
3434
.default({})
3535
.optional()
36+
37+
export function buildActionSchema(customActions: string[]) {
38+
return actionSchema.valid(...customActions)
39+
}
40+
41+
export function buildParamsSchema(customActions: string[]) {
42+
return paramsSchema.alter({
43+
action: (schema) => schema.valid(...customActions)
44+
})
45+
}

src/typings/hapi/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { type Plugin } from '@hapi/hapi'
44
import { type ServerYar, type Yar } from '@hapi/yar'
5+
import { type Schema as JoiSchema } from 'joi'
56
import { type Logger } from 'pino'
67

78
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
@@ -36,6 +37,10 @@ declare module '@hapi/hapi' {
3637
FormAction | string,
3738
(request: FormRequestPayload, context: FormContext) => Promise<string>
3839
>
40+
schemas: {
41+
actionSchema: JoiSchema
42+
paramsSchema: JoiSchema
43+
}
3944
}
4045
}
4146

0 commit comments

Comments
 (0)