@gov-cy/govcy-express-services 1.0.0-alpha.8 → 1.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.
@@ -102,7 +102,7 @@ export function storePageData(store, siteId, pageUrl, formData) {
102
102
  export function storePageDataElement(store, siteId, pageUrl, elementName, value) {
103
103
  // Ensure session structure is initialized
104
104
  initializeSiteData(store, siteId, pageUrl);
105
-
105
+
106
106
  // Store the element value
107
107
  store.siteData[siteId].inputData[pageUrl].formData[elementName] = value;
108
108
  }
@@ -191,11 +191,11 @@ export function storeSiteSubmissionData(store, siteId, submissionData) {
191
191
  // let rawData = getSiteInputData(store, siteId);
192
192
  // Store the submission data
193
193
  store.siteData[siteId].submissionData = submissionData;
194
-
194
+
195
195
  // Clear validation errors from the session
196
196
  store.siteData[siteId].inputData = {};
197
197
  // Clear presaved/temporary save data
198
- store.siteData[siteId].loadData = {};
198
+ store.siteData[siteId].loadData = {};
199
199
 
200
200
  }
201
201
 
@@ -208,7 +208,7 @@ export function storeSiteSubmissionData(store, siteId, submissionData) {
208
208
  * @param {object} result - API response
209
209
  */
210
210
  export function storeSiteEligibilityResult(store, siteId, endpointKey, result) {
211
-
211
+
212
212
  initializeSiteData(store, siteId); // Ensure the structure exists
213
213
 
214
214
  if (!store.siteData[siteId].eligibility) store.siteData[siteId].eligibility = {};
@@ -244,7 +244,7 @@ export function getSiteEligibilityResult(store, siteId, endpointKey, maxAgeMs =
244
244
  */
245
245
  export function getPageValidationErrors(store, siteId, pageUrl) {
246
246
  const validationErrors = store?.siteData?.[siteId]?.inputData?.[pageUrl]?.validationErrors || null;
247
-
247
+
248
248
  if (validationErrors) {
249
249
  // Clear validation errors from the session
250
250
  delete store.siteData[siteId].inputData[pageUrl].validationErrors;
@@ -275,7 +275,7 @@ export function getPageData(store, siteId, pageUrl) {
275
275
  */
276
276
  export function getSiteSubmissionErrors(store, siteId) {
277
277
  const validationErrors = store?.siteData?.[siteId]?.submissionErrors || null;
278
-
278
+
279
279
  if (validationErrors) {
280
280
  // Clear validation errors from the session
281
281
  delete store.siteData[siteId].submissionErrors;
@@ -294,7 +294,7 @@ export function getSiteSubmissionErrors(store, siteId) {
294
294
  */
295
295
  export function getSiteData(store, siteId) {
296
296
  const inputData = store?.siteData?.[siteId] || {};
297
-
297
+
298
298
  if (inputData) {
299
299
  return inputData;
300
300
  }
@@ -311,7 +311,7 @@ export function getSiteData(store, siteId) {
311
311
  */
312
312
  export function getSiteInputData(store, siteId) {
313
313
  const inputData = store?.siteData?.[siteId]?.inputData || {};
314
-
314
+
315
315
  if (inputData) {
316
316
  return inputData;
317
317
  }
@@ -328,7 +328,7 @@ export function getSiteInputData(store, siteId) {
328
328
  */
329
329
  export function getSiteLoadData(store, siteId) {
330
330
  const loadData = store?.siteData?.[siteId]?.loadData || {};
331
-
331
+
332
332
  if (loadData) {
333
333
  return loadData;
334
334
  }
@@ -344,9 +344,9 @@ export function getSiteLoadData(store, siteId) {
344
344
  * @returns {string|null} The reference number or null if not available
345
345
  */
346
346
  export function getSiteLoadDataReferenceNumber(store, siteId) {
347
- const ref = store?.siteData?.[siteId]?.loadData?.referenceValue;
347
+ const ref = store?.siteData?.[siteId]?.loadData?.referenceValue;
348
348
 
349
- return typeof ref === 'string' && ref.trim() !== '' ? ref : null;
349
+ return typeof ref === 'string' && ref.trim() !== '' ? ref : null;
350
350
  }
351
351
 
352
352
 
@@ -359,9 +359,9 @@ export function getSiteLoadDataReferenceNumber(store, siteId) {
359
359
  */
360
360
  export function getSiteSubmissionData(store, siteId) {
361
361
  initializeSiteData(store, siteId); // Ensure the structure exists
362
-
362
+
363
363
  const submission = store?.siteData?.[siteId]?.submissionData || {};
364
-
364
+
365
365
  if (submission) {
366
366
  return submission;
367
367
  }
@@ -388,7 +388,7 @@ export function getFormDataValue(store, siteId, pageUrl, elementName) {
388
388
  * @param {object} store The session store
389
389
  * @returns The user object from the store or null if it doesn't exist.
390
390
  */
391
- export function getUser(store){
391
+ export function getUser(store) {
392
392
  return store.user || null;
393
393
  }
394
394
 
@@ -396,3 +396,181 @@ export function clearSiteData(store, siteId) {
396
396
  delete store?.siteData[siteId];
397
397
  }
398
398
 
399
+ /**
400
+ * Check if a file reference is used in more than one place (field) across the site's inputData.
401
+ *
402
+ * A "file reference" is an object like:
403
+ * { sha256: "abc...", fileId: "xyz..." }
404
+ *
405
+ * Matching rules:
406
+ * - If both fileId and sha256 are provided, both must match.
407
+ * - If only one is provided, we match by that single property.
408
+ *
409
+ * Notes:
410
+ * - Does NOT mutate the session.
411
+ * - Safely handles missing site/pages.
412
+ * - If a form field is an array (e.g., multiple file inputs), each item is checked.
413
+ *
414
+ * @param {object} store The session object (e.g., req.session)
415
+ * @param {string} siteId The site identifier
416
+ * @param {object} params { fileId?: string, sha256?: string }
417
+ * @returns {boolean} true if the file is found in more than one place, else false
418
+ */
419
+ export function isFileUsedInSiteInputDataAgain(store, siteId, { fileId, sha256 } = {}) {
420
+ // If neither identifier is provided, we cannot match anything
421
+ if (!fileId && !sha256) return false;
422
+
423
+ // Ensure session structure is initialized
424
+ initializeSiteData(store, siteId);
425
+
426
+ // Site input data: session.siteData[siteId].inputData
427
+ const site = store?.siteData?.[siteId]?.inputData;
428
+ if (!site || typeof site !== 'object') return false;
429
+
430
+ let hits = 0; // how many fields across the site reference this file
431
+
432
+ // Loop all pages under the site
433
+ for (const pageKey of Object.keys(site)) {
434
+ const formData = site[pageKey]?.formData;
435
+ if (!formData || typeof formData !== 'object') continue;
436
+
437
+ // Loop all fields on the page
438
+ for (const [_, value] of Object.entries(formData)) {
439
+ if (value == null) continue;
440
+
441
+ // Normalize to an array to also support multi-value fields (e.g., multiple file inputs)
442
+ const candidates = Array.isArray(value) ? value : [value];
443
+
444
+ for (const candidate of candidates) {
445
+ // We only consider objects that look like file references
446
+ if (
447
+ candidate &&
448
+ typeof candidate === 'object' &&
449
+ 'fileId' in candidate &&
450
+ 'sha256' in candidate
451
+ ) {
452
+ const idMatches = fileId ? candidate.fileId === fileId : true;
453
+ const shaMatches = sha256 ? candidate.sha256 === sha256 : true;
454
+
455
+ if (idMatches && shaMatches) {
456
+ hits += 1;
457
+ // As soon as we see it in more than one place, we can answer true
458
+ if (hits > 1) return true;
459
+ }
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ // If we get here, it was used 0 or 1 times
466
+ return false;
467
+ }
468
+
469
+
470
+ /**
471
+ * Remove (replace with "") file values across ALL pages of a site,
472
+ * matching a specific fileId and/or sha256.
473
+ *
474
+ * Matching rules:
475
+ * - If BOTH fileId and sha256 are provided, a file object must match BOTH.
476
+ * - If ONLY fileId is provided, match on fileId.
477
+ * - If ONLY sha256 is provided, match on sha256.
478
+ *
479
+ * Scope:
480
+ * - Operates on every page under store.siteData[siteId].inputData.
481
+ * - Shallow traversal of formData:
482
+ * • Direct fields: formData[elementId] = { fileId, sha256, ... }
483
+ * • Arrays: [ {fileId...}, {sha256...}, "other" ] → ["", "", "other"] (for matches)
484
+ *
485
+ * Side effects:
486
+ * - Mutates store.siteData[siteId].inputData[*].formData in place.
487
+ * - Intentionally returns nothing.
488
+ *
489
+ * @param {object} store - The data-layer store.
490
+ * @param {string} siteId - The site key under store.siteData to modify.
491
+ * @param {{ fileId?: string|null, sha256?: string|null }} match - Identifiers to match.
492
+ * Provide at least one of fileId/sha256. If both are given, both must match.
493
+ */
494
+ export function removeAllFilesFromSite(
495
+ store,
496
+ siteId,
497
+ { fileId = null, sha256 = null } = {}
498
+ ) {
499
+ // Ensure session structure is initialized
500
+ initializeSiteData(store, siteId);
501
+ // --- Guard rails ---------------------------------------------------------
502
+
503
+ // Nothing to remove if neither identifier is provided.
504
+ if (!fileId && !sha256) return;
505
+
506
+ // Per your structure: dig under .inputData for the site's pages.
507
+ const site = store?.siteData?.[siteId]?.inputData;
508
+ if (!site || typeof site !== "object") return;
509
+
510
+ // --- Helpers -------------------------------------------------------------
511
+
512
+ // Is this value a "file-like" object (has fileId and/or sha256)?
513
+ const isFileLike = (v) =>
514
+ v &&
515
+ typeof v === "object" &&
516
+ (Object.prototype.hasOwnProperty.call(v, "fileId") ||
517
+ Object.prototype.hasOwnProperty.call(v, "sha256"));
518
+
519
+ // Does a file-like object match the provided criteria?
520
+ const isMatch = (obj) => {
521
+ if (!isFileLike(obj)) return false;
522
+
523
+ // Strict when both are given
524
+ if (fileId && sha256) {
525
+ return obj.fileId === fileId && obj.sha256 === sha256;
526
+ }
527
+ // Otherwise match whichever was provided
528
+ if (fileId) return obj.fileId === fileId;
529
+ if (sha256) return obj.sha256 === sha256;
530
+
531
+ return false;
532
+ };
533
+
534
+ // --- Main traversal over all pages --------------------------------------
535
+
536
+ for (const page of Object.values(site)) {
537
+ // Each page should be an object with a formData object
538
+ const formData =
539
+ page &&
540
+ typeof page === "object" &&
541
+ page.formData &&
542
+ typeof page.formData === "object"
543
+ ? page.formData
544
+ : null;
545
+
546
+ if (!formData) continue; // skip content-only pages, etc.
547
+
548
+ // For each field on this page…
549
+ for (const key of Object.keys(formData)) {
550
+ const val = formData[key];
551
+
552
+ // Case A: a single file object → replace with "" if it matches.
553
+ if (isMatch(val)) {
554
+ formData[key] = "";
555
+ continue;
556
+ }
557
+
558
+ // Case B: an array → replace ONLY the matching items with "".
559
+ if (Array.isArray(val)) {
560
+ let changed = false;
561
+ const mapped = val.map((item) => {
562
+ if (isMatch(item)) {
563
+ changed = true;
564
+ return "";
565
+ }
566
+ return item;
567
+ });
568
+ if (changed) formData[key] = mapped;
569
+ }
570
+
571
+ // Note: If you later store file-like objects deeper in nested objects,
572
+ // add a recursive visitor here (with cycle protection / max depth).
573
+ }
574
+ }
575
+ }
576
+
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
8
8
  import * as dataLayer from "./govcyDataLayer.mjs";
9
+ import * as govcyResources from "../resources/govcyResources.mjs";
9
10
 
10
11
 
11
12
  /**
@@ -17,8 +18,10 @@ import * as dataLayer from "./govcyDataLayer.mjs";
17
18
  * @param {string} siteId The site ID
18
19
  * @param {string} pageUrl The page URL
19
20
  * @param {string} lang The language
21
+ * @param {Object} fileInputElements The file input elements
22
+ * @param {string} routeParam The route parameter
20
23
  */
21
- export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null) {
24
+ export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null, routeParam = "") {
22
25
  const inputElements = ALLOWED_FORM_ELEMENTS;
23
26
  const isRootCall = !fileInputElements;
24
27
  if (isRootCall) {
@@ -75,8 +78,9 @@ export function populateFormData(formElements, theData, validationErrors, store
75
78
  element.params.sha256 = fileData.sha256;
76
79
  element.params.visuallyHiddenText = element.params.label;
77
80
  // TODO: Also need to set the `view` and `download` URLs
78
- element.params.viewHref = "#viewHref";
79
- element.params.deleteHref = "#deleteHref";
81
+ element.params.viewHref = `/${siteId}/${pageUrl}/view-file/${fieldName}`;
82
+ element.params.viewTarget = "_blank";
83
+ element.params.deleteHref = `/${siteId}/${pageUrl}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`;
80
84
  } else {
81
85
  // TODO: Ask Andreas how to handle empty file inputs
82
86
  element.params.value = "";
@@ -107,7 +111,7 @@ export function populateFormData(formElements, theData, validationErrors, store
107
111
  if (element.element === "radios" && element.params.items) {
108
112
  element.params.items.forEach(item => {
109
113
  if (item.conditionalElements) {
110
- populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements);
114
+ populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements, routeParam);
111
115
 
112
116
  // Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
113
117
  if (item.conditionalElements.some(condEl => condEl.params?.error)) {
@@ -198,11 +198,11 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
198
198
  return {
199
199
  status: 200,
200
200
  data: {
201
- sha: response.Data.sha256,
201
+ sha256: response.Data.sha256,
202
202
  filename: response.Data.fileName || '',
203
203
  fileId: response.Data.fileId,
204
204
  mimeType: response.Data.contentType || '',
205
- sha256: response.Data.fileSize || ''
205
+ fileSize: response.Data?.fileSize || ''
206
206
  }
207
207
  };
208
208
 
@@ -234,11 +234,12 @@ function containsFileInput(elements = [], targetName) {
234
234
  for (const el of elements) {
235
235
  // ✅ Direct file input match
236
236
  if (el.element === 'fileInput' && el.params?.name === targetName) {
237
- return true;
237
+ return el;
238
238
  }
239
239
  // 🔁 Recurse into nested elements (e.g. groups, conditionals)
240
240
  if (Array.isArray(el?.params?.elements)) {
241
- if (containsFileInput(el.params.elements, targetName)) return true;
241
+ const nestedMatch = containsFileInput(el.params.elements, targetName);
242
+ if (nestedMatch) return nestedMatch; // ← propagate the found element
242
243
  }
243
244
 
244
245
  // 🎯 Special case: conditional radios/checkboxes
@@ -248,7 +249,8 @@ function containsFileInput(elements = [], targetName) {
248
249
  ) {
249
250
  for (const item of el.params.items) {
250
251
  if (Array.isArray(item?.conditionalElements)) {
251
- if (containsFileInput(item.conditionalElements, targetName)) return true;
252
+ const match = containsFileInput(item.conditionalElements, targetName);
253
+ if (match) return match; // ← propagate the found element
252
254
  }
253
255
  }
254
256
  }
@@ -264,23 +266,31 @@ function containsFileInput(elements = [], targetName) {
264
266
  * @param {string} elementName The name of the element to check
265
267
  * @return {boolean} True if a fileInput exists, false otherwise
266
268
  */
267
- function pageContainsFileInput(pageTemplate, elementName) {
269
+ export function pageContainsFileInput(pageTemplate, elementName) {
268
270
  const sections = pageTemplate?.sections || [];
269
- return sections.some(section =>
270
- section?.elements?.some(el =>
271
- el.element === 'form' &&
272
- containsFileInput(el.params?.elements, elementName)
273
- )
274
- );
271
+
272
+ for (const section of sections) {
273
+ for (const el of section?.elements || []) {
274
+ if (el.element === 'form') {
275
+ const match = containsFileInput(el.params?.elements, elementName);
276
+ if (match) {
277
+ return match; // ← return the actual element
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ return null; // no match found
275
284
  }
276
285
 
286
+
277
287
  /**
278
288
  * Validates magic bytes against expected mimetype
279
289
  * @param {Buffer} buffer
280
290
  * @param {string} mimetype
281
291
  * @returns {boolean}
282
292
  */
283
- function isMagicByteValid(buffer, mimetype) {
293
+ export function isMagicByteValid(buffer, mimetype) {
284
294
  const signatures = {
285
295
  'application/pdf': [0x25, 0x50, 0x44, 0x46], // %PDF
286
296
  'image/png': [0x89, 0x50, 0x4E, 0x47], // PNG