@oneuptime/common 12.0.7 → 12.0.8

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 (186) hide show
  1. package/Models/DatabaseModels/TelemetryEntity.ts +45 -0
  2. package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
  3. package/Models/DatabaseModels/WorkflowLog.ts +38 -0
  4. package/Server/API/UserAPI.ts +16 -1
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
  9. package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
  10. package/Server/Services/TelemetryEntityService.ts +114 -1
  11. package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
  12. package/Server/Types/Workflow/TriggerCode.ts +12 -0
  13. package/Server/Types/Workflow/Workflow.ts +5 -0
  14. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
  15. package/Server/Utils/VM/VMAPI.ts +56 -8
  16. package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
  17. package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
  18. package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
  19. package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
  20. package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
  21. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
  22. package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
  23. package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
  24. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
  25. package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
  26. package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
  27. package/Tests/Types/Workflow/Templates.test.ts +403 -0
  28. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
  29. package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
  30. package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
  31. package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
  32. package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
  33. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
  34. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
  35. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
  36. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
  37. package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
  38. package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
  39. package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
  40. package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
  41. package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
  42. package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
  43. package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
  44. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
  45. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
  46. package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
  47. package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
  48. package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
  49. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
  50. package/Types/SerializableObjectDictionary.ts +133 -39
  51. package/Types/Telemetry/EntitySource.ts +40 -0
  52. package/Types/Telemetry/EntityType.ts +27 -0
  53. package/Types/Telemetry/EntityTypeGroups.ts +65 -0
  54. package/Types/Workflow/Component.ts +29 -0
  55. package/Types/Workflow/Components/API.ts +35 -0
  56. package/Types/Workflow/Components/BaseModel.ts +7 -3
  57. package/Types/Workflow/StepTrace.ts +181 -0
  58. package/Types/Workflow/TemplateSyntax.ts +499 -0
  59. package/Types/Workflow/Templates.ts +316 -0
  60. package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
  61. package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
  62. package/UI/Components/Dictionary/Dictionary.tsx +23 -6
  63. package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
  64. package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
  65. package/UI/Components/Forms/Types/Field.ts +7 -0
  66. package/UI/Components/Forms/Validation.ts +63 -1
  67. package/UI/Components/Input/Input.tsx +31 -3
  68. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
  69. package/UI/Components/Modal/Modal.tsx +37 -4
  70. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
  71. package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
  72. package/UI/Components/Workflow/Component.tsx +28 -26
  73. package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
  74. package/UI/Components/Workflow/GraphLint.ts +591 -0
  75. package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
  76. package/UI/Components/Workflow/ModelSchema.ts +196 -0
  77. package/UI/Components/Workflow/RunForm.tsx +41 -7
  78. package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
  79. package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
  80. package/UI/Components/Workflow/Utils.ts +93 -1
  81. package/UI/Components/Workflow/Workflow.tsx +77 -4
  82. package/UI/Types/LayeredDismissal.ts +31 -0
  83. package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
  86. package/Utils/Telemetry/EntityKey.ts +66 -0
  87. package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
  88. package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
  89. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
  90. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
  92. package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
  93. package/build/dist/Server/API/UserAPI.js +16 -1
  94. package/build/dist/Server/API/UserAPI.js.map +1 -1
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  101. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
  102. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  103. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
  104. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  105. package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
  106. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  107. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
  108. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  109. package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
  110. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
  111. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
  112. package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
  113. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  114. package/build/dist/Types/SerializableObjectDictionary.js +133 -39
  115. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  116. package/build/dist/Types/Telemetry/EntitySource.js +39 -0
  117. package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
  118. package/build/dist/Types/Telemetry/EntityType.js +27 -0
  119. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  120. package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
  121. package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
  122. package/build/dist/Types/Workflow/Component.js +21 -0
  123. package/build/dist/Types/Workflow/Component.js.map +1 -1
  124. package/build/dist/Types/Workflow/Components/API.js +35 -0
  125. package/build/dist/Types/Workflow/Components/API.js.map +1 -1
  126. package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
  127. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  128. package/build/dist/Types/Workflow/StepTrace.js +104 -0
  129. package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
  130. package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
  131. package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
  132. package/build/dist/Types/Workflow/Templates.js +218 -0
  133. package/build/dist/Types/Workflow/Templates.js.map +1 -0
  134. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
  135. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
  136. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
  137. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
  138. package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
  139. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  140. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
  141. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  142. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
  143. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  144. package/build/dist/UI/Components/Forms/Validation.js +44 -0
  145. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  146. package/build/dist/UI/Components/Input/Input.js +12 -4
  147. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  148. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
  149. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  150. package/build/dist/UI/Components/Modal/Modal.js +31 -5
  151. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  152. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
  153. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  154. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
  155. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  156. package/build/dist/UI/Components/Workflow/Component.js +20 -19
  157. package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
  158. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
  159. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  160. package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
  161. package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
  162. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
  163. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
  164. package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
  165. package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
  166. package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
  167. package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
  168. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
  169. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
  170. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
  171. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
  172. package/build/dist/UI/Components/Workflow/Utils.js +73 -1
  173. package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
  174. package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
  175. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  176. package/build/dist/UI/Types/LayeredDismissal.js +21 -0
  177. package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
  178. package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
  179. package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
  181. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
  183. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  184. package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
  185. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  186. package/package.json +1 -1
