@oneuptime/common 12.0.34 → 13.0.0

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 (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -0,0 +1,368 @@
1
+ import Entities from "../../../../Models/DatabaseModels/Index";
2
+ import InventoryItem from "../../../../Models/DatabaseModels/InventoryItem";
3
+ import QueryUtil from "../../../../Server/Types/Database/QueryUtil";
4
+ import Query from "../../../../Types/BaseDatabase/Query";
5
+ import Includes from "../../../../Types/BaseDatabase/Includes";
6
+ import IncludesNone from "../../../../Types/BaseDatabase/IncludesNone";
7
+ import LessThan from "../../../../Types/BaseDatabase/LessThan";
8
+ import { JSONObject } from "../../../../Types/JSON";
9
+ import JSONFunctions from "../../../../Types/JSONFunctions";
10
+ import ObjectID from "../../../../Types/ObjectID";
11
+ import EntitySource from "../../../../Types/Telemetry/EntitySource";
12
+ import EntityType from "../../../../Types/Telemetry/EntityType";
13
+ import {
14
+ InventoryLiveness,
15
+ getInventoryLivenessState,
16
+ } from "../../../../Types/Telemetry/InventoryLiveness";
17
+ import { DataSource, FindOptionsWhere, QueryRunner } from "typeorm";
18
+
19
+ /*
20
+ * Opt in with RUN_POSTGRES_INVENTORY_FACET_TESTS=true and config.env loaded.
21
+ * Tables live in a unique temporary schema; production rows are never read
22
+ * or changed. A transaction freezes CURRENT_TIMESTAMP for exact boundaries.
23
+ */
24
+ const describePostgres: typeof describe =
25
+ process.env["RUN_POSTGRES_INVENTORY_FACET_TESTS"] === "true"
26
+ ? describe
27
+ : describe.skip;
28
+
29
+ describePostgres("inventory status facets against Postgres", () => {
30
+ const schema: string = `inventory_facets_${ObjectID.generate().toString().replace(/-/g, "")}`;
31
+ const projectId: ObjectID = ObjectID.generate();
32
+ const otherProjectId: ObjectID = ObjectID.generate();
33
+ const ids: Record<string, string> = {};
34
+ let database: DataSource;
35
+ let runner: QueryRunner;
36
+ let now: Date;
37
+
38
+ beforeAll(async () => {
39
+ database = new DataSource({
40
+ type: "postgres",
41
+ host: process.env["INVENTORY_FACET_TEST_DATABASE_HOST"] || "localhost",
42
+ port: Number(process.env["INVENTORY_FACET_TEST_DATABASE_PORT"] || "5400"),
43
+ username: process.env["DATABASE_USERNAME"] || "postgres",
44
+ password: process.env["DATABASE_PASSWORD"] || "password",
45
+ database: process.env["DATABASE_NAME"] || "oneuptimedb",
46
+ entities: Entities,
47
+ schema,
48
+ synchronize: false,
49
+ extra: { options: `-c search_path=${schema},public` },
50
+ });
51
+ await database.initialize();
52
+ await database.query(`CREATE SCHEMA "${schema}"`);
53
+ /*
54
+ * A standalone temporary table lets this suite run on an empty Postgres
55
+ * instance too. Its columns are the physical inventory columns used by
56
+ * these queries; the status itself must never exist on disk.
57
+ */
58
+ await database.query(`CREATE TABLE "${schema}"."InventoryItem" (
59
+ "_id" uuid PRIMARY KEY,
60
+ "projectId" uuid NOT NULL,
61
+ "displayName" varchar NOT NULL,
62
+ "entityType" varchar NOT NULL,
63
+ "source" varchar,
64
+ "lastSeenAt" timestamptz,
65
+ "isArchived" boolean NOT NULL DEFAULT false,
66
+ "deletedAt" timestamptz
67
+ )`);
68
+ runner = database.createQueryRunner();
69
+ await runner.connect();
70
+ await runner.startTransaction();
71
+ now = (await runner.query("SELECT CURRENT_TIMESTAMP AS now"))[0].now;
72
+
73
+ for (const [name, age] of [
74
+ ["live-future", -60_000],
75
+ ["live-now", 0],
76
+ ["live-upper-boundary", 31 * 60_000 - 1],
77
+ ["recent-lower-boundary", 31 * 60_000],
78
+ ["recent-upper-boundary", 1441 * 60_000 - 1],
79
+ ["stale-lower-boundary", 1441 * 60_000],
80
+ ["stale-old", 3 * 1440 * 60_000],
81
+ ] as Array<[string, number]>) {
82
+ await seed(name, EntitySource.Discovered, age);
83
+ }
84
+ await seed("never-seen", EntitySource.Discovered, null);
85
+ await seed("manual-old", EntitySource.Manual, 365 * 1440 * 60_000);
86
+ await seed("mirrored-no-heartbeat", EntitySource.Inventory, null);
87
+ await seed("unknown-source", "unknown", 365 * 1440 * 60_000);
88
+ await seed("missing-source", null, null);
89
+ await seed("archived-stale", EntitySource.Discovered, 3000 * 60_000, {
90
+ archived: true,
91
+ });
92
+ await seed("foreign-stale", EntitySource.Discovered, 3000 * 60_000, {
93
+ projectId: otherProjectId,
94
+ });
95
+ await seed("host-stale", EntitySource.Discovered, 3000 * 60_000, {
96
+ entityType: EntityType.Host,
97
+ });
98
+ });
99
+
100
+ afterAll(async () => {
101
+ if (runner) {
102
+ if (runner.isTransactionActive) {
103
+ await runner.rollbackTransaction();
104
+ }
105
+ await runner.release();
106
+ }
107
+ if (database?.isInitialized) {
108
+ await database.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
109
+ await database.destroy();
110
+ }
111
+ });
112
+
113
+ async function seed(
114
+ name: string,
115
+ source: string | null,
116
+ age: number | null,
117
+ options: {
118
+ archived?: boolean;
119
+ projectId?: ObjectID;
120
+ entityType?: EntityType;
121
+ } = {},
122
+ ): Promise<void> {
123
+ const id: string = ObjectID.generate().toString();
124
+ ids[name] = id;
125
+ await runner.query(
126
+ `INSERT INTO "${schema}"."InventoryItem" ("_id", "projectId", "displayName", "entityType", "source", "lastSeenAt", "isArchived") VALUES ($1, $2, $3, $4, $5, $6, $7)`,
127
+ [
128
+ id,
129
+ (options.projectId || projectId).toString(),
130
+ name,
131
+ options.entityType || EntityType.Service,
132
+ source,
133
+ age === null ? null : new Date(now.getTime() - age),
134
+ Boolean(options.archived),
135
+ ],
136
+ );
137
+ }
138
+
139
+ function where(
140
+ query: Query<InventoryItem> = {},
141
+ ): FindOptionsWhere<InventoryItem> {
142
+ return QueryUtil.serializeQuery(InventoryItem, {
143
+ projectId,
144
+ isArchived: false,
145
+ ...query,
146
+ }) as unknown as FindOptionsWhere<InventoryItem>;
147
+ }
148
+
149
+ async function find(
150
+ query: Query<InventoryItem> = {},
151
+ ): Promise<Array<InventoryItem>> {
152
+ return runner.manager.getRepository(InventoryItem).find({
153
+ select: {
154
+ _id: true,
155
+ displayName: true,
156
+ source: true,
157
+ lastSeenAt: true,
158
+ inventoryStatus: true,
159
+ },
160
+ where: where(query),
161
+ order: { displayName: "ASC" },
162
+ });
163
+ }
164
+
165
+ async function names(query: Query<InventoryItem>): Promise<Array<string>> {
166
+ return (await find(query)).map((item: InventoryItem): string => {
167
+ return item.displayName!;
168
+ });
169
+ }
170
+
171
+ test("every SQL classification matches the shared badge rule at exact boundaries", async () => {
172
+ const rows: Array<InventoryItem> = await find();
173
+ expect(rows).toHaveLength(13);
174
+ for (const item of rows) {
175
+ expect({ name: item.displayName, status: item.inventoryStatus }).toEqual({
176
+ name: item.displayName,
177
+ status: getInventoryLivenessState({
178
+ source: item.source,
179
+ lastSeenAt: item.lastSeenAt,
180
+ now,
181
+ }),
182
+ });
183
+ }
184
+ });
185
+
186
+ test.each([
187
+ [
188
+ InventoryLiveness.Live,
189
+ ["live-future", "live-now", "live-upper-boundary"],
190
+ ],
191
+ [
192
+ InventoryLiveness.Recent,
193
+ ["recent-lower-boundary", "recent-upper-boundary"],
194
+ ],
195
+ [
196
+ InventoryLiveness.Stale,
197
+ ["host-stale", "stale-lower-boundary", "stale-old"],
198
+ ],
199
+ [InventoryLiveness.Never, ["never-seen"]],
200
+ [
201
+ InventoryLiveness.NotTracked,
202
+ [
203
+ "manual-old",
204
+ "mirrored-no-heartbeat",
205
+ "missing-source",
206
+ "unknown-source",
207
+ ],
208
+ ],
209
+ ] as Array<[InventoryLiveness, Array<string>]>)(
210
+ "%s selects precisely the matching rows",
211
+ async (status: InventoryLiveness, expected: Array<string>) => {
212
+ expect(await names({ inventoryStatus: new Includes([status]) })).toEqual(
213
+ expected,
214
+ );
215
+ },
216
+ );
217
+
218
+ test("multiple statuses are a union and negation is its complement including untracked rows", async () => {
219
+ const selected: Array<string> = await names({
220
+ inventoryStatus: new Includes([
221
+ InventoryLiveness.Stale,
222
+ InventoryLiveness.Never,
223
+ ]),
224
+ });
225
+ expect(selected).toEqual([
226
+ "host-stale",
227
+ "never-seen",
228
+ "stale-lower-boundary",
229
+ "stale-old",
230
+ ]);
231
+ const excluded: Array<string> = await names({
232
+ inventoryStatus: new IncludesNone([
233
+ InventoryLiveness.Stale,
234
+ InventoryLiveness.Never,
235
+ ]),
236
+ });
237
+ expect(excluded).toHaveLength(9);
238
+ expect(excluded).toEqual(
239
+ expect.arrayContaining(["manual-old", "missing-source"]),
240
+ );
241
+ expect(new Set([...selected, ...excluded])).toEqual(
242
+ new Set(await names({})),
243
+ );
244
+ expect(
245
+ selected.filter((name: string): boolean => {
246
+ return excluded.includes(name);
247
+ }),
248
+ ).toEqual([]);
249
+ });
250
+
251
+ test("status preserves type and source filters", async () => {
252
+ expect(
253
+ await names({
254
+ inventoryStatus: new Includes([InventoryLiveness.Stale]),
255
+ entityType: new Includes([EntityType.Host]),
256
+ }),
257
+ ).toEqual(["host-stale"]);
258
+ expect(
259
+ await names({
260
+ inventoryStatus: new Includes([InventoryLiveness.Stale]),
261
+ source: EntitySource.Manual,
262
+ }),
263
+ ).toEqual([]);
264
+ expect(
265
+ await names({
266
+ inventoryStatus: new Includes([InventoryLiveness.NotTracked]),
267
+ source: EntitySource.Manual,
268
+ }),
269
+ ).toEqual(["manual-old"]);
270
+ });
271
+
272
+ test("last seen dates intersect status without overwriting it", async () => {
273
+ expect(
274
+ await names({
275
+ inventoryStatus: new Includes([InventoryLiveness.Stale]),
276
+ lastSeenAt: new LessThan(
277
+ new Date(now.getTime() - 3500 * 60_000).toISOString(),
278
+ ),
279
+ }),
280
+ ).toEqual(["stale-old"]);
281
+ expect(
282
+ await names({
283
+ inventoryStatus: new Includes([InventoryLiveness.Live]),
284
+ lastSeenAt: new LessThan(
285
+ new Date(now.getTime() - 60 * 60_000).toISOString(),
286
+ ),
287
+ }),
288
+ ).toEqual([]);
289
+ });
290
+
291
+ test("status preserves explicit ids, project isolation and archived views", async () => {
292
+ expect(
293
+ await names({
294
+ inventoryStatus: new Includes([InventoryLiveness.Stale]),
295
+ _id: ids["stale-old"]!,
296
+ }),
297
+ ).toEqual(["stale-old"]);
298
+ expect(
299
+ await names({
300
+ inventoryStatus: new Includes([InventoryLiveness.Stale]),
301
+ _id: ids["foreign-stale"]!,
302
+ }),
303
+ ).toEqual([]);
304
+ expect(
305
+ await names({
306
+ inventoryStatus: new Includes([InventoryLiveness.Stale]),
307
+ isArchived: true,
308
+ }),
309
+ ).toEqual(["archived-stale"]);
310
+ });
311
+
312
+ test("count and pagination use the same status predicate", async () => {
313
+ const [rows, count]: [Array<InventoryItem>, number] = await runner.manager
314
+ .getRepository(InventoryItem)
315
+ .findAndCount({
316
+ select: { _id: true, displayName: true },
317
+ where: where({
318
+ inventoryStatus: new Includes([
319
+ InventoryLiveness.Live,
320
+ InventoryLiveness.Recent,
321
+ ]),
322
+ }),
323
+ order: { displayName: "ASC" },
324
+ skip: 2,
325
+ take: 2,
326
+ });
327
+ expect(count).toBe(5);
328
+ expect(
329
+ rows.map((item: InventoryItem): string => {
330
+ return item.displayName!;
331
+ }),
332
+ ).toEqual(["live-upper-boundary", "recent-lower-boundary"]);
333
+ });
334
+
335
+ test("a serialized API query applies the same status filter", async () => {
336
+ const serialized: JSONObject = JSON.parse(
337
+ JSON.stringify(
338
+ JSONFunctions.serialize({
339
+ inventoryStatus: new Includes([
340
+ InventoryLiveness.Never,
341
+ InventoryLiveness.Recent,
342
+ ]),
343
+ entityType: new Includes([EntityType.Service]),
344
+ }),
345
+ ),
346
+ );
347
+ expect(
348
+ await names(
349
+ JSONFunctions.deserialize(serialized) as Query<InventoryItem>,
350
+ ),
351
+ ).toEqual(["never-seen", "recent-lower-boundary", "recent-upper-boundary"]);
352
+ });
353
+
354
+ test("the empty schema needs no physical status column", async () => {
355
+ const columns: Array<{ column_name: string }> = await runner.query(
356
+ "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = 'InventoryItem'",
357
+ [schema],
358
+ );
359
+ expect(
360
+ columns.map((column: { column_name: string }): string => {
361
+ return column.column_name;
362
+ }),
363
+ ).not.toContain("inventoryStatus");
364
+ expect(
365
+ await names({ inventoryStatus: new Includes([InventoryLiveness.Never]) }),
366
+ ).toEqual(["never-seen"]);
367
+ });
368
+ });
@@ -1,14 +1,22 @@
1
1
  import ReadPermission from "../../../../../Server/Types/Database/Permissions/ReadPermission";
2
+ import QueryUtil from "../../../../../Server/Types/Database/QueryUtil";
2
3
  import Incident from "../../../../../Models/DatabaseModels/Incident";
3
4
  import Monitor from "../../../../../Models/DatabaseModels/Monitor";
4
5
  import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
6
+ import Includes from "../../../../../Types/BaseDatabase/Includes";
7
+ import IncludesAnyOfGroups from "../../../../../Types/BaseDatabase/IncludesAnyOfGroups";
8
+ import IncludesNone from "../../../../../Types/BaseDatabase/IncludesNone";
9
+ import EqualTo from "../../../../../Types/BaseDatabase/EqualTo";
10
+ import NotEqual from "../../../../../Types/BaseDatabase/NotEqual";
11
+ import Search from "../../../../../Types/BaseDatabase/Search";
12
+ import BadDataException from "../../../../../Types/Exception/BadDataException";
5
13
  import NotAuthorizedException from "../../../../../Types/Exception/NotAuthorizedException";
6
14
  import ObjectID from "../../../../../Types/ObjectID";
7
15
  import Permission, {
8
16
  UserPermission,
9
17
  UserTenantAccessPermission,
10
18
  } from "../../../../../Types/Permission";
11
- import { FindOperator } from "typeorm";
19
+ import { FindOperator, In } from "typeorm";
12
20
 
13
21
  describe("ReadPermission.checkReadBlockPermission", () => {
14
22
  const projectId: ObjectID = ObjectID.generate();
@@ -16,6 +24,26 @@ describe("ReadPermission.checkReadBlockPermission", () => {
16
24
  const blockedLabelA: ObjectID = ObjectID.generate();
17
25
  const blockedLabelB: ObjectID = ObjectID.generate();
18
26
 
27
+ beforeEach(() => {
28
+ jest
29
+ .spyOn(QueryUtil, "getManyToManyRelationMetadata")
30
+ .mockImplementation((modelType: any, column: string) => {
31
+ if (column !== "labels") {
32
+ return null;
33
+ }
34
+ return {
35
+ joinTableName:
36
+ modelType === Monitor ? "MonitorLabel" : "IncidentLabel",
37
+ ownerColumnName: modelType === Monitor ? "monitorId" : "incidentId",
38
+ relationColumnName: "labelId",
39
+ };
40
+ });
41
+ });
42
+
43
+ afterEach(() => {
44
+ jest.restoreAllMocks();
45
+ });
46
+
19
47
  function makeProps(
20
48
  permissions: Array<UserPermission>,
21
49
  ): DatabaseCommonInteractionProps {
@@ -59,18 +87,21 @@ describe("ReadPermission.checkReadBlockPermission", () => {
59
87
  }
60
88
 
61
89
  /*
62
- * The rewrite has to SUBTRACT the blocked labels, not restrict the read to
63
- * them. Asserting only that some FindOperator landed on the column would
64
- * pass just as happily if notInOrNull() were swapped for its including
65
- * counterpart, so read the rendered SQL back and return the ids it denies.
90
+ * Read the owner-row exclusion predicate, including the relation table it
91
+ * uses, so the test checks that label blocks are composed independently of
92
+ * the caller's relation filters.
66
93
  */
67
- function deniedLabelIds(operator: any): Array<string> {
94
+ function deniedLabelIds(
95
+ operator: any,
96
+ joinTableName: string = "IncidentLabel",
97
+ ): Array<string> {
68
98
  expect(operator).toBeInstanceOf(FindOperator);
69
99
  expect(operator.type).toBe("raw");
70
100
 
71
- const sql: string = operator.getSql("labelId");
72
- expect(sql).toContain("NOT IN");
73
- expect(sql).toContain("IS NULL");
101
+ const sql: string = operator.getSql("ownerId");
102
+ expect(sql).toContain("ownerId NOT IN (SELECT");
103
+ expect(sql).toContain(`FROM "${joinTableName}"`);
104
+ expect(sql).toContain(`WHERE "${joinTableName}"."labelId" IN (`);
74
105
 
75
106
  return Object.values(
76
107
  operator.objectLiteralParameters as Record<string, Array<string>>,
@@ -136,8 +167,8 @@ describe("ReadPermission.checkReadBlockPermission", () => {
136
167
  makeProps([blockPermission(Permission.ProjectMember, [blockedLabelA])]),
137
168
  );
138
169
 
139
- expect(result.query.labels).toBeDefined();
140
- expect(deniedLabelIds(result.query.labels._id)).toEqual([
170
+ expect(result.query.labels).toBeUndefined();
171
+ expect(deniedLabelIds(result.query._id)).toEqual([
141
172
  blockedLabelA.toString(),
142
173
  ]);
143
174
  // The caller's own filters survive the rewrite.
@@ -156,7 +187,7 @@ describe("ReadPermission.checkReadBlockPermission", () => {
156
187
  ]),
157
188
  );
158
189
 
159
- expect(deniedLabelIds(result.query.labels._id)).toEqual([
190
+ expect(deniedLabelIds(result.query._id)).toEqual([
160
191
  blockedLabelA.toString(),
161
192
  blockedLabelB.toString(),
162
193
  ]);
@@ -204,9 +235,225 @@ describe("ReadPermission.checkReadBlockPermission", () => {
204
235
  makeProps([blockPermission(Permission.ProjectMember, [blockedLabelA])]),
205
236
  );
206
237
 
207
- expect(result.query.labels).toBeDefined();
208
- expect(deniedLabelIds(result.query.labels._id)).toEqual([
238
+ expect(result.query.labels).toBeUndefined();
239
+ expect(deniedLabelIds(result.query._id, "MonitorLabel")).toEqual([
240
+ blockedLabelA.toString(),
241
+ ]);
242
+ });
243
+
244
+ it("preserves an existing label selection while adding the block condition", async () => {
245
+ const labels: Includes = new Includes([ObjectID.generate().toString()]);
246
+ const query: any = { projectId, labels };
247
+
248
+ const result: any = await ReadPermission.checkReadBlockPermission(
249
+ Monitor,
250
+ query,
251
+ makeProps([blockPermission(Permission.ProjectMember, [blockedLabelA])]),
252
+ );
253
+
254
+ expect(result.query.labels).toBe(labels);
255
+ expect(result.query.projectId).toBe(projectId);
256
+ expect(deniedLabelIds(result.query._id, "MonitorLabel")).toEqual([
257
+ blockedLabelA.toString(),
258
+ ]);
259
+ });
260
+
261
+ it("preserves grouped dashboard labels, project, status, and the selected record through query serialization", async () => {
262
+ const monitorId: ObjectID = ObjectID.generate();
263
+ const fixedLabelId: string = ObjectID.generate().toString();
264
+ const selectedLabelId: string = ObjectID.generate().toString();
265
+ const labels: IncludesAnyOfGroups = new IncludesAnyOfGroups([
266
+ [fixedLabelId],
267
+ [selectedLabelId],
268
+ ]);
269
+ const currentMonitorStatusId: ObjectID = ObjectID.generate();
270
+ const query: any = {
271
+ projectId,
272
+ _id: monitorId,
273
+ labels,
274
+ currentMonitorStatusId,
275
+ };
276
+
277
+ const result: any = await ReadPermission.checkReadBlockPermission(
278
+ Monitor,
279
+ query,
280
+ makeProps([blockPermission(Permission.ProjectMember, [blockedLabelA])]),
281
+ );
282
+
283
+ expect(result.query.labels).toBe(labels);
284
+ expect(result.query.projectId).toBe(projectId);
285
+ expect(result.query.currentMonitorStatusId).toBe(currentMonitorStatusId);
286
+ expect(result.query._id.type).toBe("and");
287
+ expect(result.query._id.value[0].type).toBe("raw");
288
+ expect(
289
+ Object.values(result.query._id.value[0].objectLiteralParameters),
290
+ ).toEqual([monitorId.toString()]);
291
+ expect(deniedLabelIds(result.query._id.value[1], "MonitorLabel")).toEqual([
209
292
  blockedLabelA.toString(),
210
293
  ]);
294
+
295
+ const recordAndBlockCondition: FindOperator<any> = result.query._id;
296
+ const serialized: any = QueryUtil.serializeQuery(Monitor, result.query);
297
+ expect(serialized.labels).toBeUndefined();
298
+ expect(serialized._id.type).toBe("and");
299
+ const clauses: Array<FindOperator<any>> = serialized._id.value;
300
+ expect(clauses).toHaveLength(3);
301
+ expect(clauses[0]).toBe(recordAndBlockCondition);
302
+ expect(Object.values(clauses[1]!.objectLiteralParameters || {})).toEqual([
303
+ [fixedLabelId],
304
+ ]);
305
+ expect(Object.values(clauses[2]!.objectLiteralParameters || {})).toEqual([
306
+ [selectedLabelId],
307
+ ]);
308
+ expect(Object.values(serialized.projectId.objectLiteralParameters)).toEqual(
309
+ [projectId.toString()],
310
+ );
311
+ expect(
312
+ Object.values(serialized.currentMonitorStatusId.objectLiteralParameters),
313
+ ).toEqual([currentMonitorStatusId.toString()]);
314
+ });
315
+
316
+ it("combines with an existing database ID condition without replacing it", async () => {
317
+ const idFilter: FindOperator<string> = In([ObjectID.generate().toString()]);
318
+ const query: any = { projectId, _id: idFilter };
319
+
320
+ const result: any = await ReadPermission.checkReadBlockPermission(
321
+ Incident,
322
+ query,
323
+ makeProps([blockPermission(Permission.ProjectMember, [blockedLabelA])]),
324
+ );
325
+
326
+ expect(result.query._id.type).toBe("and");
327
+ expect(result.query._id.value[0]).toBe(idFilter);
328
+ expect(deniedLabelIds(result.query._id.value[1])).toEqual([
329
+ blockedLabelA.toString(),
330
+ ]);
331
+ });
332
+
333
+ const requestedId: string = ObjectID.generate().toString();
334
+
335
+ it.each([
336
+ {
337
+ name: "EqualTo",
338
+ idFilter: new EqualTo(requestedId),
339
+ expectedSql: "Monitor._id = :",
340
+ expectedParameters: [requestedId],
341
+ },
342
+ {
343
+ name: "IncludesNone",
344
+ idFilter: new IncludesNone([new ObjectID(requestedId)]),
345
+ expectedSql: "Monitor._id NOT IN (",
346
+ expectedParameters: [[requestedId]],
347
+ },
348
+ {
349
+ name: "NotEqual",
350
+ idFilter: new NotEqual(requestedId),
351
+ expectedSql: "Monitor._id != :",
352
+ expectedParameters: [requestedId],
353
+ },
354
+ {
355
+ name: "Search",
356
+ idFilter: new Search(requestedId),
357
+ expectedSql: "CAST(Monitor._id AS TEXT) ILIKE",
358
+ expectedParameters: [`%${requestedId}%`],
359
+ },
360
+ {
361
+ name: "Includes",
362
+ idFilter: new Includes([requestedId]),
363
+ expectedSql: "Monitor._id IN (",
364
+ expectedParameters: [[requestedId]],
365
+ },
366
+ ])(
367
+ "preserves the $name ID filter when composing read label restrictions",
368
+ async ({
369
+ idFilter,
370
+ expectedSql,
371
+ expectedParameters,
372
+ }: {
373
+ idFilter: unknown;
374
+ expectedSql: string;
375
+ expectedParameters: Array<unknown>;
376
+ }): Promise<void> => {
377
+ const labels: IncludesAnyOfGroups = new IncludesAnyOfGroups([
378
+ [ObjectID.generate().toString()],
379
+ [ObjectID.generate().toString()],
380
+ ]);
381
+ const query: any = { projectId, _id: idFilter, labels };
382
+
383
+ const result: any = await ReadPermission.checkReadBlockPermission(
384
+ Monitor,
385
+ query,
386
+ makeProps([blockPermission(Permission.ProjectMember, [blockedLabelA])]),
387
+ );
388
+
389
+ expect(result.query.labels).toBe(labels);
390
+ expect(result.query._id.type).toBe("and");
391
+ const callerCondition: FindOperator<unknown> = result.query._id.value[0];
392
+ expect(callerCondition.type).toBe("raw");
393
+ expect(callerCondition.getSql?.("Monitor._id")).toContain(expectedSql);
394
+ expect(
395
+ Object.values(callerCondition.objectLiteralParameters || {}),
396
+ ).toEqual(expectedParameters);
397
+ expect(deniedLabelIds(result.query._id.value[1], "MonitorLabel")).toEqual(
398
+ [blockedLabelA.toString()],
399
+ );
400
+
401
+ const recordAndBlockCondition: FindOperator<unknown> = result.query._id;
402
+ const serialized: any = QueryUtil.serializeQuery(Monitor, result.query);
403
+ expect(serialized._id.value[0]).toBe(recordAndBlockCondition);
404
+ expect(serialized._id.value).toHaveLength(3);
405
+ },
406
+ );
407
+
408
+ it("rejects an unsupported ID condition before changing the caller's query", async () => {
409
+ const idFilter: Record<string, unknown> = { comparison: "unsupported" };
410
+ const labels: Includes = new Includes([ObjectID.generate().toString()]);
411
+ const query: any = { projectId, _id: idFilter, labels };
412
+
413
+ await expect(
414
+ ReadPermission.checkReadBlockPermission(
415
+ Monitor,
416
+ query,
417
+ makeProps([blockPermission(Permission.ProjectMember, [blockedLabelA])]),
418
+ ),
419
+ ).rejects.toThrow("unsupported ID filter");
420
+ expect(query).toEqual({ projectId, _id: idFilter, labels });
421
+ });
422
+
423
+ it("reports missing relation metadata without changing the caller's filters", async () => {
424
+ jest
425
+ .spyOn(QueryUtil, "getManyToManyRelationMetadata")
426
+ .mockReturnValue(null);
427
+ const labels: IncludesAnyOfGroups = new IncludesAnyOfGroups([
428
+ [ObjectID.generate().toString()],
429
+ [ObjectID.generate().toString()],
430
+ ]);
431
+ const monitorId: ObjectID = ObjectID.generate();
432
+ const query: any = { projectId, labels, _id: monitorId };
433
+
434
+ await expect(
435
+ ReadPermission.checkReadBlockPermission(
436
+ Monitor,
437
+ query,
438
+ makeProps([blockPermission(Permission.ProjectMember, [blockedLabelA])]),
439
+ ),
440
+ ).rejects.toThrow(BadDataException);
441
+ expect(query).toEqual({ projectId, labels, _id: monitorId });
442
+ });
443
+
444
+ it("reports an unavailable access-control column before composing label restrictions", async () => {
445
+ jest
446
+ .spyOn(Monitor.prototype, "getAccessControlColumn")
447
+ .mockReturnValue(null);
448
+ const query: any = { projectId };
449
+
450
+ await expect(
451
+ ReadPermission.checkReadBlockPermission(
452
+ Monitor,
453
+ query,
454
+ makeProps([blockPermission(Permission.ProjectMember, [blockedLabelA])]),
455
+ ),
456
+ ).rejects.toThrow("access-control relation metadata");
457
+ expect(query).toEqual({ projectId });
211
458
  });
212
459
  });