@defra/forms-engine-plugin 2.1.10 → 3.0.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.
Files changed (122) hide show
  1. package/.server/server/index.js +2 -1
  2. package/.server/server/index.js.map +1 -1
  3. package/.server/server/plugins/engine/README.md +2 -2
  4. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +2 -1
  5. package/.server/server/plugins/engine/configureEnginePlugin.js +4 -4
  6. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  7. package/.server/server/plugins/engine/helpers.d.ts +7 -11
  8. package/.server/server/plugins/engine/helpers.js +2 -2
  9. package/.server/server/plugins/engine/helpers.js.map +1 -1
  10. package/.server/server/plugins/engine/models/FormModel.js +1 -1
  11. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  12. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -1
  13. package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -1
  14. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  15. package/.server/server/plugins/engine/options.js +3 -6
  16. package/.server/server/plugins/engine/options.js.map +1 -1
  17. package/.server/server/plugins/engine/options.test.js +2 -8
  18. package/.server/server/plugins/engine/options.test.js.map +1 -1
  19. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +5 -6
  20. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  21. package/.server/server/plugins/engine/pageControllers/PageController.d.ts +6 -6
  22. package/.server/server/plugins/engine/pageControllers/PageController.js +4 -4
  23. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  24. package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +13 -13
  25. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +12 -20
  26. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  27. package/.server/server/plugins/engine/pageControllers/RepeatPageController.d.ts +7 -8
  28. package/.server/server/plugins/engine/pageControllers/RepeatPageController.js.map +1 -1
  29. package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -2
  30. package/.server/server/plugins/engine/pageControllers/StartPageController.js +1 -1
  31. package/.server/server/plugins/engine/pageControllers/StartPageController.js.map +1 -1
  32. package/.server/server/plugins/engine/pageControllers/StatusPageController.d.ts +3 -4
  33. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +1 -1
  34. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  35. package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +5 -4
  36. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +12 -1
  37. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  38. package/.server/server/plugins/engine/pageControllers/TerminalPageController.d.ts +4 -4
  39. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js +1 -1
  40. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js.map +1 -1
  41. package/.server/server/plugins/engine/pageControllers/__stubs__/server.d.ts +1 -1
  42. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js +2 -6
  43. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js.map +1 -1
  44. package/.server/server/plugins/engine/plugin.js +7 -12
  45. package/.server/server/plugins/engine/plugin.js.map +1 -1
  46. package/.server/server/plugins/engine/routes/index.d.ts +5 -5
  47. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  48. package/.server/server/plugins/engine/routes/questions.d.ts +4 -4
  49. package/.server/server/plugins/engine/routes/questions.js.map +1 -1
  50. package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -1
  51. package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -1
  52. package/.server/server/plugins/engine/types/index.d.ts +2 -2
  53. package/.server/server/plugins/engine/types/index.js.map +1 -1
  54. package/.server/server/plugins/engine/types.d.ts +10 -11
  55. package/.server/server/plugins/engine/types.js.map +1 -1
  56. package/.server/server/plugins/engine/views/partials/form.html +3 -3
  57. package/.server/server/plugins/engine/views/summary.html +21 -5
  58. package/.server/server/plugins/nunjucks/context.d.ts +5 -6
  59. package/.server/server/plugins/nunjucks/context.js +3 -3
  60. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  61. package/.server/server/routes/types.d.ts +3 -2
  62. package/.server/server/routes/types.js +1 -1
  63. package/.server/server/routes/types.js.map +1 -1
  64. package/.server/server/schemas/index.js +1 -1
  65. package/.server/server/schemas/index.js.map +1 -1
  66. package/.server/server/services/cacheService.d.ts +11 -19
  67. package/.server/server/services/cacheService.js +9 -30
  68. package/.server/server/services/cacheService.js.map +1 -1
  69. package/.server/server/types.d.ts +4 -1
  70. package/.server/server/types.js.map +1 -1
  71. package/.server/typings/hapi/index.d.js.map +1 -1
  72. package/package.json +3 -1
  73. package/src/server/index.test.ts +0 -39
  74. package/src/server/index.ts +4 -1
  75. package/src/server/plugins/engine/README.md +2 -2
  76. package/src/server/plugins/engine/components/helpers/helpers.test.ts +1 -1
  77. package/src/server/plugins/engine/configureEnginePlugin.ts +15 -11
  78. package/src/server/plugins/engine/helpers.test.ts +3 -2
  79. package/src/server/plugins/engine/helpers.ts +6 -6
  80. package/src/server/plugins/engine/models/FormModel.test.ts +2 -2
  81. package/src/server/plugins/engine/models/FormModel.ts +1 -2
  82. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +7 -7
  83. package/src/server/plugins/engine/models/SummaryViewModel.ts +1 -1
  84. package/src/server/plugins/engine/options.js +6 -6
  85. package/src/server/plugins/engine/options.test.js +2 -6
  86. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +8 -10
  87. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +9 -8
  88. package/src/server/plugins/engine/pageControllers/PageController.test.ts +11 -8
  89. package/src/server/plugins/engine/pageControllers/PageController.ts +9 -15
  90. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +35 -102
  91. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +24 -36
  92. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +4 -6
  93. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +8 -11
  94. package/src/server/plugins/engine/pageControllers/StartPageController.test.ts +4 -4
  95. package/src/server/plugins/engine/pageControllers/StartPageController.ts +1 -1
  96. package/src/server/plugins/engine/pageControllers/StatusPageController.test.ts +4 -4
  97. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +6 -4
  98. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +15 -5
  99. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +4 -4
  100. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +7 -4
  101. package/src/server/plugins/engine/pageControllers/__stubs__/server.ts +5 -6
  102. package/src/server/plugins/engine/plugin.ts +7 -13
  103. package/src/server/plugins/engine/routes/index.ts +6 -11
  104. package/src/server/plugins/engine/routes/questions.test.ts +29 -53
  105. package/src/server/plugins/engine/routes/questions.ts +6 -8
  106. package/src/server/plugins/engine/routes/repeaters/item-delete.ts +5 -14
  107. package/src/server/plugins/engine/routes/repeaters/summary.ts +5 -14
  108. package/src/server/plugins/engine/types/index.ts +4 -1
  109. package/src/server/plugins/engine/types.ts +19 -13
  110. package/src/server/plugins/engine/views/partials/form.html +3 -3
  111. package/src/server/plugins/engine/views/summary.html +21 -5
  112. package/src/server/plugins/nunjucks/context.js +3 -3
  113. package/src/server/routes/types.ts +7 -2
  114. package/src/server/schemas/index.ts +1 -1
  115. package/src/server/services/cacheService.test.ts +1 -117
  116. package/src/server/services/cacheService.ts +22 -73
  117. package/src/server/types.ts +4 -1
  118. package/src/typings/hapi/index.d.ts +6 -7
  119. package/.server/server/plugins/engine/routes/exit.d.ts +0 -46
  120. package/.server/server/plugins/engine/routes/exit.js +0 -36
  121. package/.server/server/plugins/engine/routes/exit.js.map +0 -1
  122. package/src/server/plugins/engine/routes/exit.ts +0 -47
@@ -2,6 +2,7 @@
2
2
 
