@oneuptime/common 12.0.2 → 12.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (384) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +14 -0
  2. package/Models/DatabaseModels/CodeRepository.ts +14 -6
  3. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  4. package/Models/DatabaseModels/DockerHost.ts +14 -0
  5. package/Models/DatabaseModels/GlobalOidcProject.ts +18 -0
  6. package/Models/DatabaseModels/GlobalSsoProject.ts +18 -0
  7. package/Models/DatabaseModels/Index.ts +2 -0
  8. package/Models/DatabaseModels/IoTFleet.ts +14 -0
  9. package/Models/DatabaseModels/KubernetesCluster.ts +14 -0
  10. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  11. package/Models/DatabaseModels/NetworkDevice.ts +14 -0
  12. package/Models/DatabaseModels/NetworkInterface.ts +18 -0
  13. package/Models/DatabaseModels/Project.ts +16 -2
  14. package/Models/DatabaseModels/ProxmoxCluster.ts +14 -0
  15. package/Models/DatabaseModels/Service.ts +30 -0
  16. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  17. package/Models/DatabaseModels/StatusPagePrivateUser.ts +30 -0
  18. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  19. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  20. package/Models/DatabaseModels/User.ts +30 -0
  21. package/Server/API/AIAgentDataAPI.ts +31 -0
  22. package/Server/API/BaseAPI.ts +13 -2
  23. package/Server/API/DashboardAPI.ts +458 -12
  24. package/Server/API/GitHubAPI.ts +119 -283
  25. package/Server/API/UserAPI.ts +263 -1
  26. package/Server/EnvironmentConfig.ts +38 -4
  27. package/Server/Infrastructure/GlobalCache.ts +152 -0
  28. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.ts +21 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.ts +68 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +100 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +214 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.ts +69 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  36. package/Server/Services/AlertStateTimelineService.ts +20 -0
  37. package/Server/Services/AnalyticsDatabaseService.ts +72 -1
  38. package/Server/Services/CephClusterService.ts +26 -86
  39. package/Server/Services/CloudResourceService.ts +23 -83
  40. package/Server/Services/CodeRepositoryService.ts +105 -2
  41. package/Server/Services/DatabaseService.ts +296 -2
  42. package/Server/Services/DockerHostService.ts +19 -79
  43. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  44. package/Server/Services/HostService.ts +46 -99
  45. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  46. package/Server/Services/Index.ts +2 -0
  47. package/Server/Services/IoTFleetService.ts +19 -79
  48. package/Server/Services/KubernetesClusterService.ts +17 -78
  49. package/Server/Services/LabelService.ts +57 -10
  50. package/Server/Services/MetricTypeService.ts +84 -0
  51. package/Server/Services/MonitorService.ts +89 -14
  52. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  53. package/Server/Services/MonitorTemplateService.ts +65 -0
  54. package/Server/Services/OpenTelemetryIngestService.ts +230 -26
  55. package/Server/Services/PodmanHostService.ts +19 -79
  56. package/Server/Services/ProxmoxClusterService.ts +23 -83
  57. package/Server/Services/RumApplicationService.ts +19 -80
  58. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  59. package/Server/Services/ServerlessFunctionService.ts +24 -85
  60. package/Server/Services/ServiceService.ts +299 -73
  61. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  62. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  63. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  64. package/Server/Services/TelemetryEntityService.ts +1 -0
  65. package/Server/Services/UserService.ts +184 -0
  66. package/Server/Utils/AI/Toolbox/CodeTools.ts +45 -4
  67. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -0
  68. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +157 -4
  69. package/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.ts +130 -0
  70. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  71. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  72. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  73. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  74. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  75. package/Server/Utils/PasswordHash.ts +306 -0
  76. package/Server/Utils/SingleFlight.ts +83 -0
  77. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  78. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  79. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  80. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  81. package/Tests/App/Dashboard/OverviewCustomFields.test.tsx +333 -0
  82. package/Tests/App/Dashboard/UserCustomFields.test.tsx +392 -0
  83. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  84. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  85. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  86. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  87. package/Tests/Server/API/AIAgentDataRepositoryToken.test.ts +381 -0
  88. package/Tests/Server/API/DashboardPublicAttributeValuesAPI.test.ts +943 -0
  89. package/Tests/Server/API/DashboardPublicMetricsAggregateAPI.test.ts +1296 -0
  90. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  91. package/Tests/Server/API/DashboardPublicTemplatePayloads.test.ts +598 -0
  92. package/Tests/Server/API/GitHubAppInstallationBindingAPI.test.ts +539 -0
  93. package/Tests/Server/API/Helpers.ts +6 -1
  94. package/Tests/Server/API/UserProjectsAPI.test.ts +852 -0
  95. package/Tests/Server/Infrastructure/GlobalCache.test.ts +354 -0
  96. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  97. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +215 -0
  98. package/Tests/Server/Services/AddPerUserPasswordSaltMigration.test.ts +178 -0
  99. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +582 -0
  100. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +5 -0
  101. package/Tests/Server/Services/CodeRepositoryInstallationBinding.test.ts +305 -0
  102. package/Tests/Server/Services/CodeRepositoryResolutionBinding.test.ts +147 -0
  103. package/Tests/Server/Services/DatabaseServicePerUserPasswordSalt.test.ts +870 -0
  104. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  105. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  106. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  107. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  108. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  109. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  110. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +568 -0
  111. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  112. package/Tests/Server/Services/RestoreDroppedUniqueIndexesMigration.test.ts +386 -0
  113. package/Tests/Server/Services/RestoreServiceLowerNameIndexMigration.test.ts +240 -0
  114. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  115. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  116. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  117. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  118. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +885 -0
  119. package/Tests/Server/Services/WidenHashedStringColumnsForScryptMigration.test.ts +220 -0
  120. package/Tests/Server/Utils/AI/CodeTools.test.ts +47 -1
  121. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +10 -0
  122. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  123. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +120 -0
  124. package/Tests/Server/Utils/CodeRepository/GitHubInstallationBinding.test.ts +292 -0
  125. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  126. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  127. package/Tests/Server/Utils/GitHubInstallationOwnershipVerification.test.ts +340 -0
  128. package/Tests/Server/Utils/GitHubWebhookAndTreeCacheIsolation.test.ts +250 -0
  129. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  130. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  131. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  132. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  133. package/Tests/Server/Utils/PasswordHash.test.ts +555 -0
  134. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  135. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  136. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  137. package/Tests/Types/Database/ColumnLength.test.ts +7 -1
  138. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  139. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  140. package/Tests/Types/Decimal.test.ts +37 -0
  141. package/Tests/Types/HashedStringPerUserSalt.test.ts +476 -0
  142. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +293 -0
  143. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +249 -0
  144. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +320 -0
  145. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +271 -0
  146. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  147. package/Tests/Types/Port.test.ts +38 -0
  148. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +696 -0
  149. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  150. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  151. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  152. package/Tests/UI/Utils/UserProjectsModelAPI.test.ts +714 -0
  153. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  154. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  155. package/Tests/Utils/TeamMembersByProject.test.ts +655 -0
  156. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  157. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  158. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  159. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  160. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  161. package/Types/Database/ColumnLength.ts +9 -1
  162. package/Types/Database/DatabaseProperty.ts +26 -0
  163. package/Types/Database/NumericColumnValue.ts +128 -0
  164. package/Types/Database/TableColumn.ts +11 -0
  165. package/Types/Database/UnsynchronizedIndex.ts +48 -0
  166. package/Types/Decimal.ts +16 -4
  167. package/Types/HashedString.ts +139 -4
  168. package/Types/Permission.ts +48 -0
  169. package/Types/Port.ts +16 -4
  170. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +85 -11
  171. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  172. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  173. package/UI/Components/Navbar/NavBar.tsx +12 -2
  174. package/UI/Components/Navbar/NavBarMenuModal.tsx +19 -7
  175. package/UI/Utils/ModelAPI/UserProjectsModelAPI.ts +257 -0
  176. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  177. package/Utils/Dashboard/Components/Index.ts +7 -0
  178. package/Utils/TeamMembersByProject.ts +237 -0
  179. package/build/dist/Models/DatabaseModels/CephCluster.js +16 -1
  180. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/CodeRepository.js +14 -6
  182. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  184. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/DockerHost.js +16 -1
  186. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  187. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +16 -1
  188. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
  189. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +16 -1
  190. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
  191. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  192. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  193. package/build/dist/Models/DatabaseModels/IoTFleet.js +16 -1
  194. package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
  195. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +16 -1
  196. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  197. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  198. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  199. package/build/dist/Models/DatabaseModels/NetworkDevice.js +16 -1
  200. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/NetworkInterface.js +16 -1
  202. package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/Project.js +16 -2
  204. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +16 -1
  206. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/Service.js +28 -1
  208. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  210. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  211. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +32 -0
  212. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  214. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  216. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/User.js +32 -0
  218. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  219. package/build/dist/Server/API/AIAgentDataAPI.js +21 -2
  220. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  221. package/build/dist/Server/API/BaseAPI.js +11 -1
  222. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  223. package/build/dist/Server/API/DashboardAPI.js +343 -10
  224. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  225. package/build/dist/Server/API/GitHubAPI.js +94 -167
  226. package/build/dist/Server/API/GitHubAPI.js.map +1 -1
  227. package/build/dist/Server/API/UserAPI.js +187 -2
  228. package/build/dist/Server/API/UserAPI.js.map +1 -1
  229. package/build/dist/Server/EnvironmentConfig.js +34 -4
  230. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  231. package/build/dist/Server/Infrastructure/GlobalCache.js +120 -0
  232. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  234. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  236. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  237. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js +14 -0
  238. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js.map +1 -0
  239. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js +59 -0
  240. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js.map +1 -0
  241. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +93 -0
  242. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js.map +1 -0
  243. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js +101 -0
  244. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js.map +1 -0
  245. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js +66 -0
  246. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js.map +1 -0
  247. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  248. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  249. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  250. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  251. package/build/dist/Server/Services/AnalyticsDatabaseService.js +57 -1
  252. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  253. package/build/dist/Server/Services/CephClusterService.js +26 -76
  254. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  255. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  256. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  257. package/build/dist/Server/Services/CodeRepositoryService.js +90 -2
  258. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -1
  259. package/build/dist/Server/Services/DatabaseService.js +246 -2
  260. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  261. package/build/dist/Server/Services/DockerHostService.js +19 -69
  262. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  263. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  264. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  265. package/build/dist/Server/Services/HostService.js +58 -102
  266. package/build/dist/Server/Services/HostService.js.map +1 -1
  267. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  268. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  269. package/build/dist/Server/Services/Index.js +2 -0
  270. package/build/dist/Server/Services/Index.js.map +1 -1
  271. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  272. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  273. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  274. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  275. package/build/dist/Server/Services/LabelService.js +42 -7
  276. package/build/dist/Server/Services/LabelService.js.map +1 -1
  277. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  278. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  279. package/build/dist/Server/Services/MonitorService.js +81 -9
  280. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  281. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  282. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  283. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  284. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  285. package/build/dist/Server/Services/OpenTelemetryIngestService.js +155 -25
  286. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  287. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  288. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  289. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  290. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  291. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  292. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  293. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  294. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  295. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  296. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  297. package/build/dist/Server/Services/ServiceService.js +236 -61
  298. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  299. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  300. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  301. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  302. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  303. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  304. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  305. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  306. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  307. package/build/dist/Server/Services/UserService.js +165 -0
  308. package/build/dist/Server/Services/UserService.js.map +1 -1
  309. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +31 -1
  310. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -1
  311. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -0
  312. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  313. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +126 -4
  314. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  315. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js +136 -0
  316. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js.map +1 -0
  317. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  318. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  319. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  320. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  321. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  322. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  323. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  324. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  325. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  326. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  327. package/build/dist/Server/Utils/PasswordHash.js +237 -0
  328. package/build/dist/Server/Utils/PasswordHash.js.map +1 -0
  329. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  330. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  331. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  332. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  333. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  334. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  335. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  336. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  337. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  338. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  339. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  340. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  341. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  342. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  343. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  344. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  345. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  346. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  347. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  348. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  349. package/build/dist/Types/Database/ColumnLength.js +9 -1
  350. package/build/dist/Types/Database/ColumnLength.js.map +1 -1
  351. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  352. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  353. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  354. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  355. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  356. package/build/dist/Types/Database/UnsynchronizedIndex.js +46 -0
  357. package/build/dist/Types/Database/UnsynchronizedIndex.js.map +1 -0
  358. package/build/dist/Types/Decimal.js +14 -3
  359. package/build/dist/Types/Decimal.js.map +1 -1
  360. package/build/dist/Types/HashedString.js +95 -5
  361. package/build/dist/Types/HashedString.js.map +1 -1
  362. package/build/dist/Types/Permission.js +42 -0
  363. package/build/dist/Types/Permission.js.map +1 -1
  364. package/build/dist/Types/Port.js +15 -5
  365. package/build/dist/Types/Port.js.map +1 -1
  366. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +51 -10
  367. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  368. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  369. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  370. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  371. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  372. package/build/dist/UI/Components/Navbar/NavBar.js +3 -1
  373. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  374. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +19 -7
  375. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  376. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js +156 -0
  377. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js.map +1 -0
  378. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  379. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  380. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  381. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  382. package/build/dist/Utils/TeamMembersByProject.js +145 -0
  383. package/build/dist/Utils/TeamMembersByProject.js.map +1 -0
  384. package/package.json +1 -1
