nox-validation 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/helpers.js CHANGED
@@ -319,7 +319,7 @@ const generateField = (
319
319
  hidden: false,
320
320
  }
321
321
  ) => {
322
- const { staticType } = meta;
322
+ const { staticType = null, parentInterface = null } = meta;
323
323
  childrenFields = childrenFields?.map((child) => {
324
324
  const childKey = path ? `${path}.${child.key}` : child.key;
325
325
  const childValue = path ? `${path}.${child.value}` : child.value;
@@ -327,7 +327,10 @@ const generateField = (
327
327
  return {
328
328
  ...child,
329
329
  value: childKey,
330
- meta: staticType ? { ...child?.meta, staticType } : child?.meta,
330
+ meta:
331
+ staticType || parentInterface
332
+ ? { ...child?.meta, staticType, parentInterface }
333
+ : child?.meta,
331
334
  key: childValue,
332
335
  isRelationalUpdate: path.includes("update"),
333
336
  };
@@ -418,7 +421,7 @@ const generateRelationalField = (
418
421
  [],
419
422
  "none",
420
423
  [],
421
- { staticType: "existing" }
424
+ { staticType: "existing", parentInterface: relationType }
422
425
  );
423
426
  existingField.children = [
424
427
  generateField(
@@ -458,7 +461,7 @@ const generateRelationalField = (
458
461
  [],
459
462
  "none",
460
463
  [],
461
- { staticType: "delete" }
464
+ { staticType: "delete", parentInterface: relationType }
462
465
  );
463
466
  deleteField.children = [
464
467
  generateField(
@@ -499,7 +502,7 @@ const generateRelationalField = (
499
502
  [],
500
503
  "none",
501
504
  [],
502
- { staticType: "create" }
505
+ { staticType: "create", parentInterface: relationType }
503
506
  );
504
507
  createField.children = [
505
508
  generateField(
@@ -544,7 +547,7 @@ const generateRelationalField = (
544
547
  [],
545
548
  "none",
546
549
  [],
547
- { staticType: "update" }
550
+ { staticType: "update", parentInterface: relationType }
548
551
  );
549
552
  updateField.children = [
550
553
  generateField(
@@ -591,7 +594,7 @@ const generateRelationalField = (
591
594
  [],
592
595
  "none",
593
596
  [],
594
- { staticType: "existing" }
597
+ { staticType: "existing", parentInterface: relationType }
595
598
  ),
596
599
  generateField(
597
600
  "delete",
@@ -601,7 +604,7 @@ const generateRelationalField = (
601
604
  [],
602
605
  "none",
603
606
  [],
604
- { staticType: "delete" }
607
+ { staticType: "delete", parentInterface: relationType }
605
608
  ),
606
609
  generateField(
607
610
  "create",
@@ -611,7 +614,7 @@ const generateRelationalField = (
611
614
  collectionFields,
612
615
  "none",
613
616
  [],
614
- { staticType: "create" }
617
+ { staticType: "create", parentInterface: relationType }
615
618
  ),
616
619
  generateField(
617
620
  "update",
@@ -621,7 +624,7 @@ const generateRelationalField = (
621
624
  collectionFields,
622
625
  "none",
623
626
  [],
624
- { staticType: "update" }
627
+ { staticType: "update", parentInterface: relationType }
625
628
  ),
626
629
  ];
627
630
  }
@@ -1427,10 +1430,40 @@ const getSelectedNodes = ({
1427
1430
  return updatedNode;
1428
1431
  }
1429
1432
 
1430
- function traverse(currentNode) {
1433
+ function traverse(currentNode, parentKey = null) {
1434
+ function mergePathLevels(currentPath, parentPath) {
1435
+ const cur = currentPath.split(".");
1436
+ const par = parentPath.split(".");
1437
+
1438
+ let matchIndex = -1;
1439
+
1440
+ // Find the first matching level
1441
+ for (let i = 0; i < par.length; i++) {
1442
+ if (par[i].replace(/\[\d+\]/, "") === cur[0].replace(/\[\d+\]/, "")) {
1443
+ matchIndex = i;
1444
+ break;
1445
+ }
1446
+ }
1447
+
1448
+ // If match found
1449
+ if (matchIndex !== -1) {
1450
+ const parentPart = par.slice(0, matchIndex + 1);
1451
+ const currentTail = cur.slice(1); // everything after the matched level
1452
+ return [...parentPart, ...currentTail].join(".");
1453
+ }
1454
+
1455
+ // No match → normal join
1456
+ return [...par, ...cur].join(".");
1457
+ }
1458
+
1459
+ if (parentKey)
1460
+ currentNode.key = mergePathLevels(currentNode.key, parentKey);
1461
+
1431
1462
  if (conditionFn(currentNode)) {
1432
1463
  actionFn(currentNode);
1433
- result.push(mapFn(currentNode));
1464
+ const mapped = mapFn(currentNode);
1465
+
1466
+ result.push(mapped);
1434
1467
  }
1435
1468
 
1436
1469
  if (Array.isArray(currentNode.children)) {
@@ -1447,12 +1480,15 @@ const getSelectedNodes = ({
1447
1480
  }
1448
1481
  }
1449
1482
 
1450
- if (!skipFn(currentNode)) traverse(childNode);
1483
+ if (!skipFn(currentNode)) {
1484
+ traverse(childNode, currentNode?.key);
1485
+ }
1451
1486
  });
1452
1487
  }
1453
1488
  }
1454
1489
 
1455
- traverse(node);
1490
+ traverse(node, node?.key);
1491
+
1456
1492
  return result;
1457
1493
  };
1458
1494
 
@@ -1503,12 +1539,9 @@ const formatDate = ({
1503
1539
  ];
1504
1540
 
1505
1541
  if (!formats.includes(format)) format = formats[0];
1506
- if (!date || isNaN(new Date(date).getTime())) {
1507
- throw new RangeError("Invalid time value");
1508
- }
1509
- const dateObj = date instanceof Date ? date : new Date(date);
1542
+
1510
1543
  const { year, month, day, hour, minute, second, dayPeriod } = getParts(
1511
- dateObj,
1544
+ date,
1512
1545
  timeZone
1513
1546
  );
1514
1547
 
@@ -1527,6 +1560,23 @@ const formatDate = ({
1527
1560
  .replace("A", dayPeriod);
1528
1561
  };
1529
1562
 
1563
+ const rebuildFullPath = (nodePath, fullPath, modifierFn) => {
1564
+ // 1. Find parent path
1565
+ const index = fullPath.lastIndexOf(nodePath);
1566
+ if (index === -1) {
1567
+ throw new Error("nodePath not found inside fullPath");
1568
+ }
1569
+
1570
+ const parentPath = fullPath.slice(0, index).replace(/\.$/, "");
1571
+
1572
+ // 2. Modify node path (if function provided)
1573
+ const modifiedNodePath =
1574
+ typeof modifierFn === "function" ? modifierFn(nodePath) : nodePath;
1575
+
1576
+ // 3. Rebuild final full path
1577
+ return `${parentPath}.${modifiedNodePath}`.replace(/\.+/g, ".");
1578
+ };
1579
+
1530
1580
  module.exports = {
1531
1581
  generateModifiedRules,
1532
1582
  getFieldsGroupBySchemaId,
@@ -1550,4 +1600,5 @@ module.exports = {
1550
1600
  getSelectedNodes,
1551
1601
  remainingItems,
1552
1602
  formatDate,
1603
+ rebuildFullPath,
1553
1604
  };
package/lib/validate.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  getSelectedNodes,
14
14
  remainingItems,
15
15
  formatDate,
16
+ rebuildFullPath,
16
17
  } = require("./helpers");
17
18
 
18
19
  const choices = ["radio", "checkboxes", "dropdown_multiple", "dropdown"];
@@ -60,9 +61,20 @@ const typeChecks = {
60
61
  }
61
62
  return false;
62
63
  },
63
- [constants.types.TIME]: (val) =>
64
- typeof val === "string" &&
65
- /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/.test(val),
64
+ [constants.types.TIME]: (val, data) => {
65
+ if (val instanceof Date && !isNaN(val)) return true;
66
+ if (typeof val === "string") {
67
+ if (/^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/.test(val)) return true;
68
+ const parsed = Date.parse(val);
69
+ if (!isNaN(parsed)) {
70
+ if (data && data?.key && data?.updateValue) {
71
+ data.updateValue(data.key, new Date(val));
72
+ }
73
+ return true;
74
+ }
75
+ }
76
+ return false;
77
+ },
66
78
  [constants.types.TIMESTAMP]: (val, data) => {
67
79
  if (val instanceof Date && !isNaN(val)) return true;
68
80
  if (typeof val === "string" && !isNaN(Date.parse(val))) {
@@ -411,6 +423,7 @@ const validateMetaRules = (
411
423
  if (
412
424
  (isEmpty(fieldValue) &&
413
425
  required &&
426
+ !field?.meta?.hidden &&
414
427
  ![
415
428
  constants.interfaces.FILES,
416
429
  constants.interfaces.FILE,
@@ -419,21 +432,22 @@ const validateMetaRules = (
419
432
  constants.interfaces.ONE_TO_MANY,
420
433
  constants.interfaces.MANY_TO_ONE,
421
434
  constants.interfaces.MANY_TO_ANY,
422
- constants.interfaces.SEO,
435
+
423
436
  "none",
424
437
  ].includes(field?.meta?.interface)) ||
425
438
  (field?.meta?.interface === constants.interfaces.TRANSLATIONS &&
439
+ !field?.meta?.hidden &&
426
440
  language_codes?.length &&
427
441
  !onlyFormFields) ||
428
- ([constants.interfaces.OBJECT, constants.interfaces.ITEMS].includes(
429
- field?.meta?.interface
430
- ) &&
442
+ ([
443
+ constants.interfaces.OBJECT,
444
+ constants.interfaces.SEO,
445
+ constants.interfaces.ITEMS,
446
+ ].includes(field?.meta?.interface) &&
447
+ !field?.meta?.hidden &&
431
448
  !onlyFormFields &&
432
449
  getNodes)
433
450
  ) {
434
- const isTranslationChild =
435
- field?.meta?.interface === constants.interfaces.TRANSLATIONS;
436
-
437
451
  getSelectedNodes({
438
452
  node: field,
439
453
  skipFn: (node) =>
@@ -445,17 +459,36 @@ const validateMetaRules = (
445
459
  label: node.display_label,
446
460
  }),
447
461
  actionFn: (node) => {
448
- const staticType = node?.meta?.staticType;
462
+ const isTranslationNode =
463
+ node?.meta?.parentInterface === constants.interfaces.TRANSLATIONS ||
464
+ field?.meta?.interface === constants.interfaces.TRANSLATIONS;
449
465
  let fPath = node.key?.replace("[0][0]", "[0]");
466
+ const types = ["update", "delete", "existing", "create"];
467
+
468
+ const extractFirstType = (key) => {
469
+ const regex = new RegExp(`(${types.join("|")})\\[`, "i");
470
+ const match = key.match(regex);
471
+ return match ? match[1] : null;
472
+ };
473
+
474
+ const staticType =
475
+ node?.meta?.staticType && types.includes(node?.meta?.staticType)
476
+ ? extractFirstType(node.key)
477
+ : null;
478
+
479
+ if (!fPath.includes(currentPath)) {
480
+ fPath = `${currentPath}.${fPath}`;
481
+ }
450
482
 
451
483
  if (staticType === "update") return;
452
- if (staticType === "delete" && !isTranslationChild) return;
453
- if (staticType === "existing" && !isTranslationChild) return;
484
+ if (staticType === "delete" && !isTranslationNode) return;
485
+ if (staticType === "existing" && !isTranslationNode) return;
454
486
 
455
487
  const label = formatLabel(node.display_label);
456
488
  const message = error_messages.REQUIRED.replace("{field}", label);
457
489
 
458
490
  const buildError = (path, translated) => {
491
+ if (!isEmpty(getValue(formData, path))) return;
459
492
  let obj = {
460
493
  label,
461
494
  fieldPath: path,
@@ -469,7 +502,7 @@ const validateMetaRules = (
469
502
  };
470
503
 
471
504
  const isMultiLang =
472
- isTranslationChild &&
505
+ isTranslationNode &&
473
506
  Array.isArray(language_codes) &&
474
507
  language_codes.length > 1;
475
508
 
@@ -479,17 +512,24 @@ const validateMetaRules = (
479
512
  transformedPath,
480
513
  isAdded = false;
481
514
 
482
- if (fPath.includes("create[0].")) {
515
+ if (node?.value?.includes("create[0].")) {
483
516
  const exist = fieldValue?.create?.find(
484
517
  (item) => item.languages_code === lang
485
518
  );
486
519
  if (exist) isAdded = true;
487
-
488
- langPath = fPath.replace("create[0].", `create[${index}].`);
489
- transformedPath = fPath.replace("create[0].", `${lang}.`);
520
+ langPath = rebuildFullPath(node.value, node.key, (path) =>
521
+ path.replace("create[0].", `create[${index}].`)
522
+ );
523
+ transformedPath = rebuildFullPath(node.value, node.key, (path) =>
524
+ path.replace("create[0].", `${lang}.`)
525
+ );
490
526
  } else {
491
- langPath = fPath.replace("create.", `create[${index}].`);
492
- transformedPath = fPath.replace("create.", `${lang}.`);
527
+ langPath = rebuildFullPath(node.value, node.key, (path) =>
528
+ path.replace("create.", `create[${index}].`)
529
+ );
530
+ transformedPath = rebuildFullPath(node.value, node.key, (path) =>
531
+ path.replace("create.", `${lang}.`)
532
+ );
493
533
  }
494
534
 
495
535
  if (!isAdded) buildError(langPath, transformedPath);
@@ -497,7 +537,7 @@ const validateMetaRules = (
497
537
  } else {
498
538
  const singlePath = fPath.replace(
499
539
  "create.",
500
- isTranslationChild ? "create[lang_code]." : "create[0]."
540
+ isTranslationNode ? "create[lang_code]." : "create[0]."
501
541
  );
502
542
  if (
503
543
  constants.interfaces.ITEMS === field?.meta?.interface
@@ -626,10 +666,6 @@ const handleRegexValidation = (
626
666
  message = message
627
667
  ?.replace(`{field}`, fieldLabel)
628
668
  ?.replace(`{value}`, rule.value[0]);
629
- console.log(
630
- "[handleRegexValidation] Validation failed. Adding error. Message:",
631
- message
632
- );
633
669
  addError(
634
670
  currentPath,
635
671
  { label: fieldLabel, fieldPath: currentPath, description: "", message },
@@ -660,10 +696,40 @@ const validateOperatorRule = (
660
696
  const isTime = field.type === constants.types.TIME;
661
697
  const isArray = field.type === constants.types.ARRAY;
662
698
 
663
- const parseTime = (timeStr) => {
664
- if (!timeStr || typeof timeStr !== "string") return null;
665
- const [hh, mm, ss] = timeStr.split(":").map(Number);
666
- return hh * 3600 + mm * 60 + ss;
699
+ const parseTime = (value) => {
700
+ if (!value) return null;
701
+
702
+ const toSeconds = (str) => {
703
+ const [hh = 0, mm = 0, ss = 0] = str.split(":").map(Number);
704
+ return hh * 3600 + mm * 60 + ss;
705
+ };
706
+
707
+ if (value instanceof Date && !isNaN(value)) {
708
+ const timePart = formatDate({
709
+ date: value,
710
+ format: "DD/MM/YYYY HH:mm:ss",
711
+ timeZone,
712
+ }).split(" ")[1];
713
+ const totalSeconds = toSeconds(timePart);
714
+ return totalSeconds || null;
715
+ }
716
+
717
+ if (typeof value === "string") {
718
+ if (/^\d{1,2}:\d{2}:\d{2}$/.test(value)) {
719
+ return toSeconds(value);
720
+ }
721
+ if (!isNaN(Date.parse(value))) {
722
+ const timePart = formatDate({
723
+ date: value,
724
+ format: "DD/MM/YYYY HH:mm:ss",
725
+ timeZone,
726
+ }).split(" ")[1];
727
+ const totalSeconds = toSeconds(timePart);
728
+ return totalSeconds || null;
729
+ }
730
+ }
731
+
732
+ return null;
667
733
  };
668
734
 
669
735
  // Helper to parse date string to UTC midnight timestamp
@@ -700,7 +766,7 @@ const validateOperatorRule = (
700
766
  }
701
767
  if (isDateTime) {
702
768
  if (forMessage) {
703
- const format = field?.options?.format;
769
+ const format = field?.meta?.options?.format;
704
770
  return formatDate({ date: value, format, timeZone });
705
771
  }
706
772
  // Return UTC timestamp for comparison
@@ -1514,6 +1580,7 @@ const validate = (data) => {
1514
1580
  fieldOptions,
1515
1581
  data.language_codes,
1516
1582
  existingForm,
1583
+ true,
1517
1584
  timeZone
1518
1585
  );
1519
1586
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nox-validation",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "validate dynamic schema",
5
5
  "main": "index.js",
6
6
  "scripts": {