3
3
  {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
4
4
  {% from "govuk/components/summary-list/macro.njk" import govukSummaryList %}
5
+ {% from "govuk/components/button/macro.njk" import govukButton %}
5
6
  {% from "partials/components.html" import componentList with context %}
6
7
 
7
8
  {% block content %}
@@ -31,7 +32,6 @@
31
32
 
32
33
  <form method="post" novalidate>
33
34
  <input type="hidden" name="crumb" value="{{ crumb }}">
34
- <input type="hidden" name="action" value="send">
35
35
 
36
36
  {% if declaration %}
37
37
  <h2 class="govuk-heading-m" id="declaration">Declaration</h2>
@@ -42,10 +42,26 @@
42
42
 
43
43
  {{ componentList(components) }}
44
44
 
45
- {% set isDeclaration = declaration or components | length %}
46
- <button data-prevent-double-click="true" class="govuk-button" data-module="govuk-button">
47
- {{ "Accept and send" if isDeclaration else "Send" }}
48
- </button>
45
+ <div class="govuk-button-group">
46
+ {% set isDeclaration = declaration or components | length %}
47
+
48
+ {{ govukButton({
49
+ text: "Accept and send" if isDeclaration else "Send",
50
+ name: "action",
51
+ value: "send",
52
+ preventDoubleClick: true
53
+ }) }}
54
+
55
+ {% if allowSaveAndExit %}
56
+ {{ govukButton({
57
+ text: "Save and exit",
58
+ classes: "govuk-button--secondary",
59
+ name: "action",
60
+ value: "save-and-exit",
61
+ preventDoubleClick: true
62
+ }) }}
63
+ {% endif %}
64
+ </div>
49
65
  </form>
50
66
  </div>
51
67
  </div>
@@ -1,16 +1,15 @@
1
1
  /**
2
- * @param {FormRequest | FormRequestPayload | null} request
2
+ * @param {AnyFormRequest | null} request
3
3
  */
4
- export function context(request: FormRequest | FormRequestPayload | null): Promise<ViewContext>;
4
+ export function context(request: AnyFormRequest | null): Promise<ViewContext>;
5
5
  /**
6
6
  * Returns the context for the devtool. Consumers won't have access to this.
7
- * @param {FormRequest | FormRequestPayload | null} _request
7
+ * @param {AnyFormRequest | null} _request
8
8
  * @returns {Record<string, unknown> & { assetPath: string, getDxtAssetPath: (asset: string) => string }}
9
9
  */
10
- export function devtoolContext(_request: FormRequest | FormRequestPayload | null): Record<string, unknown> & {
10
+ export function devtoolContext(_request: AnyFormRequest | null): Record<string, unknown> & {
11
11
  assetPath: string;
12
12
  getDxtAssetPath: (asset: string) => string;
13
13
  };
14
- import type { FormRequest } from '~/src/server/routes/types.js';
15
- import type { FormRequestPayload } from '~/src/server/routes/types.js';
14
+ import type { AnyFormRequest } from '~/src/server/plugins/engine/types.js';
16
15
  import type { ViewContext } from '~/src/server/plugins/nunjucks/types.js';
@@ -11,7 +11,7 @@ const logger = createLogger();
11
11
  let webpackManifest;
12
12
 
13
13
  /**
14
- * @param {FormRequest | FormRequestPayload | null} request
14
+ * @param {AnyFormRequest | null} request
15
15
  */
16
16
  export async function context(request) {
17
17
  const {
@@ -53,7 +53,7 @@ export async function context(request) {
53
53
 
54
54
  /**
55
55
  * Returns the context for the devtool. Consumers won't have access to this.
56
- * @param {FormRequest | FormRequestPayload | null} _request
56
+ * @param {AnyFormRequest | null} _request
57
57
  * @returns {Record<string, unknown> & { assetPath: string, getDxtAssetPath: (asset: string) => string }}
58
58
  */
59
59
  export function devtoolContext(_request) {
@@ -84,6 +84,6 @@ export function devtoolContext(_request) {
84
84
 
85
85
  /**
86
86
  * @import { ViewContext } from '~/src/server/plugins/nunjucks/types.js'
87
- * @import { FormRequest, FormRequestPayload } from '~/src/server/routes/types.js'
87
+ * @import { AnyFormRequest } from '~/src/server/plugins/engine/types.js'
88
88
  */
89
89
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":["readFileSync","basename","join","Boom","StatusCodes","config","createLogger","checkFormStatus","encodeUrl","safeGenerateCrumb","logger","webpackManifest","context","request","params","response","isPreview","isPreviewMode","state","formState","isResponseOK","isBoom","statusCode","OK","pluginStorage","server","plugins","consumerViewContext","Error","baseLayoutPath","viewContext","ctx","crumb","currentPath","path","url","search","previewMode","undefined","slug","devtoolContext","_request","manifestPath","get","JSON","parse","info","cdpEnvironment","designerUrl","feedbackLink","phaseTag","serviceName","serviceVersion","assetPath","getDxtAssetPath","asset"],"sources":["../../../../src/server/plugins/nunjucks/context.js"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport Boom from '@hapi/boom'\nimport { StatusCodes } from 'http-status-codes'\n\nimport { config } from '~/src/config/index.js'\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport {\n checkFormStatus,\n encodeUrl,\n safeGenerateCrumb\n} from '~/src/server/plugins/engine/helpers.js'\n\nconst logger = createLogger()\n\n/** @type {Record<string, string> | undefined} */\nlet webpackManifest\n\n/**\n * @param {FormRequest | FormRequestPayload | null} request\n */\nexport async function context(request) {\n const { params, response } = request ?? {}\n\n const { isPreview: isPreviewMode, state: formState } = checkFormStatus(params)\n\n // Only add the slug in to the context if the response is OK.\n // Footer meta links are not rendered when the slug is missing.\n const isResponseOK =\n !Boom.isBoom(response) && response?.statusCode === StatusCodes.OK\n\n const pluginStorage = request?.server.plugins['forms-engine-plugin']\n\n let consumerViewContext = {}\n\n if (!pluginStorage) {\n throw Error('context called before plugin registered')\n }\n\n if (!pluginStorage.baseLayoutPath) {\n throw Error('Missing baseLayoutPath in plugin.options.nunjucks')\n }\n\n if (typeof pluginStorage.viewContext === 'function') {\n consumerViewContext = await pluginStorage.viewContext(request)\n }\n\n /** @type {ViewContext} */\n const ctx = {\n // take consumers props first so we can override it\n ...consumerViewContext,\n baseLayoutPath: pluginStorage.baseLayoutPath,\n crumb: safeGenerateCrumb(request),\n currentPath: `${request.path}${request.url.search}`,\n previewMode: isPreviewMode ? formState : undefined,\n slug: isResponseOK ? params?.slug : undefined\n }\n\n return ctx\n}\n\n/**\n * Returns the context for the devtool. Consumers won't have access to this.\n * @param {FormRequest | FormRequestPayload | null} _request\n * @returns {Record<string, unknown> & { assetPath: string, getDxtAssetPath: (asset: string) => string }}\n */\nexport function devtoolContext(_request) {\n const manifestPath = join(config.get('publicDir'), 'assets-manifest.json')\n\n if (!webpackManifest) {\n try {\n // eslint-disable-next-line -- Allow JSON type 'any'\n webpackManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))\n } catch {\n logger.info(\n `[webpackManifestMissing] Webpack ${basename(manifestPath)} not found - running without asset manifest`\n )\n }\n }\n\n return {\n config: {\n cdpEnvironment: config.get('cdpEnvironment'),\n designerUrl: config.get('designerUrl'),\n feedbackLink: encodeUrl(config.get('feedbackLink')),\n phaseTag: config.get('phaseTag'),\n serviceName: config.get('serviceName'),\n serviceVersion: config.get('serviceVersion')\n },\n assetPath: '/assets',\n getDxtAssetPath: (asset = '') => {\n return `/${webpackManifest?.[asset] ?? asset}`\n }\n }\n}\n\n/**\n * @import { ViewContext } from '~/src/server/plugins/nunjucks/types.js'\n * @import { FormRequest, FormRequestPayload } from '~/src/server/routes/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"context.js","names":["readFileSync","basename","join","Boom","StatusCodes","config","createLogger","checkFormStatus","encodeUrl","safeGenerateCrumb","logger","webpackManifest","context","request","params","response","isPreview","isPreviewMode","state","formState","isResponseOK","isBoom","statusCode","OK","pluginStorage","server","plugins","consumerViewContext","Error","baseLayoutPath","viewContext","ctx","crumb","currentPath","path","url","search","previewMode","undefined","slug","devtoolContext","_request","manifestPath","get","JSON","parse","info","cdpEnvironment","designerUrl","feedbackLink","phaseTag","serviceName","serviceVersion","assetPath","getDxtAssetPath","asset"],"sources":["../../../../src/server/plugins/nunjucks/context.js"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport Boom from '@hapi/boom'\nimport { StatusCodes } from 'http-status-codes'\n\nimport { config } from '~/src/config/index.js'\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport {\n checkFormStatus,\n encodeUrl,\n safeGenerateCrumb\n} from '~/src/server/plugins/engine/helpers.js'\n\nconst logger = createLogger()\n\n/** @type {Record<string, string> | undefined} */\nlet webpackManifest\n\n/**\n * @param {AnyFormRequest | null} request\n */\nexport async function context(request) {\n const { params, response } = request ?? {}\n\n const { isPreview: isPreviewMode, state: formState } = checkFormStatus(params)\n\n // Only add the slug in to the context if the response is OK.\n // Footer meta links are not rendered when the slug is missing.\n const isResponseOK =\n !Boom.isBoom(response) && response?.statusCode === StatusCodes.OK\n\n const pluginStorage = request?.server.plugins['forms-engine-plugin']\n\n let consumerViewContext = {}\n\n if (!pluginStorage) {\n throw Error('context called before plugin registered')\n }\n\n if (!pluginStorage.baseLayoutPath) {\n throw Error('Missing baseLayoutPath in plugin.options.nunjucks')\n }\n\n if (typeof pluginStorage.viewContext === 'function') {\n consumerViewContext = await pluginStorage.viewContext(request)\n }\n\n /** @type {ViewContext} */\n const ctx = {\n // take consumers props first so we can override it\n ...consumerViewContext,\n baseLayoutPath: pluginStorage.baseLayoutPath,\n crumb: safeGenerateCrumb(request),\n currentPath: `${request.path}${request.url.search}`,\n previewMode: isPreviewMode ? formState : undefined,\n slug: isResponseOK ? params?.slug : undefined\n }\n\n return ctx\n}\n\n/**\n * Returns the context for the devtool. Consumers won't have access to this.\n * @param {AnyFormRequest | null} _request\n * @returns {Record<string, unknown> & { assetPath: string, getDxtAssetPath: (asset: string) => string }}\n */\nexport function devtoolContext(_request) {\n const manifestPath = join(config.get('publicDir'), 'assets-manifest.json')\n\n if (!webpackManifest) {\n try {\n // eslint-disable-next-line -- Allow JSON type 'any'\n webpackManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))\n } catch {\n logger.info(\n `[webpackManifestMissing] Webpack ${basename(manifestPath)} not found - running without asset manifest`\n )\n }\n }\n\n return {\n config: {\n cdpEnvironment: config.get('cdpEnvironment'),\n designerUrl: config.get('designerUrl'),\n feedbackLink: encodeUrl(config.get('feedbackLink')),\n phaseTag: config.get('phaseTag'),\n serviceName: config.get('serviceName'),\n serviceVersion: config.get('serviceVersion')\n },\n assetPath: '/assets',\n getDxtAssetPath: (asset = '') => {\n return `/${webpackManifest?.[asset] ?? asset}`\n }\n }\n}\n\n/**\n * @import { ViewContext } from '~/src/server/plugins/nunjucks/types.js'\n * @import { AnyFormRequest } from '~/src/server/plugins/engine/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
@@ -1,4 +1,4 @@
1
- import { type ReqRefDefaults, type Request } from '@hapi/hapi';
1
+ import { type ReqRefDefaults, type Request, type ResponseToolkit } from '@hapi/hapi';
2
2
  import { type FormPayload } from '~/src/server/plugins/engine/types.js';
3
3
  export interface FormQuery extends Partial<Record<string, string>> {
4
4
  /**
@@ -25,13 +25,14 @@ export interface FormRequestPayloadRefs extends FormRequestRefs {
25
25
  }
26
26
  export type FormRequest = Request<FormRequestRefs>;
27
27
  export type FormRequestPayload = Request<FormRequestPayloadRefs>;
28
+ export type FormResponseToolkit = Pick<ResponseToolkit, 'redirect' | 'view'>;
28
29
  export declare enum FormAction {
29
30
  Continue = "continue",
30
31
  Validate = "validate",
31
32
  Delete = "delete",
32
33
  AddAnother = "add-another",
33
34
  Send = "send",
34
- SaveAndReturn = "save-and-return"
35
+ SaveAndExit = "save-and-exit"
35
36
  }
36
37
  export declare enum FormStatus {
37
38
  Draft = "draft",
@@ -4,7 +4,7 @@ export let FormAction = /*#__PURE__*/function (FormAction) {
4
4
  FormAction["Delete"] = "delete";
5
5
  FormAction["AddAnother"] = "add-another";
6
6
  FormAction["Send"] = "send";
7
- FormAction["SaveAndReturn"] = "save-and-return";
7
+ FormAction["SaveAndExit"] = "save-and-exit";
8
8
  return FormAction;
9
9
  }({});
10
10
  export let FormStatus = /*#__PURE__*/function (FormStatus) {
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":["FormAction","FormStatus"],"sources":["../../../src/server/routes/types.ts"],"sourcesContent":["import { type ReqRefDefaults, type Request } from '@hapi/hapi'\n\nimport { type FormPayload } from '~/src/server/plugins/engine/types.js'\n\nexport interface FormQuery extends Partial<Record<string, string>> {\n /**\n * Allow preview URL direct access without relevant page checks\n */\n force?: string\n\n /**\n * Redirect location after 'continue' form action\n */\n returnUrl?: string\n}\n\nexport interface FormParams extends Partial<Record<string, string>> {\n path: string\n slug: string\n state?: FormStatus\n}\n\nexport interface FormRequestRefs\n extends Omit<ReqRefDefaults, 'Params' | 'Payload' | 'Query'> {\n Params: FormParams\n Payload: object | undefined\n Query: FormQuery\n}\n\nexport interface FormRequestPayloadRefs extends FormRequestRefs {\n Payload: FormPayload\n}\n\nexport type FormRequest = Request<FormRequestRefs>\nexport type FormRequestPayload = Request<FormRequestPayloadRefs>\n\nexport enum FormAction {\n Continue = 'continue',\n Validate = 'validate',\n Delete = 'delete',\n AddAnother = 'add-another',\n Send = 'send',\n SaveAndReturn = 'save-and-return'\n}\n\nexport enum FormStatus {\n Draft = 'draft',\n Live = 'live'\n}\n"],"mappings":"AAoCA,WAAYA,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA;AAStB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":["FormAction","FormStatus"],"sources":["../../../src/server/routes/types.ts"],"sourcesContent":["import {\n type ReqRefDefaults,\n type Request,\n type ResponseToolkit\n} from '@hapi/hapi'\n\nimport { type FormPayload } from '~/src/server/plugins/engine/types.js'\n\nexport interface FormQuery extends Partial<Record<string, string>> {\n /**\n * Allow preview URL direct access without relevant page checks\n */\n force?: string\n\n /**\n * Redirect location after 'continue' form action\n */\n returnUrl?: string\n}\n\nexport interface FormParams extends Partial<Record<string, string>> {\n path: string\n slug: string\n state?: FormStatus\n}\n\nexport interface FormRequestRefs\n extends Omit<ReqRefDefaults, 'Params' | 'Payload' | 'Query'> {\n Params: FormParams\n Payload: object | undefined\n Query: FormQuery\n}\n\nexport interface FormRequestPayloadRefs extends FormRequestRefs {\n Payload: FormPayload\n}\n\nexport type FormRequest = Request<FormRequestRefs>\nexport type FormRequestPayload = Request<FormRequestPayloadRefs>\nexport type FormResponseToolkit = Pick<ResponseToolkit, 'redirect' | 'view'>\n\nexport enum FormAction {\n Continue = 'continue',\n Validate = 'validate',\n Delete = 'delete',\n AddAnother = 'add-another',\n Send = 'send',\n SaveAndExit = 'save-and-exit'\n}\n\nexport enum FormStatus {\n Draft = 'draft',\n Live = 'live'\n}\n"],"mappings":"AAyCA,WAAYA,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA;AAStB,WAAYC,UAAU,0BAAVA,UAAU;EAAVA,UAAU;EAAVA,UAAU;EAAA,OAAVA,UAAU;AAAA","ignoreList":[]}
@@ -1,7 +1,7 @@
1
1
  import Joi from 'joi';
2
2
  import { FormAction, FormStatus } from "../routes/types.js";
3
3
  export const stateSchema = Joi.string().valid(FormStatus.Draft, FormStatus.Live).required();
4
- export const actionSchema = Joi.string().valid(FormAction.Continue, FormAction.Validate, FormAction.Delete, FormAction.AddAnother, FormAction.Send, FormAction.SaveAndReturn).default(FormAction.Validate).optional();
4
+ export const actionSchema = Joi.string().valid(FormAction.Continue, FormAction.Validate, FormAction.Delete, FormAction.AddAnother, FormAction.Send, FormAction.SaveAndExit).default(FormAction.Validate).optional();
5
5
  export const pathSchema = Joi.string().required();
6
6
  export const itemIdSchema = Joi.string().uuid().required();
7
7
  export const crumbSchema = Joi.string().optional().allow('');
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["Joi","FormAction","FormStatus","stateSchema","string","valid","Draft","Live","required","actionSchema","Continue","Validate","Delete","AddAnother","Send","SaveAndReturn","default","optional","pathSchema","itemIdSchema","uuid","crumbSchema","allow","confirmSchema","boolean","empty","paramsSchema","object","keys","action","confirm","crumb","itemId"],"sources":["../../../src/server/schemas/index.ts"],"sourcesContent":["import Joi from 'joi'\n\nimport { type FormPayloadParams } from '~/src/server/plugins/engine/types.js'\nimport { FormAction, FormStatus } from '~/src/server/routes/types.js'\n\nexport const stateSchema = Joi.string<FormStatus>()\n .valid(FormStatus.Draft, FormStatus.Live)\n .required()\n\nexport const actionSchema = Joi.string<FormAction>()\n .valid(\n FormAction.Continue,\n FormAction.Validate,\n FormAction.Delete,\n FormAction.AddAnother,\n FormAction.Send,\n FormAction.SaveAndReturn\n )\n .default(FormAction.Validate)\n .optional()\n\nexport const pathSchema = Joi.string().required()\nexport const itemIdSchema = Joi.string().uuid().required()\nexport const crumbSchema = Joi.string().optional().allow('')\nexport const confirmSchema = Joi.boolean().empty(false)\n\nexport const paramsSchema = Joi.object<FormPayloadParams>()\n .keys({\n action: actionSchema,\n confirm: confirmSchema,\n crumb: crumbSchema,\n itemId: itemIdSchema.optional()\n })\n .default({})\n .optional()\n"],"mappings":"AAAA,OAAOA,GAAG,MAAM,KAAK;AAGrB,SAASC,UAAU,EAAEC,UAAU;AAE/B,OAAO,MAAMC,WAAW,GAAGH,GAAG,CAACI,MAAM,CAAa,CAAC,CAChDC,KAAK,CAACH,UAAU,CAACI,KAAK,EAAEJ,UAAU,CAACK,IAAI,CAAC,CACxCC,QAAQ,CAAC,CAAC;AAEb,OAAO,MAAMC,YAAY,GAAGT,GAAG,CAACI,MAAM,CAAa,CAAC,CACjDC,KAAK,CACJJ,UAAU,CAACS,QAAQ,EACnBT,UAAU,CAACU,QAAQ,EACnBV,UAAU,CAACW,MAAM,EACjBX,UAAU,CAACY,UAAU,EACrBZ,UAAU,CAACa,IAAI,EACfb,UAAU,CAACc,aACb,CAAC,CACAC,OAAO,CAACf,UAAU,CAACU,QAAQ,CAAC,CAC5BM,QAAQ,CAAC,CAAC;AAEb,OAAO,MAAMC,UAAU,GAAGlB,GAAG,CAACI,MAAM,CAAC,CAAC,CAACI,QAAQ,CAAC,CAAC;AACjD,OAAO,MAAMW,YAAY,GAAGnB,GAAG,CAACI,MAAM,CAAC,CAAC,CAACgB,IAAI,CAAC,CAAC,CAACZ,QAAQ,CAAC,CAAC;AAC1D,OAAO,MAAMa,WAAW,GAAGrB,GAAG,CAACI,MAAM,CAAC,CAAC,CAACa,QAAQ,CAAC,CAAC,CAACK,KAAK,CAAC,EAAE,CAAC;AAC5D,OAAO,MAAMC,aAAa,GAAGvB,GAAG,CAACwB,OAAO,CAAC,CAAC,CAACC,KAAK,CAAC,KAAK,CAAC;AAEvD,OAAO,MAAMC,YAAY,GAAG1B,GAAG,CAAC2B,MAAM,CAAoB,CAAC,CACxDC,IAAI,CAAC;EACJC,MAAM,EAAEpB,YAAY;EACpBqB,OAAO,EAAEP,aAAa;EACtBQ,KAAK,EAAEV,WAAW;EAClBW,MAAM,EAAEb,YAAY,CAACF,QAAQ,CAAC;AAChC,CAAC,CAAC,CACDD,OAAO,CAAC,CAAC,CAAC,CAAC,CACXC,QAAQ,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["Joi","FormAction","FormStatus","stateSchema","string","valid","Draft","Live","required","actionSchema","Continue","Validate","Delete","AddAnother","Send","SaveAndExit","default","optional","pathSchema","itemIdSchema","uuid","crumbSchema","allow","confirmSchema","boolean","empty","paramsSchema","object","keys","action","confirm","crumb","itemId"],"sources":["../../../src/server/schemas/index.ts"],"sourcesContent":["import Joi from 'joi'\n\nimport { type FormPayloadParams } from '~/src/server/plugins/engine/types.js'\nimport { FormAction, FormStatus } from '~/src/server/routes/types.js'\n\nexport const stateSchema = Joi.string<FormStatus>()\n .valid(FormStatus.Draft, FormStatus.Live)\n .required()\n\nexport const actionSchema = Joi.string<FormAction>()\n .valid(\n FormAction.Continue,\n FormAction.Validate,\n FormAction.Delete,\n FormAction.AddAnother,\n FormAction.Send,\n FormAction.SaveAndExit\n )\n .default(FormAction.Validate)\n .optional()\n\nexport const pathSchema = Joi.string().required()\nexport const itemIdSchema = Joi.string().uuid().required()\nexport const crumbSchema = Joi.string().optional().allow('')\nexport const confirmSchema = Joi.boolean().empty(false)\n\nexport const paramsSchema = Joi.object<FormPayloadParams>()\n .keys({\n action: actionSchema,\n confirm: confirmSchema,\n crumb: crumbSchema,\n itemId: itemIdSchema.optional()\n })\n .default({})\n .optional()\n"],"mappings":"AAAA,OAAOA,GAAG,MAAM,KAAK;AAGrB,SAASC,UAAU,EAAEC,UAAU;AAE/B,OAAO,MAAMC,WAAW,GAAGH,GAAG,CAACI,MAAM,CAAa,CAAC,CAChDC,KAAK,CAACH,UAAU,CAACI,KAAK,EAAEJ,UAAU,CAACK,IAAI,CAAC,CACxCC,QAAQ,CAAC,CAAC;AAEb,OAAO,MAAMC,YAAY,GAAGT,GAAG,CAACI,MAAM,CAAa,CAAC,CACjDC,KAAK,CACJJ,UAAU,CAACS,QAAQ,EACnBT,UAAU,CAACU,QAAQ,EACnBV,UAAU,CAACW,MAAM,EACjBX,UAAU,CAACY,UAAU,EACrBZ,UAAU,CAACa,IAAI,EACfb,UAAU,CAACc,WACb,CAAC,CACAC,OAAO,CAACf,UAAU,CAACU,QAAQ,CAAC,CAC5BM,QAAQ,CAAC,CAAC;AAEb,OAAO,MAAMC,UAAU,GAAGlB,GAAG,CAACI,MAAM,CAAC,CAAC,CAACI,QAAQ,CAAC,CAAC;AACjD,OAAO,MAAMW,YAAY,GAAGnB,GAAG,CAACI,MAAM,CAAC,CAAC,CAACgB,IAAI,CAAC,CAAC,CAACZ,QAAQ,CAAC,CAAC;AAC1D,OAAO,MAAMa,WAAW,GAAGrB,GAAG,CAACI,MAAM,CAAC,CAAC,CAACa,QAAQ,CAAC,CAAC,CAACK,KAAK,CAAC,EAAE,CAAC;AAC5D,OAAO,MAAMC,aAAa,GAAGvB,GAAG,CAACwB,OAAO,CAAC,CAAC,CAACC,KAAK,CAAC,KAAK,CAAC;AAEvD,OAAO,MAAMC,YAAY,GAAG1B,GAAG,CAAC2B,MAAM,CAAoB,CAAC,CACxDC,IAAI,CAAC;EACJC,MAAM,EAAEpB,YAAY;EACpBqB,OAAO,EAAEP,aAAa;EACtBQ,KAAK,EAAEV,WAAW;EAClBW,MAAM,EAAEb,YAAY,CAACF,QAAQ,CAAC;AAChC,CAAC,CAAC,CACDD,OAAO,CAAC,CAAC,CAAC,CAAC,CACXC,QAAQ,CAAC,CAAC","ignoreList":[]}
@@ -1,6 +1,5 @@
1
- import { type Request, type Server } from '@hapi/hapi';
2
- import { type FormPayload, type FormState, type FormSubmissionError, type FormSubmissionState } from '~/src/server/plugins/engine/types.js';
3
- import { type FormRequest, type FormRequestPayload } from '~/src/server/routes/types.js';
1
+ import { type Server } from '@hapi/hapi';
2
+ import { type AnyFormRequest, type AnyRequest, type FormPayload, type FormState, type FormSubmissionError, type FormSubmissionState } from '~/src/server/plugins/engine/types.js';
4
3
  export declare enum ADDITIONAL_IDENTIFIER {
5
4
  Confirmation = ":confirmation"
6
5
  }
@@ -12,40 +11,33 @@ export declare class CacheService {
12
11
  cache: string | undefined;
13
12
  segment: string;
14
13
  }>;
15
- generateKey?: (request: Request | FormRequest | FormRequestPayload) => string;
16
- customFetcher?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState | null>;
17
14
  logger: Server['logger'];
18
- constructor({ server, cacheName, options }: {
15
+ constructor({ server, cacheName }: {
19
16
  server: Server;
20
17
  cacheName?: string;
21
- options?: {
22
- keyGenerator?: (request: Request | FormRequest | FormRequestPayload) => string;
23
- sessionHydrator?: (request: Request | FormRequest | FormRequestPayload) => Promise<FormSubmissionState | null>;
24
- };
25
18
  });
26
- getState(request: Request | FormRequest | FormRequestPayload): Promise<FormSubmissionState>;
27
- setState(request: FormRequest | FormRequestPayload, state: FormSubmissionState): Promise<FormSubmissionState>;
28
- getConfirmationState(request: FormRequest | FormRequestPayload): Promise<{
19
+ getState(request: AnyRequest): Promise<FormSubmissionState>;
20
+ setState(request: AnyFormRequest, state: FormSubmissionState): Promise<FormSubmissionState>;
21
+ getConfirmationState(request: AnyFormRequest): Promise<{
29
22
  confirmed?: true;
30
23
  }>;
31
- setConfirmationState(request: FormRequest | FormRequestPayload, confirmationState: {
24
+ setConfirmationState(request: AnyFormRequest, confirmationState: {
32
25
  confirmed?: true;
33
26
  }): Promise<void>;
34
- clearState(request: FormRequest | FormRequestPayload): Promise<void>;
35
- getFlash(request: FormRequest | FormRequestPayload): {
27
+ clearState(request: AnyFormRequest): Promise<void>;
28
+ getFlash(request: AnyFormRequest): {
36
29
  errors: FormSubmissionError[];
37
30
  } | undefined;
38
- setFlash(request: FormRequest | FormRequestPayload, message: {
31
+ setFlash(request: AnyFormRequest, message: {
39
32
  errors: FormSubmissionError[];
40
33
  }): void;
41
- private defaultKeyGenerator;
42
34
  /**
43
35
  * The key used to store user session data against.
44
36
  * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`
45
37
  * @param request - hapi request object
46
38
  * @param additionalIdentifier - appended to the id
47
39
  */
48
- Key(request: Request | FormRequest | FormRequestPayload, additionalIdentifier?: ADDITIONAL_IDENTIFIER): {
40
+ Key(request: AnyRequest, additionalIdentifier?: ADDITIONAL_IDENTIFIER): {
49
41
  segment: string;
50
42
  id: string;
51
43
  };
@@ -10,23 +10,14 @@ export class CacheService {
10
10
  * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}
11
11
  */
12
12
  cache;
13
- generateKey;
14
- customFetcher;
15
13
  logger;
16
14
  constructor({
17
15
  server,
18
- cacheName,
19
- options
16
+ cacheName
20
17
  }) {
21
- const {
22
- keyGenerator,
23
- sessionHydrator
24
- } = options ?? {};
25
18
  if (!cacheName) {
26
19
  server.log('warn', 'You are using the default hapi cache. Please provide a cache name in plugin registration options.');
27
20
  }
28
- this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this);
29
- this.customFetcher = sessionHydrator ?? undefined;
30
21
  this.cache = server.cache({
31
22
  cache: cacheName,
32
23
  segment: 'formSubmission'
@@ -35,16 +26,7 @@ export class CacheService {
35
26
  }
36
27
  async getState(request) {
37
28
  const key = this.Key(request);
38
- let cached = await this.cache.get(key);
39
-
40
- // If nothing in Redis, attempt to rehydrate from backend DB
41
- if (!cached && this.customFetcher) {
42
- const rehydrated = await this.customFetcher(request);
43
- if (rehydrated != null) {
44
- await this.cache.set(key, rehydrated, config.get('sessionTimeout'));
45
- cached = await this.getState(request);
46
- }
47
- }
29
+ const cached = await this.cache.get(key);
48
30
  return cached ?? {};
49
31
  }
50
32
  async setState(request, state) {
@@ -79,14 +61,6 @@ export class CacheService {
79
61
  const key = this.Key(request);
80
62
  request.yar.flash(key.id, message);
81
63
  }
82
- defaultKeyGenerator(request) {
83
- if (!request.yar.id) {
84
- throw new Error('No session ID found');
85
- }
86
- const state = request.params.state || '';
87
- const slug = request.params.slug || '';
88
- return `${request.yar.id}:${state}:${slug}:`;
89
- }
90
64
 
91
65
  /**
92
66
  * The key used to store user session data against.
@@ -95,10 +69,15 @@ export class CacheService {
95
69
  * @param additionalIdentifier - appended to the id
96
70
  */
97
71
  Key(request, additionalIdentifier) {
98
- const baseKey = this.generateKey ? this.generateKey(request) : this.defaultKeyGenerator(request);
72
+ if (!request.yar.id) {
73
+ throw new Error('No session ID found');
74
+ }
75
+ const state = request.params.state || '';
76
+ const slug = request.params.slug || '';
77
+ const key = `${request.yar.id}:${state}:${slug}:`;
99
78
  return {
100
79
  segment: partition,
101
- id: `${baseKey}${additionalIdentifier ?? ''}`
80
+ id: `${key}${additionalIdentifier ?? ''}`
102
81
  };
103
82
  }
104
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cacheService.js","names":["Hoek","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","generateKey","customFetcher","logger","constructor","server","cacheName","options","keyGenerator","sessionHydrator","log","defaultKeyGenerator","bind","undefined","segment","getState","request","key","Key","cached","get","rehydrated","set","setState","state","ttl","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","Error","params","slug","additionalIdentifier","baseKey","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Request, type Server } from '@hapi/hapi'\nimport * as Hoek from '@hapi/hoek'\n\nimport { config } from '~/src/config/index.js'\nimport { type createServer } from '~/src/server/index.js'\nimport {\n type FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\n\nconst partition = 'cache'\n\nexport enum ADDITIONAL_IDENTIFIER {\n Confirmation = ':confirmation'\n}\n\nexport class CacheService {\n /**\n * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}\n */\n cache\n generateKey?: (request: Request | FormRequest | FormRequestPayload) => string\n customFetcher?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n\n logger: Server['logger']\n\n constructor({\n server,\n cacheName,\n options\n }: {\n server: Server\n cacheName?: string\n options?: {\n keyGenerator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n }\n }) {\n const { keyGenerator, sessionHydrator } = options ?? {}\n if (!cacheName) {\n server.log(\n 'warn',\n 'You are using the default hapi cache. Please provide a cache name in plugin registration options.'\n )\n }\n this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this)\n this.customFetcher = sessionHydrator ?? undefined\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(\n request: Request | FormRequest | FormRequestPayload\n ): Promise<FormSubmissionState> {\n const key = this.Key(request)\n\n let cached = await this.cache.get(key)\n\n // If nothing in Redis, attempt to rehydrate from backend DB\n if (!cached && this.customFetcher) {\n const rehydrated = await this.customFetcher(request)\n\n if (rehydrated != null) {\n await this.cache.set(key, rehydrated, config.get('sessionTimeout'))\n cached = await this.getState(request)\n }\n }\n\n return cached ?? {}\n }\n\n async setState(\n request: FormRequest | FormRequestPayload,\n state: FormSubmissionState\n ) {\n const key = this.Key(request)\n const ttl = config.get('sessionTimeout')\n\n await this.cache.set(key, state, ttl)\n\n return this.getState(request)\n }\n\n async getConfirmationState(\n request: FormRequest | FormRequestPayload\n ): Promise<{ confirmed?: true }> {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const value = await this.cache.get(key)\n\n return value ?? {}\n }\n\n async setConfirmationState(\n request: FormRequest | FormRequestPayload,\n confirmationState: { confirmed?: true }\n ) {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const ttl = config.get('confirmationSessionTimeout')\n\n return this.cache.set(key, confirmationState, ttl)\n }\n\n async clearState(request: FormRequest | FormRequestPayload) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: FormRequest | FormRequestPayload\n ): { errors: FormSubmissionError[] } | undefined {\n const key = this.Key(request)\n const messages = request.yar.flash(key.id)\n\n if (Array.isArray(messages) && messages.length) {\n return messages.at(0) as { errors: FormSubmissionError[] }\n }\n }\n\n setFlash(\n request: FormRequest | FormRequestPayload,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n private defaultKeyGenerator(\n request: Request | FormRequest | FormRequestPayload\n ): string {\n if (!request.yar.id) {\n throw new Error('No session ID found')\n }\n\n const state = (request.params.state as string) || ''\n const slug = (request.params.slug as string) || ''\n return `${request.yar.id}:${state}:${slug}:`\n }\n\n /**\n * The key used to store user session data against.\n * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`\n * @param request - hapi request object\n * @param additionalIdentifier - appended to the id\n */\n Key(\n request: Request | FormRequest | FormRequestPayload,\n additionalIdentifier?: ADDITIONAL_IDENTIFIER\n ) {\n const baseKey = this.generateKey\n ? this.generateKey(request)\n : this.defaultKeyGenerator(request)\n\n return {\n segment: partition,\n id: `${baseKey}${additionalIdentifier ?? ''}`\n }\n }\n}\n\n/**\n * State merge helper\n * 1. Merges objects (form fields)\n * 2. Overwrites arrays\n */\nexport function merge<StateType extends FormState | FormPayload>(\n state: StateType,\n update: object\n): StateType {\n return Hoek.merge(state, update, {\n mergeArrays: false\n })\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"cacheService.js","names":["Hoek","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","logger","constructor","server","cacheName","log","segment","getState","request","key","Key","cached","get","setState","state","ttl","set","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","additionalIdentifier","Error","params","slug","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Server } from '@hapi/hapi'\nimport * as Hoek from '@hapi/hoek'\n\nimport { config } from '~/src/config/index.js'\nimport { type createServer } from '~/src/server/index.js'\nimport {\n type AnyFormRequest,\n type AnyRequest,\n type FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nconst partition = 'cache'\n\nexport enum ADDITIONAL_IDENTIFIER {\n Confirmation = ':confirmation'\n}\n\nexport class CacheService {\n /**\n * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}\n */\n cache\n logger: Server['logger']\n\n constructor({ server, cacheName }: { server: Server; cacheName?: string }) {\n if (!cacheName) {\n server.log(\n 'warn',\n 'You are using the default hapi cache. Please provide a cache name in plugin registration options.'\n )\n }\n\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(request: AnyRequest): Promise<FormSubmissionState> {\n const key = this.Key(request)\n const cached = await this.cache.get(key)\n\n return cached ?? {}\n }\n\n async setState(request: AnyFormRequest, state: FormSubmissionState) {\n const key = this.Key(request)\n const ttl = config.get('sessionTimeout')\n\n await this.cache.set(key, state, ttl)\n\n return this.getState(request)\n }\n\n async getConfirmationState(\n request: AnyFormRequest\n ): Promise<{ confirmed?: true }> {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const value = await this.cache.get(key)\n\n return value ?? {}\n }\n\n async setConfirmationState(\n request: AnyFormRequest,\n confirmationState: { confirmed?: true }\n ) {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const ttl = config.get('confirmationSessionTimeout')\n\n return this.cache.set(key, confirmationState, ttl)\n }\n\n async clearState(request: AnyFormRequest) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: AnyFormRequest\n ): { errors: FormSubmissionError[] } | undefined {\n const key = this.Key(request)\n const messages = request.yar.flash(key.id)\n\n if (Array.isArray(messages) && messages.length) {\n return messages.at(0) as { errors: FormSubmissionError[] }\n }\n }\n\n setFlash(\n request: AnyFormRequest,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n /**\n * The key used to store user session data against.\n * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`\n * @param request - hapi request object\n * @param additionalIdentifier - appended to the id\n */\n Key(request: AnyRequest, additionalIdentifier?: ADDITIONAL_IDENTIFIER) {\n if (!request.yar.id) {\n throw new Error('No session ID found')\n }\n\n const state = (request.params.state as string) || ''\n const slug = (request.params.slug as string) || ''\n const key = `${request.yar.id}:${state}:${slug}:`\n\n return {\n segment: partition,\n id: `${key}${additionalIdentifier ?? ''}`\n }\n }\n}\n\n/**\n * State merge helper\n * 1. Merges objects (form fields)\n * 2. Overwrites arrays\n */\nexport function merge<StateType extends FormState | FormPayload>(\n state: StateType,\n update: object\n): StateType {\n return Hoek.merge(state, update, {\n mergeArrays: false\n })\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,9 +1,11 @@
1
1
  import { type FormDefinition, type FormMetadata, type SubmitPayload, type SubmitResponsePayload } from '@defra/forms-model';
2
+ import { type Server } from '@hapi/hapi';
2
3
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js';
3
4
  import { type DetailItem } from '~/src/server/plugins/engine/models/types.js';
4
5
  import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js';
5
6
  import { type FormContext, type OnRequestCallback, type PluginOptions, type PreparePageEventRequestOptions } from '~/src/server/plugins/engine/types.js';
6
7
  import { type FormRequestPayload, type FormStatus } from '~/src/server/routes/types.js';
8
+ import { type CacheService } from '~/src/server/services/cacheService.js';
7
9
  export interface FormsService {
8
10
  getFormMetadata: (slug: string) => Promise<FormMetadata>;
9
11
  getFormDefinition: (id: string, state: FormStatus) => Promise<FormDefinition | undefined>;
@@ -28,7 +30,8 @@ export interface RouteConfig {
28
30
  controllers?: Record<string, typeof PageController>;
29
31
  preparePageEventRequestOptions?: PreparePageEventRequestOptions;
30
32
  onRequest?: OnRequestCallback;
31
- saveAndReturn?: PluginOptions['saveAndReturn'];
33
+ saveAndExit?: PluginOptions['saveAndExit'];
34
+ cacheServiceCreator?: (server: Server) => CacheService;
32
35
  }
33
36
  export interface OutputService {
34
37
  submit: (context: FormContext, request: FormRequestPayload, model: FormModel, emailAddress: string, items: DetailItem[], submitResponse: SubmitResponsePayload, formMetadata?: FormMetadata) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../src/server/types.ts"],"sourcesContent":["import {\n type FormDefinition,\n type FormMetadata,\n type SubmitPayload,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n type FormContext,\n type OnRequestCallback,\n type PluginOptions,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequestPayload,\n type FormStatus\n} from '~/src/server/routes/types.js'\n\nexport interface FormsService {\n getFormMetadata: (slug: string) => Promise<FormMetadata>\n getFormDefinition: (\n id: string,\n state: FormStatus\n ) => Promise<FormDefinition | undefined>\n}\n\nexport interface FormSubmissionService {\n persistFiles: (\n files: { fileId: string; initiatedRetrievalKey: string }[],\n persistedRetrievalKey: string\n ) => Promise<object>\n submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>\n}\n\nexport interface Services {\n formsService: FormsService\n formSubmissionService: FormSubmissionService\n outputService: OutputService\n}\n\nexport interface RouteConfig {\n formFileName?: string\n formFilePath?: string\n enforceCsrf?: boolean\n services?: Services\n controllers?: Record<string, typeof PageController>\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n saveAndReturn?: PluginOptions['saveAndReturn']\n}\n\nexport interface OutputService {\n submit: (\n context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload,\n formMetadata?: FormMetadata\n ) => Promise<void>\n}\n"],"mappings":"","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/server/types.ts"],"sourcesContent":["import {\n type FormDefinition,\n type FormMetadata,\n type SubmitPayload,\n type SubmitResponsePayload\n} from '@defra/forms-model'\nimport { type Server } from '@hapi/hapi'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'\nimport {\n type FormContext,\n type OnRequestCallback,\n type PluginOptions,\n type PreparePageEventRequestOptions\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequestPayload,\n type FormStatus\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/cacheService.js'\n\nexport interface FormsService {\n getFormMetadata: (slug: string) => Promise<FormMetadata>\n getFormDefinition: (\n id: string,\n state: FormStatus\n ) => Promise<FormDefinition | undefined>\n}\n\nexport interface FormSubmissionService {\n persistFiles: (\n files: { fileId: string; initiatedRetrievalKey: string }[],\n persistedRetrievalKey: string\n ) => Promise<object>\n submit: (data: SubmitPayload) => Promise<SubmitResponsePayload | undefined>\n}\n\nexport interface Services {\n formsService: FormsService\n formSubmissionService: FormSubmissionService\n outputService: OutputService\n}\n\nexport interface RouteConfig {\n formFileName?: string\n formFilePath?: string\n enforceCsrf?: boolean\n services?: Services\n controllers?: Record<string, typeof PageController>\n preparePageEventRequestOptions?: PreparePageEventRequestOptions\n onRequest?: OnRequestCallback\n saveAndExit?: PluginOptions['saveAndExit']\n cacheServiceCreator?: (server: Server) => CacheService\n}\n\nexport interface OutputService {\n submit: (\n context: FormContext,\n request: FormRequestPayload,\n model: FormModel,\n emailAddress: string,\n items: DetailItem[],\n submitResponse: SubmitResponsePayload,\n formMetadata?: FormMetadata\n ) => Promise<void>\n}\n"],"mappings":"","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.js","names":[],"sources":["../../../src/typings/hapi/index.d.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/unified-signatures */\n\nimport { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { type PluginOptions } from '~/src/server/plugins/engine/types.ts'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\nimport { type CacheService } from '~/src/server/services/index.js'\n\ndeclare module '@hapi/hapi' {\n // Here we are decorating Hapi interface types with\n // props from plugins which doesn't export @types\n interface PluginProperties {\n crumb: {\n generate?: (request: Request | FormRequest | FormRequestPayload) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: FormRequest | FormRequestPayload | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\n saveAndReturn?: PluginOptions['saveAndReturn']\n }\n }\n\n interface Request {\n logger: Logger\n yar: Yar\n }\n\n interface RequestApplicationState {\n model?: FormModel\n }\n\n interface Server {\n logger: Logger\n yar: ServerYar\n }\n\n interface ServerApplicationState {\n model?: FormModel\n models: Map<string, { model: FormModel; updatedAt: Date }>\n }\n}\n\ndeclare module '@hapi/scooter' {\n declare const hapiScooter: {\n plugin: Plugin\n }\n\n export = hapiScooter\n}\n\ndeclare module 'blankie' {\n declare const blankie: {\n plugin: Plugin<Record<string, boolean | string | string[]>>\n }\n\n export = blankie\n}\n\ndeclare module 'blipp' {\n declare const blipp: {\n plugin: Plugin\n }\n\n export = blipp\n}\n\ndeclare module 'hapi-pulse' {\n declare const hapiPulse: {\n plugin: Plugin<{\n timeout: number\n }>\n }\n\n export = hapiPulse\n}\n"],"mappings":"","ignoreList":[]}
1
+ {"version":3,"file":"index.d.js","names":[],"sources":["../../../src/typings/hapi/index.d.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/unified-signatures */\n\nimport { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type AnyFormRequest,\n type PluginOptions\n} from '~/src/server/plugins/engine/types.ts'\nimport { type CacheService } from '~/src/server/services/index.js'\n\ndeclare module '@hapi/hapi' {\n // Here we are decorating Hapi interface types with\n // props from plugins which doesn't export @types\n interface PluginProperties {\n crumb: {\n generate?: (request: AnyRequest) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: AnyFormRequest | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\n saveAndExit?: PluginOptions['saveAndExit']\n }\n }\n\n interface Request {\n logger: Logger\n yar: Yar\n }\n\n interface RequestApplicationState {\n model?: FormModel\n }\n\n interface Server {\n logger: Logger\n yar: ServerYar\n }\n\n interface ServerApplicationState {\n model?: FormModel\n models: Map<string, { model: FormModel; updatedAt: Date }>\n }\n}\n\ndeclare module '@hapi/scooter' {\n declare const hapiScooter: {\n plugin: Plugin\n }\n\n export = hapiScooter\n}\n\ndeclare module 'blankie' {\n declare const blankie: {\n plugin: Plugin<Record<string, boolean | string | string[]>>\n }\n\n export = blankie\n}\n\ndeclare module 'blipp' {\n declare const blipp: {\n plugin: Plugin\n }\n\n export = blipp\n}\n\ndeclare module 'hapi-pulse' {\n declare const hapiPulse: {\n plugin: Plugin<{\n timeout: number\n }>\n }\n\n export = hapiPulse\n}\n"],"mappings":"","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "2.1.10",
3
+ "version": "3.0.0",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -28,7 +28,9 @@
28
28
  "./services/*": "./.server/server/plugins/engine/services/*",
29
29
  "./engine/*": "./.server/server/plugins/engine/*",
30
30
  "./helpers.js": "./.server/server/plugins/engine/components/helpers.js",
31
+ "./schema.js": "./.server/server/schemas/index.js",
31
32
  "./templates/*": "./.server/server/plugins/engine/views/*",
33
+ "./cache-service.js": "./.server/server/services/cacheService.js",
32
34
  "./package.json": "./package.json"
33
35
  },
34
36
  "scripts": {
@@ -639,42 +639,3 @@ describe('prepareEnvironment', () => {
639
639
  )
640
640
  })
641
641
  })
642
-
643
- describe('Exit route handlers', () => {
644
- let server: Server
645
-
646
- beforeAll(async () => {
647
- server = await createServer({
648
- services: defaultServices
649
- })
650
- await server.initialize()
651
- })
652
-
653
- afterAll(async () => {
654
- await server.stop()
655
- })
656
-
657
- beforeEach(() => {
658
- jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata)
659
- server.app.models.clear()
660
- })
661
-
662
- test('GET /exit returns 200 with exit page content', async () => {
663
- jest.mocked(getFormMetadata).mockResolvedValueOnce({
664
- ...fixtures.form.metadata,
665
- live: fixtures.form.state
666
- })
667
-
668
- jest.mocked(getFormDefinition).mockResolvedValue(fixtures.form.definition)
669
-
670
- const options = {
671
- method: 'GET',
672
- url: `${FORM_PREFIX}/slug/exit`
673
- }
674
-
675
- const res = await server.inject(options)
676
-
677
- expect(res.statusCode).toBe(StatusCodes.OK)
678
- expect(res.result).toContain('Your progress has been saved')
679
- })
680
- })
@@ -82,8 +82,11 @@ export async function createServer(routeConfig?: RouteConfig) {
82
82
  prepareSecureContext(server)
83
83
  }
84
84
 
85
+ const cacheService = routeConfig?.cacheServiceCreator
86
+ ? routeConfig.cacheServiceCreator(server)
87
+ : undefined
85
88
  const pluginCrumb = configureCrumbPlugin(routeConfig)
86
- const pluginEngine = await configureEnginePlugin(routeConfig)
89
+ const pluginEngine = await configureEnginePlugin(routeConfig, cacheService)
87
90
 
88
91
  await server.register(pluginSession)
89
92
  await server.register(pluginPulse)
@@ -86,9 +86,9 @@ There are a number of `LiquidJS` filters available to you from within the templa
86
86
  ]
87
87
  ```
88
88
 
89
- ### Save and return
89
+ ### Save and exit
90
90
 
91
- See [our save and return feature page](/docs/features/code-based/SAVE_AND_RETURN.md).
91
+ See [our save and exit feature page](/docs/features/code-based/SAVE_AND_EXIT.md).
92
92
 
93
93
  ### Additional notes
94
94
 
@@ -25,7 +25,7 @@ describe('helpers tests', () => {
25
25
  })
26
26
 
27
27
  describe('ComponentBase tests', () => {
28
- test('should handle save and return functionality', () => {
28
+ test('should handle save and exit functionality', () => {
29
29
  const mockComponentDef = {
30
30
  type: 'TextField',
31
31
  name: 'testField',
@@ -10,17 +10,21 @@ import { formsService } from '~/src/server/plugins/engine/services/localFormsSer
10
10
  import { type PluginOptions } from '~/src/server/plugins/engine/types.js'
11
11
  import { findPackageRoot } from '~/src/server/plugins/engine/vision.js'
12
12
  import { devtoolContext } from '~/src/server/plugins/nunjucks/context.js'
13
+ import { type CacheService } from '~/src/server/services/cacheService.js'
13
14
  import { type RouteConfig } from '~/src/server/types.js'
14
15
 
15
- export const configureEnginePlugin = async ({
16
- formFileName,
17
- formFilePath,
18
- services,
19
- controllers,
20
- preparePageEventRequestOptions,
21
- onRequest,
22
- saveAndReturn
23
- }: RouteConfig = {}): Promise<{
16
+ export const configureEnginePlugin = async (
17
+ {
18
+ formFileName,
19
+ formFilePath,
20
+ services,
21
+ controllers,
22
+ preparePageEventRequestOptions,
23
+ onRequest,
24
+ saveAndExit
25
+ }: RouteConfig = {},
26
+ cache?: CacheService
27
+ ): Promise<{
24
28
  plugin: typeof plugin
25
29
  options: PluginOptions
26
30
  }> => {
@@ -50,7 +54,7 @@ export const configureEnginePlugin = async ({
50
54
  formsService: await formsService()
51
55
  },
52
56
  controllers,
53
- cacheName: 'session',
57
+ cache: cache ?? 'session',
54
58
  nunjucks: {
55
59
  baseLayoutPath: 'dxt-devtool-baselayout.html',
56
60
  paths: [join(findPackageRoot(), 'src/server/devserver')] // custom layout to make it really clear this is not the same as the runner
@@ -59,7 +63,7 @@ export const configureEnginePlugin = async ({
59
63
  preparePageEventRequestOptions,
60
64
  onRequest,
61
65
  baseUrl: 'http://localhost:3009', // always runs locally
62
- saveAndReturn
66
+ saveAndExit
63
67
  }
64
68
  }
65
69
  }
@@ -30,7 +30,8 @@ import {
30
30
  import {
31
31
  FormAction,
32
32
  FormStatus,
33
- type FormRequest
33
+ type FormRequest,
34
+ type FormResponseToolkit
34
35
  } from '~/src/server/routes/types.js'
35
36
  import definition from '~/test/form/definitions/basic.js'
36
37
  import templateDefinition from '~/test/form/definitions/templates.js'
@@ -47,7 +48,7 @@ type HrefFilter = (this: NunjucksContext, path: string) => string | undefined
47
48
  describe('Helpers', () => {
48
49
  let page: PageControllerClass
49
50
  let request: FormContextRequest
50
- let h: Pick<ResponseToolkit, 'redirect' | 'view'>
51
+ let h: FormResponseToolkit
51
52
 
52
53
  beforeEach(() => {
53
54
  const model = new FormModel(definition, {