@oneuptime/common 12.0.2 → 12.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  2. package/Models/DatabaseModels/Index.ts +2 -0
  3. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  4. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  5. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  6. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  7. package/Server/API/BaseAPI.ts +13 -2
  8. package/Server/API/DashboardAPI.ts +39 -0
  9. package/Server/EnvironmentConfig.ts +31 -1
  10. package/Server/Infrastructure/GlobalCache.ts +115 -0
  11. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  14. package/Server/Services/AlertStateTimelineService.ts +20 -0
  15. package/Server/Services/CephClusterService.ts +26 -86
  16. package/Server/Services/CloudResourceService.ts +23 -83
  17. package/Server/Services/DatabaseService.ts +111 -0
  18. package/Server/Services/DockerHostService.ts +19 -79
  19. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  20. package/Server/Services/HostService.ts +46 -99
  21. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/IoTFleetService.ts +19 -79
  24. package/Server/Services/KubernetesClusterService.ts +17 -78
  25. package/Server/Services/LabelService.ts +57 -10
  26. package/Server/Services/MetricTypeService.ts +84 -0
  27. package/Server/Services/MonitorService.ts +89 -14
  28. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  29. package/Server/Services/MonitorTemplateService.ts +65 -0
  30. package/Server/Services/PodmanHostService.ts +19 -79
  31. package/Server/Services/ProxmoxClusterService.ts +23 -83
  32. package/Server/Services/RumApplicationService.ts +19 -80
  33. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  34. package/Server/Services/ServerlessFunctionService.ts +24 -85
  35. package/Server/Services/ServiceService.ts +299 -73
  36. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  37. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  38. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  39. package/Server/Services/TelemetryEntityService.ts +1 -0
  40. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  41. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  42. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  43. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  44. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  45. package/Server/Utils/SingleFlight.ts +83 -0
  46. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  47. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  48. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  49. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  50. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  51. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  52. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  53. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  54. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  55. package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
  56. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  57. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  58. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  59. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  60. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  61. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  62. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  63. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  64. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  65. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  66. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  67. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  68. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  69. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  70. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  71. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  72. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  73. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  74. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  75. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  76. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  77. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  78. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  79. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  80. package/Tests/Types/Decimal.test.ts +37 -0
  81. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  82. package/Tests/Types/Port.test.ts +38 -0
  83. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  84. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  85. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  86. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  87. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  88. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  89. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  90. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  91. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  92. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  93. package/Types/Database/DatabaseProperty.ts +26 -0
  94. package/Types/Database/NumericColumnValue.ts +128 -0
  95. package/Types/Decimal.ts +16 -4
  96. package/Types/Permission.ts +48 -0
  97. package/Types/Port.ts +16 -4
  98. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  99. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  100. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  101. package/Utils/Dashboard/Components/Index.ts +7 -0
  102. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  103. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  105. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  107. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  109. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  110. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  111. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  113. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  114. package/build/dist/Server/API/BaseAPI.js +11 -1
  115. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  116. package/build/dist/Server/API/DashboardAPI.js +39 -0
  117. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  118. package/build/dist/Server/EnvironmentConfig.js +27 -1
  119. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  120. package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
  121. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  122. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  123. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  128. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  129. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  130. package/build/dist/Server/Services/CephClusterService.js +26 -76
  131. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  132. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  133. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  134. package/build/dist/Server/Services/DatabaseService.js +86 -0
  135. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  136. package/build/dist/Server/Services/DockerHostService.js +19 -69
  137. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  138. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  139. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  140. package/build/dist/Server/Services/HostService.js +58 -102
  141. package/build/dist/Server/Services/HostService.js.map +1 -1
  142. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  143. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  144. package/build/dist/Server/Services/Index.js +2 -0
  145. package/build/dist/Server/Services/Index.js.map +1 -1
  146. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  147. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  148. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  149. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  150. package/build/dist/Server/Services/LabelService.js +42 -7
  151. package/build/dist/Server/Services/LabelService.js.map +1 -1
  152. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  153. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  154. package/build/dist/Server/Services/MonitorService.js +81 -9
  155. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  156. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  157. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  158. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  159. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  160. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  161. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  162. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  163. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  164. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  165. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  166. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  167. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  168. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  169. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  170. package/build/dist/Server/Services/ServiceService.js +236 -61
  171. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  172. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  173. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  174. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  175. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  176. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  177. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  178. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  179. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  180. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  181. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  182. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  183. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  184. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  185. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  186. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  187. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  188. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  189. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  190. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  191. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  192. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  193. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  194. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  195. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  196. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  197. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  198. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  199. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  200. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  201. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  202. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  203. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  204. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  205. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  206. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  207. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  208. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  209. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  210. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  211. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  212. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  213. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  214. package/build/dist/Types/Decimal.js +14 -3
  215. package/build/dist/Types/Decimal.js.map +1 -1
  216. package/build/dist/Types/Permission.js +42 -0
  217. package/build/dist/Types/Permission.js.map +1 -1
  218. package/build/dist/Types/Port.js +15 -5
  219. package/build/dist/Types/Port.js.map +1 -1
  220. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  221. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  222. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  223. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  224. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  225. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  226. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  227. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  228. package/package.json +1 -1
