@gov-cy/govcy-express-services 0.2.15 β†’ 1.0.0-alpha.1

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/README.md CHANGED
@@ -32,6 +32,7 @@ The project is designed to support the [Linear structure](https://gov-cy.github.
32
32
  - [πŸ“€ Site submissions](#-site-submissions)
33
33
  - [βœ… Input validations](#-input-validations)
34
34
  - [βœ… Conditional logic](#-conditional-logic)
35
+ - [πŸ’Ύ Temporary save](#-temporary-save)
35
36
  - [πŸ›£οΈ Routes](#%EF%B8%8F-routes)
36
37
  - [πŸ‘¨β€πŸ’» Enviromental variables](#-enviromental-variables)
37
38
  - [πŸ”’ Security note](#-security-note)
@@ -55,6 +56,7 @@ The project is designed to support the [Linear structure](https://gov-cy.github.
55
56
  - Pre-filling posted values (in the same session)
56
57
  - Site level API eligibility checks
57
58
  - API integration with retry logic for form submissions.
59
+ - Optional temporary save of in-progress form data via configurable API endpoints
58
60
 
59
61
  ## πŸ“‹ Prerequisites
60
62
  - Node.js 20+
@@ -1412,6 +1414,55 @@ Explanation:
1412
1414
  - `[].concat(...)`: safely flattens a string or array into an array.
1413
1415
  - `.includes('value1')`: checks if the value is selected.
1414
1416
 
1417
+ ### πŸ’Ύ Temporary save
1418
+
1419
+ The **temporary save** feature allows user progress to be stored in an external API and automatically reloaded on the next visit.
1420
+ This is useful for long forms or cases where users may leave and return later.
1421
+
1422
+ #### 1. Configure the endpoints in your service JSON
1423
+ In your service’s `site` object, add both a `submissionGetAPIEndpoint` and `submissionPutAPIEndpoint` entry:
1424
+
1425
+ ```json
1426
+ "submissionGetAPIEndpoint": {
1427
+ "url": "TEST_SUBMISSION_GET_API_URL",
1428
+ "method": "GET",
1429
+ "clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
1430
+ "serviceId": "TEST_SUBMISSION_API_SERVICE_ID"
1431
+ },
1432
+ "submissionPutAPIEndpoint": {
1433
+ "url": "TEST_SUBMISSION_PUT_API_URL",
1434
+ "method": "PUT",
1435
+ "clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
1436
+ "serviceId": "TEST_SUBMISSION_API_SERVICE_ID"
1437
+ }
1438
+ ```
1439
+
1440
+ These values should point to environment variables that hold your real endpoint URLs and credentials.
1441
+
1442
+ #### 2. Add environment variables
1443
+ In your `secrets/.env` file (and staging/production configs), define the variables referenced above:
1444
+
1445
+ ```dotenv
1446
+ TEST_SUBMISSION_GET_API_URL=https://example.com/api/submissionData
1447
+ TEST_SUBMISSION_PUT_API_URL=https://example.com/api/submissionData
1448
+ TEST_SUBMISSION_API_CLIENT_KEY=12345678901234567890123456789000
1449
+ TEST_SUBMISSION_API_SERVICE_ID=123
1450
+ ```
1451
+
1452
+ #### 3. How it works
1453
+
1454
+ - **On first page load** for a site, `govcyLoadSubmissionData` will:
1455
+ 1. Call the GET endpoint to retrieve any saved submission.
1456
+ 2. If found, populate the session’s `inputData` so fields are pre-filled.
1457
+ 3. If not found, call the PUT endpoint to create a new temporary record.
1458
+ - **On every form POST**, after successful validation:
1459
+ - The `govcyFormsPostHandler` will fire-and-forget a `PUT` request to update the saved submission with the latest form data.
1460
+ - The payload includes all required submission fields with `submission_data` JSON-stringified.
1461
+
1462
+ #### 4. Backward compatibility
1463
+ If these endpoints are not defined in the service JSON, the temporary save/load logic is skipped entirely.
1464
+ Existing services will continue to work without modification.
1465
+
1415
1466
  ### πŸ›£οΈ Routes
1416
1467
  The project uses express.js to serve the following routes:
1417
1468
 
@@ -1500,6 +1551,11 @@ TEST_SUBMISSION_API_CLIENT_KEY=12345678901234567890123456789000
1500
1551
  TEST_SUBMISSION_API_SERVIVE_ID=123
1501
1552
  TEST_SUBMISSION_DSF_GTW_KEY=12345678901234567890123456789000
1502
1553
 
1554
+ # Optional Temporary Save GET and PUT endpoint (test service)
1555
+ TEST_SUBMISSION_GET_API_URL=http://localhost:3002/getTempSubmission
1556
+ TEST_SUBMISSION_PUT_API_URL=http://localhost:3002/save
1557
+
1558
+
1503
1559
  # Eligibility checks (optional test APIs)
1504
1560
  TEST_ELIGIBILITY_1_API_URL=http://localhost:3002/eligibility1
1505
1561
  TEST_ELIGIBILITY_2_API_URL=http://localhost:3002/eligibility2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "0.2.15",
3
+ "version": "1.0.0-alpha.1",
4
4
  "description": "An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.",
5
5
  "author": "DMRID - DSF Team",
6
6
  "license": "MIT",
package/src/index.mjs CHANGED
@@ -23,6 +23,7 @@ import { serviceConfigDataMiddleware } from './middleware/govcyConfigSiteData.mj
23
23
  import { govcyManifestHandler } from './middleware/govcyManifestHandler.mjs';
24
24
  import { govcyRoutePageHandler } from './middleware/govcyRoutePageHandler.mjs';
25
25
  import { govcyServiceEligibilityHandler } from './middleware/govcyServiceEligibilityHandler.mjs';
26
+ import { govcyLoadSubmissionData } from './middleware/govcyLoadSubmissionData.mjs';
26
27
  import { isProdOrStaging , getEnvVariable, whatsIsMyEnvironment } from './utils/govcyEnvVariables.mjs';
27
28
  import { logger } from "./utils/govcyLogger.mjs";
28
29
 
@@ -129,10 +130,10 @@ export default function initializeGovCyExpressService(){
129
130
  app.get('/:siteId/manifest.json', serviceConfigDataMiddleware, govcyManifestHandler());
130
131
 
131
132
  // 🏠 -- ROUTE: Handle route with only siteId (/:siteId or /:siteId/)
132
- app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true),govcyPageHandler(), renderGovcyPage());
133
+ app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true),govcyLoadSubmissionData(),govcyPageHandler(), renderGovcyPage());
133
134
 
134
135
  // πŸ‘€ -- ROUTE: Add Review Page Route (BEFORE the dynamic route)
135
- app.get('/:siteId/review',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPageHandler(), renderGovcyPage());
136
+ app.get('/:siteId/review',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(),govcyLoadSubmissionData(), govcyReviewPageHandler(), renderGovcyPage());
136
137
 
137
138
  // βœ…πŸ“„ -- ROUTE: Add Success PDF Route (BEFORE the dynamic route)
138
139
  app.get('/:siteId/success/pdf',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(true), govcyPDFRender());
@@ -141,7 +142,7 @@ export default function initializeGovCyExpressService(){
141
142
  app.get('/:siteId/success',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(), renderGovcyPage());
142
143
 
143
144
  // πŸ“ -- ROUTE: Dynamic route to render pages based on siteId and pageUrl, using govcyPageHandler middleware
144
- app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyPageHandler(), renderGovcyPage());
145
+ app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
145
146
 
146
147
  // πŸ“₯ -- ROUTE: Handle POST requests for review page. The `submit` action
147
148
  app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
@@ -6,6 +6,7 @@ import { logger } from "../utils/govcyLogger.mjs";
6
6
  import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
7
7
  import { getFormData } from "../utils/govcyFormHandling.mjs"
8
8
  import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
9
+ import { tempSaveIfConfigured } from "../utils/govcyTempSave.mjs";
9
10
 
10
11
 
11
12
  /**
@@ -59,7 +60,12 @@ export function govcyFormsPostHandler() {
59
60
 
60
61
  //‴️ Store validated form data in session
61
62
  dataLayer.storePageData(req.session, siteId, pageUrl, formData);
62
-
63
+
64
+ // πŸ”„ Fire-and-forget temporary save (non-blocking)
65
+ (async () => {
66
+ try { await tempSaveIfConfigured(req.session, service, siteId); }
67
+ catch (e) { /* already logged internally */ }
68
+ })();
63
69
 
64
70
  logger.debug("βœ… Form submitted successfully:", dataLayer.getPageData(req.session, siteId, pageUrl), req);
65
71
  logger.info("βœ… Form submitted successfully:", req.originalUrl);
@@ -0,0 +1,22 @@
1
+ import { logger } from "../utils/govcyLogger.mjs";
2
+ import { govcyLoadSubmissionDataAPIs } from "../utils/govcyLoadSubmissionDataAPIs.mjs";
3
+ /**
4
+ * Middleware to load submission data from APIs.
5
+ * This middleware fetches submission data from configured APIs and stores it in the session.
6
+ * @returns {function} Middleware function to load submission data from APIs
7
+ */
8
+ export function govcyLoadSubmissionData() {
9
+ return async (req, res, next) => {
10
+ try {
11
+ const service = req.serviceData;
12
+ // Extract siteId from request
13
+ const { siteId } = req.params;
14
+
15
+ return await govcyLoadSubmissionDataAPIs(req.session, service, siteId, next);
16
+
17
+ } catch (error) {
18
+ logger.error("Error in govcyLoadSubmissionData middleware:", error.message);
19
+ return next(error); // Pass the error to the next middleware
20
+ }
21
+ }
22
+ }
@@ -16,6 +16,7 @@
16
16
  export function initializeSiteData(store, siteId, pageUrl = null) {
17
17
  if (!store.siteData) store.siteData = {};
18
18
  if (!store.siteData[siteId]) store.siteData[siteId] = {};
19
+ if (!store.siteData[siteId].inputData) store.siteData[siteId].loadData = {};
19
20
  if (!store.siteData[siteId].inputData) store.siteData[siteId].inputData = {};
20
21
  if (!store.siteData[siteId].submissionData) store.siteData[siteId].submissionData = {};
21
22
 
@@ -97,6 +98,33 @@ export function storePageData(store, siteId, pageUrl, formData) {
97
98
 
98
99
  store.siteData[siteId].inputData[pageUrl]["formData"] = formData;
99
100
  }
101
+ /**
102
+ * Stores the page's input data in the data layer
103
+ * *
104
+ * @param {object} store The session store
105
+ * @param {string} siteId The site id
106
+ * @param {object} loadData The form data to be stored
107
+ */
108
+ export function storeSiteInputData(store, siteId, loadData) {
109
+ // Ensure session structure is initialized
110
+ initializeSiteData(store, siteId);
111
+
112
+ store.siteData[siteId]["inputData"] = loadData;
113
+ }
114
+
115
+ /**
116
+ * Stores the page's load data in the data layer
117
+ * *
118
+ * @param {object} store The session store
119
+ * @param {string} siteId The site id
120
+ * @param {object} loadData The form data to be stored
121
+ */
122
+ export function storeSiteLoadData(store, siteId, loadData) {
123
+ // Ensure session structure is initialized
124
+ initializeSiteData(store, siteId);
125
+
126
+ store.siteData[siteId]["loadData"] = loadData;
127
+ }
100
128
 
101
129
  /**
102
130
  * Stores the site validation errors in the data layer
@@ -156,8 +184,11 @@ export function storeSiteSubmissionData(store, siteId, submissionData) {
156
184
  // Store the submission data
157
185
  store.siteData[siteId].submissionData = submissionData;
158
186
 
159
- // Clear validation errors from the session
187
+ // Clear validation errors from the session
160
188
  store.siteData[siteId].inputData = {};
189
+ // Clear presaved/temporary save data
190
+ store.siteData[siteId].loadData = {};
191
+
161
192
  }
162
193
 
163
194
 
@@ -280,6 +311,23 @@ export function getSiteInputData(store, siteId) {
280
311
  return null;
281
312
  }
282
313
 
314
+ /**
315
+ * Get the site's load data from the store
316
+ *
317
+ * @param {object} store The session store
318
+ * @param {string} siteId |The site id
319
+ * @returns The site load data or null if none exist.
320
+ */
321
+ export function getSiteLoadData(store, siteId) {
322
+ const loadData = store?.siteData?.[siteId]?.loadData || {};
323
+
324
+ if (loadData) {
325
+ return loadData;
326
+ }
327
+
328
+ return null;
329
+ }
330
+
283
331
  /**
284
332
  * Get the site's input data from the store
285
333
  *
@@ -0,0 +1,135 @@
1
+ import { govcyApiRequest } from "./govcyApiRequest.mjs";
2
+ import { logger } from "./govcyLogger.mjs";
3
+ import { getEnvVariable, getEnvVariableBool } from "./govcyEnvVariables.mjs";
4
+ import { handleMiddlewareError } from "./govcyUtils.mjs";
5
+ import * as dataLayer from "./govcyDataLayer.mjs";
6
+
7
+ /**
8
+ * Load submission data from configured APIs and store it in the session.
9
+ * @param {object} store The session store
10
+ * @param {object} service The service configuration
11
+ * @param {string} siteId The site id
12
+ * @param {function} next The next middleware function
13
+ */
14
+ export async function govcyLoadSubmissionDataAPIs(store, service, siteId, next) {
15
+ try {
16
+
17
+ // Get the API endpoints
18
+ const getCfg = service?.site?.submissionGetAPIEndpoint;
19
+ const putCfg = service?.site?.submissionPutAPIEndpoint;
20
+
21
+ //If siteLoadData already exists, skip the API call
22
+ const siteLoadData = dataLayer.getSiteLoadData(store, siteId);
23
+ if (siteLoadData && Object.keys(siteLoadData).length > 0) {
24
+ // Data exists, skip API call
25
+ logger.debug("Load data already exists for site:", siteId);
26
+ return next();
27
+ }
28
+
29
+ // Only continue if both endpoints and required fields are defined
30
+ if (
31
+ getCfg && putCfg &&
32
+ getCfg.clientKey && getCfg.serviceId &&
33
+ putCfg.clientKey && putCfg.serviceId
34
+ ){
35
+ const user = dataLayer.getUser(store); // Get the user from the session;
36
+
37
+ // get the API endpoint URL, clientKey, serviceId from the environment variable (handle edge cases)
38
+ const getCfgUrl = getEnvVariable(getCfg?.url || "", false);
39
+ const getCfgClientKey = getEnvVariable(getCfg?.clientKey || "", false);
40
+ const getCfgServiceId = getEnvVariable(getCfg?.serviceId || "", false);
41
+ const getCfgDsfGtwApiKey = getEnvVariable(getCfg?.dsfgtwApiKey || "", "");
42
+ const getCfgParams = getCfg?.params || {};
43
+ const getCfgMethod = (getCfg?.method || "GET").toLowerCase();
44
+ const putCfgUrl = getEnvVariable(putCfg?.url || "", false);
45
+ const putCfgClientKey = getEnvVariable(putCfg?.clientKey || "", false);
46
+ const putCfgServiceId = getEnvVariable(putCfg?.serviceId || "", false);
47
+ const putCfgDsfGtwApiKey = getEnvVariable(putCfg?.dsfgtwApiKey || "", "");
48
+ const putCfgParams = putCfg?.params || {};
49
+ const putCfgMethod = (putCfg?.method || "PUT").toLowerCase();
50
+
51
+ const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES",false) ; // Default to false if not set
52
+
53
+ // check necessary values exist
54
+ if (!getCfgUrl || !getCfgClientKey
55
+ || !putCfgUrl || !putCfgClientKey ) {
56
+
57
+ return handleMiddlewareError(`🚨 Get submission environment variable missing:
58
+ getURl : ${getCfgUrl}, getClientKey : ${getCfgClientKey}
59
+ putURl : ${putCfgUrl}, putClientKey : ${putCfgClientKey}`
60
+ , 500, next)
61
+ }
62
+ // get response form GET submission API
63
+ const getResponse = await govcyApiRequest(
64
+ getCfgMethod,
65
+ getCfgUrl,
66
+ getCfgParams,
67
+ true, // use access token auth
68
+ user,
69
+ {
70
+ accept: "text/plain", // Set Accept header to text/plain
71
+ "client-key": getCfgClientKey, // Set the client key header
72
+ "service-id": getCfgServiceId, // Set the service ID header
73
+ ...(getCfgDsfGtwApiKey !== '' && { "dsfgtw-api-key": getCfgDsfGtwApiKey }) // Use the DSF API GTW secret from environment variables
74
+ },
75
+ 3,
76
+ allowSelfSignedCerts
77
+ );
78
+
79
+ // If not succeeded, handle error
80
+ if (!getResponse.Succeeded) {
81
+ logger.debug("govcyLoadSubmissionData returned succeeded false",getResponse)
82
+ return handleMiddlewareError(`🚨 govcyLoadSubmissionData returned succeeded false`, 500, next)
83
+ }
84
+
85
+ // check if getResponse.Data is defined
86
+ if (getResponse.Data) {
87
+ // Store the response in the request for later use
88
+ dataLayer.storeSiteLoadData(store, siteId, getResponse.Data);
89
+
90
+ try {
91
+ const parsed = JSON.parse(getResponse.Data.submissionData || "{}");
92
+ if (parsed && typeof parsed === "object") {
93
+ dataLayer.storeSiteInputData(store, siteId, parsed);
94
+ logger.debug(`πŸ’Ύ Input data restored from saved submission for siteId: ${siteId}`);
95
+ }
96
+ } catch (err) {
97
+ logger.warn(`⚠️ Failed to parse saved submissionData for siteId: ${siteId}`, err);
98
+ }
99
+
100
+ // if not call the PUT submission API
101
+ } else {
102
+ // If no data, call the PUT submission API to create it
103
+ const putResponse = await govcyApiRequest(
104
+ putCfgMethod,
105
+ putCfgUrl,
106
+ putCfgParams,
107
+ true, // use access token auth
108
+ user,
109
+ {
110
+ accept: "text/plain", // Set Accept header to text/plain
111
+ "client-key": putCfgClientKey, // Set the client key header
112
+ "service-id": putCfgServiceId, // Set the service ID header
113
+ ...(putCfgDsfGtwApiKey !== '' && { "dsfgtw-api-key": putCfgDsfGtwApiKey }) // Use the DSF API GTW secret from environment variables
114
+ },
115
+ 3,
116
+ allowSelfSignedCerts
117
+ );
118
+ // If not succeeded, handle error
119
+ if (!putResponse.Succeeded) {
120
+ logger.debug("govcyLoadSubmissionData returned succeeded false",putResponse)
121
+ return handleMiddlewareError(`🚨 govcyLoadSubmissionData returned succeeded false`, 500, next)
122
+ }
123
+ // Store the response in the request for later use
124
+ dataLayer.storeSiteLoadData(store, siteId, putResponse.Data);
125
+
126
+ }
127
+
128
+ }
129
+
130
+ next();
131
+ } catch (error) {
132
+ logger.error("Error in govcyLoadSubmissionData middleware:", error.message);
133
+ return next(error); // Pass the error to the next middleware
134
+ }
135
+ }
@@ -0,0 +1,70 @@
1
+ // utils/govcyTempSave.mjs
2
+ import { govcyApiRequest } from "./govcyApiRequest.mjs";
3
+ import { getEnvVariable, getEnvVariableBool } from "./govcyEnvVariables.mjs";
4
+ import * as dataLayer from "./govcyDataLayer.mjs";
5
+ import { logger } from "./govcyLogger.mjs";
6
+ /**
7
+ * Temporary save of in-progress form data via configured API endpoints.
8
+ * @param {object} store The session store
9
+ * @param {object} service The service object
10
+ * @param {string} siteId The site id
11
+ */
12
+ export async function tempSaveIfConfigured(store, service, siteId) {
13
+ // Check if temp save is configured for this service with a PUT endpoint
14
+ const putCfg = service?.site?.submissionPutAPIEndpoint;
15
+ if (!putCfg?.url || !putCfg?.clientKey || !putCfg?.serviceId) return;
16
+
17
+ //Get environment variables
18
+ const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
19
+ const url = getEnvVariable(putCfg.url || "", false);
20
+ const clientKey = getEnvVariable(putCfg.clientKey || "", false);
21
+ const serviceId = getEnvVariable(putCfg.serviceId || "", false);
22
+ const dsfGtwKey = getEnvVariable(putCfg?.dsfgtwApiKey || "", "");
23
+ const method = (putCfg?.method || "PUT").toLowerCase();
24
+ const user = dataLayer.getUser(store);
25
+
26
+ // Prepare minimal temp payload (send whole inputData snapshot)
27
+ const inputData = dataLayer.getSiteInputData(store, siteId) || {};
28
+ const tempPayload = {
29
+ // mirror final submission format: send stringified JSON
30
+ submission_data: JSON.stringify(inputData)
31
+ };
32
+
33
+ if (!url || !clientKey) {
34
+ logger.error("🚨 Temp save API configuration is incomplete:", { url, clientKey })
35
+ return; // don't break UX
36
+ }
37
+
38
+ try {
39
+ // Call the API to save temp data
40
+ const resp = await govcyApiRequest(
41
+ method,
42
+ url,
43
+ tempPayload,
44
+ true, // auth with user access token
45
+ user,
46
+ {
47
+ accept: "text/plain",
48
+ "client-key": clientKey,
49
+ "service-id": serviceId,
50
+ ...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey }),
51
+ "content-type": "application/json"
52
+ },
53
+ 3,
54
+ allowSelfSignedCerts
55
+ );
56
+
57
+ if (!resp?.Succeeded) {
58
+ logger.warn("Temp save returned Succeeded=false", resp);
59
+ return; // don’t break UX
60
+ }
61
+
62
+ logger.info("βœ… Temp save successful for site:", siteId, "Response:", resp);
63
+ // Optional: reflect any server state locally (e.g., keep referenceValue in loadData)
64
+ if (resp?.Data) {
65
+ dataLayer.storeSiteLoadData(store, siteId,resp.Data );
66
+ }
67
+ } catch (e) {
68
+ logger.error("Temp save failed (non-blocking):", e?.message);
69
+ }
70
+ }