@defra/forms-engine-plugin 4.0.37 → 4.0.39
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/forms/simple-form.yaml +14 -0
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +10 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +17 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/PageController.d.ts +14 -0
- package/.server/server/plugins/engine/pageControllers/PageController.js +16 -0
- package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StatusPageController.d.ts +1 -0
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js +5 -1
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/errors.js +1 -2
- package/.server/server/plugins/engine/pageControllers/errors.js.map +1 -1
- package/.server/server/plugins/engine/referenceNumbers.d.ts +7 -0
- package/.server/server/plugins/engine/referenceNumbers.js +30 -5
- package/.server/server/plugins/engine/referenceNumbers.js.map +1 -1
- package/.server/server/plugins/engine/views/confirmation.html +2 -1
- package/.server/server/services/cacheService.d.ts +23 -0
- package/.server/server/services/cacheService.js +26 -4
- package/.server/server/services/cacheService.js.map +1 -1
- package/package.json +3 -2
- package/src/server/forms/simple-form.yaml +14 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +49 -1
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +20 -2
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +7 -0
- package/src/server/plugins/engine/pageControllers/PageController.ts +17 -0
- package/src/server/plugins/engine/pageControllers/StatusPageController.ts +5 -1
- package/src/server/plugins/engine/pageControllers/errors.test.ts +5 -2
- package/src/server/plugins/engine/pageControllers/errors.ts +1 -2
- package/src/server/plugins/engine/referenceNumbers.test.ts +42 -1
- package/src/server/plugins/engine/referenceNumbers.ts +43 -5
- package/src/server/plugins/engine/views/confirmation.html +2 -1
- package/src/server/services/cacheService.ts +25 -4
|
@@ -41,6 +41,20 @@ pages:
|
|
|
41
41
|
schema: {}
|
|
42
42
|
id: 987c1234-56d7-89e0-1234-56789abcdef0
|
|
43
43
|
id: 23456789-0abc-def1-2345-67890abcdef1
|
|
44
|
+
- title: Upload a copy of your drivers licence
|
|
45
|
+
controller: FileUploadPageController
|
|
46
|
+
path: '/upload-driving-licence'
|
|
47
|
+
components:
|
|
48
|
+
- type: FileUploadField
|
|
49
|
+
title: Please upload a copy of your drivers licence
|
|
50
|
+
name: driversLicenceUpload
|
|
51
|
+
shortDescription: Upload drivers licence
|
|
52
|
+
hint: ''
|
|
53
|
+
options:
|
|
54
|
+
required: true
|
|
55
|
+
schema: {}
|
|
56
|
+
id: 987c1234-56d7-89e0-1234-56789abcdef1
|
|
57
|
+
id: 23456789-0abc-def1-2345-67890abcdef2
|
|
44
58
|
- title: ''
|
|
45
59
|
path: '/date-of-birth'
|
|
46
60
|
components:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type PageFileUpload } from '@defra/forms-model';
|
|
2
2
|
import { type ValidationErrorItem } from 'joi';
|
|
3
|
-
import {
|
|
3
|
+
import { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js';
|
|
4
|
+
import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js';
|
|
4
5
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js';
|
|
5
6
|
import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js';
|
|
6
7
|
import { type AnyFormRequest, type FeaturedFormPageViewModel, type FormContext, type FormContextRequest, type FormSubmissionError, type FormSubmissionState, type UploadInitiateResponse, type UploadStatusFileResponse } from '~/src/server/plugins/engine/types.js';
|
|
@@ -11,6 +12,14 @@ export declare class FileUploadPageController extends QuestionPageController {
|
|
|
11
12
|
fileUpload: FileUploadField;
|
|
12
13
|
fileDeleteViewName: string;
|
|
13
14
|
constructor(model: FormModel, pageDef: PageFileUpload);
|
|
15
|
+
/**
|
|
16
|
+
* Get supplementary state keys for clearing file upload state.
|
|
17
|
+
* Returns the nested upload path for FileUploadField components only.
|
|
18
|
+
* @param component - The component to get supplementary state keys for
|
|
19
|
+
* @returns Array containing the nested upload path, e.g., ["upload['/page-path']"]
|
|
20
|
+
* or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.
|
|
21
|
+
*/
|
|
22
|
+
getStateKeys(component: FormComponent): string[];
|
|
14
23
|
getFormDataFromState(request: FormContextRequest | undefined, state: FormSubmissionState): import("~/src/server/plugins/engine/types.js").FormPayload;
|
|
15
24
|
getState(request: AnyFormRequest): Promise<FormSubmissionState>;
|
|
16
25
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ComponentType } from '@defra/forms-model';
|
|
2
2
|
import Boom from '@hapi/boom';
|
|
3
3
|
import { wait } from '@hapi/hoek';
|
|
4
|
-
import { tempItemSchema } from "../components/FileUploadField.js";
|
|
4
|
+
import { FileUploadField, tempItemSchema } from "../components/FileUploadField.js";
|
|
5
5
|
import { getCacheService, getError, getExponentialBackoffDelay } from "../helpers.js";
|
|
6
6
|
import { QuestionPageController } from "./QuestionPageController.js";
|
|
7
7
|
import { getProxyUrlForLocalDevelopment } from "./helpers/index.js";
|
|
@@ -49,6 +49,22 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
49
49
|
this.fileUpload = fileUpload;
|
|
50
50
|
this.viewName = 'file-upload';
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get supplementary state keys for clearing file upload state.
|
|
55
|
+
* Returns the nested upload path for FileUploadField components only.
|
|
56
|
+
* @param component - The component to get supplementary state keys for
|
|
57
|
+
* @returns Array containing the nested upload path, e.g., ["upload['/page-path']"]
|
|
58
|
+
* or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.
|
|
59
|
+
*/
|
|
60
|
+
getStateKeys(component) {
|
|
61
|
+
// Only return upload keys for FileUploadField components
|
|
62
|
+
if (!(component instanceof FileUploadField)) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const pagePath = component.page?.path;
|
|
66
|
+
return pagePath ? [`upload['${pagePath}']`] : ['upload'];
|
|
67
|
+
}
|
|
52
68
|
getFormDataFromState(request, state) {
|
|
53
69
|
const {
|
|
54
70
|
fileUpload
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileUploadPageController.js","names":["ComponentType","Boom","wait","tempItemSchema","getCacheService","getError","getExponentialBackoffDelay","QuestionPageController","getProxyUrlForLocalDevelopment","getUploadStatus","initiateUpload","FileStatus","UploadStatus","MAX_UPLOADS","CDP_UPLOAD_TIMEOUT_MS","prepareStatus","status","file","form","isPending","fileStatus","pending","errorMessage","prepareFileState","fileState","FileUploadPageController","fileUpload","fileDeleteViewName","constructor","model","pageDef","collection","fileUploads","fields","filter","field","type","FileUploadField","at","length","badImplementation","path","indexOf","name","viewName","getFormDataFromState","request","state","payload","files","getFilesFromState","undefined","getState","refreshUpload","uploadState","upload","getUploadFromState","makeGetItemDeleteRouteHandler","context","h","viewModel","params","fileToRemove","find","uploadId","itemId","notFound","filename","view","backLink","getBackLink","pageTitle","itemTitle","confirmation","text","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","getFormParams","checkRemovedFiles","proceed","getErrors","details","errors","forEach","error","isUploadError","isUploadRootError","push","value","href","getViewModel","components","formComponent","id","index","proxyUrl","uploadUrl","formAction","componentsBefore","slice","checkUploadStatus","depth","initiateAndStoreNewUpload","statusResponse","badRequest","uploadStatus","initiated","err","Error","logger","gatewayTimeout","toFixed","delay","info","validationResult","validate","stripUnknown","complete","unshift","mergeState","cacheService","server","setFlash","filesUpdated","options","schema","getFormMetadata","services","formsService","max","Math","min","formMetadata","slug","notificationEmail","newUpload","accept"],"sources":["../../../../../src/server/plugins/engine/pageControllers/FileUploadPageController.ts"],"sourcesContent":["import { ComponentType, type PageFileUpload } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { wait } from '@hapi/hoek'\nimport { type ValidationErrorItem } from 'joi'\n\nimport {\n tempItemSchema,\n type FileUploadField\n} from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport {\n getCacheService,\n getError,\n getExponentialBackoffDelay\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'\nimport {\n getUploadStatus,\n initiateUpload\n} from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n FileStatus,\n UploadStatus,\n type AnyFormRequest,\n type FeaturedFormPageViewModel,\n type FileState,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type UploadInitiateResponse,\n type UploadStatusFileResponse\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst MAX_UPLOADS = 25\nconst CDP_UPLOAD_TIMEOUT_MS = 60000 // 1 minute\n\nexport function prepareStatus(status: UploadStatusFileResponse) {\n const file = status.form.file\n const isPending = file.fileStatus === FileStatus.pending\n\n if (!file.errorMessage && isPending) {\n file.errorMessage = 'The selected file has not fully uploaded'\n }\n\n return status\n}\n\nfunction prepareFileState(fileState: FileState) {\n prepareStatus(fileState.status)\n\n return fileState\n}\n\nexport class FileUploadPageController extends QuestionPageController {\n declare pageDef: PageFileUpload\n\n fileUpload: FileUploadField\n fileDeleteViewName = 'item-delete'\n\n constructor(model: FormModel, pageDef: PageFileUpload) {\n super(model, pageDef)\n\n const { collection } = this\n\n // Get the file upload fields from the collection\n const fileUploads = collection.fields.filter(\n (field): field is FileUploadField =>\n field.type === ComponentType.FileUploadField\n )\n\n const fileUpload = fileUploads.at(0)\n\n // Assert we have exactly 1 file upload component\n if (!fileUpload || fileUploads.length > 1) {\n throw Boom.badImplementation(\n `Expected 1 FileUploadFieldComponent in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assert the file upload component is the first form component\n if (collection.fields.indexOf(fileUpload) !== 0) {\n throw Boom.badImplementation(\n `Expected '${fileUpload.name}' to be the first form component in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assign the file upload component to the controller\n this.fileUpload = fileUpload\n this.viewName = 'file-upload'\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { fileUpload } = this\n\n const payload = super.getFormDataFromState(request, state)\n const files = this.getFilesFromState(state)\n\n // Append the files to the payload\n payload[fileUpload.name] = files.length ? files : undefined\n\n return payload\n }\n\n async getState(request: AnyFormRequest) {\n const { fileUpload } = this\n\n // Get the actual state\n const state = await super.getState(request)\n const files = this.getFilesFromState(state)\n\n // Overwrite the files with those in the upload state\n state[fileUpload.name] = files\n\n return this.refreshUpload(request, state)\n }\n\n /**\n * Get the uploaded files from state.\n */\n getFilesFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.files ?? []\n }\n\n /**\n * Get the initiated upload from state.\n */\n getUploadFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.upload\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { params } = request\n const { state } = context\n\n const files = this.getFilesFromState(state)\n\n const fileToRemove = files.find(\n ({ uploadId }) => uploadId === params.itemId\n )\n\n if (!fileToRemove) {\n throw Boom.notFound('File to delete not found')\n }\n\n const { filename } = fileToRemove.status.form.file\n\n return h.view(this.fileDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: `Are you sure you want to remove this file?`,\n itemTitle: filename,\n confirmation: { text: 'You cannot recover removed files.' },\n buttonConfirm: { text: 'Remove file' },\n buttonCancel: { text: 'Cancel' }\n } satisfies ItemDeletePageViewModel)\n }\n }\n\n makePostItemDeleteRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n // Check for any removed files in the POST payload\n if (confirm) {\n await this.checkRemovedFiles(request, state)\n return this.proceed(request, h, path)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getErrors(details?: ValidationErrorItem[]) {\n const { fileUpload } = this\n\n if (details) {\n const errors: FormSubmissionError[] = []\n\n details.forEach((error) => {\n const isUploadError = error.path[0] === fileUpload.name\n const isUploadRootError = isUploadError && error.path.length === 1\n\n if (!isUploadError || isUploadRootError) {\n // The error is for the root of the upload or another\n // field on the page so defer to the getError helper\n errors.push(getError(error))\n } else {\n const { context, path, type } = error\n\n if (type === 'object.unknown' && path.at(-1) === 'errorMessage') {\n const value = context?.value as string | undefined\n\n if (value) {\n const name = fileUpload.name\n const text = typeof value === 'string' ? value : 'Unknown error'\n const href = `#${name}`\n\n errors.push({ path, href, name, text })\n }\n }\n }\n })\n\n return errors\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FeaturedFormPageViewModel {\n const { fileUpload } = this\n const { state } = context\n\n const upload = this.getUploadFromState(state)\n\n const viewModel = super.getViewModel(request, context)\n const { components } = viewModel\n\n // Featured form component\n const [formComponent] = components.filter(\n ({ model }) => model.id === fileUpload.name\n )\n\n const index = components.indexOf(formComponent)\n\n const proxyUrl = getProxyUrlForLocalDevelopment(upload?.uploadUrl)\n\n return {\n ...viewModel,\n formAction: upload?.uploadUrl,\n uploadId: upload?.uploadId,\n formComponent,\n\n // Split out components before/after\n componentsBefore: components.slice(0, index),\n components: components.slice(index),\n proxyUrl\n }\n }\n\n /**\n * Refreshes the CDP upload and files in the\n * state and checks for any removed files.\n *\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise we initiate a new one.\n * @param request - the hapi request\n * @param state - the form state\n */\n private async refreshUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n state = await this.checkUploadStatus(request, state)\n\n return state\n }\n\n /**\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise a new one is initiated.\n * @param request - the hapi request\n * @param state - the form state\n * @param depth - the number of retries so far\n */\n private async checkUploadStatus(\n request: AnyFormRequest,\n state: FormSubmissionState,\n depth = 1\n ): Promise<FormSubmissionState> {\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n // If no upload exists, initiate a new one.\n if (!upload?.uploadId) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const uploadId = upload.uploadId\n const statusResponse = await getUploadStatus(uploadId)\n if (!statusResponse) {\n throw Boom.badRequest(\n `Unexpected empty response from getUploadStatus for ${uploadId}`\n )\n }\n\n // Re-use the upload if it is still in the \"initiated\" state.\n if (statusResponse.uploadStatus === UploadStatus.initiated) {\n return state\n }\n\n if (statusResponse.uploadStatus === UploadStatus.pending) {\n // Using exponential backoff delays:\n // Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)\n // A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.\n if (depth >= 5) {\n const err = new Error(\n `Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`\n )\n request.logger.error(\n err,\n `[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`\n )\n await this.initiateAndStoreNewUpload(request, state)\n throw Boom.gatewayTimeout(\n `Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`\n )\n }\n const delay = getExponentialBackoffDelay(depth)\n request.logger.info(\n `[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`\n )\n await wait(delay)\n return this.checkUploadStatus(request, state, depth + 1)\n }\n\n // Only add to files state if the file validates.\n // This secures against html tampering of the file input\n // by adding a 'multiple' attribute or it being\n // changed to a simple text field or similar.\n const validationResult = tempItemSchema.validate(\n { uploadId, status: statusResponse },\n { stripUnknown: true }\n )\n const error = validationResult.error\n const fileState = validationResult.value as FileState\n\n if (error) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const file = fileState.status.form.file\n if (file.fileStatus === FileStatus.complete) {\n files.unshift(prepareFileState(fileState))\n await this.mergeState(request, state, {\n upload: { [this.path]: { files, upload } }\n })\n } else {\n // Flash the error message.\n const { fileUpload } = this\n const cacheService = getCacheService(request.server)\n\n const name = fileUpload.name\n const text = file.errorMessage ?? 'Unknown error'\n const errors: FormSubmissionError[] = [\n { path: [name], href: `#${name}`, name, text }\n ]\n cacheService.setFlash(request, { errors })\n }\n\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n /**\n * Checks the payload for a file getting removed\n * and removes it from the upload files if found\n * @param request - the hapi request\n * @param state - the form state\n * @returns updated state if any files have been removed\n */\n private async checkRemovedFiles(\n request: FormRequestPayload,\n state: FormSubmissionState\n ) {\n const { path } = this\n const { params } = request\n\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n const filesUpdated = files.filter(\n ({ uploadId }) => uploadId !== params.itemId\n )\n\n if (filesUpdated.length === files.length) {\n return\n }\n\n await this.mergeState(request, state, {\n upload: { [path]: { files: filesUpdated, upload } }\n })\n }\n\n /**\n * Initiates a CDP file upload and stores in the upload state\n * @param request - the hapi request\n * @param state - the form state\n */\n private async initiateAndStoreNewUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n const { fileUpload, href, path } = this\n const { options, schema } = fileUpload\n const { getFormMetadata } = this.model.services.formsService\n\n const files = this.getFilesFromState(state)\n\n // Reset the upload in state\n let upload: UploadInitiateResponse | undefined\n\n // Don't initiate anymore after minimum of `schema.max` or MAX_UPLOADS\n const max = Math.min(schema.max ?? MAX_UPLOADS, MAX_UPLOADS)\n\n if (files.length < max) {\n const formMetadata = await getFormMetadata(request.params.slug)\n const notificationEmail =\n formMetadata.notificationEmail ?? 'defraforms@defra.gov.uk'\n\n const newUpload = await initiateUpload(\n href,\n notificationEmail,\n options.accept\n )\n\n if (newUpload === undefined) {\n throw Boom.badRequest('Unexpected empty response from initiateUpload')\n }\n\n upload = newUpload\n }\n\n return this.mergeState(request, state, {\n upload: { [path]: { files, upload } }\n })\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"FileUploadPageController.js","names":["ComponentType","Boom","wait","FileUploadField","tempItemSchema","getCacheService","getError","getExponentialBackoffDelay","QuestionPageController","getProxyUrlForLocalDevelopment","getUploadStatus","initiateUpload","FileStatus","UploadStatus","MAX_UPLOADS","CDP_UPLOAD_TIMEOUT_MS","prepareStatus","status","file","form","isPending","fileStatus","pending","errorMessage","prepareFileState","fileState","FileUploadPageController","fileUpload","fileDeleteViewName","constructor","model","pageDef","collection","fileUploads","fields","filter","field","type","at","length","badImplementation","path","indexOf","name","viewName","getStateKeys","component","pagePath","page","getFormDataFromState","request","state","payload","files","getFilesFromState","undefined","getState","refreshUpload","uploadState","upload","getUploadFromState","makeGetItemDeleteRouteHandler","context","h","viewModel","params","fileToRemove","find","uploadId","itemId","notFound","filename","view","backLink","getBackLink","pageTitle","itemTitle","confirmation","text","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","getFormParams","checkRemovedFiles","proceed","getErrors","details","errors","forEach","error","isUploadError","isUploadRootError","push","value","href","getViewModel","components","formComponent","id","index","proxyUrl","uploadUrl","formAction","componentsBefore","slice","checkUploadStatus","depth","initiateAndStoreNewUpload","statusResponse","badRequest","uploadStatus","initiated","err","Error","logger","gatewayTimeout","toFixed","delay","info","validationResult","validate","stripUnknown","complete","unshift","mergeState","cacheService","server","setFlash","filesUpdated","options","schema","getFormMetadata","services","formsService","max","Math","min","formMetadata","slug","notificationEmail","newUpload","accept"],"sources":["../../../../../src/server/plugins/engine/pageControllers/FileUploadPageController.ts"],"sourcesContent":["import { ComponentType, type PageFileUpload } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { wait } from '@hapi/hoek'\nimport { type ValidationErrorItem } from 'joi'\n\nimport {\n FileUploadField,\n tempItemSchema\n} from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getCacheService,\n getError,\n getExponentialBackoffDelay\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'\nimport {\n getUploadStatus,\n initiateUpload\n} from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n FileStatus,\n UploadStatus,\n type AnyFormRequest,\n type FeaturedFormPageViewModel,\n type FileState,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type UploadInitiateResponse,\n type UploadStatusFileResponse\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst MAX_UPLOADS = 25\nconst CDP_UPLOAD_TIMEOUT_MS = 60000 // 1 minute\n\nexport function prepareStatus(status: UploadStatusFileResponse) {\n const file = status.form.file\n const isPending = file.fileStatus === FileStatus.pending\n\n if (!file.errorMessage && isPending) {\n file.errorMessage = 'The selected file has not fully uploaded'\n }\n\n return status\n}\n\nfunction prepareFileState(fileState: FileState) {\n prepareStatus(fileState.status)\n\n return fileState\n}\n\nexport class FileUploadPageController extends QuestionPageController {\n declare pageDef: PageFileUpload\n\n fileUpload: FileUploadField\n fileDeleteViewName = 'item-delete'\n\n constructor(model: FormModel, pageDef: PageFileUpload) {\n super(model, pageDef)\n\n const { collection } = this\n\n // Get the file upload fields from the collection\n const fileUploads = collection.fields.filter(\n (field): field is FileUploadField =>\n field.type === ComponentType.FileUploadField\n )\n\n const fileUpload = fileUploads.at(0)\n\n // Assert we have exactly 1 file upload component\n if (!fileUpload || fileUploads.length > 1) {\n throw Boom.badImplementation(\n `Expected 1 FileUploadFieldComponent in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assert the file upload component is the first form component\n if (collection.fields.indexOf(fileUpload) !== 0) {\n throw Boom.badImplementation(\n `Expected '${fileUpload.name}' to be the first form component in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assign the file upload component to the controller\n this.fileUpload = fileUpload\n this.viewName = 'file-upload'\n }\n\n /**\n * Get supplementary state keys for clearing file upload state.\n * Returns the nested upload path for FileUploadField components only.\n * @param component - The component to get supplementary state keys for\n * @returns Array containing the nested upload path, e.g., [\"upload['/page-path']\"]\n * or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.\n */\n getStateKeys(component: FormComponent): string[] {\n // Only return upload keys for FileUploadField components\n if (!(component instanceof FileUploadField)) {\n return []\n }\n\n const pagePath = component.page?.path\n return pagePath ? [`upload['${pagePath}']`] : ['upload']\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { fileUpload } = this\n\n const payload = super.getFormDataFromState(request, state)\n const files = this.getFilesFromState(state)\n\n // Append the files to the payload\n payload[fileUpload.name] = files.length ? files : undefined\n\n return payload\n }\n\n async getState(request: AnyFormRequest) {\n const { fileUpload } = this\n\n // Get the actual state\n const state = await super.getState(request)\n const files = this.getFilesFromState(state)\n\n // Overwrite the files with those in the upload state\n state[fileUpload.name] = files\n\n return this.refreshUpload(request, state)\n }\n\n /**\n * Get the uploaded files from state.\n */\n getFilesFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.files ?? []\n }\n\n /**\n * Get the initiated upload from state.\n */\n getUploadFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.upload\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { params } = request\n const { state } = context\n\n const files = this.getFilesFromState(state)\n\n const fileToRemove = files.find(\n ({ uploadId }) => uploadId === params.itemId\n )\n\n if (!fileToRemove) {\n throw Boom.notFound('File to delete not found')\n }\n\n const { filename } = fileToRemove.status.form.file\n\n return h.view(this.fileDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: `Are you sure you want to remove this file?`,\n itemTitle: filename,\n confirmation: { text: 'You cannot recover removed files.' },\n buttonConfirm: { text: 'Remove file' },\n buttonCancel: { text: 'Cancel' }\n } satisfies ItemDeletePageViewModel)\n }\n }\n\n makePostItemDeleteRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n // Check for any removed files in the POST payload\n if (confirm) {\n await this.checkRemovedFiles(request, state)\n return this.proceed(request, h, path)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getErrors(details?: ValidationErrorItem[]) {\n const { fileUpload } = this\n\n if (details) {\n const errors: FormSubmissionError[] = []\n\n details.forEach((error) => {\n const isUploadError = error.path[0] === fileUpload.name\n const isUploadRootError = isUploadError && error.path.length === 1\n\n if (!isUploadError || isUploadRootError) {\n // The error is for the root of the upload or another\n // field on the page so defer to the getError helper\n errors.push(getError(error))\n } else {\n const { context, path, type } = error\n\n if (type === 'object.unknown' && path.at(-1) === 'errorMessage') {\n const value = context?.value as string | undefined\n\n if (value) {\n const name = fileUpload.name\n const text = typeof value === 'string' ? value : 'Unknown error'\n const href = `#${name}`\n\n errors.push({ path, href, name, text })\n }\n }\n }\n })\n\n return errors\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FeaturedFormPageViewModel {\n const { fileUpload } = this\n const { state } = context\n\n const upload = this.getUploadFromState(state)\n\n const viewModel = super.getViewModel(request, context)\n const { components } = viewModel\n\n // Featured form component\n const [formComponent] = components.filter(\n ({ model }) => model.id === fileUpload.name\n )\n\n const index = components.indexOf(formComponent)\n\n const proxyUrl = getProxyUrlForLocalDevelopment(upload?.uploadUrl)\n\n return {\n ...viewModel,\n formAction: upload?.uploadUrl,\n uploadId: upload?.uploadId,\n formComponent,\n\n // Split out components before/after\n componentsBefore: components.slice(0, index),\n components: components.slice(index),\n proxyUrl\n }\n }\n\n /**\n * Refreshes the CDP upload and files in the\n * state and checks for any removed files.\n *\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise we initiate a new one.\n * @param request - the hapi request\n * @param state - the form state\n */\n private async refreshUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n state = await this.checkUploadStatus(request, state)\n\n return state\n }\n\n /**\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise a new one is initiated.\n * @param request - the hapi request\n * @param state - the form state\n * @param depth - the number of retries so far\n */\n private async checkUploadStatus(\n request: AnyFormRequest,\n state: FormSubmissionState,\n depth = 1\n ): Promise<FormSubmissionState> {\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n // If no upload exists, initiate a new one.\n if (!upload?.uploadId) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const uploadId = upload.uploadId\n const statusResponse = await getUploadStatus(uploadId)\n if (!statusResponse) {\n throw Boom.badRequest(\n `Unexpected empty response from getUploadStatus for ${uploadId}`\n )\n }\n\n // Re-use the upload if it is still in the \"initiated\" state.\n if (statusResponse.uploadStatus === UploadStatus.initiated) {\n return state\n }\n\n if (statusResponse.uploadStatus === UploadStatus.pending) {\n // Using exponential backoff delays:\n // Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)\n // A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.\n if (depth >= 5) {\n const err = new Error(\n `Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`\n )\n request.logger.error(\n err,\n `[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`\n )\n await this.initiateAndStoreNewUpload(request, state)\n throw Boom.gatewayTimeout(\n `Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`\n )\n }\n const delay = getExponentialBackoffDelay(depth)\n request.logger.info(\n `[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`\n )\n await wait(delay)\n return this.checkUploadStatus(request, state, depth + 1)\n }\n\n // Only add to files state if the file validates.\n // This secures against html tampering of the file input\n // by adding a 'multiple' attribute or it being\n // changed to a simple text field or similar.\n const validationResult = tempItemSchema.validate(\n { uploadId, status: statusResponse },\n { stripUnknown: true }\n )\n const error = validationResult.error\n const fileState = validationResult.value as FileState\n\n if (error) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const file = fileState.status.form.file\n if (file.fileStatus === FileStatus.complete) {\n files.unshift(prepareFileState(fileState))\n await this.mergeState(request, state, {\n upload: { [this.path]: { files, upload } }\n })\n } else {\n // Flash the error message.\n const { fileUpload } = this\n const cacheService = getCacheService(request.server)\n\n const name = fileUpload.name\n const text = file.errorMessage ?? 'Unknown error'\n const errors: FormSubmissionError[] = [\n { path: [name], href: `#${name}`, name, text }\n ]\n cacheService.setFlash(request, { errors })\n }\n\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n /**\n * Checks the payload for a file getting removed\n * and removes it from the upload files if found\n * @param request - the hapi request\n * @param state - the form state\n * @returns updated state if any files have been removed\n */\n private async checkRemovedFiles(\n request: FormRequestPayload,\n state: FormSubmissionState\n ) {\n const { path } = this\n const { params } = request\n\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n const filesUpdated = files.filter(\n ({ uploadId }) => uploadId !== params.itemId\n )\n\n if (filesUpdated.length === files.length) {\n return\n }\n\n await this.mergeState(request, state, {\n upload: { [path]: { files: filesUpdated, upload } }\n })\n }\n\n /**\n * Initiates a CDP file upload and stores in the upload state\n * @param request - the hapi request\n * @param state - the form state\n */\n private async initiateAndStoreNewUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n const { fileUpload, href, path } = this\n const { options, schema } = fileUpload\n const { getFormMetadata } = this.model.services.formsService\n\n const files = this.getFilesFromState(state)\n\n // Reset the upload in state\n let upload: UploadInitiateResponse | undefined\n\n // Don't initiate anymore after minimum of `schema.max` or MAX_UPLOADS\n const max = Math.min(schema.max ?? MAX_UPLOADS, MAX_UPLOADS)\n\n if (files.length < max) {\n const formMetadata = await getFormMetadata(request.params.slug)\n const notificationEmail =\n formMetadata.notificationEmail ?? 'defraforms@defra.gov.uk'\n\n const newUpload = await initiateUpload(\n href,\n notificationEmail,\n options.accept\n )\n\n if (newUpload === undefined) {\n throw Boom.badRequest('Unexpected empty response from initiateUpload')\n }\n\n upload = newUpload\n }\n\n return this.mergeState(request, state, {\n upload: { [path]: { files, upload } }\n })\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Events, type FormDefinition, type Page, type Section } from '@defra/forms-model';
|
|
2
2
|
import { type Lifecycle, type RouteOptions, type Server } from '@hapi/hapi';
|
|
3
3
|
import { type ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js';
|
|
4
|
+
import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js';
|
|
4
5
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js';
|
|
5
6
|
import { type ExecutableCondition } from '~/src/server/plugins/engine/models/types.js';
|
|
6
7
|
import { type FormContext, type PageViewModelBase } from '~/src/server/plugins/engine/types.js';
|
|
@@ -41,5 +42,18 @@ export declare class PageController {
|
|
|
41
42
|
getStatusPath(): string;
|
|
42
43
|
makeGetRouteHandler(): (request: FormRequest, context: FormContext, h: FormResponseToolkit) => ReturnType<Lifecycle.Method<FormRequestRefs>>;
|
|
43
44
|
makePostRouteHandler(): (request: FormRequestPayload, context: FormContext, h: FormResponseToolkit) => ReturnType<Lifecycle.Method<FormRequestPayloadRefs>>;
|
|
45
|
+
/**
|
|
46
|
+
* Get supplementary state keys for clearing component state.
|
|
47
|
+
*
|
|
48
|
+
* This method returns page controller-level state keys only. The core component's
|
|
49
|
+
* state key (the component's name) is managed separately by the framework and should
|
|
50
|
+
* NOT be included in the returned array.
|
|
51
|
+
*
|
|
52
|
+
* Returns an empty array by default. Override in subclasses to provide
|
|
53
|
+
* page-specific supplementary state keys (e.g., upload state, cached data).
|
|
54
|
+
* @param _component - The component to get supplementary state keys for (optional)
|
|
55
|
+
* @returns Array of supplementary state keys to clear (excluding the component name itself)
|
|
56
|
+
*/
|
|
57
|
+
getStateKeys(_component?: FormComponent): string[];
|
|
44
58
|
shouldShowSaveAndExit(server: Server): boolean;
|
|
45
59
|
}
|
|
@@ -134,6 +134,22 @@ export class PageController {
|
|
|
134
134
|
makePostRouteHandler() {
|
|
135
135
|
throw Boom.badRequest('Unsupported POST route handler for this page');
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get supplementary state keys for clearing component state.
|
|
140
|
+
*
|
|
141
|
+
* This method returns page controller-level state keys only. The core component's
|
|
142
|
+
* state key (the component's name) is managed separately by the framework and should
|
|
143
|
+
* NOT be included in the returned array.
|
|
144
|
+
*
|
|
145
|
+
* Returns an empty array by default. Override in subclasses to provide
|
|
146
|
+
* page-specific supplementary state keys (e.g., upload state, cached data).
|
|
147
|
+
* @param _component - The component to get supplementary state keys for (optional)
|
|
148
|
+
* @returns Array of supplementary state keys to clear (excluding the component name itself)
|
|
149
|
+
*/
|
|
150
|
+
getStateKeys(_component) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
137
153
|
shouldShowSaveAndExit(server) {
|
|
138
154
|
return getSaveAndExitHelpers(server) !== undefined && this.allowSaveAndExit;
|
|
139
155
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageController.js","names":["ControllerPath","Boom","getSaveAndExitHelpers","getStartPath","normalisePath","PageController","def","name","model","pageDef","title","section","condition","events","collection","viewName","allowSaveAndExit","constructor","getSection","conditions","view","path","href","getHref","keys","getRouteOptions","postRouteOptions","viewModel","showTitle","pageTitle","sectionTitle","hideTitle","page","isStartPage","serviceUrl","feedbackLink","phaseTag","formId","phaseBanner","phase","basePath","relativeTargetPath","startsWith","substring","finalPath","replace","getSummaryPath","Summary","valueOf","getStatusPath","Status","makeGetRouteHandler","request","context","h","makePostRouteHandler","badRequest","shouldShowSaveAndExit","server","undefined"],"sources":["../../../../../src/server/plugins/engine/pageControllers/PageController.ts"],"sourcesContent":["import {\n ControllerPath,\n type Events,\n type FormDefinition,\n type Page,\n type Section\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type Lifecycle, type RouteOptions, type Server } from '@hapi/hapi'\n\nimport { type ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport {\n getSaveAndExitHelpers,\n getStartPath,\n normalisePath\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type ExecutableCondition } from '~/src/server/plugins/engine/models/types.js'\nimport {\n type FormContext,\n type PageViewModelBase\n} 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'\n\nexport class PageController {\n /**\n * The base class for all page controllers. Page controllers are responsible for generating the get and post route handlers when a user navigates to `/{id}/{path*}`.\n */\n def: FormDefinition\n name?: string\n model: FormModel\n pageDef: Page\n title: string\n section?: Section\n condition?: ExecutableCondition\n events?: Events\n collection?: ComponentCollection\n viewName = 'index'\n allowSaveAndExit = false\n\n constructor(model: FormModel, pageDef: Page) {\n const { def } = model\n\n this.def = def\n this.name = def.name\n this.model = model\n this.pageDef = pageDef\n this.title = pageDef.title\n this.events = pageDef.events\n\n // Resolve section\n if (pageDef.section) {\n this.section = model.getSection(pageDef.section)\n }\n\n // Resolve condition\n if (pageDef.condition) {\n this.condition = model.conditions[pageDef.condition]\n }\n\n // Override view name\n if (pageDef.view) {\n this.viewName = pageDef.view\n }\n }\n\n get path() {\n return this.pageDef.path\n }\n\n get href() {\n const { path } = this\n return this.getHref(`/${normalisePath(path)}`)\n }\n\n get keys() {\n return this.collection?.keys ?? []\n }\n\n /**\n * {@link https://hapi.dev/api/?v=20.1.2#route-options}\n */\n get getRouteOptions(): RouteOptions<FormRequestRefs> {\n return {}\n }\n\n /**\n * {@link https://hapi.dev/api/?v=20.1.2#route-options}\n */\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {}\n }\n\n get viewModel(): PageViewModelBase {\n const { name, section, title } = this\n\n const showTitle = true\n const pageTitle = title\n const sectionTitle = section?.hideTitle !== true ? section?.title : ''\n\n return {\n name,\n page: this,\n pageTitle,\n sectionTitle,\n showTitle,\n isStartPage: false,\n serviceUrl: this.getHref('/'),\n feedbackLink: this.feedbackLink,\n phaseTag: this.phaseTag\n }\n }\n\n get feedbackLink() {\n return `/form/feedback?formId=${this.model.formId}`\n }\n\n get phaseTag() {\n const { def } = this\n return def.phaseBanner?.phase\n }\n\n getHref(path: string): string {\n const basePath = this.model.basePath\n\n if (path === '/') {\n return `/${basePath}`\n }\n\n // if ever the path is not prefixed with a slash, add it\n const relativeTargetPath = path.startsWith('/') ? path.substring(1) : path\n let finalPath = `/${basePath}`\n if (relativeTargetPath) {\n finalPath += `/${relativeTargetPath}`\n }\n finalPath = finalPath.replace(/\\/{2,}/g, '/')\n\n return finalPath\n }\n\n getStartPath() {\n return getStartPath(this.model)\n }\n\n getSummaryPath() {\n return ControllerPath.Summary.valueOf()\n }\n\n getStatusPath() {\n return ControllerPath.Status.valueOf()\n }\n\n makeGetRouteHandler(): (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => ReturnType<Lifecycle.Method<FormRequestRefs>> {\n return (request, context, h) => {\n const { viewModel, viewName } = this\n return h.view(viewName, viewModel)\n }\n }\n\n makePostRouteHandler(): (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => ReturnType<Lifecycle.Method<FormRequestPayloadRefs>> {\n throw Boom.badRequest('Unsupported POST route handler for this page')\n }\n\n shouldShowSaveAndExit(server: Server): boolean {\n return getSaveAndExitHelpers(server) !== undefined && this.allowSaveAndExit\n }\n}\n"],"mappings":"AAAA,SACEA,cAAc,QAKT,oBAAoB;AAC3B,OAAOC,IAAI,MAAM,YAAY;
|
|
1
|
+
{"version":3,"file":"PageController.js","names":["ControllerPath","Boom","getSaveAndExitHelpers","getStartPath","normalisePath","PageController","def","name","model","pageDef","title","section","condition","events","collection","viewName","allowSaveAndExit","constructor","getSection","conditions","view","path","href","getHref","keys","getRouteOptions","postRouteOptions","viewModel","showTitle","pageTitle","sectionTitle","hideTitle","page","isStartPage","serviceUrl","feedbackLink","phaseTag","formId","phaseBanner","phase","basePath","relativeTargetPath","startsWith","substring","finalPath","replace","getSummaryPath","Summary","valueOf","getStatusPath","Status","makeGetRouteHandler","request","context","h","makePostRouteHandler","badRequest","getStateKeys","_component","shouldShowSaveAndExit","server","undefined"],"sources":["../../../../../src/server/plugins/engine/pageControllers/PageController.ts"],"sourcesContent":["import {\n ControllerPath,\n type Events,\n type FormDefinition,\n type Page,\n type Section\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type Lifecycle, type RouteOptions, type Server } from '@hapi/hapi'\n\nimport { type ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getSaveAndExitHelpers,\n getStartPath,\n normalisePath\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type ExecutableCondition } from '~/src/server/plugins/engine/models/types.js'\nimport {\n type FormContext,\n type PageViewModelBase\n} 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'\n\nexport class PageController {\n /**\n * The base class for all page controllers. Page controllers are responsible for generating the get and post route handlers when a user navigates to `/{id}/{path*}`.\n */\n def: FormDefinition\n name?: string\n model: FormModel\n pageDef: Page\n title: string\n section?: Section\n condition?: ExecutableCondition\n events?: Events\n collection?: ComponentCollection\n viewName = 'index'\n allowSaveAndExit = false\n\n constructor(model: FormModel, pageDef: Page) {\n const { def } = model\n\n this.def = def\n this.name = def.name\n this.model = model\n this.pageDef = pageDef\n this.title = pageDef.title\n this.events = pageDef.events\n\n // Resolve section\n if (pageDef.section) {\n this.section = model.getSection(pageDef.section)\n }\n\n // Resolve condition\n if (pageDef.condition) {\n this.condition = model.conditions[pageDef.condition]\n }\n\n // Override view name\n if (pageDef.view) {\n this.viewName = pageDef.view\n }\n }\n\n get path() {\n return this.pageDef.path\n }\n\n get href() {\n const { path } = this\n return this.getHref(`/${normalisePath(path)}`)\n }\n\n get keys() {\n return this.collection?.keys ?? []\n }\n\n /**\n * {@link https://hapi.dev/api/?v=20.1.2#route-options}\n */\n get getRouteOptions(): RouteOptions<FormRequestRefs> {\n return {}\n }\n\n /**\n * {@link https://hapi.dev/api/?v=20.1.2#route-options}\n */\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {}\n }\n\n get viewModel(): PageViewModelBase {\n const { name, section, title } = this\n\n const showTitle = true\n const pageTitle = title\n const sectionTitle = section?.hideTitle !== true ? section?.title : ''\n\n return {\n name,\n page: this,\n pageTitle,\n sectionTitle,\n showTitle,\n isStartPage: false,\n serviceUrl: this.getHref('/'),\n feedbackLink: this.feedbackLink,\n phaseTag: this.phaseTag\n }\n }\n\n get feedbackLink() {\n return `/form/feedback?formId=${this.model.formId}`\n }\n\n get phaseTag() {\n const { def } = this\n return def.phaseBanner?.phase\n }\n\n getHref(path: string): string {\n const basePath = this.model.basePath\n\n if (path === '/') {\n return `/${basePath}`\n }\n\n // if ever the path is not prefixed with a slash, add it\n const relativeTargetPath = path.startsWith('/') ? path.substring(1) : path\n let finalPath = `/${basePath}`\n if (relativeTargetPath) {\n finalPath += `/${relativeTargetPath}`\n }\n finalPath = finalPath.replace(/\\/{2,}/g, '/')\n\n return finalPath\n }\n\n getStartPath() {\n return getStartPath(this.model)\n }\n\n getSummaryPath() {\n return ControllerPath.Summary.valueOf()\n }\n\n getStatusPath() {\n return ControllerPath.Status.valueOf()\n }\n\n makeGetRouteHandler(): (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => ReturnType<Lifecycle.Method<FormRequestRefs>> {\n return (request, context, h) => {\n const { viewModel, viewName } = this\n return h.view(viewName, viewModel)\n }\n }\n\n makePostRouteHandler(): (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => ReturnType<Lifecycle.Method<FormRequestPayloadRefs>> {\n throw Boom.badRequest('Unsupported POST route handler for this page')\n }\n\n /**\n * Get supplementary state keys for clearing component state.\n *\n * This method returns page controller-level state keys only. The core component's\n * state key (the component's name) is managed separately by the framework and should\n * NOT be included in the returned array.\n *\n * Returns an empty array by default. Override in subclasses to provide\n * page-specific supplementary state keys (e.g., upload state, cached data).\n * @param _component - The component to get supplementary state keys for (optional)\n * @returns Array of supplementary state keys to clear (excluding the component name itself)\n */\n getStateKeys(_component?: FormComponent): string[] {\n return []\n }\n\n shouldShowSaveAndExit(server: Server): boolean {\n return getSaveAndExitHelpers(server) !== undefined && this.allowSaveAndExit\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -6,6 +6,7 @@ import { type FormRequest, type FormResponseToolkit } from '~/src/server/routes/
|
|
|
6
6
|
export declare class StatusPageController extends QuestionPageController {
|
|
7
7
|
pageDef: PageStatus;
|
|
8
8
|
allowSaveAndExit: boolean;
|
|
9
|
+
showReferenceNumber: boolean;
|
|
9
10
|
constructor(model: FormModel, pageDef: PageStatus);
|
|
10
11
|
getRelevantPath(): string;
|
|
11
12
|
makeGetRouteHandler(): (request: FormRequest, context: FormContext, h: FormResponseToolkit) => Promise<import("@hapi/hapi").ResponseObject>;
|
|
@@ -2,9 +2,11 @@ import { getCacheService } from "../helpers.js";
|
|
|
2
2
|
import { QuestionPageController } from "./QuestionPageController.js";
|
|
3
3
|
export class StatusPageController extends QuestionPageController {
|
|
4
4
|
allowSaveAndExit = false;
|
|
5
|
+
showReferenceNumber = false;
|
|
5
6
|
constructor(model, pageDef) {
|
|
6
7
|
super(model, pageDef);
|
|
7
8
|
this.viewName = 'confirmation';
|
|
9
|
+
this.showReferenceNumber = model.def.options?.showReferenceNumber ?? false;
|
|
8
10
|
}
|
|
9
11
|
getRelevantPath() {
|
|
10
12
|
return this.getStatusPath();
|
|
@@ -41,7 +43,9 @@ export class StatusPageController extends QuestionPageController {
|
|
|
41
43
|
return h.view(viewName, {
|
|
42
44
|
...viewModel,
|
|
43
45
|
submissionGuidance,
|
|
44
|
-
formName
|
|
46
|
+
formName,
|
|
47
|
+
showReferenceNumber: this.showReferenceNumber,
|
|
48
|
+
referenceNumber: context.referenceNumber
|
|
45
49
|
});
|
|
46
50
|
};
|
|
47
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatusPageController.js","names":["getCacheService","QuestionPageController","StatusPageController","allowSaveAndExit","constructor","model","pageDef","viewName","getRelevantPath","getStatusPath","makeGetRouteHandler","request","context","h","viewModel","cacheService","server","confirmationState","getConfirmationState","confirmed","proceed","getStartPath","slug","params","formsService","services","getFormMetadata","getFormMetadataById","submissionGuidance","storedFormId","formId","formName","title","undefined","view"],"sources":["../../../../../src/server/plugins/engine/pageControllers/StatusPageController.ts"],"sourcesContent":["import { type PageStatus } from '@defra/forms-model'\n\nimport { getCacheService } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class StatusPageController extends QuestionPageController {\n declare pageDef: PageStatus\n allowSaveAndExit = false\n\n constructor(model: FormModel, pageDef: PageStatus) {\n super(model, pageDef)\n this.viewName = 'confirmation'\n }\n\n getRelevantPath() {\n return this.getStatusPath()\n }\n\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel, viewName } = this\n\n const cacheService = getCacheService(request.server)\n const confirmationState = await cacheService.getConfirmationState(request)\n\n // If there's no confirmation state, then\n // redirect the user back to the start of the form\n if (!confirmationState.confirmed) {\n return this.proceed(request, h, this.getStartPath())\n }\n\n const slug = request.params.slug\n const { formsService } = this.model.services\n const { getFormMetadata, getFormMetadataById } = formsService\n\n const { submissionGuidance } = await getFormMetadata(slug)\n\n // Re-read form name if overriding display (for example, in a feedback form)\n const storedFormId = confirmationState.formId\n const formName = storedFormId\n ? (await getFormMetadataById(storedFormId)).title\n : undefined\n\n return h.view(viewName, {\n ...viewModel,\n submissionGuidance,\n formName\n })\n }\n }\n}\n"],"mappings":"AAEA,SAASA,eAAe;AAExB,SAASC,sBAAsB;AAO/B,OAAO,MAAMC,oBAAoB,SAASD,sBAAsB,CAAC;EAE/DE,gBAAgB,GAAG,KAAK;
|
|
1
|
+
{"version":3,"file":"StatusPageController.js","names":["getCacheService","QuestionPageController","StatusPageController","allowSaveAndExit","showReferenceNumber","constructor","model","pageDef","viewName","def","options","getRelevantPath","getStatusPath","makeGetRouteHandler","request","context","h","viewModel","cacheService","server","confirmationState","getConfirmationState","confirmed","proceed","getStartPath","slug","params","formsService","services","getFormMetadata","getFormMetadataById","submissionGuidance","storedFormId","formId","formName","title","undefined","view","referenceNumber"],"sources":["../../../../../src/server/plugins/engine/pageControllers/StatusPageController.ts"],"sourcesContent":["import { type PageStatus } from '@defra/forms-model'\n\nimport { getCacheService } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class StatusPageController extends QuestionPageController {\n declare pageDef: PageStatus\n allowSaveAndExit = false\n showReferenceNumber = false\n\n constructor(model: FormModel, pageDef: PageStatus) {\n super(model, pageDef)\n this.viewName = 'confirmation'\n this.showReferenceNumber = model.def.options?.showReferenceNumber ?? false\n }\n\n getRelevantPath() {\n return this.getStatusPath()\n }\n\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel, viewName } = this\n\n const cacheService = getCacheService(request.server)\n const confirmationState = await cacheService.getConfirmationState(request)\n\n // If there's no confirmation state, then\n // redirect the user back to the start of the form\n if (!confirmationState.confirmed) {\n return this.proceed(request, h, this.getStartPath())\n }\n\n const slug = request.params.slug\n const { formsService } = this.model.services\n const { getFormMetadata, getFormMetadataById } = formsService\n\n const { submissionGuidance } = await getFormMetadata(slug)\n\n // Re-read form name if overriding display (for example, in a feedback form)\n const storedFormId = confirmationState.formId\n const formName = storedFormId\n ? (await getFormMetadataById(storedFormId)).title\n : undefined\n\n return h.view(viewName, {\n ...viewModel,\n submissionGuidance,\n formName,\n showReferenceNumber: this.showReferenceNumber,\n referenceNumber: context.referenceNumber\n })\n }\n }\n}\n"],"mappings":"AAEA,SAASA,eAAe;AAExB,SAASC,sBAAsB;AAO/B,OAAO,MAAMC,oBAAoB,SAASD,sBAAsB,CAAC;EAE/DE,gBAAgB,GAAG,KAAK;EACxBC,mBAAmB,GAAG,KAAK;EAE3BC,WAAWA,CAACC,KAAgB,EAAEC,OAAmB,EAAE;IACjD,KAAK,CAACD,KAAK,EAAEC,OAAO,CAAC;IACrB,IAAI,CAACC,QAAQ,GAAG,cAAc;IAC9B,IAAI,CAACJ,mBAAmB,GAAGE,KAAK,CAACG,GAAG,CAACC,OAAO,EAAEN,mBAAmB,IAAI,KAAK;EAC5E;EAEAO,eAAeA,CAAA,EAAG;IAChB,OAAO,IAAI,CAACC,aAAa,CAAC,CAAC;EAC7B;EAEAC,mBAAmBA,CAAA,EAAG;IACpB,OAAO,OACLC,OAAoB,EACpBC,OAAoB,EACpBC,CAAsB,KACnB;MACH,MAAM;QAAEC,SAAS;QAAET;MAAS,CAAC,GAAG,IAAI;MAEpC,MAAMU,YAAY,GAAGlB,eAAe,CAACc,OAAO,CAACK,MAAM,CAAC;MACpD,MAAMC,iBAAiB,GAAG,MAAMF,YAAY,CAACG,oBAAoB,CAACP,OAAO,CAAC;;MAE1E;MACA;MACA,IAAI,CAACM,iBAAiB,CAACE,SAAS,EAAE;QAChC,OAAO,IAAI,CAACC,OAAO,CAACT,OAAO,EAAEE,CAAC,EAAE,IAAI,CAACQ,YAAY,CAAC,CAAC,CAAC;MACtD;MAEA,MAAMC,IAAI,GAAGX,OAAO,CAACY,MAAM,CAACD,IAAI;MAChC,MAAM;QAAEE;MAAa,CAAC,GAAG,IAAI,CAACrB,KAAK,CAACsB,QAAQ;MAC5C,MAAM;QAAEC,eAAe;QAAEC;MAAoB,CAAC,GAAGH,YAAY;MAE7D,MAAM;QAAEI;MAAmB,CAAC,GAAG,MAAMF,eAAe,CAACJ,IAAI,CAAC;;MAE1D;MACA,MAAMO,YAAY,GAAGZ,iBAAiB,CAACa,MAAM;MAC7C,MAAMC,QAAQ,GAAGF,YAAY,GACzB,CAAC,MAAMF,mBAAmB,CAACE,YAAY,CAAC,EAAEG,KAAK,GAC/CC,SAAS;MAEb,OAAOpB,CAAC,CAACqB,IAAI,CAAC7B,QAAQ,EAAE;QACtB,GAAGS,SAAS;QACZc,kBAAkB;QAClBG,QAAQ;QACR9B,mBAAmB,EAAE,IAAI,CAACA,mBAAmB;QAC7CkC,eAAe,EAAEvB,OAAO,CAACuB;MAC3B,CAAC,CAAC;IACJ,CAAC;EACH;AACF","ignoreList":[]}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { FileUploadField } from "../components/FileUploadField.js";
|
|
2
1
|
/**
|
|
3
2
|
* Thrown when a component has an invalid state. This is typically only required where state needs
|
|
4
3
|
* to be checked against an external source upon submission of a form. For example: file upload
|
|
@@ -18,7 +17,7 @@ export class InvalidComponentStateError extends Error {
|
|
|
18
17
|
this.userMessage = userMessage;
|
|
19
18
|
}
|
|
20
19
|
getStateKeys() {
|
|
21
|
-
const extraStateKeys = this.component
|
|
20
|
+
const extraStateKeys = this.component.page?.getStateKeys(this.component) ?? [];
|
|
22
21
|
return [this.component.name].concat(extraStateKeys);
|
|
23
22
|
}
|
|
24
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":["
|
|
1
|
+
{"version":3,"file":"errors.js","names":["InvalidComponentStateError","Error","component","userMessage","constructor","message","name","getStateKeys","extraStateKeys","page","concat"],"sources":["../../../../../src/server/plugins/engine/pageControllers/errors.ts"],"sourcesContent":["import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\n\n/**\n * Thrown when a component has an invalid state. This is typically only required where state needs\n * to be checked against an external source upon submission of a form. For example: file upload\n * has internal state (file upload IDs) but also external state (files in S3). The internal state\n * is always validated by the engine, but the external state needs validating too.\n *\n * This should be used within a formComponent.onSubmit(...).\n */\nexport class InvalidComponentStateError extends Error {\n public readonly component: FormComponent\n public readonly userMessage: string\n\n constructor(component: FormComponent, userMessage: string) {\n const message = `Invalid component state for: ${component.name}`\n super(message)\n this.name = 'InvalidComponentStateError'\n this.component = component\n this.userMessage = userMessage\n }\n\n getStateKeys() {\n const extraStateKeys =\n this.component.page?.getStateKeys(this.component) ?? []\n\n return [this.component.name].concat(extraStateKeys)\n }\n}\n"],"mappings":"AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMA,0BAA0B,SAASC,KAAK,CAAC;EACpCC,SAAS;EACTC,WAAW;EAE3BC,WAAWA,CAACF,SAAwB,EAAEC,WAAmB,EAAE;IACzD,MAAME,OAAO,GAAG,gCAAgCH,SAAS,CAACI,IAAI,EAAE;IAChE,KAAK,CAACD,OAAO,CAAC;IACd,IAAI,CAACC,IAAI,GAAG,4BAA4B;IACxC,IAAI,CAACJ,SAAS,GAAGA,SAAS;IAC1B,IAAI,CAACC,WAAW,GAAGA,WAAW;EAChC;EAEAI,YAAYA,CAAA,EAAG;IACb,MAAMC,cAAc,GAClB,IAAI,CAACN,SAAS,CAACO,IAAI,EAAEF,YAAY,CAAC,IAAI,CAACL,SAAS,CAAC,IAAI,EAAE;IAEzD,OAAO,CAAC,IAAI,CAACA,SAAS,CAACI,IAAI,CAAC,CAACI,MAAM,CAACF,cAAc,CAAC;EACrD;AACF","ignoreList":[]}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed.
|
|
3
|
+
* @param strCodes - array of binary input values
|
|
4
|
+
*/
|
|
5
|
+
export declare function convertToDecAlpha(strCodes: number[]): string;
|
|
1
6
|
/**
|
|
2
7
|
* Generates a reference number in the format of `XXX-XXX-XXX`, or `PREFIX-XXX-XXX` if a prefix is provided.
|
|
3
8
|
* Provides no guarantee on uniqueness.
|
|
9
|
+
* To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed
|
|
10
|
+
* (see https://gunkies.org/wiki/DEC_alphabet )
|
|
4
11
|
*/
|
|
5
12
|
export declare function generateUniqueReference(prefix?: string): string;
|
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { RegExpMatcher, englishDataset, englishRecommendedTransformers } from 'obscenity';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed.
|
|
6
|
+
* @param strCodes - array of binary input values
|
|
7
|
+
*/
|
|
8
|
+
export function convertToDecAlpha(strCodes) {
|
|
9
|
+
const validChars = 'ABCDEFHJKLMNPRSTUVWXYZ23456789';
|
|
10
|
+
const strLen = validChars.length;
|
|
11
|
+
const outArray = [];
|
|
12
|
+
strCodes.forEach(code => {
|
|
13
|
+
const pos = code / 256 * strLen;
|
|
14
|
+
outArray.push(validChars.charAt(pos));
|
|
15
|
+
});
|
|
16
|
+
return outArray.join('');
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
/**
|
|
4
20
|
* Generates a reference number in the format of `XXX-XXX-XXX`, or `PREFIX-XXX-XXX` if a prefix is provided.
|
|
5
21
|
* Provides no guarantee on uniqueness.
|
|
22
|
+
* To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed
|
|
23
|
+
* (see https://gunkies.org/wiki/DEC_alphabet )
|
|
6
24
|
*/
|
|
7
25
|
export function generateUniqueReference(prefix) {
|
|
8
26
|
const segmentLength = 3;
|
|
9
27
|
const segmentCount = prefix ? 2 : 3;
|
|
10
28
|
prefix = prefix ? `${prefix}-` : '';
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
);
|
|
15
|
-
|
|
29
|
+
const profanityMatcher = new RegExpMatcher({
|
|
30
|
+
...englishDataset.build(),
|
|
31
|
+
...englishRecommendedTransformers
|
|
32
|
+
});
|
|
33
|
+
let referenceNumber;
|
|
34
|
+
do {
|
|
35
|
+
const segments = Array.from({
|
|
36
|
+
length: segmentCount
|
|
37
|
+
}, () => convertToDecAlpha([...randomBytes(segmentLength)]).slice(0, segmentLength * 2));
|
|
38
|
+
referenceNumber = `${prefix}${segments.join('-')}`.toUpperCase();
|
|
39
|
+
} while (profanityMatcher.hasMatch(referenceNumber.replaceAll('-', '')));
|
|
40
|
+
return referenceNumber;
|
|
16
41
|
}
|
|
17
42
|
//# sourceMappingURL=referenceNumbers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"referenceNumbers.js","names":["randomBytes","generateUniqueReference","prefix","segmentLength","segmentCount","
|
|
1
|
+
{"version":3,"file":"referenceNumbers.js","names":["randomBytes","RegExpMatcher","englishDataset","englishRecommendedTransformers","convertToDecAlpha","strCodes","validChars","strLen","length","outArray","forEach","code","pos","push","charAt","join","generateUniqueReference","prefix","segmentLength","segmentCount","profanityMatcher","build","referenceNumber","segments","Array","from","slice","toUpperCase","hasMatch","replaceAll"],"sources":["../../../../src/server/plugins/engine/referenceNumbers.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\n\nimport {\n RegExpMatcher,\n englishDataset,\n englishRecommendedTransformers\n} from 'obscenity'\n\n/**\n * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed.\n * @param strCodes - array of binary input values\n */\nexport function convertToDecAlpha(strCodes: number[]) {\n const validChars = 'ABCDEFHJKLMNPRSTUVWXYZ23456789'\n const strLen = validChars.length\n const outArray = [] as string[]\n\n strCodes.forEach((code) => {\n const pos = (code / 256) * strLen\n outArray.push(validChars.charAt(pos))\n })\n\n return outArray.join('')\n}\n\n/**\n * Generates a reference number in the format of `XXX-XXX-XXX`, or `PREFIX-XXX-XXX` if a prefix is provided.\n * Provides no guarantee on uniqueness.\n * To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed\n * (see https://gunkies.org/wiki/DEC_alphabet )\n */\nexport function generateUniqueReference(prefix?: string) {\n const segmentLength = 3\n const segmentCount = prefix ? 2 : 3\n prefix = prefix ? `${prefix}-` : ''\n\n const profanityMatcher = new RegExpMatcher({\n ...englishDataset.build(),\n ...englishRecommendedTransformers\n })\n\n let referenceNumber\n\n do {\n const segments = Array.from({ length: segmentCount }, () =>\n convertToDecAlpha([...randomBytes(segmentLength)]).slice(\n 0,\n segmentLength * 2\n )\n )\n\n referenceNumber = `${prefix}${segments.join('-')}`.toUpperCase()\n } while (profanityMatcher.hasMatch(referenceNumber.replaceAll('-', '')))\n\n return referenceNumber\n}\n"],"mappings":"AAAA,SAASA,WAAW,QAAQ,aAAa;AAEzC,SACEC,aAAa,EACbC,cAAc,EACdC,8BAA8B,QACzB,WAAW;;AAElB;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAACC,QAAkB,EAAE;EACpD,MAAMC,UAAU,GAAG,gCAAgC;EACnD,MAAMC,MAAM,GAAGD,UAAU,CAACE,MAAM;EAChC,MAAMC,QAAQ,GAAG,EAAc;EAE/BJ,QAAQ,CAACK,OAAO,CAAEC,IAAI,IAAK;IACzB,MAAMC,GAAG,GAAID,IAAI,GAAG,GAAG,GAAIJ,MAAM;IACjCE,QAAQ,CAACI,IAAI,CAACP,UAAU,CAACQ,MAAM,CAACF,GAAG,CAAC,CAAC;EACvC,CAAC,CAAC;EAEF,OAAOH,QAAQ,CAACM,IAAI,CAAC,EAAE,CAAC;AAC1B;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,uBAAuBA,CAACC,MAAe,EAAE;EACvD,MAAMC,aAAa,GAAG,CAAC;EACvB,MAAMC,YAAY,GAAGF,MAAM,GAAG,CAAC,GAAG,CAAC;EACnCA,MAAM,GAAGA,MAAM,GAAG,GAAGA,MAAM,GAAG,GAAG,EAAE;EAEnC,MAAMG,gBAAgB,GAAG,IAAInB,aAAa,CAAC;IACzC,GAAGC,cAAc,CAACmB,KAAK,CAAC,CAAC;IACzB,GAAGlB;EACL,CAAC,CAAC;EAEF,IAAImB,eAAe;EAEnB,GAAG;IACD,MAAMC,QAAQ,GAAGC,KAAK,CAACC,IAAI,CAAC;MAAEjB,MAAM,EAAEW;IAAa,CAAC,EAAE,MACpDf,iBAAiB,CAAC,CAAC,GAAGJ,WAAW,CAACkB,aAAa,CAAC,CAAC,CAAC,CAACQ,KAAK,CACtD,CAAC,EACDR,aAAa,GAAG,CAClB,CACF,CAAC;IAEDI,eAAe,GAAG,GAAGL,MAAM,GAAGM,QAAQ,CAACR,IAAI,CAAC,GAAG,CAAC,EAAE,CAACY,WAAW,CAAC,CAAC;EAClE,CAAC,QAAQP,gBAAgB,CAACQ,QAAQ,CAACN,eAAe,CAACO,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;EAEvE,OAAOP,eAAe;AACxB","ignoreList":[]}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
<div class="govuk-grid-row">
|
|
9
9
|
<div class="govuk-grid-column-two-thirds">
|
|
10
10
|
{{ govukPanel({
|
|
11
|
-
titleText: pageTitle
|
|
11
|
+
titleText: pageTitle,
|
|
12
|
+
html: "Your reference number<br><strong>" + referenceNumber + "</strong>" if showReferenceNumber
|
|
12
13
|
}) }}
|
|
13
14
|
<h2 class="govuk-heading-m">What happens next</h2>
|
|
14
15
|
<div class="app-prose-scope">
|
|
@@ -27,6 +27,29 @@ export declare class CacheService {
|
|
|
27
27
|
setFlash(request: AnyFormRequest, message: {
|
|
28
28
|
errors: FormSubmissionError[];
|
|
29
29
|
}): void;
|
|
30
|
+
/**
|
|
31
|
+
* Resets (removes) component states from the form state by their keys.
|
|
32
|
+
* Supports both flat keys and nested paths.
|
|
33
|
+
* @param request - The Hapi request object
|
|
34
|
+
* @param componentNames - Array of state keys to remove. Uses lodash's unset syntax. Can be:
|
|
35
|
+
* - Flat keys: `'componentName'` for top-level state
|
|
36
|
+
* - Nested paths: `"upload['/my-page']"` or `'upload./my-page'` for nested state
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // Remove a flat component state
|
|
40
|
+
* await cacheService.resetComponentStates(request, ['emailAddress'])
|
|
41
|
+
*
|
|
42
|
+
* // Remove nested upload state for a specific page
|
|
43
|
+
* await cacheService.resetComponentStates(request, ["upload['/file-upload-page']"])
|
|
44
|
+
*
|
|
45
|
+
* // Remove multiple states at once
|
|
46
|
+
* await cacheService.resetComponentStates(request, [
|
|
47
|
+
* 'componentName',
|
|
48
|
+
* "upload['/my-page']"
|
|
49
|
+
* ])
|
|
50
|
+
* ```
|
|
51
|
+
* @returns The updated state after removal
|
|
52
|
+
*/
|
|
30
53
|
resetComponentStates(request: AnyFormRequest, componentNames: string[]): Promise<FormSubmissionState>;
|
|
31
54
|
/**
|
|
32
55
|
* The key used to store user session data against.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Hoek from '@hapi/hoek';
|
|
2
|
+
import unset from 'lodash/unset.js';
|
|
2
3
|
import { config } from "../../config/index.js";
|
|
3
4
|
const partition = 'cache';
|
|
4
5
|
export let ADDITIONAL_IDENTIFIER = /*#__PURE__*/function (ADDITIONAL_IDENTIFIER) {
|
|
@@ -61,13 +62,34 @@ export class CacheService {
|
|
|
61
62
|
const key = this.Key(request);
|
|
62
63
|
request.yar.flash(key.id, message);
|
|
63
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resets (removes) component states from the form state by their keys.
|
|
68
|
+
* Supports both flat keys and nested paths.
|
|
69
|
+
* @param request - The Hapi request object
|
|
70
|
+
* @param componentNames - Array of state keys to remove. Uses lodash's unset syntax. Can be:
|
|
71
|
+
* - Flat keys: `'componentName'` for top-level state
|
|
72
|
+
* - Nested paths: `"upload['/my-page']"` or `'upload./my-page'` for nested state
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* // Remove a flat component state
|
|
76
|
+
* await cacheService.resetComponentStates(request, ['emailAddress'])
|
|
77
|
+
*
|
|
78
|
+
* // Remove nested upload state for a specific page
|
|
79
|
+
* await cacheService.resetComponentStates(request, ["upload['/file-upload-page']"])
|
|
80
|
+
*
|
|
81
|
+
* // Remove multiple states at once
|
|
82
|
+
* await cacheService.resetComponentStates(request, [
|
|
83
|
+
* 'componentName',
|
|
84
|
+
* "upload['/my-page']"
|
|
85
|
+
* ])
|
|
86
|
+
* ```
|
|
87
|
+
* @returns The updated state after removal
|
|
88
|
+
*/
|
|
64
89
|
async resetComponentStates(request, componentNames) {
|
|
65
90
|
const state = await this.getState(request);
|
|
66
91
|
for (const componentName of componentNames) {
|
|
67
|
-
|
|
68
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
69
|
-
delete state[componentName];
|
|
70
|
-
}
|
|
92
|
+
unset(state, componentName);
|
|
71
93
|
}
|
|
72
94
|
return this.setState(request, state);
|
|
73
95
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cacheService.js","names":["Hoek","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","logger","constructor","server","cacheName","log","segment","getState","request","key","Key","cached","get","setState","state","ttl","set","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","resetComponentStates","componentNames","componentName","additionalIdentifier","Error","params","slug","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Server } from '@hapi/hapi'\nimport * as Hoek from '@hapi/hoek'\n\nimport { config } from '~/src/config/index.js'\nimport { type createServer } from '~/src/server/index.js'\nimport {\n type AnyFormRequest,\n type AnyRequest,\n type FormConfirmationState,\n type FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nconst partition = 'cache'\n\nexport enum ADDITIONAL_IDENTIFIER {\n Confirmation = ':confirmation'\n}\n\nexport class CacheService {\n /**\n * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}\n */\n cache\n logger: Server['logger']\n\n constructor({ server, cacheName }: { server: Server; cacheName?: string }) {\n if (!cacheName) {\n server.log(\n 'warn',\n 'You are using the default hapi cache. Please provide a cache name in plugin registration options.'\n )\n }\n\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(request: AnyRequest): Promise<FormSubmissionState> {\n const key = this.Key(request)\n const cached = await this.cache.get(key)\n\n return cached ?? {}\n }\n\n async setState(request: AnyFormRequest, state: FormSubmissionState) {\n const key = this.Key(request)\n const ttl = config.get('sessionTimeout')\n\n await this.cache.set(key, state, ttl)\n\n return this.getState(request)\n }\n\n async getConfirmationState(\n request: AnyFormRequest\n ): Promise<FormConfirmationState> {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const value = await this.cache.get(key)\n\n return value ?? {}\n }\n\n async setConfirmationState(\n request: AnyFormRequest,\n confirmationState: FormConfirmationState\n ) {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const ttl = config.get('confirmationSessionTimeout')\n\n return this.cache.set(key, confirmationState, ttl)\n }\n\n async clearState(request: AnyFormRequest) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: AnyFormRequest\n ): { errors: FormSubmissionError[] } | undefined {\n const key = this.Key(request)\n const messages = request.yar.flash(key.id)\n\n if (Array.isArray(messages) && messages.length) {\n return messages.at(0)\n }\n }\n\n setFlash(\n request: AnyFormRequest,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n async resetComponentStates(\n request: AnyFormRequest,\n componentNames: string[]\n ) {\n const state = await this.getState(request)\n\n for (const componentName of componentNames) {\n
|
|
1
|
+
{"version":3,"file":"cacheService.js","names":["Hoek","unset","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","logger","constructor","server","cacheName","log","segment","getState","request","key","Key","cached","get","setState","state","ttl","set","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","resetComponentStates","componentNames","componentName","additionalIdentifier","Error","params","slug","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Server } from '@hapi/hapi'\nimport * as Hoek from '@hapi/hoek'\nimport unset from 'lodash/unset.js'\n\nimport { config } from '~/src/config/index.js'\nimport { type createServer } from '~/src/server/index.js'\nimport {\n type AnyFormRequest,\n type AnyRequest,\n type FormConfirmationState,\n type FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nconst partition = 'cache'\n\nexport enum ADDITIONAL_IDENTIFIER {\n Confirmation = ':confirmation'\n}\n\nexport class CacheService {\n /**\n * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}\n */\n cache\n logger: Server['logger']\n\n constructor({ server, cacheName }: { server: Server; cacheName?: string }) {\n if (!cacheName) {\n server.log(\n 'warn',\n 'You are using the default hapi cache. Please provide a cache name in plugin registration options.'\n )\n }\n\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(request: AnyRequest): Promise<FormSubmissionState> {\n const key = this.Key(request)\n const cached = await this.cache.get(key)\n\n return cached ?? {}\n }\n\n async setState(request: AnyFormRequest, state: FormSubmissionState) {\n const key = this.Key(request)\n const ttl = config.get('sessionTimeout')\n\n await this.cache.set(key, state, ttl)\n\n return this.getState(request)\n }\n\n async getConfirmationState(\n request: AnyFormRequest\n ): Promise<FormConfirmationState> {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const value = await this.cache.get(key)\n\n return value ?? {}\n }\n\n async setConfirmationState(\n request: AnyFormRequest,\n confirmationState: FormConfirmationState\n ) {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const ttl = config.get('confirmationSessionTimeout')\n\n return this.cache.set(key, confirmationState, ttl)\n }\n\n async clearState(request: AnyFormRequest) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: AnyFormRequest\n ): { errors: FormSubmissionError[] } | undefined {\n const key = this.Key(request)\n const messages = request.yar.flash(key.id)\n\n if (Array.isArray(messages) && messages.length) {\n return messages.at(0)\n }\n }\n\n setFlash(\n request: AnyFormRequest,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n /**\n * Resets (removes) component states from the form state by their keys.\n * Supports both flat keys and nested paths.\n * @param request - The Hapi request object\n * @param componentNames - Array of state keys to remove. Uses lodash's unset syntax. Can be:\n * - Flat keys: `'componentName'` for top-level state\n * - Nested paths: `\"upload['/my-page']\"` or `'upload./my-page'` for nested state\n * @example\n * ```typescript\n * // Remove a flat component state\n * await cacheService.resetComponentStates(request, ['emailAddress'])\n *\n * // Remove nested upload state for a specific page\n * await cacheService.resetComponentStates(request, [\"upload['/file-upload-page']\"])\n *\n * // Remove multiple states at once\n * await cacheService.resetComponentStates(request, [\n * 'componentName',\n * \"upload['/my-page']\"\n * ])\n * ```\n * @returns The updated state after removal\n */\n async resetComponentStates(\n request: AnyFormRequest,\n componentNames: string[]\n ) {\n const state = await this.getState(request)\n\n for (const componentName of componentNames) {\n unset(state, componentName)\n }\n\n return this.setState(request, state)\n }\n\n /**\n * The key used to store user session data against.\n * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`\n * @param request - hapi request object\n * @param additionalIdentifier - appended to the id\n */\n Key(request: AnyRequest, additionalIdentifier?: ADDITIONAL_IDENTIFIER) {\n if (!request.yar.id) {\n throw new Error('No session ID found')\n }\n\n const state = (request.params.state as string) || ''\n const slug = (request.params.slug as string) || ''\n const key = `${request.yar.id}:${state}:${slug}:`\n\n return {\n segment: partition,\n id: `${key}${additionalIdentifier ?? ''}`\n }\n }\n}\n\n/**\n * State merge helper\n * 1. Merges objects (form fields)\n * 2. Overwrites arrays\n */\nexport function merge<StateType extends FormState | FormPayload>(\n state: StateType,\n update: object\n): StateType {\n return Hoek.merge(state, update, {\n mergeArrays: false\n })\n}\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/forms-engine-plugin",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.39",
|
|
4
4
|
"description": "Defra forms engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
"license": "SEE LICENSE IN LICENSE",
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@defra/forms-model": "^3.0.
|
|
73
|
+
"@defra/forms-model": "^3.0.603",
|
|
74
74
|
"@defra/hapi-tracing": "^1.29.0",
|
|
75
75
|
"@elastic/ecs-pino-format": "^1.5.0",
|
|
76
76
|
"@hapi/boom": "^10.0.1",
|
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
"lodash": "^4.17.21",
|
|
109
109
|
"marked": "^15.0.12",
|
|
110
110
|
"nunjucks": "^3.2.4",
|
|
111
|
+
"obscenity": "^0.4.5",
|
|
111
112
|
"outdent": "^0.8.0",
|
|
112
113
|
"pino": "^9.14.0",
|
|
113
114
|
"pino-pretty": "^13.1.2",
|
|
@@ -41,6 +41,20 @@ pages:
|
|
|
41
41
|
schema: {}
|
|
42
42
|
id: 987c1234-56d7-89e0-1234-56789abcdef0
|
|
43
43
|
id: 23456789-0abc-def1-2345-67890abcdef1
|
|
44
|
+
- title: Upload a copy of your drivers licence
|
|
45
|
+
controller: FileUploadPageController
|
|
46
|
+
path: '/upload-driving-licence'
|
|
47
|
+
components:
|
|
48
|
+
- type: FileUploadField
|
|
49
|
+
title: Please upload a copy of your drivers licence
|
|
50
|
+
name: driversLicenceUpload
|
|
51
|
+
shortDescription: Upload drivers licence
|
|
52
|
+
hint: ''
|
|
53
|
+
options:
|
|
54
|
+
required: true
|
|
55
|
+
schema: {}
|
|
56
|
+
id: 987c1234-56d7-89e0-1234-56789abcdef1
|
|
57
|
+
id: 23456789-0abc-def1-2345-67890abcdef2
|
|
44
58
|
- title: ''
|
|
45
59
|
path: '/date-of-birth'
|
|
46
60
|
components:
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
import { ComponentType, type ComponentDef } from '@defra/forms-model'
|
|
3
3
|
import { type ValidationErrorItem, type ValidationResult } from 'joi'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
FileUploadField,
|
|
7
|
+
tempItemSchema
|
|
8
|
+
} from '~/src/server/plugins/engine/components/FileUploadField.js'
|
|
9
|
+
import { TextField } from '~/src/server/plugins/engine/components/TextField.js'
|
|
6
10
|
import {
|
|
7
11
|
getCacheService,
|
|
8
12
|
getError
|
|
@@ -1133,4 +1137,48 @@ describe('FileUploadPageController', () => {
|
|
|
1133
1137
|
expect(controller.shouldShowSaveAndExit(serverWithSaveAndExit)).toBe(true)
|
|
1134
1138
|
})
|
|
1135
1139
|
})
|
|
1140
|
+
|
|
1141
|
+
describe('getStateKeys', () => {
|
|
1142
|
+
it('should return nested upload path for FileUploadField component', () => {
|
|
1143
|
+
const component = controller.fileUpload
|
|
1144
|
+
const stateKeys = controller.getStateKeys(component)
|
|
1145
|
+
|
|
1146
|
+
expect(stateKeys).toEqual(["upload['/file-upload-component']"])
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
it('should return empty array for non-FileUploadField components', () => {
|
|
1150
|
+
const component = new TextField(
|
|
1151
|
+
{
|
|
1152
|
+
name: 'testField',
|
|
1153
|
+
title: 'Test field',
|
|
1154
|
+
type: ComponentType.TextField,
|
|
1155
|
+
options: {},
|
|
1156
|
+
schema: {}
|
|
1157
|
+
},
|
|
1158
|
+
{ model, page: controller }
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
const stateKeys = controller.getStateKeys(component)
|
|
1162
|
+
expect(stateKeys).toEqual([])
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
it('should return fallback upload key when component has no page', () => {
|
|
1166
|
+
const componentDef: ComponentDef = {
|
|
1167
|
+
name: 'fileUpload',
|
|
1168
|
+
title: 'Upload something',
|
|
1169
|
+
type: ComponentType.FileUploadField,
|
|
1170
|
+
options: {},
|
|
1171
|
+
schema: {}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Create a component without a page reference - should return ['upload']
|
|
1175
|
+
const component = new FileUploadField(componentDef, {
|
|
1176
|
+
model,
|
|
1177
|
+
page: undefined
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
const stateKeys = controller.getStateKeys(component)
|
|
1181
|
+
expect(stateKeys).toEqual(['upload'])
|
|
1182
|
+
})
|
|
1183
|
+
})
|
|
1136
1184
|
})
|
|
@@ -4,9 +4,10 @@ import { wait } from '@hapi/hoek'
|
|
|
4
4
|
import { type ValidationErrorItem } from 'joi'
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
FileUploadField,
|
|
8
|
+
tempItemSchema
|
|
9
9
|
} from '~/src/server/plugins/engine/components/FileUploadField.js'
|
|
10
|
+
import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
10
11
|
import {
|
|
11
12
|
getCacheService,
|
|
12
13
|
getError,
|
|
@@ -97,6 +98,23 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
97
98
|
this.viewName = 'file-upload'
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Get supplementary state keys for clearing file upload state.
|
|
103
|
+
* Returns the nested upload path for FileUploadField components only.
|
|
104
|
+
* @param component - The component to get supplementary state keys for
|
|
105
|
+
* @returns Array containing the nested upload path, e.g., ["upload['/page-path']"]
|
|
106
|
+
* or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.
|
|
107
|
+
*/
|
|
108
|
+
getStateKeys(component: FormComponent): string[] {
|
|
109
|
+
// Only return upload keys for FileUploadField components
|
|
110
|
+
if (!(component instanceof FileUploadField)) {
|
|
111
|
+
return []
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pagePath = component.page?.path
|
|
115
|
+
return pagePath ? [`upload['${pagePath}']`] : ['upload']
|
|
116
|
+
}
|
|
117
|
+
|
|
100
118
|
getFormDataFromState(
|
|
101
119
|
request: FormContextRequest | undefined,
|
|
102
120
|
state: FormSubmissionState
|
|
@@ -236,4 +236,11 @@ describe('PageController', () => {
|
|
|
236
236
|
)
|
|
237
237
|
})
|
|
238
238
|
})
|
|
239
|
+
|
|
240
|
+
describe('getStateKeys', () => {
|
|
241
|
+
it('should return empty array by default', () => {
|
|
242
|
+
const stateKeys = controller1.getStateKeys()
|
|
243
|
+
expect(stateKeys).toEqual([])
|
|
244
|
+
})
|
|
245
|
+
})
|
|
239
246
|
})
|
|
@@ -9,6 +9,7 @@ import Boom from '@hapi/boom'
|
|
|
9
9
|
import { type Lifecycle, type RouteOptions, type Server } from '@hapi/hapi'
|
|
10
10
|
|
|
11
11
|
import { type ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
|
|
12
|
+
import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
12
13
|
import {
|
|
13
14
|
getSaveAndExitHelpers,
|
|
14
15
|
getStartPath,
|
|
@@ -175,6 +176,22 @@ export class PageController {
|
|
|
175
176
|
throw Boom.badRequest('Unsupported POST route handler for this page')
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Get supplementary state keys for clearing component state.
|
|
181
|
+
*
|
|
182
|
+
* This method returns page controller-level state keys only. The core component's
|
|
183
|
+
* state key (the component's name) is managed separately by the framework and should
|
|
184
|
+
* NOT be included in the returned array.
|
|
185
|
+
*
|
|
186
|
+
* Returns an empty array by default. Override in subclasses to provide
|
|
187
|
+
* page-specific supplementary state keys (e.g., upload state, cached data).
|
|
188
|
+
* @param _component - The component to get supplementary state keys for (optional)
|
|
189
|
+
* @returns Array of supplementary state keys to clear (excluding the component name itself)
|
|
190
|
+
*/
|
|
191
|
+
getStateKeys(_component?: FormComponent): string[] {
|
|
192
|
+
return []
|
|
193
|
+
}
|
|
194
|
+
|
|
178
195
|
shouldShowSaveAndExit(server: Server): boolean {
|
|
179
196
|
return getSaveAndExitHelpers(server) !== undefined && this.allowSaveAndExit
|
|
180
197
|
}
|
|
@@ -12,10 +12,12 @@ import {
|
|
|
12
12
|
export class StatusPageController extends QuestionPageController {
|
|
13
13
|
declare pageDef: PageStatus
|
|
14
14
|
allowSaveAndExit = false
|
|
15
|
+
showReferenceNumber = false
|
|
15
16
|
|
|
16
17
|
constructor(model: FormModel, pageDef: PageStatus) {
|
|
17
18
|
super(model, pageDef)
|
|
18
19
|
this.viewName = 'confirmation'
|
|
20
|
+
this.showReferenceNumber = model.def.options?.showReferenceNumber ?? false
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
getRelevantPath() {
|
|
@@ -54,7 +56,9 @@ export class StatusPageController extends QuestionPageController {
|
|
|
54
56
|
return h.view(viewName, {
|
|
55
57
|
...viewModel,
|
|
56
58
|
submissionGuidance,
|
|
57
|
-
formName
|
|
59
|
+
formName,
|
|
60
|
+
showReferenceNumber: this.showReferenceNumber,
|
|
61
|
+
referenceNumber: context.referenceNumber
|
|
58
62
|
})
|
|
59
63
|
}
|
|
60
64
|
}
|
|
@@ -16,7 +16,7 @@ describe('InvalidComponentStateError', () => {
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
describe('getStateKeys', () => {
|
|
19
|
-
it('should return component name and upload for FileUploadField', () => {
|
|
19
|
+
it('should return component name and nested upload path for FileUploadField', () => {
|
|
20
20
|
const page = model.pages.find((p) => p.path === '/file-upload-component')
|
|
21
21
|
const component = new FileUploadField(
|
|
22
22
|
{
|
|
@@ -35,7 +35,10 @@ describe('InvalidComponentStateError', () => {
|
|
|
35
35
|
)
|
|
36
36
|
const stateKeys = error.getStateKeys()
|
|
37
37
|
|
|
38
|
-
expect(stateKeys).toEqual([
|
|
38
|
+
expect(stateKeys).toEqual([
|
|
39
|
+
'fileUpload',
|
|
40
|
+
"upload['/file-upload-component']"
|
|
41
|
+
])
|
|
39
42
|
})
|
|
40
43
|
|
|
41
44
|
it('should return only component name for non-FileUploadField components', () => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'
|
|
2
1
|
import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -23,7 +22,7 @@ export class InvalidComponentStateError extends Error {
|
|
|
23
22
|
|
|
24
23
|
getStateKeys() {
|
|
25
24
|
const extraStateKeys =
|
|
26
|
-
this.component
|
|
25
|
+
this.component.page?.getStateKeys(this.component) ?? []
|
|
27
26
|
|
|
28
27
|
return [this.component.name].concat(extraStateKeys)
|
|
29
28
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
convertToDecAlpha,
|
|
3
|
+
generateUniqueReference
|
|
4
|
+
} from '~/src/server/plugins/engine/referenceNumbers.js'
|
|
2
5
|
|
|
3
6
|
describe('generateUniqueReference', () => {
|
|
4
7
|
it('should generate a reference number with 3 segments when no prefix is provided', () => {
|
|
@@ -30,4 +33,42 @@ describe('generateUniqueReference', () => {
|
|
|
30
33
|
const referenceNumber2 = generateUniqueReference()
|
|
31
34
|
expect(referenceNumber1).not.toBe(referenceNumber2)
|
|
32
35
|
})
|
|
36
|
+
|
|
37
|
+
describe('convertToDecAlpha', () => {
|
|
38
|
+
it('should generate correct characters in string', () => {
|
|
39
|
+
const allValuesHexPairs = Array.from(Array(256).keys())
|
|
40
|
+
expect(convertToDecAlpha(allValuesHexPairs)).toBe(
|
|
41
|
+
'AAAAAAAAA' +
|
|
42
|
+
'BBBBBBBBB' +
|
|
43
|
+
'CCCCCCCC' +
|
|
44
|
+
'DDDDDDDDD' +
|
|
45
|
+
'EEEEEEEE' +
|
|
46
|
+
'FFFFFFFFF' +
|
|
47
|
+
'HHHHHHHH' +
|
|
48
|
+
'JJJJJJJJJ' +
|
|
49
|
+
'KKKKKKKK' +
|
|
50
|
+
'LLLLLLLLL' +
|
|
51
|
+
'MMMMMMMM' +
|
|
52
|
+
'NNNNNNNNN' +
|
|
53
|
+
'PPPPPPPP' +
|
|
54
|
+
'RRRRRRRRR' +
|
|
55
|
+
'SSSSSSSS' +
|
|
56
|
+
'TTTTTTTTT' +
|
|
57
|
+
'UUUUUUUUU' +
|
|
58
|
+
'VVVVVVVV' +
|
|
59
|
+
'WWWWWWWWW' +
|
|
60
|
+
'XXXXXXXX' +
|
|
61
|
+
'YYYYYYYYY' +
|
|
62
|
+
'ZZZZZZZZ' +
|
|
63
|
+
'222222222' +
|
|
64
|
+
'33333333' +
|
|
65
|
+
'444444444' +
|
|
66
|
+
'55555555' +
|
|
67
|
+
'666666666' +
|
|
68
|
+
'77777777' +
|
|
69
|
+
'888888888' +
|
|
70
|
+
'99999999'
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
33
74
|
})
|
|
@@ -1,18 +1,56 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
RegExpMatcher,
|
|
5
|
+
englishDataset,
|
|
6
|
+
englishRecommendedTransformers
|
|
7
|
+
} from 'obscenity'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed.
|
|
11
|
+
* @param strCodes - array of binary input values
|
|
12
|
+
*/
|
|
13
|
+
export function convertToDecAlpha(strCodes: number[]) {
|
|
14
|
+
const validChars = 'ABCDEFHJKLMNPRSTUVWXYZ23456789'
|
|
15
|
+
const strLen = validChars.length
|
|
16
|
+
const outArray = [] as string[]
|
|
17
|
+
|
|
18
|
+
strCodes.forEach((code) => {
|
|
19
|
+
const pos = (code / 256) * strLen
|
|
20
|
+
outArray.push(validChars.charAt(pos))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return outArray.join('')
|
|
24
|
+
}
|
|
25
|
+
|
|
3
26
|
/**
|
|
4
27
|
* Generates a reference number in the format of `XXX-XXX-XXX`, or `PREFIX-XXX-XXX` if a prefix is provided.
|
|
5
28
|
* Provides no guarantee on uniqueness.
|
|
29
|
+
* To prevent confusion to users reading the reference number, ambiguous letters and numbers are removed
|
|
30
|
+
* (see https://gunkies.org/wiki/DEC_alphabet )
|
|
6
31
|
*/
|
|
7
32
|
export function generateUniqueReference(prefix?: string) {
|
|
8
33
|
const segmentLength = 3
|
|
9
34
|
const segmentCount = prefix ? 2 : 3
|
|
10
35
|
prefix = prefix ? `${prefix}-` : ''
|
|
11
36
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
)
|
|
37
|
+
const profanityMatcher = new RegExpMatcher({
|
|
38
|
+
...englishDataset.build(),
|
|
39
|
+
...englishRecommendedTransformers
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
let referenceNumber
|
|
43
|
+
|
|
44
|
+
do {
|
|
45
|
+
const segments = Array.from({ length: segmentCount }, () =>
|
|
46
|
+
convertToDecAlpha([...randomBytes(segmentLength)]).slice(
|
|
47
|
+
0,
|
|
48
|
+
segmentLength * 2
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
referenceNumber = `${prefix}${segments.join('-')}`.toUpperCase()
|
|
53
|
+
} while (profanityMatcher.hasMatch(referenceNumber.replaceAll('-', '')))
|
|
16
54
|
|
|
17
|
-
return
|
|
55
|
+
return referenceNumber
|
|
18
56
|
}
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
<div class="govuk-grid-row">
|
|
9
9
|
<div class="govuk-grid-column-two-thirds">
|
|
10
10
|
{{ govukPanel({
|
|
11
|
-
titleText: pageTitle
|
|
11
|
+
titleText: pageTitle,
|
|
12
|
+
html: "Your reference number<br><strong>" + referenceNumber + "</strong>" if showReferenceNumber
|
|
12
13
|
}) }}
|
|
13
14
|
<h2 class="govuk-heading-m">What happens next</h2>
|
|
14
15
|
<div class="app-prose-scope">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Server } from '@hapi/hapi'
|
|
2
2
|
import * as Hoek from '@hapi/hoek'
|
|
3
|
+
import unset from 'lodash/unset.js'
|
|
3
4
|
|
|
4
5
|
import { config } from '~/src/config/index.js'
|
|
5
6
|
import { type createServer } from '~/src/server/index.js'
|
|
@@ -99,6 +100,29 @@ export class CacheService {
|
|
|
99
100
|
request.yar.flash(key.id, message)
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Resets (removes) component states from the form state by their keys.
|
|
105
|
+
* Supports both flat keys and nested paths.
|
|
106
|
+
* @param request - The Hapi request object
|
|
107
|
+
* @param componentNames - Array of state keys to remove. Uses lodash's unset syntax. Can be:
|
|
108
|
+
* - Flat keys: `'componentName'` for top-level state
|
|
109
|
+
* - Nested paths: `"upload['/my-page']"` or `'upload./my-page'` for nested state
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* // Remove a flat component state
|
|
113
|
+
* await cacheService.resetComponentStates(request, ['emailAddress'])
|
|
114
|
+
*
|
|
115
|
+
* // Remove nested upload state for a specific page
|
|
116
|
+
* await cacheService.resetComponentStates(request, ["upload['/file-upload-page']"])
|
|
117
|
+
*
|
|
118
|
+
* // Remove multiple states at once
|
|
119
|
+
* await cacheService.resetComponentStates(request, [
|
|
120
|
+
* 'componentName',
|
|
121
|
+
* "upload['/my-page']"
|
|
122
|
+
* ])
|
|
123
|
+
* ```
|
|
124
|
+
* @returns The updated state after removal
|
|
125
|
+
*/
|
|
102
126
|
async resetComponentStates(
|
|
103
127
|
request: AnyFormRequest,
|
|
104
128
|
componentNames: string[]
|
|
@@ -106,10 +130,7 @@ export class CacheService {
|
|
|
106
130
|
const state = await this.getState(request)
|
|
107
131
|
|
|
108
132
|
for (const componentName of componentNames) {
|
|
109
|
-
|
|
110
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
111
|
-
delete state[componentName]
|
|
112
|
-
}
|
|
133
|
+
unset(state, componentName)
|
|
113
134
|
}
|
|
114
135
|
|
|
115
136
|
return this.setState(request, state)
|