@defra/forms-engine-plugin 3.0.3 → 3.0.5

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.
Files changed (38) hide show
  1. package/.server/server/common/helpers/redis-client.js +2 -3
  2. package/.server/server/common/helpers/redis-client.js.map +1 -1
  3. package/.server/server/plugins/engine/helpers.js +1 -2
  4. package/.server/server/plugins/engine/helpers.js.map +1 -1
  5. package/.server/server/plugins/engine/models/__snapshots__/SummaryViewModel.test.ts.snap +52 -0
  6. package/.server/server/plugins/engine/options.js +2 -1
  7. package/.server/server/plugins/engine/options.js.map +1 -1
  8. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +15 -5
  9. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
  10. package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +38 -1
  11. package/.server/server/plugins/engine/outputFormatters/machine/v2.js +1 -1
  12. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  13. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +2 -2
  14. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  15. package/.server/server/plugins/engine/routes/file-upload.js +2 -3
  16. package/.server/server/plugins/engine/routes/file-upload.js.map +1 -1
  17. package/.server/server/plugins/engine/services/notifyService.js +1 -2
  18. package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
  19. package/.server/server/plugins/engine/types.d.ts +1 -1
  20. package/.server/server/plugins/engine/types.js.map +1 -1
  21. package/.server/server/plugins/errorPages.js +3 -3
  22. package/.server/server/plugins/errorPages.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/server/common/helpers/redis-client.js +5 -3
  25. package/src/server/plugins/engine/helpers.ts +2 -3
  26. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +142 -112
  27. package/src/server/plugins/engine/models/__snapshots__/SummaryViewModel.test.ts.snap +52 -0
  28. package/src/server/plugins/engine/options.js +3 -1
  29. package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +40 -0
  30. package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +17 -14
  31. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +41 -0
  32. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +1 -1
  33. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +2 -2
  34. package/src/server/plugins/engine/routes/file-upload.ts +3 -4
  35. package/src/server/plugins/engine/services/notifyService.test.ts +1 -1
  36. package/src/server/plugins/engine/services/notifyService.ts +2 -3
  37. package/src/server/plugins/engine/types.ts +1 -1
  38. package/src/server/plugins/errorPages.ts +3 -3
@@ -45,8 +45,7 @@ export async function submit(context, request, model, emailAddress, items, submi
45
45
  });
46
46
  request.logger.info(logTags, 'Email sent successfully');
47
47
  } catch (err) {
48
- const errMsg = getErrorMessage(err);
49
- request.logger.error(errMsg, `[emailSendFailed] Error sending notification email - templateId: ${templateId} - recipient: ${emailAddress} - ${errMsg}`);
48
+ request.logger.error(err, `[emailSendFailed] Error sending notification email - templateId: ${templateId} - recipient: ${emailAddress} - ${getErrorMessage(err)}`);
50
49
  throw err;
51
50
  }
52
51
  }
