@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
@@ -3,6 +3,7 @@ import MonitorGroup from "./MonitorGroup";
3
3
  import Project from "./Project";
4
4
  import StatusPage from "./StatusPage";
5
5
  import StatusPageGroup from "./StatusPageGroup";
6
+ import StatusPageMonitorRule from "./StatusPageMonitorRule";
6
7
  import User from "./User";
7
8
  import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
8
9
  import Route from "../../Types/API/Route";
@@ -520,6 +521,78 @@ export default class StatusPageResource extends BaseModel {
520
521
  })
521
522
  public statusPageGroupId?: ObjectID = undefined;
522
523
 
524
+ /*
525
+ * Set only on resources a StatusPageMonitorRule created. It is what lets the
526
+ * rule engine undo its own work without ever touching a resource a human
527
+ * added by hand, so it is deliberately not writable through the API - the
528
+ * engine sets it as root.
529
+ */
530
+ @ColumnAccessControl({
531
+ create: [],
532
+ read: [
533
+ Permission.ProjectOwner,
534
+ Permission.ProjectAdmin,
535
+ Permission.ProjectMember,
536
+ Permission.Viewer,
537
+ Permission.StatusPageAdmin,
538
+ Permission.StatusPageMember,
539
+ Permission.StatusPageViewer,
540
+ Permission.ReadStatusPageResource,
541
+ ],
542
+ update: [],
543
+ })
544
+ @TableColumn({
545
+ manyToOneRelationColumn: "statusPageMonitorRuleId",
546
+ type: TableColumnType.Entity,
547
+ modelType: StatusPageMonitorRule,
548
+ title: "Status Page Monitor Rule",
549
+ description:
550
+ "Rule that added this resource to the status page, if it was added by a rule instead of by hand",
551
+ })
552
+ @ManyToOne(
553
+ () => {
554
+ return StatusPageMonitorRule;
555
+ },
556
+ {
557
+ eager: false,
558
+ nullable: true,
559
+ onDelete: "CASCADE",
560
+ orphanedRowAction: "nullify",
561
+ },
562
+ )
563
+ @JoinColumn({ name: "statusPageMonitorRuleId" })
564
+ public statusPageMonitorRule?: StatusPageMonitorRule = undefined;
565
+
566
+ @ColumnAccessControl({
567
+ create: [],
568
+ read: [
569
+ Permission.ProjectOwner,
570
+ Permission.ProjectAdmin,
571
+ Permission.ProjectMember,
572
+ Permission.Viewer,
573
+ Permission.StatusPageAdmin,
574
+ Permission.StatusPageMember,
575
+ Permission.StatusPageViewer,
576
+ Permission.ReadStatusPageResource,
577
+ ],
578
+ update: [],
579
+ })
580
+ @Index()
581
+ @TableColumn({
582
+ type: TableColumnType.ObjectID,
583
+ required: false,
584
+ title: "Status Page Monitor Rule ID",
585
+ description:
586
+ "ID of the rule that added this resource, if it was added by a rule instead of by hand",
587
+ example: "d0e1f2a3-b4c5-4678-9abc-def012345678",
588
+ })
589
+ @Column({
590
+ type: ColumnType.ObjectID,
591
+ nullable: true,
592
+ transformer: ObjectID.getDatabaseTransformer(),
593
+ })
594
+ public statusPageMonitorRuleId?: ObjectID = undefined;
595
+
523
596
  @ColumnAccessControl({
524
597
  create: [
525
598
  Permission.ProjectOwner,
@@ -7,6 +7,7 @@ import TableAccessControl from "../../Types/Database/AccessControl/TableAccessCo
7
7
  import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
8
8
  import ColumnLength from "../../Types/Database/ColumnLength";
9
9
  import ColumnType from "../../Types/Database/ColumnType";
10
+ import { getBigIntDatabaseTransformer } from "../../Types/Database/BigIntColumnTransformer";
10
11
  import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
11
12
  import EnableDocumentation from "../../Types/Database/EnableDocumentation";
12
13
  import TableColumn from "../../Types/Database/TableColumn";
@@ -436,26 +437,14 @@ export default class TraceDropFilter extends BaseModel {
436
437
  type: ColumnType.BigPositiveNumber,
437
438
  nullable: false,
438
439
  default: 0,
439
- transformer: {
440
- to: (value: number | null | undefined): string | null => {
441
- if (value === null || value === undefined) {
442
- return null;
443
- }
444
- return Math.trunc(value).toString();
445
- },
446
- /*
447
- * Postgres bigint comes back from the driver as a string. The UI
448
- * renders this as a number, so normalize here rather than making
449
- * every reader remember.
450
- */
451
- from: (value: string | null | undefined): number | null => {
452
- if (value === null || value === undefined) {
453
- return null;
454
- }
455
- const parsed: number = parseInt(value, 10);
456
- return isNaN(parsed) ? null : parsed;
457
- },
458
- },
440
+ /*
441
+ * Shared transformer, not an inline one: it has to preserve `undefined`
442
+ * so this NOT NULL column's DEFAULT 0 is what a create actually gets.
443
+ * An inline transformer that mapped `undefined` to `null` made every
444
+ * drop-filter insert fail the not-null constraint. See
445
+ * Types/Database/BigIntColumnTransformer.
446
+ */
447
+ transformer: getBigIntDatabaseTransformer(),
459
448
  })
460
449
  public droppedCount?: number = undefined;
461
450
 
@@ -22,6 +22,7 @@ import {
22
22
  LIMIT_PER_PROJECT,
23
23
  } from "../../Types/Database/LimitMax";
24
24
  import PartialEntity from "../../Types/Database/PartialEntity";
25
+ import { coerceNumericColumnsInJSON } from "../../Types/Database/NumericColumnValue";
25
26
  import BadDataException from "../../Types/Exception/BadDataException";
26
27
  import BadRequestException from "../../Types/Exception/BadRequestException";
27
28
  import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
@@ -418,8 +419,18 @@ export default class BaseAPI<
418
419
  );
419
420
  }
