@oneuptime/common 12.0.1 → 12.0.3
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/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
- package/Models/DatabaseModels/Host.ts +12 -4
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
- package/Models/DatabaseModels/KubernetesResource.ts +8 -7
- package/Models/DatabaseModels/LogDropFilter.ts +9 -20
- package/Models/DatabaseModels/PodmanResource.ts +6 -6
- package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
- package/Models/DatabaseModels/StatusPage.ts +95 -0
- package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
- package/Models/DatabaseModels/StatusPageResource.ts +73 -0
- package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
- package/Server/API/BaseAPI.ts +13 -2
- package/Server/API/DashboardAPI.ts +39 -0
- package/Server/API/TeamMemberAPI.ts +85 -0
- package/Server/EnvironmentConfig.ts +31 -1
- package/Server/Infrastructure/GlobalCache.ts +115 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Middleware/ProjectAuthorization.ts +66 -4
- package/Server/Services/AlertStateTimelineService.ts +20 -0
- package/Server/Services/BillingService.ts +600 -51
- package/Server/Services/CephClusterService.ts +27 -45
- package/Server/Services/CephResourceService.ts +43 -4
- package/Server/Services/CloudResourceService.ts +24 -42
- package/Server/Services/DatabaseService.ts +198 -6
- package/Server/Services/DockerHostService.ts +20 -38
- package/Server/Services/DockerResourceService.ts +62 -8
- package/Server/Services/DockerSwarmClusterService.ts +28 -46
- package/Server/Services/DockerSwarmResourceService.ts +53 -4
- package/Server/Services/HostService.ts +47 -57
- package/Server/Services/IncidentStateTimelineService.ts +20 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/IoTDeviceService.ts +1 -17
- package/Server/Services/IoTFleetService.ts +20 -38
- package/Server/Services/KubernetesClusterService.ts +18 -36
- package/Server/Services/KubernetesContainerService.ts +50 -9
- package/Server/Services/KubernetesResourceService.ts +53 -7
- package/Server/Services/LabelService.ts +153 -1
- package/Server/Services/MetricTypeService.ts +84 -0
- package/Server/Services/MonitorService.ts +146 -14
- package/Server/Services/MonitorStatusTimelineService.ts +29 -0
- package/Server/Services/MonitorTemplateService.ts +65 -0
- package/Server/Services/PodmanHostService.ts +20 -38
- package/Server/Services/PodmanResourceService.ts +62 -8
- package/Server/Services/ProjectService.ts +165 -35
- package/Server/Services/ProxmoxClusterService.ts +24 -42
- package/Server/Services/ProxmoxResourceService.ts +43 -4
- package/Server/Services/RumApplicationService.ts +20 -38
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
- package/Server/Services/ServerlessFunctionService.ts +25 -43
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
- package/Server/Services/ServiceService.ts +299 -73
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
- package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
- package/Server/Services/StatusPageService.ts +95 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
- package/Server/Services/StatusPageSubscriberService.ts +3 -0
- package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
- package/Server/Services/TelemetryEntityService.ts +1 -0
- package/Server/Types/Database/JSONColumnQuery.ts +621 -0
- package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
- package/Server/Types/Database/QueryHelper.ts +17 -32
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
- package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
- package/Server/Utils/Monitor/MonitorResource.ts +63 -36
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
- package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
- package/Server/Utils/SingleFlight.ts +83 -0
- package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
- package/Server/Utils/Telemetry/Telemetry.ts +351 -122
- package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
- package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
- package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
- package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
- package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
- package/Tests/Server/Services/BillingService.test.ts +78 -3
- package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
- package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
- package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
- package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +658 -0
- package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
- package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
- package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +636 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
- package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
- package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
- package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
- package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
- package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
- package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
- package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
- package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
- package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
- package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
- package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
- package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
- package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
- package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
- package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
- package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
- package/Tests/Types/Decimal.test.ts +37 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
- package/Tests/Types/Port.test.ts +38 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
- package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
- package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
- package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
- package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
- package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
- package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
- package/Types/CustomField/CustomFieldDefinition.ts +24 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
- package/Types/Dashboard/DashboardTemplates.ts +397 -0
- package/Types/Database/BigIntColumnTransformer.ts +74 -0
- package/Types/Database/DatabaseProperty.ts +26 -0
- package/Types/Database/NumericColumnValue.ts +128 -0
- package/Types/Date.ts +112 -0
- package/Types/Decimal.ts +16 -4
- package/Types/Events/Recurring.ts +99 -1
- package/Types/Permission.ts +48 -0
- package/Types/Port.ts +16 -4
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
- package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
- package/UI/Utils/TeamMembersByUser.ts +302 -0
- package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/Utils/StatusPage/ReportPeriod.ts +236 -0
- package/Utils/Telemetry/HostIpAddresses.ts +98 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Host.js +12 -4
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
- package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +11 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +39 -0
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +57 -2
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +27 -1
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +466 -36
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +26 -39
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CephResourceService.js +28 -3
- package/build/dist/Server/Services/CephResourceService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +23 -36
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +155 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +19 -32
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +34 -4
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -40
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
- package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +58 -64
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +1 -8
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +19 -32
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +17 -30
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
- package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +127 -0
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MetricTypeService.js +74 -0
- package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +128 -9
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +19 -32
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +34 -4
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +135 -21
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +23 -36
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +19 -32
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +24 -37
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +236 -61
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageService.js +69 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
- package/build/dist/Server/Utils/SingleFlight.js +76 -0
- package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
- package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
- package/build/dist/Types/Database/DatabaseProperty.js +25 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/NumericColumnValue.js +97 -0
- package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
- package/build/dist/Types/Date.js +68 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Decimal.js +14 -3
- package/build/dist/Types/Decimal.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +63 -0
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Port.js +15 -5
- package/build/dist/Types/Port.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +12 -1
|
@@ -11,6 +11,7 @@ import IncidentService from "../../../Server/Services/IncidentService";
|
|
|
11
11
|
import KubernetesResourceService from "../../../Server/Services/KubernetesResourceService";
|
|
12
12
|
import LogService from "../../../Server/Services/LogService";
|
|
13
13
|
import MonitorService from "../../../Server/Services/MonitorService";
|
|
14
|
+
import NetworkSiteService from "../../../Server/Services/NetworkSiteService";
|
|
14
15
|
import PodmanHostService from "../../../Server/Services/PodmanHostService";
|
|
15
16
|
import PodmanResourceService from "../../../Server/Services/PodmanResourceService";
|
|
16
17
|
import ProxmoxResourceService from "../../../Server/Services/ProxmoxResourceService";
|
|
@@ -141,6 +142,30 @@ const RESOURCE_CASES: Array<ResourceCase> = [
|
|
|
141
142
|
nonSelectColumn: "monitorSteps",
|
|
142
143
|
selectKeys: ["_id", "name", "monitorType", "currentMonitorStatus"],
|
|
143
144
|
},
|
|
145
|
+
{
|
|
146
|
+
/*
|
|
147
|
+
* The Network Map draws a FLAT set of pins, so the hierarchy columns
|
|
148
|
+
* (parentSiteId, materializedPath, depth) are deliberately outside both
|
|
149
|
+
* the allowlist and the select — structure an anonymous viewer would be
|
|
150
|
+
* able to walk but the map never shows them.
|
|
151
|
+
*/
|
|
152
|
+
resourceType: "network-site",
|
|
153
|
+
componentType: DashboardComponentType.NetworkMap,
|
|
154
|
+
service: NetworkSiteService,
|
|
155
|
+
kind: null,
|
|
156
|
+
allowedQueryKey: "networkSiteTypeId",
|
|
157
|
+
forbiddenQueryKey: "parentSiteId",
|
|
158
|
+
selectColumn: "name",
|
|
159
|
+
nonSelectColumn: "address",
|
|
160
|
+
selectKeys: [
|
|
161
|
+
"_id",
|
|
162
|
+
"name",
|
|
163
|
+
"networkSiteType",
|
|
164
|
+
"latitude",
|
|
165
|
+
"longitude",
|
|
166
|
+
"currentMonitorStatus",
|
|
167
|
+
],
|
|
168
|
+
},
|
|
144
169
|
{
|
|
145
170
|
resourceType: "host",
|
|
146
171
|
componentType: DashboardComponentType.HostList,
|
|
@@ -465,6 +490,7 @@ const ALL_SERVICES: Array<ListService> = [
|
|
|
465
490
|
IncidentService,
|
|
466
491
|
AlertService,
|
|
467
492
|
MonitorService,
|
|
493
|
+
NetworkSiteService,
|
|
468
494
|
HostService,
|
|
469
495
|
KubernetesResourceService,
|
|
470
496
|
DockerHostService,
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import CommonAPI from "../../../Server/API/CommonAPI";
|
|
2
|
+
import TeamMemberAPI from "../../../Server/API/TeamMemberAPI";
|
|
3
|
+
import TeamMemberService from "../../../Server/Services/TeamMemberService";
|
|
4
|
+
import {
|
|
5
|
+
ExpressRequest,
|
|
6
|
+
ExpressResponse,
|
|
7
|
+
NextFunction,
|
|
8
|
+
} from "../../../Server/Utils/Express";
|
|
9
|
+
import Response from "../../../Server/Utils/Response";
|
|
10
|
+
import { mockRouter } from "./Helpers";
|
|
11
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
12
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
13
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
14
|
+
import Permission, {
|
|
15
|
+
UserPermission,
|
|
16
|
+
UserTenantAccessPermission,
|
|
17
|
+
} from "../../../Types/Permission";
|
|
18
|
+
import {
|
|
19
|
+
afterEach,
|
|
20
|
+
beforeAll,
|
|
21
|
+
beforeEach,
|
|
22
|
+
describe,
|
|
23
|
+
expect,
|
|
24
|
+
test,
|
|
25
|
+
} from "@jest/globals";
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* POST /team-member/remove-user-from-project exists so that removing a person
|
|
29
|
+
* from a project is ONE server-side delete rather than a DELETE per membership
|
|
30
|
+
* issued from the browser.
|
|
31
|
+
*
|
|
32
|
+
* That distinction is the whole point, and it is invisible from the outside:
|
|
33
|
+
* TeamMemberService.onBeforeDelete refuses to remove the last accepted member
|
|
34
|
+
* of a team that shouldHaveAtLeastOneMember (the Owners team), and it refuses
|
|
35
|
+
* per request. A client-side loop therefore deletes every other membership
|
|
36
|
+
* first and only then reports the failure, leaving the person stripped of the
|
|
37
|
+
* teams the admin was just told they had not lost. A single deleteBy runs that
|
|
38
|
+
* guard across the whole set before deleting anything.
|
|
39
|
+
*
|
|
40
|
+
* So these tests pin: one deleteBy, over exactly {userId, projectId}, with the
|
|
41
|
+
* caller's own permissions (not root), and with the project taken from the
|
|
42
|
+
* request's tenant rather than from anything the caller put in the body.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
jest.mock("../../../Server/Utils/Express", () => {
|
|
46
|
+
return {
|
|
47
|
+
getRouter: () => {
|
|
48
|
+
return mockRouter;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
54
|
+
return {
|
|
55
|
+
sendEntityArrayResponse: jest.fn(),
|
|
56
|
+
sendJsonObjectResponse: jest.fn(),
|
|
57
|
+
sendEmptySuccessResponse: jest.fn(),
|
|
58
|
+
sendEntityResponse: jest.fn(),
|
|
59
|
+
sendErrorResponse: jest.fn(),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const ROUTE: string = "/team-member/remove-user-from-project";
|
|
64
|
+
|
|
65
|
+
type RouteCallResult = {
|
|
66
|
+
thrownToNext: unknown;
|
|
67
|
+
nextCallCount: number;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function buildProps(data: {
|
|
71
|
+
projectId: ObjectID | undefined;
|
|
72
|
+
userId: ObjectID;
|
|
73
|
+
}): DatabaseCommonInteractionProps {
|
|
74
|
+
const memberPermission: UserPermission = {
|
|
75
|
+
_type: "UserPermission",
|
|
76
|
+
permission: Permission.ProjectAdmin,
|
|
77
|
+
labelIds: [],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const permissionMap: Dictionary<UserTenantAccessPermission> = {};
|
|
81
|
+
|
|
82
|
+
if (data.projectId) {
|
|
83
|
+
permissionMap[data.projectId.toString()] = {
|
|
84
|
+
_type: "UserTenantAccessPermission",
|
|
85
|
+
projectId: data.projectId,
|
|
86
|
+
permissions: [memberPermission],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
tenantId: data.projectId,
|
|
92
|
+
userId: data.userId,
|
|
93
|
+
userTenantAccessPermission: permissionMap,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function callRoute(data: {
|
|
98
|
+
body: Dictionary<unknown>;
|
|
99
|
+
authenticatedUserId?: ObjectID | undefined;
|
|
100
|
+
}): Promise<RouteCallResult> {
|
|
101
|
+
const req: ExpressRequest = {
|
|
102
|
+
params: {},
|
|
103
|
+
query: {},
|
|
104
|
+
body: data.body,
|
|
105
|
+
headers: {},
|
|
106
|
+
userAuthorization: data.authenticatedUserId
|
|
107
|
+
? { userId: data.authenticatedUserId }
|
|
108
|
+
: undefined,
|
|
109
|
+
} as unknown as ExpressRequest;
|
|
110
|
+
|
|
111
|
+
const res: ExpressResponse = {
|
|
112
|
+
send: jest.fn(),
|
|
113
|
+
json: jest.fn(),
|
|
114
|
+
status: jest.fn().mockReturnThis(),
|
|
115
|
+
} as unknown as ExpressResponse;
|
|
116
|
+
|
|
117
|
+
const next: jest.Mock = jest.fn();
|
|
118
|
+
|
|
119
|
+
await mockRouter
|
|
120
|
+
.match("POST", ROUTE)
|
|
121
|
+
.handlerFunction(req, res, next as unknown as NextFunction);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
thrownToNext: next.mock.calls[0] ? next.mock.calls[0][0] : undefined,
|
|
125
|
+
nextCallCount: next.mock.calls.length,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe("POST /team-member/remove-user-from-project", () => {
|
|
130
|
+
let projectId: ObjectID;
|
|
131
|
+
let callerUserId: ObjectID;
|
|
132
|
+
let targetUserId: ObjectID;
|
|
133
|
+
let deleteBySpy: jest.SpyInstance;
|
|
134
|
+
let propsSpy: jest.SpyInstance;
|
|
135
|
+
|
|
136
|
+
beforeAll(() => {
|
|
137
|
+
mockRouter.routes.length = 0;
|
|
138
|
+
new TeamMemberAPI();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
jest.clearAllMocks();
|
|
143
|
+
projectId = ObjectID.generate();
|
|
144
|
+
callerUserId = ObjectID.generate();
|
|
145
|
+
targetUserId = ObjectID.generate();
|
|
146
|
+
|
|
147
|
+
deleteBySpy = jest
|
|
148
|
+
.spyOn(TeamMemberService, "deleteBy")
|
|
149
|
+
.mockResolvedValue(3);
|
|
150
|
+
|
|
151
|
+
propsSpy = jest
|
|
152
|
+
.spyOn(CommonAPI, "getDatabaseCommonInteractionProps")
|
|
153
|
+
.mockResolvedValue(
|
|
154
|
+
buildProps({ projectId: projectId, userId: callerUserId }),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
afterEach(() => {
|
|
159
|
+
jest.restoreAllMocks();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("the route is registered", () => {
|
|
163
|
+
expect(() => {
|
|
164
|
+
return mockRouter.match("POST", ROUTE);
|
|
165
|
+
}).not.toThrow();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("removes every membership in ONE delete, scoped to the user and project", async () => {
|
|
169
|
+
await callRoute({
|
|
170
|
+
body: { userId: targetUserId.toString() },
|
|
171
|
+
authenticatedUserId: callerUserId,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(deleteBySpy).toHaveBeenCalledTimes(1);
|
|
175
|
+
|
|
176
|
+
const deleteBy: {
|
|
177
|
+
query: Dictionary<unknown>;
|
|
178
|
+
props: DatabaseCommonInteractionProps;
|
|
179
|
+
} = deleteBySpy.mock.calls[0]![0] as {
|
|
180
|
+
query: Dictionary<unknown>;
|
|
181
|
+
props: DatabaseCommonInteractionProps;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
expect((deleteBy.query["userId"] as ObjectID).toString()).toBe(
|
|
185
|
+
targetUserId.toString(),
|
|
186
|
+
);
|
|
187
|
+
expect((deleteBy.query["projectId"] as ObjectID).toString()).toBe(
|
|
188
|
+
projectId.toString(),
|
|
189
|
+
);
|
|
190
|
+
// Nothing else narrows it — every team the user is on is in scope.
|
|
191
|
+
expect(Object.keys(deleteBy.query).sort()).toEqual(["projectId", "userId"]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("runs the delete with the caller's own permissions, never as root", async () => {
|
|
195
|
+
await callRoute({
|
|
196
|
+
body: { userId: targetUserId.toString() },
|
|
197
|
+
authenticatedUserId: callerUserId,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const props: DatabaseCommonInteractionProps = (
|
|
201
|
+
deleteBySpy.mock.calls[0]![0] as { props: DatabaseCommonInteractionProps }
|
|
202
|
+
).props;
|
|
203
|
+
|
|
204
|
+
expect(props.isRoot).toBeFalsy();
|
|
205
|
+
expect(props.userId?.toString()).toBe(callerUserId.toString());
|
|
206
|
+
expect(props.tenantId?.toString()).toBe(projectId.toString());
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("reports how many memberships were removed", async () => {
|
|
210
|
+
await callRoute({
|
|
211
|
+
body: { userId: targetUserId.toString() },
|
|
212
|
+
authenticatedUserId: callerUserId,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(Response.sendJsonObjectResponse).toHaveBeenCalledWith(
|
|
216
|
+
expect.anything(),
|
|
217
|
+
expect.anything(),
|
|
218
|
+
{ numberOfMembershipsDeleted: 3 },
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("takes the project from the request's tenant, not from the body", async () => {
|
|
223
|
+
const attackerSuppliedProjectId: ObjectID = ObjectID.generate();
|
|
224
|
+
|
|
225
|
+
await callRoute({
|
|
226
|
+
body: {
|
|
227
|
+
userId: targetUserId.toString(),
|
|
228
|
+
projectId: attackerSuppliedProjectId.toString(),
|
|
229
|
+
},
|
|
230
|
+
authenticatedUserId: callerUserId,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const query: Dictionary<unknown> = (
|
|
234
|
+
deleteBySpy.mock.calls[0]![0] as { query: Dictionary<unknown> }
|
|
235
|
+
).query;
|
|
236
|
+
|
|
237
|
+
expect((query["projectId"] as ObjectID).toString()).toBe(
|
|
238
|
+
projectId.toString(),
|
|
239
|
+
);
|
|
240
|
+
expect((query["projectId"] as ObjectID).toString()).not.toBe(
|
|
241
|
+
attackerSuppliedProjectId.toString(),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("rejects an unauthenticated caller without deleting anything", async () => {
|
|
246
|
+
await callRoute({ body: { userId: targetUserId.toString() } });
|
|
247
|
+
|
|
248
|
+
expect(deleteBySpy).not.toHaveBeenCalled();
|
|
249
|
+
expect(Response.sendErrorResponse).toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("rejects a request with no tenant without deleting anything", async () => {
|
|
253
|
+
propsSpy.mockResolvedValue(
|
|
254
|
+
buildProps({ projectId: undefined, userId: callerUserId }),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
await callRoute({
|
|
258
|
+
body: { userId: targetUserId.toString() },
|
|
259
|
+
authenticatedUserId: callerUserId,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(deleteBySpy).not.toHaveBeenCalled();
|
|
263
|
+
expect(Response.sendErrorResponse).toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("rejects a missing user id without deleting anything", async () => {
|
|
267
|
+
await callRoute({ body: {}, authenticatedUserId: callerUserId });
|
|
268
|
+
|
|
269
|
+
expect(deleteBySpy).not.toHaveBeenCalled();
|
|
270
|
+
expect(Response.sendErrorResponse).toHaveBeenCalled();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("rejects a user id that is not a uuid without deleting anything", async () => {
|
|
274
|
+
const result: RouteCallResult = await callRoute({
|
|
275
|
+
body: { userId: "'; DROP TABLE TeamMember; --" },
|
|
276
|
+
authenticatedUserId: callerUserId,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(deleteBySpy).not.toHaveBeenCalled();
|
|
280
|
+
expect(result.nextCallCount).toBe(1);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("passes a server rejection on rather than reporting success", async () => {
|
|
284
|
+
/*
|
|
285
|
+
* This is the last-accepted-Owner guard. It has to reach the caller as a
|
|
286
|
+
* failure with nothing deleted — which is exactly what the single deleteBy
|
|
287
|
+
* buys over a per-membership loop.
|
|
288
|
+
*/
|
|
289
|
+
const guardError: Error = new Error(
|
|
290
|
+
"This team should have at least 1 member who has accepted the invitation.",
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
deleteBySpy.mockRejectedValue(guardError);
|
|
294
|
+
|
|
295
|
+
const result: RouteCallResult = await callRoute({
|
|
296
|
+
body: { userId: targetUserId.toString() },
|
|
297
|
+
authenticatedUserId: callerUserId,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(result.thrownToNext).toBe(guardError);
|
|
301
|
+
expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -18,6 +18,7 @@ type MockClient = {
|
|
|
18
18
|
expire: jest.Mock;
|
|
19
19
|
get: jest.Mock;
|
|
20
20
|
del: jest.Mock;
|
|
21
|
+
eval: jest.Mock;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
describe("GlobalCache.setString", () => {
|
|
@@ -29,6 +30,7 @@ describe("GlobalCache.setString", () => {
|
|
|
29
30
|
expire: jest.fn().mockResolvedValue(1),
|
|
30
31
|
get: jest.fn(),
|
|
31
32
|
del: jest.fn().mockResolvedValue(1),
|
|
33
|
+
eval: jest.fn().mockResolvedValue(1),
|
|
32
34
|
};
|
|
33
35
|
(Redis.getClient as jest.Mock).mockReturnValue(client);
|
|
34
36
|
(Redis.isConnected as jest.Mock).mockReturnValue(true);
|
|
@@ -110,6 +112,7 @@ describe("GlobalCache.deleteKey", () => {
|
|
|
110
112
|
expire: jest.fn().mockResolvedValue(1),
|
|
111
113
|
get: jest.fn(),
|
|
112
114
|
del: jest.fn().mockResolvedValue(1),
|
|
115
|
+
eval: jest.fn().mockResolvedValue(1),
|
|
113
116
|
};
|
|
114
117
|
(Redis.getClient as jest.Mock).mockReturnValue(client);
|
|
115
118
|
(Redis.isConnected as jest.Mock).mockReturnValue(true);
|
|
@@ -141,3 +144,226 @@ describe("GlobalCache.deleteKey", () => {
|
|
|
141
144
|
expect(client.del).not.toHaveBeenCalled();
|
|
142
145
|
});
|
|
143
146
|
});
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* The atomic fence primitives.
|
|
150
|
+
*
|
|
151
|
+
* These exist because `getString()` followed by `setString()` is
|
|
152
|
+
* check-then-act, and the OneUptime ingest path is precisely the workload
|
|
153
|
+
* that turns that race from theoretical into load-bearing: when thousands of
|
|
154
|
+
* concurrent jobs across ~100 worker pods resolve the same row in the same
|
|
155
|
+
* instant, every one of them reads the same miss and every one of them
|
|
156
|
+
* proceeds. A fence written to admit ONE writer per window admitted all of
|
|
157
|
+
* them, and the row-lock convoy that followed took production down (1,017
|
|
158
|
+
* active connections, 892 parked on row locks, the tail waiting 3.7 hours).
|
|
159
|
+
*
|
|
160
|
+
* The assertions below are about atomicity — that exactly one Redis command
|
|
161
|
+
* is issued, and that it carries the NX / compare-and-claim semantics. A
|
|
162
|
+
* refactor that "simplifies" either of these back into a read plus a write
|
|
163
|
+
* would keep every other test in the repo passing and reintroduce the outage.
|
|
164
|
+
*/
|
|
165
|
+
describe("GlobalCache.setStringIfNotExists", () => {
|
|
166
|
+
let client: MockClient;
|
|
167
|
+
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
client = {
|
|
170
|
+
set: jest.fn().mockResolvedValue("OK"),
|
|
171
|
+
expire: jest.fn().mockResolvedValue(1),
|
|
172
|
+
get: jest.fn(),
|
|
173
|
+
del: jest.fn().mockResolvedValue(1),
|
|
174
|
+
eval: jest.fn().mockResolvedValue(1),
|
|
175
|
+
};
|
|
176
|
+
(Redis.getClient as jest.Mock).mockReturnValue(client);
|
|
177
|
+
(Redis.isConnected as jest.Mock).mockReturnValue(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
afterEach(() => {
|
|
181
|
+
jest.clearAllMocks();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("claims the fence with a single atomic SET ... EX ... NX", async () => {
|
|
185
|
+
const acquired: boolean = await GlobalCache.setStringIfNotExists(
|
|
186
|
+
"ns",
|
|
187
|
+
"key",
|
|
188
|
+
"1",
|
|
189
|
+
{ expiresInSeconds: 60 },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(acquired).toBe(true);
|
|
193
|
+
expect(client.set).toHaveBeenCalledTimes(1);
|
|
194
|
+
expect(client.set).toHaveBeenCalledWith("ns-key", "1", "EX", 60, "NX");
|
|
195
|
+
// No read: a GET here would be the check-then-act race coming back.
|
|
196
|
+
expect(client.get).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("returns false when another caller already holds the fence", async () => {
|
|
200
|
+
client.set.mockResolvedValue(null);
|
|
201
|
+
|
|
202
|
+
await expect(
|
|
203
|
+
GlobalCache.setStringIfNotExists("ns", "key", "1", {
|
|
204
|
+
expiresInSeconds: 60,
|
|
205
|
+
}),
|
|
206
|
+
).resolves.toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/*
|
|
210
|
+
* Only a literal "OK" means the key was created. Anything else — a driver
|
|
211
|
+
* change, a proxy rewriting the reply — must read as "did not acquire":
|
|
212
|
+
* losing a fence costs one redundant write, wrongly winning one costs the
|
|
213
|
+
* mutual exclusion the fence exists to provide.
|
|
214
|
+
*/
|
|
215
|
+
test("treats any non-OK reply as not acquired", async () => {
|
|
216
|
+
client.set.mockResolvedValue("QUEUED");
|
|
217
|
+
|
|
218
|
+
await expect(
|
|
219
|
+
GlobalCache.setStringIfNotExists("ns", "key", "1"),
|
|
220
|
+
).resolves.toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("throws when the cache is not connected", async () => {
|
|
224
|
+
(Redis.isConnected as jest.Mock).mockReturnValue(false);
|
|
225
|
+
|
|
226
|
+
await expect(
|
|
227
|
+
GlobalCache.setStringIfNotExists("ns", "key", "1"),
|
|
228
|
+
).rejects.toThrow(DatabaseNotConnectedException);
|
|
229
|
+
expect(client.set).not.toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("defaults the TTL to 30 days when no option is passed", async () => {
|
|
233
|
+
await GlobalCache.setStringIfNotExists("ns", "key", "1");
|
|
234
|
+
|
|
235
|
+
expect(client.set).toHaveBeenCalledWith(
|
|
236
|
+
"ns-key",
|
|
237
|
+
"1",
|
|
238
|
+
"EX",
|
|
239
|
+
OneUptimeDate.getSecondsInDays(30),
|
|
240
|
+
"NX",
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("GlobalCache.setStringIfChanged", () => {
|
|
246
|
+
let client: MockClient;
|
|
247
|
+
|
|
248
|
+
beforeEach(() => {
|
|
249
|
+
client = {
|
|
250
|
+
set: jest.fn().mockResolvedValue("OK"),
|
|
251
|
+
expire: jest.fn().mockResolvedValue(1),
|
|
252
|
+
get: jest.fn(),
|
|
253
|
+
del: jest.fn().mockResolvedValue(1),
|
|
254
|
+
eval: jest.fn().mockResolvedValue(1),
|
|
255
|
+
};
|
|
256
|
+
(Redis.getClient as jest.Mock).mockReturnValue(client);
|
|
257
|
+
(Redis.isConnected as jest.Mock).mockReturnValue(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
afterEach(() => {
|
|
261
|
+
jest.clearAllMocks();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
/*
|
|
265
|
+
* Plain NX is the wrong primitive for a fingerprint throttle: it fails
|
|
266
|
+
* whenever the key exists, including when it holds a STALE fingerprint, so
|
|
267
|
+
* a genuinely changed payload (a new service.version after a deploy) would
|
|
268
|
+
* be suppressed for the whole window and never persisted at all.
|
|
269
|
+
*/
|
|
270
|
+
test("claims the window in one atomic EVAL, with no separate read", async () => {
|
|
271
|
+
const claimed: boolean = await GlobalCache.setStringIfChanged(
|
|
272
|
+
"ns",
|
|
273
|
+
"key",
|
|
274
|
+
"fingerprint",
|
|
275
|
+
{ expiresInSeconds: 300 },
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(claimed).toBe(true);
|
|
279
|
+
expect(client.eval).toHaveBeenCalledTimes(1);
|
|
280
|
+
expect(client.get).not.toHaveBeenCalled();
|
|
281
|
+
expect(client.set).not.toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("returns false when the stored value already matches", async () => {
|
|
285
|
+
client.eval.mockResolvedValue(0);
|
|
286
|
+
|
|
287
|
+
await expect(
|
|
288
|
+
GlobalCache.setStringIfChanged("ns", "key", "fingerprint"),
|
|
289
|
+
).resolves.toBe(false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
/*
|
|
293
|
+
* The key must travel as KEYS[1], not be interpolated into the script body
|
|
294
|
+
* — Redis Cluster routes a script by its declared keys, and an inlined key
|
|
295
|
+
* makes the script run on an arbitrary node.
|
|
296
|
+
*/
|
|
297
|
+
test("declares exactly one key, passed as KEYS[1]", async () => {
|
|
298
|
+
await GlobalCache.setStringIfChanged("ns", "key", "fingerprint", {
|
|
299
|
+
expiresInSeconds: 300,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const args: Array<unknown> = client.eval.mock.calls[0] as Array<unknown>;
|
|
303
|
+
const script: string = args[0] as string;
|
|
304
|
+
|
|
305
|
+
expect(args[1]).toBe(1);
|
|
306
|
+
expect(args[2]).toBe("ns-key");
|
|
307
|
+
expect(args[3]).toBe("fingerprint");
|
|
308
|
+
expect(args[4]).toBe("300");
|
|
309
|
+
expect(script).toContain("KEYS[1]");
|
|
310
|
+
expect(script).not.toContain("ns-key");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("throws when the cache is not connected", async () => {
|
|
314
|
+
(Redis.isConnected as jest.Mock).mockReturnValue(false);
|
|
315
|
+
|
|
316
|
+
await expect(
|
|
317
|
+
GlobalCache.setStringIfChanged("ns", "key", "fingerprint"),
|
|
318
|
+
).rejects.toThrow(DatabaseNotConnectedException);
|
|
319
|
+
expect(client.eval).not.toHaveBeenCalled();
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
/*
|
|
324
|
+
* A fixed TTL lets a fleet-wide event — a deploy, a scale-up, a Redis restart
|
|
325
|
+
* — align every row's throttle window. They then all expire in the same
|
|
326
|
+
* second and the herd re-forms on a fixed period, which is the convoy back
|
|
327
|
+
* again on a timer. Jitter breaks the alignment permanently: windows drift
|
|
328
|
+
* apart after the first expiry and stay apart.
|
|
329
|
+
*/
|
|
330
|
+
describe("GlobalCache.withJitter", () => {
|
|
331
|
+
test("never returns less than the requested TTL", () => {
|
|
332
|
+
for (let i: number = 0; i < 500; i++) {
|
|
333
|
+
expect(GlobalCache.withJitter(60)).toBeGreaterThanOrEqual(60);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
/*
|
|
338
|
+
* The ceiling is load-bearing. Every markDisconnected* sweep runs at 15
|
|
339
|
+
* minutes against a 5-minute fence; +25% takes that to 6.25 minutes, still
|
|
340
|
+
* 2.4x clear. Widening this without re-checking those thresholds would flap
|
|
341
|
+
* healthy resources between connected and disconnected.
|
|
342
|
+
*/
|
|
343
|
+
test("never exceeds the requested TTL by more than 25%", () => {
|
|
344
|
+
for (let i: number = 0; i < 500; i++) {
|
|
345
|
+
expect(GlobalCache.withJitter(300)).toBeLessThanOrEqual(375);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("actually spreads values rather than returning a constant", () => {
|
|
350
|
+
const seen: Set<number> = new Set<number>();
|
|
351
|
+
|
|
352
|
+
for (let i: number = 0; i < 200; i++) {
|
|
353
|
+
seen.add(GlobalCache.withJitter(600));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
expect(seen.size).toBeGreaterThan(1);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("returns integers — Redis EX rejects fractional seconds", () => {
|
|
360
|
+
for (let i: number = 0; i < 100; i++) {
|
|
361
|
+
expect(Number.isInteger(GlobalCache.withJitter(37))).toBe(true);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("passes through non-positive TTLs unchanged", () => {
|
|
366
|
+
expect(GlobalCache.withJitter(0)).toBe(0);
|
|
367
|
+
expect(GlobalCache.withJitter(-1)).toBe(-1);
|
|
368
|
+
});
|
|
369
|
+
});
|