@@ -1 +1 @@
1
- {"version":3,"file":"notifyService.js","names":["getErrorMessage","config","escapeMarkdown","checkFormStatus","getFormatter","sendNotification","templateId","get","submit","context","request","model","emailAddress","items","submitResponse","formMetadata","Promise","resolve","logTags","formStatus","params","logger","info","formName","name","subject","isPreview","outputAudience","def","output","audience","outputVersion","version","outputFormatter","body","Buffer","from","toString","personalisation","err","errMsg","error"],"sources":["../../../../../src/server/plugins/engine/services/notifyService.ts"],"sourcesContent":["import {\n getErrorMessage,\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { config } from '~/src/config/index.js'\nimport { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers/index.js'\nimport { checkFormStatus } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { getFormatter } from '~/src/server/plugins/engine/outputFormatters/index.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport { type FormRequestPayload } from '~/src/server/routes/types.js'\nimport { sendNotification } from '~/src/server/utils/notify.js'\n\nconst templateId = config.get('notifyTemplateId')\n\n/**\n * Optional GOV.UK Notify service for consumers who want email notifications\n * Can be disabled by not providing notifyTemplateId in config\n * Can be overridden by providing a custom outputService in the services config\n */\nexport async function submit(\n context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload,\n formMetadata?: FormMetadata\n) {\n if (!templateId) {\n return Promise.resolve()\n }\n\n const logTags = ['submit', 'email']\n const formStatus = checkFormStatus(request.params)\n\n // Get submission email personalisation\n request.logger.info(logTags, 'Getting personalisation data')\n\n const formName = escapeMarkdown(model.name)\n const subject = formStatus.isPreview\n ? `TEST FORM SUBMISSION: ${formName}`\n : `Form submission: ${formName}`\n\n const outputAudience = model.def.output?.audience ?? 'human'\n const outputVersion = model.def.output?.version ?? '1'\n\n const outputFormatter = getFormatter(outputAudience, outputVersion)\n let body = outputFormatter(\n context,\n items,\n model,\n submitResponse,\n formStatus,\n formMetadata\n )\n\n // GOV.UK Notify transforms quotes into curly quotes, so we can't just send the raw payload\n // This is logic specific to Notify, so we include the logic here rather than in the formatter\n if (outputAudience === 'machine') {\n body = Buffer.from(body).toString('base64')\n }\n\n request.logger.info(logTags, 'Sending email')\n\n try {\n // Send submission email\n await sendNotification({\n templateId,\n emailAddress,\n personalisation: {\n subject,\n body\n }\n })\n\n request.logger.info(logTags, 'Email sent successfully')\n } catch (err) {\n const errMsg = getErrorMessage(err)\n request.logger.error(\n errMsg,\n `[emailSendFailed] Error sending notification email - templateId: ${templateId} - recipient: ${emailAddress} - ${errMsg}`\n )\n\n throw err\n }\n}\n"],"mappings":"AAAA,SACEA,eAAe,QAGV,oBAAoB;AAE3B,SAASC,MAAM;AACf,SAASC,cAAc;AACvB,SAASC,eAAe;AAGxB,SAASC,YAAY;AAGrB,SAASC,gBAAgB;AAEzB,MAAMC,UAAU,GAAGL,MAAM,CAACM,GAAG,CAAC,kBAAkB,CAAC;;AAEjD;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,MAAMA,CAC1BC,OAAoB,EACpBC,OAA2B,EAC3BC,KAAgB,EAChBC,YAAoB,EACpBC,KAAmB,EACnBC,cAAqC,EACrCC,YAA2B,EAC3B;EACA,IAAI,CAACT,UAAU,EAAE;IACf,OAAOU,OAAO,CAACC,OAAO,CAAC,CAAC;EAC1B;EAEA,MAAMC,OAAO,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC;EACnC,MAAMC,UAAU,GAAGhB,eAAe,CAACO,OAAO,CAACU,MAAM,CAAC;;EAElD;EACAV,OAAO,CAACW,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,8BAA8B,CAAC;EAE5D,MAAMK,QAAQ,GAAGrB,cAAc,CAACS,KAAK,CAACa,IAAI,CAAC;EAC3C,MAAMC,OAAO,GAAGN,UAAU,CAACO,SAAS,GAChC,yBAAyBH,QAAQ,EAAE,GACnC,oBAAoBA,QAAQ,EAAE;EAElC,MAAMI,cAAc,GAAGhB,KAAK,CAACiB,GAAG,CAACC,MAAM,EAAEC,QAAQ,IAAI,OAAO;EAC5D,MAAMC,aAAa,GAAGpB,KAAK,CAACiB,GAAG,CAACC,MAAM,EAAEG,OAAO,IAAI,GAAG;EAEtD,MAAMC,eAAe,GAAG7B,YAAY,CAACuB,cAAc,EAAEI,aAAa,CAAC;EACnE,IAAIG,IAAI,GAAGD,eAAe,CACxBxB,OAAO,EACPI,KAAK,EACLF,KAAK,EACLG,cAAc,EACdK,UAAU,EACVJ,YACF,CAAC;;EAED;EACA;EACA,IAAIY,cAAc,KAAK,SAAS,EAAE;IAChCO,IAAI,GAAGC,MAAM,CAACC,IAAI,CAACF,IAAI,CAAC,CAACG,QAAQ,CAAC,QAAQ,CAAC;EAC7C;EAEA3B,OAAO,CAACW,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,eAAe,CAAC;EAE7C,IAAI;IACF;IACA,MAAMb,gBAAgB,CAAC;MACrBC,UAAU;MACVM,YAAY;MACZ0B,eAAe,EAAE;QACfb,OAAO;QACPS;MACF;IACF,CAAC,CAAC;IAEFxB,OAAO,CAACW,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,yBAAyB,CAAC;EACzD,CAAC,CAAC,OAAOqB,GAAG,EAAE;IACZ,MAAMC,MAAM,GAAGxC,eAAe,CAACuC,GAAG,CAAC;IACnC7B,OAAO,CAACW,MAAM,CAACoB,KAAK,CAClBD,MAAM,EACN,oEAAoElC,UAAU,iBAAiBM,YAAY,MAAM4B,MAAM,EACzH,CAAC;IAED,MAAMD,GAAG;EACX;AACF","ignoreList":[]}
1
+ {"version":3,"file":"notifyService.js","names":["getErrorMessage","config","escapeMarkdown","checkFormStatus","getFormatter","sendNotification","templateId","get","submit","context","request","model","emailAddress","items","submitResponse","formMetadata","Promise","resolve","logTags","formStatus","params","logger","info","formName","name","subject","isPreview","outputAudience","def","output","audience","outputVersion","version","outputFormatter","body","Buffer","from","toString","personalisation","err","error"],"sources":["../../../../../src/server/plugins/engine/services/notifyService.ts"],"sourcesContent":["import {\n getErrorMessage,\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { config } from '~/src/config/index.js'\nimport { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers/index.js'\nimport { checkFormStatus } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { getFormatter } from '~/src/server/plugins/engine/outputFormatters/index.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport { type FormRequestPayload } from '~/src/server/routes/types.js'\nimport { sendNotification } from '~/src/server/utils/notify.js'\n\nconst templateId = config.get('notifyTemplateId')\n\n/**\n * Optional GOV.UK Notify service for consumers who want email notifications\n * Can be disabled by not providing notifyTemplateId in config\n * Can be overridden by providing a custom outputService in the services config\n */\nexport async function submit(\n context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload,\n formMetadata?: FormMetadata\n) {\n if (!templateId) {\n return Promise.resolve()\n }\n\n const logTags = ['submit', 'email']\n const formStatus = checkFormStatus(request.params)\n\n // Get submission email personalisation\n request.logger.info(logTags, 'Getting personalisation data')\n\n const formName = escapeMarkdown(model.name)\n const subject = formStatus.isPreview\n ? `TEST FORM SUBMISSION: ${formName}`\n : `Form submission: ${formName}`\n\n const outputAudience = model.def.output?.audience ?? 'human'\n const outputVersion = model.def.output?.version ?? '1'\n\n const outputFormatter = getFormatter(outputAudience, outputVersion)\n let body = outputFormatter(\n context,\n items,\n model,\n submitResponse,\n formStatus,\n formMetadata\n )\n\n // GOV.UK Notify transforms quotes into curly quotes, so we can't just send the raw payload\n // This is logic specific to Notify, so we include the logic here rather than in the formatter\n if (outputAudience === 'machine') {\n body = Buffer.from(body).toString('base64')\n }\n\n request.logger.info(logTags, 'Sending email')\n\n try {\n // Send submission email\n await sendNotification({\n templateId,\n emailAddress,\n personalisation: {\n subject,\n body\n }\n })\n\n request.logger.info(logTags, 'Email sent successfully')\n } catch (err) {\n request.logger.error(\n err,\n `[emailSendFailed] Error sending notification email - templateId: ${templateId} - recipient: ${emailAddress} - ${getErrorMessage(err)}`\n )\n\n throw err\n }\n}\n"],"mappings":"AAAA,SACEA,eAAe,QAGV,oBAAoB;AAE3B,SAASC,MAAM;AACf,SAASC,cAAc;AACvB,SAASC,eAAe;AAGxB,SAASC,YAAY;AAGrB,SAASC,gBAAgB;AAEzB,MAAMC,UAAU,GAAGL,MAAM,CAACM,GAAG,CAAC,kBAAkB,CAAC;;AAEjD;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,MAAMA,CAC1BC,OAAoB,EACpBC,OAA2B,EAC3BC,KAAgB,EAChBC,YAAoB,EACpBC,KAAmB,EACnBC,cAAqC,EACrCC,YAA2B,EAC3B;EACA,IAAI,CAACT,UAAU,EAAE;IACf,OAAOU,OAAO,CAACC,OAAO,CAAC,CAAC;EAC1B;EAEA,MAAMC,OAAO,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC;EACnC,MAAMC,UAAU,GAAGhB,eAAe,CAACO,OAAO,CAACU,MAAM,CAAC;;EAElD;EACAV,OAAO,CAACW,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,8BAA8B,CAAC;EAE5D,MAAMK,QAAQ,GAAGrB,cAAc,CAACS,KAAK,CAACa,IAAI,CAAC;EAC3C,MAAMC,OAAO,GAAGN,UAAU,CAACO,SAAS,GAChC,yBAAyBH,QAAQ,EAAE,GACnC,oBAAoBA,QAAQ,EAAE;EAElC,MAAMI,cAAc,GAAGhB,KAAK,CAACiB,GAAG,CAACC,MAAM,EAAEC,QAAQ,IAAI,OAAO;EAC5D,MAAMC,aAAa,GAAGpB,KAAK,CAACiB,GAAG,CAACC,MAAM,EAAEG,OAAO,IAAI,GAAG;EAEtD,MAAMC,eAAe,GAAG7B,YAAY,CAACuB,cAAc,EAAEI,aAAa,CAAC;EACnE,IAAIG,IAAI,GAAGD,eAAe,CACxBxB,OAAO,EACPI,KAAK,EACLF,KAAK,EACLG,cAAc,EACdK,UAAU,EACVJ,YACF,CAAC;;EAED;EACA;EACA,IAAIY,cAAc,KAAK,SAAS,EAAE;IAChCO,IAAI,GAAGC,MAAM,CAACC,IAAI,CAACF,IAAI,CAAC,CAACG,QAAQ,CAAC,QAAQ,CAAC;EAC7C;EAEA3B,OAAO,CAACW,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,eAAe,CAAC;EAE7C,IAAI;IACF;IACA,MAAMb,gBAAgB,CAAC;MACrBC,UAAU;MACVM,YAAY;MACZ0B,eAAe,EAAE;QACfb,OAAO;QACPS;MACF;IACF,CAAC,CAAC;IAEFxB,OAAO,CAACW,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,yBAAyB,CAAC;EACzD,CAAC,CAAC,OAAOqB,GAAG,EAAE;IACZ7B,OAAO,CAACW,MAAM,CAACmB,KAAK,CAClBD,GAAG,EACH,oEAAoEjC,UAAU,iBAAiBM,YAAY,MAAMZ,eAAe,CAACuC,GAAG,CAAC,EACvI,CAAC;IAED,MAAMA,GAAG;EACX;AACF","ignoreList":[]}
@@ -317,7 +317,7 @@ export type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {
317
317
  };