@@ -445,3 +445,243 @@ describe("NetworkTopologyUtil.buildTopology", () => {
445
445
  });
446
446
  });
447
447
  });
448
+
449
+ /*
450
+ * Roles are what give every node on the map its shape, so the builder has
451
+ * to attach one to all three kinds of node — the devices it was given,
452
+ * the unmanaged peers it invented from neighbor claims, and the endpoints
453
+ * it hung off switch ports.
454
+ */
455
+ describe("NetworkTopologyUtil.buildTopology — device roles", () => {
456
+ const now: Date = new Date("2026-07-22T12:00:00Z");
457
+ const fresh: Date = new Date("2026-07-22T11:55:00Z");
458
+
459
+ const nodeById: (
460
+ result: TopologyBuildResult,
461
+ id: string,
462
+ ) => NetworkTopologyNode | undefined = (
463
+ result: TopologyBuildResult,
464
+ id: string,
465
+ ): NetworkTopologyNode | undefined => {
466
+ return result.nodes.find((node: NetworkTopologyNode) => {
467
+ return node.id === id;
468
+ });
469
+ };
470
+
471
+ test("managed devices are classified from their SNMP identity", () => {
472
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
473
+ [
474
+ {
475
+ id: "d1",
476
+ name: "core-1",
477
+ lastSeenAt: fresh,
478
+ deviceModel: "Catalyst 9300-48P",
479
+ },
480
+ {
481
+ id: "d2",
482
+ name: "edge-1",
483
+ lastSeenAt: fresh,
484
+ sysDescr: "FortiGate-60F v7.2.5",
485
+ },
486
+ {
487
+ id: "d3",
488
+ name: "dmz-1",
489
+ lastSeenAt: fresh,
490
+ sysObjectId: "1.3.6.1.4.1.3375.2.1.3.4.10",
491
+ },
492
+ ],
493
+ now,
494
+ );
495
+
496
+ expect(nodeById(result, "d1")?.role).toBe("switch");
497
+ expect(nodeById(result, "d2")?.role).toBe("firewall");
498
+ expect(nodeById(result, "d3")?.role).toBe("loadBalancer");
499
+ });
500
+
501
+ test("a device with no identity at all still carries an explicit role", () => {
502
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
503
+ [{ id: "d1", name: "device-42", lastSeenAt: fresh }],
504
+ now,
505
+ );
506
+ /*
507
+ * Explicitly "unknown" rather than absent — readers must not have to
508
+ * distinguish "we could not tell" from "this payload is old".
509
+ */
510
+ expect(nodeById(result, "d1")?.role).toBe("unknown");
511
+ });
512
+
513
+ test("the hostname is read as a last resort, sysName ahead of name", () => {
514
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
515
+ [
516
+ { id: "d1", name: "idf2-sw-3", lastSeenAt: fresh },
517
+ {
518
+ id: "d2",
519
+ name: "unhelpful",
520
+ sysName: "core-rtr-1",
521
+ lastSeenAt: fresh,
522
+ },
523
+ ],
524
+ now,
525
+ );
526
+ expect(nodeById(result, "d1")?.role).toBe("switch");
527
+ expect(nodeById(result, "d2")?.role).toBe("router");
528
+ });
529
+
530
+ test("an unmanaged CDP peer is classified from the platform it advertises", () => {
531
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
532
+ [
533
+ {
534
+ id: "d1",
535
+ name: "edge-1",
536
+ lastSeenAt: fresh,
537
+ cdpNeighbors: [
538
+ {
539
+ localInterfaceIndex: 1,
540
+ remoteDeviceId: "ap-lobby",
541
+ remotePlatform: "cisco AIR-CAP3702I-A-K9",
542
+ },
543
+ ],
544
+ },
545
+ ],
546
+ now,
547
+ );
548
+ expect(nodeById(result, "unmanaged:ap-lobby")?.role).toBe(
549
+ "wirelessAccessPoint",
550
+ );
551
+ });
552
+
553
+ test("a peer known only by name falls back to the naming convention", () => {
554
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
555
+ [
556
+ {
557
+ id: "d1",
558
+ name: "edge-1",
559
+ lastSeenAt: fresh,
560
+ lldpNeighbors: [
561
+ { localInterfaceIndex: 1, remoteSysName: "dist-sw-9" },
562
+ ],
563
+ },
564
+ ],
565
+ now,
566
+ );
567
+ expect(nodeById(result, "unmanaged:dist-sw-9")?.role).toBe("switch");
568
+ });
569
+
570
+ test("a peer with nothing to go on is unknown, not guessed at", () => {
571
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
572
+ [
573
+ {
574
+ id: "d1",
575
+ name: "edge-1",
576
+ lastSeenAt: fresh,
577
+ lldpNeighbors: [{ localInterfaceIndex: 1, remoteSysName: "peer-a" }],
578
+ },
579
+ ],
580
+ now,
581
+ );
582
+ expect(nodeById(result, "unmanaged:peer-a")?.role).toBe("unknown");
583
+ });
584
+
585
+ test("a later claim that brings a platform re-derives the peer's role", () => {
586
+ /*
587
+ * LLDP reports the peer first, by name only, so it starts out
588
+ * unknown; the CDP claim that follows carries the platform string —
589
+ * the only real evidence about that box — and must be allowed to
590
+ * change the shape it is drawn as.
591
+ */
592
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
593
+ [
594
+ {
595
+ id: "d1",
596
+ name: "edge-1",
597
+ lastSeenAt: fresh,
598
+ lldpNeighbors: [{ localInterfaceIndex: 1, remoteSysName: "peer-a" }],
599
+ cdpNeighbors: [
600
+ {
601
+ localInterfaceIndex: 1,
602
+ remoteDeviceId: "peer-a",
603
+ remotePlatform: "cisco WS-C2960X-48TS-L",
604
+ },
605
+ ],
606
+ },
607
+ ],
608
+ now,
609
+ );
610
+ const peer: NetworkTopologyNode | undefined = nodeById(
611
+ result,
612
+ "unmanaged:peer-a",
613
+ );
614
+ expect(peer?.deviceModel).toBe("cisco WS-C2960X-48TS-L");
615
+ expect(peer?.role).toBe("switch");
616
+ });
617
+
618
+ test("endpoints are hosts unless their classification says otherwise", () => {
619
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
620
+ [{ id: "d1", name: "edge-1", lastSeenAt: fresh }],
621
+ now,
622
+ [],
623
+ [
624
+ {
625
+ id: "e1",
626
+ macAddress: "aa:aa:aa:aa:aa:01",
627
+ attachedNetworkDeviceId: "d1",
628
+ lastSeenAt: fresh,
629
+ },
630
+ {
631
+ id: "e2",
632
+ macAddress: "aa:aa:aa:aa:aa:02",
633
+ classification: "Camera",
634
+ attachedNetworkDeviceId: "d1",
635
+ lastSeenAt: fresh,
636
+ },
637
+ {
638
+ id: "e3",
639
+ macAddress: "aa:aa:aa:aa:aa:03",
640
+ vendor: "Zebra Technologies",
641
+ attachedNetworkDeviceId: "d1",
642
+ lastSeenAt: fresh,
643
+ },
644
+ {
645
+ id: "e4",
646
+ macAddress: "aa:aa:aa:aa:aa:04",
647
+ classification: "POS terminal",
648
+ attachedNetworkDeviceId: "d1",
649
+ lastSeenAt: fresh,
650
+ },
651
+ ],
652
+ );
653
+
654
+ expect(nodeById(result, "endpoint:e1")?.role).toBe("host");
655
+ expect(nodeById(result, "endpoint:e2")?.role).toBe("camera");
656
+ expect(nodeById(result, "endpoint:e3")?.role).toBe("printer");
657
+ expect(nodeById(result, "endpoint:e4")?.role).toBe("host");
658
+ });
659
+
660
+ test("every node the builder emits carries a role", () => {
661
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
662
+ [
663
+ {
664
+ id: "d1",
665
+ name: "edge-1",
666
+ lastSeenAt: fresh,
667
+ lldpNeighbors: [{ localInterfaceIndex: 1, remoteSysName: "peer-a" }],
668
+ },
669
+ ],
670
+ now,
671
+ [],
672
+ [
673
+ {
674
+ id: "e1",
675
+ macAddress: "aa:aa:aa:aa:aa:01",
676
+ attachedNetworkDeviceId: "d1",
677
+ lastSeenAt: fresh,
678
+ },
679
+ ],
680
+ );
681
+
682
+ expect(result.nodes).toHaveLength(3);
683
+ for (const node of result.nodes) {
684
+ expect(node.role).toBeDefined();
685
+ }
686
+ });
687
+ });
@@ -0,0 +1,469 @@
1
+ import { AnalyticsModelSchema } from "../../../Utils/Schema/AnalyticsModelSchema";
2
+ import Zod from "../../../Utils/Schema/Zod";
3
+ import AnalyticsModels from "../../../Models/AnalyticsModels/Index";
4
+ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
5
+ import AnalyticsTableColumn from "../../../Types/AnalyticsDatabase/TableColumn";
6
+ import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
7
+ import Log from "../../../Models/AnalyticsModels/Log";
8
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
9
+
10
+ /*
11
+ * Unit tests for AnalyticsModelSchema — the Zod schema generator that backs the
12
+ * OpenAPI spec and request validation for every ClickHouse-backed analytics
13
+ * model (Log, Span, Metric, RUM, …).
14
+ *
15
+ * The generator is pure: it instantiates a model, reads its column metadata and
16
+ * per-column access control, and derives a Zod schema. No database access is
17
+ * involved, so these tests exercise the real production code paths directly.
18
+ *
19
+ * Assertions are derived dynamically from each model's own column metadata
20
+ * rather than hard-coding column names, so they stay correct as models evolve
21
+ * while still pinning down the type/permission semantics that the schema must
22
+ * preserve.
23
+ */
24
+
25
+ type ModelCtor = new () => AnalyticsBaseModel;
26
+
27
+ // Column types the generator treats as sortable (see getSortableTypes()).
28
+ const SORTABLE_TYPES: Array<TableColumnType> = [
29
+ TableColumnType.Text,
30
+ TableColumnType.Number,
31
+ TableColumnType.LongNumber,
32
+ TableColumnType.Date,
33
+ TableColumnType.Boolean,
34
+ TableColumnType.ObjectID,
35
+ TableColumnType.Decimal,
36
+ TableColumnType.IP,
37
+ TableColumnType.Port,
38
+ ];
39
+
40
+ // Column types the generator treats as groupable (see getGroupByModelSchema()).
41
+ const GROUPABLE_TYPES: Array<TableColumnType> = [
42
+ TableColumnType.Text,
43
+ TableColumnType.ObjectID,
44
+ TableColumnType.Boolean,
45
+ TableColumnType.Date,
46
+ TableColumnType.Number,
47
+ TableColumnType.IP,
48
+ TableColumnType.Port,
49
+ ];
50
+
51
+ const CREATE_EXCLUDED_FIELDS: Array<string> = ["_id", "createdAt", "updatedAt"];
52
+
53
+ const isZodObject: (schema: unknown) => boolean = (
54
+ schema: unknown,
55
+ ): boolean => {
56
+ return schema instanceof Zod.ZodObject;
57
+ };
58
+
59
+ const shapeKeys: (schema: any) => Array<string> = (
60
+ schema: any,
61
+ ): Array<string> => {
62
+ return Object.keys(schema.shape);
63
+ };
64
+
65
+ const findColumnOfType: (
66
+ columns: Array<AnalyticsTableColumn>,
67
+ type: TableColumnType,
68
+ keysAllowed: Array<string>,
69
+ ) => AnalyticsTableColumn | undefined = (
70
+ columns: Array<AnalyticsTableColumn>,
71
+ type: TableColumnType,
72
+ keysAllowed: Array<string>,
73
+ ): AnalyticsTableColumn | undefined => {
74
+ return columns.find((column: AnalyticsTableColumn) => {
75
+ return column.type === type && keysAllowed.includes(column.key);
76
+ });
77
+ };
78
+
79
+ describe("AnalyticsModelSchema", () => {
80
+ describe("getModelSchema (read schema)", () => {
81
+ test("returns a non-empty ZodObject for a real model", () => {
82
+ const schema: any = AnalyticsModelSchema.getModelSchema({
83
+ modelType: Log,
84
+ });
85
+
86
+ expect(isZodObject(schema)).toBe(true);
87
+ expect(shapeKeys(schema).length).toBeGreaterThan(0);
88
+ });
89
+
90
+ test("only includes columns that grant read access", () => {
91
+ const model: Log = new Log();
92
+ const schema: any = AnalyticsModelSchema.getModelSchema({
93
+ modelType: Log,
94
+ });
95
+ const keys: Array<string> = shapeKeys(schema);
96
+
97
+ // Every column that ended up in the schema must have read permissions.
98
+ for (const key of keys) {
99
+ const accessControl: any = model.getColumnAccessControlFor(key);
100
+ expect(accessControl).toBeTruthy();
101
+ expect(Array.isArray(accessControl.read)).toBe(true);
102
+ expect(accessControl.read.length).toBeGreaterThan(0);
103
+ }
104
+ });
105
+
106
+ test("validates the runtime type of a Text column", () => {
107
+ const model: Log = new Log();
108
+ const schema: any = AnalyticsModelSchema.getModelSchema({
109
+ modelType: Log,
110
+ });
111
+ const textColumn: AnalyticsTableColumn | undefined = findColumnOfType(
112
+ model.getTableColumns(),
113
+ TableColumnType.Text,
114
+ shapeKeys(schema),
115
+ );
116
+
117
+ // Guard: Log always exposes readable Text columns; assert we found one.
118
+ expect(textColumn).toBeDefined();
119
+
120
+ /*
121
+ * Use partial() so unrelated required columns don't dominate the result;
122
+ * this isolates the single field's type constraint.
123
+ */
124
+ const partial: any = schema.partial();
125
+
126
+ expect(
127
+ partial.safeParse({ [textColumn!.key]: "a string value" }).success,
128
+ ).toBe(true);
129
+ expect(partial.safeParse({ [textColumn!.key]: 12345 }).success).toBe(
130
+ false,
131
+ );
132
+ });
133
+
134
+ test("validates the runtime type of a Number column", () => {
135
+ const model: Log = new Log();
136
+ const schema: any = AnalyticsModelSchema.getModelSchema({
137
+ modelType: Log,
138
+ });
139
+ const numberColumn: AnalyticsTableColumn | undefined = findColumnOfType(
140
+ model.getTableColumns(),
141
+ TableColumnType.Number,
142
+ shapeKeys(schema),
143
+ );
144
+
145
+ expect(numberColumn).toBeDefined();
146
+
147
+ const partial: any = schema.partial();
148
+
149
+ expect(partial.safeParse({ [numberColumn!.key]: 42 }).success).toBe(true);
150
+ expect(
151
+ partial.safeParse({ [numberColumn!.key]: "not a number" }).success,
152
+ ).toBe(false);
153
+ });
154
+
155
+ test("strips unknown keys instead of rejecting them", () => {
156
+ const schema: any = AnalyticsModelSchema.getModelSchema({
157
+ modelType: Log,
158
+ });
159
+ const parsed: any = schema
160
+ .partial()
161
+ .parse({ someKeyThatDoesNotExist: "value" });
162
+
163
+ expect(parsed.someKeyThatDoesNotExist).toBeUndefined();
164
+ });
165
+
166
+ test("builds a schema for every registered analytics model", () => {
167
+ for (const modelType of AnalyticsModels as Array<ModelCtor>) {
168
+ const schema: any = AnalyticsModelSchema.getModelSchema({ modelType });
169
+ expect(isZodObject(schema)).toBe(true);
170
+ }
171
+ });
172
+ });
173
+
174
+ describe("getCreateModelSchema", () => {
175
+ test("excludes system-managed fields from the create shape", () => {
176
+ const schema: any = AnalyticsModelSchema.getCreateModelSchema({
177
+ modelType: Log,
178
+ });
179
+ const keys: Array<string> = shapeKeys(schema);
180
+
181
+ for (const excluded of CREATE_EXCLUDED_FIELDS) {
182
+ expect(keys).not.toContain(excluded);
183
+ }
184
+ });
185
+
186
+ test("excludes default-value columns from the create shape", () => {
187
+ const model: Log = new Log();
188
+ const schema: any = AnalyticsModelSchema.getCreateModelSchema({
189
+ modelType: Log,
190
+ });
191
+ const keys: Array<string> = shapeKeys(schema);
192
+
193
+ for (const column of model.getTableColumns()) {
194
+ if (column.isDefaultValueColumn) {
195
+ expect(keys).not.toContain(column.key);
196
+ }
197
+ }
198
+ });
199
+
200
+ test("only includes columns that grant create access", () => {
201
+ const model: Log = new Log();
202
+ const schema: any = AnalyticsModelSchema.getCreateModelSchema({
203
+ modelType: Log,
204
+ });
205
+
206
+ for (const key of shapeKeys(schema)) {
207
+ const accessControl: any = model.getColumnAccessControlFor(key);
208
+ expect(accessControl).toBeTruthy();
209
+ expect(accessControl.create.length).toBeGreaterThan(0);
210
+ }
211
+ });
212
+
213
+ test("rejects a payload missing a required create column", () => {
214
+ const model: Log = new Log();
215
+ const schema: any = AnalyticsModelSchema.getCreateModelSchema({
216
+ modelType: Log,
217
+ });
218
+ const keys: Array<string> = shapeKeys(schema);
219
+
220
+ const requiredColumns: Array<AnalyticsTableColumn> = model
221
+ .getTableColumns()
222
+ .filter((column: AnalyticsTableColumn) => {
223
+ return column.required === true && keys.includes(column.key);
224
+ });
225
+
226
+ /*
227
+ * Log has required, create-permitted columns (e.g. projectId); an empty
228
+ * payload must therefore fail validation.
229
+ */
230
+ expect(requiredColumns.length).toBeGreaterThan(0);
231
+ expect(schema.safeParse({}).success).toBe(false);
232
+ });
233
+
234
+ test("builds a create schema for every registered analytics model", () => {
235
+ for (const modelType of AnalyticsModels as Array<ModelCtor>) {
236
+ const schema: any = AnalyticsModelSchema.getCreateModelSchema({
237
+ modelType,
238
+ });
239
+ expect(isZodObject(schema)).toBe(true);
240
+ }
241
+ });
242
+
243
+ test("honours disableOpenApiSchema without changing validation", () => {
244
+ const withOpenApi: any = AnalyticsModelSchema.getCreateModelSchema({
245
+ modelType: Log,
246
+ });
247
+ const withoutOpenApi: any = AnalyticsModelSchema.getCreateModelSchema({
248
+ modelType: Log,
249
+ disableOpenApiSchema: true,
250
+ });
251
+
252
+ expect(isZodObject(withoutOpenApi)).toBe(true);
253
+ expect(shapeKeys(withoutOpenApi).sort()).toEqual(
254
+ shapeKeys(withOpenApi).sort(),
255
+ );
256
+ });
257
+ });
258
+
259
+ describe("getSelectModelSchema", () => {
260
+ test("marks every field as an optional boolean flag", () => {
261
+ const schema: any = AnalyticsModelSchema.getSelectModelSchema({
262
+ modelType: Log,
263
+ });
264
+ const keys: Array<string> = shapeKeys(schema);
265
+ const someKey: string = keys[0]!;
266
+
267
+ expect(keys.length).toBeGreaterThan(0);
268
+ expect(schema.safeParse({}).success).toBe(true);
269
+ expect(schema.safeParse({ [someKey]: true }).success).toBe(true);
270
+ expect(schema.safeParse({ [someKey]: false }).success).toBe(true);
271
+ expect(schema.safeParse({ [someKey]: "yes" }).success).toBe(false);
272
+ });
273
+
274
+ test("builds a select schema for every registered analytics model", () => {
275
+ for (const modelType of AnalyticsModels as Array<ModelCtor>) {
276
+ const schema: any = AnalyticsModelSchema.getSelectModelSchema({
277
+ modelType,
278
+ });
279
+ expect(isZodObject(schema)).toBe(true);
280
+ }
281
+ });
282
+ });
283
+
284
+ describe("getSortModelSchema", () => {
285
+ test("includes only sortable column types", () => {
286
+ const model: Log = new Log();
287
+ const schema: any = AnalyticsModelSchema.getSortModelSchema({
288
+ modelType: Log,
289
+ });
290
+ const keys: Array<string> = shapeKeys(schema);
291
+
292
+ for (const column of model.getTableColumns()) {
293
+ if (SORTABLE_TYPES.includes(column.type)) {
294
+ // Sortable columns must be present.
295
+ expect(keys).toContain(column.key);
296
+ } else {
297
+ // Non-sortable columns (JSON, arrays, maps, …) must be omitted.
298
+ expect(keys).not.toContain(column.key);
299
+ }
300
+ }
301
+ });
302
+
303
+ test("accepts only ASC/DESC sort orders", () => {
304
+ const model: Log = new Log();
305
+ const schema: any = AnalyticsModelSchema.getSortModelSchema({
306
+ modelType: Log,
307
+ });
308
+ const sortableColumn: AnalyticsTableColumn | undefined = model
309
+ .getTableColumns()
310
+ .find((column: AnalyticsTableColumn) => {
311
+ return SORTABLE_TYPES.includes(column.type);
312
+ });
313
+
314
+ expect(sortableColumn).toBeDefined();
315
+
316
+ const key: string = sortableColumn!.key;
317
+
318
+ expect(schema.safeParse({}).success).toBe(true);
319
+ expect(schema.safeParse({ [key]: SortOrder.Ascending }).success).toBe(
320
+ true,
321
+ );
322
+ expect(schema.safeParse({ [key]: SortOrder.Descending }).success).toBe(
323
+ true,
324
+ );
325
+ expect(schema.safeParse({ [key]: "SIDEWAYS" }).success).toBe(false);
326
+ });
327
+
328
+ test("builds a sort schema for every registered analytics model", () => {
329
+ for (const modelType of AnalyticsModels as Array<ModelCtor>) {
330
+ const schema: any = AnalyticsModelSchema.getSortModelSchema({
331
+ modelType,
332
+ });
333
+ expect(isZodObject(schema)).toBe(true);
334
+ }
335
+ });
336
+ });
337
+
338
+ describe("getGroupByModelSchema", () => {
339
+ test("includes only groupable column types", () => {
340
+ const model: Log = new Log();
341
+ const schema: any = AnalyticsModelSchema.getGroupByModelSchema({
342
+ modelType: Log,
343
+ });
344
+ const keys: Array<string> = shapeKeys(schema);
345
+
346
+ for (const column of model.getTableColumns()) {
347
+ if (GROUPABLE_TYPES.includes(column.type)) {
348
+ expect(keys).toContain(column.key);
349
+ } else {
350
+ expect(keys).not.toContain(column.key);
351
+ }
352
+ }
353
+ });
354
+
355
+ test("accepts only the literal true for a groupable field", () => {
356
+ const model: Log = new Log();
357
+ const schema: any = AnalyticsModelSchema.getGroupByModelSchema({
358
+ modelType: Log,
359
+ });
360
+ const groupableColumn: AnalyticsTableColumn | undefined = model
361
+ .getTableColumns()
362
+ .find((column: AnalyticsTableColumn) => {
363
+ return GROUPABLE_TYPES.includes(column.type);
364
+ });
365
+
366
+ expect(groupableColumn).toBeDefined();
367
+
368
+ const key: string = groupableColumn!.key;
369
+
370
+ expect(schema.safeParse({}).success).toBe(true);
371
+ expect(schema.safeParse({ [key]: true }).success).toBe(true);
372
+ // Only `true` is meaningful for grouping; `false` must be rejected.
373
+ expect(schema.safeParse({ [key]: false }).success).toBe(false);
374
+ });
375
+
376
+ test("builds a group-by schema for every registered analytics model", () => {
377
+ for (const modelType of AnalyticsModels as Array<ModelCtor>) {
378
+ const schema: any = AnalyticsModelSchema.getGroupByModelSchema({
379
+ modelType,
380
+ });
381
+ expect(isZodObject(schema)).toBe(true);
382
+ }
383
+ });
384
+ });
385
+
386
+ describe("getQueryModelSchema", () => {
387
+ test("accepts an empty query and is a ZodObject", () => {
388
+ const schema: any = AnalyticsModelSchema.getQueryModelSchema({
389
+ modelType: Log,
390
+ });
391
+
392
+ expect(isZodObject(schema)).toBe(true);
393
+ expect(schema.safeParse({}).success).toBe(true);
394
+ });
395
+
396
+ test("validates operator forms for a Text column", () => {
397
+ const model: Log = new Log();
398
+ const schema: any = AnalyticsModelSchema.getQueryModelSchema({
399
+ modelType: Log,
400
+ });
401
+ const textColumn: AnalyticsTableColumn | undefined = findColumnOfType(
402
+ model.getTableColumns(),
403
+ TableColumnType.Text,
404
+ shapeKeys(schema),
405
+ );
406
+
407
+ expect(textColumn).toBeDefined();
408
+
409
+ const key: string = textColumn!.key;
410
+
411
+ // EqualTo on a Text column is expressed as a bare string value.
412
+ expect(schema.safeParse({ [key]: "some value" }).success).toBe(true);
413
+ // Envelope operators that are valid for Text columns.
414
+ expect(
415
+ schema.safeParse({ [key]: { _type: "Search", value: "x" } }).success,
416
+ ).toBe(true);
417
+ expect(schema.safeParse({ [key]: { _type: "IsNull" } }).success).toBe(
418
+ true,
419
+ );
420
+ /*
421
+ * GreaterThan is not a valid operator for Text columns, so no union
422
+ * branch accepts it.
423
+ */
424
+ expect(
425
+ schema.safeParse({ [key]: { _type: "GreaterThan", value: "x" } })
426
+ .success,
427
+ ).toBe(false);
428
+ // A numeric value has no matching branch for a Text column either.
429
+ expect(schema.safeParse({ [key]: 12345 }).success).toBe(false);
430
+ });
431
+
432
+ test("validates numeric range operators for a Number column", () => {
433
+ const model: Log = new Log();
434
+ const schema: any = AnalyticsModelSchema.getQueryModelSchema({
435
+ modelType: Log,
436
+ });
437
+ const numberColumn: AnalyticsTableColumn | undefined = findColumnOfType(
438
+ model.getTableColumns(),
439
+ TableColumnType.Number,
440
+ shapeKeys(schema),
441
+ );
442
+
443
+ expect(numberColumn).toBeDefined();
444
+
445
+ const key: string = numberColumn!.key;
446
+
447
+ // EqualTo on a Number column is a bare numeric value.
448
+ expect(schema.safeParse({ [key]: 5 }).success).toBe(true);
449
+ // GreaterThan is valid for numeric columns and uses an envelope.
450
+ expect(
451
+ schema.safeParse({ [key]: { _type: "GreaterThan", value: 5 } }).success,
452
+ ).toBe(true);
453
+ // A string value does not satisfy any numeric branch.
454
+ expect(
455
+ schema.safeParse({ [key]: { _type: "GreaterThan", value: "nope" } })
456
+ .success,
457
+ ).toBe(false);
458
+ });
459
+
460
+ test("builds a query schema for every registered analytics model", () => {
461
+ for (const modelType of AnalyticsModels as Array<ModelCtor>) {
462
+ const schema: any = AnalyticsModelSchema.getQueryModelSchema({
463
+ modelType,
464
+ });
465
+ expect(isZodObject(schema)).toBe(true);
466
+ }
467
+ });
468
+ });
469
+ });