@defra/forms-engine-plugin 1.0.0 → 1.0.2

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 (47) hide show
  1. package/.server/server/plugins/engine/README.md +56 -0
  2. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +2 -1
  3. package/.server/server/plugins/engine/configureEnginePlugin.js +1 -1
  4. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  5. package/.server/server/plugins/engine/plugin.d.ts +2 -27
  6. package/.server/server/plugins/engine/plugin.js +17 -594
  7. package/.server/server/plugins/engine/plugin.js.map +1 -1
  8. package/.server/server/plugins/engine/registrationOptions.d.ts +1 -0
  9. package/.server/server/plugins/engine/registrationOptions.js +2 -0
  10. package/.server/server/plugins/engine/registrationOptions.js.map +1 -0
  11. package/.server/server/plugins/engine/routes/file-upload.d.ts +4 -0
  12. package/.server/server/plugins/engine/routes/file-upload.js +41 -0
  13. package/.server/server/plugins/engine/routes/file-upload.js.map +1 -0
  14. package/.server/server/plugins/engine/routes/index.d.ts +7 -0
  15. package/.server/server/plugins/engine/routes/index.js +141 -0
  16. package/.server/server/plugins/engine/routes/index.js.map +1 -0
  17. package/.server/server/plugins/engine/routes/questions.d.ts +3 -0
  18. package/.server/server/plugins/engine/routes/questions.js +168 -0
  19. package/.server/server/plugins/engine/routes/questions.js.map +1 -0
  20. package/.server/server/plugins/engine/routes/repeaters/item-delete.d.ts +3 -0
  21. package/.server/server/plugins/engine/routes/repeaters/item-delete.js +106 -0
  22. package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -0
  23. package/.server/server/plugins/engine/routes/repeaters/summary.d.ts +3 -0
  24. package/.server/server/plugins/engine/routes/repeaters/summary.js +98 -0
  25. package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -0
  26. package/.server/server/plugins/engine/types.d.ts +19 -1
  27. package/.server/server/plugins/engine/types.js.map +1 -1
  28. package/.server/server/plugins/engine/vision.d.ts +12 -0
  29. package/.server/server/plugins/engine/vision.js +55 -0
  30. package/.server/server/plugins/engine/vision.js.map +1 -0
  31. package/.server/server/services/cacheService.d.ts +12 -3
  32. package/.server/server/services/cacheService.js +35 -8
  33. package/.server/server/services/cacheService.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/server/plugins/engine/README.md +56 -0
  36. package/src/server/plugins/engine/configureEnginePlugin.ts +3 -5
  37. package/src/server/plugins/engine/plugin.ts +35 -765
  38. package/src/server/plugins/engine/registrationOptions.ts +0 -0
  39. package/src/server/plugins/engine/routes/file-upload.ts +54 -0
  40. package/src/server/plugins/engine/routes/index.ts +187 -0
  41. package/src/server/plugins/engine/routes/questions.ts +208 -0
  42. package/src/server/plugins/engine/routes/repeaters/item-delete.ts +157 -0
  43. package/src/server/plugins/engine/routes/repeaters/summary.ts +137 -0
  44. package/src/server/plugins/engine/types.ts +26 -1
  45. package/src/server/plugins/engine/vision.ts +95 -0
  46. package/src/server/services/cacheService.test.ts +98 -2
  47. package/src/server/services/cacheService.ts +57 -8
@@ -85,3 +85,59 @@ There are a number of `LiquidJS` filters available to you from within the templa
85
85
  }
86
86
  ]
