@defra/forms-engine-plugin 5.0.0-alpha.20 → 5.0.0-alpha.21

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 (24) hide show
  1. package/.server/server/plugins/engine/models/FormModel.js +16 -6
  2. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  3. package/.server/server/plugins/engine/models/types.d.ts +28 -1
  4. package/.server/server/plugins/engine/models/types.js.map +1 -1
  5. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +11 -1
  6. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
  7. package/.server/server/plugins/engine/pageControllers/helpers/submission.d.ts +34 -1
  8. package/.server/server/plugins/engine/pageControllers/helpers/submission.js +97 -0
  9. package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -1
  10. package/.server/server/plugins/engine/types/enums.d.ts +3 -3
  11. package/.server/server/plugins/engine/types/enums.js +5 -4
  12. package/.server/server/plugins/engine/types/enums.js.map +1 -1
  13. package/.server/server/plugins/engine/types/schema.js +9 -3
  14. package/.server/server/plugins/engine/types/schema.js.map +1 -1
  15. package/.server/server/plugins/engine/types.d.ts +13 -2
  16. package/.server/server/plugins/engine/types.js.map +1 -1
  17. package/package.json +2 -2
  18. package/src/server/plugins/engine/models/FormModel.ts +25 -6
  19. package/src/server/plugins/engine/models/types.ts +31 -0
  20. package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +15 -1
  21. package/src/server/plugins/engine/pageControllers/helpers/submission.ts +137 -1
  22. package/src/server/plugins/engine/types/enums.ts +4 -3
  23. package/src/server/plugins/engine/types/schema.ts +16 -4
  24. package/src/server/plugins/engine/types.ts +14 -1
@@ -1,4 +1,4 @@
1
- import { ComponentType, ConditionsModel, ControllerPath, ControllerType, SchemaVersion, convertConditionWrapperFromV2, formDefinitionSchema, formDefinitionV2Schema, generateConditionAlias, hasComponents, hasComponentsEvenIfNoNext, hasRepeater, isConditionWrapperV2, yesNoListId, yesNoListName } from '@defra/forms-model';
1
+ import { ComponentType, ConditionEvaluationOutcome, ConditionsModel, ControllerPath, ControllerType, SchemaVersion, convertConditionWrapperFromV2, formDefinitionSchema, formDefinitionV2Schema, generateConditionAlias, getErrorMessage, hasComponents, hasComponentsEvenIfNoNext, hasRepeater, isConditionWrapperV2, yesNoListId, yesNoListName } from '@defra/forms-model';
2
2
  import { add, format } from 'date-fns';
3
3
  import { Parser } from 'expr-eval-fork';
4
4
  import joi from 'joi';
@@ -189,20 +189,30 @@ export class FormModel {
189
189
  cause
190
190
  });
191
191
  }
192
- const fn = evaluationState => {
192
+ const evaluate = evaluationState => {
193
193
  const ctx = this.toConditionContext(evaluationState, this.conditions);
194
194
  try {
195
- return expr.evaluate(ctx);
196
- } catch {
197
- return false;
195
+ return {
196
+ outcome: expr.evaluate(ctx) ? ConditionEvaluationOutcome.True : ConditionEvaluationOutcome.False
197
+ };
198
+ } catch (err) {
199
+ return {
200
+ outcome: ConditionEvaluationOutcome.Error,
201
+ error: getErrorMessage(err)
202
+ };
198
203
  }
199
204
  };
205
+
206
+ // A failed evaluation continues to route as `false`. `evaluate` exists so
207
+ // that the two can be told apart when recording outcomes for submission.
208
+ const fn = evaluationState => evaluate(evaluationState).outcome === ConditionEvaluationOutcome.True;
200
209
  return {
201
210
  name,
202
211
  displayName,
203
212
  value,
204
213
  expr,
205
- fn
214
+ fn,
215
+ evaluate
206
216
  };
207
217
  }