318
318
  export type RichFormValue = FormValue | FormPayload | DatePartsState | MonthYearState | UkAddressState;
319
319
  export interface FormAdapterSubmissionMessageData {
320
- main: Record<string, RichFormValue>;
320
+ main: Record<string, RichFormValue | null>;
321
321
  repeaters: Record<string, Record<string, RichFormValue>[]>;
322
322
  files: Record<string, FormAdapterFile[]>;
323
323
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage\n extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAqDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AA8Nd;AACA;AACA","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FileUploadField } from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n submittedVersionNumber?: number\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<\n FormRequest,\n 'app' | 'method' | 'params' | 'path' | 'query' | 'url' | 'server'\n >\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport {\n FileStatus,\n UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n allowSaveAndExit: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cache?: CacheService | string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n saveAndExit?: SaveAndExitHandler\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: FormAdapterSubmissionSchemaVersion\n timestamp: Date\n referenceNumber: string\n formName: string\n formId: string\n formSlug: string\n status: FormStatus\n isPreview: boolean\n notificationEmail: string\n versionMetadata?: FormVersionMetadata\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterSubmissionMessageResult {\n files: {\n main: string\n repeaters: Record<string, string>\n }\n}\n\n/**\n * A detail item specifically for files\n */\nexport type FileUploadFieldDetailitem = Omit<DetailItemField, 'field'> & {\n field: FileUploadField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue | null>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage\n extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAqDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAsGA,SACEA,UAAU,EACVC,YAAY;;AA8Nd;AACA;AACA","ignoreList":[]}
@@ -11,13 +11,13 @@ export default {
11
11
  // An error was raised during
12
12
  // processing the request
13
13
  const statusCode = response.output.statusCode;
14
- const error = {
14
+ const err = {
15
15
  statusCode,
16
16
  message: response.message,
17
17
  stack: response.stack
18
18
  };
19
- request.logger.error(error, `[httpError] HTTP ${statusCode} error occurred - ${response.message} - path: ${request.path} - method: ${request.method}`);
20
- return h.response(error).code(statusCode);
19
+ request.logger.error(err, `[httpError] HTTP ${statusCode} error occurred - ${response.message} - path: ${request.path} - method: ${request.method}`);
20
+ return h.response(err).code(statusCode);
21
21
  }
22
22
  return h.continue;
23
23
  });
@@ -1 +1 @@
1
- {"version":3,"file":"errorPages.js","names":["plugin","name","register","server","ext","request","h","response","isBoom","statusCode","output","error","message","stack","logger","path","method","code","continue"],"sources":["../../../src/server/plugins/errorPages.ts"],"sourcesContent":["import {\n type Request,\n type ResponseToolkit,\n type ServerRegisterPluginObject\n} from '@hapi/hapi'\n\n/*\n * Add an `onPreResponse` listener to return error pages\n */\nexport default {\n plugin: {\n name: 'error-pages',\n register(server) {\n server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {\n const response = request.response\n\n if ('isBoom' in response && response.isBoom) {\n // An error was raised during\n // processing the request\n const statusCode = response.output.statusCode\n\n const error = {\n statusCode,\n message: response.message,\n stack: response.stack\n }\n\n request.logger.error(\n error,\n `[httpError] HTTP ${statusCode} error occurred - ${response.message} - path: ${request.path} - method: ${request.method}`\n )\n\n return h.response(error).code(statusCode)\n }\n\n return h.continue\n })\n }\n }\n} satisfies ServerRegisterPluginObject<void>\n"],"mappings":"AAMA;AACA;AACA;AACA,eAAe;EACbA,MAAM,EAAE;IACNC,IAAI,EAAE,aAAa;IACnBC,QAAQA,CAACC,MAAM,EAAE;MACfA,MAAM,CAACC,GAAG,CAAC,eAAe,EAAE,CAACC,OAAgB,EAAEC,CAAkB,KAAK;QACpE,MAAMC,QAAQ,GAAGF,OAAO,CAACE,QAAQ;QAEjC,IAAI,QAAQ,IAAIA,QAAQ,IAAIA,QAAQ,CAACC,MAAM,EAAE;UAC3C;UACA;UACA,MAAMC,UAAU,GAAGF,QAAQ,CAACG,MAAM,CAACD,UAAU;UAE7C,MAAME,KAAK,GAAG;YACZF,UAAU;YACVG,OAAO,EAAEL,QAAQ,CAACK,OAAO;YACzBC,KAAK,EAAEN,QAAQ,CAACM;UAClB,CAAC;UAEDR,OAAO,CAACS,MAAM,CAACH,KAAK,CAClBA,KAAK,EACL,oBAAoBF,UAAU,qBAAqBF,QAAQ,CAACK,OAAO,YAAYP,OAAO,CAACU,IAAI,cAAcV,OAAO,CAACW,MAAM,EACzH,CAAC;UAED,OAAOV,CAAC,CAACC,QAAQ,CAACI,KAAK,CAAC,CAACM,IAAI,CAACR,UAAU,CAAC;QAC3C;QAEA,OAAOH,CAAC,CAACY,QAAQ;MACnB,CAAC,CAAC;IACJ;EACF;AACF,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"errorPages.js","names":["plugin","name","register","server","ext","request","h","response","isBoom","statusCode","output","err","message","stack","logger","error","path","method","code","continue"],"sources":["../../../src/server/plugins/errorPages.ts"],"sourcesContent":["import {\n type Request,\n type ResponseToolkit,\n type ServerRegisterPluginObject\n} from '@hapi/hapi'\n\n/*\n * Add an `onPreResponse` listener to return error pages\n */\nexport default {\n plugin: {\n name: 'error-pages',\n register(server) {\n server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {\n const response = request.response\n\n if ('isBoom' in response && response.isBoom) {\n // An error was raised during\n // processing the request\n const statusCode = response.output.statusCode\n\n const err = {\n statusCode,\n message: response.message,\n stack: response.stack\n }\n\n request.logger.error(\n err,\n `[httpError] HTTP ${statusCode} error occurred - ${response.message} - path: ${request.path} - method: ${request.method}`\n )\n\n return h.response(err).code(statusCode)\n }\n\n return h.continue\n })\n }\n }\n} satisfies ServerRegisterPluginObject<void>\n"],"mappings":"AAMA;AACA;AACA;AACA,eAAe;EACbA,MAAM,EAAE;IACNC,IAAI,EAAE,aAAa;IACnBC,QAAQA,CAACC,MAAM,EAAE;MACfA,MAAM,CAACC,GAAG,CAAC,eAAe,EAAE,CAACC,OAAgB,EAAEC,CAAkB,KAAK;QACpE,MAAMC,QAAQ,GAAGF,OAAO,CAACE,QAAQ;QAEjC,IAAI,QAAQ,IAAIA,QAAQ,IAAIA,QAAQ,CAACC,MAAM,EAAE;UAC3C;UACA;UACA,MAAMC,UAAU,GAAGF,QAAQ,CAACG,MAAM,CAACD,UAAU;UAE7C,MAAME,GAAG,GAAG;YACVF,UAAU;YACVG,OAAO,EAAEL,QAAQ,CAACK,OAAO;YACzBC,KAAK,EAAEN,QAAQ,CAACM;UAClB,CAAC;UAEDR,OAAO,CAACS,MAAM,CAACC,KAAK,CAClBJ,GAAG,EACH,oBAAoBF,UAAU,qBAAqBF,QAAQ,CAACK,OAAO,YAAYP,OAAO,CAACW,IAAI,cAAcX,OAAO,CAACY,MAAM,EACzH,CAAC;UAED,OAAOX,CAAC,CAACC,QAAQ,CAACI,GAAG,CAAC,CAACO,IAAI,CAACT,UAAU,CAAC;QACzC;QAEA,OAAOH,CAAC,CAACa,QAAQ;MACnB,CAAC,CAAC;IACJ;EACF;AACF,CAAC","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -63,9 +63,11 @@ export function buildRedisClient() {
63
63
  )
64
64
  })
65
65
 
66
- redisClient.on('error', (error) => {
67
- const err = getErrorMessage(error)
68
- logger.error(err, `[redisConnectionError] Redis connection error - ${err}`)
66
+ redisClient.on('error', (err) => {
67
+ logger.error(
68
+ err,
69
+ `[redisConnectionError] Redis connection error - ${getErrorMessage(err)}`
70
+ )
69
71
  })
70
72
 
71
73
  return redisClient
@@ -149,10 +149,9 @@ export function encodeUrl(link?: string) {
149
149
  try {
150
150
  return new URL(link).toString() // escape the search params without breaking the ? and & reserved characters in rfc2368
151
151
  } catch (err) {
152
- const errMsg = getErrorMessage(err)
153
152
  logger.error(
154
- errMsg,
155
- `[urlEncodingFailed] Failed to encode URL: ${link} - ${errMsg}`
153
+ err,
154
+ `[urlEncodingFailed] Failed to encode URL: ${link} - ${getErrorMessage(err)}`
156
155
  )
157
156
  throw err
158
157
  }
@@ -65,7 +65,9 @@ describe('SummaryViewModel', () => {
65
65
  'Pizzas',
66
66
  'Pizza'
67
67
  ],
68
- values: ['Collection', 'Not supplied']
68
+ values: ['Collection', 'Not supplied'],
69
+ answers: ['Collection', ''],
70
+ names: ['orderType', 'pizza']
69
71
  },
70
72
  {
71
73
  description: '1 item',
@@ -87,7 +89,9 @@ describe('SummaryViewModel', () => {
87
89
  'Pizzas',
88
90
  'Pizza'
89
91
  ],
90
- values: ['Delivery', 'You added 1 Pizza']
92
+ values: ['Delivery', 'You added 1 Pizza'],
93
+ answers: ['Delivery', 'You added 1 Pizza'],
94
+ names: ['orderType', 'pizza']
91
95
  },
92
96
  {
93
97
  description: '2 items',
@@ -114,142 +118,168 @@ describe('SummaryViewModel', () => {
114
118
  'Pizzas',
115
119
  'Pizza'
116
120
  ],
117
- values: ['Delivery', 'You added 2 Pizzas']
121
+ values: ['Delivery', 'You added 2 Pizzas'],
122
+ answers: ['Delivery', 'You added 2 Pizzas'],
123
+ names: ['orderType', 'pizza']
118
124
  }
119
- ])('Check answers ($description)', ({ state, keys, values }) => {
120
- beforeEach(() => {
121
- context = model.getFormContext(request, state)
122
- summaryViewModel = new SummaryViewModel(request, page, context)
123
- })
125
+ ])(
126
+ 'Check answers ($description)',
127
+ ({ state, keys, values, names, answers }) => {
128
+ beforeEach(() => {
129
+ context = model.getFormContext(request, state)
130
+ summaryViewModel = new SummaryViewModel(request, page, context)
131
+ })
124
132
 
125
- it('should add title for each section', () => {
126
- const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
133
+ it('should add title for each section', () => {
134
+ const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
127
135
 
128
- // 1st summary list has no title
129
- expect(checkAnswers1).toHaveProperty('title', undefined)
136
+ // 1st summary list has no title
137
+ expect(checkAnswers1).toHaveProperty('title', undefined)
130
138
 
131
- // 2nd summary list has section title
132
- expect(checkAnswers2).toHaveProperty('title', {
133
- text: 'Food'
139
+ // 2nd summary list has section title
140
+ expect(checkAnswers2).toHaveProperty('title', {
141
+ text: 'Food'
142
+ })
134
143
  })
135
- })
136
144
 
137
- it('should add summary list for each section', () => {
138
- expect(summaryViewModel.checkAnswers).toHaveLength(2)
145
+ it('should add summary list for each section', () => {
146
+ expect(summaryViewModel.checkAnswers).toHaveLength(2)
139
147
 
140
- const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
148
+ const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
141
149
 
142
- const { summaryList: summaryList1 } = checkAnswers1
143
- const { summaryList: summaryList2 } = checkAnswers2
150
+ const { summaryList: summaryList1 } = checkAnswers1
151
+ const { summaryList: summaryList2 } = checkAnswers2
144
152
 
145
- expect(summaryList1).toHaveProperty('rows', [
146
- {
147
- key: {
148
- text: keys[2]
149
- },
150
- value: {
151
- classes: 'app-prose-scope',
152
- html: values[0]
153
- },
154
- actions: {
155
- items: [
156
- {
157
- classes: 'govuk-link--no-visited-state',
158
- href: `${basePath}/delivery-or-collection?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
159
- text: 'Change',
160
- visuallyHiddenText: keys[0]
161
- }
162
- ]
153
+ expect(summaryList1).toHaveProperty('rows', [
154
+ {
155
+ key: {
156
+ text: keys[2]
157
+ },
158
+ value: {
159
+ classes: 'app-prose-scope',
160
+ html: values[0]
161
+ },
162
+ actions: {
163
+ items: [
164
+ {
165
+ classes: 'govuk-link--no-visited-state',
166
+ href: `${basePath}/delivery-or-collection?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
167
+ text: 'Change',
168
+ visuallyHiddenText: keys[0]
169
+ }
170
+ ]
171
+ }
163
172
  }
164
- }
165
- ])
173
+ ])
166
174
 
167
- expect(summaryList2).toHaveProperty('rows', [
168
- {
169
- key: {
170
- text: keys[1]
171
- },
172
- value: {
173
- classes: 'app-prose-scope',
174
- html: values[1]
175
- },
176
- actions: {
177
- items: [
178
- {
179
- classes: 'govuk-link--no-visited-state',
180
- href: `${basePath}/pizza-order/summary?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
181
- text: 'Change',
182
- visuallyHiddenText: 'Pizza'
183
- }
184
- ]
175
+ expect(summaryList2).toHaveProperty('rows', [
176
+ {
177
+ key: {
178
+ text: keys[1]
179
+ },
180
+ value: {
181
+ classes: 'app-prose-scope',
182
+ html: values[1]
183
+ },
184
+ actions: {
185
+ items: [
186
+ {
187
+ classes: 'govuk-link--no-visited-state',
188
+ href: `${basePath}/pizza-order/summary?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
189
+ text: 'Change',
190
+ visuallyHiddenText: 'Pizza'
191
+ }
192
+ ]
193
+ }
185
194
  }
186
- }
187
- ])
188
- })
195
+ ])
196
+ })
189
197
 
190
- it('should add summary list for each section (preview URL direct access)', () => {
191
- request.query.force = '' // Preview URL '?force'
192
- context = model.getFormContext(request, state)
193
- summaryViewModel = new SummaryViewModel(request, page, context)
198
+ it('should add summary list for each section (preview URL direct access)', () => {
199
+ request.query.force = '' // Preview URL '?force'
200
+ context = model.getFormContext(request, state)
201
+ summaryViewModel = new SummaryViewModel(request, page, context)
194
202
 
195
- expect(summaryViewModel.checkAnswers).toHaveLength(2)
203
+ expect(summaryViewModel.checkAnswers).toHaveLength(2)
196
204
 
197
- const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
205
+ const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
198
206
 
199
- const { summaryList: summaryList1 } = checkAnswers1
200
- const { summaryList: summaryList2 } = checkAnswers2
207
+ const { summaryList: summaryList1 } = checkAnswers1
208
+ const { summaryList: summaryList2 } = checkAnswers2
201
209
 
202
- expect(summaryList1).toHaveProperty('rows', [
203
- {
204
- key: {
205
- text: keys[2]
206
- },
207
- value: {
208
- classes: 'app-prose-scope',
209
- html: values[0]
210
- },
211
- actions: {
212
- items: []
210
+ expect(summaryList1).toHaveProperty('rows', [
211
+ {
212
+ key: {
213
+ text: keys[2]
214
+ },
215
+ value: {
216
+ classes: 'app-prose-scope',
217
+ html: values[0]
218
+ },
219
+ actions: {
220
+ items: []
221
+ }
213
222
  }
214
- }
215
- ])
223
+ ])
216
224
 
217
- expect(summaryList2).toHaveProperty('rows', [
218
- {
219
- key: {
220
- text: keys[1]
221
- },
222
- value: {
223
- classes: 'app-prose-scope',
224
- html: values[1]
225
- },
226
- actions: {
227
- items: []
225
+ expect(summaryList2).toHaveProperty('rows', [
226
+ {
227
+ key: {
228
+ text: keys[1]
229
+ },
230
+ value: {
231
+ classes: 'app-prose-scope',
232
+ html: values[1]
233
+ },
234
+ actions: {
235
+ items: []
236
+ }
228
237
  }
229
- }
230
- ])
231
- })
238
+ ])
239
+ })
232
240
 
233
- it('should use correct summary labels', () => {
234
- request.query.force = '' // Preview URL '?force'
235
- context = model.getFormContext(request, state)
236
- summaryViewModel = new SummaryViewModel(request, page, context)
241
+ it('should use correct summary labels', () => {
242
+ request.query.force = '' // Preview URL '?force'
243
+ context = model.getFormContext(request, state)
244
+ summaryViewModel = new SummaryViewModel(request, page, context)
237
245
 
238
- expect(summaryViewModel.details).toHaveLength(2)
246
+ expect(summaryViewModel.details).toHaveLength(2)
239
247
 
240
- const [details1, details2] = summaryViewModel.details
248
+ const [details1, details2] = summaryViewModel.details
241
249
 
242
- expect(details1.items[0]).toMatchObject({
243
- title: keys[2],
244
- label: keys[0]
245
- })
250
+ expect(details1.items[0]).toMatchObject({
251
+ name: names[0],
252
+ value: answers[0],
253
+ title: keys[2],
254
+ label: keys[0]
255
+ })
256
+
257
+ expect(details2.items[0]).toMatchObject({
258
+ name: names[1],
259
+ value: answers[1],
260
+ title: keys[1],
261
+ label: keys[4]
262
+ })
246
263
 
247
- expect(details2.items[0]).toMatchObject({
248
- title: keys[1],
249
- label: keys[4]
264
+ const snapshot = [
265
+ {
266
+ name: names[0],
267
+ value: answers[0],
268
+ title: keys[2],
269
+ label: keys[0]
270
+ },
271
+ {
272
+ name: names[1],
273
+ value: answers[1],
274
+ title: keys[1],
275
+ label: keys[4]
276
+ }
277
+ ]
278
+
279
+ expect(snapshot).toMatchSnapshot()
250
280
  })
251
- })
252
- })
281
+ }
282
+ )
253
283
  })
254
284
 
255
285
  describe('SummaryPageController', () => {
@@ -0,0 +1,52 @@
1
+ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
+
3
+ exports[`SummaryViewModel Check answers (0 items) should use correct summary labels 1`] = `
4
+ [
5
+ {
6
+ "label": "How would you like to receive your pizza?",
7
+ "name": "orderType",
8
+ "title": "How you would like to receive your pizza",
9
+ "value": "Collection",
10
+ },
11
+ {
12
+ "label": "Pizza",
13
+ "name": "pizza",
14
+ "title": "Pizzas",
15
+ "value": "",
16
+ },
17
+ ]
18
+ `;
19
+
20
+ exports[`SummaryViewModel Check answers (1 item) should use correct summary labels 1`] = `
21
+ [
22
+ {
23
+ "label": "How would you like to receive your pizza?",
24
+ "name": "orderType",
25
+ "title": "How you would like to receive your pizza",
26
+ "value": "Delivery",
27
+ },
28
+ {
29
+ "label": "Pizza",
30
+ "name": "pizza",
31
+ "title": "Pizza added",
32
+ "value": "You added 1 Pizza",
33
+ },
34
+ ]
35
+ `;
36
+
37
+ exports[`SummaryViewModel Check answers (2 items) should use correct summary labels 1`] = `
38
+ [
39
+ {
40
+ "label": "How would you like to receive your pizza?",
41
+ "name": "orderType",
42
+ "title": "How you would like to receive your pizza",
43
+ "value": "Delivery",
44
+ },
45
+ {
46
+ "label": "Pizza",
47
+ "name": "pizza",
48
+ "title": "Pizzas added",
49
+ "value": "You added 2 Pizzas",
50
+ },
51
+ ]
52
+ `;
@@ -1,3 +1,4 @@
1
+ import { getErrorMessage } from '@defra/forms-model'
1
2
  import Joi from 'joi'
2
3
 
3
4
  import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
@@ -39,7 +40,8 @@ export function validatePluginOptions(options) {
39
40
 
40
41
  if (result.error) {
41
42
  logger.error(
42
- `Missing required properties in plugin options: ${result.error.message}`
43
+ result.error,
44
+ `Missing required properties in plugin options: ${getErrorMessage(result.error)}`
43
45
  )
44
46
  throw new Error('Invalid plugin options', result.error)
45
47
  }
@@ -723,6 +723,46 @@ describe('Adapter v1 formatter', () => {
723
723
  expect(parsedBody.meta.versionMetadata).toBeUndefined()
724
724
  })
725
725
 
726
+ it('should handle optional fields that are undefined', () => {
727
+ const formMetadata: Partial<FormMetadata> = {
728
+ id: 'form-123',
729
+ slug: 'test-form',
730
+ title: 'Test Form',
731
+ notificationEmail: 'test@example.com'
732
+ } as FormMetadata
733
+
734
+ const formStatus = {
735
+ isPreview: false,
736
+ state: FormStatus.Live
737
+ }
738
+
739
+ const dummyField: Field = {
740
+ getFormValueFromState: (_) => undefined
741
+ } as Field
742
+
743
+ const items: DetailItem[] = [
744
+ {
745
+ name: 'exampleField3',
746
+ label: 'Example Field 3',
747
+ href: '/example-field-3',
748
+ title: 'Example Field 3 Title',
749
+ field: dummyField,
750
+ value: ''
751
+ } as DetailItemField
752
+ ]
753
+
754
+ const body = format(
755
+ context,
756
+ items,
757
+ model,
758
+ submitResponse,
759
+ formStatus,
760
+ formMetadata as FormMetadata
761
+ )
762
+ const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
763
+ expect(parsedBody.data.main).toEqual({ exampleField3: null })
764
+ })
765
+
726
766
  describe('version metadata handling', () => {
727
767
  it('should include versionMetadata when context has submittedVersionNumber and formMetadata has versions', () => {
728
768
  const formMetadata: Partial<FormMetadata> = {