@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,852 @@
1
+ import CommonAPI from "../../../Server/API/CommonAPI";
2
+ import UserAPI from "../../../Server/API/UserAPI";
3
+ import MasterAdminAuthorization from "../../../Server/Middleware/MasterAdminAuthorization";
4
+ import TeamMemberService from "../../../Server/Services/TeamMemberService";
5
+ import {
6
+ ExpressRequest,
7
+ ExpressResponse,
8
+ NextFunction,
9
+ } from "../../../Server/Utils/Express";
10
+ import Response from "../../../Server/Utils/Response";
11
+ import { mockRouter } from "./Helpers";
12
+ import Project from "../../../Models/DatabaseModels/Project";
13
+ import Team from "../../../Models/DatabaseModels/Team";
14
+ import TeamMember from "../../../Models/DatabaseModels/TeamMember";
15
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
16
+ import { DEFAULT_LIMIT } from "../../../Types/Database/LimitMax";
17
+ import Dictionary from "../../../Types/Dictionary";
18
+ import { JSONObject } from "../../../Types/JSON";
19
+ import ObjectID from "../../../Types/ObjectID";
20
+ import PositiveNumber from "../../../Types/PositiveNumber";
21
+ import UserType from "../../../Types/UserType";
22
+ import {
23
+ afterEach,
24
+ beforeAll,
25
+ beforeEach,
26
+ describe,
27
+ expect,
28
+ test,
29
+ } from "@jest/globals";
30
+
31
+ /*
32
+ * The two master-admin endpoints behind the Admin Dashboard's
33
+ * User > Projects page.
34
+ *
35
+ * POST /user/:userId/projects exists because no CRUD list answers "which
36
+ * projects is this person in?": /team-member/get-list returns MEMBERSHIPS, so a
37
+ * user on three teams of one project comes back as three rows and pages as
38
+ * three. The grouping, the page window applied to the GROUPED list, and the
39
+ * count being a number of projects are the whole point, and all three fail
40
+ * silently — the table just shows duplicate projects, or offers pages that
41
+ * render empty.
42
+ *
43
+ * POST /user/:userId/remove-from-project is the master-admin twin of
44
+ * /team-member/remove-user-from-project. That one takes its project from the
45
+ * request's tenant; a master admin sends no tenant, so this one is told the
46
+ * project. What must not change is the rest: ONE deleteBy over the whole set
47
+ * (so the Owners-team guard runs before anything is deleted) with the caller's
48
+ * own props rather than root.
49
+ *
50
+ * Both are guarded by the master-admin middleware, which is the only thing
51
+ * standing between "list one user's projects" and "read across every tenant on
52
+ * the instance". That the routes carry it is asserted directly, because
53
+ * nothing else in these tests would notice its absence.
54
+ */
55
+
56
+ jest.mock("../../../Server/Utils/Express", () => {
57
+ return {
58
+ getRouter: () => {
59
+ return mockRouter;
60
+ },
61
+ };
62
+ });
63
+
64
+ jest.mock("../../../Server/Utils/Response", () => {
65
+ return {
66
+ sendEntityArrayResponse: jest.fn(),
67
+ sendJsonArrayResponse: jest.fn(),
68
+ sendJsonObjectResponse: jest.fn(),
69
+ sendEmptySuccessResponse: jest.fn(),
70
+ sendEntityResponse: jest.fn(),
71
+ sendErrorResponse: jest.fn(),
72
+ sendFileResponse: jest.fn(),
73
+ sendFileByPath: jest.fn(),
74
+ setNoCacheHeaders: jest.fn(),
75
+ };
76
+ });
77
+
78
+ const PROJECTS_ROUTE: string = "/user/:userId/projects";
79
+ const REMOVE_ROUTE: string = "/user/:userId/remove-from-project";
80
+
81
+ const USER_ID: string = "00000000-0000-4000-8000-000000000001";
82
+ const PROJECT_ONE_ID: string = "00000000-0000-4000-8000-0000000000a1";
83
+ const PROJECT_TWO_ID: string = "00000000-0000-4000-8000-0000000000a2";
84
+
85
+ type MembershipSpec = {
86
+ id: string;
87
+ projectId: string;
88
+ projectName: string;
89
+ teamId?: string | undefined;
90
+ teamName?: string | undefined;
91
+ hasAcceptedInvitation?: boolean | undefined;
92
+ createdAt?: Date | undefined;
93
+ };
94
+
95
+ function buildMembership(spec: MembershipSpec): TeamMember {
96
+ const teamMember: TeamMember = new TeamMember();
97
+
98
+ teamMember._id = spec.id;
99
+ teamMember.projectId = new ObjectID(spec.projectId);
100
+ teamMember.hasAcceptedInvitation = spec.hasAcceptedInvitation ?? true;
101
+
102
+ const project: Project = new Project();
103
+ project._id = spec.projectId;
104
+ project.name = spec.projectName;
105
+ teamMember.project = project;
106
+
107
+ if (spec.teamId) {
108
+ teamMember.teamId = new ObjectID(spec.teamId);
109
+
110
+ const team: Team = new Team();
111
+ team._id = spec.teamId;
112
+ team.name = spec.teamName || spec.teamId;
113
+ teamMember.team = team;
114
+ }
115
+
116
+ if (spec.createdAt) {
117
+ teamMember.createdAt = spec.createdAt;
118
+ }
119
+
120
+ return teamMember;
121
+ }
122
+
123
+ type RouteCallResult = {
124
+ thrownToNext: unknown;
125
+ nextCallCount: number;
126
+ };
127
+
128
+ async function callRoute(data: {
129
+ route: string;
130
+ params?: Dictionary<string> | undefined;
131
+ query?: Dictionary<string> | undefined;
132
+ body?: Dictionary<unknown> | undefined;
133
+ }): Promise<RouteCallResult> {
134
+ const req: ExpressRequest = {
135
+ params: data.params || {},
136
+ query: data.query || {},
137
+ body: data.body || {},
138
+ headers: {},
139
+ } as unknown as ExpressRequest;
140
+
141
+ const res: ExpressResponse = {
142
+ send: jest.fn(),
143
+ json: jest.fn(),
144
+ status: jest.fn().mockReturnThis(),
145
+ } as unknown as ExpressResponse;
146
+
147
+ const next: jest.Mock = jest.fn();
148
+
149
+ await mockRouter
150
+ .match("POST", data.route)
151
+ .handlerFunction(req, res, next as unknown as NextFunction);
152
+
153
+ return {
154
+ thrownToNext: next.mock.calls[0] ? next.mock.calls[0][0] : undefined,
155
+ nextCallCount: next.mock.calls.length,
156
+ };
157
+ }
158
+
159
+ function sentRows(): Array<JSONObject> {
160
+ const call: Array<unknown> | undefined = (
161
+ Response.sendJsonArrayResponse as unknown as {
162
+ mock: { calls: Array<Array<unknown>> };
163
+ }
164
+ ).mock.calls[0];
165
+
166
+ return (call?.[2] as Array<JSONObject>) || [];
167
+ }
168
+
169
+ function sentCount(): number {
170
+ const call: Array<unknown> | undefined = (
171
+ Response.sendJsonArrayResponse as unknown as {
172
+ mock: { mock: never; calls: Array<Array<unknown>> };
173
+ }
174
+ ).mock.calls[0];
175
+
176
+ return (call?.[3] as PositiveNumber)?.toNumber();
177
+ }
178
+
179
+ function projectNamesSent(): Array<string> {
180
+ return sentRows().map((row: JSONObject) => {
181
+ return ((row["project"] as JSONObject)?.["name"] as string) || "";
182
+ });
183
+ }
184
+
185
+ describe("POST /user/:userId/projects", () => {
186
+ let findBySpy: jest.SpyInstance;
187
+
188
+ beforeAll(() => {
189
+ mockRouter.routes.length = 0;
190
+ new UserAPI();
191
+ });
192
+
193
+ beforeEach(() => {
194
+ jest.clearAllMocks();
195
+
196
+ findBySpy = jest.spyOn(TeamMemberService, "findBy").mockResolvedValue([]);
197
+ });
198
+
199
+ afterEach(() => {
200
+ jest.restoreAllMocks();
201
+ });
202
+
203
+ test("the route is registered", () => {
204
+ expect(() => {
205
+ return mockRouter.match("POST", PROJECTS_ROUTE);
206
+ }).not.toThrow();
207
+ });
208
+
209
+ test("only a master admin can reach it", () => {
210
+ expect(mockRouter.match("POST", PROJECTS_ROUTE).middleware).toBe(
211
+ MasterAdminAuthorization.isAuthorizedMasterAdminMiddleware,
212
+ );
213
+ });
214
+
215
+ test("reads only the memberships of the user in the path", async () => {
216
+ await callRoute({
217
+ route: PROJECTS_ROUTE,
218
+ params: { userId: USER_ID },
219
+ });
220
+
221
+ expect(findBySpy).toHaveBeenCalledTimes(1);
222
+
223
+ const findBy: { query: Dictionary<unknown> } = findBySpy.mock
224
+ .calls[0]![0] as { query: Dictionary<unknown> };
225
+
226
+ expect((findBy.query["userId"] as ObjectID).toString()).toBe(USER_ID);
227
+ // Nothing else narrows it — every project the user is in is in scope.
228
+ expect(Object.keys(findBy.query)).toEqual(["userId"]);
229
+ });
230
+
231
+ test("selects the project and team each membership belongs to", async () => {
232
+ /*
233
+ * A row is a project carrying its teams. Drop either relation from the
234
+ * select and the response still has the right SHAPE — just with a blank
235
+ * project name and no teams.
236
+ */
237
+ await callRoute({
238
+ route: PROJECTS_ROUTE,
239
+ params: { userId: USER_ID },
240
+ });
241
+
242
+ const select: Dictionary<unknown> = (
243
+ findBySpy.mock.calls[0]![0] as { select: Dictionary<unknown> }
244
+ ).select;
245
+
246
+ expect(select["project"]).toEqual(
247
+ expect.objectContaining({ _id: true, name: true }),
248
+ );
249
+ expect(select["team"]).toEqual(
250
+ expect.objectContaining({ _id: true, name: true }),
251
+ );
252
+ expect(select["hasAcceptedInvitation"]).toBe(true);
253
+ });
254
+
255
+ test("returns one row per project, not one per membership", async () => {
256
+ findBySpy.mockResolvedValue([
257
+ buildMembership({
258
+ id: "m1",
259
+ projectId: PROJECT_ONE_ID,
260
+ projectName: "Acme Production",
261
+ teamId: "team-owners",
262
+ teamName: "Owners",
263
+ }),
264
+ buildMembership({
265
+ id: "m2",
266
+ projectId: PROJECT_ONE_ID,
267
+ projectName: "Acme Production",
268
+ teamId: "team-eng",
269
+ teamName: "Engineering",
270
+ }),
271
+ buildMembership({
272
+ id: "m3",
273
+ projectId: PROJECT_TWO_ID,
274
+ projectName: "Acme Staging",
275
+ teamId: "team-eng-staging",
276
+ teamName: "Engineering",
277
+ }),
278
+ ]);
279
+
280
+ await callRoute({
281
+ route: PROJECTS_ROUTE,
282
+ params: { userId: USER_ID },
283
+ });
284
+
285
+ expect(projectNamesSent()).toEqual(["Acme Production", "Acme Staging"]);
286
+ });
287
+
288
+ test("counts projects, not memberships", async () => {
289
+ /*
290
+ * The pager divides this by the page size. A membership count would offer
291
+ * pages that render empty.
292
+ */
293
+ findBySpy.mockResolvedValue([
294
+ buildMembership({
295
+ id: "m1",
296
+ projectId: PROJECT_ONE_ID,
297
+ projectName: "Acme Production",
298
+ teamId: "t1",
299
+ }),
300
+ buildMembership({
301
+ id: "m2",
302
+ projectId: PROJECT_ONE_ID,
303
+ projectName: "Acme Production",
304
+ teamId: "t2",
305
+ }),
306
+ buildMembership({
307
+ id: "m3",
308
+ projectId: PROJECT_ONE_ID,
309
+ projectName: "Acme Production",
310
+ teamId: "t3",
311
+ }),
312
+ ]);
313
+
314
+ await callRoute({
315
+ route: PROJECTS_ROUTE,
316
+ params: { userId: USER_ID },
317
+ });
318
+
319
+ expect(sentCount()).toBe(1);
320
+ });
321
+
322
+ test("carries every team of the project on its row", async () => {
323
+ findBySpy.mockResolvedValue([
324
+ buildMembership({
325
+ id: "m1",
326
+ projectId: PROJECT_ONE_ID,
327
+ projectName: "Acme Production",
328
+ teamId: "team-owners",
329
+ teamName: "Owners",
330
+ }),
331
+ buildMembership({
332
+ id: "m2",
333
+ projectId: PROJECT_ONE_ID,
334
+ projectName: "Acme Production",
335
+ teamId: "team-eng",
336
+ teamName: "Engineering",
337
+ }),
338
+ ]);
339
+
340
+ await callRoute({
341
+ route: PROJECTS_ROUTE,
342
+ params: { userId: USER_ID },
343
+ });
344
+
345
+ const row: JSONObject = sentRows()[0]!;
346
+
347
+ expect(
348
+ (row["teams"] as Array<JSONObject>).map((team: JSONObject) => {
349
+ return team["name"];
350
+ }),
351
+ ).toEqual(["Engineering", "Owners"]);
352
+ expect(row["teamCount"]).toBe(2);
353
+ expect(row["teamMemberIds"]).toEqual(["m1", "m2"]);
354
+ });
355
+
356
+ test("reports the user the row belongs to", async () => {
357
+ findBySpy.mockResolvedValue([
358
+ buildMembership({
359
+ id: "m1",
360
+ projectId: PROJECT_ONE_ID,
361
+ projectName: "Acme Production",
362
+ teamId: "t1",
363
+ }),
364
+ ]);
365
+
366
+ await callRoute({
367
+ route: PROJECTS_ROUTE,
368
+ params: { userId: USER_ID },
369
+ });
370
+
371
+ expect(JSON.stringify(sentRows()[0]!["userId"])).toContain(USER_ID);
372
+ });
373
+
374
+ test("reports a user with one accepted team as a member, with the rest pending", async () => {
375
+ findBySpy.mockResolvedValue([
376
+ buildMembership({
377
+ id: "m1",
378
+ projectId: PROJECT_ONE_ID,
379
+ projectName: "Acme Production",
380
+ teamId: "t1",
381
+ hasAcceptedInvitation: true,
382
+ }),
383
+ buildMembership({
384
+ id: "m2",
385
+ projectId: PROJECT_ONE_ID,
386
+ projectName: "Acme Production",
387
+ teamId: "t2",
388
+ hasAcceptedInvitation: false,
389
+ }),
390
+ ]);
391
+
392
+ await callRoute({
393
+ route: PROJECTS_ROUTE,
394
+ params: { userId: USER_ID },
395
+ });
396
+
397
+ expect(sentRows()[0]!["hasAcceptedInvitation"]).toBe(true);
398
+ expect(sentRows()[0]!["pendingTeamCount"]).toBe(1);
399
+ });
400
+
401
+ test("sorts the projects by name", async () => {
402
+ findBySpy.mockResolvedValue([
403
+ buildMembership({
404
+ id: "m1",
405
+ projectId: PROJECT_TWO_ID,
406
+ projectName: "zebra corp",
407
+ teamId: "t1",
408
+ }),
409
+ buildMembership({
410
+ id: "m2",
411
+ projectId: PROJECT_ONE_ID,
412
+ projectName: "Acme Production",
413
+ teamId: "t2",
414
+ }),
415
+ ]);
416
+
417
+ await callRoute({
418
+ route: PROJECTS_ROUTE,
419
+ params: { userId: USER_ID },
420
+ });
421
+
422
+ expect(projectNamesSent()).toEqual(["Acme Production", "zebra corp"]);
423
+ });
424
+
425
+ test("returns an empty list for a user in no project", async () => {
426
+ await callRoute({
427
+ route: PROJECTS_ROUTE,
428
+ params: { userId: USER_ID },
429
+ });
430
+
431
+ expect(sentRows()).toEqual([]);
432
+ expect(sentCount()).toBe(0);
433
+ });
434
+ });
435
+
436
+ describe("POST /user/:userId/projects — paging", () => {
437
+ let findBySpy: jest.SpyInstance;
438
+
439
+ beforeAll(() => {
440
+ mockRouter.routes.length = 0;
441
+ new UserAPI();
442
+ });
443
+
444
+ beforeEach(() => {
445
+ jest.clearAllMocks();
446
+
447
+ /*
448
+ * Five projects, one membership each, deliberately out of alphabetical
449
+ * order so a page window applied before the sort would be visible.
450
+ */
451
+ findBySpy = jest.spyOn(TeamMemberService, "findBy").mockResolvedValue([
452
+ buildMembership({
453
+ id: "m5",
454
+ projectId: "00000000-0000-4000-8000-0000000000e5",
455
+ projectName: "Echo",
456
+ teamId: "t5",
457
+ }),
458
+ buildMembership({
459
+ id: "m3",
460
+ projectId: "00000000-0000-4000-8000-0000000000e3",
461
+ projectName: "Charlie",
462
+ teamId: "t3",
463
+ }),
464
+ buildMembership({
465
+ id: "m1",
466
+ projectId: "00000000-0000-4000-8000-0000000000e1",
467
+ projectName: "Alpha",
468
+ teamId: "t1",
469
+ }),
470
+ buildMembership({
471
+ id: "m4",
472
+ projectId: "00000000-0000-4000-8000-0000000000e4",
473
+ projectName: "Delta",
474
+ teamId: "t4",
475
+ }),
476
+ buildMembership({
477
+ id: "m2",
478
+ projectId: "00000000-0000-4000-8000-0000000000e2",
479
+ projectName: "Bravo",
480
+ teamId: "t2",
481
+ }),
482
+ ]);
483
+ });
484
+
485
+ afterEach(() => {
486
+ jest.restoreAllMocks();
487
+ });
488
+
489
+ test("returns the requested page of projects", async () => {
490
+ await callRoute({
491
+ route: PROJECTS_ROUTE,
492
+ params: { userId: USER_ID },
493
+ query: { skip: "2", limit: "2" },
494
+ });
495
+
496
+ expect(projectNamesSent()).toEqual(["Charlie", "Delta"]);
497
+ });
498
+
499
+ test("reports the total across every page, not the page size", async () => {
500
+ await callRoute({
501
+ route: PROJECTS_ROUTE,
502
+ params: { userId: USER_ID },
503
+ query: { skip: "0", limit: "2" },
504
+ });
505
+
506
+ expect(sentCount()).toBe(5);
507
+ });
508
+
509
+ test("returns an empty page past the end rather than wrapping", async () => {
510
+ await callRoute({
511
+ route: PROJECTS_ROUTE,
512
+ params: { userId: USER_ID },
513
+ query: { skip: "10", limit: "2" },
514
+ });
515
+
516
+ expect(sentRows()).toEqual([]);
517
+ expect(sentCount()).toBe(5);
518
+ });
519
+
520
+ test("pages the grouped list, so a page boundary never splits a project", async () => {
521
+ /*
522
+ * Two projects, four memberships. Asking for one project must return one
523
+ * project with both its teams — not one of its memberships.
524
+ */
525
+ findBySpy.mockResolvedValue([
526
+ buildMembership({
527
+ id: "m1",
528
+ projectId: PROJECT_ONE_ID,
529
+ projectName: "Acme Production",
530
+ teamId: "t1",
531
+ teamName: "Owners",
532
+ }),
533
+ buildMembership({
534
+ id: "m2",
535
+ projectId: PROJECT_ONE_ID,
536
+ projectName: "Acme Production",
537
+ teamId: "t2",
538
+ teamName: "Engineering",
539
+ }),
540
+ buildMembership({
541
+ id: "m3",
542
+ projectId: PROJECT_TWO_ID,
543
+ projectName: "Acme Staging",
544
+ teamId: "t3",
545
+ teamName: "Owners",
546
+ }),
547
+ buildMembership({
548
+ id: "m4",
549
+ projectId: PROJECT_TWO_ID,
550
+ projectName: "Acme Staging",
551
+ teamId: "t4",
552
+ teamName: "Engineering",
553
+ }),
554
+ ]);
555
+
556
+ await callRoute({
557
+ route: PROJECTS_ROUTE,
558
+ params: { userId: USER_ID },
559
+ query: { skip: "0", limit: "1" },
560
+ });
561
+
562
+ expect(projectNamesSent()).toEqual(["Acme Production"]);
563
+ expect(sentRows()[0]!["teamCount"]).toBe(2);
564
+ expect(sentCount()).toBe(2);
565
+ });
566
+
567
+ test("falls back to a default page rather than returning everything when paging is unparseable", async () => {
568
+ /*
569
+ * A NaN skip/limit turns Array.slice into "from 0 to the end", which is
570
+ * the failure that matters: it silently ignores paging and ships every
571
+ * project the user is in to a caller that asked for a page. The dataset is
572
+ * deliberately larger than DEFAULT_LIMIT so "a default page" and
573
+ * "everything" are distinguishable.
574
+ */
575
+ findBySpy.mockResolvedValue(
576
+ Array.from(
577
+ { length: DEFAULT_LIMIT + 4 },
578
+ (_unused: unknown, index: number) => {
579
+ return buildMembership({
580
+ id: `m${index}`,
581
+ projectId: `00000000-0000-4000-8000-0000000001${(index + 10).toString()}`,
582
+ // Zero-padded so alphabetical order is numerical order.
583
+ projectName: `Project ${(index + 1).toString().padStart(2, "0")}`,
584
+ teamId: `t${index}`,
585
+ });
586
+ },
587
+ ),
588
+ );
589
+
590
+ await callRoute({
591
+ route: PROJECTS_ROUTE,
592
+ params: { userId: USER_ID },
593
+ query: { skip: "not-a-number", limit: "-3" },
594
+ });
595
+
596
+ expect(sentRows()).toHaveLength(DEFAULT_LIMIT);
597
+ expect(projectNamesSent()[0]).toBe("Project 01");
598
+ // The total still reports every project, so the caller can page on.
599
+ expect(sentCount()).toBe(DEFAULT_LIMIT + 4);
600
+ });
601
+
602
+ test("treats an absent skip and limit the same way", async () => {
603
+ await callRoute({
604
+ route: PROJECTS_ROUTE,
605
+ params: { userId: USER_ID },
606
+ });
607
+
608
+ // Fewer projects than DEFAULT_LIMIT here, so the whole list is one page.
609
+ expect(projectNamesSent()).toEqual([
610
+ "Alpha",
611
+ "Bravo",
612
+ "Charlie",
613
+ "Delta",
614
+ "Echo",
615
+ ]);
616
+ expect(sentCount()).toBe(5);
617
+ });
618
+ });
619
+
620
+ describe("POST /user/:userId/projects — bad input", () => {
621
+ let findBySpy: jest.SpyInstance;
622
+
623
+ beforeAll(() => {
624
+ mockRouter.routes.length = 0;
625
+ new UserAPI();
626
+ });
627
+
628
+ beforeEach(() => {
629
+ jest.clearAllMocks();
630
+ findBySpy = jest.spyOn(TeamMemberService, "findBy").mockResolvedValue([]);
631
+ });
632
+
633
+ afterEach(() => {
634
+ jest.restoreAllMocks();
635
+ });
636
+
637
+ test("rejects a missing user id without reading anything", async () => {
638
+ const result: RouteCallResult = await callRoute({
639
+ route: PROJECTS_ROUTE,
640
+ params: {},
641
+ });
642
+
643
+ expect(findBySpy).not.toHaveBeenCalled();
644
+ expect(result.nextCallCount).toBe(1);
645
+ });
646
+
647
+ test("rejects a user id that is not a uuid without reading anything", async () => {
648
+ const result: RouteCallResult = await callRoute({
649
+ route: PROJECTS_ROUTE,
650
+ params: { userId: "'; DROP TABLE TeamMember; --" },
651
+ });
652
+
653
+ expect(findBySpy).not.toHaveBeenCalled();
654
+ expect(result.nextCallCount).toBe(1);
655
+ });
656
+
657
+ test("passes a read failure on rather than reporting an empty list", async () => {
658
+ const readError: Error = new Error("connection terminated");
659
+ findBySpy.mockRejectedValue(readError);
660
+
661
+ const result: RouteCallResult = await callRoute({
662
+ route: PROJECTS_ROUTE,
663
+ params: { userId: USER_ID },
664
+ });
665
+
666
+ expect(result.thrownToNext).toBe(readError);
667
+ expect(Response.sendJsonArrayResponse).not.toHaveBeenCalled();
668
+ });
669
+ });
670
+
671
+ describe("POST /user/:userId/remove-from-project", () => {
672
+ let deleteBySpy: jest.SpyInstance;
673
+ let propsSpy: jest.SpyInstance;
674
+ let callerUserId: ObjectID;
675
+
676
+ beforeAll(() => {
677
+ mockRouter.routes.length = 0;
678
+ new UserAPI();
679
+ });
680
+
681
+ beforeEach(() => {
682
+ jest.clearAllMocks();
683
+
684
+ callerUserId = ObjectID.generate();
685
+
686
+ deleteBySpy = jest
687
+ .spyOn(TeamMemberService, "deleteBy")
688
+ .mockResolvedValue(3);
689
+
690
+ propsSpy = jest
691
+ .spyOn(CommonAPI, "getDatabaseCommonInteractionProps")
692
+ .mockResolvedValue({
693
+ userId: callerUserId,
694
+ userType: UserType.MasterAdmin,
695
+ isMasterAdmin: true,
696
+ } as DatabaseCommonInteractionProps);
697
+ });
698
+
699
+ afterEach(() => {
700
+ jest.restoreAllMocks();
701
+ });
702
+
703
+ test("the route is registered", () => {
704
+ expect(() => {
705
+ return mockRouter.match("POST", REMOVE_ROUTE);
706
+ }).not.toThrow();
707
+ });
708
+
709
+ test("only a master admin can reach it", () => {
710
+ expect(mockRouter.match("POST", REMOVE_ROUTE).middleware).toBe(
711
+ MasterAdminAuthorization.isAuthorizedMasterAdminMiddleware,
712
+ );
713
+ });
714
+
715
+ test("removes every membership in ONE delete, scoped to the user and project", async () => {
716
+ await callRoute({
717
+ route: REMOVE_ROUTE,
718
+ params: { userId: USER_ID },
719
+ body: { projectId: PROJECT_ONE_ID },
720
+ });
721
+
722
+ expect(deleteBySpy).toHaveBeenCalledTimes(1);
723
+
724
+ const deleteBy: { query: Dictionary<unknown> } = deleteBySpy.mock
725
+ .calls[0]![0] as { query: Dictionary<unknown> };
726
+
727
+ expect((deleteBy.query["userId"] as ObjectID).toString()).toBe(USER_ID);
728
+ expect((deleteBy.query["projectId"] as ObjectID).toString()).toBe(
729
+ PROJECT_ONE_ID,
730
+ );
731
+ // Nothing else narrows it — every team the user is on is in scope.
732
+ expect(Object.keys(deleteBy.query).sort()).toEqual(["projectId", "userId"]);
733
+ });
734
+
735
+ test("leaves the user's other projects alone", async () => {
736
+ await callRoute({
737
+ route: REMOVE_ROUTE,
738
+ params: { userId: USER_ID },
739
+ body: { projectId: PROJECT_ONE_ID },
740
+ });
741
+
742
+ const query: Dictionary<unknown> = (
743
+ deleteBySpy.mock.calls[0]![0] as { query: Dictionary<unknown> }
744
+ ).query;
745
+
746
+ expect((query["projectId"] as ObjectID).toString()).not.toBe(
747
+ PROJECT_TWO_ID,
748
+ );
749
+ });
750
+
751
+ test("runs the delete with the caller's own props, never as root", async () => {
752
+ /*
753
+ * isRoot would skip TeamMemberService's SCIM Push Groups guard. A master
754
+ * admin removing people is fine; a master admin silently fighting the
755
+ * customer's identity provider is not.
756
+ */
757
+ await callRoute({
758
+ route: REMOVE_ROUTE,
759
+ params: { userId: USER_ID },
760
+ body: { projectId: PROJECT_ONE_ID },
761
+ });
762
+
763
+ const props: DatabaseCommonInteractionProps = (
764
+ deleteBySpy.mock.calls[0]![0] as { props: DatabaseCommonInteractionProps }
765
+ ).props;
766
+
767
+ expect(props.isRoot).toBeFalsy();
768
+ expect(props.isMasterAdmin).toBe(true);
769
+ expect(props.userId?.toString()).toBe(callerUserId.toString());
770
+ });
771
+
772
+ test("reports how many memberships were removed", async () => {
773
+ await callRoute({
774
+ route: REMOVE_ROUTE,
775
+ params: { userId: USER_ID },
776
+ body: { projectId: PROJECT_ONE_ID },
777
+ });
778
+
779
+ expect(Response.sendJsonObjectResponse).toHaveBeenCalledWith(
780
+ expect.anything(),
781
+ expect.anything(),
782
+ { numberOfMembershipsDeleted: 3 },
783
+ );
784
+ });
785
+
786
+ test("rejects a missing project id without deleting anything", async () => {
787
+ await callRoute({
788
+ route: REMOVE_ROUTE,
789
+ params: { userId: USER_ID },
790
+ body: {},
791
+ });
792
+
793
+ expect(deleteBySpy).not.toHaveBeenCalled();
794
+ expect(Response.sendErrorResponse).toHaveBeenCalled();
795
+ });
796
+
797
+ test("rejects a project id that is not a uuid without deleting anything", async () => {
798
+ const result: RouteCallResult = await callRoute({
799
+ route: REMOVE_ROUTE,
800
+ params: { userId: USER_ID },
801
+ body: { projectId: "'; DROP TABLE TeamMember; --" },
802
+ });
803
+
804
+ expect(deleteBySpy).not.toHaveBeenCalled();
805
+ expect(result.nextCallCount).toBe(1);
806
+ });
807
+
808
+ test("rejects a missing user id without deleting anything", async () => {
809
+ const result: RouteCallResult = await callRoute({
810
+ route: REMOVE_ROUTE,
811
+ params: {},
812
+ body: { projectId: PROJECT_ONE_ID },
813
+ });
814
+
815
+ expect(deleteBySpy).not.toHaveBeenCalled();
816
+ expect(result.nextCallCount).toBe(1);
817
+ });
818
+
819
+ test("rejects a user id that is not a uuid without deleting anything", async () => {
820
+ const result: RouteCallResult = await callRoute({
821
+ route: REMOVE_ROUTE,
822
+ params: { userId: "not-a-uuid" },
823
+ body: { projectId: PROJECT_ONE_ID },
824
+ });
825
+
826
+ expect(deleteBySpy).not.toHaveBeenCalled();
827
+ expect(result.nextCallCount).toBe(1);
828
+ expect(propsSpy).not.toHaveBeenCalled();
829
+ });
830
+
831
+ test("passes a server rejection on rather than reporting success", async () => {
832
+ /*
833
+ * This is the last-accepted-Owner guard. It has to reach the caller as a
834
+ * failure with nothing deleted — which is exactly what the single deleteBy
835
+ * buys over a per-membership loop.
836
+ */
837
+ const guardError: Error = new Error(
838
+ "This team should have at least 1 member who has accepted the invitation.",
839
+ );
840
+
841
+ deleteBySpy.mockRejectedValue(guardError);
842
+
843
+ const result: RouteCallResult = await callRoute({
844
+ route: REMOVE_ROUTE,
845
+ params: { userId: USER_ID },
846
+ body: { projectId: PROJECT_ONE_ID },
847
+ });
848
+
849
+ expect(result.thrownToNext).toBe(guardError);
850
+ expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
851
+ });
852
+ });