@gov-cy/govcy-express-services 0.2.16 β†’ 1.0.0-alpha.10

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.
@@ -0,0 +1,142 @@
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
+ [200, 404] // Allowed HTTP status codes
78
+ );
79
+
80
+ // If not succeeded, handle error
81
+ if (!getResponse.Succeeded) {
82
+ logger.debug("govcyLoadSubmissionData returned succeeded false",getResponse)
83
+ return handleMiddlewareError(`🚨 govcyLoadSubmissionData returned succeeded false`, 500, next)
84
+ }
85
+
86
+ // check if getResponse.Data is defined
87
+ if (getResponse.Data) {
88
+ // Store the response in the request for later use
89
+ dataLayer.storeSiteLoadData(store, siteId, getResponse.Data);
90
+
91
+ try {
92
+ const parsed = JSON.parse(getResponse.Data.submissionData
93
+ || getResponse.Data.submission_data
94
+ || "{}");
95
+ if (parsed && typeof parsed === "object") {
96
+ dataLayer.storeSiteInputData(store, siteId, parsed);
97
+ logger.debug(`πŸ’Ύ Input data restored from saved submission for siteId: ${siteId}`);
98
+ }
99
+ } catch (err) {
100
+ logger.warn(`⚠️ Failed to parse saved submissionData for siteId: ${siteId}`, err);
101
+ }
102
+
103
+ // if not call the PUT submission API
104
+ } else {
105
+ const tempPutPayload = {
106
+ // submission_data: JSON.stringify({}),
107
+ submissionData: JSON.stringify({})
108
+ };
109
+ // If no data, call the PUT submission API to create it
110
+ const putResponse = await govcyApiRequest(
111
+ putCfgMethod,
112
+ putCfgUrl,
113
+ tempPutPayload,
114
+ true, // use access token auth
115
+ user,
116
+ {
117
+ accept: "text/plain", // Set Accept header to text/plain
118
+ "client-key": putCfgClientKey, // Set the client key header
119
+ "service-id": putCfgServiceId, // Set the service ID header
120
+ ...(putCfgDsfGtwApiKey !== '' && { "dsfgtw-api-key": putCfgDsfGtwApiKey }) // Use the DSF API GTW secret from environment variables
121
+ },
122
+ 3,
123
+ allowSelfSignedCerts
124
+ );
125
+ // If not succeeded, handle error
126
+ if (!putResponse.Succeeded) {
127
+ logger.debug("govcyLoadSubmissionData returned succeeded false",putResponse)
128
+ return handleMiddlewareError(`🚨 govcyLoadSubmissionData returned succeeded false`, 500, next)
129
+ }
130
+ // Store the response in the request for later use
131
+ dataLayer.storeSiteLoadData(store, siteId, putResponse.Data);
132
+
133
+ }
134
+
135
+ }
136
+
137
+ next();
138
+ } catch (error) {
139
+ logger.error("Error in govcyLoadSubmissionData middleware:", error.message);
140
+ return next(error); // Pass the error to the next middleware
141
+ }
142
+ }
@@ -66,6 +66,12 @@ export function prepareSubmissionData(req, siteId, service) {
66
66
  // Store in submissionData
67
67
  submissionData[pageUrl].formData[elId] = value;
68
68
 
69
+ // handle fileInput
70
+ if (elType === "fileInput") {
71
+ // change the name of the key to include "Attachment" at the end but not have the original key
72
+ submissionData[pageUrl].formData[elId + "Attachment"] = value;
73
+ delete submissionData[pageUrl].formData[elId];
74
+ }
69
75
 
70
76
  // πŸ”„ If radios with conditionalElements, walk ALL options
71
77
  if (elType === "radios" && Array.isArray(element.params?.items)) {
@@ -85,6 +91,12 @@ export function prepareSubmissionData(req, siteId, service) {
85
91
 
86
92
  // Store even if the field was not visible to user
87
93
  submissionData[pageUrl].formData[condId] = condValue;
94
+ // handle fileInput
95
+ if (condType === "fileInput") {
96
+ // change the name of the key to include "Attachment" at the end but not have the original key
97
+ submissionData[pageUrl].formData[condId + "Attachment"] = condValue;
98
+ delete submissionData[pageUrl].formData[condId];
99
+ }
88
100
  }
89
101
  }
90
102
  }
@@ -291,6 +303,10 @@ function getValue(formElement, pageUrl, req, siteId) {
291
303
  let value = ""
292
304
  if (formElement.element === "dateInput") {
293
305
  value = getDateInputISO(pageUrl, formElement.params.name, req, siteId);
306
+ } else if (formElement.element === "fileInput") {
307
+ // unneeded handle of `Attachment` at the end
308
+ // value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name + "Attachment");
309
+ value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
294
310
  } else {
295
311
  value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
296
312
  }
@@ -340,6 +356,17 @@ function getValueLabel(formElement, value, pageUrl, req, siteId, service) {
340
356
  return govcyResources.getSameMultilingualObject(service.site.languages, formattedDate);
341
357
  }
342
358
 
359
+ // handle fileInput
360
+ if (formElement.element === "fileInput") {
361
+ // TODO: Ask Andreas how to handle empty file inputs
362
+ if (value) {
363
+ return govcyResources.staticResources.text.fileUploaded;
364
+ } else {
365
+ return govcyResources.getSameMultilingualObject(service.site.languages, "");
366
+ // return govcyResources.staticResources.text.fileNotUploaded;
367
+ }
368
+ }
369
+
343
370
  // textInput, textArea, etc.
344
371
  return govcyResources.getSameMultilingualObject(service.site.languages, value);
345
372
  }
