@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,386 @@
1
+ import CephCluster from "../../../Models/DatabaseModels/CephCluster";
2
+ import DockerHost from "../../../Models/DatabaseModels/DockerHost";
3
+ import GlobalOIDCProject from "../../../Models/DatabaseModels/GlobalOidcProject";
4
+ import GlobalSSOProject from "../../../Models/DatabaseModels/GlobalSsoProject";
5
+ import IoTFleet from "../../../Models/DatabaseModels/IoTFleet";
6
+ import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
7
+ import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
8
+ import NetworkInterface from "../../../Models/DatabaseModels/NetworkInterface";
9
+ import ProxmoxCluster from "../../../Models/DatabaseModels/ProxmoxCluster";
10
+ import { RestoreDroppedUniqueIndexes1786200000000 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes";
11
+ import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
12
+ import { QueryRunner, getMetadataArgsStorage } from "typeorm";
13
+ import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
14
+ import { describe, expect, test } from "@jest/globals";
15
+
16
+ /*
17
+ * Nine UNIQUE indexes were deleted from production by autogenerated migrations
18
+ * and never restored. They were found by auditing every hand-named index ever
19
+ * created in a migration up() against a database with all registered
20
+ * migrations applied: 308 exist, 302 are absent, and ten of the absent ones
21
+ * carried a UNIQUE/partial/expression definition TypeORM cannot express and so
22
+ * never re-created. (The tenth, the Service LOWER(name) index, is covered by
23
+ * RestoreServiceLowerNameIndexMigration.test.ts.)
24
+ *
25
+ * The cause is structural, so the fix has to be structural too:
26
+ * RdbmsSchemaBuilder.dropOldIndices matches database indexes to entity metadata
27
+ * BY NAME and drops every one it cannot find. An index that lives only in a
28
+ * migration is invisible to entity metadata and gets dropped again on the next
29
+ * `migration:generate`.
30
+ *
31
+ * Three things must therefore all hold, and a regression in ANY one puts the
32
+ * index back on death row:
33
+ *
34
+ * 1. Each entity declares its index BY NAME with `synchronize: false`.
35
+ * 2. The migration creates all nine, partial on "deletedAt" IS NULL, and
36
+ * repairs duplicates first so it cannot fail on real data.
37
+ * 3. The migration is registered — an unregistered migration never runs.
38
+ *
39
+ * Pure metadata/mocks — no Postgres connection anywhere.
40
+ */
41
+
42
+ type ModelClass = { new (): unknown };
43
+
44
+ // [entity, index name, table name]
45
+ const PINNED_INDEXES: Array<[ModelClass, string, string]> = [
46
+ [CephCluster, "IDX_ceph_cluster_slug", "CephCluster"],
47
+ [DockerHost, "IDX_docker_host_slug", "DockerHost"],
48
+ [IoTFleet, "IDX_iot_fleet_slug", "IoTFleet"],
49
+ [KubernetesCluster, "IDX_kubernetes_cluster_slug", "KubernetesCluster"],
50
+ [NetworkDevice, "IDX_network_device_slug", "NetworkDevice"],
51
+ [ProxmoxCluster, "IDX_proxmox_cluster_slug", "ProxmoxCluster"],
52
+ [
53
+ GlobalOIDCProject,
54
+ "IDX_GlobalOIDCProject_globalOidcId_projectId",
55
+ "GlobalOIDCProject",
56
+ ],
57
+ [
58
+ GlobalSSOProject,
59
+ "IDX_GlobalSSOProject_globalSsoId_projectId",
60
+ "GlobalSSOProject",
61
+ ],
62
+ [
63
+ NetworkInterface,
64
+ "IDX_network_interface_device_ifindex",
65
+ "NetworkInterface",
66
+ ],
67
+ ];
68
+
69
+ type CapturedQueriesFunction = (
70
+ run: (queryRunner: QueryRunner) => Promise<void>,
71
+ ) => Promise<Array<string>>;
72
+
73
+ const capturedQueries: CapturedQueriesFunction = async (
74
+ run: (queryRunner: QueryRunner) => Promise<void>,
75
+ ): Promise<Array<string>> => {
76
+ const queries: Array<string> = [];
77
+ const queryRunner: QueryRunner = {
78
+ query: async (sql: string): Promise<void> => {
79
+ queries.push(sql);
80
+ },
81
+ } as unknown as QueryRunner;
82
+
83
+ await run(queryRunner);
84
+ return queries;
85
+ };
86
+
87
+ type UpQueriesFunction = () => Promise<Array<string>>;
88
+
89
+ const upQueries: UpQueriesFunction = (): Promise<Array<string>> => {
90
+ return capturedQueries(async (qr: QueryRunner): Promise<void> => {
91
+ await new RestoreDroppedUniqueIndexes1786200000000().up(qr);
92
+ });
93
+ };
94
+
95
+ describe("restored UNIQUE indexes", () => {
96
+ describe("1. the entity declarations that keep them alive", () => {
97
+ test.each(PINNED_INDEXES)(
98
+ "%p declares %s by name",
99
+ (model: ModelClass, indexName: string): void => {
100
+ const found: IndexMetadataArgs | undefined =
101
+ getMetadataArgsStorage().indices.find(
102
+ (index: IndexMetadataArgs): boolean => {
103
+ return index.target === model && index.name === indexName;
104
+ },
105
+ );
106
+
107
+ expect(found).toBeDefined();
108
+ },
109
+ );
110
+
111
+ test.each(PINNED_INDEXES)(
112
+ "%p declares %s as UNIQUE and partial",
113
+ (model: ModelClass, indexName: string): void => {
114
+ /*
115
+ * THE assertion. These are declared NATIVELY, not pinned with
116
+ * synchronize:false, so the schema builder owns them and the drift
117
+ * check actively polices them. For that to work the declaration has to
118
+ * match the database: same name, same uniqueness, same columns.
119
+ * dropOldIndices compares exactly those, and drops on any mismatch.
120
+ */
121
+ const found: IndexMetadataArgs | undefined =
122
+ getMetadataArgsStorage().indices.find(
123
+ (index: IndexMetadataArgs): boolean => {
124
+ return index.target === model && index.name === indexName;
125
+ },
126
+ );
127
+
128
+ expect(found?.unique).toBe(true);
129
+ expect(found?.where).toBe('"deletedAt" IS NULL');
130
+ },
131
+ );
132
+
133
+ test.each(PINNED_INDEXES)(
134
+ "%p leaves %s under TypeORM's ownership",
135
+ (model: ModelClass, indexName: string): void => {
136
+ /*
137
+ * The opposite of the Service LOWER(name) index, which MUST opt out
138
+ * because @Index() cannot express an expression index. These nine can
139
+ * all be expressed, so opting them out would only hide them from the
140
+ * drift check for no benefit.
141
+ */
142
+ const found: IndexMetadataArgs | undefined =
143
+ getMetadataArgsStorage().indices.find(
144
+ (index: IndexMetadataArgs): boolean => {
145
+ return index.target === model && index.name === indexName;
146
+ },
147
+ );
148
+
149
+ expect(found?.synchronize).not.toBe(false);
150
+ },
151
+ );
152
+ });
153
+
154
+ describe("2. the migration's SQL contract", () => {
155
+ test("creates all nine indexes and no others", async () => {
156
+ const creates: Array<string> = (await upQueries()).filter(
157
+ (sql: string): boolean => {
158
+ return sql.includes("CREATE UNIQUE INDEX");
159
+ },
160
+ );
161
+
162
+ expect(creates).toHaveLength(PINNED_INDEXES.length);
163
+
164
+ for (const [, indexName] of PINNED_INDEXES) {
165
+ expect(
166
+ creates.some((sql: string): boolean => {
167
+ return sql.includes(`"${indexName}"`);
168
+ }),
169
+ ).toBe(true);
170
+ }
171
+ });
172
+
173
+ test("every index is UNIQUE — the lost guarantee is the point", async () => {
174
+ const creates: Array<string> = (await upQueries()).filter(
175
+ (sql: string): boolean => {
176
+ return sql.includes("CREATE UNIQUE INDEX");
177
+ },
178
+ );
179
+
180
+ for (const sql of creates) {
181
+ expect(sql).toContain("CREATE UNIQUE INDEX");
182
+ }
183
+ });
184
+
185
+ test("every index is partial on deletedAt IS NULL", async () => {
186
+ /*
187
+ * Matches how the app reads (DatabaseService adds deletedAt IS NULL to
188
+ * every non-withDeleted query) and stops a soft-deleted row holding a
189
+ * slug hostage forever.
190
+ */
191
+ const creates: Array<string> = (await upQueries()).filter(
192
+ (sql: string): boolean => {
193
+ return sql.includes("CREATE UNIQUE INDEX");
194
+ },
195
+ );
196
+
197
+ for (const sql of creates) {
198
+ expect(sql).toContain('WHERE "deletedAt" IS NULL');
199
+ }
200
+ });
201
+
202
+ test("every index is idempotent", async () => {
203
+ /*
204
+ * An operator may build these by hand ahead of the deploy. IF NOT EXISTS
205
+ * makes that a no-op rather than a failed migration.
206
+ */
207
+ const creates: Array<string> = (await upQueries()).filter(
208
+ (sql: string): boolean => {
209
+ return sql.includes("CREATE UNIQUE INDEX");
210
+ },
211
+ );
212
+
213
+ for (const sql of creates) {
214
+ expect(sql).toContain("IF NOT EXISTS");
215
+ }
216
+ });
217
+
218
+ test("no index is created CONCURRENTLY", async () => {
219
+ /*
220
+ * migrationsTransactionMode defaults to "all", and CREATE INDEX
221
+ * CONCURRENTLY cannot run inside a transaction block — it would abort
222
+ * the entire migration run on boot.
223
+ */
224
+ for (const sql of await upQueries()) {
225
+ expect(sql).not.toContain("CONCURRENTLY");
226
+ }
227
+ });
228
+
229
+ test("every index is repaired BEFORE it is created", async () => {
230
+ /*
231
+ * A UNIQUE index cannot be built over existing duplicates. If a CREATE
232
+ * ever preceded its repair, the migration would fail on real data and
233
+ * block the deploy — which is the failure mode this ordering prevents.
234
+ */
235
+ const queries: Array<string> = await upQueries();
236
+
237
+ for (const [, indexName, table] of PINNED_INDEXES) {
238
+ const createAt: number = queries.findIndex((sql: string): boolean => {
239
+ return sql.includes(`"${indexName}"`);
240
+ });
241
+ const repairAt: number = queries.findIndex((sql: string): boolean => {
242
+ return sql.includes(`UPDATE "${table}"`);
243
+ });
244
+
245
+ expect(repairAt).toBeGreaterThan(-1);
246
+ expect(createAt).toBeGreaterThan(repairAt);
247
+ }
248
+ });
249
+
250
+ test("repairs are deterministic — oldest row always wins", async () => {
251
+ const repairs: Array<string> = (await upQueries()).filter(
252
+ (sql: string): boolean => {
253
+ return sql.includes("ROW_NUMBER()");
254
+ },
255
+ );
256
+
257
+ expect(repairs).toHaveLength(PINNED_INDEXES.length);
258
+
259
+ for (const sql of repairs) {
260
+ expect(sql).toContain('ORDER BY "createdAt" ASC, _id ASC');
261
+ // Only the losers are touched.
262
+ expect(sql).toContain("r.rn > 1");
263
+ }
264
+ });
265
+
266
+ test("slug repair RENAMES and never deletes", async () => {
267
+ /*
268
+ * A slug collision must not cost the user a row. The losing row keeps
269
+ * all its data and gets a discriminator appended.
270
+ */
271
+ const slugRepairs: Array<string> = (await upQueries()).filter(
272
+ (sql: string): boolean => {
273
+ return sql.includes('SET "slug"');
274
+ },
275
+ );
276
+
277
+ expect(slugRepairs).toHaveLength(6);
278
+
279
+ for (const sql of slugRepairs) {
280
+ expect(sql).toContain("LEFT(t._id::text, 8)");
281
+ expect(sql).not.toContain("DELETE");
282
+ }
283
+ });
284
+
285
+ test("duplicate mapping rows are SOFT-deleted, never hard-deleted", async () => {
286
+ /*
287
+ * Soft-deleting takes the row out of a partial index without destroying
288
+ * it and without touching any foreign key, so no cascade can fire.
289
+ */
290
+ const softDeletes: Array<string> = (await upQueries()).filter(
291
+ (sql: string): boolean => {
292
+ return sql.includes('SET "deletedAt" = CURRENT_TIMESTAMP');
293
+ },
294
+ );
295
+
296
+ expect(softDeletes).toHaveLength(3);
297
+ });
298
+
299
+ test("the migration never hard-deletes anything", async () => {
300
+ for (const sql of await upQueries()) {
301
+ expect(sql).not.toContain("DELETE FROM");
302
+ }
303
+ });
304
+
305
+ test("repairs only consider live rows", async () => {
306
+ /*
307
+ * The index is partial, so a soft-deleted duplicate is already outside
308
+ * it and must not be "repaired".
309
+ */
310
+ const repairs: Array<string> = (await upQueries()).filter(
311
+ (sql: string): boolean => {
312
+ return sql.includes("ROW_NUMBER()");
313
+ },
314
+ );
315
+
316
+ for (const sql of repairs) {
317
+ expect(sql).toContain('WHERE "deletedAt" IS NULL');
318
+ }
319
+ });
320
+
321
+ test("down() drops exactly the nine indexes up() created", async () => {
322
+ const queries: Array<string> = await capturedQueries(
323
+ async (qr: QueryRunner): Promise<void> => {
324
+ await new RestoreDroppedUniqueIndexes1786200000000().down(qr);
325
+ },
326
+ );
327
+
328
+ expect(queries).toHaveLength(PINNED_INDEXES.length);
329
+
330
+ for (const [, indexName] of PINNED_INDEXES) {
331
+ expect(
332
+ queries.some((sql: string): boolean => {
333
+ return sql.includes("DROP INDEX") && sql.includes(indexName);
334
+ }),
335
+ ).toBe(true);
336
+ }
337
+ });
338
+
339
+ test("down() does not attempt to un-repair data", async () => {
340
+ /*
341
+ * A renamed slug and a soft-deleted duplicate are indistinguishable
342
+ * afterwards from ones the application produced. Guessing would corrupt
343
+ * live data, so down() only reverses the schema.
344
+ */
345
+ const queries: Array<string> = await capturedQueries(
346
+ async (qr: QueryRunner): Promise<void> => {
347
+ await new RestoreDroppedUniqueIndexes1786200000000().down(qr);
348
+ },
349
+ );
350
+
351
+ for (const sql of queries) {
352
+ expect(sql).not.toContain("UPDATE");
353
+ }
354
+ });
355
+ });
356
+
357
+ describe("3. registration", () => {
358
+ test("the migration is registered so it actually runs on boot", () => {
359
+ expect(SchemaMigrations).toContain(
360
+ RestoreDroppedUniqueIndexes1786200000000,
361
+ );
362
+ });
363
+
364
+ test("it runs after every migration that dropped one of these indexes", () => {
365
+ /*
366
+ * Restoring before the drop would leave the database with no index
367
+ * again. 1783937343400 is the migration that removed the NetworkDevice
368
+ * and NetworkInterface indexes.
369
+ */
370
+ const names: Array<string> = SchemaMigrations.map(
371
+ (migration: unknown): string => {
372
+ return (migration as { name: string }).name;
373
+ },
374
+ );
375
+
376
+ const restoreAt: number = names.indexOf(
377
+ "RestoreDroppedUniqueIndexes1786200000000",
378
+ );
379
+ const dropperAt: number = names.indexOf("MigrationName1783937343400");
380
+
381
+ expect(restoreAt).toBeGreaterThan(-1);
382
+ expect(dropperAt).toBeGreaterThan(-1);
383
+ expect(restoreAt).toBeGreaterThan(dropperAt);
384
+ });
385
+ });
386
+ });
@@ -0,0 +1,240 @@
1
+ import Service from "../../../Models/DatabaseModels/Service";
2
+ import { RestoreServiceLowerNameIndex1786100000000 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex";
3
+ import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
4
+ import { QueryRunner, getMetadataArgsStorage } from "typeorm";
5
+ import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
6
+ import { describe, expect, test } from "@jest/globals";
7
+
8
+ const INDEX_NAME: string = "IDX_SERVICE_PROJECT_LOWER_NAME";
9
+
10
+ /*
11
+ * The telemetry ingest path resolves `service.name` case-insensitively, so the
12
+ * predicate is `LOWER("Service"."name") = $2`. Only an EXPRESSION index can
13
+ * serve that; an index over the raw `name` column cannot, and the lookup
14
+ * degrades to O(services in project) heap fetches on the hottest read in the
15
+ * product. Production ran at 100% CPU on it.
16
+ *
17
+ * The index this migration creates has been deleted from production once
18
+ * already. It was created as IDX_SERVICE_PROJECT_NAME_UNIQUE by migration
19
+ * 1774559064921 and dropped by autogenerated migration 1775735059360, whose
20
+ * down() "restores" it WITHOUT the LOWER() — so even the round-trip loses it.
21
+ * The cause is structural: TypeORM's schema builder matches database indexes to
22
+ * entity metadata by NAME and drops every one it cannot find, and an expression
23
+ * index that exists only in a migration is invisible to entity metadata.
24
+ *
25
+ * Three things must therefore all hold, and a regression in ANY one silently
26
+ * reintroduces the incident:
27
+ *
28
+ * 1. The entity declares the index BY NAME with `synchronize: false`. This is
29
+ * what stops the next `migration:generate` emitting a DROP for it. Without
30
+ * it the migration below gets undone and nobody notices until CPU spikes.
31
+ * 2. The migration's SQL contract: up() creates an expression index on
32
+ * LOWER("name"), partial on "deletedAt" IS NULL, and down() drops it.
33
+ * 3. The migration is registered in SchemaMigrations/Index.ts. An
34
+ * unregistered migration is dead code that never runs on boot.
35
+ *
36
+ * Pure metadata/mocks — no Postgres connection anywhere.
37
+ */
38
+ describe("Service LOWER(name) expression index", () => {
39
+ describe("1. the entity declaration that keeps it alive", () => {
40
+ type FindIndexFunction = () => IndexMetadataArgs | undefined;
41
+
42
+ const findIndex: FindIndexFunction = (): IndexMetadataArgs | undefined => {
43
+ return getMetadataArgsStorage().indices.find(
44
+ (index: IndexMetadataArgs): boolean => {
45
+ return index.target === Service && index.name === INDEX_NAME;
46
+ },
47
+ );
48
+ };
49
+
50
+ test("Service declares the index by name", () => {
51
+ /*
52
+ * Name matching is the whole mechanism — RdbmsSchemaBuilder looks up
53
+ * `metadata.indices.find((i) => i.name === tableIndex.name)` and drops
54
+ * anything unmatched. The name here must equal the one in the migration.
55
+ */
56
+ expect(findIndex()).toBeDefined();
57
+ });
58
+
59
+ test("the declaration opts OUT of schema synchronization", () => {
60
+ /*
61
+ * THE assertion. `synchronize: false` makes the builder skip the index in
62
+ * dropOldIndices AND in createNewIndices. Flip this to true (or drop the
63
+ * option) and TypeORM owns the index again — at which point it cannot
64
+ * express LOWER() and will replace it with a raw-column index, which is
65
+ * exactly the regression that caused the incident.
66
+ */
67
+ expect(findIndex()?.synchronize).toBe(false);
68
+ });
69
+
70
+ test("the pre-existing raw-name unique index is left untouched", () => {
71
+ /*
72
+ * The expression index is additive. The raw (projectId, name) unique
73
+ * index still enforces the create-race guard that the service layer's
74
+ * catch-and-refetch path depends on.
75
+ */
76
+ const rawUnique: IndexMetadataArgs | undefined = getMetadataArgsStorage()
77
+ .indices.filter((index: IndexMetadataArgs): boolean => {
78
+ return index.target === Service;
79
+ })
80
+ .find((index: IndexMetadataArgs): boolean => {
81
+ return index.unique === true && index.name === undefined;
82
+ });
83
+
84
+ expect(rawUnique).toBeDefined();
85
+ });
86
+ });
87
+
88
+ describe("2. the migration's SQL contract", () => {
89
+ type CapturedQueriesFunction = (
90
+ run: (queryRunner: QueryRunner) => Promise<void>,
91
+ ) => Promise<Array<string>>;
92
+
93
+ const capturedQueries: CapturedQueriesFunction = async (
94
+ run: (queryRunner: QueryRunner) => Promise<void>,
95
+ ): Promise<Array<string>> => {
96
+ const queries: Array<string> = [];
97
+ const queryRunner: QueryRunner = {
98
+ query: async (sql: string): Promise<void> => {
99
+ queries.push(sql);
100
+ },
101
+ } as unknown as QueryRunner;
102
+
103
+ await run(queryRunner);
104
+ return queries;
105
+ };
106
+
107
+ test("up() indexes the LOWER() EXPRESSION, not the raw column", () => {
108
+ /*
109
+ * An index on ("projectId", "name") is not a substitute and would leave
110
+ * the predicate non-sargable. This is the difference between O(log n) and
111
+ * O(services in project) per ingest resolution.
112
+ */
113
+ return capturedQueries(async (qr: QueryRunner): Promise<void> => {
114
+ await new RestoreServiceLowerNameIndex1786100000000().up(qr);
115
+ }).then((queries: Array<string>): void => {
116
+ expect(queries).toHaveLength(1);
117
+ expect(queries[0]).toContain('LOWER("name")');
118
+ expect(queries[0]).toContain('ON "Service"');
119
+ expect(queries[0]).toContain(INDEX_NAME);
120
+ });
121
+ });
122
+
123
+ test("up() matches the query's soft-delete predicate", async () => {
124
+ /*
125
+ * The lookup always carries `deletedAt IS NULL` (DatabaseService adds it
126
+ * for every non-withDeleted read). A partial index on the same predicate
127
+ * stays usable and stays small.
128
+ */
129
+ const queries: Array<string> = await capturedQueries(
130
+ async (qr: QueryRunner): Promise<void> => {
131
+ await new RestoreServiceLowerNameIndex1786100000000().up(qr);
132
+ },
133
+ );
134
+
135
+ expect(queries[0]).toContain('WHERE "deletedAt" IS NULL');
136
+ });
137
+
138
+ test("up() carries createdAt so the ORDER BY needs no Sort node", async () => {
139
+ const queries: Array<string> = await capturedQueries(
140
+ async (qr: QueryRunner): Promise<void> => {
141
+ await new RestoreServiceLowerNameIndex1786100000000().up(qr);
142
+ },
143
+ );
144
+
145
+ expect(queries[0]).toContain('"createdAt"');
146
+ });
147
+
148
+ test("up() is idempotent so a hand-built index does not break the deploy", async () => {
149
+ /*
150
+ * The incident mitigation is to build this CONCURRENTLY on the live
151
+ * database under the same name before deploying. IF NOT EXISTS makes the
152
+ * migration a no-op in that case instead of a failed deploy.
153
+ */
154
+ const queries: Array<string> = await capturedQueries(
155
+ async (qr: QueryRunner): Promise<void> => {
156
+ await new RestoreServiceLowerNameIndex1786100000000().up(qr);
157
+ },
158
+ );
159
+
160
+ expect(queries[0]).toContain("IF NOT EXISTS");
161
+ });
162
+
163
+ test("up() is NOT concurrent — migrations run inside one transaction", async () => {
164
+ /*
165
+ * TypeORM defaults migrationsTransactionMode to "all", and CREATE INDEX
166
+ * CONCURRENTLY cannot run inside a transaction block. Adding CONCURRENTLY
167
+ * here would abort the entire migration run on boot.
168
+ */
169
+ const queries: Array<string> = await capturedQueries(
170
+ async (qr: QueryRunner): Promise<void> => {
171
+ await new RestoreServiceLowerNameIndex1786100000000().up(qr);
172
+ },
173
+ );
174
+
175
+ expect(queries[0]).not.toContain("CONCURRENTLY");
176
+ });
177
+
178
+ test("up() does NOT recreate the index as UNIQUE", async () => {
179
+ /*
180
+ * The dropped original was unique. Recreating it that way fails on any
181
+ * project already holding two services whose names differ only by case —
182
+ * which the surviving raw-name unique index permits. Restoring
183
+ * case-insensitive uniqueness needs a dedupe pass and its own migration.
184
+ */
185
+ const queries: Array<string> = await capturedQueries(
186
+ async (qr: QueryRunner): Promise<void> => {
187
+ await new RestoreServiceLowerNameIndex1786100000000().up(qr);
188
+ },
189
+ );
190
+
191
+ expect(queries[0]).not.toContain("UNIQUE");
192
+ });
193
+
194
+ test("down() drops exactly what up() created", async () => {
195
+ const queries: Array<string> = await capturedQueries(
196
+ async (qr: QueryRunner): Promise<void> => {
197
+ await new RestoreServiceLowerNameIndex1786100000000().down(qr);
198
+ },
199
+ );
200
+
201
+ expect(queries).toHaveLength(1);
202
+ expect(queries[0]).toContain("DROP INDEX");
203
+ expect(queries[0]).toContain(INDEX_NAME);
204
+ });
205
+ });
206
+
207
+ describe("3. registration", () => {
208
+ test("the migration is registered so it actually runs on boot", () => {
209
+ /*
210
+ * CI enforces this separately, but an unregistered migration is dead
211
+ * code — the index would never be created in any environment.
212
+ */
213
+ expect(SchemaMigrations).toContain(
214
+ RestoreServiceLowerNameIndex1786100000000,
215
+ );
216
+ });
217
+
218
+ test("it is registered AFTER the migration that dropped the original", () => {
219
+ /*
220
+ * Ordering is the correctness condition: 1775735059360 drops
221
+ * IDX_SERVICE_PROJECT_NAME_UNIQUE. Running this restore before that drop
222
+ * would leave a database with no usable index again.
223
+ */
224
+ const names: Array<string> = SchemaMigrations.map(
225
+ (migration: unknown): string => {
226
+ return (migration as { name: string }).name;
227
+ },
228
+ );
229
+
230
+ const restoreAt: number = names.indexOf(
231
+ "RestoreServiceLowerNameIndex1786100000000",
232
+ );
233
+ const dropperAt: number = names.indexOf("MigrationName1775735059360");
234
+
235
+ expect(restoreAt).toBeGreaterThan(-1);
236
+ expect(dropperAt).toBeGreaterThan(-1);
237
+ expect(restoreAt).toBeGreaterThan(dropperAt);
238
+ });
239
+ });
240
+ });