@@ -0,0 +1,614 @@
1
+ import AlertStateService from "../../../Server/Services/AlertStateService";
2
+ import AlertStateTimelineService from "../../../Server/Services/AlertStateTimelineService";
3
+ import IncidentStateService from "../../../Server/Services/IncidentStateService";
4
+ import IncidentStateTimelineService from "../../../Server/Services/IncidentStateTimelineService";
5
+ import MonitorService from "../../../Server/Services/MonitorService";
6
+ import MonitorStatusService from "../../../Server/Services/MonitorStatusService";
7
+ import MonitorStatusTimelineService from "../../../Server/Services/MonitorStatusTimelineService";
8
+ import MonitorTemplateService from "../../../Server/Services/MonitorTemplateService";
9
+ import ProjectService from "../../../Server/Services/ProjectService";
10
+ import ScheduledMaintenanceStateService from "../../../Server/Services/ScheduledMaintenanceStateService";
11
+ import ScheduledMaintenanceStateTimelineService from "../../../Server/Services/ScheduledMaintenanceStateTimelineService";
12
+ import DatabaseService from "../../../Server/Services/DatabaseService";
13
+ import MonitorStepsProjectValidator from "../../../Server/Utils/Monitor/MonitorStepsProjectValidator";
14
+ import ProjectScopedReferenceValidator from "../../../Server/Utils/Database/ProjectScopedReferenceValidator";
15
+ import DatabaseBaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
16
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
17
+ import MonitorTemplate from "../../../Models/DatabaseModels/MonitorTemplate";
18
+ import { JSONObject, ObjectType } from "../../../Types/JSON";
19
+ import ObjectID from "../../../Types/ObjectID";
20
+ import {
21
+ afterEach,
22
+ beforeEach,
23
+ describe,
24
+ expect,
25
+ jest,
26
+ test,
27
+ } from "@jest/globals";
28
+
29
+ /*
30
+ * Contract under test: issue #3039 — the write paths that used to accept a
31
+ * reference id matching no record at all.
32
+ *
33
+ * Two distinct halves are covered here.
34
+ *
35
+ * The four *StateTimeline services are the choke point every status/state
36
+ * change funnels through. A bogus id used to travel all the way to Postgres and
37
+ * come back as a raw 23503 ("violates foreign key constraint
38
+ * FK_574feb4161c5216c2c7ee0faaf8" — the error in the issue), which is an opaque
39
+ * 500 over the API and a job that retries forever in the probe worker. The
40
+ * three sibling services DID look their state up, but only inside the ordering
41
+ * branch, so the FIRST timeline row for a record always went through unchecked.
42
+ *
43
+ * MonitorService and MonitorTemplateService own the monitorSteps blob, which
44
+ * has no foreign key behind it at all. They must pass what is currently stored
45
+ * to the validator, so that an id the record ALREADY holds is exempt: a monitor
46
+ * saved broken before this guard existed has to stay editable.
47
+ */
48
+
49
+ const PROJECT_ID: ObjectID = new ObjectID(
50
+ "0b5fbbd0-8d1c-4a72-9a2b-4b8bd1cba0e9",
51
+ );
52
+ const MONITOR_STATUS_ID: ObjectID = new ObjectID(
53
+ "8ff02a3f-3f18-4c1d-9db1-6cf27bb2a4a1",
54
+ );
55
+ const INCIDENT_STATE_ID: ObjectID = new ObjectID(
56
+ "2b0a94a4-2f8c-49f0-8a2e-0f1ff5df41c9",
57
+ );
58
+ const ALERT_STATE_ID: ObjectID = new ObjectID(
59
+ "6a56b0f9-6c8f-4f76-9b53-0a1a5b0ec1a2",
60
+ );
61
+ const SCHEDULED_MAINTENANCE_STATE_ID: ObjectID = new ObjectID(
62
+ "7c2f6b40-6a1b-4f18-9d0e-2c5ba2d3f0a7",
63
+ );
64
+ const MONITOR_ID: ObjectID = new ObjectID(
65
+ "1b7c5d3e-9a4f-4c2b-8e61-3d5a7f9b1c22",
66
+ );
67
+
68
+ type ValidatorCall = {
69
+ projectId: ObjectID | undefined;
70
+ subject?: string | undefined;
71
+ references: Array<{
72
+ modelName: string;
73
+ id: ObjectID | string | undefined | null;
74
+ service: DatabaseService<DatabaseBaseModel>;
75
+ mustExist?: boolean | undefined;
76
+ }>;
77
+ };
78
+
79
+ // Identity -> readable name, so an assertion can name the service it expects.
80
+ const SERVICE_NAMES: Map<unknown, string> = new Map<unknown, string>([
81
+ [MonitorStatusService, "MonitorStatusService"],
82
+ [IncidentStateService, "IncidentStateService"],
83
+ [AlertStateService, "AlertStateService"],
84
+ [ScheduledMaintenanceStateService, "ScheduledMaintenanceStateService"],
85
+ ]);
86
+
87
+ type StepsValidatorCall = {
88
+ monitorSteps?: unknown;
89
+ projectId: ObjectID | undefined;
90
+ alreadyStoredMonitorSteps?: unknown;
91
+ };
92
+
93
+ let validatorCalls: Array<ValidatorCall> = [];
94
+
95
+ /*
96
+ * Records what each service asks the validator about. `rejects` throws only
97
+ * when the call carries at least one id, so a guard that has been emptied out
98
+ * cannot masquerade as a working one by throwing anyway.
99
+ */
100
+ function spyOnValidator(rejects?: boolean): void {
101
+ jest
102
+ .spyOn(ProjectScopedReferenceValidator, "validateReferencesBelongToProject")
103
+ .mockImplementation((async (data: ValidatorCall): Promise<void> => {
104
+ validatorCalls.push(data);
105
+
106
+ const hasId: boolean = data.references.some(
107
+ (reference: { id: ObjectID | string | undefined | null }) => {
108
+ return Boolean(reference.id);
109
+ },
110
+ );
111
+
112
+ if (rejects && hasId) {
113
+ throw new Error("references records that do not exist");
114
+ }
115
+ }) as never);
116
+ }
117
+
118
+ function callHook(
119
+ service: unknown,
120
+ hook: "onBeforeCreate" | "onBeforeUpdate",
121
+ payload: unknown,
122
+ ): Promise<unknown> {
123
+ return (service as Record<string, (input: unknown) => Promise<unknown>>)[
124
+ hook
125
+ ]!(payload);
126
+ }
127
+
128
+ function serviceNameOf(service: unknown): string {
129
+ return SERVICE_NAMES.get(service) || "unknown";
130
+ }
131
+
132
+ const stepsReferencing: (statusId: string) => JSONObject = (
133
+ statusId: string,
134
+ ): JSONObject => {
135
+ return {
136
+ _type: ObjectType.MonitorSteps,
137
+ value: {
138
+ defaultMonitorStatusId: statusId,
139
+ monitorStepsInstanceArray: [],
140
+ },
141
+ };
142
+ };
143
+
144
+ describe("missing-reference guard on write", () => {
145
+ beforeEach(() => {
146
+ validatorCalls = [];
147
+ });
148
+
149
+ afterEach(() => {
150
+ jest.restoreAllMocks();
151
+ });
152
+
153
+ describe("MonitorStatusTimelineService", () => {
154
+ /*
155
+ * This is the exact insert the issue reports failing. It is checked before
156
+ * the dedupe lookups so a bad id costs one query rather than three.
157
+ */
158
+ test("create checks the monitor status against MonitorStatusService", async () => {
159
+ spyOnValidator();
160
+ jest
161
+ .spyOn(MonitorStatusTimelineService, "findOneBy")
162
+ .mockResolvedValue(null as never);
163
+
164
+ await callHook(MonitorStatusTimelineService, "onBeforeCreate", {
165
+ data: {
166
+ monitorId: MONITOR_ID,
167
+ projectId: PROJECT_ID,
168
+ monitorStatusId: MONITOR_STATUS_ID,
169
+ },
170
+ props: {},
171
+ });
172
+
173
+ expect(validatorCalls).toHaveLength(1);
174
+ expect(validatorCalls[0]!.subject).toBe("monitor status timeline");
175
+ expect(validatorCalls[0]!.references).toHaveLength(1);
176
+ expect(validatorCalls[0]!.references[0]!.modelName).toBe(
177
+ "Monitor Status",
178
+ );
179
+ expect(validatorCalls[0]!.references[0]!.id).toBe(MONITOR_STATUS_ID);
180
+ expect(serviceNameOf(validatorCalls[0]!.references[0]!.service)).toBe(
181
+ "MonitorStatusService",
182
+ );
183
+ expect(validatorCalls[0]!.projectId).toBe(PROJECT_ID);
184
+ /*
185
+ * The whole point is that the record must EXIST. Passing
186
+ * `mustExist: false` here would opt this column out of issue #3039's fix
187
+ * while every other assertion in this test still passed, so pin that the
188
+ * service does not — the validator's default is strict.
189
+ */
190
+ expect(validatorCalls[0]!.references[0]!.mustExist).not.toBe(false);
191
+ });
192
+
193
+ test("create is rejected when the monitor status does not exist", async () => {
194
+ spyOnValidator(true);
195
+ jest
196
+ .spyOn(MonitorStatusTimelineService, "findOneBy")
197
+ .mockResolvedValue(null as never);
198
+
199
+ await expect(
200
+ callHook(MonitorStatusTimelineService, "onBeforeCreate", {
201
+ data: {
202
+ monitorId: MONITOR_ID,
203
+ projectId: PROJECT_ID,
204
+ monitorStatusId: MONITOR_STATUS_ID,
205
+ },
206
+ props: {},
207
+ }),
208
+ ).rejects.toThrow("do not exist");
209
+ });
210
+
211
+ test("create falls back to the write's tenant when the row carries no project", async () => {
212
+ spyOnValidator();
213
+ jest
214
+ .spyOn(MonitorStatusTimelineService, "findOneBy")
215
+ .mockResolvedValue(null as never);
216
+
217
+ await callHook(MonitorStatusTimelineService, "onBeforeCreate", {
218
+ data: {
219
+ monitorId: MONITOR_ID,
220
+ monitorStatusId: MONITOR_STATUS_ID,
221
+ },
222
+ props: { tenantId: PROJECT_ID },
223
+ });
224
+
225
+ expect(validatorCalls[0]!.projectId).toBe(PROJECT_ID);
226
+ });
227
+ });
228
+
229
+ describe("IncidentStateTimelineService", () => {
230
+ test("create checks the incident state against IncidentStateService", async () => {
231
+ spyOnValidator();
232
+ jest
233
+ .spyOn(IncidentStateTimelineService, "findOneBy")
234
+ .mockResolvedValue(null as never);
235
+
236
+ await callHook(IncidentStateTimelineService, "onBeforeCreate", {
237
+ data: {
238
+ incidentId: MONITOR_ID,
239
+ projectId: PROJECT_ID,
240
+ incidentStateId: INCIDENT_STATE_ID,
241
+ },
242
+ props: {},
243
+ });
244
+
245
+ expect(validatorCalls).toHaveLength(1);
246
+ expect(validatorCalls[0]!.subject).toBe("incident state timeline");
247
+ expect(validatorCalls[0]!.references[0]!.modelName).toBe(
248
+ "Incident State",
249
+ );
250
+ expect(validatorCalls[0]!.references[0]!.id).toBe(INCIDENT_STATE_ID);
251
+ expect(serviceNameOf(validatorCalls[0]!.references[0]!.service)).toBe(
252
+ "IncidentStateService",
253
+ );
254
+ expect(validatorCalls[0]!.references[0]!.mustExist).not.toBe(false);
255
+ });
256
+
257
+ test("create is rejected when the incident state does not exist", async () => {
258
+ spyOnValidator(true);
259
+ jest
260
+ .spyOn(IncidentStateTimelineService, "findOneBy")
261
+ .mockResolvedValue(null as never);
262
+
263
+ await expect(
264
+ callHook(IncidentStateTimelineService, "onBeforeCreate", {
265
+ data: {
266
+ incidentId: MONITOR_ID,
267
+ projectId: PROJECT_ID,
268
+ incidentStateId: INCIDENT_STATE_ID,
269
+ },
270
+ props: {},
271
+ }),
272
+ ).rejects.toThrow("do not exist");
273
+ });
274
+ });
275
+
276
+ describe("AlertStateTimelineService", () => {
277
+ test("create checks the alert state against AlertStateService", async () => {
278
+ spyOnValidator();
279
+ jest
280
+ .spyOn(AlertStateTimelineService, "findOneBy")
281
+ .mockResolvedValue(null as never);
282
+
283
+ await callHook(AlertStateTimelineService, "onBeforeCreate", {
284
+ data: {
285
+ alertId: MONITOR_ID,
286
+ projectId: PROJECT_ID,
287
+ alertStateId: ALERT_STATE_ID,
288
+ },
289
+ props: {},
290
+ });
291
+
292
+ expect(validatorCalls).toHaveLength(1);
293
+ expect(validatorCalls[0]!.subject).toBe("alert state timeline");
294
+ expect(validatorCalls[0]!.references[0]!.modelName).toBe("Alert State");
295
+ expect(validatorCalls[0]!.references[0]!.id).toBe(ALERT_STATE_ID);
296
+ expect(serviceNameOf(validatorCalls[0]!.references[0]!.service)).toBe(
297
+ "AlertStateService",
298
+ );
299
+ expect(validatorCalls[0]!.references[0]!.mustExist).not.toBe(false);
300
+ });
301
+
302
+ test("create is rejected when the alert state does not exist", async () => {
303
+ spyOnValidator(true);
304
+ jest
305
+ .spyOn(AlertStateTimelineService, "findOneBy")
306
+ .mockResolvedValue(null as never);
307
+
308
+ await expect(
309
+ callHook(AlertStateTimelineService, "onBeforeCreate", {
310
+ data: {
311
+ alertId: MONITOR_ID,
312
+ projectId: PROJECT_ID,
313
+ alertStateId: ALERT_STATE_ID,
314
+ },
315
+ props: {},
316
+ }),
317
+ ).rejects.toThrow("do not exist");
318
+ });
319
+ });
320
+
321
+ describe("ScheduledMaintenanceStateTimelineService", () => {
322
+ test("create checks the state against ScheduledMaintenanceStateService", async () => {
323
+ spyOnValidator();
324
+ jest
325
+ .spyOn(ScheduledMaintenanceStateTimelineService, "findOneBy")
326
+ .mockResolvedValue(null as never);
327
+
328
+ await callHook(
329
+ ScheduledMaintenanceStateTimelineService,
330
+ "onBeforeCreate",
331
+ {
332
+ data: {
333
+ scheduledMaintenanceId: MONITOR_ID,
334
+ projectId: PROJECT_ID,
335
+ scheduledMaintenanceStateId: SCHEDULED_MAINTENANCE_STATE_ID,
336
+ },
337
+ props: {},
338
+ },
339
+ );
340
+
341
+ expect(validatorCalls).toHaveLength(1);
342
+ expect(validatorCalls[0]!.subject).toBe(
343
+ "scheduled maintenance state timeline",
344
+ );
345
+ expect(validatorCalls[0]!.references[0]!.modelName).toBe(
346
+ "Scheduled Maintenance State",
347
+ );
348
+ expect(validatorCalls[0]!.references[0]!.id).toBe(
349
+ SCHEDULED_MAINTENANCE_STATE_ID,
350
+ );
351
+ expect(serviceNameOf(validatorCalls[0]!.references[0]!.service)).toBe(
352
+ "ScheduledMaintenanceStateService",
353
+ );
354
+ expect(validatorCalls[0]!.references[0]!.mustExist).not.toBe(false);
355
+ });
356
+
357
+ test("create is rejected when the state does not exist", async () => {
358
+ spyOnValidator(true);
359
+ jest
360
+ .spyOn(ScheduledMaintenanceStateTimelineService, "findOneBy")
361
+ .mockResolvedValue(null as never);
362
+
363
+ await expect(
364
+ callHook(ScheduledMaintenanceStateTimelineService, "onBeforeCreate", {
365
+ data: {
366
+ scheduledMaintenanceId: MONITOR_ID,
367
+ projectId: PROJECT_ID,
368
+ scheduledMaintenanceStateId: SCHEDULED_MAINTENANCE_STATE_ID,
369
+ },
370
+ props: {},
371
+ }),
372
+ ).rejects.toThrow("do not exist");
373
+ });
374
+ });
375
+
376
+ describe("MonitorService passes the stored monitorSteps to the validator", () => {
377
+ /*
378
+ * Without this the guard would refuse every edit of a monitor that already
379
+ * holds a dangling id — including renaming it — over a field the user never
380
+ * touched and cannot see is wrong.
381
+ */
382
+ function spyOnStepsValidator(): Array<StepsValidatorCall> {
383
+ const calls: Array<StepsValidatorCall> = [];
384
+
385
+ jest
386
+ .spyOn(
387
+ MonitorStepsProjectValidator,
388
+ "validateMonitorStepsBelongToProject",
389
+ )
390
+ .mockImplementation((async (
391
+ data: StepsValidatorCall,
392
+ ): Promise<void> => {
393
+ calls.push({
394
+ monitorSteps: data.monitorSteps,
395
+ projectId: data.projectId,
396
+ alreadyStoredMonitorSteps: data.alreadyStoredMonitorSteps,
397
+ });
398
+ }) as never);
399
+
400
+ return calls;
401
+ }
402
+
403
+ test("update hands over what each matched monitor currently stores", async () => {
404
+ spyOnValidator();
405
+ const calls: Array<StepsValidatorCall> = spyOnStepsValidator();
406
+
407
+ const stored: JSONObject = stepsReferencing(MONITOR_STATUS_ID.toString());
408
+ const incoming: JSONObject = stepsReferencing(
409
+ MONITOR_STATUS_ID.toString(),
410
+ );
411
+
412
+ const monitor: Monitor = new Monitor();
413
+ monitor.projectId = PROJECT_ID;
414
+ monitor.monitorSteps = stored as never;
415
+
416
+ jest
417
+ .spyOn(MonitorService, "findBy")
418
+ .mockResolvedValue([monitor] as never);
419
+
420
+ await callHook(MonitorService, "onBeforeUpdate", {
421
+ data: { monitorSteps: incoming },
422
+ query: {},
423
+ props: { tenantId: PROJECT_ID },
424
+ });
425
+
426
+ expect(calls).toHaveLength(1);
427
+ expect(calls[0]!.projectId).toBe(PROJECT_ID);
428
+ /*
429
+ * Identity, not equality, on both blobs: `stored` and `incoming` are
430
+ * structurally identical, so a swap of the two arguments — validating the
431
+ * incoming payload as if it were what was already saved, which would
432
+ * exempt every id from the check — passes a deep-equality assertion.
433
+ */
434
+ expect(calls[0]!.monitorSteps).toBe(incoming);
435
+ expect(calls[0]!.alreadyStoredMonitorSteps).toBe(stored);
436
+ });
437
+
438
+ test("update validates each matched monitor against its own project", async () => {
439
+ /*
440
+ * A root/API update does not always carry a tenantId, so the project has
441
+ * to come from the monitor the query actually matched.
442
+ */
443
+ spyOnValidator();
444
+ const calls: Array<StepsValidatorCall> = spyOnStepsValidator();
445
+
446
+ const first: Monitor = new Monitor();
447
+ first.projectId = PROJECT_ID;
448
+ const second: Monitor = new Monitor();
449
+ second.projectId = MONITOR_ID;
450
+
451
+ jest
452
+ .spyOn(MonitorService, "findBy")
453
+ .mockResolvedValue([first, second] as never);
454
+
455
+ await callHook(MonitorService, "onBeforeUpdate", {
456
+ data: { monitorSteps: stepsReferencing(MONITOR_STATUS_ID.toString()) },
457
+ query: {},
458
+ props: {},
459
+ });
460
+
461
+ expect(
462
+ calls.map((call: { projectId: ObjectID | undefined }) => {
463
+ return call.projectId;
464
+ }),
465
+ ).toEqual([PROJECT_ID, MONITOR_ID]);
466
+ });
467
+
468
+ test("create grandfathers nothing, because a create introduces every id it carries", async () => {
469
+ spyOnValidator();
470
+ const calls: Array<StepsValidatorCall> = spyOnStepsValidator();
471
+
472
+ /*
473
+ * onBeforeCreate resolves the project's operational status for the new
474
+ * monitor after validating, and that lookup would otherwise hit the
475
+ * database. The plan lookup runs before it and only when billing is
476
+ * enabled, which depends on the environment this suite is run in.
477
+ */
478
+ jest.spyOn(ProjectService, "getCurrentPlan").mockResolvedValue({
479
+ plan: null,
480
+ isSubscriptionUnpaid: false,
481
+ } as never);
482
+ jest
483
+ .spyOn(MonitorStatusService, "findOneBy")
484
+ .mockResolvedValue({ id: MONITOR_STATUS_ID } as never);
485
+ jest
486
+ .spyOn(MonitorService, "addDefaultProbesToMonitor")
487
+ .mockResolvedValue(undefined as never);
488
+
489
+ await callHook(MonitorService, "onBeforeCreate", {
490
+ data: {
491
+ monitorType: "Manual",
492
+ monitorSteps: stepsReferencing(MONITOR_STATUS_ID.toString()),
493
+ },
494
+ props: { tenantId: PROJECT_ID },
495
+ });
496
+
497
+ expect(calls).toHaveLength(1);
498
+ expect(calls[0]!.alreadyStoredMonitorSteps).toBeUndefined();
499
+ });
500
+ });
501
+
502
+ describe("MonitorTemplateService", () => {
503
+ /*
504
+ * A template's monitorSteps reaches real monitors through "create monitor
505
+ * from template" and through syncLinkedMonitors, which pushes the blob onto
506
+ * every linked monitor. Validating it here is what makes the error land on
507
+ * the template the bad id was typed into rather than on a monitor sync days
508
+ * later.
509
+ */
510
+ test("create validates the template's monitorSteps", async () => {
511
+ const calls: Array<{ projectId: ObjectID | undefined }> = [];
512
+
513
+ jest
514
+ .spyOn(
515
+ MonitorStepsProjectValidator,
516
+ "validateMonitorStepsBelongToProject",
517
+ )
518
+ .mockImplementation((async (data: {
519
+ projectId: ObjectID | undefined;
520
+ }): Promise<void> => {
521
+ calls.push({ projectId: data.projectId });
522
+ }) as never);
523
+
524
+ await callHook(MonitorTemplateService, "onBeforeCreate", {
525
+ data: { monitorSteps: stepsReferencing(MONITOR_STATUS_ID.toString()) },
526
+ props: { tenantId: PROJECT_ID },
527
+ });
528
+
529
+ expect(calls).toEqual([{ projectId: PROJECT_ID }]);
530
+ });
531
+
532
+ test("create is rejected when the template names a status that does not exist", async () => {
533
+ jest
534
+ .spyOn(
535
+ MonitorStepsProjectValidator,
536
+ "validateMonitorStepsBelongToProject",
537
+ )
538
+ .mockImplementation((async (): Promise<void> => {
539
+ throw new Error("references records that do not exist");
540
+ }) as never);
541
+
542
+ await expect(
543
+ callHook(MonitorTemplateService, "onBeforeCreate", {
544
+ data: {
545
+ monitorSteps: stepsReferencing(MONITOR_STATUS_ID.toString()),
546
+ },
547
+ props: { tenantId: PROJECT_ID },
548
+ }),
549
+ ).rejects.toThrow("do not exist");
550
+ });
551
+
552
+ test("update hands over what the template currently stores", async () => {
553
+ const calls: Array<{ alreadyStoredMonitorSteps: unknown }> = [];
554
+
555
+ jest
556
+ .spyOn(
557
+ MonitorStepsProjectValidator,
558
+ "validateMonitorStepsBelongToProject",
559
+ )
560
+ .mockImplementation((async (data: {
561
+ alreadyStoredMonitorSteps: unknown;
562
+ }): Promise<void> => {
563
+ calls.push({
564
+ alreadyStoredMonitorSteps: data.alreadyStoredMonitorSteps,
565
+ });
566
+ }) as never);
567
+
568
+ const stored: JSONObject = stepsReferencing(MONITOR_STATUS_ID.toString());
569
+
570
+ const template: MonitorTemplate = new MonitorTemplate();
571
+ template.projectId = PROJECT_ID;
572
+ template.monitorSteps = stored as never;
573
+
574
+ jest
575
+ .spyOn(MonitorTemplateService, "findBy")
576
+ .mockResolvedValue([template] as never);
577
+
578
+ await callHook(MonitorTemplateService, "onBeforeUpdate", {
579
+ data: { monitorSteps: stepsReferencing(MONITOR_STATUS_ID.toString()) },
580
+ query: {},
581
+ props: { tenantId: PROJECT_ID },
582
+ });
583
+
584
+ expect(calls).toHaveLength(1);
585
+ expect(calls[0]!.alreadyStoredMonitorSteps).toBe(stored);
586
+ });
587
+
588
+ test("update looks nothing up when monitorSteps is not being written", async () => {
589
+ const calls: Array<unknown> = [];
590
+
591
+ jest
592
+ .spyOn(
593
+ MonitorStepsProjectValidator,
594
+ "validateMonitorStepsBelongToProject",
595
+ )
596
+ .mockImplementation((async (): Promise<void> => {
597
+ calls.push(true);
598
+ }) as never);
599
+
600
+ const findBy: unknown = jest
601
+ .spyOn(MonitorTemplateService, "findBy")
602
+ .mockResolvedValue([] as never);
603
+
604
+ await callHook(MonitorTemplateService, "onBeforeUpdate", {
605
+ data: { description: "renamed" },
606
+ query: {},
607
+ props: { tenantId: PROJECT_ID },
608
+ });
609
+
610
+ expect(calls).toHaveLength(0);
611
+ expect(findBy).not.toHaveBeenCalled();
612
+ });
613
+ });
614
+ });
@@ -71,6 +71,29 @@ jest.mock("../../../Server/Utils/Logger", () => {
71
71
  };
72
72
  });
73
73
 
74
+ /*
75
+ * Past the fast-path, the util screens the criteria's monitor status against
76
+ * the monitor's project before writing (issue #3039 — a status that no longer
77
+ * exists used to reach Postgres as a raw foreign key violation and fail the
78
+ * whole ingest run). That is a database round trip; answer "usable" here so
79
+ * these cases stay about the fast-path. The skip behaviour itself is pinned by
80
+ * Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts, which
81
+ * also asserts this lookup is NOT reached on the fast-path.
82
+ */
83
+ jest.mock(
84
+ "../../../Server/Utils/Database/ProjectScopedReferenceValidator",
85
+ () => {
86
+ return {
87
+ __esModule: true,
88
+ default: {
89
+ isUsableInProject: () => {
90
+ return Promise.resolve(true);
91
+ },
92
+ },
93
+ };
94
+ },
95
+ );
96
+
74
97
  import MonitorStatusTimelineUtil from "../../../Server/Utils/Monitor/MonitorStatusTimeline";
75
98
  import {
76
99
  MONITOR_STATUS_SAME_AS_PREVIOUS_ERROR_MESSAGE,