@@ -0,0 +1,1212 @@
1
+ import Label from "../../../Models/DatabaseModels/Label";
2
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
3
+ import StatusPageMonitorRule from "../../../Models/DatabaseModels/StatusPageMonitorRule";
4
+ import DatabaseConfig from "../../../Server/DatabaseConfig";
5
+ import LabelService from "../../../Server/Services/LabelService";
6
+ import MonitorFeedService from "../../../Server/Services/MonitorFeedService";
7
+ import MonitorService from "../../../Server/Services/MonitorService";
8
+ import ServiceLevelObjectiveMonitorRuleEngineService from "../../../Server/Services/ServiceLevelObjectiveMonitorRuleEngineService";
9
+ import ServiceLevelObjectiveService from "../../../Server/Services/ServiceLevelObjectiveService";
10
+ import StatusPageMonitorRuleEngineService, {
11
+ StatusPageMonitorRuleSyncResult,
12
+ } from "../../../Server/Services/StatusPageMonitorRuleEngineService";
13
+ import StatusPageMonitorRuleService from "../../../Server/Services/StatusPageMonitorRuleService";
14
+ import StatusPageGroupService from "../../../Server/Services/StatusPageGroupService";
15
+ import StatusPageResourceService from "../../../Server/Services/StatusPageResourceService";
16
+ import StatusPageResource from "../../../Models/DatabaseModels/StatusPageResource";
17
+ import ProjectScopedReferenceValidator from "../../../Server/Utils/Database/ProjectScopedReferenceValidator";
18
+ import StatusPageGroup from "../../../Models/DatabaseModels/StatusPageGroup";
19
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
20
+ import DeleteBy from "../../../Server/Types/Database/DeleteBy";
21
+ import {
22
+ OnCreate,
23
+ OnDelete,
24
+ OnUpdate,
25
+ } from "../../../Server/Types/Database/Hooks";
26
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
27
+ import URL from "../../../Types/API/URL";
28
+ import BadDataException from "../../../Types/Exception/BadDataException";
29
+ import ObjectID from "../../../Types/ObjectID";
30
+ import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
31
+
32
+ /*
33
+ * Contract under test - the places that have to notice a status page monitor
34
+ * rule became stale, plus the guardrails on writing one. The engine itself is
35
+ * covered separately; what matters here is that something actually calls it,
36
+ * and that a half-filled form is refused rather than silently interpreted.
37
+ *
38
+ * - MonitorService, when a monitor is created, and when its labels, name or
39
+ * description change. All three are match criteria, so all three can pull
40
+ * a monitor onto a status page or push it off one.
41
+ *
42
+ * - LabelService, when a label is deleted. Postgres cascades the rule's join
43
+ * rows away without any service hook firing, so a rule that used that
44
+ * label would otherwise keep monitors nothing explains.
45
+ *
46
+ * - StatusPageMonitorRuleService itself, on create, update and delete.
47
+ *
48
+ * Every sync is best-effort: it must never fail the write that triggered it.
49
+ */
50
+
51
+ const PROJECT_ID: ObjectID = new ObjectID(
52
+ "22222222-2222-4222-8222-222222222222",
53
+ );
54
+ const STATUS_PAGE_ID: ObjectID = new ObjectID(
55
+ "33333333-3333-4333-8333-333333333333",
56
+ );
57
+ const MONITOR_ID: ObjectID = new ObjectID(
58
+ "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
59
+ );
60
+ const OTHER_MONITOR_ID: ObjectID = new ObjectID(
61
+ "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
62
+ );
63
+ const LABEL_ID: ObjectID = new ObjectID("dddddddd-dddd-4ddd-8ddd-dddddddddddd");
64
+ const RULE_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
65
+ const OTHER_RULE_ID: ObjectID = new ObjectID(
66
+ "1a1a1a1a-1a1a-4a1a-8a1a-1a1a1a1a1a1a",
67
+ );
68
+ const OTHER_STATUS_PAGE_ID: ObjectID = new ObjectID(
69
+ "34343434-3434-4434-8434-343434343434",
70
+ );
71
+ const GROUP_ID: ObjectID = new ObjectID("44444444-4444-4444-8444-444444444444");
72
+
73
+ // Calls a protected hook without widening the service's public surface.
74
+ function callHook(
75
+ service: unknown,
76
+ name: string,
77
+ ...args: Array<unknown>
78
+ ): Promise<unknown> {
79
+ const hooks: Record<
80
+ string,
81
+ (...hookArgs: Array<unknown>) => Promise<unknown>
82
+ > = service as Record<
83
+ string,
84
+ (...hookArgs: Array<unknown>) => Promise<unknown>
85
+ >;
86
+
87
+ return hooks[name]!.apply(service, args);
88
+ }
89
+
90
+ function monitorUpdate(data: Record<string, unknown>): OnUpdate<Monitor> {
91
+ return {
92
+ updateBy: {
93
+ query: { _id: MONITOR_ID.toString() },
94
+ data: data,
95
+ props: { isRoot: true, tenantId: PROJECT_ID },
96
+ limit: 1,
97
+ skip: 0,
98
+ } as unknown as UpdateBy<Monitor>,
99
+ carryForward: null,
100
+ };
101
+ }
102
+
103
+ function ruleCreate(
104
+ data: Record<string, unknown>,
105
+ ): CreateBy<StatusPageMonitorRule> {
106
+ return {
107
+ data: data,
108
+ props: { isRoot: true, tenantId: PROJECT_ID },
109
+ } as unknown as CreateBy<StatusPageMonitorRule>;
110
+ }
111
+
112
+ function ruleUpdate(
113
+ data: Record<string, unknown>,
114
+ ): OnUpdate<StatusPageMonitorRule> {
115
+ return {
116
+ updateBy: {
117
+ query: { _id: RULE_ID.toString() },
118
+ data: data,
119
+ props: { isRoot: true, tenantId: PROJECT_ID },
120
+ limit: 1,
121
+ skip: 0,
122
+ } as unknown as UpdateBy<StatusPageMonitorRule>,
123
+ carryForward: null,
124
+ };
125
+ }
126
+
127
+ function fakeMonitorRow(): Monitor {
128
+ return {
129
+ id: MONITOR_ID,
130
+ _id: MONITOR_ID.toString(),
131
+ projectId: PROJECT_ID,
132
+ name: "API",
133
+ } as unknown as Monitor;
134
+ }
135
+
136
+ function fakeLabelRow(id: ObjectID): Label {
137
+ return { id: id, _id: id.toString(), name: "Production" } as unknown as Label;
138
+ }
139
+
140
+ function emptySyncResult(): StatusPageMonitorRuleSyncResult {
141
+ return {
142
+ monitorIdsAdded: [],
143
+ statusPageResourceIdsRemoved: [],
144
+ statusPageResourceIdsUpdated: [],
145
+ monitorIdsReleased: [],
146
+ };
147
+ }
148
+
149
+ function fakeRuleRow(id: ObjectID): StatusPageMonitorRule {
150
+ return {
151
+ id: id,
152
+ _id: id.toString(),
153
+ projectId: PROJECT_ID,
154
+ statusPageId: STATUS_PAGE_ID,
155
+ } as unknown as StatusPageMonitorRule;
156
+ }
157
+
158
+ describe("MonitorService.onUpdateSuccess - keeping status page monitor rules honest", () => {
159
+ let syncRulesForMonitorSpy: jest.SpyInstance;
160
+
161
+ beforeEach(() => {
162
+ syncRulesForMonitorSpy = jest
163
+ .spyOn(StatusPageMonitorRuleEngineService, "syncRulesForMonitor")
164
+ .mockResolvedValue([]);
165
+
166
+ jest
167
+ .spyOn(
168
+ ServiceLevelObjectiveMonitorRuleEngineService,
169
+ "syncSlosForMonitor",
170
+ )
171
+ .mockResolvedValue([]);
172
+ jest
173
+ .spyOn(MonitorService, "findOneById")
174
+ .mockResolvedValue(fakeMonitorRow());
175
+ jest
176
+ .spyOn(DatabaseConfig, "getDashboardUrl")
177
+ .mockResolvedValue(URL.fromString("https://oneuptime.test/dashboard"));
178
+ jest.spyOn(LabelService, "findBy").mockResolvedValue([]);
179
+ jest
180
+ .spyOn(MonitorFeedService, "createMonitorFeedItem")
181
+ .mockResolvedValue(undefined as never);
182
+ });
183
+
184
+ afterEach(() => {
185
+ jest.restoreAllMocks();
186
+ });
187
+
188
+ it("re-runs the rules when a monitor gains a label", async () => {
189
+ await callHook(
190
+ MonitorService,
191
+ "onUpdateSuccess",
192
+ monitorUpdate({ labels: [{ _id: LABEL_ID.toString() }] }),
193
+ [MONITOR_ID],
194
+ );
195
+
196
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledTimes(1);
197
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledWith({
198
+ monitorId: MONITOR_ID,
199
+ projectId: PROJECT_ID,
200
+ });
201
+ });
202
+
203
+ it("re-runs them when the last label is removed - `[]` is an edit, not an absence", async () => {
204
+ await callHook(
205
+ MonitorService,
206
+ "onUpdateSuccess",
207
+ monitorUpdate({ labels: [] }),
208
+ [MONITOR_ID],
209
+ );
210
+
211
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledTimes(1);
212
+ });
213
+
214
+ /*
215
+ * Name and description are match criteria for these rules even though they
216
+ * are not for SLO rules, so a rename genuinely can move a monitor between
217
+ * status pages.
218
+ */
219
+ it("re-runs them when a monitor is renamed", async () => {
220
+ await callHook(
221
+ MonitorService,
222
+ "onUpdateSuccess",
223
+ monitorUpdate({ name: "api-checkout" }),
224
+ [MONITOR_ID],
225
+ );
226
+
227
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledTimes(1);
228
+ });
229
+
230
+ it("re-runs them when a monitor's description changes", async () => {
231
+ await callHook(
232
+ MonitorService,
233
+ "onUpdateSuccess",
234
+ monitorUpdate({ description: "now tier-1" }),
235
+ [MONITOR_ID],
236
+ );
237
+
238
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledTimes(1);
239
+ });
240
+
241
+ it("re-runs them for every monitor a bulk edit touched", async () => {
242
+ await callHook(
243
+ MonitorService,
244
+ "onUpdateSuccess",
245
+ monitorUpdate({ labels: [{ _id: LABEL_ID.toString() }] }),
246
+ [MONITOR_ID, OTHER_MONITOR_ID],
247
+ );
248
+
249
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledTimes(2);
250
+ expect(syncRulesForMonitorSpy).toHaveBeenNthCalledWith(2, {
251
+ monitorId: OTHER_MONITOR_ID,
252
+ projectId: PROJECT_ID,
253
+ });
254
+ });
255
+
256
+ it("does not run them for an edit that cannot change what a rule matches", async () => {
257
+ await callHook(
258
+ MonitorService,
259
+ "onUpdateSuccess",
260
+ monitorUpdate({ disableActiveMonitoring: true }),
261
+ [MONITOR_ID],
262
+ );
263
+
264
+ expect(syncRulesForMonitorSpy).not.toHaveBeenCalled();
265
+ });
266
+
267
+ it("does not fail the monitor update when the sync throws", async () => {
268
+ syncRulesForMonitorSpy.mockRejectedValue(new Error("db down"));
269
+
270
+ await expect(
271
+ callHook(
272
+ MonitorService,
273
+ "onUpdateSuccess",
274
+ monitorUpdate({ labels: [] }),
275
+ [MONITOR_ID],
276
+ ),
277
+ ).resolves.toBeDefined();
278
+ });
279
+
280
+ it("keeps syncing the remaining monitors after one of them fails", async () => {
281
+ syncRulesForMonitorSpy
282
+ .mockRejectedValueOnce(new Error("db down"))
283
+ .mockResolvedValueOnce([]);
284
+
285
+ await callHook(
286
+ MonitorService,
287
+ "onUpdateSuccess",
288
+ monitorUpdate({ labels: [] }),
289
+ [MONITOR_ID, OTHER_MONITOR_ID],
290
+ );
291
+
292
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledTimes(2);
293
+ });
294
+ });
295
+
296
+ describe("LabelService delete hooks - a deleted label must not strand resources", () => {
297
+ let syncResourcesForRuleSpy: jest.SpyInstance;
298
+ let ruleFindBySpy: jest.SpyInstance;
299
+
300
+ beforeEach(() => {
301
+ syncResourcesForRuleSpy = jest
302
+ .spyOn(StatusPageMonitorRuleEngineService, "syncResourcesForRule")
303
+ .mockResolvedValue(emptySyncResult());
304
+
305
+ jest
306
+ .spyOn(
307
+ ServiceLevelObjectiveMonitorRuleEngineService,
308
+ "syncMonitorsForSlo",
309
+ )
310
+ .mockResolvedValue({ monitorIdsAdded: [], monitorIdsRemoved: [] });
311
+ jest.spyOn(ServiceLevelObjectiveService, "findBy").mockResolvedValue([]);
312
+ jest
313
+ .spyOn(LabelService, "findBy")
314
+ .mockResolvedValue([fakeLabelRow(LABEL_ID)]);
315
+ ruleFindBySpy = jest
316
+ .spyOn(StatusPageMonitorRuleService, "findBy")
317
+ .mockResolvedValue([fakeRuleRow(RULE_ID), fakeRuleRow(OTHER_RULE_ID)]);
318
+ });
319
+
320
+ afterEach(() => {
321
+ jest.restoreAllMocks();
322
+ });
323
+
324
+ /*
325
+ * The return is mocked, so the QUERY is what decides production behaviour:
326
+ * ask for the wrong thing and the hook re-runs the wrong rules (or none).
327
+ */
328
+ it("looks the rules up by the labels the delete actually matched", async () => {
329
+ await callHook(LabelService, "onBeforeDelete", {
330
+ query: { _id: LABEL_ID.toString() },
331
+ props: { isRoot: true },
332
+ });
333
+
334
+ const call: { query: { monitorLabels: Array<ObjectID> } } = ruleFindBySpy
335
+ .mock.calls[0]![0] as { query: { monitorLabels: Array<ObjectID> } };
336
+
337
+ expect(
338
+ call.query.monitorLabels.map((id: ObjectID) => {
339
+ return id.toString();
340
+ }),
341
+ ).toEqual([LABEL_ID.toString()]);
342
+ });
343
+
344
+ it("does not go looking for rules when the delete matched no labels", async () => {
345
+ jest.spyOn(LabelService, "findBy").mockResolvedValue([]);
346
+
347
+ await callHook(LabelService, "onBeforeDelete", {
348
+ query: { _id: LABEL_ID.toString() },
349
+ props: { isRoot: true },
350
+ });
351
+
352
+ expect(ruleFindBySpy).not.toHaveBeenCalled();
353
+ });
354
+
355
+ it("tolerates a label delete that carried nothing forward", async () => {
356
+ await expect(
357
+ callHook(LabelService, "onDeleteSuccess", {
358
+ deleteBy: {} as unknown as DeleteBy<Label>,
359
+ carryForward: null,
360
+ } as OnDelete<Label>),
361
+ ).resolves.toBeDefined();
362
+
363
+ expect(syncResourcesForRuleSpy).not.toHaveBeenCalled();
364
+ });
365
+
366
+ it("notes down the rules that used the label while it still exists", async () => {
367
+ const onDelete: OnDelete<Label> = (await callHook(
368
+ LabelService,
369
+ "onBeforeDelete",
370
+ { query: { _id: LABEL_ID.toString() }, props: { isRoot: true } },
371
+ )) as OnDelete<Label>;
372
+
373
+ expect(ruleFindBySpy).toHaveBeenCalledTimes(1);
374
+ expect(
375
+ (
376
+ onDelete.carryForward as {
377
+ statusPageMonitorRuleIds: Array<ObjectID>;
378
+ }
379
+ ).statusPageMonitorRuleIds,
380
+ ).toEqual([RULE_ID, OTHER_RULE_ID]);
381
+ });
382
+
383
+ it("re-runs each of those rules once the label is gone", async () => {
384
+ await callHook(LabelService, "onDeleteSuccess", {
385
+ deleteBy: {} as unknown as DeleteBy<Label>,
386
+ carryForward: {
387
+ serviceLevelObjectiveIds: [],
388
+ statusPageMonitorRuleIds: [RULE_ID, OTHER_RULE_ID],
389
+ },
390
+ } as OnDelete<Label>);
391
+
392
+ expect(syncResourcesForRuleSpy).toHaveBeenCalledTimes(2);
393
+ expect(syncResourcesForRuleSpy).toHaveBeenNthCalledWith(1, {
394
+ statusPageMonitorRuleId: RULE_ID,
395
+ });
396
+ });
397
+
398
+ it("does not fail the label delete when a rule sync throws", async () => {
399
+ syncResourcesForRuleSpy.mockRejectedValue(new Error("db down"));
400
+
401
+ await expect(
402
+ callHook(LabelService, "onDeleteSuccess", {
403
+ deleteBy: {} as unknown as DeleteBy<Label>,
404
+ carryForward: { statusPageMonitorRuleIds: [RULE_ID] },
405
+ } as OnDelete<Label>),
406
+ ).resolves.toBeDefined();
407
+ });
408
+
409
+ it("still deletes the label when collecting the affected rules throws", async () => {
410
+ ruleFindBySpy.mockRejectedValue(new Error("db down"));
411
+
412
+ const onDelete: OnDelete<Label> = (await callHook(
413
+ LabelService,
414
+ "onBeforeDelete",
415
+ { query: { _id: LABEL_ID.toString() }, props: { isRoot: true } },
416
+ )) as OnDelete<Label>;
417
+
418
+ expect(
419
+ (
420
+ onDelete.carryForward as {
421
+ statusPageMonitorRuleIds: Array<ObjectID>;
422
+ }
423
+ ).statusPageMonitorRuleIds,
424
+ ).toEqual([]);
425
+ });
426
+ });
427
+
428
+ describe("StatusPageMonitorRuleService.onBeforeCreate - refusing an ambiguous rule", () => {
429
+ beforeEach(() => {
430
+ // Reference scoping is exercised in its own describe below.
431
+ jest
432
+ .spyOn(
433
+ ProjectScopedReferenceValidator,
434
+ "validateReferencesBelongToProject",
435
+ )
436
+ .mockResolvedValue(undefined);
437
+ jest.spyOn(StatusPageGroupService, "findOneById").mockResolvedValue(null);
438
+ });
439
+
440
+ afterEach(() => {
441
+ jest.restoreAllMocks();
442
+ });
443
+
444
+ it("refuses a rule with no match criteria rather than claiming every monitor", async () => {
445
+ await expect(
446
+ callHook(
447
+ StatusPageMonitorRuleService,
448
+ "onBeforeCreate",
449
+ ruleCreate({ statusPageId: STATUS_PAGE_ID, name: "Everything" }),
450
+ ),
451
+ ).rejects.toThrow(BadDataException);
452
+ });
453
+
454
+ it("names the escape hatch in the error, so the refusal is actionable", async () => {
455
+ await expect(
456
+ callHook(
457
+ StatusPageMonitorRuleService,
458
+ "onBeforeCreate",
459
+ ruleCreate({ statusPageId: STATUS_PAGE_ID, name: "Everything" }),
460
+ ),
461
+ ).rejects.toThrow(/\.\*/);
462
+ });
463
+
464
+ it("accepts a rule that only names labels", async () => {
465
+ await expect(
466
+ callHook(
467
+ StatusPageMonitorRuleService,
468
+ "onBeforeCreate",
469
+ ruleCreate({
470
+ statusPageId: STATUS_PAGE_ID,
471
+ name: "Production",
472
+ monitorLabels: [fakeLabelRow(LABEL_ID)],
473
+ }),
474
+ ),
475
+ ).resolves.toBeDefined();
476
+ });
477
+
478
+ it("accepts a rule that only names a name pattern", async () => {
479
+ await expect(
480
+ callHook(
481
+ StatusPageMonitorRuleService,
482
+ "onBeforeCreate",
483
+ ruleCreate({
484
+ statusPageId: STATUS_PAGE_ID,
485
+ name: "APIs",
486
+ monitorNamePattern: "^api",
487
+ }),
488
+ ),
489
+ ).resolves.toBeDefined();
490
+ });
491
+
492
+ it("accepts a rule that only names a description pattern", async () => {
493
+ await expect(
494
+ callHook(
495
+ StatusPageMonitorRuleService,
496
+ "onBeforeCreate",
497
+ ruleCreate({
498
+ statusPageId: STATUS_PAGE_ID,
499
+ name: "Tier 1",
500
+ monitorDescriptionPattern: "tier-1",
501
+ }),
502
+ ),
503
+ ).resolves.toBeDefined();
504
+ });
505
+
506
+ it("requires a status page - a rule with nowhere to add monitors is meaningless", async () => {
507
+ await expect(
508
+ callHook(
509
+ StatusPageMonitorRuleService,
510
+ "onBeforeCreate",
511
+ ruleCreate({ name: "Orphan", monitorNamePattern: ".*" }),
512
+ ),
513
+ ).rejects.toThrow(BadDataException);
514
+ });
515
+
516
+ /*
517
+ * An invalid regex would otherwise be accepted and then silently match
518
+ * nothing forever, which reads as "the rule is broken" with no clue why.
519
+ */
520
+ it("rejects a name pattern that is not a valid regex", async () => {
521
+ await expect(
522
+ callHook(
523
+ StatusPageMonitorRuleService,
524
+ "onBeforeCreate",
525
+ ruleCreate({
526
+ statusPageId: STATUS_PAGE_ID,
527
+ name: "Broken",
528
+ monitorNamePattern: "([unclosed",
529
+ }),
530
+ ),
531
+ ).rejects.toThrow(BadDataException);
532
+ });
533
+
534
+ it("rejects a description pattern that is neither a regex nor a glob", async () => {
535
+ await expect(
536
+ callHook(
537
+ StatusPageMonitorRuleService,
538
+ "onBeforeCreate",
539
+ ruleCreate({
540
+ statusPageId: STATUS_PAGE_ID,
541
+ name: "Broken",
542
+ monitorDescriptionPattern: "tier-(1",
543
+ }),
544
+ ),
545
+ ).rejects.toThrow(BadDataException);
546
+ });
547
+
548
+ /*
549
+ * #2940: `*api*` is not a valid regex ("Nothing to repeat"), but it is the
550
+ * syntax the sibling network device rules take and the one people reach for.
551
+ * Rejecting it - or accepting it and matching nothing - is the bug.
552
+ */
553
+ it("accepts a `*` wildcard glob, the other syntax rules take", async () => {
554
+ await expect(
555
+ callHook(
556
+ StatusPageMonitorRuleService,
557
+ "onBeforeCreate",
558
+ ruleCreate({
559
+ statusPageId: STATUS_PAGE_ID,
560
+ name: "Checkout",
561
+ monitorNamePattern: "*checkout*",
562
+ }),
563
+ ),
564
+ ).resolves.toBeDefined();
565
+ });
566
+
567
+ it("points at both accepted syntaxes when it rejects a pattern", async () => {
568
+ await expect(
569
+ callHook(
570
+ StatusPageMonitorRuleService,
571
+ "onBeforeCreate",
572
+ ruleCreate({
573
+ statusPageId: STATUS_PAGE_ID,
574
+ name: "Broken",
575
+ monitorNamePattern: "api-(01",
576
+ }),
577
+ ),
578
+ ).rejects.toThrow(/wildcard/);
579
+ });
580
+ });
581
+
582
+ describe("StatusPageMonitorRuleService write hooks - running the rule", () => {
583
+ let syncResourcesForRuleSpy: jest.SpyInstance;
584
+ let removeResourcesSpy: jest.SpyInstance;
585
+ let ruleFindBySpy: jest.SpyInstance;
586
+ let syncRulesForMonitorSpy: jest.SpyInstance;
587
+ let resourceDeleteSpy: jest.SpyInstance;
588
+
589
+ beforeEach(() => {
590
+ syncResourcesForRuleSpy = jest
591
+ .spyOn(StatusPageMonitorRuleEngineService, "syncResourcesForRule")
592
+ .mockResolvedValue(emptySyncResult());
593
+ removeResourcesSpy = jest
594
+ .spyOn(StatusPageMonitorRuleEngineService, "removeResourcesAddedByRule")
595
+ .mockResolvedValue([]);
596
+ syncRulesForMonitorSpy = jest
597
+ .spyOn(StatusPageMonitorRuleEngineService, "syncRulesForMonitor")
598
+ .mockResolvedValue([]);
599
+ resourceDeleteSpy = jest
600
+ .spyOn(StatusPageResourceService, "deleteOneById")
601
+ .mockResolvedValue(1);
602
+ ruleFindBySpy = jest
603
+ .spyOn(StatusPageMonitorRuleService, "findBy")
604
+ .mockResolvedValue([fakeRuleRow(RULE_ID)]);
605
+ });
606
+
607
+ afterEach(() => {
608
+ jest.restoreAllMocks();
609
+ });
610
+
611
+ /*
612
+ * Backfill. A rule that only applied to monitors created after it would be
613
+ * useless for the case the feature exists for - a project that already has
614
+ * dozens of monitors.
615
+ */
616
+ it("runs a new rule against the monitors that already exist", async () => {
617
+ await callHook(
618
+ StatusPageMonitorRuleService,
619
+ "onCreateSuccess",
620
+ {
621
+ createBy: ruleCreate({}),
622
+ carryForward: null,
623
+ } as OnCreate<StatusPageMonitorRule>,
624
+ fakeRuleRow(RULE_ID),
625
+ );
626
+
627
+ expect(syncResourcesForRuleSpy).toHaveBeenCalledWith({
628
+ statusPageMonitorRuleId: RULE_ID,
629
+ });
630
+ });
631
+
632
+ it("does not fail the create when the backfill throws", async () => {
633
+ syncResourcesForRuleSpy.mockRejectedValue(new Error("db down"));
634
+
635
+ await expect(
636
+ callHook(
637
+ StatusPageMonitorRuleService,
638
+ "onCreateSuccess",
639
+ {
640
+ createBy: ruleCreate({}),
641
+ carryForward: null,
642
+ } as OnCreate<StatusPageMonitorRule>,
643
+ fakeRuleRow(RULE_ID),
644
+ ),
645
+ ).resolves.toBeDefined();
646
+ });
647
+
648
+ it("re-runs the rule after it is edited", async () => {
649
+ await callHook(
650
+ StatusPageMonitorRuleService,
651
+ "onUpdateSuccess",
652
+ ruleUpdate({ monitorNamePattern: "^api" }),
653
+ [RULE_ID],
654
+ );
655
+
656
+ expect(syncResourcesForRuleSpy).toHaveBeenCalledWith({
657
+ statusPageMonitorRuleId: RULE_ID,
658
+ });
659
+ });
660
+
661
+ it("re-runs every rule a bulk edit touched", async () => {
662
+ await callHook(
663
+ StatusPageMonitorRuleService,
664
+ "onUpdateSuccess",
665
+ ruleUpdate({ isEnabled: false }),
666
+ [RULE_ID, OTHER_RULE_ID],
667
+ );
668
+
669
+ expect(syncResourcesForRuleSpy).toHaveBeenCalledTimes(2);
670
+ });
671
+
672
+ it("does not fail the edit when the re-run throws", async () => {
673
+ syncResourcesForRuleSpy.mockRejectedValue(new Error("db down"));
674
+
675
+ await expect(
676
+ callHook(
677
+ StatusPageMonitorRuleService,
678
+ "onUpdateSuccess",
679
+ ruleUpdate({ isEnabled: false }),
680
+ [RULE_ID],
681
+ ),
682
+ ).resolves.toBeDefined();
683
+ });
684
+
685
+ it("rejects an edit that would leave an invalid regex on the rule", async () => {
686
+ await expect(
687
+ callHook(StatusPageMonitorRuleService, "onBeforeUpdate", {
688
+ query: { _id: RULE_ID.toString() },
689
+ data: { monitorNamePattern: "([unclosed" },
690
+ props: { isRoot: true },
691
+ }),
692
+ ).rejects.toThrow(BadDataException);
693
+ });
694
+
695
+ /*
696
+ * An edit can empty a rule out as easily as a create can. The check has to
697
+ * run against the merged rule, not just the fields the edit mentions.
698
+ */
699
+ it("rejects an edit that would strip the rule's last match criterion", async () => {
700
+ ruleFindBySpy.mockResolvedValue([
701
+ {
702
+ ...fakeRuleRow(RULE_ID),
703
+ monitorLabels: [fakeLabelRow(LABEL_ID)],
704
+ } as unknown as StatusPageMonitorRule,
705
+ ]);
706
+
707
+ await expect(
708
+ callHook(StatusPageMonitorRuleService, "onBeforeUpdate", {
709
+ query: { _id: RULE_ID.toString() },
710
+ data: { monitorLabels: [] },
711
+ props: { isRoot: true },
712
+ }),
713
+ ).rejects.toThrow(BadDataException);
714
+ });
715
+
716
+ it("allows clearing the labels when a stored name pattern still matches monitors", async () => {
717
+ ruleFindBySpy.mockResolvedValue([
718
+ {
719
+ ...fakeRuleRow(RULE_ID),
720
+ monitorLabels: [fakeLabelRow(LABEL_ID)],
721
+ monitorNamePattern: "^api",
722
+ } as unknown as StatusPageMonitorRule,
723
+ ]);
724
+
725
+ await expect(
726
+ callHook(StatusPageMonitorRuleService, "onBeforeUpdate", {
727
+ query: { _id: RULE_ID.toString() },
728
+ data: { monitorLabels: [] },
729
+ props: { isRoot: true },
730
+ }),
731
+ ).resolves.toBeDefined();
732
+ });
733
+
734
+ it("allows swapping one criterion for another in a single edit", async () => {
735
+ ruleFindBySpy.mockResolvedValue([
736
+ {
737
+ ...fakeRuleRow(RULE_ID),
738
+ monitorLabels: [fakeLabelRow(LABEL_ID)],
739
+ } as unknown as StatusPageMonitorRule,
740
+ ]);
741
+
742
+ await expect(
743
+ callHook(StatusPageMonitorRuleService, "onBeforeUpdate", {
744
+ query: { _id: RULE_ID.toString() },
745
+ data: { monitorLabels: [], monitorNamePattern: "^api" },
746
+ props: { isRoot: true },
747
+ }),
748
+ ).resolves.toBeDefined();
749
+ });
750
+
751
+ it("does not read the rule back for an edit that cannot touch the criteria", async () => {
752
+ await callHook(StatusPageMonitorRuleService, "onBeforeUpdate", {
753
+ query: { _id: RULE_ID.toString() },
754
+ data: { isEnabled: false },
755
+ props: { isRoot: true },
756
+ });
757
+
758
+ expect(ruleFindBySpy).not.toHaveBeenCalled();
759
+ });
760
+
761
+ // Deleting a rule has to undo it: the resources it added go with it.
762
+ it("removes the resources a rule added when the rule is deleted", async () => {
763
+ await callHook(
764
+ StatusPageMonitorRuleService,
765
+ "onDeleteSuccess",
766
+ {
767
+ deleteBy: {} as unknown as DeleteBy<StatusPageMonitorRule>,
768
+ carryForward: null,
769
+ },
770
+ [RULE_ID],
771
+ );
772
+
773
+ expect(removeResourcesSpy).toHaveBeenCalledWith({
774
+ statusPageMonitorRuleId: RULE_ID,
775
+ });
776
+ });
777
+
778
+ it("undoes every rule a bulk delete covers", async () => {
779
+ await callHook(
780
+ StatusPageMonitorRuleService,
781
+ "onDeleteSuccess",
782
+ {
783
+ deleteBy: {} as unknown as DeleteBy<StatusPageMonitorRule>,
784
+ carryForward: null,
785
+ },
786
+ [RULE_ID, OTHER_RULE_ID],
787
+ );
788
+
789
+ expect(removeResourcesSpy).toHaveBeenCalledTimes(2);
790
+ });
791
+
792
+ it("still reports the delete as done when the cleanup throws", async () => {
793
+ removeResourcesSpy.mockRejectedValue(new Error("db down"));
794
+
795
+ await expect(
796
+ callHook(
797
+ StatusPageMonitorRuleService,
798
+ "onDeleteSuccess",
799
+ {
800
+ deleteBy: {} as unknown as DeleteBy<StatusPageMonitorRule>,
801
+ carryForward: null,
802
+ },
803
+ [RULE_ID],
804
+ ),
805
+ ).resolves.toBeDefined();
806
+ });
807
+
808
+ /*
809
+ * The security property behind doing the destructive work in
810
+ * onDeleteSuccess.
811
+ *
812
+ * DatabaseService runs onBeforeDelete BEFORE checkDeleteQueryPermission, so
813
+ * a hook there sees the caller's raw, un-tenant-scoped query. A destructive
814
+ * root-privileged pass over it would let anyone delete another project's
815
+ * status page resources just by naming that project's rule id - the
816
+ * permission check would then reject the rule delete, far too late.
817
+ *
818
+ * onBeforeDelete may therefore only READ, and only through the caller's own
819
+ * props. onDeleteSuccess is handed the ids that survived the permission
820
+ * check, which is where anything destructive belongs.
821
+ */
822
+ it("destroys nothing from onBeforeDelete, which runs before the permission check", async () => {
823
+ jest.spyOn(StatusPageResourceService, "findBy").mockResolvedValue([]);
824
+
825
+ await callHook(StatusPageMonitorRuleService, "onBeforeDelete", {
826
+ query: { _id: RULE_ID.toString() },
827
+ props: { isRoot: false, tenantId: PROJECT_ID },
828
+ } as unknown as DeleteBy<StatusPageMonitorRule>);
829
+
830
+ expect(removeResourcesSpy).not.toHaveBeenCalled();
831
+ expect(resourceDeleteSpy).not.toHaveBeenCalled();
832
+ });
833
+
834
+ it("reads the doomed rules through the caller's own props, never as root", async () => {
835
+ jest.spyOn(StatusPageResourceService, "findBy").mockResolvedValue([]);
836
+
837
+ await callHook(StatusPageMonitorRuleService, "onBeforeDelete", {
838
+ query: { _id: RULE_ID.toString() },
839
+ props: { isRoot: false, tenantId: PROJECT_ID },
840
+ } as unknown as DeleteBy<StatusPageMonitorRule>);
841
+
842
+ const call: { props: { isRoot?: boolean; tenantId?: ObjectID } } =
843
+ ruleFindBySpy.mock.calls[0]![0] as {
844
+ props: { isRoot?: boolean; tenantId?: ObjectID };
845
+ };
846
+
847
+ expect(call.props.isRoot).toBe(false);
848
+ expect(call.props.tenantId).toEqual(PROJECT_ID);
849
+ });
850
+
851
+ /*
852
+ * Deleting a rule and disabling one are the same intent, so they must leave
853
+ * the same page. Disabling routes through syncResourcesForRule, which offers
854
+ * each released monitor to the other rules; deleting has to do the same, or
855
+ * a monitor a sibling rule still claims silently drops off a public page.
856
+ *
857
+ * The monitor ids have to be captured BEFORE the delete: the foreign key is
858
+ * ON DELETE CASCADE, so by onDeleteSuccess the resources naming them are
859
+ * already gone.
860
+ */
861
+ it("notes down the monitors a doomed rule publishes, before the cascade takes them", async () => {
862
+ jest.spyOn(StatusPageResourceService, "findBy").mockResolvedValue([
863
+ {
864
+ monitorId: MONITOR_ID,
865
+ } as unknown as StatusPageResource,
866
+ ]);
867
+
868
+ const onDelete: OnDelete<StatusPageMonitorRule> = (await callHook(
869
+ StatusPageMonitorRuleService,
870
+ "onBeforeDelete",
871
+ {
872
+ query: { _id: RULE_ID.toString() },
873
+ props: { isRoot: false, tenantId: PROJECT_ID },
874
+ } as unknown as DeleteBy<StatusPageMonitorRule>,
875
+ )) as OnDelete<StatusPageMonitorRule>;
876
+
877
+ expect(
878
+ (
879
+ onDelete.carryForward as {
880
+ monitorIdsByRuleId: Record<string, Array<string>>;
881
+ }
882
+ ).monitorIdsByRuleId[RULE_ID.toString()],
883
+ ).toEqual([MONITOR_ID.toString()]);
884
+ });
885
+
886
+ it("offers those monitors to the surviving rules once the rule is gone", async () => {
887
+ await callHook(
888
+ StatusPageMonitorRuleService,
889
+ "onDeleteSuccess",
890
+ {
891
+ deleteBy: {
892
+ props: { tenantId: PROJECT_ID },
893
+ } as unknown as DeleteBy<StatusPageMonitorRule>,
894
+ carryForward: {
895
+ monitorIdsByRuleId: {
896
+ [RULE_ID.toString()]: [MONITOR_ID.toString()],
897
+ },
898
+ },
899
+ },
900
+ [RULE_ID],
901
+ );
902
+
903
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledTimes(1);
904
+ const call: { monitorId: ObjectID } = syncRulesForMonitorSpy.mock
905
+ .calls[0]![0] as { monitorId: ObjectID };
906
+ expect(call.monitorId.toString()).toBe(MONITOR_ID.toString());
907
+ });
908
+
909
+ it("only re-homes monitors belonging to rules the delete actually removed", async () => {
910
+ await callHook(
911
+ StatusPageMonitorRuleService,
912
+ "onDeleteSuccess",
913
+ {
914
+ deleteBy: {
915
+ props: { tenantId: PROJECT_ID },
916
+ } as unknown as DeleteBy<StatusPageMonitorRule>,
917
+ carryForward: {
918
+ monitorIdsByRuleId: {
919
+ [RULE_ID.toString()]: [MONITOR_ID.toString()],
920
+ // A rule the permission check rejected; nothing was deleted for it.
921
+ [OTHER_RULE_ID.toString()]: [OTHER_MONITOR_ID.toString()],
922
+ },
923
+ },
924
+ },
925
+ [RULE_ID],
926
+ );
927
+
928
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledTimes(1);
929
+ });
930
+
931
+ it("still finishes the delete when the re-homing throws", async () => {
932
+ syncRulesForMonitorSpy.mockRejectedValue(new Error("db down"));
933
+
934
+ await expect(
935
+ callHook(
936
+ StatusPageMonitorRuleService,
937
+ "onDeleteSuccess",
938
+ {
939
+ deleteBy: {
940
+ props: { tenantId: PROJECT_ID },
941
+ } as unknown as DeleteBy<StatusPageMonitorRule>,
942
+ carryForward: {
943
+ monitorIdsByRuleId: {
944
+ [RULE_ID.toString()]: [MONITOR_ID.toString()],
945
+ },
946
+ },
947
+ },
948
+ [RULE_ID],
949
+ ),
950
+ ).resolves.toBeDefined();
951
+ });
952
+
953
+ it("tolerates a delete that carried nothing forward", async () => {
954
+ await expect(
955
+ callHook(
956
+ StatusPageMonitorRuleService,
957
+ "onDeleteSuccess",
958
+ {
959
+ deleteBy: {} as unknown as DeleteBy<StatusPageMonitorRule>,
960
+ carryForward: null,
961
+ },
962
+ [RULE_ID],
963
+ ),
964
+ ).resolves.toBeDefined();
965
+
966
+ expect(syncRulesForMonitorSpy).not.toHaveBeenCalled();
967
+ });
968
+
969
+ it("still deletes the rule when noting the monitors down fails", async () => {
970
+ ruleFindBySpy.mockRejectedValue(new Error("db down"));
971
+
972
+ const onDelete: OnDelete<StatusPageMonitorRule> = (await callHook(
973
+ StatusPageMonitorRuleService,
974
+ "onBeforeDelete",
975
+ {
976
+ query: { _id: RULE_ID.toString() },
977
+ props: { isRoot: false, tenantId: PROJECT_ID },
978
+ } as unknown as DeleteBy<StatusPageMonitorRule>,
979
+ )) as OnDelete<StatusPageMonitorRule>;
980
+
981
+ expect(
982
+ (
983
+ onDelete.carryForward as {
984
+ monitorIdsByRuleId: Record<string, Array<string>>;
985
+ }
986
+ ).monitorIdsByRuleId,
987
+ ).toEqual({});
988
+ });
989
+ });
990
+
991
+ /*
992
+ * A rule decides what shows up on a PUBLIC page, so the two ids it carries -
993
+ * the status page it is for and the group it drops monitors into - are the
994
+ * ones worth checking. Neither is validated by the framework.
995
+ */
996
+ describe("StatusPageMonitorRuleService - keeping a rule's references in scope", () => {
997
+ let validatorSpy: jest.SpyInstance;
998
+ let groupFindOneByIdSpy: jest.SpyInstance;
999
+
1000
+ beforeEach(() => {
1001
+ validatorSpy = jest
1002
+ .spyOn(
1003
+ ProjectScopedReferenceValidator,
1004
+ "validateReferencesBelongToProject",
1005
+ )
1006
+ .mockResolvedValue(undefined);
1007
+ groupFindOneByIdSpy = jest
1008
+ .spyOn(StatusPageGroupService, "findOneById")
1009
+ .mockResolvedValue(null);
1010
+ });
1011
+
1012
+ afterEach(() => {
1013
+ jest.restoreAllMocks();
1014
+ });
1015
+
1016
+ it("checks the status page and the group against the caller's project", async () => {
1017
+ await callHook(
1018
+ StatusPageMonitorRuleService,
1019
+ "onBeforeCreate",
1020
+ ruleCreate({
1021
+ statusPageId: STATUS_PAGE_ID,
1022
+ statusPageGroupId: GROUP_ID,
1023
+ name: "Production",
1024
+ monitorNamePattern: ".*",
1025
+ }),
1026
+ );
1027
+
1028
+ const call: {
1029
+ projectId: ObjectID;
1030
+ references: Array<{ modelName: string; id: unknown }>;
1031
+ } = validatorSpy.mock.calls[0]![0] as {
1032
+ projectId: ObjectID;
1033
+ references: Array<{ modelName: string; id: unknown }>;
1034
+ };
1035
+
1036
+ expect(call.projectId).toEqual(PROJECT_ID);
1037
+ expect(
1038
+ call.references.map((r: { modelName: string }) => {
1039
+ return r.modelName;
1040
+ }),
1041
+ ).toEqual(["Status Page", "Status Page Group"]);
1042
+ });
1043
+
1044
+ it("refuses a rule whose status page belongs to another project", async () => {
1045
+ validatorSpy.mockRejectedValue(
1046
+ new BadDataException("Status Page belongs to another project"),
1047
+ );
1048
+
1049
+ await expect(
1050
+ callHook(
1051
+ StatusPageMonitorRuleService,
1052
+ "onBeforeCreate",
1053
+ ruleCreate({
1054
+ statusPageId: STATUS_PAGE_ID,
1055
+ name: "Cross tenant",
1056
+ monitorNamePattern: ".*",
1057
+ }),
1058
+ ),
1059
+ ).rejects.toThrow(BadDataException);
1060
+ });
1061
+
1062
+ /*
1063
+ * A group on a different status page renders nowhere: the page only draws
1064
+ * its own groups. The rule would look configured and add nothing visible.
1065
+ */
1066
+ it("refuses a group that belongs to a different status page", async () => {
1067
+ groupFindOneByIdSpy.mockResolvedValue({
1068
+ id: GROUP_ID,
1069
+ _id: GROUP_ID.toString(),
1070
+ statusPageId: OTHER_STATUS_PAGE_ID,
1071
+ } as unknown as StatusPageGroup);
1072
+
1073
+ await expect(
1074
+ callHook(
1075
+ StatusPageMonitorRuleService,
1076
+ "onBeforeCreate",
1077
+ ruleCreate({
1078
+ statusPageId: STATUS_PAGE_ID,
1079
+ statusPageGroupId: GROUP_ID,
1080
+ name: "Wrong page",
1081
+ monitorNamePattern: ".*",
1082
+ }),
1083
+ ),
1084
+ ).rejects.toThrow(BadDataException);
1085
+ });
1086
+
1087
+ it("accepts a group that belongs to the rule's own status page", async () => {
1088
+ groupFindOneByIdSpy.mockResolvedValue({
1089
+ id: GROUP_ID,
1090
+ _id: GROUP_ID.toString(),
1091
+ statusPageId: STATUS_PAGE_ID,
1092
+ } as unknown as StatusPageGroup);
1093
+
1094
+ await expect(
1095
+ callHook(
1096
+ StatusPageMonitorRuleService,
1097
+ "onBeforeCreate",
1098
+ ruleCreate({
1099
+ statusPageId: STATUS_PAGE_ID,
1100
+ statusPageGroupId: GROUP_ID,
1101
+ name: "Right page",
1102
+ monitorNamePattern: ".*",
1103
+ }),
1104
+ ),
1105
+ ).resolves.toBeDefined();
1106
+ });
1107
+
1108
+ it("leaves a group id that matches no row to the foreign key to reject", async () => {
1109
+ groupFindOneByIdSpy.mockResolvedValue(null);
1110
+
1111
+ await expect(
1112
+ callHook(
1113
+ StatusPageMonitorRuleService,
1114
+ "onBeforeCreate",
1115
+ ruleCreate({
1116
+ statusPageId: STATUS_PAGE_ID,
1117
+ statusPageGroupId: GROUP_ID,
1118
+ name: "Dangling",
1119
+ monitorNamePattern: ".*",
1120
+ }),
1121
+ ),
1122
+ ).resolves.toBeDefined();
1123
+ });
1124
+
1125
+ it("does not look up a group when the rule names none", async () => {
1126
+ await callHook(
1127
+ StatusPageMonitorRuleService,
1128
+ "onBeforeCreate",
1129
+ ruleCreate({
1130
+ statusPageId: STATUS_PAGE_ID,
1131
+ name: "Ungrouped",
1132
+ monitorNamePattern: ".*",
1133
+ }),
1134
+ );
1135
+
1136
+ expect(groupFindOneByIdSpy).not.toHaveBeenCalled();
1137
+ });
1138
+
1139
+ /*
1140
+ * Re-pointing a saved rule at another group is an ordinary edit, so the same
1141
+ * checks apply on the way through.
1142
+ */
1143
+ it("checks a re-pointed group on update, not only on create", async () => {
1144
+ jest
1145
+ .spyOn(StatusPageMonitorRuleService, "findBy")
1146
+ .mockResolvedValue([fakeRuleRow(RULE_ID)]);
1147
+ groupFindOneByIdSpy.mockResolvedValue({
1148
+ id: GROUP_ID,
1149
+ _id: GROUP_ID.toString(),
1150
+ statusPageId: OTHER_STATUS_PAGE_ID,
1151
+ } as unknown as StatusPageGroup);
1152
+
1153
+ await expect(
1154
+ callHook(StatusPageMonitorRuleService, "onBeforeUpdate", {
1155
+ query: { _id: RULE_ID.toString() },
1156
+ data: { statusPageGroupId: GROUP_ID },
1157
+ props: { isRoot: false, tenantId: PROJECT_ID },
1158
+ }),
1159
+ ).rejects.toThrow(BadDataException);
1160
+ });
1161
+
1162
+ it("does not re-check the group on an edit that leaves it alone", async () => {
1163
+ await callHook(StatusPageMonitorRuleService, "onBeforeUpdate", {
1164
+ query: { _id: RULE_ID.toString() },
1165
+ data: { isEnabled: false },
1166
+ props: { isRoot: false, tenantId: PROJECT_ID },
1167
+ });
1168
+
1169
+ expect(groupFindOneByIdSpy).not.toHaveBeenCalled();
1170
+ expect(validatorSpy).not.toHaveBeenCalled();
1171
+ });
1172
+
1173
+ /*
1174
+ * onBeforeUpdate runs before DatabaseService applies the update query's
1175
+ * permission check, so the query it sees is the caller's raw one. Pinning
1176
+ * the tenant onto the validation read is what stops it becoming an oracle
1177
+ * for another project's rules.
1178
+ */
1179
+ it("pins the caller's project onto the validation read", async () => {
1180
+ const findBySpy: jest.SpyInstance = jest
1181
+ .spyOn(StatusPageMonitorRuleService, "findBy")
1182
+ .mockResolvedValue([fakeRuleRow(RULE_ID)]);
1183
+
1184
+ await callHook(StatusPageMonitorRuleService, "onBeforeUpdate", {
1185
+ query: { _id: RULE_ID.toString() },
1186
+ data: { monitorNamePattern: "^api" },
1187
+ props: { isRoot: false, tenantId: PROJECT_ID },
1188
+ });
1189
+
1190
+ const call: { query: { projectId?: ObjectID } } = findBySpy.mock
1191
+ .calls[0]![0] as { query: { projectId?: ObjectID } };
1192
+
1193
+ expect(call.query.projectId).toEqual(PROJECT_ID);
1194
+ });
1195
+
1196
+ it("leaves the query alone for a genuinely root caller, which carries no tenant", async () => {
1197
+ const findBySpy: jest.SpyInstance = jest
1198
+ .spyOn(StatusPageMonitorRuleService, "findBy")
1199
+ .mockResolvedValue([fakeRuleRow(RULE_ID)]);
1200
+
1201
+ await callHook(StatusPageMonitorRuleService, "onBeforeUpdate", {
1202
+ query: { _id: RULE_ID.toString() },
1203
+ data: { monitorNamePattern: "^api" },
1204
+ props: { isRoot: true },
1205
+ });
1206
+
1207
+ const call: { query: { projectId?: ObjectID } } = findBySpy.mock
1208
+ .calls[0]![0] as { query: { projectId?: ObjectID } };
1209
+
1210
+ expect(call.query.projectId).toBeUndefined();
1211
+ });
1212
+ });