@defra/forms-engine-plugin 2.1.3 → 2.1.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 (52) hide show
  1. package/.server/server/plugins/engine/models/FormModel.js +1 -1
  2. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  3. package/.server/server/plugins/engine/outputFormatters/adapter/v1.d.ts +6 -0
  4. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +23 -0
  5. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -0
  6. package/.server/server/plugins/engine/outputFormatters/human/v1.d.ts +2 -2
  7. package/.server/server/plugins/engine/outputFormatters/human/v1.js +1 -1
  8. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  9. package/.server/server/plugins/engine/outputFormatters/index.d.ts +4 -3
  10. package/.server/server/plugins/engine/outputFormatters/index.js +4 -0
  11. package/.server/server/plugins/engine/outputFormatters/index.js.map +1 -1
  12. package/.server/server/plugins/engine/outputFormatters/machine/v1.d.ts +2 -2
  13. package/.server/server/plugins/engine/outputFormatters/machine/v1.js +1 -1
  14. package/.server/server/plugins/engine/outputFormatters/machine/v1.js.map +1 -1
  15. package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +4 -1
  16. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  17. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +5 -4
  18. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  19. package/.server/server/plugins/engine/services/localFormsService.d.ts +1 -1
  20. package/.server/server/plugins/engine/services/notifyService.d.ts +7 -2
  21. package/.server/server/plugins/engine/services/notifyService.js +11 -2
  22. package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
  23. package/.server/server/plugins/engine/types/index.d.ts +10 -0
  24. package/.server/server/plugins/engine/types/index.js +4 -0
  25. package/.server/server/plugins/engine/types/index.js.map +1 -0
  26. package/.server/server/plugins/engine/types/schema.d.ts +5 -0
  27. package/.server/server/plugins/engine/types/schema.js +24 -0
  28. package/.server/server/plugins/engine/types/schema.js.map +1 -0
  29. package/.server/server/plugins/engine/types.d.ts +37 -1
  30. package/.server/server/plugins/engine/types.js +4 -0
  31. package/.server/server/plugins/engine/types.js.map +1 -1
  32. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  33. package/.server/server/plugins/nunjucks/filters/page.d.ts +1 -1
  34. package/.server/server/types.d.ts +1 -1
  35. package/.server/server/types.js.map +1 -1
  36. package/package.json +6 -1
  37. package/src/server/plugins/engine/models/FormModel.test.ts +64 -0
  38. package/src/server/plugins/engine/models/FormModel.ts +5 -1
  39. package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +506 -0
  40. package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +53 -0
  41. package/src/server/plugins/engine/outputFormatters/human/v1.ts +6 -2
  42. package/src/server/plugins/engine/outputFormatters/index.ts +11 -3
  43. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +6 -2
  44. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +1 -1
  45. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +15 -4
  46. package/src/server/plugins/engine/services/notifyService.test.ts +156 -1
  47. package/src/server/plugins/engine/services/notifyService.ts +24 -3
  48. package/src/server/plugins/engine/types/index.ts +96 -0
  49. package/src/server/plugins/engine/types/schema.test.ts +152 -0
  50. package/src/server/plugins/engine/types/schema.ts +45 -0
  51. package/src/server/plugins/engine/types.ts +51 -1
  52. package/src/server/types.ts +2 -1
@@ -5,7 +5,16 @@ import { checkFormStatus } from "../helpers.js";
5
5
  import { getFormatter } from "../outputFormatters/index.js";
6
6
  import { sendNotification } from "../../../utils/notify.js";
7
7
  const templateId = config.get('notifyTemplateId');
