Skip to content

Commit

Permalink
Merge pull request #675 from DEFRA/feature/services-strategy-pattern
Browse files Browse the repository at this point in the history
Feature/services strategy pattern
  • Loading branch information
alexluckett authored Jan 22, 2025
2 parents 83ba365 + 585076d commit 2f193fe
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 24 deletions.
7 changes: 4 additions & 3 deletions src/server/plugins/engine/configureEnginePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@ import { type RouteConfig } from '~/src/server/types.js'

export const configureEnginePlugin = async ({
formFileName,
formFilePath
formFilePath,
services
}: RouteConfig = {}): Promise<ServerRegisterPluginObject<PluginOptions>> => {
let model: FormModel | undefined

if (formFileName && formFilePath) {
const definition = await getForm(join(formFilePath, formFileName))
const { name } = parse(formFileName)

model = new FormModel(definition, { basePath: name })
model = new FormModel(definition, { basePath: name }, services)
}

return {
plugin,
options: { model }
options: { model, services }
}
}

Expand Down
13 changes: 9 additions & 4 deletions src/server/plugins/engine/models/FormModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
type PageControllerClass
} from '~/src/server/plugins/engine/pageControllers/helpers.js'
import { validationOptions as opts } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
import {
type FormContext,
type FormContextRequest,
Expand All @@ -34,10 +35,8 @@ import {
} from '~/src/server/plugins/engine/types.js'
import { FormAction } from '~/src/server/routes/types.js'
import { merge } from '~/src/server/services/cacheService.js'
import { type Services } from '~/src/server/types.js'

/**
* Responsible for instantiating the {@link PageControllerClass} and condition context from a form JSON
*/
export class FormModel {
/** The runtime engine that should be used */
engine?: Engine
Expand All @@ -52,8 +51,13 @@ export class FormModel {
basePath: string
conditions: Partial<Record<string, ExecutableCondition>>
pages: PageControllerClass[]
services: Services

constructor(def: typeof this.def, options: { basePath: string }) {
constructor(
def: typeof this.def,
options: { basePath: string },
services: Services = defaultServices
) {
const result = formDefinitionSchema.validate(def, { abortEarly: false })

if (result.error) {
Expand Down Expand Up @@ -89,6 +93,7 @@ export class FormModel {
this.values = result.value
this.basePath = options.basePath
this.conditions = {}
this.services = services

def.conditions.forEach((conditionDef) => {
const condition = this.makeCondition(conditionDef)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
} from '~/src/server/plugins/engine/helpers.js'
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
import { getFormMetadata } from '~/src/server/plugins/engine/services/formsService.js'
import {
type FormContext,
type FormContextRequest,
Expand Down Expand Up @@ -398,6 +397,8 @@ export class QuestionPageController extends PageController {

const startPath = this.getStartPath()
const summaryPath = this.getSummaryPath()
const { formsService } = this.model.services
const { getFormMetadata } = formsService

// Warn the user if the form has no notification email set only on start page and summary page
if ([startPath, summaryPath].includes(path) && !isForceAccess) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { type ResponseToolkit } from '@hapi/hapi'

import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
import { getFormMetadata } from '~/src/server/plugins/engine/services/formsService.js'
import { type FormContext } from '~/src/server/plugins/engine/types.js'
import { type FormRequest } from '~/src/server/routes/types.js'

Expand Down Expand Up @@ -37,6 +36,9 @@ export class StatusPageController extends QuestionPageController {
}

const slug = request.params.slug
const { formsService } = this.model.services
const { getFormMetadata } = formsService

const { submissionGuidance } = await getFormMetadata(slug)

return h.view(viewName, {
Expand Down
20 changes: 14 additions & 6 deletions src/server/plugins/engine/pageControllers/SummaryPageController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ import {
type DetailItem
} from '~/src/server/plugins/engine/models/types.js'
import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
import {
persistFiles,
submit
} from '~/src/server/plugins/engine/services/formSubmissionService.js'
import { getFormMetadata } from '~/src/server/plugins/engine/services/formsService.js'
import {
type FormContext,
type FormContextRequest,
Expand Down Expand Up @@ -108,6 +103,8 @@ export class SummaryPageController extends QuestionPageController {
const { state } = context

const { cacheService } = request.services([])
const { formsService } = this.model.services
const { getFormMetadata } = formsService

// Get the form metadata using the `slug` param
const { notificationEmail } = await getFormMetadata(params.slug)
Expand Down Expand Up @@ -160,6 +157,8 @@ async function extendFileRetention(
state: FormSubmissionState,
updatedRetrievalKey: string
) {
const { formSubmissionService } = model.services
const { persistFiles } = formSubmissionService
const files: { fileId: string; initiatedRetrievalKey: string }[] = []

// For each file upload component with files in
Expand Down Expand Up @@ -190,10 +189,14 @@ async function extendFileRetention(
}

function submitData(
model: FormModel,
items: DetailItem[],
retrievalKey: string,
sessionId: string
) {
const { formSubmissionService } = model.services
const { submit } = formSubmissionService

const payload: SubmitPayload = {
sessionId,
retrievalKey,
Expand Down Expand Up @@ -248,7 +251,12 @@ async function sendEmail(

// Submit data
request.logger.info(logTags, 'Submitting data')
const submitResponse = await submitData(items, emailAddress, request.yar.id)
const submitResponse = await submitData(
model,
items,
emailAddress,
request.yar.id
)

if (submitResponse === undefined) {
throw Boom.badRequest('Unexpected empty response from submit api')
Expand Down
16 changes: 8 additions & 8 deletions src/server/plugins/engine/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ import { FormModel } from '~/src/server/plugins/engine/models/index.js'
import { FileUploadPageController } from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'
import { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'
import {
getFormDefinition,
getFormMetadata
} from '~/src/server/plugins/engine/services/formsService.js'
import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
import { type FormContext } from '~/src/server/plugins/engine/types.js'
import {
type FormRequest,
Expand All @@ -43,17 +40,20 @@ import {
pathSchema,
stateSchema
} from '~/src/server/schemas/index.js'
import { type Services } from '~/src/server/types.js'

export interface PluginOptions {
model?: FormModel
services?: Services
}

export const plugin = {
name: '@defra/forms-runner/engine',
dependencies: '@hapi/vision',
multiple: true,
register(server, options) {
const { model } = options
const { model, services = defaultServices } = options
const { formsService } = services

server.app.model = model

Expand All @@ -77,7 +77,7 @@ export const plugin = {
const { isPreview, state: formState } = checkFormStatus(path)

// Get the form metadata using the `slug` param
const metadata = await getFormMetadata(slug)
const metadata = await formsService.getFormMetadata(slug)

const { id, [formState]: state } = metadata

Expand All @@ -101,7 +101,7 @@ export const plugin = {
)

// Get the form definition using the `id` from the metadata
const definition = await getFormDefinition(id, formState)
const definition = await formsService.getFormDefinition(id, formState)

if (!definition) {
throw Boom.notFound(
Expand All @@ -125,7 +125,7 @@ export const plugin = {
: slug

// Construct the form model
const model = new FormModel(definition, { basePath })
const model = new FormModel(definition, { basePath }, services)

// Create new item and add it to the item cache
item = { model, updatedAt: state.updatedAt }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const submissionUrl = config.get('submissionUrl')
* @param {string} persistedRetrievalKey - final retrieval key when submitting
*/
export async function persistFiles(files, persistedRetrievalKey) {
const postJsonByType = /** @type {typeof postJson} */ (postJson)
const postJsonByType = /** @type {typeof postJson<object>} */ (postJson)

const payload = {
files,
Expand Down
2 changes: 2 additions & 0 deletions src/server/plugins/engine/services/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as formsService from '~/src/server/plugins/engine/services/formsService.js'
export * as formSubmissionService from '~/src/server/plugins/engine/services/formSubmissionService.js'
31 changes: 31 additions & 0 deletions src/server/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import {
type FormDefinition,
type FormMetadata,
type SubmitPayload,
type SubmitResponsePayload
} from '@defra/forms-model'

import { type FormStatus } from '~/src/server/routes/types.js'

export interface FormsService {
getFormMetadata: (slug: string) => Promise<FormMetadata>
getFormDefinition: (
id: string,
state: FormStatus
) => Promise<FormDefinition | undefined>
}

export interface FormSubmissionService {
persistFiles: (
files: { fileId: string; initiatedRetrievalKey: string }[],
persistedRetrievalKey: string
) => Promise<object>
submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>
}

export interface Services {
formsService: FormsService
formSubmissionService: FormSubmissionService
}

export interface RouteConfig {
formFileName?: string
formFilePath?: string
enforceCsrf?: boolean
services?: Services
}

0 comments on commit 2f193fe

Please sign in to comment.