208
218
  toConditionContext(evaluationState, conditions) {
@@ -1 +1 @@
1
- {"version":3,"file":"FormModel.js","names":["ComponentType","ConditionsModel","ControllerPath","ControllerType","SchemaVersion","convertConditionWrapperFromV2","formDefinitionSchema","formDefinitionV2Schema","generateConditionAlias","hasComponents","hasComponentsEvenIfNoNext","hasRepeater","isConditionWrapperV2","yesNoListId","yesNoListName","add","format","Parser","joi","logger","EN_GB","hasListFormField","todayAsDateOnly","ConditionBuildError","SchemaValidationError","findPage","getError","getPage","loadFormTranslations","createTranslator","extractBaseTranslations","createFormI18nInstance","getAvailableLanguages","createPage","validationOptions","opts","defaultServices","FormAction","merge","FormModel","engine","schemaVersion","def","lists","sections","name","formId","values","basePath","ordnanceSurveyApiKey","conditions","pages","services","i18nInstance","controllers","pageDefMap","listDefMap","listDefIdMap","componentDefMap","componentDefIdMap","pageMap","componentMap","constructor","options","schema","V1","warn","result","validate","abortEarly","error","structuredClone","value","baseTranslations","push","id","title","type","items","text","moreThanOnePaymentQuestion","Error","Map","map","page","path","list","filter","flatMap","components","component","forEach","conditionDef","condition","makeCondition","pageDef","some","controller","Status","collection","language","metadata","translations","makeFilteredSchema","relevantPages","object","required","concat","stateSchema","parser","operators","logical","Object","assign","functions","dateForComparison","timePeriod","timeUnit","displayName","expr","toConditionExpression","cause","fn","evaluationState","ctx","toConditionContext","evaluate","context","conditionId","propertyName","V2","defineProperty","get","from","parse","toExpression","getList","nameOrId","find","getSection","section","getFormContext","request","state","errors","translator","query","currentPath","startPath","getStartPath","isForceAccess","relevantState","payload","getFormDataFromState","paths","data","referenceNumber","getReferenceNumber","languages","validateFormPayload","nextPage","initialiseContext","assignEvaluationState","assignRelevantState","pageStateIsInvalid","getNextPath","validateFormState","assignPaths","emptyState","freeze","getContextValueFromState","key","keys","listFields","fields","field","undefined","YesNoField","hasOptionalItems","item","length","fieldStateIsInvalid","validValues","fieldState","getFormValueFromState","isInvalid","isArray","Array","every","includes","t","href","getComponentById","componentId","getListById","listId","getConditionById","numOfPaymentFields","comp","PaymentField","action","getFormParams","Validate","startsWith","External","update","CheckboxesField","formState","getStateFromValidForm","previousPages","relevantPage","model","stripUnknown","errorsState","details","$$__referenceNumber"],"sources":["../../../../../src/server/plugins/engine/models/FormModel.ts"],"sourcesContent":["import {\n ComponentType,\n ConditionsModel,\n ControllerPath,\n ControllerType,\n SchemaVersion,\n convertConditionWrapperFromV2,\n formDefinitionSchema,\n formDefinitionV2Schema,\n generateConditionAlias,\n hasComponents,\n hasComponentsEvenIfNoNext,\n hasRepeater,\n isConditionWrapperV2,\n yesNoListId,\n yesNoListName,\n type ComponentDef,\n type ConditionWrapper,\n type ConditionWrapperV2,\n type ConditionsModelData,\n type DateUnits,\n type Engine,\n type FormDefinition,\n type List,\n type Page,\n type Section\n} from '@defra/forms-model'\nimport { add, format } from 'date-fns'\nimport { Parser, type Value } from 'expr-eval-fork'\nimport { type i18n } from 'i18next'\nimport joi from 'joi'\n\nimport { logger } from '~/src/server/common/helpers/logging/logger.js'\nimport { EN_GB } from '~/src/server/constants.js'\nimport { type ListFormComponent } from '~/src/server/plugins/engine/components/ListFormComponent.js'\nimport {} from '~/src/server/plugins/engine/components/YesNoField.js'\nimport {\n hasListFormField,\n type Component\n} from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'\nimport {\n ConditionBuildError,\n SchemaValidationError\n} from '~/src/server/plugins/engine/errors.js'\nimport {\n findPage,\n getError,\n getPage\n} from '~/src/server/plugins/engine/helpers.js'\nimport { loadFormTranslations } from '~/src/server/plugins/engine/i18n/createFormTranslator.js'\nimport { createTranslator } from '~/src/server/plugins/engine/i18n/createTranslator.js'\nimport { extractBaseTranslations } from '~/src/server/plugins/engine/i18n/extractBaseTranslations.js'\nimport { createFormI18nInstance } from '~/src/server/plugins/engine/i18n/index.js'\nimport { getAvailableLanguages } from '~/src/server/plugins/engine/i18n/languages.js'\nimport { type Translator } from '~/src/server/plugins/engine/i18n/types.js'\nimport { type ExecutableCondition } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n createPage,\n type PageControllerClass\n} from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { validationOptions as opts } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport {\n type FormContext,\n type FormContextRequest,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport { FormAction } from '~/src/server/routes/types.js'\nimport { merge } from '~/src/server/services/cacheService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport class FormModel {\n /** The runtime engine that should be used */\n engine?: Engine\n\n schemaVersion: SchemaVersion\n\n /** the entire form JSON as an object */\n def: FormDefinition\n\n lists: FormDefinition['lists']\n sections: FormDefinition['sections'] = []\n name: string\n formId: string\n values: FormDefinition\n basePath: string\n ordnanceSurveyApiKey?: string\n conditions: Partial<Record<string, ExecutableCondition>>\n pages: PageControllerClass[]\n services: Services\n private readonly i18nInstance: i18n\n\n controllers?: Record<string, typeof PageController>\n pageDefMap: Map<string, Page>\n\n listDefMap: Map<string, List>\n listDefIdMap: Map<string, List>\n\n componentDefMap: Map<string, ComponentDef>\n componentDefIdMap: Map<string, ComponentDef>\n\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n\n constructor(\n def: typeof this.def,\n options: {\n basePath: string\n ordnanceSurveyApiKey?: string\n formId?: string\n },\n services: Services = defaultServices,\n controllers?: Record<string, typeof PageController>\n ) {\n let schema = formDefinitionV2Schema\n\n if (!def.schema || def.schema === SchemaVersion.V1) {\n logger.warn(\n `[DEPRECATION NOTICE] Form \"${def.name}\" constructed with legacy V1 schema. See https://defra.github.io/forms-engine-plugin/schemas/form-definition-schema.html.`\n )\n schema = formDefinitionSchema\n }\n\n const result = schema.validate(def, { abortEarly: false })\n\n if (result.error) {\n throw new SchemaValidationError(result.error)\n }\n\n // Make a clone of the shallow copy returned\n // by joi so as not to change the source data.\n def = structuredClone(result.value)\n\n const baseTranslations = extractBaseTranslations(def)\n this.i18nInstance = createFormI18nInstance(baseTranslations)\n loadFormTranslations(def, this.i18nInstance)\n\n // Add default lists. Yes/No text stored as i18n key constants so they\n // resolve to the user's language at render time via the translator.\n def.lists.push({\n id: def.schema === SchemaVersion.V1 ? yesNoListName : yesNoListId,\n name: '__yesNo',\n title: 'Yes/No',\n type: 'boolean',\n items: [\n {\n id: '02900d42-83d1-4c72-a719-c4e8228952fa',\n text: 'components.yesNoField.yes',\n value: true\n },\n {\n id: 'f39000eb-c51b-4019-8f82-bbda0423f04d',\n text: 'components.yesNoField.no',\n value: false\n }\n ]\n })\n\n this.engine = def.engine\n this.schemaVersion = def.schema ?? SchemaVersion.V1\n this.def = def\n this.lists = def.lists\n this.sections = def.sections\n this.name = def.name ?? ''\n this.formId = options.formId ?? ''\n this.values = result.value\n this.basePath = options.basePath\n this.ordnanceSurveyApiKey = options.ordnanceSurveyApiKey\n this.conditions = {}\n this.services = services\n this.controllers = controllers\n\n // Assert that there is only one payment question (if any)\n if (this.moreThanOnePaymentQuestion()) {\n throw new Error(\n 'Invalid form definition: Only one payment question is allowed per form'\n )\n }\n\n this.pageDefMap = new Map(def.pages.map((page) => [page.path, page]))\n this.listDefMap = new Map(def.lists.map((list) => [list.name, list]))\n this.listDefIdMap = new Map(\n def.lists\n .filter((list) => list.id) // Skip lists without an ID\n // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style\n .map((list) => [list.id as string, list])\n )\n this.componentDefMap = new Map(\n def.pages\n .filter(hasComponents)\n .flatMap((page) =>\n page.components.map((component) => [component.name, component])\n )\n )\n this.componentDefIdMap = new Map(\n def.pages.filter(hasComponents).flatMap((page) =>\n page.components\n .filter((component) => component.id) // Skip components without an ID\n .map((component) => {\n // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style\n return [component.id as string, component]\n })\n )\n )\n\n def.conditions.forEach((conditionDef) => {\n const condition = this.makeCondition(\n isConditionWrapperV2(conditionDef)\n ? convertConditionWrapperFromV2(conditionDef, this)\n : conditionDef\n )\n this.conditions[condition.name] = condition\n })\n\n this.pages = def.pages.map((pageDef) => createPage(this, pageDef))\n\n if (\n !def.pages.some(\n ({ controller }) =>\n // Check for user-provided status page (optional)\n controller === ControllerType.Status\n )\n ) {\n this.pages.push(\n createPage(this, {\n title: 'pages.confirmation.formSubmitted',\n path: ControllerPath.Status,\n controller: ControllerType.Status\n })\n )\n }\n\n this.pageMap = new Map(this.pages.map((page) => [page.path, page]))\n this.componentMap = new Map(\n this.pages.flatMap((page) =>\n page.collection.components.map((component) => [\n component.name,\n component\n ])\n )\n )\n }\n\n /** Returns a scoped translator pair for the given language. */\n createTranslator(language = EN_GB): Translator {\n // @ts-expect-error - dynamic language lookup\n if (!this.def.metadata?.translations?.[language]) {\n // If not translations defined in the FormDefinition, always default to English\n language = EN_GB\n }\n return createTranslator(this.i18nInstance, language)\n }\n\n /**\n * build the entire model schema from individual pages/sections and filter out answers\n * for pages which are no longer accessible due to an answer that has been changed\n */\n makeFilteredSchema(relevantPages: PageControllerClass[]) {\n // Build the entire model schema\n // from the individual pages/sections\n let schema = joi.object<FormSubmissionState>().required()\n\n relevantPages.forEach((page) => {\n schema = schema.concat(page.collection.stateSchema)\n })\n\n return schema\n }\n\n /**\n * Instantiates a Condition based on {@link ConditionWrapper}\n * @param condition\n */\n makeCondition(condition: ConditionWrapper): ExecutableCondition {\n const parser = new Parser({\n operators: {\n logical: true\n }\n })\n\n Object.assign(parser.functions, {\n dateForComparison(timePeriod: number, timeUnit: DateUnits) {\n // The time element must be stripped (hence using startOfDay() which has no time element),\n // then formatted as YYYY-MM-DD otherwise we can hit time element and BST issues giving the\n // wrong date to compare against.\n // Do not use .toISOString() to format the date as that introduces BST errors.\n return format(\n add(todayAsDateOnly(), { [timeUnit]: timePeriod }),\n 'yyyy-MM-dd'\n )\n }\n })\n\n const { name, displayName, value } = condition\n\n let expr\n try {\n expr = this.toConditionExpression(value, parser)\n } catch (cause) {\n throw new ConditionBuildError(displayName, { cause })\n }\n\n const fn = (evaluationState: FormState) => {\n const ctx = this.toConditionContext(evaluationState, this.conditions)\n try {\n return expr.evaluate(ctx) as boolean\n } catch {\n return false\n }\n }\n\n return {\n name,\n displayName,\n value,\n expr,\n fn\n }\n }\n\n toConditionContext(\n evaluationState: FormState,\n conditions: Partial<Record<string, ExecutableCondition>>\n ) {\n const context = { ...evaluationState }\n\n for (const conditionId in conditions) {\n const propertyName =\n this.schemaVersion === SchemaVersion.V2\n ? generateConditionAlias(conditionId)\n : conditionId\n\n Object.defineProperty(context, propertyName, {\n get() {\n return conditions[conditionId]?.fn(evaluationState)\n }\n })\n }\n\n return context as Extract<Value, Record<string, Value>>\n }\n\n toConditionExpression(value: ConditionsModelData, parser: Parser) {\n const conditions = ConditionsModel.from(value)\n return parser.parse(conditions.toExpression())\n }\n\n getList(nameOrId: string): List | undefined {\n return this.schemaVersion === SchemaVersion.V1\n ? this.lists.find((list) => list.name === nameOrId)\n : this.lists.find((list) => list.id === nameOrId)\n }\n\n getSection(nameOrId: string): Section | undefined {\n return this.schemaVersion === SchemaVersion.V1\n ? this.sections.find((section) => section.name === nameOrId)\n : this.sections.find((section) => section.id === nameOrId)\n }\n\n /**\n * Form context for the current page\n */\n getFormContext(\n request: FormContextRequest,\n state: FormSubmissionState,\n errors?: FormSubmissionError[],\n translator?: Translator\n ): FormContext {\n const { query } = request\n\n const page = getPage(this, request)\n\n // Determine form paths\n const currentPath = page.path\n const startPath = page.getStartPath()\n\n // Preview URL direct access is allowed\n const isForceAccess = 'force' in query\n\n let context: FormContext = {\n evaluationState: {},\n relevantState: {},\n relevantPages: [],\n payload: page.getFormDataFromState(request, state),\n state,\n paths: [],\n errors,\n isForceAccess,\n data: {},\n pageDefMap: this.pageDefMap,\n listDefMap: this.listDefMap,\n componentDefMap: this.componentDefMap,\n pageMap: this.pageMap,\n componentMap: this.componentMap,\n referenceNumber: getReferenceNumber(state),\n languages: getAvailableLanguages(this.def),\n translator\n }\n\n // Validate current page\n context = validateFormPayload(request, page, context, translator)\n\n // Find start page\n let nextPage = findPage(this, startPath)\n\n this.initialiseContext(context)\n\n // Walk form pages from start\n while (nextPage) {\n // Add page to context\n context.relevantPages.push(nextPage)\n\n this.assignEvaluationState(context, nextPage)\n\n this.assignRelevantState(context, nextPage)\n\n // Stop at current page\n if (\n this.pageStateIsInvalid(context, nextPage, translator) ||\n nextPage.path === currentPath\n ) {\n break\n }\n\n // Apply conditions to determine next page\n nextPage = findPage(this, nextPage.getNextPath(context))\n }\n\n // Validate form state\n context = validateFormState(request, page, context)\n\n // Add paths for navigation\n this.assignPaths(context)\n\n return context\n }\n\n private initialiseContext(context: FormContext) {\n // Initialise `evaluationState` for all keys using empty state.\n // This is because the current condition evaluation library (eval-expr)\n // will throw if an expression uses a key that is undefined.\n const emptyState = Object.freeze({})\n\n for (const page of this.pages) {\n const { collection, pageDef } = page\n\n if (!hasRepeater(pageDef)) {\n Object.assign(\n context.evaluationState,\n collection.getContextValueFromState(emptyState)\n )\n }\n }\n }\n\n private assignEvaluationState(\n context: FormContext,\n page: PageControllerClass\n ) {\n const { collection, pageDef } = page\n // Skip evaluation state for repeater pages\n\n if (!hasRepeater(pageDef)) {\n Object.assign(\n context.evaluationState,\n collection.getContextValueFromState(context.state)\n )\n }\n }\n\n private assignRelevantState(context: FormContext, page: PageControllerClass) {\n // Copy relevant state by expected keys\n for (const key of page.keys) {\n if (typeof context.state[key] !== 'undefined') {\n context.relevantState[key] = context.state[key]\n }\n }\n }\n\n private pageStateIsInvalid(\n context: FormContext,\n page: PageControllerClass,\n translator?: Translator\n ) {\n // Get any list-bound fields on the page\n const listFields = page.collection.fields.filter(hasListFormField)\n\n // For each list field that is bound to a list that contains any conditional items,\n // we need to check any answers are still valid. Do this by evaluating the conditions\n // and ensuring any current answers are all included in the set of valid answers\n for (const field of listFields) {\n const list = field.list\n\n // Filter out YesNo as they can't be conditional\n if (list !== undefined && field.type !== ComponentType.YesNoField) {\n const hasOptionalItems =\n list.items.filter((item) => item.condition).length > 0\n\n if (hasOptionalItems) {\n return this.fieldStateIsInvalid(context, field, list, translator)\n }\n }\n }\n }\n\n private fieldStateIsInvalid(\n context: FormContext,\n field: ListFormComponent,\n list: List,\n translator?: Translator\n ) {\n const { evaluationState, state } = context\n\n const validValues = list.items\n .filter((item) =>\n item.condition\n ? this.conditions[item.condition]?.fn(evaluationState)\n : true\n )\n .map((item) => item.value)\n\n // Get the field state\n const fieldState = field.getFormValueFromState(state)\n\n if (fieldState !== undefined) {\n let isInvalid = false\n const isArray = Array.isArray(fieldState)\n\n // Check if any saved state value(s) are still valid\n // and return true if any are invalid\n if (isArray) {\n isInvalid = !fieldState.every((item) => validValues.includes(item))\n } else {\n isInvalid = !validValues.includes(fieldState)\n }\n\n if (isInvalid) {\n context.errors ??= []\n\n const text = (translator ?? this.createTranslator('en-GB')).t(\n 'errors.optionsMismatch'\n )\n\n context.errors.push({\n text,\n name: field.name,\n href: `#${field.name}`,\n path: [`#${field.name}`]\n })\n }\n\n return isInvalid\n }\n }\n\n private assignPaths(context: FormContext) {\n for (const { keys, path } of context.relevantPages) {\n context.paths.push(path)\n\n // Stop at page with errors\n if (\n context.errors?.some(({ name, path }) => {\n return keys.includes(name) || keys.some((key) => path.includes(key))\n })\n ) {\n break\n }\n }\n }\n\n getComponentById(componentId: string): ComponentDef | undefined {\n return this.componentDefIdMap.get(componentId)\n }\n\n getListById(listId: string): List | undefined {\n return this.listDefIdMap.get(listId)\n }\n\n /**\n * Returns a condition by its ID. O(n) lookup time.\n * @param conditionId\n * @returns\n */\n getConditionById(conditionId: string): ConditionWrapperV2 | undefined {\n return this.def.conditions\n .filter(isConditionWrapperV2)\n .find((condition) => condition.id === conditionId)\n }\n\n /**\n * Checks that only one payment field exists (if any payments fields exist)\n */\n moreThanOnePaymentQuestion() {\n const numOfPaymentFields = this.def.pages\n .flatMap((page) =>\n hasComponentsEvenIfNoNext(page) ? page.components : []\n )\n .filter((comp) => comp.type === ComponentType.PaymentField).length\n return numOfPaymentFields > 1\n }\n}\n\n/**\n * Validate current page only\n */\nfunction validateFormPayload(\n request: FormContextRequest,\n page: PageControllerClass,\n context: FormContext,\n translator?: Translator\n): FormContext {\n const { collection } = page\n const { payload, state } = context\n\n const { action } = page.getFormParams(request)\n\n // Skip validation GET requests or other actions\n if (\n !request.payload ||\n (action &&\n action !== FormAction.Validate &&\n !action.startsWith(FormAction.External))\n ) {\n return context\n }\n\n // For checkbox fields missing in the payload (i.e. unchecked),\n // explicitly set their value to undefined so that any previously\n // stored value is cleared and required field validation is enforced.\n const update = { ...request.payload }\n collection.fields.forEach((field) => {\n if (\n field.type === ComponentType.CheckboxesField &&\n !(field.name in update)\n ) {\n update[field.name] = undefined\n }\n })\n\n const { value, errors } = collection.validate(\n { ...payload, ...update },\n translator\n )\n\n // Add sanitised payload (ready to save)\n const formState = page.getStateFromValidForm(request, state, value)\n\n return {\n ...context,\n payload: merge(payload, value),\n state: merge(state, formState),\n errors\n }\n}\n\n/**\n * Validate entire form state\n */\nfunction validateFormState(\n request: FormContextRequest,\n page: PageControllerClass,\n context: FormContext\n): FormContext {\n const { errors = [], relevantPages, relevantState } = context\n\n // Exclude current page\n const previousPages = relevantPages.filter(\n (relevantPage) => relevantPage !== page\n )\n\n // Validate relevant state\n const { error } = page.model\n .makeFilteredSchema(previousPages)\n .validate(relevantState, { ...opts, stripUnknown: true })\n\n // Add relevant state errors\n if (error) {\n const errorsState = error.details.map((item) => getError(EN_GB, item))\n return { ...context, errors: errors.concat(errorsState) }\n }\n\n return context\n}\n\nfunction getReferenceNumber(state: FormSubmissionState): string {\n if (\n !state.$$__referenceNumber ||\n typeof state.$$__referenceNumber !== 'string'\n ) {\n throw Error('Reference number not found in form state')\n }\n\n return state.$$__referenceNumber\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"FormModel.js","names":["ComponentType","ConditionEvaluationOutcome","ConditionsModel","ControllerPath","ControllerType","SchemaVersion","convertConditionWrapperFromV2","formDefinitionSchema","formDefinitionV2Schema","generateConditionAlias","getErrorMessage","hasComponents","hasComponentsEvenIfNoNext","hasRepeater","isConditionWrapperV2","yesNoListId","yesNoListName","add","format","Parser","joi","logger","EN_GB","hasListFormField","todayAsDateOnly","ConditionBuildError","SchemaValidationError","findPage","getError","getPage","loadFormTranslations","createTranslator","extractBaseTranslations","createFormI18nInstance","getAvailableLanguages","createPage","validationOptions","opts","defaultServices","FormAction","merge","FormModel","engine","schemaVersion","def","lists","sections","name","formId","values","basePath","ordnanceSurveyApiKey","conditions","pages","services","i18nInstance","controllers","pageDefMap","listDefMap","listDefIdMap","componentDefMap","componentDefIdMap","pageMap","componentMap","constructor","options","schema","V1","warn","result","validate","abortEarly","error","structuredClone","value","baseTranslations","push","id","title","type","items","text","moreThanOnePaymentQuestion","Error","Map","map","page","path","list","filter","flatMap","components","component","forEach","conditionDef","condition","makeCondition","pageDef","some","controller","Status","collection","language","metadata","translations","makeFilteredSchema","relevantPages","object","required","concat","stateSchema","parser","operators","logical","Object","assign","functions","dateForComparison","timePeriod","timeUnit","displayName","expr","toConditionExpression","cause","evaluate","evaluationState","ctx","toConditionContext","outcome","True","False","err","fn","context","conditionId","propertyName","V2","defineProperty","get","from","parse","toExpression","getList","nameOrId","find","getSection","section","getFormContext","request","state","errors","translator","query","currentPath","startPath","getStartPath","isForceAccess","relevantState","payload","getFormDataFromState","paths","data","referenceNumber","getReferenceNumber","languages","validateFormPayload","nextPage","initialiseContext","assignEvaluationState","assignRelevantState","pageStateIsInvalid","getNextPath","validateFormState","assignPaths","emptyState","freeze","getContextValueFromState","key","keys","listFields","fields","field","undefined","YesNoField","hasOptionalItems","item","length","fieldStateIsInvalid","validValues","fieldState","getFormValueFromState","isInvalid","isArray","Array","every","includes","t","href","getComponentById","componentId","getListById","listId","getConditionById","numOfPaymentFields","comp","PaymentField","action","getFormParams","Validate","startsWith","External","update","CheckboxesField","formState","getStateFromValidForm","previousPages","relevantPage","model","stripUnknown","errorsState","details","$$__referenceNumber"],"sources":["../../../../../src/server/plugins/engine/models/FormModel.ts"],"sourcesContent":["import {\n ComponentType,\n ConditionEvaluationOutcome,\n ConditionsModel,\n ControllerPath,\n ControllerType,\n SchemaVersion,\n convertConditionWrapperFromV2,\n formDefinitionSchema,\n formDefinitionV2Schema,\n generateConditionAlias,\n getErrorMessage,\n hasComponents,\n hasComponentsEvenIfNoNext,\n hasRepeater,\n isConditionWrapperV2,\n yesNoListId,\n yesNoListName,\n type ComponentDef,\n type ConditionWrapper,\n type ConditionWrapperV2,\n type ConditionsModelData,\n type DateUnits,\n type Engine,\n type FormDefinition,\n type List,\n type Page,\n type Section\n} from '@defra/forms-model'\nimport { add, format } from 'date-fns'\nimport { Parser, type Value } from 'expr-eval-fork'\nimport { type i18n } from 'i18next'\nimport joi from 'joi'\n\nimport { logger } from '~/src/server/common/helpers/logging/logger.js'\nimport { EN_GB } from '~/src/server/constants.js'\nimport { type ListFormComponent } from '~/src/server/plugins/engine/components/ListFormComponent.js'\nimport {} from '~/src/server/plugins/engine/components/YesNoField.js'\nimport {\n hasListFormField,\n type Component\n} from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'\nimport {\n ConditionBuildError,\n SchemaValidationError\n} from '~/src/server/plugins/engine/errors.js'\nimport {\n findPage,\n getError,\n getPage\n} from '~/src/server/plugins/engine/helpers.js'\nimport { loadFormTranslations } from '~/src/server/plugins/engine/i18n/createFormTranslator.js'\nimport { createTranslator } from '~/src/server/plugins/engine/i18n/createTranslator.js'\nimport { extractBaseTranslations } from '~/src/server/plugins/engine/i18n/extractBaseTranslations.js'\nimport { createFormI18nInstance } from '~/src/server/plugins/engine/i18n/index.js'\nimport { getAvailableLanguages } from '~/src/server/plugins/engine/i18n/languages.js'\nimport { type Translator } from '~/src/server/plugins/engine/i18n/types.js'\nimport {\n type ConditionEvaluation,\n type ExecutableCondition\n} from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n createPage,\n type PageControllerClass\n} from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { validationOptions as opts } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport {\n type FormContext,\n type FormContextRequest,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport { FormAction } from '~/src/server/routes/types.js'\nimport { merge } from '~/src/server/services/cacheService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport class FormModel {\n /** The runtime engine that should be used */\n engine?: Engine\n\n schemaVersion: SchemaVersion\n\n /** the entire form JSON as an object */\n def: FormDefinition\n\n lists: FormDefinition['lists']\n sections: FormDefinition['sections'] = []\n name: string\n formId: string\n values: FormDefinition\n basePath: string\n ordnanceSurveyApiKey?: string\n conditions: Partial<Record<string, ExecutableCondition>>\n pages: PageControllerClass[]\n services: Services\n private readonly i18nInstance: i18n\n\n controllers?: Record<string, typeof PageController>\n pageDefMap: Map<string, Page>\n\n listDefMap: Map<string, List>\n listDefIdMap: Map<string, List>\n\n componentDefMap: Map<string, ComponentDef>\n componentDefIdMap: Map<string, ComponentDef>\n\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n\n constructor(\n def: typeof this.def,\n options: {\n basePath: string\n ordnanceSurveyApiKey?: string\n formId?: string\n },\n services: Services = defaultServices,\n controllers?: Record<string, typeof PageController>\n ) {\n let schema = formDefinitionV2Schema\n\n if (!def.schema || def.schema === SchemaVersion.V1) {\n logger.warn(\n `[DEPRECATION NOTICE] Form \"${def.name}\" constructed with legacy V1 schema. See https://defra.github.io/forms-engine-plugin/schemas/form-definition-schema.html.`\n )\n schema = formDefinitionSchema\n }\n\n const result = schema.validate(def, { abortEarly: false })\n\n if (result.error) {\n throw new SchemaValidationError(result.error)\n }\n\n // Make a clone of the shallow copy returned\n // by joi so as not to change the source data.\n def = structuredClone(result.value)\n\n const baseTranslations = extractBaseTranslations(def)\n this.i18nInstance = createFormI18nInstance(baseTranslations)\n loadFormTranslations(def, this.i18nInstance)\n\n // Add default lists. Yes/No text stored as i18n key constants so they\n // resolve to the user's language at render time via the translator.\n def.lists.push({\n id: def.schema === SchemaVersion.V1 ? yesNoListName : yesNoListId,\n name: '__yesNo',\n title: 'Yes/No',\n type: 'boolean',\n items: [\n {\n id: '02900d42-83d1-4c72-a719-c4e8228952fa',\n text: 'components.yesNoField.yes',\n value: true\n },\n {\n id: 'f39000eb-c51b-4019-8f82-bbda0423f04d',\n text: 'components.yesNoField.no',\n value: false\n }\n ]\n })\n\n this.engine = def.engine\n this.schemaVersion = def.schema ?? SchemaVersion.V1\n this.def = def\n this.lists = def.lists\n this.sections = def.sections\n this.name = def.name ?? ''\n this.formId = options.formId ?? ''\n this.values = result.value\n this.basePath = options.basePath\n this.ordnanceSurveyApiKey = options.ordnanceSurveyApiKey\n this.conditions = {}\n this.services = services\n this.controllers = controllers\n\n // Assert that there is only one payment question (if any)\n if (this.moreThanOnePaymentQuestion()) {\n throw new Error(\n 'Invalid form definition: Only one payment question is allowed per form'\n )\n }\n\n this.pageDefMap = new Map(def.pages.map((page) => [page.path, page]))\n this.listDefMap = new Map(def.lists.map((list) => [list.name, list]))\n this.listDefIdMap = new Map(\n def.lists\n .filter((list) => list.id) // Skip lists without an ID\n // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style\n .map((list) => [list.id as string, list])\n )\n this.componentDefMap = new Map(\n def.pages\n .filter(hasComponents)\n .flatMap((page) =>\n page.components.map((component) => [component.name, component])\n )\n )\n this.componentDefIdMap = new Map(\n def.pages.filter(hasComponents).flatMap((page) =>\n page.components\n .filter((component) => component.id) // Skip components without an ID\n .map((component) => {\n // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style\n return [component.id as string, component]\n })\n )\n )\n\n def.conditions.forEach((conditionDef) => {\n const condition = this.makeCondition(\n isConditionWrapperV2(conditionDef)\n ? convertConditionWrapperFromV2(conditionDef, this)\n : conditionDef\n )\n this.conditions[condition.name] = condition\n })\n\n this.pages = def.pages.map((pageDef) => createPage(this, pageDef))\n\n if (\n !def.pages.some(\n ({ controller }) =>\n // Check for user-provided status page (optional)\n controller === ControllerType.Status\n )\n ) {\n this.pages.push(\n createPage(this, {\n title: 'pages.confirmation.formSubmitted',\n path: ControllerPath.Status,\n controller: ControllerType.Status\n })\n )\n }\n\n this.pageMap = new Map(this.pages.map((page) => [page.path, page]))\n this.componentMap = new Map(\n this.pages.flatMap((page) =>\n page.collection.components.map((component) => [\n component.name,\n component\n ])\n )\n )\n }\n\n /** Returns a scoped translator pair for the given language. */\n createTranslator(language = EN_GB): Translator {\n // @ts-expect-error - dynamic language lookup\n if (!this.def.metadata?.translations?.[language]) {\n // If not translations defined in the FormDefinition, always default to English\n language = EN_GB\n }\n return createTranslator(this.i18nInstance, language)\n }\n\n /**\n * build the entire model schema from individual pages/sections and filter out answers\n * for pages which are no longer accessible due to an answer that has been changed\n */\n makeFilteredSchema(relevantPages: PageControllerClass[]) {\n // Build the entire model schema\n // from the individual pages/sections\n let schema = joi.object<FormSubmissionState>().required()\n\n relevantPages.forEach((page) => {\n schema = schema.concat(page.collection.stateSchema)\n })\n\n return schema\n }\n\n /**\n * Instantiates a Condition based on {@link ConditionWrapper}\n * @param condition\n */\n makeCondition(condition: ConditionWrapper): ExecutableCondition {\n const parser = new Parser({\n operators: {\n logical: true\n }\n })\n\n Object.assign(parser.functions, {\n dateForComparison(timePeriod: number, timeUnit: DateUnits) {\n // The time element must be stripped (hence using startOfDay() which has no time element),\n // then formatted as YYYY-MM-DD otherwise we can hit time element and BST issues giving the\n // wrong date to compare against.\n // Do not use .toISOString() to format the date as that introduces BST errors.\n return format(\n add(todayAsDateOnly(), { [timeUnit]: timePeriod }),\n 'yyyy-MM-dd'\n )\n }\n })\n\n const { name, displayName, value } = condition\n\n let expr\n try {\n expr = this.toConditionExpression(value, parser)\n } catch (cause) {\n throw new ConditionBuildError(displayName, { cause })\n }\n\n const evaluate = (evaluationState: FormState): ConditionEvaluation => {\n const ctx = this.toConditionContext(evaluationState, this.conditions)\n\n try {\n return {\n outcome: (expr.evaluate(ctx) as boolean)\n ? ConditionEvaluationOutcome.True\n : ConditionEvaluationOutcome.False\n }\n } catch (err) {\n return {\n outcome: ConditionEvaluationOutcome.Error,\n error: getErrorMessage(err)\n }\n }\n }\n\n // A failed evaluation continues to route as `false`. `evaluate` exists so\n // that the two can be told apart when recording outcomes for submission.\n const fn = (evaluationState: FormState) =>\n evaluate(evaluationState).outcome === ConditionEvaluationOutcome.True\n\n return {\n name,\n displayName,\n value,\n expr,\n fn,\n evaluate\n }\n }\n\n toConditionContext(\n evaluationState: FormState,\n conditions: Partial<Record<string, ExecutableCondition>>\n ) {\n const context = { ...evaluationState }\n\n for (const conditionId in conditions) {\n const propertyName =\n this.schemaVersion === SchemaVersion.V2\n ? generateConditionAlias(conditionId)\n : conditionId\n\n Object.defineProperty(context, propertyName, {\n get() {\n return conditions[conditionId]?.fn(evaluationState)\n }\n })\n }\n\n return context as Extract<Value, Record<string, Value>>\n }\n\n toConditionExpression(value: ConditionsModelData, parser: Parser) {\n const conditions = ConditionsModel.from(value)\n return parser.parse(conditions.toExpression())\n }\n\n getList(nameOrId: string): List | undefined {\n return this.schemaVersion === SchemaVersion.V1\n ? this.lists.find((list) => list.name === nameOrId)\n : this.lists.find((list) => list.id === nameOrId)\n }\n\n getSection(nameOrId: string): Section | undefined {\n return this.schemaVersion === SchemaVersion.V1\n ? this.sections.find((section) => section.name === nameOrId)\n : this.sections.find((section) => section.id === nameOrId)\n }\n\n /**\n * Form context for the current page\n */\n getFormContext(\n request: FormContextRequest,\n state: FormSubmissionState,\n errors?: FormSubmissionError[],\n translator?: Translator\n ): FormContext {\n const { query } = request\n\n const page = getPage(this, request)\n\n // Determine form paths\n const currentPath = page.path\n const startPath = page.getStartPath()\n\n // Preview URL direct access is allowed\n const isForceAccess = 'force' in query\n\n let context: FormContext = {\n evaluationState: {},\n relevantState: {},\n relevantPages: [],\n payload: page.getFormDataFromState(request, state),\n state,\n paths: [],\n errors,\n isForceAccess,\n data: {},\n pageDefMap: this.pageDefMap,\n listDefMap: this.listDefMap,\n componentDefMap: this.componentDefMap,\n pageMap: this.pageMap,\n componentMap: this.componentMap,\n referenceNumber: getReferenceNumber(state),\n languages: getAvailableLanguages(this.def),\n translator\n }\n\n // Validate current page\n context = validateFormPayload(request, page, context, translator)\n\n // Find start page\n let nextPage = findPage(this, startPath)\n\n this.initialiseContext(context)\n\n // Walk form pages from start\n while (nextPage) {\n // Add page to context\n context.relevantPages.push(nextPage)\n\n this.assignEvaluationState(context, nextPage)\n\n this.assignRelevantState(context, nextPage)\n\n // Stop at current page\n if (\n this.pageStateIsInvalid(context, nextPage, translator) ||\n nextPage.path === currentPath\n ) {\n break\n }\n\n // Apply conditions to determine next page\n nextPage = findPage(this, nextPage.getNextPath(context))\n }\n\n // Validate form state\n context = validateFormState(request, page, context)\n\n // Add paths for navigation\n this.assignPaths(context)\n\n return context\n }\n\n private initialiseContext(context: FormContext) {\n // Initialise `evaluationState` for all keys using empty state.\n // This is because the current condition evaluation library (eval-expr)\n // will throw if an expression uses a key that is undefined.\n const emptyState = Object.freeze({})\n\n for (const page of this.pages) {\n const { collection, pageDef } = page\n\n if (!hasRepeater(pageDef)) {\n Object.assign(\n context.evaluationState,\n collection.getContextValueFromState(emptyState)\n )\n }\n }\n }\n\n private assignEvaluationState(\n context: FormContext,\n page: PageControllerClass\n ) {\n const { collection, pageDef } = page\n // Skip evaluation state for repeater pages\n\n if (!hasRepeater(pageDef)) {\n Object.assign(\n context.evaluationState,\n collection.getContextValueFromState(context.state)\n )\n }\n }\n\n private assignRelevantState(context: FormContext, page: PageControllerClass) {\n // Copy relevant state by expected keys\n for (const key of page.keys) {\n if (typeof context.state[key] !== 'undefined') {\n context.relevantState[key] = context.state[key]\n }\n }\n }\n\n private pageStateIsInvalid(\n context: FormContext,\n page: PageControllerClass,\n translator?: Translator\n ) {\n // Get any list-bound fields on the page\n const listFields = page.collection.fields.filter(hasListFormField)\n\n // For each list field that is bound to a list that contains any conditional items,\n // we need to check any answers are still valid. Do this by evaluating the conditions\n // and ensuring any current answers are all included in the set of valid answers\n for (const field of listFields) {\n const list = field.list\n\n // Filter out YesNo as they can't be conditional\n if (list !== undefined && field.type !== ComponentType.YesNoField) {\n const hasOptionalItems =\n list.items.filter((item) => item.condition).length > 0\n\n if (hasOptionalItems) {\n return this.fieldStateIsInvalid(context, field, list, translator)\n }\n }\n }\n }\n\n private fieldStateIsInvalid(\n context: FormContext,\n field: ListFormComponent,\n list: List,\n translator?: Translator\n ) {\n const { evaluationState, state } = context\n\n const validValues = list.items\n .filter((item) =>\n item.condition\n ? this.conditions[item.condition]?.fn(evaluationState)\n : true\n )\n .map((item) => item.value)\n\n // Get the field state\n const fieldState = field.getFormValueFromState(state)\n\n if (fieldState !== undefined) {\n let isInvalid = false\n const isArray = Array.isArray(fieldState)\n\n // Check if any saved state value(s) are still valid\n // and return true if any are invalid\n if (isArray) {\n isInvalid = !fieldState.every((item) => validValues.includes(item))\n } else {\n isInvalid = !validValues.includes(fieldState)\n }\n\n if (isInvalid) {\n context.errors ??= []\n\n const text = (translator ?? this.createTranslator('en-GB')).t(\n 'errors.optionsMismatch'\n )\n\n context.errors.push({\n text,\n name: field.name,\n href: `#${field.name}`,\n path: [`#${field.name}`]\n })\n }\n\n return isInvalid\n }\n }\n\n private assignPaths(context: FormContext) {\n for (const { keys, path } of context.relevantPages) {\n context.paths.push(path)\n\n // Stop at page with errors\n if (\n context.errors?.some(({ name, path }) => {\n return keys.includes(name) || keys.some((key) => path.includes(key))\n })\n ) {\n break\n }\n }\n }\n\n getComponentById(componentId: string): ComponentDef | undefined {\n return this.componentDefIdMap.get(componentId)\n }\n\n getListById(listId: string): List | undefined {\n return this.listDefIdMap.get(listId)\n }\n\n /**\n * Returns a condition by its ID. O(n) lookup time.\n * @param conditionId\n * @returns\n */\n getConditionById(conditionId: string): ConditionWrapperV2 | undefined {\n return this.def.conditions\n .filter(isConditionWrapperV2)\n .find((condition) => condition.id === conditionId)\n }\n\n /**\n * Checks that only one payment field exists (if any payments fields exist)\n */\n moreThanOnePaymentQuestion() {\n const numOfPaymentFields = this.def.pages\n .flatMap((page) =>\n hasComponentsEvenIfNoNext(page) ? page.components : []\n )\n .filter((comp) => comp.type === ComponentType.PaymentField).length\n return numOfPaymentFields > 1\n }\n}\n\n/**\n * Validate current page only\n */\nfunction validateFormPayload(\n request: FormContextRequest,\n page: PageControllerClass,\n context: FormContext,\n translator?: Translator\n): FormContext {\n const { collection } = page\n const { payload, state } = context\n\n const { action } = page.getFormParams(request)\n\n // Skip validation GET requests or other actions\n if (\n !request.payload ||\n (action &&\n action !== FormAction.Validate &&\n !action.startsWith(FormAction.External))\n ) {\n return context\n }\n\n // For checkbox fields missing in the payload (i.e. unchecked),\n // explicitly set their value to undefined so that any previously\n // stored value is cleared and required field validation is enforced.\n const update = { ...request.payload }\n collection.fields.forEach((field) => {\n if (\n field.type === ComponentType.CheckboxesField &&\n !(field.name in update)\n ) {\n update[field.name] = undefined\n }\n })\n\n const { value, errors } = collection.validate(\n { ...payload, ...update },\n translator\n )\n\n // Add sanitised payload (ready to save)\n const formState = page.getStateFromValidForm(request, state, value)\n\n return {\n ...context,\n payload: merge(payload, value),\n state: merge(state, formState),\n errors\n }\n}\n\n/**\n * Validate entire form state\n */\nfunction validateFormState(\n request: FormContextRequest,\n page: PageControllerClass,\n context: FormContext\n): FormContext {\n const { errors = [], relevantPages, relevantState } = context\n\n // Exclude current page\n const previousPages = relevantPages.filter(\n (relevantPage) => relevantPage !== page\n )\n\n // Validate relevant state\n const { error } = page.model\n .makeFilteredSchema(previousPages)\n .validate(relevantState, { ...opts, stripUnknown: true })\n\n // Add relevant state errors\n if (error) {\n const errorsState = error.details.map((item) => getError(EN_GB, item))\n return { ...context, errors: errors.concat(errorsState) }\n }\n\n return context\n}\n\nfunction getReferenceNumber(state: FormSubmissionState): string {\n if (\n !state.$$__referenceNumber ||\n typeof state.$$__referenceNumber !== 'string'\n ) {\n throw Error('Reference number not found in form state')\n }\n\n return state.$$__referenceNumber\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,12 +1,39 @@
1
- import { type ConditionWrapper, type Section } from '@defra/forms-model';
1
+ import { type ConditionEvaluationOutcome, type ConditionWrapper, type Section } from '@defra/forms-model';
2
2
  import { type Expression } from 'expr-eval-fork';
3
3
  import { type Field } from '../../../../server/plugins/engine/components/helpers/components.js';
4
4
  import { type RepeatPageController } from '../../../../server/plugins/engine/pageControllers/RepeatPageController.js';
5
5
  import { type PageControllerClass } from '../../../../server/plugins/engine/pageControllers/helpers/pages.js';
6
6
  import { type FormState, type FormSubmissionError } from '../../../../server/plugins/engine/types.js';
7
+ /**
8
+ * The result of evaluating a condition, keeping a failed evaluation distinct
9
+ * from one that legitimately returned `false`.
10
+ * @see {@link ExecutableCondition.evaluate}
11
+ */
12
+ export interface ConditionEvaluation {
13
+ outcome: ConditionEvaluationOutcome;
14
+ error?: string;
15
+ }
16
+ /**
17
+ * A form condition paired with the parsed expression and callbacks needed to
18
+ * run it against a form submission state
19
+ * Created by `FormModel.makeCondition`
20
+ */
7
21
  export type ExecutableCondition = ConditionWrapper & {
22
+ /**
23
+ * Parsed expression for the condition's {@link ConditionWrapper.value},
24
+ * evaluated against a context built from the submission state
25
+ */
8
26
  expr: Expression;
27
+ /**
28
+ * Evaluates the condition, used for page routing and component visibility
29
+ * A failed evaluation is reported as `false`
30
+ */
9
31
  fn: (evaluationState: FormState) => boolean;
32
+ /**
33
+ * As `fn`, but reports whether evaluation failed rather than defaulting a
34
+ * failure to `false`. Used to record condition outcomes on submission.
35
+ */
36
+ evaluate: (evaluationState: FormState) => ConditionEvaluation;
10
37
  };
11
38
  /**
12
39
  * Used to render a row on a Summary List (check your answers)
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../../../src/server/plugins/engine/models/types.ts"],"sourcesContent":["import {\n type ConditionWrapper,\n type FormComponentsDef,\n type Section\n} from '@defra/forms-model'\nimport { type Expression } from 'expr-eval-fork'\n\nimport {\n getAnswer,\n type Field\n} from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FormState,\n type FormSubmissionError\n} from '~/src/server/plugins/engine/types.js'\n\nexport type ExecutableCondition = ConditionWrapper & {\n expr: Expression\n fn: (evaluationState: FormState) => boolean\n}\n\n/**\n * Used to render a row on a Summary List (check your answers)\n */\nexport interface DetailItemBase {\n /**\n * Name of the component defined in the JSON\n * @see {@link FormComponentsDef.name}\n */\n name: string\n\n /**\n * Field label, used for change link visually hidden text\n * @see {@link FormComponentsDef.title}\n */\n label: string\n\n /**\n * Field change link\n */\n href: string\n\n /**\n * Form submission state (or repeat state for sub items)\n */\n state: FormState\n\n /**\n * Field submission state error, used to flag unanswered questions\n * Shown as 'Complete all unanswered questions before submitting the form'\n */\n error?: FormSubmissionError\n}\n\nexport interface DetailItemField extends DetailItemBase {\n /**\n * Field page controller instance\n */\n page: Exclude<PageControllerClass, RepeatPageController>\n\n /**\n * Check answers summary list key\n * For example, 'Date of birth'\n */\n title: string\n\n /**\n * Check answers summary list value, formatted by {@link getAnswer}\n * For example, date fields formatted as '25 December 2022'\n */\n value: string\n\n /**\n * Field component instance\n */\n field: Field\n}\n\nexport interface DetailItemRepeat extends DetailItemBase {\n /**\n * Repeat page controller instance\n */\n page: RepeatPageController\n\n /**\n * Check answers summary list key\n * For example, 'Pizza' or 'Pizza added'\n */\n title: string\n\n /**\n * Check answers summary list value\n * For example, 'You added 2 Pizzas'\n */\n value: string\n\n /**\n * Repeater field detail items\n */\n subItems: DetailItemField[][]\n}\n\nexport type DetailItem = DetailItemField | DetailItemRepeat\n\n/**\n * Used to render a row on a Summary List (check your answers)\n */\nexport interface Detail {\n name?: Section['name']\n title?: Section['title']\n items: DetailItem[]\n}\n"],"mappings":"","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../../../src/server/plugins/engine/models/types.ts"],"sourcesContent":["import {\n type ConditionEvaluationOutcome,\n type ConditionWrapper,\n type FormComponentsDef,\n type Section\n} from '@defra/forms-model'\nimport { type Expression } from 'expr-eval-fork'\n\nimport {\n getAnswer,\n type Field\n} from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport {\n type FormState,\n type FormSubmissionError\n} from '~/src/server/plugins/engine/types.js'\n\n/**\n * The result of evaluating a condition, keeping a failed evaluation distinct\n * from one that legitimately returned `false`.\n * @see {@link ExecutableCondition.evaluate}\n */\nexport interface ConditionEvaluation {\n outcome: ConditionEvaluationOutcome\n error?: string\n}\n\n/**\n * A form condition paired with the parsed expression and callbacks needed to\n * run it against a form submission state\n * Created by `FormModel.makeCondition`\n */\nexport type ExecutableCondition = ConditionWrapper & {\n /**\n * Parsed expression for the condition's {@link ConditionWrapper.value},\n * evaluated against a context built from the submission state\n */\n expr: Expression\n\n /**\n * Evaluates the condition, used for page routing and component visibility\n * A failed evaluation is reported as `false`\n */\n fn: (evaluationState: FormState) => boolean\n\n /**\n * As `fn`, but reports whether evaluation failed rather than defaulting a\n * failure to `false`. Used to record condition outcomes on submission.\n */\n evaluate: (evaluationState: FormState) => ConditionEvaluation\n}\n\n/**\n * Used to render a row on a Summary List (check your answers)\n */\nexport interface DetailItemBase {\n /**\n * Name of the component defined in the JSON\n * @see {@link FormComponentsDef.name}\n */\n name: string\n\n /**\n * Field label, used for change link visually hidden text\n * @see {@link FormComponentsDef.title}\n */\n label: string\n\n /**\n * Field change link\n */\n href: string\n\n /**\n * Form submission state (or repeat state for sub items)\n */\n state: FormState\n\n /**\n * Field submission state error, used to flag unanswered questions\n * Shown as 'Complete all unanswered questions before submitting the form'\n */\n error?: FormSubmissionError\n}\n\nexport interface DetailItemField extends DetailItemBase {\n /**\n * Field page controller instance\n */\n page: Exclude<PageControllerClass, RepeatPageController>\n\n /**\n * Check answers summary list key\n * For example, 'Date of birth'\n */\n title: string\n\n /**\n * Check answers summary list value, formatted by {@link getAnswer}\n * For example, date fields formatted as '25 December 2022'\n */\n value: string\n\n /**\n * Field component instance\n */\n field: Field\n}\n\nexport interface DetailItemRepeat extends DetailItemBase {\n /**\n * Repeat page controller instance\n */\n page: RepeatPageController\n\n /**\n * Check answers summary list key\n * For example, 'Pizza' or 'Pizza added'\n */\n title: string\n\n /**\n * Check answers summary list value\n * For example, 'You added 2 Pizzas'\n */\n value: string\n\n /**\n * Repeater field detail items\n */\n subItems: DetailItemField[][]\n}\n\nexport type DetailItem = DetailItemField | DetailItemRepeat\n\n/**\n * Used to render a row on a Summary List (check your answers)\n */\nexport interface Detail {\n name?: Section['name']\n title?: Section['title']\n items: DetailItem[]\n}\n"],"mappings":"","ignoreList":[]}
@@ -1,6 +1,8 @@
1
+ import { Engine } from '@defra/forms-model';
1
2
  import { EN_GB } from "../../../../constants.js";
2
3
  import { getFormVersion } from "../../helpers.js";
3
4
  import { categoriseData } from "../machine/v2.js";
5
+ import { buildConditionEvaluations } from "../../pageControllers/helpers/submission.js";
4
6
  import { FormAdapterSubmissionSchemaVersion } from "../../types/enums.js";
5
7
  export function format(context, items, model, submitResponse, formStatus, formMetadata) {
6
8
  const csvFiles = extractCsvFiles(submitResponse);
@@ -40,7 +42,15 @@ export function format(context, items, model, submitResponse, formStatus, formMe
40
42
  const payload = {
41
43
  meta,
42
44
  data,
43
- result
45
+ result,
46
+ // Recorded here because only the engine holds the walked evaluation state
47
+ // the conditions were judged against; forms-notify-listener receives the
48
+ // flat submitted answers and resolves the outputs that qualify from these
49
+ // outcomes alone. Condition ids are only stable in V2, so a V1 definition
50
+ // has nothing to report - the property is still emitted, because its
51
+ // absence is what marks a message as predating this and sends the listener
52
+ // down its legacy path.
53
+ conditionEvaluations: model.engine === Engine.V2 ? buildConditionEvaluations(model, context) : []
44
54
  };
45
55
  return JSON.stringify(payload);
46
56
  }
@@ -1 +1 @@
1
- {"version":3,"file":"v1.js","names":["EN_GB","getFormVersion","categoriseData","FormAdapterSubmissionSchemaVersion","format","context","items","model","submitResponse","formStatus","formMetadata","csvFiles","extractCsvFiles","main","v2Main","v2Data","versionMetadata","def","meta","schemaVersion","V1","timestamp","Date","referenceNumber","formName","name","formId","id","formSlug","slug","status","state","isPreview","notificationEmail","language","translator","Object","fromEntries","entries","map","key","value","undefined","data","result","files","payload","JSON","stringify","repeaters"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/adapter/v1.ts"],"sourcesContent":["import {\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { EN_GB } from '~/src/server/constants.js'\nimport {\n getFormVersion,\n type checkFormStatus\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { categoriseData } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'\nimport { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js'\nimport {\n type FormAdapterSubmissionMessageData,\n type FormAdapterSubmissionMessageMeta,\n type FormAdapterSubmissionMessagePayload,\n type FormAdapterSubmissionMessageResult,\n type FormContext\n} from '~/src/server/plugins/engine/types.js'\n\nexport function format(\n context: FormContext,\n items: DetailItem[],\n model: FormModel,\n submitResponse: SubmitResponsePayload,\n formStatus: ReturnType<typeof checkFormStatus>,\n formMetadata?: FormMetadata\n): string {\n const csvFiles = extractCsvFiles(submitResponse)\n\n const { main: v2Main, ...v2Data } = categoriseData(items)\n\n const versionMetadata = getFormVersion(model.def)\n\n const meta: FormAdapterSubmissionMessageMeta = {\n schemaVersion: FormAdapterSubmissionSchemaVersion.V1,\n timestamp: new Date(),\n referenceNumber: context.referenceNumber,\n formName: model.name,\n formId: formMetadata?.id ?? '',\n formSlug: formMetadata?.slug ?? '',\n status: formStatus.state,\n isPreview: formStatus.isPreview,\n notificationEmail: formMetadata?.notificationEmail ?? '',\n language: context.translator?.language ?? EN_GB\n }\n\n if (versionMetadata) {\n meta.versionMetadata = versionMetadata\n }\n\n const main = Object.fromEntries(\n Object.entries(v2Main).map(([key, value]) => {\n if (value === undefined) {\n return [key, null]\n }\n\n return [key, value]\n })\n )\n\n const data: FormAdapterSubmissionMessageData = {\n main,\n ...v2Data\n }\n\n const result: FormAdapterSubmissionMessageResult = {\n files: csvFiles\n }\n\n const payload: FormAdapterSubmissionMessagePayload = {\n meta,\n data,\n result\n }\n\n return JSON.stringify(payload)\n}\n\nfunction extractCsvFiles(\n submitResponse: SubmitResponsePayload\n): FormAdapterSubmissionMessageResult['files'] {\n const result =\n submitResponse.result as Partial<FormAdapterSubmissionMessageResult>\n\n return {\n main: result.files?.main ?? '',\n repeaters: result.files?.repeaters ?? {}\n }\n}\n"],"mappings":"AAKA,SAASA,KAAK;AACd,SACEC,cAAc;AAKhB,SAASC,cAAc;AACvB,SAASC,kCAAkC;AAS3C,OAAO,SAASC,MAAMA,CACpBC,OAAoB,EACpBC,KAAmB,EACnBC,KAAgB,EAChBC,cAAqC,EACrCC,UAA8C,EAC9CC,YAA2B,EACnB;EACR,MAAMC,QAAQ,GAAGC,eAAe,CAACJ,cAAc,CAAC;EAEhD,MAAM;IAAEK,IAAI,EAAEC,MAAM;IAAE,GAAGC;EAAO,CAAC,GAAGb,cAAc,CAACI,KAAK,CAAC;EAEzD,MAAMU,eAAe,GAAGf,cAAc,CAACM,KAAK,CAACU,GAAG,CAAC;EAEjD,MAAMC,IAAsC,GAAG;IAC7CC,aAAa,EAAEhB,kCAAkC,CAACiB,EAAE;IACpDC,SAAS,EAAE,IAAIC,IAAI,CAAC,CAAC;IACrBC,eAAe,EAAElB,OAAO,CAACkB,eAAe;IACxCC,QAAQ,EAAEjB,KAAK,CAACkB,IAAI;IACpBC,MAAM,EAAEhB,YAAY,EAAEiB,EAAE,IAAI,EAAE;IAC9BC,QAAQ,EAAElB,YAAY,EAAEmB,IAAI,IAAI,EAAE;IAClCC,MAAM,EAAErB,UAAU,CAACsB,KAAK;IACxBC,SAAS,EAAEvB,UAAU,CAACuB,SAAS;IAC/BC,iBAAiB,EAAEvB,YAAY,EAAEuB,iBAAiB,IAAI,EAAE;IACxDC,QAAQ,EAAE7B,OAAO,CAAC8B,UAAU,EAAED,QAAQ,IAAIlC;EAC5C,CAAC;EAED,IAAIgB,eAAe,EAAE;IACnBE,IAAI,CAACF,eAAe,GAAGA,eAAe;EACxC;EAEA,MAAMH,IAAI,GAAGuB,MAAM,CAACC,WAAW,CAC7BD,MAAM,CAACE,OAAO,CAACxB,MAAM,CAAC,CAACyB,GAAG,CAAC,CAAC,CAACC,GAAG,EAAEC,KAAK,CAAC,KAAK;IAC3C,IAAIA,KAAK,KAAKC,SAAS,EAAE;MACvB,OAAO,CAACF,GAAG,EAAE,IAAI,CAAC;IACpB;IAEA,OAAO,CAACA,GAAG,EAAEC,KAAK,CAAC;EACrB,CAAC,CACH,CAAC;EAED,MAAME,IAAsC,GAAG;IAC7C9B,IAAI;IACJ,GAAGE;EACL,CAAC;EAED,MAAM6B,MAA0C,GAAG;IACjDC,KAAK,EAAElC;EACT,CAAC;EAED,MAAMmC,OAA4C,GAAG;IACnD5B,IAAI;IACJyB,IAAI;IACJC;EACF,CAAC;EAED,OAAOG,IAAI,CAACC,SAAS,CAACF,OAAO,CAAC;AAChC;AAEA,SAASlC,eAAeA,CACtBJ,cAAqC,EACQ;EAC7C,MAAMoC,MAAM,GACVpC,cAAc,CAACoC,MAAqD;EAEtE,OAAO;IACL/B,IAAI,EAAE+B,MAAM,CAACC,KAAK,EAAEhC,IAAI,IAAI,EAAE;IAC9BoC,SAAS,EAAEL,MAAM,CAACC,KAAK,EAAEI,SAAS,IAAI,CAAC;EACzC,CAAC;AACH","ignoreList":[]}
1
+ {"version":3,"file":"v1.js","names":["Engine","EN_GB","getFormVersion","categoriseData","buildConditionEvaluations","FormAdapterSubmissionSchemaVersion","format","context","items","model","submitResponse","formStatus","formMetadata","csvFiles","extractCsvFiles","main","v2Main","v2Data","versionMetadata","def","meta","schemaVersion","V1","timestamp","Date","referenceNumber","formName","name","formId","id","formSlug","slug","status","state","isPreview","notificationEmail","language","translator","Object","fromEntries","entries","map","key","value","undefined","data","result","files","payload","conditionEvaluations","engine","V2","JSON","stringify","repeaters"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/adapter/v1.ts"],"sourcesContent":["import {\n Engine,\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { EN_GB } from '~/src/server/constants.js'\nimport {\n getFormVersion,\n type checkFormStatus\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { categoriseData } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'\nimport { buildConditionEvaluations } from '~/src/server/plugins/engine/pageControllers/helpers/submission.js'\nimport { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js'\nimport {\n type FormAdapterSubmissionMessageData,\n type FormAdapterSubmissionMessageMeta,\n type FormAdapterSubmissionMessagePayload,\n type FormAdapterSubmissionMessageResult,\n type FormContext\n} from '~/src/server/plugins/engine/types.js'\n\nexport function format(\n context: FormContext,\n items: DetailItem[],\n model: FormModel,\n submitResponse: SubmitResponsePayload,\n formStatus: ReturnType<typeof checkFormStatus>,\n formMetadata?: FormMetadata\n): string {\n const csvFiles = extractCsvFiles(submitResponse)\n\n const { main: v2Main, ...v2Data } = categoriseData(items)\n\n const versionMetadata = getFormVersion(model.def)\n\n const meta: FormAdapterSubmissionMessageMeta = {\n schemaVersion: FormAdapterSubmissionSchemaVersion.V1,\n timestamp: new Date(),\n referenceNumber: context.referenceNumber,\n formName: model.name,\n formId: formMetadata?.id ?? '',\n formSlug: formMetadata?.slug ?? '',\n status: formStatus.state,\n isPreview: formStatus.isPreview,\n notificationEmail: formMetadata?.notificationEmail ?? '',\n language: context.translator?.language ?? EN_GB\n }\n\n if (versionMetadata) {\n meta.versionMetadata = versionMetadata\n }\n\n const main = Object.fromEntries(\n Object.entries(v2Main).map(([key, value]) => {\n if (value === undefined) {\n return [key, null]\n }\n\n return [key, value]\n })\n )\n\n const data: FormAdapterSubmissionMessageData = {\n main,\n ...v2Data\n }\n\n const result: FormAdapterSubmissionMessageResult = {\n files: csvFiles\n }\n\n const payload: FormAdapterSubmissionMessagePayload = {\n meta,\n data,\n result,\n\n // Recorded here because only the engine holds the walked evaluation state\n // the conditions were judged against; forms-notify-listener receives the\n // flat submitted answers and resolves the outputs that qualify from these\n // outcomes alone. Condition ids are only stable in V2, so a V1 definition\n // has nothing to report - the property is still emitted, because its\n // absence is what marks a message as predating this and sends the listener\n // down its legacy path.\n conditionEvaluations:\n model.engine === Engine.V2\n ? buildConditionEvaluations(model, context)\n : []\n }\n\n return JSON.stringify(payload)\n}\n\nfunction extractCsvFiles(\n submitResponse: SubmitResponsePayload\n): FormAdapterSubmissionMessageResult['files'] {\n const result =\n submitResponse.result as Partial<FormAdapterSubmissionMessageResult>\n\n return {\n main: result.files?.main ?? '',\n repeaters: result.files?.repeaters ?? {}\n }\n}\n"],"mappings":"AAAA,SACEA,MAAM,QAGD,oBAAoB;AAE3B,SAASC,KAAK;AACd,SACEC,cAAc;AAKhB,SAASC,cAAc;AACvB,SAASC,yBAAyB;AAClC,SAASC,kCAAkC;AAS3C,OAAO,SAASC,MAAMA,CACpBC,OAAoB,EACpBC,KAAmB,EACnBC,KAAgB,EAChBC,cAAqC,EACrCC,UAA8C,EAC9CC,YAA2B,EACnB;EACR,MAAMC,QAAQ,GAAGC,eAAe,CAACJ,cAAc,CAAC;EAEhD,MAAM;IAAEK,IAAI,EAAEC,MAAM;IAAE,GAAGC;EAAO,CAAC,GAAGd,cAAc,CAACK,KAAK,CAAC;EAEzD,MAAMU,eAAe,GAAGhB,cAAc,CAACO,KAAK,CAACU,GAAG,CAAC;EAEjD,MAAMC,IAAsC,GAAG;IAC7CC,aAAa,EAAEhB,kCAAkC,CAACiB,EAAE;IACpDC,SAAS,EAAE,IAAIC,IAAI,CAAC,CAAC;IACrBC,eAAe,EAAElB,OAAO,CAACkB,eAAe;IACxCC,QAAQ,EAAEjB,KAAK,CAACkB,IAAI;IACpBC,MAAM,EAAEhB,YAAY,EAAEiB,EAAE,IAAI,EAAE;IAC9BC,QAAQ,EAAElB,YAAY,EAAEmB,IAAI,IAAI,EAAE;IAClCC,MAAM,EAAErB,UAAU,CAACsB,KAAK;IACxBC,SAAS,EAAEvB,UAAU,CAACuB,SAAS;IAC/BC,iBAAiB,EAAEvB,YAAY,EAAEuB,iBAAiB,IAAI,EAAE;IACxDC,QAAQ,EAAE7B,OAAO,CAAC8B,UAAU,EAAED,QAAQ,IAAInC;EAC5C,CAAC;EAED,IAAIiB,eAAe,EAAE;IACnBE,IAAI,CAACF,eAAe,GAAGA,eAAe;EACxC;EAEA,MAAMH,IAAI,GAAGuB,MAAM,CAACC,WAAW,CAC7BD,MAAM,CAACE,OAAO,CAACxB,MAAM,CAAC,CAACyB,GAAG,CAAC,CAAC,CAACC,GAAG,EAAEC,KAAK,CAAC,KAAK;IAC3C,IAAIA,KAAK,KAAKC,SAAS,EAAE;MACvB,OAAO,CAACF,GAAG,EAAE,IAAI,CAAC;IACpB;IAEA,OAAO,CAACA,GAAG,EAAEC,KAAK,CAAC;EACrB,CAAC,CACH,CAAC;EAED,MAAME,IAAsC,GAAG;IAC7C9B,IAAI;IACJ,GAAGE;EACL,CAAC;EAED,MAAM6B,MAA0C,GAAG;IACjDC,KAAK,EAAElC;EACT,CAAC;EAED,MAAMmC,OAA4C,GAAG;IACnD5B,IAAI;IACJyB,IAAI;IACJC,MAAM;IAEN;IACA;IACA;IACA;IACA;IACA;IACA;IACAG,oBAAoB,EAClBxC,KAAK,CAACyC,MAAM,KAAKlD,MAAM,CAACmD,EAAE,GACtB/C,yBAAyB,CAACK,KAAK,EAAEF,OAAO,CAAC,GACzC;EACR,CAAC;EAED,OAAO6C,IAAI,CAACC,SAAS,CAACL,OAAO,CAAC;AAChC;AAEA,SAASlC,eAAeA,CACtBJ,cAAqC,EACQ;EAC7C,MAAMoC,MAAM,GACVpC,cAAc,CAACoC,MAAqD;EAEtE,OAAO;IACL/B,IAAI,EAAE+B,MAAM,CAACC,KAAK,EAAEhC,IAAI,IAAI,EAAE;IAC9BuC,SAAS,EAAER,MAAM,CAACC,KAAK,EAAEO,SAAS,IAAI,CAAC;EACzC,CAAC;AACH","ignoreList":[]}
@@ -1,6 +1,8 @@
1
- import { type SubmitPayload } from '@defra/forms-model';
1
+ import { type SubmitConditionEvaluation, type SubmitPayload } from '@defra/forms-model';
2
2
  import { type Translator } from '../../../../../server/plugins/engine/i18n/types.js';
3
+ import { type FormModel } from '../../../../../server/plugins/engine/models/FormModel.js';
3
4
  import { type DetailItem, type DetailItemField } from '../../../../../server/plugins/engine/models/types.js';
5
+ import { type FormContext, type FormStateValue } from '../../../../../server/plugins/engine/types.js';
4
6
  export interface SubmitRecord {
5
7
  name: string;
6
8
  title: string;
@@ -26,3 +28,34 @@ export declare function buildPaymentRecords(item: DetailItemField, translator: T
26
28
  * Builds the repeater submission records from repeater items.
27
29
  */
28
30
  export declare function buildRepeaterRecords(items: DetailItem[], translator: Translator): SubmitPayload['repeaters'];
31
+ /**
32
+ * Records the outcome of every condition in the form definition, evaluated
33
+ * against the answers as they stand at the point of submission.
34
+ *
35
+ * Evaluated here rather than by the consumer because only the engine holds
36
+ * the context to evaluate against: `evaluationState` is built by walking the
37
+ * form from its start page along the path taken, applying page conditions as
38
+ * it goes. A consumer receives the flat submitted answers, not that walked
39
+ * state, and reproducing the walk in a second codebase would leave two
40
+ * implementations of the same logic free to diverge.
41
+ *
42
+ * Each record carries the components the condition depends on and whether each
43
+ * was answered. An unanswered question still yields a boolean - negative
44
+ * operators such as "is not" return `true` against the seeded `null` - so the
45
+ * outcome alone cannot be read as evidence that the user gave that answer.
46
+ *
47
+ * V2 definitions only. `conditionId` is the V2 condition id, and the
48
+ * references are resolved from the component ids V2 conditions carry - V1
49
+ * conditions reference components by name, and V1 components need not have
50
+ * an id at all.
51
+ */
52
+ export declare function buildConditionEvaluations(model: FormModel, context: FormContext): SubmitConditionEvaluation[];
53
+ /**
54
+ * Whether a component held an answer at the point a condition was evaluated.
55
+ *
56
+ * The engine seeds every component in `evaluationState` with `null` before the
57
+ * page walk begins, so an unanswered question is present but empty rather than
58
+ * absent.
59
+ * @see {@link FormModel.initialiseContext}
60
+ */
61
+ export declare function isAnswered(value: FormStateValue | undefined): boolean;
@@ -1,3 +1,4 @@
1
+ import { isConditionWrapperV2 } from '@defra/forms-model';
1
2
  import { GeospatialField } from "../../components/GeospatialField.js";
2
3
  import { PaymentField } from "../../components/PaymentField.js";
3
4
  import { getAnswer } from "../../components/helpers/components.js";
@@ -95,4 +96,100 @@ export function buildRepeaterRecords(items, translator) {
95
96
  }))
96
97
  }));
97
98
  }
99
+
100
+ /**
101
+ * Records the outcome of every condition in the form definition, evaluated
102
+ * against the answers as they stand at the point of submission.
103
+ *
104
+ * Evaluated here rather than by the consumer because only the engine holds
105
+ * the context to evaluate against: `evaluationState` is built by walking the
106
+ * form from its start page along the path taken, applying page conditions as
107
+ * it goes. A consumer receives the flat submitted answers, not that walked
108
+ * state, and reproducing the walk in a second codebase would leave two
109
+ * implementations of the same logic free to diverge.
110
+ *
111
+ * Each record carries the components the condition depends on and whether each
112
+ * was answered. An unanswered question still yields a boolean - negative
113
+ * operators such as "is not" return `true` against the seeded `null` - so the
114
+ * outcome alone cannot be read as evidence that the user gave that answer.
115
+ *
116
+ * V2 definitions only. `conditionId` is the V2 condition id, and the
117
+ * references are resolved from the component ids V2 conditions carry - V1
118
+ * conditions reference components by name, and V1 components need not have
119
+ * an id at all.
120
+ */
121
+ export function buildConditionEvaluations(model, context) {
122
+ const {
123
+ evaluationState
124
+ } = context;
125
+ return model.def.conditions.filter(isConditionWrapperV2).flatMap(conditionDef => {
126
+ const condition = model.conditions[conditionDef.id];
127
+ if (!condition) {
128
+ return [];
129
+ }
130
+ const {
131
+ outcome
132
+ } = condition.evaluate(evaluationState);
133
+ const references = collectReferences(model, conditionDef, evaluationState);
134
+ return {
135
+ conditionId: conditionDef.id,
136
+ outcome,
137
+ references: [...references.values()]
138
+ };
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Whether a component held an answer at the point a condition was evaluated.
144
+ *
145
+ * The engine seeds every component in `evaluationState` with `null` before the
146
+ * page walk begins, so an unanswered question is present but empty rather than
147
+ * absent.
148
+ * @see {@link FormModel.initialiseContext}
149
+ */
150
+ export function isAnswered(value) {
151
+ if (value === undefined || value === null) {
152
+ return false;
153
+ }
154
+ if (Array.isArray(value)) {
155
+ return value.length > 0;
156
+ }
157
+ return value !== '';
158
+ }
159
+ function isConditionDataV2(item) {
160
+ return 'componentId' in item;
161
+ }
162
+
163
+ /**
164
+ * Recursive function that collects every component a condition depends on, following nested condition
165
+ * references. Results are keyed by component id so a component referenced more
166
+ * than once is reported once.
167
+ */
168
+ function collectReferences(model, conditionDef, evaluationState, references = new Map(), visited = new Set()) {
169
+ if (visited.has(conditionDef.id)) {
170
+ return references;
171
+ }
172
+ visited.add(conditionDef.id);
173
+ for (const item of conditionDef.items) {
174
+ if (isConditionDataV2(item)) {
175
+ const component = model.getComponentById(item.componentId);
176
+
177
+ // A condition referencing a component that no longer exists cannot be
178
+ // resolved to a name, so there is nothing meaningful to report for it
179
+ if (component) {
180
+ references.set(item.componentId, {
181
+ componentId: item.componentId,
182
+ componentName: component.name,
183
+ answered: isAnswered(evaluationState[component.name])
184
+ });
185
+ }
186
+ continue;
187
+ }
188
+ const referenced = model.getConditionById(item.conditionId);
189
+ if (referenced) {
190
+ collectReferences(model, referenced, evaluationState, references, visited);
191
+ }
192
+ }
193
+ return references;
194
+ }
98
195
  //# sourceMappingURL=submission.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"submission.js","names":["GeospatialField","PaymentField","getAnswer","formatCurrency","formatPaymentDate","buildMainRecords","items","translator","fieldItems","filter","item","records","field","push","buildPaymentRecords","value","getFormValueFromState","state","name","title","label","undefined","JSON","stringify","format","paymentState","getPaymentStateFromState","description","amount","reference","preAuth","createdAt","language","buildRepeaterRecords","map","subItems","detailItems","subItem","formValue"],"sources":["../../../../../../src/server/plugins/engine/pageControllers/helpers/submission.ts"],"sourcesContent":["import { type SubmitPayload } from '@defra/forms-model'\n\nimport { GeospatialField } from '~/src/server/plugins/engine/components/GeospatialField.js'\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type Translator } from '~/src/server/plugins/engine/i18n/types.js'\nimport {\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport {\n formatCurrency,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\n\nexport interface SubmitRecord {\n name: string\n title: string\n value: string\n}\n\n/**\n * Builds the main submission records from field items.\n * Regular fields are converted to single records, while PaymentField\n * components are expanded into four separate records.\n */\nexport function buildMainRecords(\n items: DetailItem[],\n translator: Translator\n): SubmitRecord[] {\n const fieldItems = items.filter(\n (item): item is DetailItemField => 'field' in item\n )\n\n const records: SubmitRecord[] = []\n\n for (const item of fieldItems) {\n if (item.field instanceof PaymentField) {\n records.push(...buildPaymentRecords(item, translator))\n } else if (item.field instanceof GeospatialField) {\n // Stringify of GeoJSON is done here rather than inside `getContextValueFromState`\n // so we don't incur the overhead of JSON.stringify on every request when building context\n const value = item.field.getFormValueFromState(item.state)\n records.push({\n name: item.name,\n title: item.label,\n value: value === undefined ? '' : JSON.stringify(value)\n })\n } else {\n records.push({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, translator, { format: 'data' })\n })\n }\n }\n\n return records\n}\n\n/**\n * Expands a PaymentField into four submission records:\n * - Payment description\n * - Payment amount (formatted with currency symbol)\n * - Payment reference\n * - Payment date (formatted date/time)\n *\n * Returns an empty array if no payment state exists.\n */\nexport function buildPaymentRecords(\n item: DetailItemField,\n translator: Translator\n): SubmitRecord[] {\n const paymentState = (item.field as PaymentField).getPaymentStateFromState(\n item.state\n )\n\n if (!paymentState) {\n return []\n }\n\n return [\n {\n name: `${item.name}_paymentDescription`,\n title: 'Payment description',\n value: paymentState.description\n },\n {\n name: `${item.name}_paymentAmount`,\n title: 'Payment amount',\n value: formatCurrency(paymentState.amount)\n },\n {\n name: `${item.name}_paymentReference`,\n title: 'Payment reference',\n value: paymentState.reference\n },\n {\n name: `${item.name}_paymentDate`,\n title: 'Payment date',\n value: paymentState.preAuth?.createdAt\n ? formatPaymentDate(paymentState.preAuth.createdAt, translator.language)\n : ''\n }\n ]\n}\n\n/**\n * Builds the repeater submission records from repeater items.\n */\nexport function buildRepeaterRecords(\n items: DetailItem[],\n translator: Translator\n): SubmitPayload['repeaters'] {\n return items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: item.subItems.map((detailItems) =>\n detailItems.map((subItem) => {\n let value\n\n if (subItem.field instanceof GeospatialField) {\n // Stringify of GeoJSON is done here rather than inside `getContextValueFromState`\n // so we don't incur the overhead of JSON.stringify on every request when building context\n const formValue = subItem.field.getFormValueFromState(subItem.state)\n value = formValue === undefined ? '' : JSON.stringify(formValue)\n } else {\n value = getAnswer(subItem.field, subItem.state, translator, {\n format: 'data'\n })\n }\n\n return {\n name: subItem.name,\n title: subItem.label,\n value\n }\n })\n )\n }))\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"submission.js","names":["isConditionWrapperV2","GeospatialField","PaymentField","getAnswer","formatCurrency","formatPaymentDate","buildMainRecords","items","translator","fieldItems","filter","item","records","field","push","buildPaymentRecords","value","getFormValueFromState","state","name","title","label","undefined","JSON","stringify","format","paymentState","getPaymentStateFromState","description","amount","reference","preAuth","createdAt","language","buildRepeaterRecords","map","subItems","detailItems","subItem","formValue","buildConditionEvaluations","model","context","evaluationState","def","conditions","flatMap","conditionDef","condition","id","outcome","evaluate","references","collectReferences","conditionId","values","isAnswered","Array","isArray","length","isConditionDataV2","Map","visited","Set","has","add","component","getComponentById","componentId","set","componentName","answered","referenced","getConditionById"],"sources":["../../../../../../src/server/plugins/engine/pageControllers/helpers/submission.ts"],"sourcesContent":["import {\n isConditionWrapperV2,\n type ConditionDataV2,\n type ConditionRefDataV2,\n type ConditionWrapperV2,\n type SubmitConditionEvaluation,\n type SubmitConditionReference,\n type SubmitPayload\n} from '@defra/forms-model'\n\nimport { GeospatialField } from '~/src/server/plugins/engine/components/GeospatialField.js'\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type Translator } from '~/src/server/plugins/engine/i18n/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport {\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport {\n type FormContext,\n type FormState,\n type FormStateValue\n} from '~/src/server/plugins/engine/types.js'\nimport {\n formatCurrency,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\n\nexport interface SubmitRecord {\n name: string\n title: string\n value: string\n}\n\n/**\n * Builds the main submission records from field items.\n * Regular fields are converted to single records, while PaymentField\n * components are expanded into four separate records.\n */\nexport function buildMainRecords(\n items: DetailItem[],\n translator: Translator\n): SubmitRecord[] {\n const fieldItems = items.filter(\n (item): item is DetailItemField => 'field' in item\n )\n\n const records: SubmitRecord[] = []\n\n for (const item of fieldItems) {\n if (item.field instanceof PaymentField) {\n records.push(...buildPaymentRecords(item, translator))\n } else if (item.field instanceof GeospatialField) {\n // Stringify of GeoJSON is done here rather than inside `getContextValueFromState`\n // so we don't incur the overhead of JSON.stringify on every request when building context\n const value = item.field.getFormValueFromState(item.state)\n records.push({\n name: item.name,\n title: item.label,\n value: value === undefined ? '' : JSON.stringify(value)\n })\n } else {\n records.push({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, translator, { format: 'data' })\n })\n }\n }\n\n return records\n}\n\n/**\n * Expands a PaymentField into four submission records:\n * - Payment description\n * - Payment amount (formatted with currency symbol)\n * - Payment reference\n * - Payment date (formatted date/time)\n *\n * Returns an empty array if no payment state exists.\n */\nexport function buildPaymentRecords(\n item: DetailItemField,\n translator: Translator\n): SubmitRecord[] {\n const paymentState = (item.field as PaymentField).getPaymentStateFromState(\n item.state\n )\n\n if (!paymentState) {\n return []\n }\n\n return [\n {\n name: `${item.name}_paymentDescription`,\n title: 'Payment description',\n value: paymentState.description\n },\n {\n name: `${item.name}_paymentAmount`,\n title: 'Payment amount',\n value: formatCurrency(paymentState.amount)\n },\n {\n name: `${item.name}_paymentReference`,\n title: 'Payment reference',\n value: paymentState.reference\n },\n {\n name: `${item.name}_paymentDate`,\n title: 'Payment date',\n value: paymentState.preAuth?.createdAt\n ? formatPaymentDate(paymentState.preAuth.createdAt, translator.language)\n : ''\n }\n ]\n}\n\n/**\n * Builds the repeater submission records from repeater items.\n */\nexport function buildRepeaterRecords(\n items: DetailItem[],\n translator: Translator\n): SubmitPayload['repeaters'] {\n return items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: item.subItems.map((detailItems) =>\n detailItems.map((subItem) => {\n let value\n\n if (subItem.field instanceof GeospatialField) {\n // Stringify of GeoJSON is done here rather than inside `getContextValueFromState`\n // so we don't incur the overhead of JSON.stringify on every request when building context\n const formValue = subItem.field.getFormValueFromState(subItem.state)\n value = formValue === undefined ? '' : JSON.stringify(formValue)\n } else {\n value = getAnswer(subItem.field, subItem.state, translator, {\n format: 'data'\n })\n }\n\n return {\n name: subItem.name,\n title: subItem.label,\n value\n }\n })\n )\n }))\n}\n\n/**\n * Records the outcome of every condition in the form definition, evaluated\n * against the answers as they stand at the point of submission.\n *\n * Evaluated here rather than by the consumer because only the engine holds\n * the context to evaluate against: `evaluationState` is built by walking the\n * form from its start page along the path taken, applying page conditions as\n * it goes. A consumer receives the flat submitted answers, not that walked\n * state, and reproducing the walk in a second codebase would leave two\n * implementations of the same logic free to diverge.\n *\n * Each record carries the components the condition depends on and whether each\n * was answered. An unanswered question still yields a boolean - negative\n * operators such as \"is not\" return `true` against the seeded `null` - so the\n * outcome alone cannot be read as evidence that the user gave that answer.\n *\n * V2 definitions only. `conditionId` is the V2 condition id, and the\n * references are resolved from the component ids V2 conditions carry - V1\n * conditions reference components by name, and V1 components need not have\n * an id at all.\n */\nexport function buildConditionEvaluations(\n model: FormModel,\n context: FormContext\n): SubmitConditionEvaluation[] {\n const { evaluationState } = context\n\n return model.def.conditions\n .filter(isConditionWrapperV2)\n .flatMap((conditionDef) => {\n const condition = model.conditions[conditionDef.id]\n\n if (!condition) {\n return []\n }\n\n const { outcome } = condition.evaluate(evaluationState)\n\n const references = collectReferences(model, conditionDef, evaluationState)\n\n return {\n conditionId: conditionDef.id,\n outcome,\n references: [...references.values()]\n }\n })\n}\n\n/**\n * Whether a component held an answer at the point a condition was evaluated.\n *\n * The engine seeds every component in `evaluationState` with `null` before the\n * page walk begins, so an unanswered question is present but empty rather than\n * absent.\n * @see {@link FormModel.initialiseContext}\n */\nexport function isAnswered(value: FormStateValue | undefined) {\n if (value === undefined || value === null) {\n return false\n }\n\n if (Array.isArray(value)) {\n return value.length > 0\n }\n\n return value !== ''\n}\n\nfunction isConditionDataV2(\n item: ConditionDataV2 | ConditionRefDataV2\n): item is ConditionDataV2 {\n return 'componentId' in item\n}\n\n/**\n * Recursive function that collects every component a condition depends on, following nested condition\n * references. Results are keyed by component id so a component referenced more\n * than once is reported once.\n */\nfunction collectReferences(\n model: FormModel,\n conditionDef: ConditionWrapperV2,\n evaluationState: FormState,\n references: Map<string, SubmitConditionReference> = new Map<\n string,\n SubmitConditionReference\n >(),\n visited: Set<string> = new Set<string>()\n) {\n if (visited.has(conditionDef.id)) {\n return references\n }\n\n visited.add(conditionDef.id)\n\n for (const item of conditionDef.items) {\n if (isConditionDataV2(item)) {\n const component = model.getComponentById(item.componentId)\n\n // A condition referencing a component that no longer exists cannot be\n // resolved to a name, so there is nothing meaningful to report for it\n if (component) {\n references.set(item.componentId, {\n componentId: item.componentId,\n componentName: component.name,\n answered: isAnswered(evaluationState[component.name])\n })\n }\n\n continue\n }\n\n const referenced = model.getConditionById(item.conditionId)\n\n if (referenced) {\n collectReferences(model, referenced, evaluationState, references, visited)\n }\n }\n\n return references\n}\n"],"mappings":"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","ignoreList":[]}
@@ -8,6 +8,6 @@ export declare enum FileStatus {
8
8
  rejected = "rejected",
9
9
  pending = "pending"
10
10
  }
11
- export declare enum FormAdapterSubmissionSchemaVersion {
12
- V1 = 1
13
- }
11
+ export declare const FormAdapterSubmissionSchemaVersion: {
12
+ readonly V1: 1;
13
+ };
@@ -10,8 +10,9 @@ export let FileStatus = /*#__PURE__*/function (FileStatus) {
10
10
  FileStatus["pending"] = "pending";
11
11
  return FileStatus;
12
12
  }({});
13
- export let FormAdapterSubmissionSchemaVersion = /*#__PURE__*/function (FormAdapterSubmissionSchemaVersion) {
14
- FormAdapterSubmissionSchemaVersion[FormAdapterSubmissionSchemaVersion["V1"] = 1] = "V1";
15
- return FormAdapterSubmissionSchemaVersion;
16
- }({});
13
+
14
+ // Changed from an enum to enforce numeric values
15
+ export const FormAdapterSubmissionSchemaVersion = {
16
+ V1: 1
17
+ };
17
18
  //# sourceMappingURL=enums.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"enums.js","names":["UploadStatus","FileStatus","FormAdapterSubmissionSchemaVersion"],"sources":["../../../../../src/server/plugins/engine/types/enums.ts"],"sourcesContent":["export 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 enum FormAdapterSubmissionSchemaVersion {\n V1 = 1\n}\n"],"mappings":"AAAA,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;AAMtB,WAAYC,kCAAkC,0BAAlCA,kCAAkC;EAAlCA,kCAAkC,CAAlCA,kCAAkC;EAAA,OAAlCA,kCAAkC;AAAA","ignoreList":[]}
1
+ {"version":3,"file":"enums.js","names":["UploadStatus","FileStatus","FormAdapterSubmissionSchemaVersion","V1"],"sources":["../../../../../src/server/plugins/engine/types/enums.ts"],"sourcesContent":["export 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\n// Changed from an enum to enforce numeric values\nexport const FormAdapterSubmissionSchemaVersion = {\n V1: 1\n} as const satisfies Record<string, number>\n"],"mappings":"AAAA,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;;AAMtB;AACA,OAAO,MAAMC,kCAAkC,GAAG;EAChDC,EAAE,EAAE;AACN,CAA2C","ignoreList":[]}
@@ -1,8 +1,8 @@
1
- import { FormStatus, formVersionMetadataSchema, idSchema, notificationEmailAddressSchema, slugSchema, titleSchema } from '@defra/forms-model';
1
+ import { FormStatus, formSubmitConditionEvaluationSchema, formVersionMetadataSchema, idSchema, notificationEmailAddressSchema, slugSchema, titleSchema } from '@defra/forms-model';
2
2
  import Joi from 'joi';
3
3
  import { FormAdapterSubmissionSchemaVersion } from "./enums.js";
4
4
  export const formAdapterSubmissionMessageMetaSchema = Joi.object().keys({
5
- schemaVersion: Joi.string().valid(...Object.values(FormAdapterSubmissionSchemaVersion)),
5
+ schemaVersion: Joi.number().valid(...Object.values(FormAdapterSubmissionSchemaVersion)).required(),
6
6
  timestamp: Joi.date().required(),
7
7
  referenceNumber: Joi.string().required(),
8
8
  formName: titleSchema,
@@ -41,6 +41,12 @@ export const formAdapterSubmissionMessageResultSchema = Joi.object().keys({
41
41
  export const formAdapterSubmissionMessagePayloadSchema = Joi.object().keys({
42
42
  meta: formAdapterSubmissionMessageMetaSchema.required(),
43
43
  data: formAdapterSubmissionMessageDataSchema.required(),
44
- result: formAdapterSubmissionMessageResultSchema.required()
44
+ result: formAdapterSubmissionMessageResultSchema.required(),
45
+ // Optional so that messages published before this existed - which may
46
+ // still be in flight or sitting on a dead-letter queue - continue to
47
+ // validate. Consumers treat its absence as "resolve the recipients from
48
+ // the form definition yourself"
49
+ // This should be required at a later point in time.
50
+ conditionEvaluations: Joi.array().items(formSubmitConditionEvaluationSchema).optional().description('Outcome of every condition in the form definition, evaluated against the final answers at submission')
45
51
  });
46
52
  //# sourceMappingURL=schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","names":["FormStatus","formVersionMetadataSchema","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","versionMetadata","optional","language","custom","pattern","any","unknown","description","formAdapterSubmissionMessageDataSchema","main","repeaters","payment","paymentId","reference","amount","number","descriptionInEng","createdAt","files","array","items","fileName","fileId","userDownloadLink","formAdapterSubmissionMessageResultSchema","formAdapterSubmissionMessagePayloadSchema","meta","data","result"],"sources":["../../../../../src/server/plugins/engine/types/schema.ts"],"sourcesContent":["import {\n FormStatus,\n formVersionMetadataSchema,\n idSchema,\n notificationEmailAddressSchema,\n slugSchema,\n titleSchema\n} from '@defra/forms-model'\nimport Joi from 'joi'\n\nimport { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js'\nimport {\n type FormAdapterSubmissionMessageData,\n type FormAdapterSubmissionMessageMeta,\n type FormAdapterSubmissionMessagePayload,\n type FormAdapterSubmissionMessageResult\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 versionMetadata: formVersionMetadataSchema.optional(),\n language: Joi.string().optional(),\n custom: Joi.object()\n .pattern(/^/, Joi.any())\n .unknown()\n .optional()\n .description('Custom properties for the message')\n })\n\nexport const formAdapterSubmissionMessageDataSchema =\n Joi.object<FormAdapterSubmissionMessageData>().keys({\n main: Joi.object(),\n repeaters: Joi.object(),\n payment: Joi.object()\n .keys({\n paymentId: Joi.string().required(),\n reference: Joi.string().required(),\n amount: Joi.number().required(),\n description: Joi.string().required(),\n descriptionInEng: Joi.string().optional(),\n createdAt: Joi.string().required()\n })\n .optional(),\n files: Joi.object().pattern(\n Joi.string(),\n Joi.array().items(\n Joi.object().keys({\n fileName: Joi.string().required(),\n fileId: Joi.string().required(),\n userDownloadLink: Joi.string().required()\n })\n )\n )\n })\n\nexport const formAdapterSubmissionMessageResultSchema =\n Joi.object<FormAdapterSubmissionMessageResult>().keys({\n files: Joi.object()\n .keys({\n main: Joi.string().required(),\n repeaters: Joi.object()\n })\n .required()\n })\n\nexport const formAdapterSubmissionMessagePayloadSchema =\n Joi.object<FormAdapterSubmissionMessagePayload>().keys({\n meta: formAdapterSubmissionMessageMetaSchema.required(),\n data: formAdapterSubmissionMessageDataSchema.required(),\n result: formAdapterSubmissionMessageResultSchema.required()\n })\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"schema.js","names":["FormStatus","formSubmitConditionEvaluationSchema","formVersionMetadataSchema","idSchema","notificationEmailAddressSchema","slugSchema","titleSchema","Joi","FormAdapterSubmissionSchemaVersion","formAdapterSubmissionMessageMetaSchema","object","keys","schemaVersion","number","valid","Object","values","required","timestamp","date","referenceNumber","string","formName","formId","formSlug","status","isPreview","boolean","notificationEmail","versionMetadata","optional","language","custom","pattern","any","unknown","description","formAdapterSubmissionMessageDataSchema","main","repeaters","payment","paymentId","reference","amount","descriptionInEng","createdAt","files","array","items","fileName","fileId","userDownloadLink","formAdapterSubmissionMessageResultSchema","formAdapterSubmissionMessagePayloadSchema","meta","data","result","conditionEvaluations"],"sources":["../../../../../src/server/plugins/engine/types/schema.ts"],"sourcesContent":["import {\n FormStatus,\n formSubmitConditionEvaluationSchema,\n formVersionMetadataSchema,\n idSchema,\n notificationEmailAddressSchema,\n slugSchema,\n titleSchema\n} from '@defra/forms-model'\nimport Joi from 'joi'\n\nimport { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js'\nimport {\n type FormAdapterSubmissionMessageData,\n type FormAdapterSubmissionMessageMeta,\n type FormAdapterSubmissionMessagePayload,\n type FormAdapterSubmissionMessageResult\n} from '~/src/server/plugins/engine/types.js'\n\nexport const formAdapterSubmissionMessageMetaSchema =\n Joi.object<FormAdapterSubmissionMessageMeta>().keys({\n schemaVersion: Joi.number()\n .valid(...Object.values(FormAdapterSubmissionSchemaVersion))\n .required(),\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 versionMetadata: formVersionMetadataSchema.optional(),\n language: Joi.string().optional(),\n custom: Joi.object()\n .pattern(/^/, Joi.any())\n .unknown()\n .optional()\n .description('Custom properties for the message')\n })\n\nexport const formAdapterSubmissionMessageDataSchema =\n Joi.object<FormAdapterSubmissionMessageData>().keys({\n main: Joi.object(),\n repeaters: Joi.object(),\n payment: Joi.object()\n .keys({\n paymentId: Joi.string().required(),\n reference: Joi.string().required(),\n amount: Joi.number().required(),\n description: Joi.string().required(),\n descriptionInEng: Joi.string().optional(),\n createdAt: Joi.string().required()\n })\n .optional(),\n files: Joi.object().pattern(\n Joi.string(),\n Joi.array().items(\n Joi.object().keys({\n fileName: Joi.string().required(),\n fileId: Joi.string().required(),\n userDownloadLink: Joi.string().required()\n })\n )\n )\n })\n\nexport const formAdapterSubmissionMessageResultSchema =\n Joi.object<FormAdapterSubmissionMessageResult>().keys({\n files: Joi.object()\n .keys({\n main: Joi.string().required(),\n repeaters: Joi.object()\n })\n .required()\n })\n\nexport const formAdapterSubmissionMessagePayloadSchema =\n Joi.object<FormAdapterSubmissionMessagePayload>().keys({\n meta: formAdapterSubmissionMessageMetaSchema.required(),\n data: formAdapterSubmissionMessageDataSchema.required(),\n result: formAdapterSubmissionMessageResultSchema.required(),\n // Optional so that messages published before this existed - which may\n // still be in flight or sitting on a dead-letter queue - continue to\n // validate. Consumers treat its absence as \"resolve the recipients from\n // the form definition yourself\"\n // This should be required at a later point in time.\n conditionEvaluations: Joi.array()\n .items(formSubmitConditionEvaluationSchema)\n .optional()\n .description(\n 'Outcome of every condition in the form definition, evaluated against the final answers at submission'\n )\n })\n"],"mappings":"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","ignoreList":[]}
@@ -1,4 +1,4 @@
1
- import { type ComponentDef, type Event, type FormVersionMetadata, type Item, type List, type Page, type PaymentFieldComponent, type UkAddressFieldComponent } from '@defra/forms-model';
1
+ import { type ComponentDef, type Event, type FormVersionMetadata, type Item, type List, type Page, type PaymentFieldComponent, type SubmitConditionEvaluation, type UkAddressFieldComponent } from '@defra/forms-model';
2
2
  import { type PluginProperties, type Request, type RequestQuery, type ResponseObject } from '@hapi/hapi';
3
3
  import { type Yar } from '@hapi/yar';
4
4
  import { type JoiExpression, type ValidationErrorItem } from 'joi';
@@ -396,7 +396,7 @@ export interface PluginOptions {
396
396
  ordnanceSurveyApiSecret?: string;
397
397
  }
398
398
  export interface FormAdapterSubmissionMessageMeta {
399
- schemaVersion: FormAdapterSubmissionSchemaVersion;
399
+ schemaVersion: (typeof FormAdapterSubmissionSchemaVersion)[keyof typeof FormAdapterSubmissionSchemaVersion];
400
400
  timestamp: Date;
401
401
  referenceNumber: string;
402
402
  formName: string;
@@ -456,6 +456,17 @@ export interface FormAdapterSubmissionMessagePayload {
456
456
  meta: FormAdapterSubmissionMessageMeta;
457
457
  data: FormAdapterSubmissionMessageData;
458
458
  result: FormAdapterSubmissionMessageResult;
459
+ /**
460
+ * The outcome of every condition in the form definition, evaluated against
461
+ * the final answers at the point of submission.
462
+ *
463
+ * Always emitted, empty when there is nothing to report - a V1-engine form
464
+ * has no stable condition ids to record against. Optional only for
465
+ * backwards compatibility: a message published before this existed carries
466
+ * no evaluations at all, and consumers fall back to resolving the recipients
467
+ * from the form definition themselves.
468
+ */
469
+ conditionEvaluations?: SubmitConditionEvaluation[];
459
470
  }
460
471
  export interface FormAdapterSubmissionMessage extends FormAdapterSubmissionMessagePayload {
461
472
  messageId: string;
@@ -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 FormVersionMetadata,\n type Item,\n type List,\n type Page,\n type PaymentFieldComponent,\n type UkAddressFieldComponent\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type RequestQuery,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type Yar } from '@hapi/yar'\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 {\n type FileUploadField,\n type PaymentField\n} from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type EastingNorthingState,\n type LatLongState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type Translator } from '~/src/server/plugins/engine/i18n/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 { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/index.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\nexport interface CacheRequest {\n yar: { id: string | undefined }\n params: { slug?: string; state?: string }\n}\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 extends Pick<\n ValidationErrorItem,\n 'context' | 'path'\n> {\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 FormConfirmationState {\n confirmed?: true\n formId?: string\n referenceNumber?: string\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 | GeospatialState\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 languages: { name: string; code: string }[]\n translator: Translator | undefined\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 errorCode?: string\n errorMessage?: string\n errorParams?: Record<string, number | string | string[]>\n }\n | {\n fileStatus: FileStatus.complete\n errorCode?: string\n errorMessage?: undefined\n errorParams?: Record<string, number | string | string[]>\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\n/**\n * A longitude/latitude coordinate pair in WGS84 format\n * Format: [longitude, latitude]\n */\nexport type Coordinates = [longitude: number, latitude: number]\n\n/**\n * GeoJSON Point geometry\n */\nexport interface PointGeometry {\n type: 'Point'\n coordinates: Coordinates\n}\n\n/**\n * GeoJSON LineString geometry\n */\nexport interface LineStringGeometry {\n type: 'LineString'\n coordinates: Coordinates[]\n}\n\n/**\n * GeoJSON Polygon geometry\n */\nexport interface PolygonGeometry {\n type: 'Polygon'\n coordinates: Coordinates[][]\n}\n\n/**\n * Supported geometry types\n */\nexport type Geometry = PointGeometry | LineStringGeometry | PolygonGeometry\n\n/**\n * Feature metadata\n */\nexport interface FeatureProperties {\n /**\n * Human-readable description of the feature\n */\n description: string\n /**\n * The OS grid reference of the first coordinate of the feature\n */\n coordinateGridReference?: string\n /**\n * The OS grid reference of the centroid of the feature\n */\n centroidGridReference?: string\n}\n\n/**\n * A single GeoJSON Feature\n */\nexport interface Feature {\n id: string\n type: 'Feature'\n properties: FeatureProperties\n geometry: Geometry\n}\n\n/**\n * A GeoJSON FeatureCollection\n */\nexport type FeatureCollection = Feature[]\n\nexport type GeospatialState = FeatureCollection\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 t?: (key: string, opts?: Record<string, unknown>) => 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 showSubmitButton?: boolean\n showPaymentExpiredNotification?: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n h: FormResponseToolkit,\n context: FormContext\n) =>\n | ResponseObject\n | FormResponseToolkit['continue']\n | Promise<ResponseObject | FormResponseToolkit['continue']>\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport type GetLanguageHandler = (query?: RequestQuery, yar?: Yar) => string\n\nexport interface ExternalArgs {\n component: ComponentDef\n controller: QuestionPageController\n sourceUrl: string\n actionArgs?: Record<string, string>\n isLive: boolean\n isPreview: boolean\n}\n\nexport interface PostcodeLookupExternalArgs extends ExternalArgs {\n component: UkAddressFieldComponent\n actionArgs: { step: string }\n}\n\nexport interface PaymentExternalArgs extends ExternalArgs {\n component: PaymentFieldComponent\n}\n\nexport interface ExternalStateAppendage {\n component: string\n data: FormStateValue | FormState\n}\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 getLanguage?: GetLanguageHandler\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n ordnanceSurveyApiKey?: string\n ordnanceSurveyApiSecret?: string\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 language?: string\n custom?: Record<string, unknown>\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterPayment {\n paymentId: string\n reference: string\n amount: number\n description: string\n descriptionInEng?: string\n createdAt: 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}\n\n/**\n * A detail item specifically for payments\n */\nexport type PaymentFieldDetailItem = Omit<DetailItemField, 'field'> & {\n field: PaymentField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n | EastingNorthingState\n | LatLongState\n | GeospatialState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue | null>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n payment?: FormAdapterPayment\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n}\n\nexport interface FormAdapterSubmissionMessage extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAkEA;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;;AA2BA;AACA;AACA;AACA;;AAwGA,SACEA,UAAU,EACVC,YAAY;;AAoEd;AACA;AACA;AACA;;AAGA;AACA;AACA;;AAMA;AACA;AACA;;AAMA;AACA;AACA;;AAMA;AACA;AACA;;AAGA;AACA;AACA;;AAgBA;AACA;AACA;;AAQA;AACA;AACA;;AA+MA;AACA;AACA;;AAKA;AACA;AACA","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":["FileStatus","UploadStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormVersionMetadata,\n type Item,\n type List,\n type Page,\n type PaymentFieldComponent,\n type SubmitConditionEvaluation,\n type UkAddressFieldComponent\n} from '@defra/forms-model'\nimport {\n type PluginProperties,\n type Request,\n type RequestQuery,\n type ResponseObject\n} from '@hapi/hapi'\nimport { type Yar } from '@hapi/yar'\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 {\n type FileUploadField,\n type PaymentField\n} from '~/src/server/plugins/engine/components/index.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel,\n type DatePartsState,\n type EastingNorthingState,\n type LatLongState,\n type MonthYearState\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type Translator } from '~/src/server/plugins/engine/i18n/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 { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/index.js'\nimport {\n type FileStatus,\n type FormAdapterSubmissionSchemaVersion,\n type UploadStatus\n} from '~/src/server/plugins/engine/types/enums.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\nexport type AnyFormRequest = FormRequest | FormRequestPayload\nexport type AnyRequest = Request | AnyFormRequest\n\nexport interface CacheRequest {\n yar: { id: string | undefined }\n params: { slug?: string; state?: string }\n}\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 extends Pick<\n ValidationErrorItem,\n 'context' | 'path'\n> {\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 FormConfirmationState {\n confirmed?: true\n formId?: string\n referenceNumber?: string\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 | GeospatialState\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 languages: { name: string; code: string }[]\n translator: Translator | undefined\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 errorCode?: string\n errorMessage?: string\n errorParams?: Record<string, number | string | string[]>\n }\n | {\n fileStatus: FileStatus.complete\n errorCode?: string\n errorMessage?: undefined\n errorParams?: Record<string, number | string | string[]>\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\n/**\n * A longitude/latitude coordinate pair in WGS84 format\n * Format: [longitude, latitude]\n */\nexport type Coordinates = [longitude: number, latitude: number]\n\n/**\n * GeoJSON Point geometry\n */\nexport interface PointGeometry {\n type: 'Point'\n coordinates: Coordinates\n}\n\n/**\n * GeoJSON LineString geometry\n */\nexport interface LineStringGeometry {\n type: 'LineString'\n coordinates: Coordinates[]\n}\n\n/**\n * GeoJSON Polygon geometry\n */\nexport interface PolygonGeometry {\n type: 'Polygon'\n coordinates: Coordinates[][]\n}\n\n/**\n * Supported geometry types\n */\nexport type Geometry = PointGeometry | LineStringGeometry | PolygonGeometry\n\n/**\n * Feature metadata\n */\nexport interface FeatureProperties {\n /**\n * Human-readable description of the feature\n */\n description: string\n /**\n * The OS grid reference of the first coordinate of the feature\n */\n coordinateGridReference?: string\n /**\n * The OS grid reference of the centroid of the feature\n */\n centroidGridReference?: string\n}\n\n/**\n * A single GeoJSON Feature\n */\nexport interface Feature {\n id: string\n type: 'Feature'\n properties: FeatureProperties\n geometry: Geometry\n}\n\n/**\n * A GeoJSON FeatureCollection\n */\nexport type FeatureCollection = Feature[]\n\nexport type GeospatialState = FeatureCollection\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 t?: (key: string, opts?: Record<string, unknown>) => 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 showSubmitButton?: boolean\n showPaymentExpiredNotification?: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n allowSaveAndExit: boolean\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: AnyFormRequest,\n h: FormResponseToolkit,\n context: FormContext\n) =>\n | ResponseObject\n | FormResponseToolkit['continue']\n | Promise<ResponseObject | FormResponseToolkit['continue']>\n\nexport type SaveAndExitHandler = (\n request: FormRequestPayload,\n h: FormResponseToolkit,\n context: FormContext\n) => ResponseObject\n\nexport type GetLanguageHandler = (query?: RequestQuery, yar?: Yar) => string\n\nexport interface ExternalArgs {\n component: ComponentDef\n controller: QuestionPageController\n sourceUrl: string\n actionArgs?: Record<string, string>\n isLive: boolean\n isPreview: boolean\n}\n\nexport interface PostcodeLookupExternalArgs extends ExternalArgs {\n component: UkAddressFieldComponent\n actionArgs: { step: string }\n}\n\nexport interface PaymentExternalArgs extends ExternalArgs {\n component: PaymentFieldComponent\n}\n\nexport interface ExternalStateAppendage {\n component: string\n data: FormStateValue | FormState\n}\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 getLanguage?: GetLanguageHandler\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n ordnanceSurveyApiKey?: string\n ordnanceSurveyApiSecret?: string\n}\n\nexport interface FormAdapterSubmissionMessageMeta {\n schemaVersion: (typeof FormAdapterSubmissionSchemaVersion)[keyof typeof 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 language?: string\n custom?: Record<string, unknown>\n}\n\nexport type FormAdapterSubmissionMessageMetaSerialised = Omit<\n FormAdapterSubmissionMessageMeta,\n 'schemaVersion' | 'timestamp' | 'status'\n> & {\n schemaVersion: number\n status: string\n timestamp: string\n}\nexport interface FormAdapterFile {\n fileName: string\n fileId: string\n userDownloadLink: string\n}\n\nexport interface FormAdapterPayment {\n paymentId: string\n reference: string\n amount: number\n description: string\n descriptionInEng?: string\n createdAt: 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}\n\n/**\n * A detail item specifically for payments\n */\nexport type PaymentFieldDetailItem = Omit<DetailItemField, 'field'> & {\n field: PaymentField\n}\nexport type RichFormValue =\n | FormValue\n | FormPayload\n | DatePartsState\n | MonthYearState\n | UkAddressState\n | EastingNorthingState\n | LatLongState\n | GeospatialState\n\nexport interface FormAdapterSubmissionMessageData {\n main: Record<string, RichFormValue | null>\n repeaters: Record<string, Record<string, RichFormValue>[]>\n files: Record<string, FormAdapterFile[]>\n payment?: FormAdapterPayment\n}\n\nexport interface FormAdapterSubmissionMessagePayload {\n meta: FormAdapterSubmissionMessageMeta\n data: FormAdapterSubmissionMessageData\n result: FormAdapterSubmissionMessageResult\n\n /**\n * The outcome of every condition in the form definition, evaluated against\n * the final answers at the point of submission.\n *\n * Always emitted, empty when there is nothing to report - a V1-engine form\n * has no stable condition ids to record against. Optional only for\n * backwards compatibility: a message published before this existed carries\n * no evaluations at all, and consumers fall back to resolving the recipients\n * from the form definition themselves.\n */\n conditionEvaluations?: SubmitConditionEvaluation[]\n}\n\nexport interface FormAdapterSubmissionMessage extends FormAdapterSubmissionMessagePayload {\n messageId: string\n recordCreatedAt: Date\n}\n\nexport interface FormAdapterSubmissionService {\n handleFormSubmission: (\n submissionMessage: FormAdapterSubmissionMessage\n ) => unknown\n}\n"],"mappings":"AAmEA;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;;AA2BA;AACA;AACA;AACA;;AAwGA,SACEA,UAAU,EACVC,YAAY;;AAoEd;AACA;AACA;AACA;;AAGA;AACA;AACA;;AAMA;AACA;AACA;;AAMA;AACA;AACA;;AAMA;AACA;AACA;;AAGA;AACA;AACA;;AAgBA;AACA;AACA;;AAQA;AACA;AACA;;AA+MA;AACA;AACA;;AAKA;AACA;AACA","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "5.0.0-alpha.20",
3
+ "version": "5.0.0-alpha.21",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -92,7 +92,7 @@
92
92
  },
93
93
  "license": "SEE LICENSE IN LICENSE",
94
94
  "dependencies": {
95
- "@defra/forms-model": "^3.0.698",
95
+ "@defra/forms-model": "^3.0.704",
96
96
  "@defra/hapi-tracing": "^1.29.0",
97
97
  "@defra/interactive-map": "0.0.33-alpha",
98
98
  "@elastic/ecs-pino-format": "^1.5.0",
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ComponentType,
3
+ ConditionEvaluationOutcome,
3
4
  ConditionsModel,
4
5
  ControllerPath,
5
6
  ControllerType,
@@ -8,6 +9,7 @@ import {
8
9
  formDefinitionSchema,
9
10
  formDefinitionV2Schema,
10
11
  generateConditionAlias,
12
+ getErrorMessage,
11
13
  hasComponents,
12
14
  hasComponentsEvenIfNoNext,
13
15
  hasRepeater,
@@ -54,7 +56,10 @@ import { extractBaseTranslations } from '../i18n/extractBaseTranslations.js'
54
56
  import { createFormI18nInstance } from '../i18n/index.js'
55
57
  import { getAvailableLanguages } from '../i18n/languages.js'
56
58
  import { type Translator } from '../i18n/types.js'
57
- import { type ExecutableCondition } from './types.js'
59
+ import {
60
+ type ConditionEvaluation,
61
+ type ExecutableCondition
62
+ } from './types.js'
58
63
  import { type PageController } from '../pageControllers/PageController.js'
59
64
  import {
60
65
  createPage,
@@ -304,21 +309,35 @@ export class FormModel {
304
309
  throw new ConditionBuildError(displayName, { cause })
305
310
  }
306
311
 
307
- const fn = (evaluationState: FormState) => {
312
+ const evaluate = (evaluationState: FormState): ConditionEvaluation => {
308
313
  const ctx = this.toConditionContext(evaluationState, this.conditions)
314
+
309
315
  try {
310
- return expr.evaluate(ctx) as boolean
311
- } catch {
312
- return false
316
+ return {
317
+ outcome: (expr.evaluate(ctx) as boolean)
318
+ ? ConditionEvaluationOutcome.True
319
+ : ConditionEvaluationOutcome.False
320
+ }
321
+ } catch (err) {
322
+ return {
323
+ outcome: ConditionEvaluationOutcome.Error,
324
+ error: getErrorMessage(err)
325
+ }
313
326
  }
314
327
  }
315
328
 
329
+ // A failed evaluation continues to route as `false`. `evaluate` exists so
330
+ // that the two can be told apart when recording outcomes for submission.
331
+ const fn = (evaluationState: FormState) =>
332
+ evaluate(evaluationState).outcome === ConditionEvaluationOutcome.True
333
+
316
334
  return {
317
335
  name,
318
336
  displayName,
319
337
  value,
320
338
  expr,
321
- fn
339
+ fn,
340
+ evaluate
322
341
  }
323
342
  }
324
343
 
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type ConditionEvaluationOutcome,
2
3
  type ConditionWrapper,
3
4
  type FormComponentsDef,
4
5
  type Section
@@ -16,9 +17,39 @@ import {
16
17
  type FormSubmissionError
17
18
  } from '../types.js'
18
19
 
20
+ /**
21
+ * The result of evaluating a condition, keeping a failed evaluation distinct
22
+ * from one that legitimately returned `false`.
23
+ * @see {@link ExecutableCondition.evaluate}
24
+ */
25
+ export interface ConditionEvaluation {
26
+ outcome: ConditionEvaluationOutcome
27
+ error?: string
28
+ }
29
+
30
+ /**
31
+ * A form condition paired with the parsed expression and callbacks needed to
32
+ * run it against a form submission state
33
+ * Created by `FormModel.makeCondition`
34
+ */
19
35
  export type ExecutableCondition = ConditionWrapper & {
36
+ /**
37
+ * Parsed expression for the condition's {@link ConditionWrapper.value},
38
+ * evaluated against a context built from the submission state
39
+ */
20
40
  expr: Expression
41
+
42
+ /**
43
+ * Evaluates the condition, used for page routing and component visibility
44
+ * A failed evaluation is reported as `false`
45
+ */
21
46
  fn: (evaluationState: FormState) => boolean
47
+
48
+ /**
49
+ * As `fn`, but reports whether evaluation failed rather than defaulting a
50
+ * failure to `false`. Used to record condition outcomes on submission.
51
+ */
52
+ evaluate: (evaluationState: FormState) => ConditionEvaluation
22
53
  }
23
54
 
24
55
  /**
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Engine,
2
3
  type FormMetadata,
3
4
  type SubmitResponsePayload
4
5
  } from '@defra/forms-model'
@@ -11,6 +12,7 @@ import {
11
12
  import { type FormModel } from '../../models/FormModel.js'
12
13
  import { type DetailItem } from '../../models/types.js'
13
14
  import { categoriseData } from '../machine/v2.js'
15
+ import { buildConditionEvaluations } from '../../pageControllers/helpers/submission.js'
14
16
  import { FormAdapterSubmissionSchemaVersion } from '../../types/enums.js'
15
17
  import {
16
18
  type FormAdapterSubmissionMessageData,
@@ -73,7 +75,19 @@ export function format(
73
75
  const payload: FormAdapterSubmissionMessagePayload = {
74
76
  meta,
75
77
  data,
76
- result
78
+ result,
79
+
80
+ // Recorded here because only the engine holds the walked evaluation state
81
+ // the conditions were judged against; forms-notify-listener receives the
82
+ // flat submitted answers and resolves the outputs that qualify from these
83
+ // outcomes alone. Condition ids are only stable in V2, so a V1 definition
84
+ // has nothing to report - the property is still emitted, because its
85
+ // absence is what marks a message as predating this and sends the listener
86
+ // down its legacy path.
87
+ conditionEvaluations:
88
+ model.engine === Engine.V2
89
+ ? buildConditionEvaluations(model, context)
90
+ : []
77
91
  }
78
92
 
79
93
  return JSON.stringify(payload)
@@ -1,13 +1,27 @@
1
- import { type SubmitPayload } from '@defra/forms-model'
1
+ import {
2
+ isConditionWrapperV2,
3
+ type ConditionDataV2,
4
+ type ConditionRefDataV2,
5
+ type ConditionWrapperV2,
6
+ type SubmitConditionEvaluation,
7
+ type SubmitConditionReference,
8
+ type SubmitPayload
9
+ } from '@defra/forms-model'
2
10
 
3
11
  import { GeospatialField } from '../../components/GeospatialField.js'
4
12
  import { PaymentField } from '../../components/PaymentField.js'
5
13
  import { getAnswer } from '../../components/helpers/components.js'
6
14
  import { type Translator } from '../../i18n/types.js'
15
+ import { type FormModel } from '../../models/FormModel.js'
7
16
  import {
8
17
  type DetailItem,
9
18
  type DetailItemField
10
19
  } from '../../models/types.js'
20
+ import {
21
+ type FormContext,
22
+ type FormState,
23
+ type FormStateValue
24
+ } from '../../types.js'
11
25
  import {
12
26
  formatCurrency,
13
27
  formatPaymentDate
@@ -141,3 +155,125 @@ export function buildRepeaterRecords(
141
155
  )
142
156
  }))
143
157
  }
158
+
159
+ /**
160
+ * Records the outcome of every condition in the form definition, evaluated
161
+ * against the answers as they stand at the point of submission.
162
+ *
163
+ * Evaluated here rather than by the consumer because only the engine holds
164
+ * the context to evaluate against: `evaluationState` is built by walking the
165
+ * form from its start page along the path taken, applying page conditions as
166
+ * it goes. A consumer receives the flat submitted answers, not that walked
167
+ * state, and reproducing the walk in a second codebase would leave two
168
+ * implementations of the same logic free to diverge.
169
+ *
170
+ * Each record carries the components the condition depends on and whether each
171
+ * was answered. An unanswered question still yields a boolean - negative
172
+ * operators such as "is not" return `true` against the seeded `null` - so the
173
+ * outcome alone cannot be read as evidence that the user gave that answer.
174
+ *
175
+ * V2 definitions only. `conditionId` is the V2 condition id, and the
176
+ * references are resolved from the component ids V2 conditions carry - V1
177
+ * conditions reference components by name, and V1 components need not have
178
+ * an id at all.
179
+ */
180
+ export function buildConditionEvaluations(
181
+ model: FormModel,
182
+ context: FormContext
183
+ ): SubmitConditionEvaluation[] {
184
+ const { evaluationState } = context
185
+
186
+ return model.def.conditions
187
+ .filter(isConditionWrapperV2)
188
+ .flatMap((conditionDef) => {
189
+ const condition = model.conditions[conditionDef.id]
190
+
191
+ if (!condition) {
192
+ return []
193
+ }
194
+
195
+ const { outcome } = condition.evaluate(evaluationState)
196
+
197
+ const references = collectReferences(model, conditionDef, evaluationState)
198
+
199
+ return {
200
+ conditionId: conditionDef.id,
201
+ outcome,
202
+ references: [...references.values()]
203
+ }
204
+ })
205
+ }
206
+
207
+ /**
208
+ * Whether a component held an answer at the point a condition was evaluated.
209
+ *
210
+ * The engine seeds every component in `evaluationState` with `null` before the
211
+ * page walk begins, so an unanswered question is present but empty rather than
212
+ * absent.
213
+ * @see {@link FormModel.initialiseContext}
214
+ */
215
+ export function isAnswered(value: FormStateValue | undefined) {
216
+ if (value === undefined || value === null) {
217
+ return false
218
+ }
219
+
220
+ if (Array.isArray(value)) {
221
+ return value.length > 0
222
+ }
223
+
224
+ return value !== ''
225
+ }
226
+
227
+ function isConditionDataV2(
228
+ item: ConditionDataV2 | ConditionRefDataV2
229
+ ): item is ConditionDataV2 {
230
+ return 'componentId' in item
231
+ }
232
+
233
+ /**
234
+ * Recursive function that collects every component a condition depends on, following nested condition
235
+ * references. Results are keyed by component id so a component referenced more
236
+ * than once is reported once.
237
+ */
238
+ function collectReferences(
239
+ model: FormModel,
240
+ conditionDef: ConditionWrapperV2,
241
+ evaluationState: FormState,
242
+ references: Map<string, SubmitConditionReference> = new Map<
243
+ string,
244
+ SubmitConditionReference
245
+ >(),
246
+ visited: Set<string> = new Set<string>()
247
+ ) {
248
+ if (visited.has(conditionDef.id)) {
249
+ return references
250
+ }
251
+
252
+ visited.add(conditionDef.id)
253
+
254
+ for (const item of conditionDef.items) {
255
+ if (isConditionDataV2(item)) {
256
+ const component = model.getComponentById(item.componentId)
257
+
258
+ // A condition referencing a component that no longer exists cannot be
259
+ // resolved to a name, so there is nothing meaningful to report for it
260
+ if (component) {
261
+ references.set(item.componentId, {
262
+ componentId: item.componentId,
263
+ componentName: component.name,
264
+ answered: isAnswered(evaluationState[component.name])
265
+ })
266
+ }
267
+
268
+ continue
269
+ }
270
+
271
+ const referenced = model.getConditionById(item.conditionId)
272
+
273
+ if (referenced) {
274
+ collectReferences(model, referenced, evaluationState, references, visited)
275
+ }
276
+ }
277
+
278
+ return references
279
+ }
@@ -10,6 +10,7 @@ export enum FileStatus {
10
10
  pending = 'pending'
11
11
  }
12
12
 
13
- export enum FormAdapterSubmissionSchemaVersion {
14
- V1 = 1
15
- }
13
+ // Changed from an enum to enforce numeric values
14
+ export const FormAdapterSubmissionSchemaVersion = {
15
+ V1: 1
16
+ } as const satisfies Record<string, number>
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  FormStatus,
3
+ formSubmitConditionEvaluationSchema,
3
4
  formVersionMetadataSchema,
4
5
  idSchema,
5
6
  notificationEmailAddressSchema,
@@ -18,9 +19,9 @@ import {
18
19
 
19
20
  export const formAdapterSubmissionMessageMetaSchema =
20
21
  Joi.object<FormAdapterSubmissionMessageMeta>().keys({
21
- schemaVersion: Joi.string().valid(
22
- ...Object.values(FormAdapterSubmissionSchemaVersion)
23
- ),
22
+ schemaVersion: Joi.number()
23
+ .valid(...Object.values(FormAdapterSubmissionSchemaVersion))
24
+ .required(),
24
25
  timestamp: Joi.date().required(),
25
26
  referenceNumber: Joi.string().required(),
26
27
  formName: titleSchema,
@@ -80,5 +81,16 @@ export const formAdapterSubmissionMessagePayloadSchema =
80
81
  Joi.object<FormAdapterSubmissionMessagePayload>().keys({
81
82
  meta: formAdapterSubmissionMessageMetaSchema.required(),
82
83
  data: formAdapterSubmissionMessageDataSchema.required(),
83
- result: formAdapterSubmissionMessageResultSchema.required()
84
+ result: formAdapterSubmissionMessageResultSchema.required(),
85
+ // Optional so that messages published before this existed - which may
86
+ // still be in flight or sitting on a dead-letter queue - continue to
87
+ // validate. Consumers treat its absence as "resolve the recipients from
88
+ // the form definition yourself"
89
+ // This should be required at a later point in time.
90
+ conditionEvaluations: Joi.array()
91
+ .items(formSubmitConditionEvaluationSchema)
92
+ .optional()
93
+ .description(
94
+ 'Outcome of every condition in the form definition, evaluated against the final answers at submission'
95
+ )
84
96
  })
@@ -6,6 +6,7 @@ import {
6
6
  type List,
7
7
  type Page,
8
8
  type PaymentFieldComponent,
9
+ type SubmitConditionEvaluation,
9
10
  type UkAddressFieldComponent
10
11
  } from '@defra/forms-model'
11
12
  import {
@@ -529,7 +530,7 @@ export interface PluginOptions {
529
530
  }
530
531
 
531
532
  export interface FormAdapterSubmissionMessageMeta {
532
- schemaVersion: FormAdapterSubmissionSchemaVersion
533
+ schemaVersion: (typeof FormAdapterSubmissionSchemaVersion)[keyof typeof FormAdapterSubmissionSchemaVersion]
533
534
  timestamp: Date
534
535
  referenceNumber: string
535
536
  formName: string
@@ -607,6 +608,18 @@ export interface FormAdapterSubmissionMessagePayload {
607
608
  meta: FormAdapterSubmissionMessageMeta
608
609
  data: FormAdapterSubmissionMessageData
609
610
  result: FormAdapterSubmissionMessageResult
611
+
612
+ /**
613
+ * The outcome of every condition in the form definition, evaluated against
614
+ * the final answers at the point of submission.
615
+ *
616
+ * Always emitted, empty when there is nothing to report - a V1-engine form
617
+ * has no stable condition ids to record against. Optional only for
618
+ * backwards compatibility: a message published before this existed carries
619
+ * no evaluations at all, and consumers fall back to resolving the recipients
620
+ * from the form definition themselves.
621
+ */
622
+ conditionEvaluations?: SubmitConditionEvaluation[]
610
623
  }
611
624
 
612
625
  export interface FormAdapterSubmissionMessage extends FormAdapterSubmissionMessagePayload {