@@ -410,6 +437,33 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
410
437
  };
411
438
  }
412
439
 
440
+ /**
441
+ * Helper function to create a summary list item for file links.
442
+ * @param {object} key the key of multilingual object
443
+ * @param {string} value the value
444
+ * @param {string} siteId the site id
445
+ * @param {string} pageUrl the page url
446
+ * @param {string} elementName the element name
447
+ * @returns {object} the summary list item with file link
448
+ */
449
+ function createSummaryListItemFileLink(key, value, siteId, pageUrl, elementName) {
450
+ return {
451
+ "key": key,
452
+ "value": [
453
+ {
454
+ "element": "htmlElement",
455
+ "params": {
456
+ "text": {
457
+ "en": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.en}<span class="govcy-visually-hidden"> ${key?.en || ""}</span></a>`,
458
+ "el": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.el}<span class="govcy-visually-hidden"> ${key?.el || ""}</span></a>`,
459
+ "tr": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.tr}<span class="govcy-visually-hidden"> ${key?.tr || ""}</span></a>`
460
+ }
461
+ }
462
+ }
463
+ ]
464
+ };
465
+ }
466
+
413
467
 
414
468
 
415
469
 
@@ -425,8 +479,14 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
425
479
  for (const field of fields) {
426
480
  const label = field.label;
427
481
  const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang);
428
- // add the field to the summary entry
429
- summaryListInner.params.items.push(createSummaryListItem(label, valueLabel));
482
+ // --- HACK --- to see if this is a file element
483
+ // check if field.value is an object with `sha256` and `fileId` properties
484
+ if (typeof field.value === "object" && field.value.hasOwnProperty("sha256") && field.value.hasOwnProperty("fileId") && showChangeLinks) {
485
+ summaryListInner.params.items.push(createSummaryListItemFileLink(label, valueLabel, siteId, pageUrl, field.name));
486
+ } else {
487
+ // add the field to the summary entry
488
+ summaryListInner.params.items.push(createSummaryListItem(label, valueLabel));
489
+ }
430
490
  }
431
491
 
432
492
  // Add inner summary list to the main summary list
@@ -0,0 +1,71 @@
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
+ submissionData: JSON.stringify(inputData)
32
+ };
33
+
34
+ if (!url || !clientKey) {
35
+ logger.error("🚨 Temp save API configuration is incomplete:", { url, clientKey })
36
+ return; // don't break UX
37
+ }
38
+
39
+ try {
40
+ // Call the API to save temp data
41
+ const resp = await govcyApiRequest(
42
+ method,
43
+ url,
44
+ tempPayload,
45
+ true, // auth with user access token
46
+ user,
47
+ {
48
+ accept: "text/plain",
49
+ "client-key": clientKey,
50
+ "service-id": serviceId,
51
+ ...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey }),
52
+ "content-type": "application/json"
53
+ },
54
+ 3,
55
+ allowSelfSignedCerts
56
+ );
57
+
58
+ if (!resp?.Succeeded) {
59
+ logger.warn("Temp save returned Succeeded=false", resp);
60
+ return; // don’t break UX
61
+ }
62
+
63
+ logger.info("βœ… Temp save successful for site:", siteId, "Response:", resp);
64
+ // Optional: reflect any server state locally (e.g., keep referenceValue in loadData)
65
+ if (resp?.Data) {
66
+ dataLayer.storeSiteLoadData(store, siteId,resp.Data );
67
+ }
68
+ } catch (e) {
69
+ logger.error("Temp save failed (non-blocking):", e?.message);
70
+ }
71
+ }
@@ -263,6 +263,9 @@ export function validateFormElements(elements, formData, pageUrl) {
263
263
  formData[`${field.params.name}_day`]]
264
264
  .filter(Boolean) // Remove empty values
265
265
  .join("-") // Join remaining parts
266
+ // unneeded handle of `Attachment` at the end
267
+ // : (field.element === "fileInput") // Handle fileInput
268
+ // ? formData[`${field.params.name}Attachment`] || ""
266
269
  : formData[field.params.name] || ""; // Get submitted value
267
270
 
268
271
  //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items
@@ -310,6 +313,10 @@ export function validateFormElements(elements, formData, pageUrl) {
310
313
  formData[`${conditionalElement.params.name}_day`]]
311
314
  .filter(Boolean) // Remove empty values
312
315
  .join("-") // Join remaining parts
316
+ : (conditionalElement.element === "fileInput") // Handle fileInput
317
+ // unneeded handle of `Attachment` at the end
318
+ // ? formData[`${conditionalElement.params.name}Attachment`] || ""
319
+ ? formData[`${conditionalElement.params.name}`] || ""
313
320
  : formData[conditionalElement.params.name] || ""; // Get submitted value
314
321
 
315
322
  //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`