420
421
 
421
- const item: PartialEntity<TBaseModel> = JSONFunctions.deserialize(
422
- dataInBody as JSONObject,
422
+ /*
423
+ * Coerced the same way createItem's BaseModel.fromJSON coerces, so a
424
+ * PATCH and a POST agree on what a number column holds. The dashboard's
425
+ * number fields are `<input type="number">`, whose value is a string,
426
+ * and the save-time hooks read these values before Postgres would
427
+ * coerce them — a sample percentage of "10" was rejected as "not a
428
+ * number" on a form that had one filled in.
429
+ * github.com/OneUptime/oneuptime/issues/3027
430
+ */
431
+ const item: PartialEntity<TBaseModel> = coerceNumericColumnsInJSON(
432
+ JSONFunctions.deserialize(dataInBody as JSONObject),
433
+ new this.entityType(),
423
434
  ) as PartialEntity<TBaseModel>;
424
435
 
425
436
  delete (item as any)["_id"];
@@ -50,6 +50,7 @@ import PodmanResource from "../../Models/DatabaseModels/PodmanResource";
50
50
  import ProxmoxResource from "../../Models/DatabaseModels/ProxmoxResource";
51
51
  import CephResource from "../../Models/DatabaseModels/CephResource";
52
52
  import DockerSwarmResource from "../../Models/DatabaseModels/DockerSwarmResource";
53
+ import NetworkSite from "../../Models/DatabaseModels/NetworkSite";
53
54
  import Span from "../../Models/AnalyticsModels/Span";
54
55
  import Log from "../../Models/AnalyticsModels/Log";
55
56
  import IncidentService from "../Services/IncidentService";
@@ -64,6 +65,7 @@ import PodmanResourceService from "../Services/PodmanResourceService";
64
65
  import ProxmoxResourceService from "../Services/ProxmoxResourceService";
65
66
  import CephResourceService from "../Services/CephResourceService";
66
67
  import DockerSwarmResourceService from "../Services/DockerSwarmResourceService";
68
+ import NetworkSiteService from "../Services/NetworkSiteService";
67
69
  import SpanService from "../Services/SpanService";
68
70
  import LogService from "../Services/LogService";
69
71
  import DashboardComponentType from "../../Types/Dashboard/DashboardComponentType";
@@ -165,6 +167,43 @@ const PUBLIC_DASHBOARD_RESOURCES: Record<
165
167
  currentMonitorStatus: { name: true, color: true },
166
168
  },
167
169
  },
