@defra/forms-engine-plugin 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.server/server/plugins/engine/index.js +7 -0
- package/.server/server/plugins/engine/index.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.js +5 -4
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +2 -0
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/package.json +2 -2
- package/src/server/index.test.ts +15 -1
- package/src/server/plugins/engine/index.ts +7 -0
- package/src/server/plugins/engine/models/FormModel.test.ts +132 -8
- package/src/server/plugins/engine/models/FormModel.ts +6 -3
- package/src/server/plugins/engine/types.ts +2 -0
|
@@ -29,6 +29,13 @@ export const prepareNunjucksEnvironment = function (env, pluginOptions) {
|
|
|
29
29
|
engine.registerFilter(name, filter);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
// Apply any additional globals to nunjucks engines
|
|
34
|
+
if (pluginOptions.globals) {
|
|
35
|
+
Object.entries(pluginOptions.globals).forEach(([name, fn]) => {
|
|
36
|
+
env.addGlobal(name, fn);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
32
39
|
};
|
|
33
40
|
export default plugin;
|
|
34
41
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["markdownToHtml","engine","plugin","checkComponentTemplates","checkErrorTemplates","evaluate","govukRebrand","filters","getPageHref","context","globals","VIEW_PATH","PLUGIN_PATH","prepareNunjucksEnvironment","env","pluginOptions","name","nunjucksFilter","Object","entries","addFilter","text","baseUrl","nunjucksGlobal","addGlobal","filter","registerFilter"],"sources":["../../../../src/server/plugins/engine/index.ts"],"sourcesContent":["import { markdownToHtml } from '@defra/forms-model'\nimport { type Environment } from 'nunjucks'\n\nimport { engine } from '~/src/server/plugins/engine/helpers.js'\nimport { plugin } from '~/src/server/plugins/engine/plugin.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport {\n checkComponentTemplates,\n checkErrorTemplates,\n evaluate,\n govukRebrand\n} from '~/src/server/plugins/nunjucks/environment.js'\nimport * as filters from '~/src/server/plugins/nunjucks/filters/index.js'\n\nexport { getPageHref } from '~/src/server/plugins/engine/helpers.js'\nexport { context } from '~/src/server/plugins/nunjucks/context.js'\n\nconst globals = {\n checkComponentTemplates,\n checkErrorTemplates,\n evaluate,\n govukRebrand\n}\n\nexport const VIEW_PATH = 'src/server/plugins/engine/views'\nexport const PLUGIN_PATH = 'node_modules/@defra/forms-engine-plugin'\n\nexport const prepareNunjucksEnvironment = function (\n env: Environment,\n pluginOptions: PluginOptions\n) {\n for (const [name, nunjucksFilter] of Object.entries(filters)) {\n env.addFilter(name, nunjucksFilter)\n }\n\n env.addFilter('markdown', (text: string) =>\n markdownToHtml(text, pluginOptions.baseUrl)\n )\n\n for (const [name, nunjucksGlobal] of Object.entries(globals)) {\n env.addGlobal(name, nunjucksGlobal)\n }\n\n // Apply any additional filters to both the liquid and nunjucks engines\n if (pluginOptions.filters) {\n for (const [name, filter] of Object.entries(pluginOptions.filters)) {\n env.addFilter(name, filter)\n engine.registerFilter(name, filter)\n }\n }\n}\n\nexport default plugin\n"],"mappings":"AAAA,SAASA,cAAc,QAAQ,oBAAoB;AAGnD,SAASC,MAAM;AACf,SAASC,MAAM;AAEf,SACEC,uBAAuB,EACvBC,mBAAmB,EACnBC,QAAQ,EACRC,YAAY;AAEd,OAAO,KAAKC,OAAO;AAEnB,SAASC,WAAW;AACpB,SAASC,OAAO;AAEhB,MAAMC,OAAO,GAAG;EACdP,uBAAuB;EACvBC,mBAAmB;EACnBC,QAAQ;EACRC;AACF,CAAC;AAED,OAAO,MAAMK,SAAS,GAAG,iCAAiC;AAC1D,OAAO,MAAMC,WAAW,GAAG,yCAAyC;AAEpE,OAAO,MAAMC,0BAA0B,GAAG,SAAAA,CACxCC,GAAgB,EAChBC,aAA4B,EAC5B;EACA,KAAK,MAAM,CAACC,IAAI,EAAEC,cAAc,CAAC,IAAIC,MAAM,CAACC,OAAO,CAACZ,OAAO,CAAC,EAAE;IAC5DO,GAAG,CAACM,SAAS,CAACJ,IAAI,EAAEC,cAAc,CAAC;EACrC;EAEAH,GAAG,CAACM,SAAS,CAAC,UAAU,EAAGC,IAAY,IACrCrB,cAAc,CAACqB,IAAI,EAAEN,aAAa,CAACO,OAAO,CAC5C,CAAC;EAED,KAAK,MAAM,CAACN,IAAI,EAAEO,cAAc,CAAC,IAAIL,MAAM,CAACC,OAAO,CAACT,OAAO,CAAC,EAAE;IAC5DI,GAAG,CAACU,SAAS,CAACR,IAAI,EAAEO,cAAc,CAAC;EACrC;;EAEA;EACA,IAAIR,aAAa,CAACR,OAAO,EAAE;IACzB,KAAK,MAAM,CAACS,IAAI,EAAES,MAAM,CAAC,IAAIP,MAAM,CAACC,OAAO,CAACJ,aAAa,CAACR,OAAO,CAAC,EAAE;MAClEO,GAAG,CAACM,SAAS,CAACJ,IAAI,EAAES,MAAM,CAAC;MAC3BxB,MAAM,CAACyB,cAAc,CAACV,IAAI,EAAES,MAAM,CAAC;IACrC;EACF;AACF,CAAC;AAED,
|
|
1
|
+
{"version":3,"file":"index.js","names":["markdownToHtml","engine","plugin","checkComponentTemplates","checkErrorTemplates","evaluate","govukRebrand","filters","getPageHref","context","globals","VIEW_PATH","PLUGIN_PATH","prepareNunjucksEnvironment","env","pluginOptions","name","nunjucksFilter","Object","entries","addFilter","text","baseUrl","nunjucksGlobal","addGlobal","filter","registerFilter","forEach","fn"],"sources":["../../../../src/server/plugins/engine/index.ts"],"sourcesContent":["import { markdownToHtml } from '@defra/forms-model'\nimport { type Environment } from 'nunjucks'\n\nimport { engine } from '~/src/server/plugins/engine/helpers.js'\nimport { plugin } from '~/src/server/plugins/engine/plugin.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport {\n checkComponentTemplates,\n checkErrorTemplates,\n evaluate,\n govukRebrand\n} from '~/src/server/plugins/nunjucks/environment.js'\nimport * as filters from '~/src/server/plugins/nunjucks/filters/index.js'\n\nexport { getPageHref } from '~/src/server/plugins/engine/helpers.js'\nexport { context } from '~/src/server/plugins/nunjucks/context.js'\n\nconst globals = {\n checkComponentTemplates,\n checkErrorTemplates,\n evaluate,\n govukRebrand\n}\n\nexport const VIEW_PATH = 'src/server/plugins/engine/views'\nexport const PLUGIN_PATH = 'node_modules/@defra/forms-engine-plugin'\n\nexport const prepareNunjucksEnvironment = function (\n env: Environment,\n pluginOptions: PluginOptions\n) {\n for (const [name, nunjucksFilter] of Object.entries(filters)) {\n env.addFilter(name, nunjucksFilter)\n }\n\n env.addFilter('markdown', (text: string) =>\n markdownToHtml(text, pluginOptions.baseUrl)\n )\n\n for (const [name, nunjucksGlobal] of Object.entries(globals)) {\n env.addGlobal(name, nunjucksGlobal)\n }\n\n // Apply any additional filters to both the liquid and nunjucks engines\n if (pluginOptions.filters) {\n for (const [name, filter] of Object.entries(pluginOptions.filters)) {\n env.addFilter(name, filter)\n engine.registerFilter(name, filter)\n }\n }\n\n // Apply any additional globals to nunjucks engines\n if (pluginOptions.globals) {\n Object.entries(pluginOptions.globals).forEach(([name, fn]) => {\n env.addGlobal(name, fn)\n })\n }\n}\n\nexport default plugin\n"],"mappings":"AAAA,SAASA,cAAc,QAAQ,oBAAoB;AAGnD,SAASC,MAAM;AACf,SAASC,MAAM;AAEf,SACEC,uBAAuB,EACvBC,mBAAmB,EACnBC,QAAQ,EACRC,YAAY;AAEd,OAAO,KAAKC,OAAO;AAEnB,SAASC,WAAW;AACpB,SAASC,OAAO;AAEhB,MAAMC,OAAO,GAAG;EACdP,uBAAuB;EACvBC,mBAAmB;EACnBC,QAAQ;EACRC;AACF,CAAC;AAED,OAAO,MAAMK,SAAS,GAAG,iCAAiC;AAC1D,OAAO,MAAMC,WAAW,GAAG,yCAAyC;AAEpE,OAAO,MAAMC,0BAA0B,GAAG,SAAAA,CACxCC,GAAgB,EAChBC,aAA4B,EAC5B;EACA,KAAK,MAAM,CAACC,IAAI,EAAEC,cAAc,CAAC,IAAIC,MAAM,CAACC,OAAO,CAACZ,OAAO,CAAC,EAAE;IAC5DO,GAAG,CAACM,SAAS,CAACJ,IAAI,EAAEC,cAAc,CAAC;EACrC;EAEAH,GAAG,CAACM,SAAS,CAAC,UAAU,EAAGC,IAAY,IACrCrB,cAAc,CAACqB,IAAI,EAAEN,aAAa,CAACO,OAAO,CAC5C,CAAC;EAED,KAAK,MAAM,CAACN,IAAI,EAAEO,cAAc,CAAC,IAAIL,MAAM,CAACC,OAAO,CAACT,OAAO,CAAC,EAAE;IAC5DI,GAAG,CAACU,SAAS,CAACR,IAAI,EAAEO,cAAc,CAAC;EACrC;;EAEA;EACA,IAAIR,aAAa,CAACR,OAAO,EAAE;IACzB,KAAK,MAAM,CAACS,IAAI,EAAES,MAAM,CAAC,IAAIP,MAAM,CAACC,OAAO,CAACJ,aAAa,CAACR,OAAO,CAAC,EAAE;MAClEO,GAAG,CAACM,SAAS,CAACJ,IAAI,EAAES,MAAM,CAAC;MAC3BxB,MAAM,CAACyB,cAAc,CAACV,IAAI,EAAES,MAAM,CAAC;IACrC;EACF;;EAEA;EACA,IAAIV,aAAa,CAACL,OAAO,EAAE;IACzBQ,MAAM,CAACC,OAAO,CAACJ,aAAa,CAACL,OAAO,CAAC,CAACiB,OAAO,CAAC,CAAC,CAACX,IAAI,EAAEY,EAAE,CAAC,KAAK;MAC5Dd,GAAG,CAACU,SAAS,CAACR,IAAI,EAAEY,EAAE,CAAC;IACzB,CAAC,CAAC;EACJ;AACF,CAAC;AAED,eAAe1B,MAAM","ignoreList":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ComponentType, ConditionsModel, ControllerPath, ControllerType, Engine, SchemaVersion, convertConditionWrapperFromV2, formDefinitionSchema, formDefinitionV2Schema, hasComponents, hasRepeater, isConditionWrapperV2, yesNoListId, yesNoListName } from '@defra/forms-model';
|
|
1
|
+
import { ComponentType, ConditionsModel, ControllerPath, ControllerType, Engine, SchemaVersion, convertConditionWrapperFromV2, formDefinitionSchema, formDefinitionV2Schema, generateConditionAlias, hasComponents, hasRepeater, isConditionWrapperV2, yesNoListId, yesNoListName } from '@defra/forms-model';
|
|
2
2
|
import { add, format } from 'date-fns';
|
|
3
3
|
import { Parser } from 'expr-eval';
|
|
4
4
|
import joi from 'joi';
|
|
@@ -172,10 +172,11 @@ export class FormModel {
|
|
|
172
172
|
const context = {
|
|
173
173
|
...evaluationState
|
|
174
174
|
};
|
|
175
|
-
for (const
|
|
176
|
-
|
|
175
|
+
for (const conditionId in conditions) {
|
|
176
|
+
const alias = generateConditionAlias(conditionId);
|
|
177
|
+
Object.defineProperty(context, alias, {
|
|
177
178
|
get() {
|
|
178
|
-
return conditions[
|
|
179
|
+
return conditions[conditionId]?.fn(evaluationState);
|
|
179
180
|
}
|
|
180
181
|
});
|
|
181
182
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormModel.js","names":["ComponentType","ConditionsModel","ControllerPath","ControllerType","Engine","SchemaVersion","convertConditionWrapperFromV2","formDefinitionSchema","formDefinitionV2Schema","hasComponents","hasRepeater","isConditionWrapperV2","yesNoListId","yesNoListName","add","format","Parser","joi","hasListFormField","todayAsDateOnly","findPage","getError","getPage","setPageTitles","createPage","validationOptions","opts","defaultServices","FormAction","merge","FormModel","engine","schemaVersion","def","lists","sections","name","values","basePath","conditions","pages","services","controllers","pageDefMap","listDefMap","listDefIdMap","componentDefMap","componentDefIdMap","pageMap","componentMap","constructor","options","schema","V2","result","validate","abortEarly","error","structuredClone","value","push","id","V1","title","type","items","text","Map","map","page","path","list","filter","flatMap","components","component","forEach","conditionDef","condition","makeCondition","pageDef","some","controller","Status","collection","makeFilteredSchema","relevantPages","object","required","concat","stateSchema","parser","operators","logical","Object","assign","functions","dateForComparison","timePeriod","timeUnit","displayName","expr","toConditionExpression","fn","evaluationState","ctx","toConditionContext","evaluate","context","key","defineProperty","get","from","parse","toExpression","getList","nameOrId","find","getFormContext","request","state","errors","query","currentPath","startPath","getStartPath","isForceAccess","relevantState","payload","getFormDataFromState","paths","data","referenceNumber","getReferenceNumber","validateFormPayload","nextPage","initialiseContext","assignEvaluationState","assignRelevantState","pageStateIsInvalid","getNextPath","validateFormState","assignPaths","keys","getContextValueFromState","listFields","fields","field","undefined","YesNoField","hasOptionalItems","item","length","fieldStateIsInvalid","validValues","fieldState","getFormValueFromState","isInvalid","isArray","Array","every","includes","href","getComponentById","componentId","getListById","listId","getConditionById","conditionId","action","getFormParams","Validate","update","CheckboxesField","formState","getStateFromValidForm","previousPages","relevantPage","model","stripUnknown","errorsState","details","$$__referenceNumber","Error"],"sources":["../../../../../src/server/plugins/engine/models/FormModel.ts"],"sourcesContent":["import {\n ComponentType,\n ConditionsModel,\n ControllerPath,\n ControllerType,\n Engine,\n SchemaVersion,\n convertConditionWrapperFromV2,\n formDefinitionSchema,\n formDefinitionV2Schema,\n hasComponents,\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 FormDefinition,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { add, format } from 'date-fns'\nimport { Parser, type Value } from 'expr-eval'\nimport joi from 'joi'\n\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.js'\nimport { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'\nimport {\n findPage,\n getError,\n getPage,\n setPageTitles\n} from '~/src/server/plugins/engine/helpers.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.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 values: FormDefinition\n basePath: string\n conditions: Partial<Record<string, ExecutableCondition>>\n pages: PageControllerClass[]\n services: Services\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: { basePath: string },\n services: Services = defaultServices,\n controllers?: Record<string, typeof PageController>\n ) {\n let schema = formDefinitionSchema\n\n if (def.schema === SchemaVersion.V2) {\n schema = formDefinitionV2Schema\n }\n\n const result = schema.validate(def, { abortEarly: false })\n\n if (result.error) {\n throw 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 // Add default lists\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: 'Yes',\n value: true\n },\n {\n id: 'f39000eb-c51b-4019-8f82-bbda0423f04d',\n text: 'No',\n value: false\n }\n ]\n })\n\n // Fix up page titles\n setPageTitles(def)\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.values = result.value\n this.basePath = options.basePath\n this.conditions = {}\n this.services = services\n this.controllers = controllers\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: 'Form submitted',\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 /**\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 const expr = this.toConditionExpression(value, parser)\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 key in conditions) {\n Object.defineProperty(context, key, {\n get() {\n return conditions[key]?.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 /**\n * Form context for the current page\n */\n getFormContext(\n request: FormContextRequest,\n state: FormState,\n errors?: FormSubmissionError[]\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 }\n\n // Validate current page\n context = validateFormPayload(request, page, context)\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) ||\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 // For the V2 engine, we need to initialise `evaluationState` to null\n // for all keys. This is because the current condition evaluation\n // library (eval-expr) will throw if an expression uses a key that is undefined.\n if (this.engine === Engine.V2) {\n for (const page of this.pages) {\n for (const key of page.keys) {\n context.evaluationState[key] = null\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(context: FormContext, page: PageControllerClass) {\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)\n }\n }\n }\n }\n\n private fieldStateIsInvalid(\n context: FormContext,\n field: ListFormComponent,\n list: List\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 =\n 'Options are different because you changed a previous answer'\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/**\n * Validate current page only\n */\nfunction validateFormPayload(\n request: FormContextRequest,\n page: PageControllerClass,\n context: FormContext\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 (!request.payload || action !== FormAction.Validate) {\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,\n ...update\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(getError)\n return { ...context, errors: errors.concat(errorsState) }\n }\n\n return context\n}\n\nfunction getReferenceNumber(state: FormState): 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","ConditionsModel","ControllerPath","ControllerType","Engine","SchemaVersion","convertConditionWrapperFromV2","formDefinitionSchema","formDefinitionV2Schema","generateConditionAlias","hasComponents","hasRepeater","isConditionWrapperV2","yesNoListId","yesNoListName","add","format","Parser","joi","hasListFormField","todayAsDateOnly","findPage","getError","getPage","setPageTitles","createPage","validationOptions","opts","defaultServices","FormAction","merge","FormModel","engine","schemaVersion","def","lists","sections","name","values","basePath","conditions","pages","services","controllers","pageDefMap","listDefMap","listDefIdMap","componentDefMap","componentDefIdMap","pageMap","componentMap","constructor","options","schema","V2","result","validate","abortEarly","error","structuredClone","value","push","id","V1","title","type","items","text","Map","map","page","path","list","filter","flatMap","components","component","forEach","conditionDef","condition","makeCondition","pageDef","some","controller","Status","collection","makeFilteredSchema","relevantPages","object","required","concat","stateSchema","parser","operators","logical","Object","assign","functions","dateForComparison","timePeriod","timeUnit","displayName","expr","toConditionExpression","fn","evaluationState","ctx","toConditionContext","evaluate","context","conditionId","alias","defineProperty","get","from","parse","toExpression","getList","nameOrId","find","getFormContext","request","state","errors","query","currentPath","startPath","getStartPath","isForceAccess","relevantState","payload","getFormDataFromState","paths","data","referenceNumber","getReferenceNumber","validateFormPayload","nextPage","initialiseContext","assignEvaluationState","assignRelevantState","pageStateIsInvalid","getNextPath","validateFormState","assignPaths","key","keys","getContextValueFromState","listFields","fields","field","undefined","YesNoField","hasOptionalItems","item","length","fieldStateIsInvalid","validValues","fieldState","getFormValueFromState","isInvalid","isArray","Array","every","includes","href","getComponentById","componentId","getListById","listId","getConditionById","action","getFormParams","Validate","update","CheckboxesField","formState","getStateFromValidForm","previousPages","relevantPage","model","stripUnknown","errorsState","details","$$__referenceNumber","Error"],"sources":["../../../../../src/server/plugins/engine/models/FormModel.ts"],"sourcesContent":["import {\n ComponentType,\n ConditionsModel,\n ControllerPath,\n ControllerType,\n Engine,\n SchemaVersion,\n convertConditionWrapperFromV2,\n formDefinitionSchema,\n formDefinitionV2Schema,\n generateConditionAlias,\n hasComponents,\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 FormDefinition,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { add, format } from 'date-fns'\nimport { Parser, type Value } from 'expr-eval'\nimport joi from 'joi'\n\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.js'\nimport { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'\nimport {\n findPage,\n getError,\n getPage,\n setPageTitles\n} from '~/src/server/plugins/engine/helpers.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.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 values: FormDefinition\n basePath: string\n conditions: Partial<Record<string, ExecutableCondition>>\n pages: PageControllerClass[]\n services: Services\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: { basePath: string },\n services: Services = defaultServices,\n controllers?: Record<string, typeof PageController>\n ) {\n let schema = formDefinitionSchema\n\n if (def.schema === SchemaVersion.V2) {\n schema = formDefinitionV2Schema\n }\n\n const result = schema.validate(def, { abortEarly: false })\n\n if (result.error) {\n throw 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 // Add default lists\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: 'Yes',\n value: true\n },\n {\n id: 'f39000eb-c51b-4019-8f82-bbda0423f04d',\n text: 'No',\n value: false\n }\n ]\n })\n\n // Fix up page titles\n setPageTitles(def)\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.values = result.value\n this.basePath = options.basePath\n this.conditions = {}\n this.services = services\n this.controllers = controllers\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: 'Form submitted',\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 /**\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 const expr = this.toConditionExpression(value, parser)\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 alias = generateConditionAlias(conditionId)\n\n Object.defineProperty(context, alias, {\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 /**\n * Form context for the current page\n */\n getFormContext(\n request: FormContextRequest,\n state: FormState,\n errors?: FormSubmissionError[]\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 }\n\n // Validate current page\n context = validateFormPayload(request, page, context)\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) ||\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 // For the V2 engine, we need to initialise `evaluationState` to null\n // for all keys. This is because the current condition evaluation\n // library (eval-expr) will throw if an expression uses a key that is undefined.\n if (this.engine === Engine.V2) {\n for (const page of this.pages) {\n for (const key of page.keys) {\n context.evaluationState[key] = null\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(context: FormContext, page: PageControllerClass) {\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)\n }\n }\n }\n }\n\n private fieldStateIsInvalid(\n context: FormContext,\n field: ListFormComponent,\n list: List\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 =\n 'Options are different because you changed a previous answer'\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/**\n * Validate current page only\n */\nfunction validateFormPayload(\n request: FormContextRequest,\n page: PageControllerClass,\n context: FormContext\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 (!request.payload || action !== FormAction.Validate) {\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,\n ...update\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(getError)\n return { ...context, errors: errors.concat(errorsState) }\n }\n\n return context\n}\n\nfunction getReferenceNumber(state: FormState): 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":[]}
|
|
@@ -249,6 +249,7 @@ export interface FeaturedFormPageViewModel extends FormPageViewModel {
|
|
|
249
249
|
proxyUrl: string | null;
|
|
250
250
|
}
|
|
251
251
|
export type PageViewModel = PageViewModelBase | ItemDeletePageViewModel | FormPageViewModel | RepeaterSummaryPageViewModel | FeaturedFormPageViewModel;
|
|
252
|
+
export type GlobalFunction = (value: unknown) => unknown;
|
|
252
253
|
export type FilterFunction = (value: unknown) => unknown;
|
|
253
254
|
export interface ErrorMessageTemplate {
|
|
254
255
|
type: string;
|
|
@@ -265,6 +266,7 @@ export interface PluginOptions {
|
|
|
265
266
|
services?: Services;
|
|
266
267
|
controllers?: Record<string, typeof PageController>;
|
|
267
268
|
cacheName?: string;
|
|
269
|
+
globals?: Record<string, GlobalFunction>;
|
|
268
270
|
filters?: Record<string, FilterFunction>;
|
|
269
271
|
keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string;
|
|
270
272
|
sessionHydrator?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":["UploadStatus","FileStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { type PluginProperties, type Request } from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<FormRequest, 'app' | 'method' | 'params' | 'path' | 'query' | 'url'>\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport enum UploadStatus {\n initiated = 'initiated',\n pending = 'pending',\n ready = 'ready'\n}\n\nexport enum FileStatus {\n complete = 'complete',\n rejected = 'rejected',\n pending = 'pending'\n}\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: FormRequest | FormRequestPayload,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n filters?: Record<string, FilterFunction>\n keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState>\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n"],"mappings":"AAgCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAkGA,WAAYA,YAAY,0BAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAA,OAAZA,YAAY;AAAA;AAMxB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"types.js","names":["UploadStatus","FileStatus"],"sources":["../../../../src/server/plugins/engine/types.ts"],"sourcesContent":["import {\n type ComponentDef,\n type Event,\n type FormDefinition,\n type FormMetadata,\n type Item,\n type List,\n type Page\n} from '@defra/forms-model'\nimport { type PluginProperties, type Request } from '@hapi/hapi'\nimport { type JoiExpression, type ValidationErrorItem } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type Component } from '~/src/server/plugins/engine/components/helpers.js'\nimport {\n type BackLink,\n type ComponentText,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'\nimport { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'\nimport {\n type FormAction,\n type FormParams,\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type RequestOptions } from '~/src/server/services/httpService.js'\nimport { type Services } from '~/src/server/types.js'\n\n/**\n * Form submission state stores the following in Redis:\n * Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`\n * a) . e.g:\n * ```ts\n * {\n * _C9PRHmsgt: 'Ben',\n * WfLk9McjzX: 'Music',\n * IK7jkUFCBL: 'Royal Academy of Music'\n * }\n * ```\n *\n * b)\n * ```ts\n * {\n * checkBeforeYouStart: { ukPassport: true },\n * applicantDetails: {\n * numberOfApplicants: 1,\n * phoneNumber: '77777777',\n * emailAddress: 'aaa@aaa.com'\n * },\n * applicantOneDetails: {\n * firstName: 'a',\n * middleName: 'a',\n * lastName: 'a',\n * address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }\n * }\n * }\n * ```\n */\n\n/**\n * Form submission state\n */\nexport type FormSubmissionState = {\n upload?: Record<string, TempFileState>\n} & FormState\n\nexport interface FormSubmissionError\n extends Pick<ValidationErrorItem, 'context' | 'path'> {\n href: string // e.g: '#dateField__day'\n name: string // e.g: 'dateField__day'\n text: string // e.g: 'Date field must be a real date'\n}\n\nexport interface FormPayloadParams {\n action?: FormAction\n confirm?: true\n crumb?: string\n itemId?: string\n}\n\n/**\n * Form POST for question pages\n * (after Joi has converted value types)\n */\nexport type FormPayload = FormPayloadParams & Partial<Record<string, FormValue>>\n\nexport type FormValue =\n | Item['value']\n | Item['value'][]\n | UploadState\n | RepeatListState\n | undefined\n\nexport type FormState = Partial<Record<string, FormStateValue>>\nexport type FormStateValue = Exclude<FormValue, undefined> | null\n\nexport interface FormValidationResult<\n ValueType extends FormPayload | FormSubmissionState\n> {\n value: ValueType\n errors: FormSubmissionError[] | undefined\n}\n\nexport interface FormContext {\n /**\n * Evaluation form state only (filtered by visited paths),\n * with values formatted for condition evaluation using\n * {@link FormComponent.getContextValueFromState}\n */\n evaluationState: FormState\n\n /**\n * Relevant form state only (filtered by visited paths)\n */\n relevantState: FormState\n\n /**\n * Relevant pages only (filtered by visited paths)\n */\n relevantPages: PageControllerClass[]\n\n /**\n * Form submission payload (single page)\n */\n payload: FormPayload\n\n /**\n * Form submission state (entire form)\n */\n state: FormSubmissionState\n\n /**\n * Validation errors (entire form)\n */\n errors?: FormSubmissionError[]\n\n /**\n * Visited paths evaluated from form state\n */\n paths: string[]\n\n /**\n * Preview URL direct access is allowed\n */\n isForceAccess: boolean\n\n /**\n * Miscellaneous extra data from event responses\n */\n data: object\n\n pageDefMap: Map<string, Page>\n listDefMap: Map<string, List>\n componentDefMap: Map<string, ComponentDef>\n pageMap: Map<string, PageControllerClass>\n componentMap: Map<string, Component>\n referenceNumber: string\n}\n\nexport type FormContextRequest = (\n | {\n method: 'get'\n payload?: undefined\n }\n | {\n method: 'post'\n payload: FormPayload\n }\n | {\n method: FormRequest['method']\n payload?: object | undefined\n }\n) &\n Pick<FormRequest, 'app' | 'method' | 'params' | 'path' | 'query' | 'url'>\n\nexport interface UploadInitiateResponse {\n uploadId: string\n uploadUrl: string\n statusUrl: string\n}\n\nexport enum UploadStatus {\n initiated = 'initiated',\n pending = 'pending',\n ready = 'ready'\n}\n\nexport enum FileStatus {\n complete = 'complete',\n rejected = 'rejected',\n pending = 'pending'\n}\n\nexport type UploadState = FileState[]\n\nexport type FileUpload = {\n fileId: string\n filename: string\n contentLength: number\n} & (\n | {\n fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending\n errorMessage?: string\n }\n | {\n fileStatus: FileStatus.complete\n errorMessage?: undefined\n }\n)\n\nexport interface FileUploadMetadata {\n retrievalKey: string\n}\n\nexport type UploadStatusResponse =\n | {\n uploadStatus: UploadStatus.initiated\n metadata: FileUploadMetadata\n form: { file?: undefined }\n }\n | {\n uploadStatus: UploadStatus.pending | UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles?: number\n }\n | {\n uploadStatus: UploadStatus.ready\n metadata: FileUploadMetadata\n form: { file: FileUpload }\n numberOfRejectedFiles: 0\n }\n\nexport type UploadStatusFileResponse = Exclude<\n UploadStatusResponse,\n { uploadStatus: UploadStatus.initiated }\n>\n\nexport interface FileState {\n uploadId: string\n status: UploadStatusFileResponse\n}\n\nexport interface TempFileState {\n upload?: UploadInitiateResponse\n files: UploadState\n}\n\nexport interface RepeatItemState extends FormPayload {\n itemId: string\n}\n\nexport type RepeatListState = RepeatItemState[]\n\nexport interface CheckAnswers {\n title?: ComponentText\n summaryList: SummaryList\n}\n\nexport interface SummaryList {\n classes?: string\n rows: SummaryListRow[]\n}\n\nexport interface SummaryListRow {\n key: ComponentText\n value: ComponentText\n actions?: { items: SummaryListAction[] }\n}\n\nexport type SummaryListAction = ComponentText & {\n href: string\n visuallyHiddenText: string\n}\n\nexport interface PageViewModelBase extends Partial<ViewContext> {\n page: PageController\n name?: string\n pageTitle: string\n sectionTitle?: string\n showTitle: boolean\n isStartPage: boolean\n backLink?: BackLink\n feedbackLink?: string\n serviceUrl: string\n phaseTag?: string\n}\n\nexport interface ItemDeletePageViewModel extends PageViewModelBase {\n context: FormContext\n itemTitle: string\n confirmation?: ComponentText\n buttonConfirm: ComponentText\n buttonCancel: ComponentText\n}\n\nexport interface FormPageViewModel extends PageViewModelBase {\n components: ComponentViewModel[]\n context: FormContext\n errors?: FormSubmissionError[]\n hasMissingNotificationEmail?: boolean\n}\n\nexport interface RepeaterSummaryPageViewModel extends PageViewModelBase {\n context: FormContext\n errors?: FormSubmissionError[]\n checkAnswers: CheckAnswers[]\n repeatTitle: string\n}\n\nexport interface FeaturedFormPageViewModel extends FormPageViewModel {\n formAction?: string\n formComponent: ComponentViewModel\n componentsBefore: ComponentViewModel[]\n uploadId: string | undefined\n proxyUrl: string | null\n}\n\nexport type PageViewModel =\n | PageViewModelBase\n | ItemDeletePageViewModel\n | FormPageViewModel\n | RepeaterSummaryPageViewModel\n | FeaturedFormPageViewModel\n\nexport type GlobalFunction = (value: unknown) => unknown\nexport type FilterFunction = (value: unknown) => unknown\nexport interface ErrorMessageTemplate {\n type: string\n template: JoiExpression\n}\n\nexport interface ErrorMessageTemplateList {\n baseErrors: ErrorMessageTemplate[]\n advancedSettingsErrors: ErrorMessageTemplate[]\n}\n\nexport type PreparePageEventRequestOptions = (\n options: RequestOptions,\n event: Event,\n page: PageControllerClass,\n context: FormContext\n) => void\n\nexport type OnRequestCallback = (\n request: FormRequest | FormRequestPayload,\n params: FormParams,\n definition: FormDefinition,\n metadata: FormMetadata\n) => void\n\nexport interface PluginOptions {\n model?: FormModel\n services?: Services\n controllers?: Record<string, typeof PageController>\n cacheName?: string\n globals?: Record<string, GlobalFunction>\n filters?: Record<string, FilterFunction>\n keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState>\n pluginPath?: string\n nunjucks: {\n baseLayoutPath: string\n paths: string[]\n }\n viewContext: PluginProperties['forms-engine-plugin']['viewContext']\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n baseUrl: string // base URL of the application, protocol and hostname e.g. \"https://myapp.com\"\n}\n"],"mappings":"AAgCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAmBA;AACA;AACA;AACA;;AAkGA,WAAYA,YAAY,0BAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAZA,YAAY;EAAA,OAAZA,YAAY;AAAA;AAMxB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/forms-engine-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Defra forms engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"license": "SEE LICENSE IN LICENSE",
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@defra/forms-model": "^3.0.
|
|
66
|
+
"@defra/forms-model": "^3.0.506",
|
|
67
67
|
"@defra/hapi-tracing": "^1.0.0",
|
|
68
68
|
"@elastic/ecs-pino-format": "^1.5.0",
|
|
69
69
|
"@hapi/boom": "^10.0.1",
|
package/src/server/index.test.ts
CHANGED
|
@@ -609,7 +609,7 @@ describe('prepareEnvironment', () => {
|
|
|
609
609
|
)
|
|
610
610
|
})
|
|
611
611
|
|
|
612
|
-
test('registers
|
|
612
|
+
test('registers base globals', () => {
|
|
613
613
|
const expectedGlobals = [
|
|
614
614
|
'checkComponentTemplates',
|
|
615
615
|
'checkErrorTemplates',
|
|
@@ -624,4 +624,18 @@ describe('prepareEnvironment', () => {
|
|
|
624
624
|
expect(mockEnv.addGlobal).toHaveBeenCalledWith(name, expect.any(Function))
|
|
625
625
|
})
|
|
626
626
|
})
|
|
627
|
+
|
|
628
|
+
test('registers additional globals', () => {
|
|
629
|
+
prepareNunjucksEnvironment(mockEnv, {
|
|
630
|
+
...mockPluginOptions,
|
|
631
|
+
globals: {
|
|
632
|
+
customGlobal: (value) => value
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
expect(mockEnv.addGlobal).toHaveBeenCalledWith(
|
|
637
|
+
'customGlobal',
|
|
638
|
+
expect.any(Function)
|
|
639
|
+
)
|
|
640
|
+
})
|
|
627
641
|
})
|
|
@@ -48,6 +48,13 @@ export const prepareNunjucksEnvironment = function (
|
|
|
48
48
|
engine.registerFilter(name, filter)
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
// Apply any additional globals to nunjucks engines
|
|
53
|
+
if (pluginOptions.globals) {
|
|
54
|
+
Object.entries(pluginOptions.globals).forEach(([name, fn]) => {
|
|
55
|
+
env.addGlobal(name, fn)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
export default plugin
|
|
@@ -12,6 +12,7 @@ import definition from '~/test/form/definitions/conditions-escaping.js'
|
|
|
12
12
|
import conditionsListDefinition from '~/test/form/definitions/conditions-list.js'
|
|
13
13
|
import relativeDatesDefinition from '~/test/form/definitions/conditions-relative-dates-v2.js'
|
|
14
14
|
import fieldsRequiredDefinition from '~/test/form/definitions/fields-required.js'
|
|
15
|
+
import joinedConditionsDefinition from '~/test/form/definitions/joined-conditions-test.js'
|
|
15
16
|
|
|
16
17
|
jest.mock('~/src/server/plugins/engine/date-helper.ts')
|
|
17
18
|
|
|
@@ -42,8 +43,6 @@ describe('FormModel', () => {
|
|
|
42
43
|
})
|
|
43
44
|
|
|
44
45
|
it('Gets a list by ID', () => {
|
|
45
|
-
jest.mock('@defra/forms-model')
|
|
46
|
-
|
|
47
46
|
const definitionWithLists: FormDefinition = {
|
|
48
47
|
...definitionV2,
|
|
49
48
|
lists: [
|
|
@@ -90,8 +89,6 @@ describe('FormModel', () => {
|
|
|
90
89
|
})
|
|
91
90
|
|
|
92
91
|
it('Gets a component by ID', () => {
|
|
93
|
-
jest.mock('@defra/forms-model')
|
|
94
|
-
|
|
95
92
|
formDefinitionV2Schema.validate = jest
|
|
96
93
|
.fn()
|
|
97
94
|
.mockReturnValue({ value: definitionV2 })
|
|
@@ -105,7 +102,6 @@ describe('FormModel', () => {
|
|
|
105
102
|
})
|
|
106
103
|
|
|
107
104
|
it('gets a condition by its ID', () => {
|
|
108
|
-
jest.mock('@defra/forms-model')
|
|
109
105
|
formDefinitionV2Schema.validate = jest
|
|
110
106
|
.fn()
|
|
111
107
|
.mockReturnValue({ value: definitionV2 })
|
|
@@ -117,8 +113,6 @@ describe('FormModel', () => {
|
|
|
117
113
|
})
|
|
118
114
|
|
|
119
115
|
it('throws an error if schema validation fails', () => {
|
|
120
|
-
jest.mock('@defra/forms-model')
|
|
121
|
-
|
|
122
116
|
formDefinitionV2Schema.validate = jest.fn().mockReturnValueOnce({
|
|
123
117
|
error: 'Validation error'
|
|
124
118
|
})
|
|
@@ -304,7 +298,6 @@ describe('FormModel', () => {
|
|
|
304
298
|
|
|
305
299
|
describe('makeCondition', () => {
|
|
306
300
|
test('relative date condition', () => {
|
|
307
|
-
jest.mock('@defra/forms-model')
|
|
308
301
|
formDefinitionV2Schema.validate = jest
|
|
309
302
|
.fn()
|
|
310
303
|
.mockReturnValue({ value: relativeDatesDefinition })
|
|
@@ -347,3 +340,134 @@ describe('FormModel', () => {
|
|
|
347
340
|
})
|
|
348
341
|
})
|
|
349
342
|
})
|
|
343
|
+
|
|
344
|
+
describe('FormModel - Joined Conditions', () => {
|
|
345
|
+
it('should handle joined conditions correctly', () => {
|
|
346
|
+
formDefinitionV2Schema.validate = jest
|
|
347
|
+
.fn()
|
|
348
|
+
.mockReturnValue({ value: joinedConditionsDefinition })
|
|
349
|
+
|
|
350
|
+
const model = new FormModel(joinedConditionsDefinition, {
|
|
351
|
+
basePath: 'test'
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
expect(model.conditions).toBeDefined()
|
|
355
|
+
expect(Object.keys(model.conditions)).toHaveLength(3)
|
|
356
|
+
|
|
357
|
+
const joinedCondition =
|
|
358
|
+
model.conditions['db43c6bc-9ce6-478b-8345-4fff5eff2ba3']
|
|
359
|
+
expect(joinedCondition).toBeDefined()
|
|
360
|
+
expect(joinedCondition?.displayName).toBe('joined condition')
|
|
361
|
+
|
|
362
|
+
const stateAllTrue = { fsZNJr: 'Bob', DaBGpS: true }
|
|
363
|
+
expect(joinedCondition?.fn(stateAllTrue)).toBe(true)
|
|
364
|
+
|
|
365
|
+
const statePartialTrue = { fsZNJr: 'Alice', DaBGpS: true }
|
|
366
|
+
expect(joinedCondition?.fn(statePartialTrue)).toBe(false)
|
|
367
|
+
|
|
368
|
+
const stateFalse = { fsZNJr: 'Alice', DaBGpS: false }
|
|
369
|
+
expect(joinedCondition?.fn(stateFalse)).toBe(false)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('should evaluate page conditions using joined conditions', () => {
|
|
373
|
+
formDefinitionV2Schema.validate = jest
|
|
374
|
+
.fn()
|
|
375
|
+
.mockReturnValue({ value: joinedConditionsDefinition })
|
|
376
|
+
|
|
377
|
+
const model = new FormModel(joinedConditionsDefinition, {
|
|
378
|
+
basePath: 'test'
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const joinedConditionPage = model.pages.find(
|
|
382
|
+
(page) => page.path === '/joined-condition-page'
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
expect(joinedConditionPage?.condition).toBeDefined()
|
|
386
|
+
|
|
387
|
+
const trueState = { fsZNJr: 'Bob', DaBGpS: true }
|
|
388
|
+
expect(joinedConditionPage?.condition?.fn(trueState)).toBe(true)
|
|
389
|
+
|
|
390
|
+
const falseState = { fsZNJr: 'Bob', DaBGpS: false }
|
|
391
|
+
expect(joinedConditionPage?.condition?.fn(falseState)).toBe(false)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
describe('generateConditionAlias', () => {
|
|
395
|
+
it('should generate valid JavaScript identifiers from condition IDs', () => {
|
|
396
|
+
formDefinitionV2Schema.validate = jest
|
|
397
|
+
.fn()
|
|
398
|
+
.mockReturnValue({ value: joinedConditionsDefinition })
|
|
399
|
+
|
|
400
|
+
const model = new FormModel(joinedConditionsDefinition, {
|
|
401
|
+
basePath: 'test'
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const evaluationState = { fsZNJr: 'Bob', DaBGpS: true }
|
|
405
|
+
|
|
406
|
+
const context = model.toConditionContext(
|
|
407
|
+
evaluationState,
|
|
408
|
+
model.conditions
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
expect(context).toHaveProperty('cond_d15aff7a622440a28e5f51a5af2f7910')
|
|
412
|
+
expect(context).toHaveProperty('cond_d1f9fcc7f09847e79d314f5ee57ba985')
|
|
413
|
+
expect(context).toHaveProperty('cond_db43c6bc9ce6478b83454fff5eff2ba3')
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
describe('toConditionExpression', () => {
|
|
418
|
+
it('should handle V2 engine with display name replacement', () => {
|
|
419
|
+
formDefinitionV2Schema.validate = jest
|
|
420
|
+
.fn()
|
|
421
|
+
.mockReturnValue({ value: joinedConditionsDefinition })
|
|
422
|
+
|
|
423
|
+
const model = new FormModel(joinedConditionsDefinition, {
|
|
424
|
+
basePath: 'test'
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
const joinedCondition =
|
|
428
|
+
model.conditions['db43c6bc-9ce6-478b-8345-4fff5eff2ba3']
|
|
429
|
+
expect(joinedCondition).toBeDefined()
|
|
430
|
+
|
|
431
|
+
const stateTrue = { fsZNJr: 'Bob', DaBGpS: true }
|
|
432
|
+
const stateFalse = { fsZNJr: 'Alice', DaBGpS: false }
|
|
433
|
+
|
|
434
|
+
expect(joinedCondition?.fn(stateTrue)).toBe(true)
|
|
435
|
+
expect(joinedCondition?.fn(stateFalse)).toBe(false)
|
|
436
|
+
|
|
437
|
+
expect(joinedCondition?.expr).toBeDefined()
|
|
438
|
+
expect(typeof joinedCondition?.expr.evaluate).toBe('function')
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('should handle V1 engine without display name replacement', () => {
|
|
442
|
+
const model = new FormModel(definition, { basePath: 'test' })
|
|
443
|
+
|
|
444
|
+
const condition = model.conditions.ZCXeMz
|
|
445
|
+
expect(condition).toBeDefined()
|
|
446
|
+
expect(condition?.expr).toBeDefined()
|
|
447
|
+
|
|
448
|
+
const testState = { NIJphU: "ap'ostrophe's", iraEpG: "shouldn't've" }
|
|
449
|
+
expect(condition?.fn(testState)).toBe(true)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('should handle conditions without display names', () => {
|
|
453
|
+
const definitionWithoutDisplayName = {
|
|
454
|
+
...joinedConditionsDefinition,
|
|
455
|
+
conditions: joinedConditionsDefinition.conditions.map((condition) => ({
|
|
456
|
+
...condition,
|
|
457
|
+
displayName: condition.displayName || 'fallback'
|
|
458
|
+
}))
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
formDefinitionV2Schema.validate = jest
|
|
462
|
+
.fn()
|
|
463
|
+
.mockReturnValue({ value: definitionWithoutDisplayName })
|
|
464
|
+
|
|
465
|
+
const model = new FormModel(definitionWithoutDisplayName, {
|
|
466
|
+
basePath: 'test'
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
expect(model.conditions).toBeDefined()
|
|
470
|
+
expect(Object.keys(model.conditions)).toHaveLength(3)
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
})
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
convertConditionWrapperFromV2,
|
|
9
9
|
formDefinitionSchema,
|
|
10
10
|
formDefinitionV2Schema,
|
|
11
|
+
generateConditionAlias,
|
|
11
12
|
hasComponents,
|
|
12
13
|
hasRepeater,
|
|
13
14
|
isConditionWrapperV2,
|
|
@@ -276,10 +277,12 @@ export class FormModel {
|
|
|
276
277
|
) {
|
|
277
278
|
const context = { ...evaluationState }
|
|
278
279
|
|
|
279
|
-
for (const
|
|
280
|
-
|
|
280
|
+
for (const conditionId in conditions) {
|
|
281
|
+
const alias = generateConditionAlias(conditionId)
|
|
282
|
+
|
|
283
|
+
Object.defineProperty(context, alias, {
|
|
281
284
|
get() {
|
|
282
|
-
return conditions[
|
|
285
|
+
return conditions[conditionId]?.fn(evaluationState)
|
|
283
286
|
}
|
|
284
287
|
})
|
|
285
288
|
}
|
|
@@ -327,6 +327,7 @@ export type PageViewModel =
|
|
|
327
327
|
| RepeaterSummaryPageViewModel
|
|
328
328
|
| FeaturedFormPageViewModel
|
|
329
329
|
|
|
330
|
+
export type GlobalFunction = (value: unknown) => unknown
|
|
330
331
|
export type FilterFunction = (value: unknown) => unknown
|
|
331
332
|
export interface ErrorMessageTemplate {
|
|
332
333
|
type: string
|
|
@@ -357,6 +358,7 @@ export interface PluginOptions {
|
|
|
357
358
|
services?: Services
|
|
358
359
|
controllers?: Record<string, typeof PageController>
|
|
359
360
|
cacheName?: string
|
|
361
|
+
globals?: Record<string, GlobalFunction>
|
|
360
362
|
filters?: Record<string, FilterFunction>
|
|
361
363
|
keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string
|
|
362
364
|
sessionHydrator?: (
|