@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.
- package/Models/DatabaseModels/TelemetryEntity.ts +45 -0
- package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
- package/Models/DatabaseModels/WorkflowLog.ts +38 -0
- package/Server/API/UserAPI.ts +16 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
- package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
- package/Server/Services/TelemetryEntityService.ts +114 -1
- package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
- package/Server/Types/Workflow/TriggerCode.ts +12 -0
- package/Server/Types/Workflow/Workflow.ts +5 -0
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
- package/Server/Utils/VM/VMAPI.ts +56 -8
- package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
- package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
- package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
- package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
- package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
- package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
- package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
- package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
- package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
- package/Tests/Types/Workflow/Templates.test.ts +403 -0
- package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
- package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
- package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
- package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
- package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
- package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
- package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
- package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
- package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
- package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
- package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
- package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
- package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
- package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
- package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
- package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
- package/Types/SerializableObjectDictionary.ts +133 -39
- package/Types/Telemetry/EntitySource.ts +40 -0
- package/Types/Telemetry/EntityType.ts +27 -0
- package/Types/Telemetry/EntityTypeGroups.ts +65 -0
- package/Types/Workflow/Component.ts +29 -0
- package/Types/Workflow/Components/API.ts +35 -0
- package/Types/Workflow/Components/BaseModel.ts +7 -3
- package/Types/Workflow/StepTrace.ts +181 -0
- package/Types/Workflow/TemplateSyntax.ts +499 -0
- package/Types/Workflow/Templates.ts +316 -0
- package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
- package/UI/Components/Dictionary/Dictionary.tsx +23 -6
- package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
- package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
- package/UI/Components/Forms/Types/Field.ts +7 -0
- package/UI/Components/Forms/Validation.ts +63 -1
- package/UI/Components/Input/Input.tsx +31 -3
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
- package/UI/Components/Modal/Modal.tsx +37 -4
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
- package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
- package/UI/Components/Workflow/Component.tsx +28 -26
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
- package/UI/Components/Workflow/GraphLint.ts +591 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
- package/UI/Components/Workflow/ModelSchema.ts +196 -0
- package/UI/Components/Workflow/RunForm.tsx +41 -7
- package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
- package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
- package/UI/Components/Workflow/Utils.ts +93 -1
- package/UI/Components/Workflow/Workflow.tsx +77 -4
- package/UI/Types/LayeredDismissal.ts +31 -0
- package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
- package/Utils/Telemetry/EntityKey.ts +66 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
- package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +16 -1
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
- package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +133 -39
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/EntitySource.js +39 -0
- package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +27 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
- package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
- package/build/dist/Types/Workflow/Component.js +21 -0
- package/build/dist/Types/Workflow/Component.js.map +1 -1
- package/build/dist/Types/Workflow/Components/API.js +35 -0
- package/build/dist/Types/Workflow/Components/API.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
- package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
- package/build/dist/Types/Workflow/StepTrace.js +104 -0
- package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
- package/build/dist/Types/Workflow/Templates.js +218 -0
- package/build/dist/Types/Workflow/Templates.js.map +1 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +44 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +12 -4
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +31 -5
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Component.js +20 -19
- package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
- package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
- package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
- package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
- package/build/dist/UI/Components/Workflow/Utils.js +73 -1
- package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Types/LayeredDismissal.js +21 -0
- package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- 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
|
+
});
|