170
+ "network-site": {
171
+ modelType: NetworkSite,
172
+ service: NetworkSiteService,
173
+ widgets: {
174
+ [DashboardComponentType.NetworkMap]: null,
175
+ },
176
+ /*
177
+ * The map filters by site type and by whether the rolled-up status is
178
+ * operational — nothing else. Notably NOT `parentSiteId` or
179
+ * `materializedPath`: the widget draws a flat set of pins rather than
180
+ * walking a hierarchy, so exposing the tree shape here would hand an
181
+ * anonymous viewer a structure the map never shows them.
182
+ */
183
+ allowedQueryKeys: [
184
+ "networkSiteTypeId",
185
+ "currentMonitorStatus",
186
+ "currentMonitorStatusId",
187
+ ],
188
+ select: {
189
+ _id: true,
190
+ name: true,
191
+ /*
192
+ * The type NAME comes from the networkSiteType relation. NetworkSite
193
+ * .siteType is the deprecated pre-lookup-table string and is dropped
194
+ * in a follow-up PR, so it must not be read from new code.
195
+ */
196
+ networkSiteType: { name: true },
197
+ latitude: true,
198
+ longitude: true,
199
+ currentMonitorStatus: {
200
+ name: true,
201
+ color: true,
202
+ priority: true,
203
+ isOperationalState: true,
204
+ },
205
+ },
206
+ },
168
207
  host: {
169
208
  modelType: Host,
170
209
  service: HostService,
@@ -167,13 +167,43 @@ export const PostgresStatementTimeoutMs: number = parseInt(
167
167
  10,
168
168
  );
169
169
 
170
+ /*
171
+ * Postgres-side lock timeout (ms). Caps how long a statement will WAIT for a
172
+ * row/table lock before giving up — distinct from statement_timeout, which
173
+ * caps total execution.
174
+ *
175
+ * Without it, contention on a hot row degrades into a strictly-ordered queue:
176
+ * every waiter pins a backend for the sum of everyone ahead of it. That is the
177
+ * row-lock convoy that took production down — 892 connections parked on locks,
178
+ * the tail waiting 3.7 hours — and no amount of database capacity changes it,
179
+ * because the queue itself is the failure. A short lock_timeout converts an
180
+ * unbounded wait into a fast, retryable error.
181
+ *
182
+ * Must stay well below statement_timeout so a lock wait surfaces as
183
+ * `lock_not_available` (55P03) rather than a generic statement timeout — the
184
+ * two want very different handling.
185
+ */
186
+ export const PostgresLockTimeoutMs: number = parseInt(
187
+ process.env["DATABASE_LOCK_TIMEOUT_MS"] || "3000",
188
+ 10,
189
+ );
190
+
170
191
  /*
171
192
  * Node-postgres client-side query timeout (ms). Belt-and-braces for the
172
193
  * server-side statement_timeout — fires even if the connection has gone
173
194
  * silent or the server-side timeout doesn't kick in.
195
+ *
196
+ * Deliberately LONGER than statement_timeout. These two used to be equal, and
197
+ * because the client timer starts before the packet even reaches the backend,
198
+ * the client always won by a round trip — so the app never observed Postgres's
199
+ * real SQLSTATE and, worse, the client-side timeout only ABANDONS the query:
200
+ * the backend keeps running and keeps its place in the lock queue. Letting the
201
+ * server win means contention is cancelled server-side instead of accumulating
202
+ * invisibly behind a pooler.
174
203
  */
175
204
  export const PostgresQueryTimeoutMs: number = parseInt(
176
- process.env["DATABASE_QUERY_TIMEOUT_MS"] || "30000",
205
+ process.env["DATABASE_QUERY_TIMEOUT_MS"] ||
206
+ String(PostgresStatementTimeoutMs + 5000),
177
207
  10,
178
208
  );
179
209
 
@@ -176,6 +176,121 @@ export default abstract class GlobalCache {
176
176
  await client.set(`${namespace}-${key}`, value, "EX", expiresInSeconds);
177
177
  }
178
178
 
179
+ /*
180
+ * Atomic acquire-once fence: SET ... EX ... NX. Returns true ONLY for the
181
+ * caller that created the key; every concurrent caller gets false until it
182
+ * expires.
183
+ *
184
+ * This exists because `getString()` followed by `setString()` is a
185
+ * check-then-act race, and at ingest concurrency that race is not
186
+ * theoretical — it is the whole problem. When N workers resolve the same
187
+ * row in the same instant they ALL read a miss and they ALL proceed, so a
188
+ * fence meant to admit one writer per window admits N. With 100 worker pods
189
+ * that turned a once-a-minute heartbeat into thousands of simultaneous
190
+ * UPDATEs against the same handful of rows, and the resulting row-lock
191
+ * convoy starved the Postgres connection pool (some statements queued for
192
+ * hours). Redis evaluates SET NX atomically, so exactly one caller wins no
193
+ * matter how many arrive together.
194
+ *
195
+ * Fence keys should carry the TTL jitter from `withJitter()` — see there for
196
+ * why synchronized expiry re-creates the herd this is meant to prevent.
197
+ */
198
+ @CaptureSpan()
199
+ public static async setStringIfNotExists(
200
+ namespace: string,
201
+ key: string,
202
+ value: string,
203
+ options?: CacheSetOptions,
204
+ ): Promise<boolean> {
205
+ const client: ClientType | null = Redis.getClient();
206
+
207
+ if (!client || !Redis.isConnected()) {
208
+ throw new DatabaseNotConnectedException("Cache is not connected");
209
+ }
210
+
211
+ const expiresInSeconds: number =
212
+ options?.expiresInSeconds ?? OneUptimeDate.getSecondsInDays(30);
213
+
214
+ const result: string | null = await client.set(
215
+ `${namespace}-${key}`,
216
+ value,
217
+ "EX",
218
+ expiresInSeconds,
219
+ "NX",
220
+ );
221
+
222
+ /*
223
+ * ioredis resolves to "OK" when the key was created and to null when it
224
+ * already existed. Anything else (a driver/protocol change) is treated as
225
+ * "did not acquire" — losing the fence is safe, winning it wrongly is not.
226
+ */
227
+ return result === "OK";
228
+ }
229
+
230
+ /*
231
+ * Atomic compare-and-claim: claim the window unless the key ALREADY holds
232
+ * `value`. Returns true only for the caller that claimed it.
233
+ *
234
+ * `setStringIfNotExists` is the right primitive for a presence fence and the
235
+ * wrong one for a fingerprint throttle. Plain NX fails whenever the key
236
+ * exists — including when it holds a STALE fingerprint — so a genuinely
237
+ * changed payload (a new service.version after a deploy, a changed host IP)
238
+ * would be suppressed for the whole window and nobody would persist it. This
239
+ * keeps the bust-on-change behaviour that callers depend on while collapsing
240
+ * the GET-then-SET race into one atomic evaluation.
241
+ *
242
+ * The key is passed as KEYS[1] rather than inlined into the script body so
243
+ * the script stays correct on Redis Cluster, which routes by declared keys.
244
+ */
245
+ @CaptureSpan()
246
+ public static async setStringIfChanged(
247
+ namespace: string,
248
+ key: string,
249
+ value: string,
250
+ options?: CacheSetOptions,
251
+ ): Promise<boolean> {
252
+ const client: ClientType | null = Redis.getClient();
253
+
254
+ if (!client || !Redis.isConnected()) {
255
+ throw new DatabaseNotConnectedException("Cache is not connected");
256
+ }
257
+
258
+ const expiresInSeconds: number =
259
+ options?.expiresInSeconds ?? OneUptimeDate.getSecondsInDays(30);
260
+
261
+ const result: unknown = await client.eval(
262
+ "if redis.call('GET', KEYS[1]) == ARGV[1] then return 0 " +
263
+ "else redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]) return 1 end",
264
+ 1,
265
+ `${namespace}-${key}`,
266
+ value,
267
+ String(expiresInSeconds),
268
+ );
269
+
270
+ return result === 1;
271
+ }
272
+
273
+ /*
274
+ * Spread a fence TTL over [ttl, ttl + 25%].
275
+ *
276
+ * A fixed TTL makes every fence for a fleet of rows expire in lockstep once
277
+ * their writes have been aligned by a common event — a deploy, a worker
278
+ * scale-up, a Redis restart. The window then reopens for thousands of rows
279
+ * in the same second and the herd re-forms on a one-minute period. Jitter
280
+ * breaks that alignment permanently: the fences drift apart after the first
281
+ * window and stay apart.
282
+ *
283
+ * The upper bound stays well inside the 15-minute disconnection sweep, so a
284
+ * jittered heartbeat can never make a live resource look disconnected.
285
+ */
286
+ public static withJitter(expiresInSeconds: number): number {
287
+ if (expiresInSeconds <= 0) {
288
+ return expiresInSeconds;
289
+ }
290
+
291
+ return Math.ceil(expiresInSeconds * (1 + Math.random() * 0.25));
292
+ }
293
+
179
294
  @CaptureSpan()
180
295
  public static async deleteKey(namespace: string, key: string): Promise<void> {
181
296
  const client: ClientType | null = Redis.getClient();
@@ -10,6 +10,7 @@ import {
10
10
  DatabaseUsername,
11
11
  MaxPostgresConnections,
12
12
  PostgresConnectionAcquireTimeoutMs,
13
+ PostgresLockTimeoutMs,
13
14
  PostgresIdleInTransactionTimeoutMs,
14
15
  PostgresIdleSessionTimeoutMs,
15
16
  PostgresIdleTimeoutMs,
@@ -61,6 +62,21 @@ const dataSourceOptions: DataSourceOptions = {
61
62
  statement_timeout: PostgresStatementTimeoutMs,
62
63
  query_timeout: PostgresQueryTimeoutMs,
63
64
  idle_in_transaction_session_timeout: PostgresIdleInTransactionTimeoutMs,
65
+ /*
66
+ * Bound how long a statement WAITS for a lock, so contention on a hot row
67
+ * fails fast instead of forming a queue (see PostgresLockTimeoutMs).
68
+ *
69
+ * Excluded on the migration path, and that exclusion is load-bearing:
70
+ * App/Migrate.ts loads these same options and connects DIRECTLY to the
71
+ * backend (bypassing any pooler), where startup parameters really do take
72
+ * effect. A 3s lock_timeout there would abort ACCESS EXCLUSIVE DDL on any
73
+ * table with live traffic — only the two migrations that set their own
74
+ * `SET LOCAL lock_timeout` expect to fail that way; the rest must be free
75
+ * to wait.
76
+ */
77
+ ...(PostgresLockTimeoutMs > 0 && !RunDatabaseMigrationsOnBoot
78
+ ? { lock_timeout: PostgresLockTimeoutMs }
79
+ : {}),
64
80
  /*
65
81
  * Detect dead TCP peers (ungraceful client exit / network partition) so
66
82
  * orphaned server-side connections get torn down instead of lingering
@@ -0,0 +1,123 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddStatusPageMonitorRule1786005052769
4
+ implements MigrationInterface
5
+ {
6
+ public name = "AddStatusPageMonitorRule1786005052769";
7
+
8
+ public async up(queryRunner: QueryRunner): Promise<void> {
9
+ await queryRunner.query(
10
+ `CREATE TABLE "StatusPageMonitorRule" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "statusPageId" uuid NOT NULL, "name" character varying(100) NOT NULL, "description" character varying(500), "isEnabled" boolean NOT NULL DEFAULT true, "monitorNamePattern" character varying(500), "monitorDescriptionPattern" character varying(500), "statusPageGroupId" uuid, "showCurrentStatus" boolean NOT NULL DEFAULT true, "showUptimePercent" boolean NOT NULL DEFAULT true, "uptimePercentPrecision" character varying, "showStatusHistoryChart" boolean NOT NULL DEFAULT true, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_f8d44792805d19b9a80921b6aa6" PRIMARY KEY ("_id"))`,
11
+ );
12
+ await queryRunner.query(
13
+ `CREATE INDEX "IDX_1dfd95ad32f7b687ef4d7ba591" ON "StatusPageMonitorRule" ("projectId") `,
14
+ );
15
+ await queryRunner.query(
16
+ `CREATE INDEX "IDX_473317deaa55a8305a047e4098" ON "StatusPageMonitorRule" ("statusPageId") `,
17
+ );
18
+ await queryRunner.query(
19
+ `CREATE INDEX "IDX_61196848685b7e7f5a6a54278b" ON "StatusPageMonitorRule" ("name") `,
20
+ );
21
+ await queryRunner.query(
22
+ `CREATE INDEX "IDX_71d1869039530178fd5f7e6faa" ON "StatusPageMonitorRule" ("isEnabled") `,
23
+ );
24
+ await queryRunner.query(
25
+ `CREATE INDEX "IDX_3e6d0bc4d444371aaa13a1c14c" ON "StatusPageMonitorRule" ("statusPageGroupId") `,
26
+ );
27
+ await queryRunner.query(
28
+ `CREATE TABLE "StatusPageMonitorRuleMonitorLabel" ("statusPageMonitorRuleId" uuid NOT NULL, "labelId" uuid NOT NULL, CONSTRAINT "PK_59234d33b435d8e771cd3728ea4" PRIMARY KEY ("statusPageMonitorRuleId", "labelId"))`,
29
+ );
30
+ await queryRunner.query(
31
+ `CREATE INDEX "IDX_0eb2b2a3601ea9813b8e162bd3" ON "StatusPageMonitorRuleMonitorLabel" ("statusPageMonitorRuleId") `,
32
+ );
33
+ await queryRunner.query(
34
+ `CREATE INDEX "IDX_bc08b888a870ecc9167516230a" ON "StatusPageMonitorRuleMonitorLabel" ("labelId") `,
35
+ );
36
+ await queryRunner.query(
37
+ `ALTER TABLE "StatusPageResource" ADD "statusPageMonitorRuleId" uuid`,
38
+ );
39
+ await queryRunner.query(
40
+ `CREATE INDEX "IDX_4e1ed45d9506da817dc7811601" ON "StatusPageResource" ("statusPageMonitorRuleId") `,
41
+ );
42
+ await queryRunner.query(
43
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_1dfd95ad32f7b687ef4d7ba5910" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
44
+ );
45
+ await queryRunner.query(
46
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_473317deaa55a8305a047e4098b" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
47
+ );
48
+ await queryRunner.query(
49
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_3e6d0bc4d444371aaa13a1c14cc" FOREIGN KEY ("statusPageGroupId") REFERENCES "StatusPageGroup"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
50
+ );
51
+ await queryRunner.query(
52
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_60d22031be1695cbd1405376cbf" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
53
+ );
54
+ await queryRunner.query(
55
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_3416ec12899c9dde670d8dea290" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
56
+ );
57
+ await queryRunner.query(
58
+ `ALTER TABLE "StatusPageResource" ADD CONSTRAINT "FK_4e1ed45d9506da817dc78116010" FOREIGN KEY ("statusPageMonitorRuleId") REFERENCES "StatusPageMonitorRule"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
59
+ );
60
+ await queryRunner.query(
61
+ `ALTER TABLE "StatusPageMonitorRuleMonitorLabel" ADD CONSTRAINT "FK_0eb2b2a3601ea9813b8e162bd3b" FOREIGN KEY ("statusPageMonitorRuleId") REFERENCES "StatusPageMonitorRule"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
62
+ );
63
+ await queryRunner.query(
64
+ `ALTER TABLE "StatusPageMonitorRuleMonitorLabel" ADD CONSTRAINT "FK_bc08b888a870ecc9167516230ad" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
65
+ );
66
+ }
67
+
68
+ public async down(queryRunner: QueryRunner): Promise<void> {
69
+ await queryRunner.query(
70
+ `ALTER TABLE "StatusPageMonitorRuleMonitorLabel" DROP CONSTRAINT "FK_bc08b888a870ecc9167516230ad"`,
71
+ );
72
+ await queryRunner.query(
73
+ `ALTER TABLE "StatusPageMonitorRuleMonitorLabel" DROP CONSTRAINT "FK_0eb2b2a3601ea9813b8e162bd3b"`,
74
+ );
75
+ await queryRunner.query(
76
+ `ALTER TABLE "StatusPageResource" DROP CONSTRAINT "FK_4e1ed45d9506da817dc78116010"`,
77
+ );
78
+ await queryRunner.query(
79
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_3416ec12899c9dde670d8dea290"`,
80
+ );
81
+ await queryRunner.query(
82
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_60d22031be1695cbd1405376cbf"`,
83
+ );
84
+ await queryRunner.query(
85
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_3e6d0bc4d444371aaa13a1c14cc"`,
86
+ );
87
+ await queryRunner.query(
88
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_473317deaa55a8305a047e4098b"`,
89
+ );
90
+ await queryRunner.query(
91
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_1dfd95ad32f7b687ef4d7ba5910"`,
92
+ );
93
+ await queryRunner.query(
94
+ `DROP INDEX "public"."IDX_4e1ed45d9506da817dc7811601"`,
95
+ );
96
+ await queryRunner.query(
97
+ `ALTER TABLE "StatusPageResource" DROP COLUMN "statusPageMonitorRuleId"`,
98
+ );
99
+ await queryRunner.query(
100
+ `DROP INDEX "public"."IDX_bc08b888a870ecc9167516230a"`,
101
+ );
102
+ await queryRunner.query(
103
+ `DROP INDEX "public"."IDX_0eb2b2a3601ea9813b8e162bd3"`,
104
+ );
105
+ await queryRunner.query(`DROP TABLE "StatusPageMonitorRuleMonitorLabel"`);
106
+ await queryRunner.query(
107
+ `DROP INDEX "public"."IDX_3e6d0bc4d444371aaa13a1c14c"`,
108
+ );
109
+ await queryRunner.query(
110
+ `DROP INDEX "public"."IDX_71d1869039530178fd5f7e6faa"`,
111
+ );
112
+ await queryRunner.query(
113
+ `DROP INDEX "public"."IDX_61196848685b7e7f5a6a54278b"`,
114
+ );
115
+ await queryRunner.query(
116
+ `DROP INDEX "public"."IDX_473317deaa55a8305a047e4098"`,
117
+ );
118
+ await queryRunner.query(
119
+ `DROP INDEX "public"."IDX_1dfd95ad32f7b687ef4d7ba591"`,
120
+ );
121
+ await queryRunner.query(`DROP TABLE "StatusPageMonitorRule"`);
122
+ }
123
+ }
@@ -498,6 +498,7 @@ import { WidenInventoryResourceNameColumns1785930000000 } from "./1785930000000-
498
498
  import { AddStatusPageReportPeriod1785930709405 } from "./1785930709405-AddStatusPageReportPeriod";
499
499
  import { AddIncidentInvestigationGating1785790000000 } from "./1785790000000-AddIncidentInvestigationGating";
500
500
  import { AddRemediationVerification1785768089408 } from "./1785768089408-AddRemediationVerification";
501
+ import { AddStatusPageMonitorRule1786005052769 } from "./1786005052769-AddStatusPageMonitorRule";
501
502
 
502
503
  export default [
503
504
  InitialMigration,
@@ -1000,4 +1001,5 @@ export default [
1000
1001
  IncreaseHostIpAddressesLength1785915638551,
1001
1002
  WidenInventoryResourceNameColumns1785930000000,
1002
1003
  AddStatusPageReportPeriod1785930709405,
1004
+ AddStatusPageMonitorRule1786005052769,
1003
1005
  ];
@@ -19,6 +19,7 @@ import { JSONObject } from "../../Types/JSON";
19
19
  import AlertInternalNote from "../../Models/DatabaseModels/AlertInternalNote";
20
20
  import AlertInternalNoteService from "./AlertInternalNoteService";
21
21
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
22
+ import ProjectScopedReferenceValidator from "../Utils/Database/ProjectScopedReferenceValidator";
22
23
  import logger, { LogAttributes } from "../Utils/Logger";
23
24
  import AlertFeedService from "./AlertFeedService";
24
25
  import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed";
@@ -118,6 +119,25 @@ export class Service extends DatabaseService<AlertStateTimeline> {
118
119
  throw new BadDataException("alertStateId is null");
119
120
  }
120
121
 
122
+ /*
123
+ * Same guard, same reason, as MonitorStatusTimelineService: an id that
124
+ * exists nowhere (or belongs to another project) used to reach Postgres
125
+ * and come back as a raw foreign key violation. The ordering check below
126
+ * does look the state up, but only when there IS a preceding row with an
127
+ * order, so the first timeline row for an alert went through unchecked.
128
+ */
129
+ await ProjectScopedReferenceValidator.validateReferencesBelongToProject({
130
+ projectId: createBy.props.tenantId || createBy.data.projectId,
131
+ subject: "alert state timeline",
132
+ references: [
133
+ {
134
+ modelName: "Alert State",
135
+ id: alertStateId,
136
+ service: AlertStateService,
137
+ },
138
+ ],
139
+ });
140
+
121
141
  const stateBeforeThis: AlertStateTimeline | null = await this.findOneBy({
122
142
  query: {
123
143
  alertId: createBy.data.alertId,