@defra/forms-engine-plugin 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.server/server/forms/page-events.yaml +87 -0
- package/.server/server/index.js +2 -1
- package/.server/server/index.js.map +1 -1
- package/.server/server/plugins/engine/helpers.d.ts +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +9 -2
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +1 -2
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/routes/questions.d.ts +4 -2
- package/.server/server/plugins/engine/routes/questions.js +67 -43
- package/.server/server/plugins/engine/routes/questions.js.map +1 -1
- package/.server/server/plugins/engine/services/localFormsService.js +7 -9
- package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +1 -1
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/routes/dummy-api.d.ts +38 -0
- package/.server/server/routes/dummy-api.js +33 -0
- package/.server/server/routes/dummy-api.js.map +1 -0
- package/.server/server/routes/index.d.ts +1 -0
- package/.server/server/routes/index.js +1 -0
- package/.server/server/routes/index.js.map +1 -1
- package/.server/server/services/cacheService.d.ts +0 -2
- package/.server/server/services/cacheService.js +1 -4
- package/.server/server/services/cacheService.js.map +1 -1
- package/package.json +4 -2
- package/src/server/forms/page-events.yaml +87 -0
- package/src/server/index.ts +4 -2
- package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +5 -4
- package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +5 -4
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +81 -16
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +13 -1
- package/src/server/plugins/engine/plugin.ts +1 -2
- package/src/server/plugins/engine/routes/questions.test.ts +416 -0
- package/src/server/plugins/engine/routes/questions.ts +96 -40
- package/src/server/plugins/engine/services/localFormsService.js +7 -8
- package/src/server/plugins/engine/types.ts +0 -1
- package/src/server/routes/dummy-api.test.ts +96 -0
- package/src/server/routes/dummy-api.ts +62 -0
- package/src/server/routes/index.ts +1 -0
- package/src/server/services/cacheService.test.ts +8 -2
- package/src/server/services/cacheService.ts +1 -13
- package/.server/server/forms/register-as-a-unicorn-breeder.json +0 -393
- package/src/server/forms/register-as-a-unicorn-breeder.json +0 -393
|
@@ -32,20 +32,18 @@ export const formsService = async () => {
|
|
|
32
32
|
// Instantiate the file loader form service
|
|
33
33
|
const loader = new FileFormService();
|
|
34
34
|
|
|
35
|
-
// Add a
|
|
36
|
-
await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.
|
|
35
|
+
// Add a Yaml form
|
|
36
|
+
await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.yaml', {
|
|
37
37
|
...metadata,
|
|
38
|
-
id: '
|
|
38
|
+
id: '641aeafd-13dd-40fa-9186-001703800efb',
|
|
39
39
|
title: 'Register as a unicorn breeder',
|
|
40
40
|
slug: 'register-as-a-unicorn-breeder'
|
|
41
41
|
});
|
|
42
|
-
|
|
43
|
-
// Add a Yaml form
|
|
44
|
-
await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.yaml', {
|
|
42
|
+
await loader.addForm('src/server/forms/page-events.yaml', {
|
|
45
43
|
...metadata,
|
|
46
|
-
id: '
|
|
47
|
-
title: '
|
|
48
|
-
slug: '
|
|
44
|
+
id: '511db05e-ebbd-42e8-8270-5fe93f5c9762',
|
|
45
|
+
title: 'Page events demo',
|
|
46
|
+
slug: 'page-events-demo'
|
|
49
47
|
});
|
|
50
48
|
await loader.addForm('src/server/forms/components.json', {
|
|
51
49
|
...metadata,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localFormsService.js","names":["config","FileFormService","now","Date","user","id","displayName","author","createdAt","createdBy","updatedAt","updatedBy","metadata","organisation","teamName","teamEmail","submissionGuidance","notificationEmail","get","live","formsService","loader","addForm","title","slug","toFormsService"],"sources":["../../../../../src/server/plugins/engine/services/localFormsService.js"],"sourcesContent":["import { config } from '~/src/config/index.js'\nimport { FileFormService } from '~/src/server/utils/file-form-service.js'\n\n// Create shared form metadata\nconst now = new Date()\nconst user = { id: 'user', displayName: 'Username' }\nconst author = {\n createdAt: now,\n createdBy: user,\n updatedAt: now,\n updatedBy: user\n}\nconst metadata = {\n organisation: 'Defra',\n teamName: 'Team name',\n teamEmail: 'team@defra.gov.uk',\n submissionGuidance: \"Thanks for your submission, we'll be in touch\",\n notificationEmail: config.get('submissionEmailAddress'),\n ...author,\n live: author\n}\n\n/**\n * Return an function rather than the service directly. This is to prevent consumer applications\n * blowing up as they won't have these files on disk. We can defer the execution until when it's\n * needed, i.e. the createServer function of the devtool.\n */\nexport const formsService = async () => {\n // Instantiate the file loader form service\n const loader = new FileFormService()\n\n // Add a
|
|
1
|
+
{"version":3,"file":"localFormsService.js","names":["config","FileFormService","now","Date","user","id","displayName","author","createdAt","createdBy","updatedAt","updatedBy","metadata","organisation","teamName","teamEmail","submissionGuidance","notificationEmail","get","live","formsService","loader","addForm","title","slug","toFormsService"],"sources":["../../../../../src/server/plugins/engine/services/localFormsService.js"],"sourcesContent":["import { config } from '~/src/config/index.js'\nimport { FileFormService } from '~/src/server/utils/file-form-service.js'\n\n// Create shared form metadata\nconst now = new Date()\nconst user = { id: 'user', displayName: 'Username' }\nconst author = {\n createdAt: now,\n createdBy: user,\n updatedAt: now,\n updatedBy: user\n}\nconst metadata = {\n organisation: 'Defra',\n teamName: 'Team name',\n teamEmail: 'team@defra.gov.uk',\n submissionGuidance: \"Thanks for your submission, we'll be in touch\",\n notificationEmail: config.get('submissionEmailAddress'),\n ...author,\n live: author\n}\n\n/**\n * Return an function rather than the service directly. This is to prevent consumer applications\n * blowing up as they won't have these files on disk. We can defer the execution until when it's\n * needed, i.e. the createServer function of the devtool.\n */\nexport const formsService = async () => {\n // Instantiate the file loader form service\n const loader = new FileFormService()\n\n // Add a Yaml form\n await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.yaml', {\n ...metadata,\n id: '641aeafd-13dd-40fa-9186-001703800efb',\n title: 'Register as a unicorn breeder',\n slug: 'register-as-a-unicorn-breeder'\n })\n\n await loader.addForm('src/server/forms/page-events.yaml', {\n ...metadata,\n id: '511db05e-ebbd-42e8-8270-5fe93f5c9762',\n title: 'Page events demo',\n slug: 'page-events-demo'\n })\n\n await loader.addForm('src/server/forms/components.json', {\n ...metadata,\n id: '6a872d3b-13f9e-804ce3e-4830-5c45fb32',\n title: 'Components',\n slug: 'components'\n })\n\n return loader.toFormsService()\n}\n"],"mappings":"AAAA,SAASA,MAAM;AACf,SAASC,eAAe;;AAExB;AACA,MAAMC,GAAG,GAAG,IAAIC,IAAI,CAAC,CAAC;AACtB,MAAMC,IAAI,GAAG;EAAEC,EAAE,EAAE,MAAM;EAAEC,WAAW,EAAE;AAAW,CAAC;AACpD,MAAMC,MAAM,GAAG;EACbC,SAAS,EAAEN,GAAG;EACdO,SAAS,EAAEL,IAAI;EACfM,SAAS,EAAER,GAAG;EACdS,SAAS,EAAEP;AACb,CAAC;AACD,MAAMQ,QAAQ,GAAG;EACfC,YAAY,EAAE,OAAO;EACrBC,QAAQ,EAAE,WAAW;EACrBC,SAAS,EAAE,mBAAmB;EAC9BC,kBAAkB,EAAE,+CAA+C;EACnEC,iBAAiB,EAAEjB,MAAM,CAACkB,GAAG,CAAC,wBAAwB,CAAC;EACvD,GAAGX,MAAM;EACTY,IAAI,EAAEZ;AACR,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMa,YAAY,GAAG,MAAAA,CAAA,KAAY;EACtC;EACA,MAAMC,MAAM,GAAG,IAAIpB,eAAe,CAAC,CAAC;;EAEpC;EACA,MAAMoB,MAAM,CAACC,OAAO,CAAC,qDAAqD,EAAE;IAC1E,GAAGV,QAAQ;IACXP,EAAE,EAAE,sCAAsC;IAC1CkB,KAAK,EAAE,+BAA+B;IACtCC,IAAI,EAAE;EACR,CAAC,CAAC;EAEF,MAAMH,MAAM,CAACC,OAAO,CAAC,mCAAmC,EAAE;IACxD,GAAGV,QAAQ;IACXP,EAAE,EAAE,sCAAsC;IAC1CkB,KAAK,EAAE,kBAAkB;IACzBC,IAAI,EAAE;EACR,CAAC,CAAC;EAEF,MAAMH,MAAM,CAACC,OAAO,CAAC,kCAAkC,EAAE;IACvD,GAAGV,QAAQ;IACXP,EAAE,EAAE,sCAAsC;IAC1CkB,KAAK,EAAE,YAAY;IACnBC,IAAI,EAAE;EACR,CAAC,CAAC;EAEF,OAAOH,MAAM,CAACI,cAAc,CAAC,CAAC;AAChC,CAAC","ignoreList":[]}
|
|
@@ -273,7 +273,7 @@ export interface PluginOptions {
|
|
|
273
273
|
saveAndReturn?: {
|
|
274
274
|
keyGenerator: (request: RequestType) => string;
|
|
275
275
|
sessionHydrator: (request: RequestType) => Promise<FormSubmissionState>;
|
|
276
|
-
sessionPersister: (
|
|
276
|
+
sessionPersister: (state: FormSubmissionState, request: RequestType) => Promise<void>;
|
|
277
277
|
};
|
|
278
278
|
pluginPath?: string;
|
|
279
279
|
nunjucks: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["UploadStatus","FileStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { type PluginProperties, type Request } from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\ntype RequestType = Request | FormRequest | FormRequestPayload\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport enum UploadStatus {\n initiated = 'initiated',\n pending = 'pending',\n ready = 'ready'\n}\n\nexport enum FileStatus {\n complete = 'complete',\n rejected = 'rejected',\n pending = 'pending'\n}\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndReturn: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: FormRequest | FormRequestPayload,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndReturn?: {\n keyGenerator: (request: RequestType) => string\n sessionHydrator: (request: RequestType) => Promise<FormSubmissionState>\n sessionPersister: (\n key: string,\n state: FormSubmissionState,\n request: RequestType\n ) => Promise<void>\n }\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n"],"mappings":"AAkCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAqGA,WAAYA,YAAY,0BAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAA,OAAZA,YAAY;AAAA;AAMxB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"types.js","names":["UploadStatus","FileStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { type PluginProperties, type Request } from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\ntype RequestType = Request | FormRequest | FormRequestPayload\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport enum UploadStatus {\n initiated = 'initiated',\n pending = 'pending',\n ready = 'ready'\n}\n\nexport enum FileStatus {\n complete = 'complete',\n rejected = 'rejected',\n pending = 'pending'\n}\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndReturn: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: FormRequest | FormRequestPayload,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndReturn?: {\n keyGenerator: (request: RequestType) => string\n sessionHydrator: (request: RequestType) => Promise<FormSubmissionState>\n sessionPersister: (\n state: FormSubmissionState,\n request: RequestType\n ) => Promise<void>\n }\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n"],"mappings":"AAkCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAqGA,WAAYA,YAAY,0BAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAA,OAAZA,YAAY;AAAA;AAMxB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Request, type ResponseToolkit } from '@hapi/hapi';
|
|
2
|
+
declare const _default: ({
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
handler(request: Request<{
|
|
6
|
+
Payload: {
|
|
7
|
+
meta: {
|
|
8
|
+
referenceNumber: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
}>, _h: ResponseToolkit): {
|
|
12
|
+
submissionEvent: string;
|
|
13
|
+
submissionReferenceNumber: string;
|
|
14
|
+
};
|
|
15
|
+
} | {
|
|
16
|
+
method: string;
|
|
17
|
+
path: string;
|
|
18
|
+
handler(request: Request<{
|
|
19
|
+
Payload: {
|
|
20
|
+
data: {
|
|
21
|
+
main: {
|
|
22
|
+
applicantFirstName: string;
|
|
23
|
+
applicantLastName: string;
|
|
24
|
+
dateOfBirth: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
meta: {
|
|
28
|
+
event: string;
|
|
29
|
+
referenceNumber: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}>, _h: ResponseToolkit): {
|
|
33
|
+
calculatedAge: number;
|
|
34
|
+
submissionEvent: string;
|
|
35
|
+
submissionReferenceNumber: string;
|
|
36
|
+
};
|
|
37
|
+
})[];
|
|
38
|
+
export default _default;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function calculateAge(day, month, year) {
|
|
2
|
+
const dobDate = new Date(Number(day), Number(month) - 1, Number(year));
|
|
3
|
+
const today = new Date();
|
|
4
|
+
let age = today.getFullYear() - dobDate.getFullYear();
|
|
5
|
+
const m = today.getMonth() - dobDate.getMonth();
|
|
6
|
+
if (m < 0 || m === 0 && today.getDate() < dobDate.getDate()) {
|
|
7
|
+
age--;
|
|
8
|
+
}
|
|
9
|
+
return age;
|
|
10
|
+
}
|
|
11
|
+
export default [{
|
|
12
|
+
method: 'POST',
|
|
13
|
+
path: '/api/example/on-load-page',
|
|
14
|
+
handler(request, _h) {
|
|
15
|
+
return {
|
|
16
|
+
submissionEvent: 'GET',
|
|
17
|
+
submissionReferenceNumber: request.payload.meta.referenceNumber
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
path: '/api/example/on-summary',
|
|
23
|
+
handler(request, _h) {
|
|
24
|
+
const [day, month, year] = request.payload.data.main.dateOfBirth.split('-');
|
|
25
|
+
const age = calculateAge(day, month, year);
|
|
26
|
+
return {
|
|
27
|
+
calculatedAge: age,
|
|
28
|
+
submissionEvent: 'POST',
|
|
29
|
+
submissionReferenceNumber: request.payload.meta.referenceNumber // example of receiving a payload from DXT
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}];
|
|
33
|
+
//# sourceMappingURL=dummy-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dummy-api.js","names":["calculateAge","day","month","year","dobDate","Date","Number","today","age","getFullYear","m","getMonth","getDate","method","path","handler","request","_h","submissionEvent","submissionReferenceNumber","payload","meta","referenceNumber","data","main","dateOfBirth","split","calculatedAge"],"sources":["../../../src/server/routes/dummy-api.ts"],"sourcesContent":["import { type Request, type ResponseToolkit } from '@hapi/hapi'\n\nfunction calculateAge(day: string, month: string, year: string) {\n const dobDate = new Date(Number(day), Number(month) - 1, Number(year))\n\n const today = new Date()\n\n let age = today.getFullYear() - dobDate.getFullYear()\n const m = today.getMonth() - dobDate.getMonth()\n\n if (m < 0 || (m === 0 && today.getDate() < dobDate.getDate())) {\n age--\n }\n\n return age\n}\n\nexport default [\n {\n method: 'POST',\n path: '/api/example/on-load-page',\n handler(\n request: Request<{ Payload: { meta: { referenceNumber: string } } }>,\n _h: ResponseToolkit\n ) {\n return {\n submissionEvent: 'GET',\n submissionReferenceNumber: request.payload.meta.referenceNumber\n }\n }\n },\n {\n method: 'POST',\n path: '/api/example/on-summary',\n handler(\n request: Request<{\n Payload: {\n data: {\n main: {\n applicantFirstName: string\n applicantLastName: string\n dateOfBirth: string\n }\n }\n meta: { event: string; referenceNumber: string }\n }\n }>,\n _h: ResponseToolkit\n ) {\n const [day, month, year] =\n request.payload.data.main.dateOfBirth.split('-')\n\n const age = calculateAge(day, month, year)\n\n return {\n calculatedAge: age,\n submissionEvent: 'POST',\n submissionReferenceNumber: request.payload.meta.referenceNumber // example of receiving a payload from DXT\n }\n }\n }\n]\n"],"mappings":"AAEA,SAASA,YAAYA,CAACC,GAAW,EAAEC,KAAa,EAAEC,IAAY,EAAE;EAC9D,MAAMC,OAAO,GAAG,IAAIC,IAAI,CAACC,MAAM,CAACL,GAAG,CAAC,EAAEK,MAAM,CAACJ,KAAK,CAAC,GAAG,CAAC,EAAEI,MAAM,CAACH,IAAI,CAAC,CAAC;EAEtE,MAAMI,KAAK,GAAG,IAAIF,IAAI,CAAC,CAAC;EAExB,IAAIG,GAAG,GAAGD,KAAK,CAACE,WAAW,CAAC,CAAC,GAAGL,OAAO,CAACK,WAAW,CAAC,CAAC;EACrD,MAAMC,CAAC,GAAGH,KAAK,CAACI,QAAQ,CAAC,CAAC,GAAGP,OAAO,CAACO,QAAQ,CAAC,CAAC;EAE/C,IAAID,CAAC,GAAG,CAAC,IAAKA,CAAC,KAAK,CAAC,IAAIH,KAAK,CAACK,OAAO,CAAC,CAAC,GAAGR,OAAO,CAACQ,OAAO,CAAC,CAAE,EAAE;IAC7DJ,GAAG,EAAE;EACP;EAEA,OAAOA,GAAG;AACZ;AAEA,eAAe,CACb;EACEK,MAAM,EAAE,MAAM;EACdC,IAAI,EAAE,2BAA2B;EACjCC,OAAOA,CACLC,OAAoE,EACpEC,EAAmB,EACnB;IACA,OAAO;MACLC,eAAe,EAAE,KAAK;MACtBC,yBAAyB,EAAEH,OAAO,CAACI,OAAO,CAACC,IAAI,CAACC;IAClD,CAAC;EACH;AACF,CAAC,EACD;EACET,MAAM,EAAE,MAAM;EACdC,IAAI,EAAE,yBAAyB;EAC/BC,OAAOA,CACLC,OAWE,EACFC,EAAmB,EACnB;IACA,MAAM,CAAChB,GAAG,EAAEC,KAAK,EAAEC,IAAI,CAAC,GACtBa,OAAO,CAACI,OAAO,CAACG,IAAI,CAACC,IAAI,CAACC,WAAW,CAACC,KAAK,CAAC,GAAG,CAAC;IAElD,MAAMlB,GAAG,GAAGR,YAAY,CAACC,GAAG,EAAEC,KAAK,EAAEC,IAAI,CAAC;IAE1C,OAAO;MACLwB,aAAa,EAAEnB,GAAG;MAClBU,eAAe,EAAE,MAAM;MACvBC,yBAAyB,EAAEH,OAAO,CAACI,OAAO,CAACC,IAAI,CAACC,eAAe,CAAC;IAClE,CAAC;EACH;AACF,CAAC,CACF","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["default","publicRoutes"],"sources":["../../../src/server/routes/index.ts"],"sourcesContent":["export { default as publicRoutes } from '~/src/server/routes/public.js'\n"],"mappings":"AAAA,SAASA,OAAO,IAAIC,YAAY","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"index.js","names":["default","publicRoutes","dummyApiRoutes"],"sources":["../../../src/server/routes/index.ts"],"sourcesContent":["export { default as publicRoutes } from '~/src/server/routes/public.js'\nexport { default as dummyApiRoutes } from '~/src/server/routes/dummy-api.js'\n"],"mappings":"AAAA,SAASA,OAAO,IAAIC,YAAY;AAChC,SAASD,OAAO,IAAIE,cAAc","ignoreList":[]}
|
|
@@ -14,7 +14,6 @@ export declare class CacheService {
|
|
|
14
14
|
}>;
|
|
15
15
|
generateKey?: (request: Request | FormRequest | FormRequestPayload) => string;
|
|
16
16
|
customFetcher?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState | null>;
|
|
17
|
-
customPersister?: (key: string, state: FormSubmissionState, request: Request | FormRequest | FormRequestPayload) => Promise<void>;
|
|
18
17
|
logger: Server['logger'];
|
|
19
18
|
constructor({ server, cacheName, options }: {
|
|
20
19
|
server: Server;
|
|
@@ -22,7 +21,6 @@ export declare class CacheService {
|
|
|
22
21
|
options?: {
|
|
23
22
|
keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string;
|
|
24
23
|
sessionHydrator?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState | null>;
|
|
25
|
-
sessionPersister?: (key: string, state: FormSubmissionState, request: Request | FormRequest | FormRequestPayload) => Promise<void>;
|
|
26
24
|
};
|
|
27
25
|
});
|
|
28
26
|
getState(request: Request | FormRequest | FormRequestPayload): Promise<FormSubmissionState>;
|
|
@@ -12,7 +12,6 @@ export class CacheService {
|
|
|
12
12
|
cache;
|
|
13
13
|
generateKey;
|
|
14
14
|
customFetcher;
|
|
15
|
-
customPersister;
|
|
16
15
|
logger;
|
|
17
16
|
constructor({
|
|
18
17
|
server,
|
|
@@ -21,15 +20,13 @@ export class CacheService {
|
|
|
21
20
|
}) {
|
|
22
21
|
const {
|
|
23
22
|
keyGenerator,
|
|
24
|
-
sessionHydrator
|
|
25
|
-
sessionPersister
|
|
23
|
+
sessionHydrator
|
|
26
24
|
} = options ?? {};
|
|
27
25
|
if (!cacheName) {
|
|
28
26
|
server.log('warn', 'You are using the default hapi cache. Please provide a cache name in plugin registration options.');
|
|
29
27
|
}
|
|
30
28
|
this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this);
|
|
31
29
|
this.customFetcher = sessionHydrator ?? undefined;
|
|
32
|
-
this.customPersister = sessionPersister ?? undefined;
|
|
33
30
|
this.cache = server.cache({
|
|
34
31
|
cache: cacheName,
|
|
35
32
|
segment: 'formSubmission'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cacheService.js","names":["Hoek","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","generateKey","customFetcher","customPersister","logger","constructor","server","cacheName","options","keyGenerator","sessionHydrator","sessionPersister","log","defaultKeyGenerator","bind","undefined","segment","getState","request","key","Key","cached","get","rehydrated","set","setState","state","ttl","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","Error","params","slug","additionalIdentifier","baseKey","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Request, 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 FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/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 generateKey?: (request: Request | FormRequest | FormRequestPayload) => string\n customFetcher?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n\n customPersister?: (\n key: string,\n state: FormSubmissionState,\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<void>\n\n logger: Server['logger']\n\n constructor({\n server,\n cacheName,\n options\n }: {\n server: Server\n cacheName?: string\n options?: {\n keyGenerator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n sessionPersister?: (\n key: string,\n state: FormSubmissionState,\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<void>\n }\n }) {\n const { keyGenerator, sessionHydrator, sessionPersister } = options ?? {}\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 this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this)\n this.customFetcher = sessionHydrator ?? undefined\n this.customPersister = sessionPersister ?? undefined\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(\n request: Request | FormRequest | FormRequestPayload\n ): Promise<FormSubmissionState> {\n const key = this.Key(request)\n\n let cached = await this.cache.get(key)\n\n // If nothing in Redis, attempt to rehydrate from backend DB\n if (!cached && this.customFetcher) {\n const rehydrated = await this.customFetcher(request)\n\n if (rehydrated != null) {\n await this.cache.set(key, rehydrated, config.get('sessionTimeout'))\n cached = await this.getState(request)\n }\n }\n\n return cached ?? {}\n }\n\n async setState(\n request: FormRequest | FormRequestPayload,\n state: FormSubmissionState\n ) {\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: FormRequest | FormRequestPayload\n ): Promise<{ confirmed?: true }> {\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: FormRequest | FormRequestPayload,\n confirmationState: { confirmed?: true }\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: FormRequest | FormRequestPayload) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: FormRequest | FormRequestPayload\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) as { errors: FormSubmissionError[] }\n }\n }\n\n setFlash(\n request: FormRequest | FormRequestPayload,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n private defaultKeyGenerator(\n request: Request | FormRequest | FormRequestPayload\n ): string {\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 return `${request.yar.id}:${state}:${slug}:`\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(\n request: Request | FormRequest | FormRequestPayload,\n additionalIdentifier?: ADDITIONAL_IDENTIFIER\n ) {\n const baseKey = this.generateKey\n ? this.generateKey(request)\n : this.defaultKeyGenerator(request)\n\n return {\n segment: partition,\n id: `${baseKey}${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":[]}
|
|
1
|
+
{"version":3,"file":"cacheService.js","names":["Hoek","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","generateKey","customFetcher","logger","constructor","server","cacheName","options","keyGenerator","sessionHydrator","log","defaultKeyGenerator","bind","undefined","segment","getState","request","key","Key","cached","get","rehydrated","set","setState","state","ttl","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","Error","params","slug","additionalIdentifier","baseKey","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Request, 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 FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/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 generateKey?: (request: Request | FormRequest | FormRequestPayload) => string\n customFetcher?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n\n logger: Server['logger']\n\n constructor({\n server,\n cacheName,\n options\n }: {\n server: Server\n cacheName?: string\n options?: {\n keyGenerator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n }\n }) {\n const { keyGenerator, sessionHydrator } = options ?? {}\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 this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this)\n this.customFetcher = sessionHydrator ?? undefined\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(\n request: Request | FormRequest | FormRequestPayload\n ): Promise<FormSubmissionState> {\n const key = this.Key(request)\n\n let cached = await this.cache.get(key)\n\n // If nothing in Redis, attempt to rehydrate from backend DB\n if (!cached && this.customFetcher) {\n const rehydrated = await this.customFetcher(request)\n\n if (rehydrated != null) {\n await this.cache.set(key, rehydrated, config.get('sessionTimeout'))\n cached = await this.getState(request)\n }\n }\n\n return cached ?? {}\n }\n\n async setState(\n request: FormRequest | FormRequestPayload,\n state: FormSubmissionState\n ) {\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: FormRequest | FormRequestPayload\n ): Promise<{ confirmed?: true }> {\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: FormRequest | FormRequestPayload,\n confirmationState: { confirmed?: true }\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: FormRequest | FormRequestPayload) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: FormRequest | FormRequestPayload\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) as { errors: FormSubmissionError[] }\n }\n }\n\n setFlash(\n request: FormRequest | FormRequestPayload,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n private defaultKeyGenerator(\n request: Request | FormRequest | FormRequestPayload\n ): string {\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 return `${request.yar.id}:${state}:${slug}:`\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(\n request: Request | FormRequest | FormRequestPayload,\n additionalIdentifier?: ADDITIONAL_IDENTIFIER\n ) {\n const baseKey = this.generateKey\n ? this.generateKey(request)\n : this.defaultKeyGenerator(request)\n\n return {\n segment: partition,\n id: `${baseKey}${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": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Defra forms engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"license": "SEE LICENSE IN LICENSE",
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@defra/forms-model": "^3.0.506",
|
|
67
|
-
"@defra/hapi-tracing": "^1.
|
|
67
|
+
"@defra/hapi-tracing": "^1.26.0",
|
|
68
68
|
"@elastic/ecs-pino-format": "^1.5.0",
|
|
69
69
|
"@hapi/boom": "^10.0.1",
|
|
70
70
|
"@hapi/catbox": "^12.1.1",
|
|
@@ -168,6 +168,8 @@
|
|
|
168
168
|
"jest-extended": "^4.0.2",
|
|
169
169
|
"jsdom": "^26.1.0",
|
|
170
170
|
"lint-staged": "^15.3.0",
|
|
171
|
+
"mockdate": "^3.0.5",
|
|
172
|
+
"nock": "^14.0.8",
|
|
171
173
|
"postcss": "^8.5.6",
|
|
172
174
|
"postcss-load-config": "^6.0.1",
|
|
173
175
|
"postcss-loader": "^8.1.1",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Page events
|
|
3
|
+
engine: V2
|
|
4
|
+
schema: 2
|
|
5
|
+
startPage: '/summary'
|
|
6
|
+
pages:
|
|
7
|
+
- title: Your name
|
|
8
|
+
path: '/your-name'
|
|
9
|
+
components:
|
|
10
|
+
- type: TextField
|
|
11
|
+
title: What is your first name?
|
|
12
|
+
name: applicantFirstName
|
|
13
|
+
shortDescription: Your first name
|
|
14
|
+
hint: ''
|
|
15
|
+
options:
|
|
16
|
+
required: true
|
|
17
|
+
schema: {}
|
|
18
|
+
id: 1fb8e182-c709-4792-8f83-e01d8b1fee1a
|
|
19
|
+
- type: TextField
|
|
20
|
+
title: What is your last name?
|
|
21
|
+
name: applicantLastName
|
|
22
|
+
shortDescription: Your last name
|
|
23
|
+
hint: ''
|
|
24
|
+
options:
|
|
25
|
+
required: true
|
|
26
|
+
schema: {}
|
|
27
|
+
id: b68df7f1-d4f4-4c17-83c8-402f584906c9
|
|
28
|
+
next: []
|
|
29
|
+
id: 622a35ec-3795-418a-81f3-a45746959045
|
|
30
|
+
- title: ''
|
|
31
|
+
path: '/date-of-birth'
|
|
32
|
+
events:
|
|
33
|
+
onLoad:
|
|
34
|
+
type: http
|
|
35
|
+
options:
|
|
36
|
+
method: 'POST'
|
|
37
|
+
url: http://localhost:3009/api/example/on-load-page
|
|
38
|
+
components:
|
|
39
|
+
- type: Html
|
|
40
|
+
# technically context.data.submissionReferenceNumber is redundant as the reference number is available locally as context.referenceNumber
|
|
41
|
+
# but this is to demonstrate that the server can send data back to the client
|
|
42
|
+
content: >
|
|
43
|
+
<p class="govuk-body">
|
|
44
|
+
The backend received a full copy of the form state even though you haven't submitted the form yet.
|
|
45
|
+
</p>
|
|
46
|
+
<p class="govuk-body">
|
|
47
|
+
Your submission was received with the reference: <strong>{{ context.data.submissionReferenceNumber }}</strong>.
|
|
48
|
+
</p>
|
|
49
|
+
id: 334b10dc-3373-4928-8fed-575578a67de8
|
|
50
|
+
- type: DatePartsField
|
|
51
|
+
title: When is {{ applicantFirstName }} {{ applicantLastName }}'s birthday?
|
|
52
|
+
name: dateOfBirth
|
|
53
|
+
shortDescription: Your birthday
|
|
54
|
+
hint: ''
|
|
55
|
+
options:
|
|
56
|
+
required: true
|
|
57
|
+
schema: {}
|
|
58
|
+
id: '00738799-3489-4ab2-a57b-542eecb31bfa'
|
|
59
|
+
next: []
|
|
60
|
+
id: da0fbdb4-a2de-4650-be16-9ba552af135f
|
|
61
|
+
- id: 449a45f6-4541-4a46-91bd-8b8931b07b50
|
|
62
|
+
title: Summary
|
|
63
|
+
path: '/summary'
|
|
64
|
+
controller: SummaryPageController
|
|
65
|
+
events:
|
|
66
|
+
onLoad:
|
|
67
|
+
type: http
|
|
68
|
+
options:
|
|
69
|
+
method: 'POST'
|
|
70
|
+
url: http://localhost:3009/api/example/on-summary
|
|
71
|
+
onSave:
|
|
72
|
+
type: http
|
|
73
|
+
options:
|
|
74
|
+
method: 'POST'
|
|
75
|
+
url: http://localhost:3009/api/example/on-summary
|
|
76
|
+
components:
|
|
77
|
+
- type: Html
|
|
78
|
+
content: >
|
|
79
|
+
<h2 class="govuk-heading-m">Your age</h1>
|
|
80
|
+
<p class="govuk-body">
|
|
81
|
+
We've calculated that you are {{ context.data.calculatedAge }} years old. Only proceed if this is correct.
|
|
82
|
+
</p>
|
|
83
|
+
id: c42ea488-a38c-4b67-b8fa-4cf543a4f82d
|
|
84
|
+
next: []
|
|
85
|
+
conditions: []
|
|
86
|
+
sections: []
|
|
87
|
+
lists: []
|
package/src/server/index.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { Engine as CatboxRedis } from '@hapi/catbox-redis'
|
|
|
3
3
|
import hapi, {
|
|
4
4
|
type Request,
|
|
5
5
|
type ResponseToolkit,
|
|
6
|
-
type ServerOptions
|
|
6
|
+
type ServerOptions,
|
|
7
|
+
type ServerRoute
|
|
7
8
|
} from '@hapi/hapi'
|
|
8
9
|
import inert from '@hapi/inert'
|
|
9
10
|
import Scooter from '@hapi/scooter'
|
|
@@ -21,7 +22,7 @@ import pluginErrorPages from '~/src/server/plugins/errorPages.js'
|
|
|
21
22
|
import { plugin as pluginViews } from '~/src/server/plugins/nunjucks/index.js'
|
|
22
23
|
import pluginPulse from '~/src/server/plugins/pulse.js'
|
|
23
24
|
import pluginSession from '~/src/server/plugins/session.js'
|
|
24
|
-
import { publicRoutes } from '~/src/server/routes/index.js'
|
|
25
|
+
import { dummyApiRoutes, publicRoutes } from '~/src/server/routes/index.js'
|
|
25
26
|
import { prepareSecureContext } from '~/src/server/secure-context.js'
|
|
26
27
|
import { type RouteConfig } from '~/src/server/types.js'
|
|
27
28
|
|
|
@@ -120,6 +121,7 @@ export async function createServer(routeConfig?: RouteConfig) {
|
|
|
120
121
|
name: 'router',
|
|
121
122
|
register: (server) => {
|
|
122
123
|
server.route(publicRoutes)
|
|
124
|
+
server.route(dummyApiRoutes as ServerRoute[])
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
2
|
+
|
|
2
3
|
import { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'
|
|
3
4
|
import { type Field } from '~/src/server/plugins/engine/components/helpers.js'
|
|
4
5
|
import { FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
@@ -8,11 +9,11 @@ import {
|
|
|
8
9
|
type DetailItemRepeat
|
|
9
10
|
} from '~/src/server/plugins/engine/models/types.js'
|
|
10
11
|
import { format } from '~/src/server/plugins/engine/outputFormatters/machine/v1.js'
|
|
12
|
+
import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
|
|
11
13
|
import {
|
|
12
14
|
FileStatus,
|
|
13
15
|
UploadStatus,
|
|
14
|
-
type FileState
|
|
15
|
-
type FormContextRequest
|
|
16
|
+
type FileState
|
|
16
17
|
} from '~/src/server/plugins/engine/types.js'
|
|
17
18
|
import { FormStatus } from '~/src/server/routes/types.js'
|
|
18
19
|
import definition from '~/test/form/definitions/repeat-mixed.js'
|
|
@@ -64,7 +65,7 @@ const state = {
|
|
|
64
65
|
|
|
65
66
|
const pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
|
|
66
67
|
|
|
67
|
-
const request = {
|
|
68
|
+
const request = buildFormContextRequest({
|
|
68
69
|
method: 'get',
|
|
69
70
|
url: pageUrl,
|
|
70
71
|
path: pageUrl.pathname,
|
|
@@ -74,7 +75,7 @@ const request = {
|
|
|
74
75
|
},
|
|
75
76
|
query: {},
|
|
76
77
|
app: { model }
|
|
77
|
-
}
|
|
78
|
+
})
|
|
78
79
|
|
|
79
80
|
const context = model.getFormContext(request, state)
|
|
80
81
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
2
|
+
|
|
2
3
|
import { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'
|
|
3
4
|
import { type Field } from '~/src/server/plugins/engine/components/helpers.js'
|
|
4
5
|
import { FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
@@ -8,11 +9,11 @@ import {
|
|
|
8
9
|
type DetailItemRepeat
|
|
9
10
|
} from '~/src/server/plugins/engine/models/types.js'
|
|
10
11
|
import { format } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
|
|
12
|
+
import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
|
|
11
13
|
import {
|
|
12
14
|
FileStatus,
|
|
13
15
|
UploadStatus,
|
|
14
|
-
type FileState
|
|
15
|
-
type FormContextRequest
|
|
16
|
+
type FileState
|
|
16
17
|
} from '~/src/server/plugins/engine/types.js'
|
|
17
18
|
import { FormStatus } from '~/src/server/routes/types.js'
|
|
18
19
|
import definition from '~/test/form/definitions/repeat-mixed.js'
|
|
@@ -64,7 +65,7 @@ const state = {
|
|
|
64
65
|
|
|
65
66
|
const pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
|
|
66
67
|
|
|
67
|
-
const request = {
|
|
68
|
+
const request = buildFormContextRequest({
|
|
68
69
|
method: 'get',
|
|
69
70
|
url: pageUrl,
|
|
70
71
|
path: pageUrl.pathname,
|
|
@@ -74,7 +75,7 @@ const request = {
|
|
|
74
75
|
},
|
|
75
76
|
query: {},
|
|
76
77
|
app: { model }
|
|
77
|
-
}
|
|
78
|
+
})
|
|
78
79
|
|
|
79
80
|
const context = model.getFormContext(request, state)
|
|
80
81
|
|