@defra/forms-engine-plugin 3.0.9 → 4.0.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.
- package/.server/server/plugins/engine/plugin.js +3 -2
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/routes/index.d.ts +2 -2
- package/.server/server/plugins/engine/routes/index.js +10 -8
- package/.server/server/plugins/engine/routes/index.js.map +1 -1
- package/.server/server/plugins/engine/routes/questions.d.ts +4 -4
- package/.server/server/plugins/engine/routes/questions.js +10 -10
- package/.server/server/plugins/engine/routes/questions.js.map +1 -1
- package/.server/server/plugins/engine/routes/repeaters/item-delete.d.ts +2 -1
- package/.server/server/plugins/engine/routes/repeaters/item-delete.js +31 -27
- package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -1
- package/.server/server/plugins/engine/routes/repeaters/summary.d.ts +2 -1
- package/.server/server/plugins/engine/routes/repeaters/summary.js +31 -27
- package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +3 -3
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/routes/types.d.ts +1 -1
- package/.server/server/routes/types.js.map +1 -1
- package/package.json +1 -1
- package/src/server/plugins/engine/helpers.test.ts +2 -1
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +2 -1
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +8 -4
- package/src/server/plugins/engine/pageControllers/SummaryPageController.test.ts +2 -1
- package/src/server/plugins/engine/plugin.ts +10 -4
- package/src/server/plugins/engine/routes/index.test.ts +316 -0
- package/src/server/plugins/engine/routes/index.ts +11 -6
- package/src/server/plugins/engine/routes/questions.test.ts +126 -15
- package/src/server/plugins/engine/routes/questions.ts +71 -57
- package/src/server/plugins/engine/routes/repeaters/item-delete.test.ts +83 -0
- package/src/server/plugins/engine/routes/repeaters/item-delete.ts +39 -33
- package/src/server/plugins/engine/routes/repeaters/summary.test.ts +75 -0
- package/src/server/plugins/engine/routes/repeaters/summary.ts +28 -22
- package/src/server/plugins/engine/types.ts +6 -7
- package/src/server/routes/types.ts +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"summary.js","names":["slugSchema","Boom","Joi","RepeatPageController","redirectOrMakeHandler","actionSchema","crumbSchema","pathSchema","stateSchema","getHandler","request","h","params","page","context","notFound","path","makeGetListSummaryRouteHandler","postHandler","isForceAccess","makePostListSummaryRouteHandler","getRoutes","getRouteOptions","postRouteOptions","method","handler","options","validate","object","keys","slug","state","payload","crumb","action","required"],"sources":["../../../../../../src/server/plugins/engine/routes/repeaters/summary.ts"],"sourcesContent":["// List summary GET route\nimport { slugSchema } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type RouteOptions, type ServerRoute } from '@hapi/hapi'\nimport Joi from 'joi'\n\nimport { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'\nimport { redirectOrMakeHandler } from '~/src/server/plugins/engine/routes/index.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormRequestRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\nimport {\n actionSchema,\n crumbSchema,\n pathSchema,\n stateSchema\n} from '~/src/server/schemas/index.js'\n\nfunction getHandler(request: FormRequest, h: FormResponseToolkit) {\n
|
|
1
|
+
{"version":3,"file":"summary.js","names":["slugSchema","Boom","Joi","RepeatPageController","redirectOrMakeHandler","actionSchema","crumbSchema","pathSchema","stateSchema","getHandler","onRequest","request","h","params","page","context","notFound","path","makeGetListSummaryRouteHandler","postHandler","isForceAccess","makePostListSummaryRouteHandler","getRoutes","getRouteOptions","postRouteOptions","method","handler","options","validate","object","keys","slug","state","payload","crumb","action","required"],"sources":["../../../../../../src/server/plugins/engine/routes/repeaters/summary.ts"],"sourcesContent":["// List summary GET route\nimport { slugSchema } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type RouteOptions, type ServerRoute } from '@hapi/hapi'\nimport Joi from 'joi'\n\nimport { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'\nimport { redirectOrMakeHandler } from '~/src/server/plugins/engine/routes/index.js'\nimport { type OnRequestCallback } from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormRequestRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\nimport {\n actionSchema,\n crumbSchema,\n pathSchema,\n stateSchema\n} from '~/src/server/schemas/index.js'\n\nfunction getHandler(onRequest?: OnRequestCallback) {\n return async function (request: FormRequest, h: FormResponseToolkit) {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, onRequest, (page, context) => {\n if (!(page instanceof RepeatPageController)) {\n throw Boom.notFound(`No repeater page found for /${params.path}`)\n }\n\n return page.makeGetListSummaryRouteHandler()(request, context, h)\n })\n }\n}\n\nfunction postHandler(onRequest?: OnRequestCallback) {\n return async function (request: FormRequestPayload, h: FormResponseToolkit) {\n const { params } = request\n\n return redirectOrMakeHandler(request, h, onRequest, (page, context) => {\n const { isForceAccess } = context\n\n if (isForceAccess || !(page instanceof RepeatPageController)) {\n throw Boom.notFound(`No repeater page found for /${params.path}`)\n }\n\n return page.makePostListSummaryRouteHandler()(request, context, h)\n })\n }\n}\n\nexport function getRoutes(\n getRouteOptions: RouteOptions<FormRequestRefs>,\n postRouteOptions: RouteOptions<FormRequestPayloadRefs>,\n onRequest?: OnRequestCallback\n): (ServerRoute<FormRequestRefs> | ServerRoute<FormRequestPayloadRefs>)[] {\n return [\n {\n method: 'get',\n path: '/{slug}/{path}/summary',\n handler: getHandler(onRequest),\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema\n })\n }\n }\n },\n\n {\n method: 'get',\n path: '/preview/{state}/{slug}/{path}/summary',\n handler: getHandler(onRequest),\n options: {\n ...getRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema\n })\n }\n }\n },\n\n {\n method: 'post',\n path: '/{slug}/{path}/summary',\n handler: postHandler(onRequest),\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n slug: slugSchema,\n path: pathSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .required()\n }\n }\n },\n\n {\n method: 'post',\n path: '/preview/{state}/{slug}/{path}/summary',\n handler: postHandler(onRequest),\n options: {\n ...postRouteOptions,\n validate: {\n params: Joi.object().keys({\n state: stateSchema,\n slug: slugSchema,\n path: pathSchema\n }),\n payload: Joi.object()\n .keys({\n crumb: crumbSchema,\n action: actionSchema\n })\n .required()\n }\n }\n }\n ]\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ComponentDef, type Event, type
|
|
1
|
+
import { type ComponentDef, type Event, type FormVersionMetadata, type Item, type List, type Page } from '@defra/forms-model';
|
|
2
2
|
import { type PluginProperties, type Request, type ResponseObject } from '@hapi/hapi';
|
|
3
3
|
import { type JoiExpression, type ValidationErrorItem } from 'joi';
|
|
4
4
|
import { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js';
|
|
@@ -11,7 +11,7 @@ import { type PageController } from '~/src/server/plugins/engine/pageControllers
|
|
|
11
11
|
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js';
|
|
12
12
|
import { type FileStatus, type FormAdapterSubmissionSchemaVersion, type UploadStatus } from '~/src/server/plugins/engine/types/enums.js';
|
|
13
13
|
import { type ViewContext } from '~/src/server/plugins/nunjucks/types.js';
|
|
14
|
-
import { type FormAction, type
|
|
14
|
+
import { type FormAction, type FormRequest, type FormRequestPayload, type FormResponseToolkit, type FormStatus } from '~/src/server/routes/types.js';
|
|
15
15
|
import { type CacheService } from '~/src/server/services/cacheService.js';
|
|
16
16
|
import { type RequestOptions } from '~/src/server/services/httpService.js';
|
|
17
17
|
import { type Services } from '~/src/server/types.js';
|
|
@@ -261,7 +261,7 @@ export interface ErrorMessageTemplateList {
|
|
|
261
261
|
advancedSettingsErrors: ErrorMessageTemplate[];
|
|
262
262
|
}
|
|
263
263
|
export type PreparePageEventRequestOptions = (options: RequestOptions, event: Event, page: PageControllerClass, context: FormContext) => void;
|
|
264
|
-
export type OnRequestCallback = (request: AnyFormRequest,
|
|
264
|
+
export type OnRequestCallback = (request: AnyFormRequest, h: FormResponseToolkit, context: FormContext) => ResponseObject | FormResponseToolkit['continue'] | Promise<ResponseObject | FormResponseToolkit['continue']>;
|
|
265
265
|
export type SaveAndExitHandler = (request: FormRequestPayload, h: FormResponseToolkit, context: FormContext) => ResponseObject;
|
|
266
266
|
export interface PluginOptions {
|
|
267
267
|
model?: FormModel;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n custom?: Record<string, unknown>\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue | null>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage\n extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAqDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AA+Nd;AACA;AACA","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n h: FormResponseToolkit,\n context: FormContext\n) =>\n | ResponseObject\n | FormResponseToolkit['continue']\n | Promise<ResponseObject | FormResponseToolkit['continue']>\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n custom?: Record<string, unknown>\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue | null>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage\n extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAkDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AAiOd;AACA;AACA","ignoreList":[]}
|
|
@@ -25,7 +25,7 @@ export interface FormRequestPayloadRefs extends FormRequestRefs {
|
|
|
25
25
|
}
|
|
26
26
|
export type FormRequest = Request<FormRequestRefs>;
|
|
27
27
|
export type FormRequestPayload = Request<FormRequestPayloadRefs>;
|
|
28
|
-
export type FormResponseToolkit = Pick<ResponseToolkit, 'redirect' | 'view'>;
|
|
28
|
+
export type FormResponseToolkit = Pick<ResponseToolkit, 'redirect' | 'view' | 'continue'>;
|
|
29
29
|
export declare enum FormAction {
|
|
30
30
|
Continue = "continue",
|
|
31
31
|
Validate = "validate",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["FormAction","FormStatus"],"sources":["../../../src/server/routes/types.ts"],"sourcesContent":["import {\n type ReqRefDefaults,\n type Request,\n type ResponseToolkit\n} from '@hapi/hapi'\n\nimport { type FormPayload } from '~/src/server/plugins/engine/types.js'\n\nexport interface FormQuery extends Partial<Record<string, string>> {\n /**\n * Allow preview URL direct access without relevant page checks\n */\n force?: string\n\n /**\n * Redirect location after 'continue' form action\n */\n returnUrl?: string\n}\n\nexport interface FormParams extends Partial<Record<string, string>> {\n path: string\n slug: string\n state?: FormStatus\n}\n\nexport interface FormRequestRefs\n extends Omit<ReqRefDefaults, 'Params' | 'Payload' | 'Query'> {\n Params: FormParams\n Payload: object | undefined\n Query: FormQuery\n}\n\nexport interface FormRequestPayloadRefs extends FormRequestRefs {\n Payload: FormPayload\n}\n\nexport type FormRequest = Request<FormRequestRefs>\nexport type FormRequestPayload = Request<FormRequestPayloadRefs>\nexport type FormResponseToolkit = Pick
|
|
1
|
+
{"version":3,"file":"types.js","names":["FormAction","FormStatus"],"sources":["../../../src/server/routes/types.ts"],"sourcesContent":["import {\n type ReqRefDefaults,\n type Request,\n type ResponseToolkit\n} from '@hapi/hapi'\n\nimport { type FormPayload } from '~/src/server/plugins/engine/types.js'\n\nexport interface FormQuery extends Partial<Record<string, string>> {\n /**\n * Allow preview URL direct access without relevant page checks\n */\n force?: string\n\n /**\n * Redirect location after 'continue' form action\n */\n returnUrl?: string\n}\n\nexport interface FormParams extends Partial<Record<string, string>> {\n path: string\n slug: string\n state?: FormStatus\n}\n\nexport interface FormRequestRefs\n extends Omit<ReqRefDefaults, 'Params' | 'Payload' | 'Query'> {\n Params: FormParams\n Payload: object | undefined\n Query: FormQuery\n}\n\nexport interface FormRequestPayloadRefs extends FormRequestRefs {\n Payload: FormPayload\n}\n\nexport type FormRequest = Request<FormRequestRefs>\nexport type FormRequestPayload = Request<FormRequestPayloadRefs>\nexport type FormResponseToolkit = Pick<\n ResponseToolkit,\n 'redirect' | 'view' | 'continue'\n>\n\nexport enum FormAction {\n Continue = 'continue',\n Validate = 'validate',\n Delete = 'delete',\n AddAnother = 'add-another',\n Send = 'send',\n SaveAndExit = 'save-and-exit'\n}\n\nexport enum FormStatus {\n Draft = 'draft',\n Live = 'live'\n}\n"],"mappings":"AA4CA,WAAYA,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA;AAStB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -805,7 +805,8 @@ describe('QuestionPageController', () => {
|
|
|
805
805
|
|
|
806
806
|
const h: FormResponseToolkit = {
|
|
807
807
|
redirect: jest.fn().mockReturnValue(response),
|
|
808
|
-
view: jest.fn()
|
|
808
|
+
view: jest.fn(),
|
|
809
|
+
continue: Symbol('continue')
|
|
809
810
|
}
|
|
810
811
|
|
|
811
812
|
it('returns default route options', () => {
|
|
@@ -1373,7 +1374,8 @@ describe('QuestionPageController V2', () => {
|
|
|
1373
1374
|
|
|
1374
1375
|
const h: FormResponseToolkit = {
|
|
1375
1376
|
redirect: jest.fn().mockReturnValue(response),
|
|
1376
|
-
view: jest.fn()
|
|
1377
|
+
view: jest.fn(),
|
|
1378
|
+
continue: Symbol('continue')
|
|
1377
1379
|
}
|
|
1378
1380
|
|
|
1379
1381
|
it('returns default route options', () => {
|
|
@@ -1532,7 +1534,8 @@ describe('Save and Exit functionality', () => {
|
|
|
1532
1534
|
|
|
1533
1535
|
const h: FormResponseToolkit = {
|
|
1534
1536
|
redirect: jest.fn().mockReturnValue(response),
|
|
1535
|
-
view: jest.fn()
|
|
1537
|
+
view: jest.fn(),
|
|
1538
|
+
continue: Symbol('continue')
|
|
1536
1539
|
}
|
|
1537
1540
|
|
|
1538
1541
|
beforeEach(() => {
|
|
@@ -1663,7 +1666,8 @@ describe('Save and Exit functionality', () => {
|
|
|
1663
1666
|
|
|
1664
1667
|
const mockH = {
|
|
1665
1668
|
redirect: jest.fn().mockReturnValue(mockResponse),
|
|
1666
|
-
view: jest.fn()
|
|
1669
|
+
view: jest.fn(),
|
|
1670
|
+
continue: Symbol('continue')
|
|
1667
1671
|
}
|
|
1668
1672
|
|
|
1669
1673
|
const postHandler = controller1.makePostRouteHandler()
|
|
@@ -34,7 +34,8 @@ export const plugin = {
|
|
|
34
34
|
saveAndExit,
|
|
35
35
|
nunjucks: nunjucksOptions,
|
|
36
36
|
viewContext,
|
|
37
|
-
preparePageEventRequestOptions
|
|
37
|
+
preparePageEventRequestOptions,
|
|
38
|
+
onRequest
|
|
38
39
|
} = options
|
|
39
40
|
|
|
40
41
|
const cacheService =
|
|
@@ -83,10 +84,15 @@ export const plugin = {
|
|
|
83
84
|
...getQuestionRoutes(
|
|
84
85
|
getRouteOptions,
|
|
85
86
|
postRouteOptions,
|
|
86
|
-
preparePageEventRequestOptions
|
|
87
|
+
preparePageEventRequestOptions,
|
|
88
|
+
onRequest
|
|
89
|
+
),
|
|
90
|
+
...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest),
|
|
91
|
+
...getRepeaterItemDeleteRoutes(
|
|
92
|
+
getRouteOptions,
|
|
93
|
+
postRouteOptions,
|
|
94
|
+
onRequest
|
|
87
95
|
),
|
|
88
|
-
...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions),
|
|
89
|
-
...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions),
|
|
90
96
|
...getFileUploadStatusRoutes()
|
|
91
97
|
]
|
|
92
98
|
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import Boom from '@hapi/boom'
|
|
2
|
+
import { type ResponseObject, type ResponseToolkit } from '@hapi/hapi'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
findPage,
|
|
6
|
+
getCacheService,
|
|
7
|
+
getPage,
|
|
8
|
+
proceed
|
|
9
|
+
} from '~/src/server/plugins/engine/helpers.js'
|
|
10
|
+
import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
11
|
+
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
|
|
12
|
+
import { redirectOrMakeHandler } from '~/src/server/plugins/engine/routes/index.js'
|
|
13
|
+
import {
|
|
14
|
+
type AnyFormRequest,
|
|
15
|
+
type OnRequestCallback
|
|
16
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
17
|
+
import { type FormResponseToolkit } from '~/src/server/routes/types.js'
|
|
18
|
+
|
|
19
|
+
jest.mock('~/src/server/plugins/engine/helpers')
|
|
20
|
+
|
|
21
|
+
describe('redirectOrMakeHandler', () => {
|
|
22
|
+
const mockServer = {} as unknown as Parameters<
|
|
23
|
+
typeof redirectOrMakeHandler
|
|
24
|
+
>[0]['server']
|
|
25
|
+
const mockRequest: AnyFormRequest = {
|
|
26
|
+
server: mockServer,
|
|
27
|
+
app: {},
|
|
28
|
+
params: { path: 'test-path' },
|
|
29
|
+
query: {}
|
|
30
|
+
} as unknown as AnyFormRequest
|
|
31
|
+
|
|
32
|
+
const mockH: FormResponseToolkit = {
|
|
33
|
+
redirect: jest.fn(),
|
|
34
|
+
view: jest.fn(),
|
|
35
|
+
continue: Symbol('continue')
|
|
36
|
+
} as unknown as FormResponseToolkit
|
|
37
|
+
|
|
38
|
+
let mockPage: PageControllerClass
|
|
39
|
+
|
|
40
|
+
const mockModel: FormModel = {
|
|
41
|
+
def: {
|
|
42
|
+
metadata: {
|
|
43
|
+
submission: { code: 'TEST-CODE' }
|
|
44
|
+
} as { submission: { code: string } }
|
|
45
|
+
},
|
|
46
|
+
getFormContext: jest.fn().mockReturnValue({
|
|
47
|
+
isForceAccess: false,
|
|
48
|
+
data: {}
|
|
49
|
+
})
|
|
50
|
+
} as unknown as FormModel
|
|
51
|
+
|
|
52
|
+
const mockMakeHandler = jest
|
|
53
|
+
.fn()
|
|
54
|
+
.mockResolvedValue({ statusCode: 200 } as ResponseObject)
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
jest.clearAllMocks()
|
|
58
|
+
mockRequest.app = { model: mockModel }
|
|
59
|
+
|
|
60
|
+
// Reset mock page
|
|
61
|
+
mockPage = {
|
|
62
|
+
getState: jest.fn().mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
63
|
+
mergeState: jest
|
|
64
|
+
.fn()
|
|
65
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
66
|
+
getRelevantPath: jest.fn().mockReturnValue('/test-path'),
|
|
67
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
68
|
+
getHref: jest.fn().mockReturnValue('/test-href'),
|
|
69
|
+
path: '/test-path'
|
|
70
|
+
} as unknown as PageControllerClass
|
|
71
|
+
|
|
72
|
+
// Reset mock model
|
|
73
|
+
mockModel.getFormContext = jest.fn().mockReturnValue({
|
|
74
|
+
isForceAccess: false,
|
|
75
|
+
data: {}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Setup mocks
|
|
79
|
+
;(getCacheService as jest.Mock).mockReturnValue({
|
|
80
|
+
getFlash: jest.fn().mockReturnValue({ errors: [] })
|
|
81
|
+
})
|
|
82
|
+
;(getPage as jest.Mock).mockReturnValue(mockPage)
|
|
83
|
+
;(findPage as jest.Mock).mockReturnValue({ next: [] })
|
|
84
|
+
;(proceed as jest.Mock).mockReturnValue({ statusCode: 302 })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('onRequest callback functionality', () => {
|
|
88
|
+
it('should call onRequest callback when provided', async () => {
|
|
89
|
+
const onRequestCallback: OnRequestCallback = jest
|
|
90
|
+
.fn()
|
|
91
|
+
.mockResolvedValue(undefined)
|
|
92
|
+
|
|
93
|
+
await redirectOrMakeHandler(
|
|
94
|
+
mockRequest,
|
|
95
|
+
mockH,
|
|
96
|
+
onRequestCallback,
|
|
97
|
+
mockMakeHandler
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
expect(onRequestCallback).toHaveBeenCalledWith(
|
|
101
|
+
mockRequest,
|
|
102
|
+
mockH as ResponseToolkit,
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
isForceAccess: false,
|
|
105
|
+
data: {}
|
|
106
|
+
})
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should not call onRequest callback when not provided', async () => {
|
|
111
|
+
const onRequestCallback = jest.fn()
|
|
112
|
+
|
|
113
|
+
await redirectOrMakeHandler(
|
|
114
|
+
mockRequest,
|
|
115
|
+
mockH,
|
|
116
|
+
undefined,
|
|
117
|
+
mockMakeHandler
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
expect(onRequestCallback).not.toHaveBeenCalled()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should return takeover response when onRequest returns takeover response', async () => {
|
|
124
|
+
const takeoverResponse = {
|
|
125
|
+
statusCode: 302,
|
|
126
|
+
headers: { location: '/redirect-url' },
|
|
127
|
+
_takeover: true
|
|
128
|
+
} as unknown as ResponseObject
|
|
129
|
+
|
|
130
|
+
const onRequestCallback: OnRequestCallback = jest
|
|
131
|
+
.fn()
|
|
132
|
+
.mockResolvedValue(takeoverResponse)
|
|
133
|
+
|
|
134
|
+
const result = await redirectOrMakeHandler(
|
|
135
|
+
mockRequest,
|
|
136
|
+
mockH,
|
|
137
|
+
onRequestCallback,
|
|
138
|
+
mockMakeHandler
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
expect(result).toBe(takeoverResponse)
|
|
142
|
+
expect(mockMakeHandler).not.toHaveBeenCalled()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should continue processing when onRequest returns h.continue', async () => {
|
|
146
|
+
const onRequestCallback: OnRequestCallback = jest
|
|
147
|
+
.fn()
|
|
148
|
+
.mockResolvedValue(mockH.continue)
|
|
149
|
+
|
|
150
|
+
await redirectOrMakeHandler(
|
|
151
|
+
mockRequest,
|
|
152
|
+
mockH,
|
|
153
|
+
onRequestCallback,
|
|
154
|
+
mockMakeHandler
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
expect(mockMakeHandler).toHaveBeenCalledWith(mockPage, expect.any(Object))
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should handle onRequest callback errors', async () => {
|
|
161
|
+
const error = new Error('onRequest callback error')
|
|
162
|
+
const onRequestCallback: OnRequestCallback = jest
|
|
163
|
+
.fn()
|
|
164
|
+
.mockRejectedValue(error)
|
|
165
|
+
|
|
166
|
+
await expect(
|
|
167
|
+
redirectOrMakeHandler(
|
|
168
|
+
mockRequest,
|
|
169
|
+
mockH,
|
|
170
|
+
onRequestCallback,
|
|
171
|
+
mockMakeHandler
|
|
172
|
+
)
|
|
173
|
+
).rejects.toThrow('onRequest callback error')
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('existing functionality', () => {
|
|
178
|
+
it('should throw error when model is missing', async () => {
|
|
179
|
+
mockRequest.app = {}
|
|
180
|
+
|
|
181
|
+
await expect(
|
|
182
|
+
redirectOrMakeHandler(mockRequest, mockH, undefined, mockMakeHandler)
|
|
183
|
+
).rejects.toThrow(Boom.notFound('No model found for /test-path'))
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should call makeHandler when page is relevant', async () => {
|
|
187
|
+
const testPage = {
|
|
188
|
+
getState: jest
|
|
189
|
+
.fn()
|
|
190
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
191
|
+
mergeState: jest
|
|
192
|
+
.fn()
|
|
193
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
194
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
195
|
+
getHref: jest.fn().mockReturnValue('/test-href'),
|
|
196
|
+
getRelevantPath: jest.fn().mockReturnValue('/test-path'),
|
|
197
|
+
path: '/test-path'
|
|
198
|
+
} as unknown as PageControllerClass
|
|
199
|
+
;(getPage as jest.Mock).mockReturnValue(testPage)
|
|
200
|
+
|
|
201
|
+
await redirectOrMakeHandler(
|
|
202
|
+
mockRequest,
|
|
203
|
+
mockH,
|
|
204
|
+
undefined,
|
|
205
|
+
mockMakeHandler
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
expect(mockMakeHandler).toHaveBeenCalledWith(testPage, expect.any(Object))
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should call makeHandler when context has force access', async () => {
|
|
212
|
+
mockModel.getFormContext = jest.fn().mockReturnValue({
|
|
213
|
+
isForceAccess: true,
|
|
214
|
+
data: {}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
await redirectOrMakeHandler(
|
|
218
|
+
mockRequest,
|
|
219
|
+
mockH,
|
|
220
|
+
undefined,
|
|
221
|
+
mockMakeHandler
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
expect(mockMakeHandler).toHaveBeenCalledWith(mockPage, expect.any(Object))
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should redirect when page is not relevant', async () => {
|
|
228
|
+
const testPage = {
|
|
229
|
+
getState: jest
|
|
230
|
+
.fn()
|
|
231
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
232
|
+
mergeState: jest
|
|
233
|
+
.fn()
|
|
234
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
235
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
236
|
+
getHref: jest.fn().mockReturnValue('/test-href'),
|
|
237
|
+
getRelevantPath: jest.fn().mockReturnValue('/other-path'),
|
|
238
|
+
path: '/test-path'
|
|
239
|
+
} as unknown as PageControllerClass
|
|
240
|
+
;(getPage as jest.Mock).mockReturnValue(testPage)
|
|
241
|
+
|
|
242
|
+
await redirectOrMakeHandler(
|
|
243
|
+
mockRequest,
|
|
244
|
+
mockH,
|
|
245
|
+
undefined,
|
|
246
|
+
mockMakeHandler
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
expect(proceed).toHaveBeenCalledWith(mockRequest, mockH, '/test-href')
|
|
250
|
+
expect(mockMakeHandler).not.toHaveBeenCalled()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should set returnUrl when redirecting and next pages exist', async () => {
|
|
254
|
+
const testPage = {
|
|
255
|
+
getState: jest
|
|
256
|
+
.fn()
|
|
257
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
258
|
+
mergeState: jest
|
|
259
|
+
.fn()
|
|
260
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
261
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
262
|
+
getRelevantPath: jest.fn().mockReturnValue('/other-path'),
|
|
263
|
+
path: '/test-path',
|
|
264
|
+
getHref: jest
|
|
265
|
+
.fn()
|
|
266
|
+
.mockReturnValueOnce('/summary-href') // First call: for summaryPath (returnUrl)
|
|
267
|
+
.mockReturnValueOnce('/relevant-path-href') // Second call: for relevantPath (redirect)
|
|
268
|
+
} as unknown as PageControllerClass
|
|
269
|
+
;(getPage as jest.Mock).mockReturnValue(testPage)
|
|
270
|
+
;(findPage as jest.Mock).mockReturnValue({ next: ['next-page'] })
|
|
271
|
+
|
|
272
|
+
await redirectOrMakeHandler(
|
|
273
|
+
mockRequest,
|
|
274
|
+
mockH,
|
|
275
|
+
undefined,
|
|
276
|
+
mockMakeHandler
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
expect(mockRequest.query.returnUrl).toBe('/summary-href')
|
|
280
|
+
expect(proceed).toHaveBeenCalledWith(
|
|
281
|
+
mockRequest,
|
|
282
|
+
mockH,
|
|
283
|
+
'/relevant-path-href'
|
|
284
|
+
)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('should not set returnUrl when redirecting and no next pages exist', async () => {
|
|
288
|
+
const testPage = {
|
|
289
|
+
getState: jest
|
|
290
|
+
.fn()
|
|
291
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
292
|
+
mergeState: jest
|
|
293
|
+
.fn()
|
|
294
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
295
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
296
|
+
getHref: jest.fn().mockReturnValue('/test-href'),
|
|
297
|
+
getRelevantPath: jest.fn().mockReturnValue('/other-path'),
|
|
298
|
+
path: '/test-path'
|
|
299
|
+
} as unknown as PageControllerClass
|
|
300
|
+
;(getPage as jest.Mock).mockReturnValue(testPage)
|
|
301
|
+
const returnUrlBefore = mockRequest.query.returnUrl
|
|
302
|
+
;(findPage as jest.Mock).mockReturnValue({ next: [] })
|
|
303
|
+
|
|
304
|
+
await redirectOrMakeHandler(
|
|
305
|
+
mockRequest,
|
|
306
|
+
mockH,
|
|
307
|
+
undefined,
|
|
308
|
+
mockMakeHandler
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
// returnUrl should not be set if next pages don't exist
|
|
312
|
+
expect(mockRequest.query.returnUrl).toBe(returnUrlBefore)
|
|
313
|
+
expect(proceed).toHaveBeenCalledWith(mockRequest, mockH, '/test-href')
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
})
|
|
@@ -23,6 +23,7 @@ import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
|
|
|
23
23
|
import {
|
|
24
24
|
type AnyFormRequest,
|
|
25
25
|
type FormContext,
|
|
26
|
+
type OnRequestCallback,
|
|
26
27
|
type PluginOptions
|
|
27
28
|
} from '~/src/server/plugins/engine/types.js'
|
|
28
29
|
import {
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
export async function redirectOrMakeHandler(
|
|
34
35
|
request: AnyFormRequest,
|
|
35
36
|
h: FormResponseToolkit,
|
|
37
|
+
onRequest: OnRequestCallback | undefined,
|
|
36
38
|
makeHandler: (
|
|
37
39
|
page: PageControllerClass,
|
|
38
40
|
context: FormContext
|
|
@@ -69,6 +71,14 @@ export async function redirectOrMakeHandler(
|
|
|
69
71
|
const relevantPath = page.getRelevantPath(request, context)
|
|
70
72
|
const summaryPath = page.getSummaryPath()
|
|
71
73
|
|
|
74
|
+
// Call the onRequest callback if it has been supplied
|
|
75
|
+
if (onRequest) {
|
|
76
|
+
const result = await onRequest(request, h, context)
|
|
77
|
+
if (result !== h.continue) {
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
// Return handler for relevant pages or preview URL direct access
|
|
73
83
|
if (relevantPath.startsWith(page.path) || context.isForceAccess) {
|
|
74
84
|
return makeHandler(page, context)
|
|
@@ -89,7 +99,7 @@ export function makeLoadFormPreHandler(server: Server, options: PluginOptions) {
|
|
|
89
99
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- hapi types are wrong
|
|
90
100
|
const prefix = server.realm.modifiers.route.prefix ?? ''
|
|
91
101
|
|
|
92
|
-
const { services = defaultServices, controllers
|
|
102
|
+
const { services = defaultServices, controllers } = options
|
|
93
103
|
|
|
94
104
|
const { formsService } = services
|
|
95
105
|
|
|
@@ -166,11 +176,6 @@ export function makeLoadFormPreHandler(server: Server, options: PluginOptions) {
|
|
|
166
176
|
server.app.models.set(key, item)
|
|
167
177
|
}
|
|
168
178
|
|
|
169
|
-
// Call the onRequest callback if it has been supplied
|
|
170
|
-
if (onRequest) {
|
|
171
|
-
onRequest(request, params, item.model.def, metadata)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
179
|
// Assign the model to the request data
|
|
175
180
|
// for use in the downstream handler
|
|
176
181
|
request.app.model = item.model
|