8
- export async function submit(context, request, model, emailAddress, items, submitResponse) {
8
+
9
+ /**
10
+ * Optional GOV.UK Notify service for consumers who want email notifications
11
+ * Can be disabled by not providing notifyTemplateId in config
12
+ * Can be overridden by providing a custom outputService in the services config
13
+ */
14
+ export async function submit(context, request, model, emailAddress, items, submitResponse, formMetadata) {
15
+ if (!templateId) {
16
+ return Promise.resolve();
17
+ }
9
18
  const logTags = ['submit', 'email'];
10
19
  const formStatus = checkFormStatus(request.params);
11
20
 
@@ -16,7 +25,7 @@ export async function submit(context, request, model, emailAddress, items, submi
16
25
  const outputAudience = model.def.output?.audience ?? 'human';
17
26
  const outputVersion = model.def.output?.version ?? '1';
18
27
  const outputFormatter = getFormatter(outputAudience, outputVersion);
19
- let body = outputFormatter(context, items, model, submitResponse, formStatus);
28
+ let body = outputFormatter(context, items, model, submitResponse, formStatus, formMetadata);
20
29
 
21
30
  // GOV.UK Notify transforms quotes into curly quotes, so we can't just send the raw payload
22
31
  // This is logic specific to Notify, so we include the logic here rather than in the formatter
@@ -1 +1 @@
1
- {"version":3,"file":"notifyService.js","names":["getErrorMessage","config","escapeMarkdown","checkFormStatus","getFormatter","sendNotification","templateId","get","submit","context","request","model","emailAddress","items","submitResponse","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 { getErrorMessage, type SubmitResponsePayload } from '@defra/forms-model'\n\nimport { config } from '~/src/config/index.js'\nimport { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers.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\nexport async function submit(\n context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload\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(context, items, model, submitResponse, formStatus)\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,SAASA,eAAe,QAAoC,oBAAoB;AAEhF,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,OAAO,eAAeC,MAAMA,CAC1BC,OAAoB,EACpBC,OAA2B,EAC3BC,KAAgB,EAChBC,YAAoB,EACpBC,KAAmB,EACnBC,cAAqC,EACrC;EACA,MAAMC,OAAO,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC;EACnC,MAAMC,UAAU,GAAGb,eAAe,CAACO,OAAO,CAACO,MAAM,CAAC;;EAElD;EACAP,OAAO,CAACQ,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,8BAA8B,CAAC;EAE5D,MAAMK,QAAQ,GAAGlB,cAAc,CAACS,KAAK,CAACU,IAAI,CAAC;EAC3C,MAAMC,OAAO,GAAGN,UAAU,CAACO,SAAS,GAChC,yBAAyBH,QAAQ,EAAE,GACnC,oBAAoBA,QAAQ,EAAE;EAElC,MAAMI,cAAc,GAAGb,KAAK,CAACc,GAAG,CAACC,MAAM,EAAEC,QAAQ,IAAI,OAAO;EAC5D,MAAMC,aAAa,GAAGjB,KAAK,CAACc,GAAG,CAACC,MAAM,EAAEG,OAAO,IAAI,GAAG;EAEtD,MAAMC,eAAe,GAAG1B,YAAY,CAACoB,cAAc,EAAEI,aAAa,CAAC;EACnE,IAAIG,IAAI,GAAGD,eAAe,CAACrB,OAAO,EAAEI,KAAK,EAAEF,KAAK,EAAEG,cAAc,EAAEE,UAAU,CAAC;;EAE7E;EACA;EACA,IAAIQ,cAAc,KAAK,SAAS,EAAE;IAChCO,IAAI,GAAGC,MAAM,CAACC,IAAI,CAACF,IAAI,CAAC,CAACG,QAAQ,CAAC,QAAQ,CAAC;EAC7C;EAEAxB,OAAO,CAACQ,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,eAAe,CAAC;EAE7C,IAAI;IACF;IACA,MAAMV,gBAAgB,CAAC;MACrBC,UAAU;MACVM,YAAY;MACZuB,eAAe,EAAE;QACfb,OAAO;QACPS;MACF;IACF,CAAC,CAAC;IAEFrB,OAAO,CAACQ,MAAM,CAACC,IAAI,CAACJ,OAAO,EAAE,yBAAyB,CAAC;EACzD,CAAC,CAAC,OAAOqB,GAAG,EAAE;IACZ,MAAMC,MAAM,GAAGrC,eAAe,CAACoC,GAAG,CAAC;IACnC1B,OAAO,CAACQ,MAAM,CAACoB,KAAK,CAClBD,MAAM,EACN,oEAAoE/B,UAAU,iBAAiBM,YAAY,MAAMyB,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","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.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":[]}
@@ -0,0 +1,10 @@
1
+ export type { CheckAnswers, ErrorMessageTemplate, ErrorMessageTemplateList, FeaturedFormPageViewModel, FileState, FilterFunction, FormAdapterSubmissionMessage, FormAdapterSubmissionMessageData, FormAdapterSubmissionMessageMeta, FormAdapterSubmissionMessageMetaSerialised, FormAdapterSubmissionMessagePayload, FormAdapterSubmissionService, FormContext, FormContextRequest, FormPageViewModel, FormPayload, FormPayloadParams, FormState, FormStateValue, FormSubmissionError, FormSubmissionState, FormValidationResult, FormValue, GlobalFunction, ItemDeletePageViewModel, OnRequestCallback, PageViewModel, PageViewModelBase, PluginOptions, PreparePageEventRequestOptions, RepeatItemState, RepeatListState, RepeaterSummaryPageViewModel, SummaryList, SummaryListAction, SummaryListRow, TempFileState, UploadInitiateResponse, UploadStatusFileResponse, UploadStatusResponse } from '~/src/server/plugins/engine/types.js';
2
+ export { FileStatus, FormAdapterSubmissionSchemaVersion, UploadStatus } from '~/src/server/plugins/engine/types.js';
3
+ export type { Detail, DetailItem, DetailItemBase, DetailItemField, DetailItemRepeat, ExecutableCondition } from '~/src/server/plugins/engine/models/types.js';
4
+ export type { BackLink, ComponentText, ComponentViewModel, Content, DateInputItem, DatePartsState, Label, ListItem, ListItemLabel, MonthYearState, ViewModel } from '~/src/server/plugins/engine/components/types.js';
5
+ export type { UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js';
6
+ export type { FormParams, FormQuery, FormRequest, FormRequestPayload, FormRequestPayloadRefs, FormRequestRefs } from '~/src/server/routes/types.js';
7
+ export { FormAction, FormStatus } from '~/src/server/routes/types.js';
8
+ export type { FormSubmissionService, FormsService, OutputService, RouteConfig, Services } from '~/src/server/types.js';
9
+ export type { RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js';
10
+ export * from '~/src/server/plugins/engine/types/schema.js';
@@ -0,0 +1,4 @@
1
+ export { FileStatus, FormAdapterSubmissionSchemaVersion, UploadStatus } from "../types.js";
2
+ export { FormAction, FormStatus } from "../../../routes/types.js";
3
+ export * from "./schema.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["FileStatus","FormAdapterSubmissionSchemaVersion","UploadStatus","FormAction","FormStatus"],"sources":["../../../../../src/server/plugins/engine/types/index.ts"],"sourcesContent":["export type {\n CheckAnswers,\n ErrorMessageTemplate,\n ErrorMessageTemplateList,\n FeaturedFormPageViewModel,\n FileState,\n FilterFunction,\n FormAdapterSubmissionMessage,\n FormAdapterSubmissionMessageData,\n FormAdapterSubmissionMessageMeta,\n FormAdapterSubmissionMessageMetaSerialised,\n FormAdapterSubmissionMessagePayload,\n FormAdapterSubmissionService,\n FormContext,\n FormContextRequest,\n FormPageViewModel,\n FormPayload,\n FormPayloadParams,\n FormState,\n FormStateValue,\n FormSubmissionError,\n FormSubmissionState,\n FormValidationResult,\n FormValue,\n GlobalFunction,\n ItemDeletePageViewModel,\n OnRequestCallback,\n PageViewModel,\n PageViewModelBase,\n PluginOptions,\n PreparePageEventRequestOptions,\n RepeatItemState,\n RepeatListState,\n RepeaterSummaryPageViewModel,\n SummaryList,\n SummaryListAction,\n SummaryListRow,\n TempFileState,\n UploadInitiateResponse,\n UploadStatusFileResponse,\n UploadStatusResponse\n} from '~/src/server/plugins/engine/types.js'\n\nexport {\n FileStatus,\n FormAdapterSubmissionSchemaVersion,\n UploadStatus\n} from '~/src/server/plugins/engine/types.js'\n\nexport type {\n Detail,\n DetailItem,\n DetailItemBase,\n DetailItemField,\n DetailItemRepeat,\n ExecutableCondition\n} from '~/src/server/plugins/engine/models/types.js'\n\nexport type {\n BackLink,\n ComponentText,\n ComponentViewModel,\n Content,\n DateInputItem,\n DatePartsState,\n Label,\n ListItem,\n ListItemLabel,\n MonthYearState,\n ViewModel\n} from '~/src/server/plugins/engine/components/types.js'\n\nexport type { UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'\n\nexport type {\n FormParams,\n FormQuery,\n FormRequest,\n FormRequestPayload,\n FormRequestPayloadRefs,\n FormRequestRefs\n} from '~/src/server/routes/types.js'\n\nexport { FormAction, FormStatus } from '~/src/server/routes/types.js'\n\nexport type {\n FormSubmissionService,\n FormsService,\n OutputService,\n RouteConfig,\n Services\n} from '~/src/server/types.js'\n\nexport type { RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'\n\nexport * from '~/src/server/plugins/engine/types/schema.js'\n"],"mappings":"AA2CA,SACEA,UAAU,EACVC,kCAAkC,EAClCC,YAAY;AAqCd,SAASC,UAAU,EAAEC,UAAU;AAY/B","ignoreList":[]}
@@ -0,0 +1,5 @@
1
+ import Joi from 'joi';
2
+ import { type FormAdapterSubmissionMessageData, type FormAdapterSubmissionMessageMeta, type FormAdapterSubmissionMessagePayload } from '~/src/server/plugins/engine/types.js';
3
+ export declare const formAdapterSubmissionMessageMetaSchema: Joi.ObjectSchema<FormAdapterSubmissionMessageMeta>;
4
+ export declare const formAdapterSubmissionMessageDataSchema: Joi.ObjectSchema<FormAdapterSubmissionMessageData>;
5
+ export declare const formAdapterSubmissionMessagePayloadSchema: Joi.ObjectSchema<FormAdapterSubmissionMessagePayload>;
@@ -0,0 +1,24 @@
1
+ import { FormStatus, idSchema, notificationEmailAddressSchema, slugSchema, titleSchema } from '@defra/forms-model';
2
+ import Joi from 'joi';
3
+ import { FormAdapterSubmissionSchemaVersion } from "../types.js";
4
+ export const formAdapterSubmissionMessageMetaSchema = Joi.object().keys({
5
+ schemaVersion: Joi.string().valid(...Object.values(FormAdapterSubmissionSchemaVersion)),
6
+ timestamp: Joi.date().required(),
7
+ referenceNumber: Joi.string().required(),
8
+ formName: titleSchema,
9
+ formId: idSchema,
10
+ formSlug: slugSchema,
11
+ status: Joi.string().valid(...Object.values(FormStatus)).required(),
12
+ isPreview: Joi.boolean().required(),
13
+ notificationEmail: notificationEmailAddressSchema.required()
14
+ });
15
+ export const formAdapterSubmissionMessageDataSchema = Joi.object().keys({
16
+ main: Joi.object(),
17
+ repeaters: Joi.object(),
18
+ files: Joi.object()
19
+ });
20
+ export const formAdapterSubmissionMessagePayloadSchema = Joi.object().keys({
21
+ meta: formAdapterSubmissionMessageMetaSchema.required(),
22
+ data: formAdapterSubmissionMessageDataSchema.required()
23
+ });
24
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","names":["FormStatus","idSchema","notificationEmailAddressSchema","slugSchema","titleSchema","Joi","FormAdapterSubmissionSchemaVersion","formAdapterSubmissionMessageMetaSchema","object","keys","schemaVersion","string","valid","Object","values","timestamp","date","required","referenceNumber","formName","formId","formSlug","status","isPreview","boolean","notificationEmail","formAdapterSubmissionMessageDataSchema","main","repeaters","files","formAdapterSubmissionMessagePayloadSchema","meta","data"],"sources":["../../../../../src/server/plugins/engine/types/schema.ts"],"sourcesContent":["import {\n FormStatus,\n idSchema,\n notificationEmailAddressSchema,\n slugSchema,\n titleSchema\n} from '@defra/forms-model'\nimport Joi from 'joi'\n\nimport {\n FormAdapterSubmissionSchemaVersion,\n type FormAdapterSubmissionMessageData,\n type FormAdapterSubmissionMessageMeta,\n type FormAdapterSubmissionMessagePayload\n} from '~/src/server/plugins/engine/types.js'\n\nexport const formAdapterSubmissionMessageMetaSchema =\n Joi.object<FormAdapterSubmissionMessageMeta>().keys({\n schemaVersion: Joi.string().valid(\n ...Object.values(FormAdapterSubmissionSchemaVersion)\n ),\n timestamp: Joi.date().required(),\n referenceNumber: Joi.string().required(),\n formName: titleSchema,\n formId: idSchema,\n formSlug: slugSchema,\n status: Joi.string()\n .valid(...Object.values(FormStatus))\n .required(),\n isPreview: Joi.boolean().required(),\n notificationEmail: notificationEmailAddressSchema.required()\n })\n\nexport const formAdapterSubmissionMessageDataSchema =\n Joi.object<FormAdapterSubmissionMessageData>().keys({\n main: Joi.object(),\n repeaters: Joi.object(),\n files: Joi.object()\n })\n\nexport const formAdapterSubmissionMessagePayloadSchema =\n Joi.object<FormAdapterSubmissionMessagePayload>().keys({\n meta: formAdapterSubmissionMessageMetaSchema.required(),\n data: formAdapterSubmissionMessageDataSchema.required()\n })\n"],"mappings":"AAAA,SACEA,UAAU,EACVC,QAAQ,EACRC,8BAA8B,EAC9BC,UAAU,EACVC,WAAW,QACN,oBAAoB;AAC3B,OAAOC,GAAG,MAAM,KAAK;AAErB,SACEC,kCAAkC;AAMpC,OAAO,MAAMC,sCAAsC,GACjDF,GAAG,CAACG,MAAM,CAAmC,CAAC,CAACC,IAAI,CAAC;EAClDC,aAAa,EAAEL,GAAG,CAACM,MAAM,CAAC,CAAC,CAACC,KAAK,CAC/B,GAAGC,MAAM,CAACC,MAAM,CAACR,kCAAkC,CACrD,CAAC;EACDS,SAAS,EAAEV,GAAG,CAACW,IAAI,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;EAChCC,eAAe,EAAEb,GAAG,CAACM,MAAM,CAAC,CAAC,CAACM,QAAQ,CAAC,CAAC;EACxCE,QAAQ,EAAEf,WAAW;EACrBgB,MAAM,EAAEnB,QAAQ;EAChBoB,QAAQ,EAAElB,UAAU;EACpBmB,MAAM,EAAEjB,GAAG,CAACM,MAAM,CAAC,CAAC,CACjBC,KAAK,CAAC,GAAGC,MAAM,CAACC,MAAM,CAACd,UAAU,CAAC,CAAC,CACnCiB,QAAQ,CAAC,CAAC;EACbM,SAAS,EAAElB,GAAG,CAACmB,OAAO,CAAC,CAAC,CAACP,QAAQ,CAAC,CAAC;EACnCQ,iBAAiB,EAAEvB,8BAA8B,CAACe,QAAQ,CAAC;AAC7D,CAAC,CAAC;AAEJ,OAAO,MAAMS,sCAAsC,GACjDrB,GAAG,CAACG,MAAM,CAAmC,CAAC,CAACC,IAAI,CAAC;EAClDkB,IAAI,EAAEtB,GAAG,CAACG,MAAM,CAAC,CAAC;EAClBoB,SAAS,EAAEvB,GAAG,CAACG,MAAM,CAAC,CAAC;EACvBqB,KAAK,EAAExB,GAAG,CAACG,MAAM,CAAC;AACpB,CAAC,CAAC;AAEJ,OAAO,MAAMsB,yCAAyC,GACpDzB,GAAG,CAACG,MAAM,CAAsC,CAAC,CAACC,IAAI,CAAC;EACrDsB,IAAI,EAAExB,sCAAsC,CAACU,QAAQ,CAAC,CAAC;EACvDe,IAAI,EAAEN,sCAAsC,CAACT,QAAQ,CAAC;AACxD,CAAC,CAAC","ignoreList":[]}
@@ -4,10 +4,11 @@ import { type JoiExpression, type ValidationErrorItem } from 'joi';
4
4
  import { type Component } from '~/src/server/plugins/engine/components/helpers.js';
5
5
  import { type BackLink, type ComponentText, type ComponentViewModel } from '~/src/server/plugins/engine/components/types.js';
6
6
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js';
7
+ import { type RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js';
7
8
  import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js';
8
9
  import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js';
9
10
  import { type ViewContext } from '~/src/server/plugins/nunjucks/types.js';
10
- import { type FormAction, type FormParams, type FormRequest, type FormRequestPayload } from '~/src/server/routes/types.js';
11
+ import { type FormAction, type FormParams, type FormRequest, type FormRequestPayload, type FormStatus } from '~/src/server/routes/types.js';
11
12
  import { type RequestOptions } from '~/src/server/services/httpService.js';
12
13
  import { type Services } from '~/src/server/types.js';
13
14
  type RequestType = Request | FormRequest | FormRequestPayload;
@@ -285,4 +286,39 @@ export interface PluginOptions {
285
286
  onRequest?: OnRequestCallback;
286
287
  baseUrl: string;
287
288
  }
289
+ export interface FormAdapterSubmissionMessageMeta {
290
+ schemaVersion: FormAdapterSubmissionSchemaVersion;
291
+ timestamp: Date;
292
+ referenceNumber: string;
293
+ formName: string;
294
+ formId: string;
295
+ formSlug: string;
296
+ status: FormStatus;
297
+ isPreview: boolean;
298
+ notificationEmail: string;
299
+ }
300
+ export type FormAdapterSubmissionMessageMetaSerialised = Omit<FormAdapterSubmissionMessageMeta, 'schemaVersion' | 'timestamp' | 'status'> & {
301
+ schemaVersion: string;
302
+ status: string;
303
+ timestamp: string;
304
+ };
305
+ export interface FormAdapterSubmissionMessageData {
306
+ main: Record<string, RichFormValue>;
307
+ repeaters: Record<string, Record<string, RichFormValue>[]>;
308
+ files: Record<string, Record<string, string>[]>;
309
+ }
310
+ export declare enum FormAdapterSubmissionSchemaVersion {
311
+ V1 = 1
312
+ }
313
+ export interface FormAdapterSubmissionMessagePayload {
314
+ meta: FormAdapterSubmissionMessageMeta;
315
+ data: FormAdapterSubmissionMessageData;
316
+ }
317
+ export interface FormAdapterSubmissionMessage extends FormAdapterSubmissionMessagePayload {
318
+ messageId: string;
319
+ recordCreatedAt: Date;
320
+ }
321
+ export interface FormAdapterSubmissionService {
322
+ handleFormSubmission: (submissionMessage: FormAdapterSubmissionMessage) => unknown;
323
+ }
288
324
  export {};
@@ -50,4 +50,8 @@ export let FileStatus = /*#__PURE__*/function (FileStatus) {
50
50
  FileStatus["pending"] = "pending";
51
51
  return FileStatus;
52
52
  }({});
53
+ export let FormAdapterSubmissionSchemaVersion = /*#__PURE__*/function (FormAdapterSubmissionSchemaVersion) {
54
+ FormAdapterSubmissionSchemaVersion[FormAdapterSubmissionSchemaVersion["V1"] = 1] = "V1";
55
+ return FormAdapterSubmissionSchemaVersion;
56
+ }({});
53
57
  //# sourceMappingURL=types.js.map
@@ -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 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","FormAdapterSubmissionSchemaVersion"],"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 RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.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 type FormStatus\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\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}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: string\n status: string\n timestamp: string\n}\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, Record<string, string>[]>\n}\n\nexport enum FormAdapterSubmissionSchemaVersion {\n V1 = 1\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\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":"AAoCA;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;AA0NtB,WAAYC,kCAAkC,0BAAlCA,kCAAkC;EAAlCA,kCAAkC,CAAlCA,kCAAkC;EAAA,OAAlCA,kCAAkC;AAAA","ignoreList":[]}
@@ -3,5 +3,5 @@
3
3
  * @this {NunjucksContext}
4
4
  * @param {string} name - The name of the component
5
5
  */
6
- export function field(this: NunjucksContext, name: string): import("../../engine/components/SelectField.js").SelectField | import("../../engine/components/RadiosField.js").RadiosField | import("../../engine/components/YesNoField.js").YesNoField | import("../../engine/components/CheckboxesField.js").CheckboxesField | import("../../engine/components/DatePartsField.js").DatePartsField | import("../../engine/components/EmailAddressField.js").EmailAddressField | import("../../engine/components/MonthYearField.js").MonthYearField | import("../../engine/components/MultilineTextField.js").MultilineTextField | import("../../engine/components/NumberField.js").NumberField | import("../../engine/components/TelephoneNumberField.js").TelephoneNumberField | import("../../engine/components/TextField.js").TextField | import("../../engine/components/UkAddressField.js").UkAddressField | import("../../engine/components/FileUploadField.js").FileUploadField | import("../../engine/components/Details.js").Details | import("../../engine/components/Html.js").Html | import("../../engine/components/InsetText.js").InsetText | import("../../engine/components/List.js").List | import("../../engine/components/Markdown.js").Markdown | undefined;
6
+ export function field(this: NunjucksContext, name: string): import("../../engine/components/TextField.js").TextField | import("../../engine/components/SelectField.js").SelectField | import("../../engine/components/RadiosField.js").RadiosField | import("../../engine/components/YesNoField.js").YesNoField | import("../../engine/components/CheckboxesField.js").CheckboxesField | import("../../engine/components/DatePartsField.js").DatePartsField | import("../../engine/components/EmailAddressField.js").EmailAddressField | import("../../engine/components/MonthYearField.js").MonthYearField | import("../../engine/components/MultilineTextField.js").MultilineTextField | import("../../engine/components/NumberField.js").NumberField | import("../../engine/components/TelephoneNumberField.js").TelephoneNumberField | import("../../engine/components/UkAddressField.js").UkAddressField | import("../../engine/components/FileUploadField.js").FileUploadField | import("../../engine/components/Details.js").Details | import("../../engine/components/Html.js").Html | import("../../engine/components/InsetText.js").InsetText | import("../../engine/components/List.js").List | import("../../engine/components/Markdown.js").Markdown | undefined;
7
7
  import type { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js';
@@ -3,5 +3,5 @@
3
3
  * @this {NunjucksContext}
4
4
  * @param {string} path - The path of the page
5
5
  */
6
- export function page(this: NunjucksContext, path: string): import("../../engine/pageControllers/QuestionPageController.js").QuestionPageController | import("../../engine/pageControllers/StartPageController.js").StartPageController | import("../../engine/pageControllers/RepeatPageController.js").RepeatPageController | undefined;
6
+ export function page(this: NunjucksContext, path: string): import("../../engine/pageControllers/StartPageController.js").StartPageController | import("../../engine/pageControllers/QuestionPageController.js").QuestionPageController | import("../../engine/pageControllers/RepeatPageController.js").RepeatPageController | undefined;
7
7
  import type { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js';
@@ -31,5 +31,5 @@ export interface RouteConfig {
31
31
  saveAndReturn?: PluginOptions['saveAndReturn'];
32
32
  }
33
33
  export interface OutputService {
34
- submit: (context: FormContext, request: FormRequestPayload, model: FormModel, emailAddress: string, items: DetailItem[], submitResponse: SubmitResponsePayload) => Promise<void>;
34
+ submit: (context: FormContext, request: FormRequestPayload, model: FormModel, emailAddress: string, items: DetailItem[], submitResponse: SubmitResponsePayload, formMetadata?: FormMetadata) => Promise<void>;
35
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../src/server/types.ts"],"sourcesContent":["import {\n type FormDefinition,\n type FormMetadata,\n type SubmitPayload,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n type FormContext,\n type OnRequestCallback,\n type PluginOptions,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequestPayload,\n type FormStatus\n} from '~/src/server/routes/types.js'\n\nexport interface FormsService {\n getFormMetadata: (slug: string) => Promise<FormMetadata>\n getFormDefinition: (\n id: string,\n state: FormStatus\n ) => Promise<FormDefinition | undefined>\n}\n\nexport interface FormSubmissionService {\n persistFiles: (\n files: { fileId: string; initiatedRetrievalKey: string }[],\n persistedRetrievalKey: string\n ) => Promise<object>\n submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>\n}\n\nexport interface Services {\n formsService: FormsService\n formSubmissionService: FormSubmissionService\n outputService: OutputService\n}\n\nexport interface RouteConfig {\n formFileName?: string\n formFilePath?: string\n enforceCsrf?: boolean\n services?: Services\n controllers?: Record<string, typeof PageController>\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n saveAndReturn?: PluginOptions['saveAndReturn']\n}\n\nexport interface OutputService {\n submit: (\n context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload\n ) => Promise<void>\n}\n"],"mappings":"","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/server/types.ts"],"sourcesContent":["import {\n type FormDefinition,\n type FormMetadata,\n type SubmitPayload,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n type FormContext,\n type OnRequestCallback,\n type PluginOptions,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequestPayload,\n type FormStatus\n} from '~/src/server/routes/types.js'\n\nexport interface FormsService {\n getFormMetadata: (slug: string) => Promise<FormMetadata>\n getFormDefinition: (\n id: string,\n state: FormStatus\n ) => Promise<FormDefinition | undefined>\n}\n\nexport interface FormSubmissionService {\n persistFiles: (\n files: { fileId: string; initiatedRetrievalKey: string }[],\n persistedRetrievalKey: string\n ) => Promise<object>\n submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>\n}\n\nexport interface Services {\n formsService: FormsService\n formSubmissionService: FormSubmissionService\n outputService: OutputService\n}\n\nexport interface RouteConfig {\n formFileName?: string\n formFilePath?: string\n enforceCsrf?: boolean\n services?: Services\n controllers?: Record<string, typeof PageController>\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n saveAndReturn?: PluginOptions['saveAndReturn']\n}\n\nexport interface OutputService {\n submit: (\n context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload,\n formMetadata?: FormMetadata\n ) => Promise<void>\n}\n"],"mappings":"","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -14,6 +14,11 @@
14
14
  "import": "./.server/server/plugins/engine/index.js",
15
15
  "default": "./.server/server/plugins/engine/index.js"
16
16
  },
17
+ "./types": {
18
+ "types": "./.server/server/plugins/engine/types/index.d.ts",
19
+ "import": "./.server/server/plugins/engine/types/index.js",
20
+ "default": "./.server/server/plugins/engine/types/index.js"
21
+ },
17
22
  "./shared.js": "./.server/client/javascripts/shared.js",
18
23
  "./shared.min.js": "./.public/javascripts/shared.min.js",
19
24
  "./shared.min.js.map": "./.public/javascripts/shared.min.js.map",
@@ -9,6 +9,7 @@ import { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'
9
9
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
10
10
  import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
11
11
  import { type FormContextRequest } from '~/src/server/plugins/engine/types.js'
12
+ import { FormAction } from '~/src/server/routes/types.js'
12
13
  import { V2 as definitionV2 } from '~/test/form/definitions/conditions-basic.js'
13
14
  import definition from '~/test/form/definitions/conditions-escaping.js'
14
15
  import conditionsListDefinition from '~/test/form/definitions/conditions-list.js'
@@ -184,6 +185,69 @@ describe('FormModel', () => {
184
185
  })
185
186
 
186
187
  describe('getFormContext', () => {
188
+ it.each([FormAction.Validate, FormAction.SaveAndReturn, undefined])(
189
+ 'returns a form context with the correct payload and state when action is %s',
190
+ (action) => {
191
+ const formModel = new FormModel(fieldsRequiredDefinition, {
192
+ basePath: '/components'
193
+ })
194
+
195
+ const state = {
196
+ $$__referenceNumber: 'foobar'
197
+ }
198
+ const pageUrl = new URL('http://example.com/components/fields-required')
199
+
200
+ const request: FormContextRequest = buildFormContextRequest({
201
+ method: 'post',
202
+ payload: {
203
+ crumb: 'dummyCrumb',
204
+ action,
205
+ textField: 'Hello world'
206
+ },
207
+ query: {},
208
+ path: pageUrl.pathname,
209
+ params: { path: 'components', slug: 'fields-required' },
210
+ url: pageUrl,
211
+ app: { model: formModel }
212
+ })
213
+
214
+ const context = formModel.getFormContext(request, state)
215
+
216
+ expect(context.payload.textField).toBe('Hello world')
217
+ expect(context.state.textField).toBe('Hello world')
218
+ expect(context.referenceNumber).toEqual(expect.any(String))
219
+ }
220
+ )
221
+
222
+ it('returns without updating the state when the action is not validate or saveAndReturn', () => {
223
+ const formModel = new FormModel(fieldsRequiredDefinition, {
224
+ basePath: '/components'
225
+ })
226
+
227
+ const state = {
228
+ $$__referenceNumber: 'foobar',
229
+ textField: 'old'
230
+ }
231
+ const pageUrl = new URL('http://example.com/components/fields-required')
232
+
233
+ const request: FormContextRequest = buildFormContextRequest({
234
+ method: 'post',
235
+ payload: { crumb: 'dummyCrumb', action: 'continue', textField: 'new' },
236
+ query: {},
237
+ path: pageUrl.pathname,
238
+ params: { path: 'components', slug: 'fields-required' },
239
+ url: pageUrl,
240
+ app: { model: formModel }
241
+ })
242
+
243
+ const context = formModel.getFormContext(request, state)
244
+
245
+ // Early return branch should not merge payload into state
246
+ expect(context.state.textField).toBe('old')
247
+ expect(context.errors).toBeUndefined()
248
+ expect(context.referenceNumber).toEqual(expect.any(String))
249
+ })
250
+
187
251
  it('clears a previous checkbox field value when the field is omitted from the payload', () => {
188
252
  const formModel = new FormModel(fieldsRequiredDefinition, {
189
253
  basePath: '/components'
@@ -541,7 +541,11 @@ function validateFormPayload(
541
541
  const { action } = page.getFormParams(request)
542
542
 
543
543
  // Skip validation GET requests or other actions
544
- if (!request.payload || action !== FormAction.Validate) {
544
+ if (
545
+ !request.payload ||
546
+ (action &&
547
+ ![FormAction.Validate, FormAction.SaveAndReturn].includes(action))
548
+ ) {
545
549
  return context
546
550
  }
547
551