87
87
  ```
88
+
89
+ ## Session Rehydration
90
+
91
+ To support Save and Return functionality, this application now supports session rehydration. This allows user session state to be recovered across browser sessions or devices — even after the in-memory Redis session has expired.
92
+
93
+ ### How it works
94
+
95
+ To support session rehydration from a backend (e.g. for Save & Return), the consuming application must provide two functions when registering the DXT engine plugin:
96
+
97
+ ```
98
+ export interface PluginOptions {
99
+ ...
100
+ keyGenerator?: (request) => string
101
+ sessionHydrator?: (request) => Promise<FormSubmissionState | null>
102
+ ...
103
+ }
104
+
105
+ ```
106
+
107
+ 1. `keyGenerator(request)`
108
+
109
+ This generates a stable and consistent cache key used to store and retrieve user state. It should return a string based on persistent identifiers such as userId, businessId, and grantId — i.e., something like:
110
+
111
+ ```
112
+ const keyGenerator = request => {
113
+ const { userId, businessId, grantId } = request.app.userContext
114
+ return `${userId}:${businessId}:${grantId}`
115
+ }
116
+ ```
117
+
118
+ 2. `sessionHydrator(request, key)`
119
+
120
+ This function is called when no session state is found in Redis. It should fetch saved state (e.g., from an API) using the provided key and return it in the same structure expected by the form engine:
121
+
122
+ ```
123
+ const sessionHydrator = async (request, key) => {
124
+ const response = await fetch(`https://backend.api/state/${key}`)
125
+ if (!response.ok) return null
126
+ return await response.json() // Must match form engine state shape
127
+ }
128
+ ```
129
+
130
+ #### Session flow
131
+
132
+ - When user resumes a journey and Redis session data is missing or expired, DXT will use `keyGenerator` and `sessionHydrator` to fetch the saved state from an external API (e.g. `/state` endpoint).
133
+ - The fetched state is written back into Redis and used to continue the user journey.
134
+ - The rehydrated state must include enough information to satisfy schema validation on the current or next page.
135
+ - To properly resume a session, users should be redirected to the `/summary` page. This ensures the UI has all required answers preloaded and avoids invalid transitions from deep links.
136
+
137
+ ### Additional notes
138
+
139
+ Flash messaging and other ephemeral session data still rely on yar.id.
140
+
141
+ If the restored state does not satisfy the schema for the current page, the user will be redirected to the first incomplete step.
142
+
143
+ In development, a mock identity and /state response can be used to simulate a persisted session.
@@ -1,5 +1,6 @@
1
1
  import { type FormDefinition } from '@defra/forms-model';
2
- import { plugin, type PluginOptions } from '~/src/server/plugins/engine/plugin.js';
2
+ import { plugin } from '~/src/server/plugins/engine/plugin.js';
3
+ import { type PluginOptions } from '~/src/server/plugins/engine/types.js';
3
4
  import { type RouteConfig } from '~/src/server/types.js';
4
5
  export declare const configureEnginePlugin: ({ formFileName, formFilePath, services, controllers }?: RouteConfig) => Promise<{
5
6
  plugin: typeof plugin;
@@ -2,9 +2,9 @@ import { join, parse } from 'node:path';
2
2
  import { FORM_PREFIX } from "../../constants.js";
3
3
  import { FormModel } from "./models/FormModel.js";
4
4
  import { plugin } from "./plugin.js";
5
- import { findPackageRoot } from "./plugin.js";
6
5
  import * as defaultServices from "./services/index.js";
7
6
  import { formsService } from "./services/localFormsService.js";
7
+ import { findPackageRoot } from "./vision.js";
8
8
  import { devtoolContext } from "../nunjucks/context.js";
9
9
  export const configureEnginePlugin = async ({
10
10
  formFileName,
@@ -1 +1 @@
1
- {"version":3,"file":"configureEnginePlugin.js","names":["join","parse","FORM_PREFIX","FormModel","plugin","findPackageRoot","defaultServices","formsService","devtoolContext","configureEnginePlugin","formFileName","formFilePath","services","controllers","model","definition","getForm","name","initialBasePath","basePath","options","cacheName","nunjucks","baseLayoutPath","paths","viewContext","importPath","ext","attributes","type","formImport","with","default"],"sources":["../../../../src/server/plugins/engine/configureEnginePlugin.ts"],"sourcesContent":["import { join, parse } from 'node:path'\n\nimport { type FormDefinition } from '@defra/forms-model'\n\nimport { FORM_PREFIX } from '~/src/server/constants.js'\nimport { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport {\n plugin,\n type PluginOptions\n} from '~/src/server/plugins/engine/plugin.js'\nimport { findPackageRoot } from '~/src/server/plugins/engine/plugin.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport { formsService } from '~/src/server/plugins/engine/services/localFormsService.js'\nimport { devtoolContext } from '~/src/server/plugins/nunjucks/context.js'\nimport { type RouteConfig } from '~/src/server/types.js'\n\nexport const configureEnginePlugin = async ({\n formFileName,\n formFilePath,\n services,\n controllers\n}: RouteConfig = {}): Promise<{\n plugin: typeof plugin\n options: PluginOptions\n}> => {\n let model: FormModel | undefined\n\n if (formFileName && formFilePath) {\n const definition = await getForm(join(formFilePath, formFileName))\n const { name } = parse(formFileName)\n\n const initialBasePath = `${FORM_PREFIX}${name}`\n\n model = new FormModel(\n definition,\n { basePath: initialBasePath },\n services,\n controllers\n )\n }\n\n return {\n plugin,\n options: {\n model,\n services: services ?? {\n // services for testing, else use the disk loader option for running this service locally\n ...defaultServices,\n formsService: await formsService()\n },\n controllers,\n cacheName: 'session',\n nunjucks: {\n baseLayoutPath: 'dxt-devtool-baselayout.html',\n paths: [join(findPackageRoot(), 'src/server/devserver')] // custom layout to make it really clear this is not the same as the runner\n },\n viewContext: devtoolContext\n }\n }\n}\n\nexport async function getForm(importPath: string) {\n const { ext } = parse(importPath)\n\n const attributes: ImportAttributes = {\n type: ext === '.json' ? 'json' : 'module'\n }\n\n const formImport = import(importPath, { with: attributes }) as Promise<{\n default: FormDefinition\n }>\n\n const { default: definition } = await formImport\n return definition\n}\n"],"mappings":"AAAA,SAASA,IAAI,EAAEC,KAAK,QAAQ,WAAW;AAIvC,SAASC,WAAW;AACpB,SAASC,SAAS;AAClB,SACEC,MAAM;AAGR,SAASC,eAAe;AACxB,OAAO,KAAKC,eAAe;AAC3B,SAASC,YAAY;AACrB,SAASC,cAAc;AAGvB,OAAO,MAAMC,qBAAqB,GAAG,MAAAA,CAAO;EAC1CC,YAAY;EACZC,YAAY;EACZC,QAAQ;EACRC;AACW,CAAC,GAAG,CAAC,CAAC,KAGb;EACJ,IAAIC,KAA4B;EAEhC,IAAIJ,YAAY,IAAIC,YAAY,EAAE;IAChC,MAAMI,UAAU,GAAG,MAAMC,OAAO,CAAChB,IAAI,CAACW,YAAY,EAAED,YAAY,CAAC,CAAC;IAClE,MAAM;MAAEO;IAAK,CAAC,GAAGhB,KAAK,CAACS,YAAY,CAAC;IAEpC,MAAMQ,eAAe,GAAG,GAAGhB,WAAW,GAAGe,IAAI,EAAE;IAE/CH,KAAK,GAAG,IAAIX,SAAS,CACnBY,UAAU,EACV;MAAEI,QAAQ,EAAED;IAAgB,CAAC,EAC7BN,QAAQ,EACRC,WACF,CAAC;EACH;EAEA,OAAO;IACLT,MAAM;IACNgB,OAAO,EAAE;MACPN,KAAK;MACLF,QAAQ,EAAEA,QAAQ,IAAI;QACpB;QACA,GAAGN,eAAe;QAClBC,YAAY,EAAE,MAAMA,YAAY,CAAC;MACnC,CAAC;MACDM,WAAW;MACXQ,SAAS,EAAE,SAAS;MACpBC,QAAQ,EAAE;QACRC,cAAc,EAAE,6BAA6B;QAC7CC,KAAK,EAAE,CAACxB,IAAI,CAACK,eAAe,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC,CAAC;MAC3D,CAAC;MACDoB,WAAW,EAAEjB;IACf;EACF,CAAC;AACH,CAAC;AAED,OAAO,eAAeQ,OAAOA,CAACU,UAAkB,EAAE;EAChD,MAAM;IAAEC;EAAI,CAAC,GAAG1B,KAAK,CAACyB,UAAU,CAAC;EAEjC,MAAME,UAA4B,GAAG;IACnCC,IAAI,EAAEF,GAAG,KAAK,OAAO,GAAG,MAAM,GAAG;EACnC,CAAC;EAED,MAAMG,UAAU,GAAG,MAAM,CAACJ,UAAU,EAAE;IAAEK,IAAI,EAAEH;EAAW,CAAC,CAExD;EAEF,MAAM;IAAEI,OAAO,EAAEjB;EAAW,CAAC,GAAG,MAAMe,UAAU;EAChD,OAAOf,UAAU;AACnB","ignoreList":[]}
1
+ {"version":3,"file":"configureEnginePlugin.js","names":["join","parse","FORM_PREFIX","FormModel","plugin","defaultServices","formsService","findPackageRoot","devtoolContext","configureEnginePlugin","formFileName","formFilePath","services","controllers","model","definition","getForm","name","initialBasePath","basePath","options","cacheName","nunjucks","baseLayoutPath","paths","viewContext","importPath","ext","attributes","type","formImport","with","default"],"sources":["../../../../src/server/plugins/engine/configureEnginePlugin.ts"],"sourcesContent":["import { join, parse } from 'node:path'\n\nimport { type FormDefinition } from '@defra/forms-model'\n\nimport { FORM_PREFIX } from '~/src/server/constants.js'\nimport { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { plugin } from '~/src/server/plugins/engine/plugin.js'\nimport * as defaultServices from '~/src/server/plugins/engine/services/index.js'\nimport { formsService } from '~/src/server/plugins/engine/services/localFormsService.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.js'\nimport { findPackageRoot } from '~/src/server/plugins/engine/vision.js'\nimport { devtoolContext } from '~/src/server/plugins/nunjucks/context.js'\nimport { type RouteConfig } from '~/src/server/types.js'\n\nexport const configureEnginePlugin = async ({\n formFileName,\n formFilePath,\n services,\n controllers\n}: RouteConfig = {}): Promise<{\n plugin: typeof plugin\n options: PluginOptions\n}> => {\n let model: FormModel | undefined\n\n if (formFileName && formFilePath) {\n const definition = await getForm(join(formFilePath, formFileName))\n const { name } = parse(formFileName)\n\n const initialBasePath = `${FORM_PREFIX}${name}`\n\n model = new FormModel(\n definition,\n { basePath: initialBasePath },\n services,\n controllers\n )\n }\n\n return {\n plugin,\n options: {\n model,\n services: services ?? {\n // services for testing, else use the disk loader option for running this service locally\n ...defaultServices,\n formsService: await formsService()\n },\n controllers,\n cacheName: 'session',\n nunjucks: {\n baseLayoutPath: 'dxt-devtool-baselayout.html',\n paths: [join(findPackageRoot(), 'src/server/devserver')] // custom layout to make it really clear this is not the same as the runner\n },\n viewContext: devtoolContext\n }\n }\n}\n\nexport async function getForm(importPath: string) {\n const { ext } = parse(importPath)\n\n const attributes: ImportAttributes = {\n type: ext === '.json' ? 'json' : 'module'\n }\n\n const formImport = import(importPath, { with: attributes }) as Promise<{\n default: FormDefinition\n }>\n\n const { default: definition } = await formImport\n return definition\n}\n"],"mappings":"AAAA,SAASA,IAAI,EAAEC,KAAK,QAAQ,WAAW;AAIvC,SAASC,WAAW;AACpB,SAASC,SAAS;AAClB,SAASC,MAAM;AACf,OAAO,KAAKC,eAAe;AAC3B,SAASC,YAAY;AAErB,SAASC,eAAe;AACxB,SAASC,cAAc;AAGvB,OAAO,MAAMC,qBAAqB,GAAG,MAAAA,CAAO;EAC1CC,YAAY;EACZC,YAAY;EACZC,QAAQ;EACRC;AACW,CAAC,GAAG,CAAC,CAAC,KAGb;EACJ,IAAIC,KAA4B;EAEhC,IAAIJ,YAAY,IAAIC,YAAY,EAAE;IAChC,MAAMI,UAAU,GAAG,MAAMC,OAAO,CAAChB,IAAI,CAACW,YAAY,EAAED,YAAY,CAAC,CAAC;IAClE,MAAM;MAAEO;IAAK,CAAC,GAAGhB,KAAK,CAACS,YAAY,CAAC;IAEpC,MAAMQ,eAAe,GAAG,GAAGhB,WAAW,GAAGe,IAAI,EAAE;IAE/CH,KAAK,GAAG,IAAIX,SAAS,CACnBY,UAAU,EACV;MAAEI,QAAQ,EAAED;IAAgB,CAAC,EAC7BN,QAAQ,EACRC,WACF,CAAC;EACH;EAEA,OAAO;IACLT,MAAM;IACNgB,OAAO,EAAE;MACPN,KAAK;MACLF,QAAQ,EAAEA,QAAQ,IAAI;QACpB;QACA,GAAGP,eAAe;QAClBC,YAAY,EAAE,MAAMA,YAAY,CAAC;MACnC,CAAC;MACDO,WAAW;MACXQ,SAAS,EAAE,SAAS;MACpBC,QAAQ,EAAE;QACRC,cAAc,EAAE,6BAA6B;QAC7CC,KAAK,EAAE,CAACxB,IAAI,CAACO,eAAe,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC,CAAC;MAC3D,CAAC;MACDkB,WAAW,EAAEjB;IACf;EACF,CAAC;AACH,CAAC;AAED,OAAO,eAAeQ,OAAOA,CAACU,UAAkB,EAAE;EAChD,MAAM;IAAEC;EAAI,CAAC,GAAG1B,KAAK,CAACyB,UAAU,CAAC;EAEjC,MAAME,UAA4B,GAAG;IACnCC,IAAI,EAAEF,GAAG,KAAK,OAAO,GAAG,MAAM,GAAG;EACnC,CAAC;EAED,MAAMG,UAAU,GAAG,MAAM,CAACJ,UAAU,EAAE;IAAEK,IAAI,EAAEH;EAAW,CAAC,CAExD;EAEF,MAAM;IAAEI,OAAO,EAAEjB;EAAW,CAAC,GAAG,MAAMe,UAAU;EAChD,OAAOf,UAAU;AACnB","ignoreList":[]}
@@ -1,33 +1,8 @@
1
- import { type PluginProperties, type Server } from '@hapi/hapi';
2
- import { type Environment } from 'nunjucks';
3
- import { FormModel } from '~/src/server/plugins/engine/models/index.js';
4
- import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js';
5
- import { type FilterFunction } from '~/src/server/plugins/engine/types.js';
6
- import { type Services } from '~/src/server/types.js';
7
- export declare function findPackageRoot(): string;
8
- export interface PluginOptions {
9
- model?: FormModel;
10
- services?: Services;
11
- controllers?: Record<string, typeof PageController>;
12
- cacheName?: string;
13
- filters?: Record<string, FilterFunction>;
14
- pluginPath?: string;
15
- nunjucks: {
16
- baseLayoutPath: string;
17
- paths: string[];
18
- };
19
- viewContext: PluginProperties['forms-engine-plugin']['viewContext'];
20
- }
1
+ import { type Server } from '@hapi/hapi';
2
+ import { type PluginOptions } from '~/src/server/plugins/engine/types.js';
21
3
  export declare const plugin: {
22
4
  name: string;
23
5
  dependencies: string[];
24
6
  multiple: true;
25
7
  register(server: Server, options: PluginOptions): Promise<void>;
26
8
  };
27
- interface CompileOptions {
28
- environment: Environment;
29
- }
30
- export interface EngineConfigurationObject {
31
- compileOptions: CompileOptions;
32
- }
33
- export {};