@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,147 @@
1
+ import { getBigIntDatabaseTransformer } from "../../../Types/Database/BigIntColumnTransformer";
2
+ import { ValueTransformer } from "typeorm/decorator/options/ValueTransformer";
3
+ import { describe, expect, it } from "@jest/globals";
4
+
5
+ /*
6
+ * Contract under test — the Postgres bigint round-trip used by counter
7
+ * columns.
8
+ *
9
+ * The interesting half is `to(undefined)`. TypeORM applies a column
10
+ * transformer BEFORE it decides whether the column was supplied at all:
11
+ *
12
+ * InsertQueryBuilder.createColumnValueExpression
13
+ * -> driver.preparePersistentValue (runs transformer.to)
14
+ * -> if (value === undefined) expression += "DEFAULT"
15
+ *
16
+ * so a transformer that answers `null` for `undefined` has already turned
17
+ * "the caller did not set this column" into "the caller set it to NULL" by
18
+ * the time that check runs, and the column DEFAULT is unreachable. On the
19
+ * drop-filter `droppedCount` columns — NOT NULL DEFAULT 0, written only by
20
+ * the ingest path — that made every single insert fail:
21
+ *
22
+ * null value in column "droppedCount" of relation "LogDropFilter"
23
+ * violates not-null constraint
24
+ *
25
+ * i.e. creating any log drop filter in the dashboard returned HTTP 500.
26
+ * github.com/OneUptime/oneuptime/issues/3026
27
+ */
28
+
29
+ const transformer: ValueTransformer = getBigIntDatabaseTransformer();
30
+
31
+ describe("getBigIntDatabaseTransformer().to", () => {
32
+ /*
33
+ * The regression. `undefined` and `null` are NOT interchangeable here:
34
+ * one means "use the column default", the other means "write NULL".
35
+ */
36
+ it("passes undefined through so the column DEFAULT applies", () => {
37
+ expect(transformer.to(undefined)).toBeUndefined();
38
+ });
39
+
40
+ it("does not confuse undefined with null", () => {
41
+ expect(transformer.to(undefined)).not.toBeNull();
42
+ });
43
+
44
+ it("keeps an explicit null as null", () => {
45
+ expect(transformer.to(null)).toBeNull();
46
+ });
47
+
48
+ it("stringifies a number, because the pg driver binds bigint as text", () => {
49
+ expect(transformer.to(0)).toBe("0");
50
+ expect(transformer.to(1)).toBe("1");
51
+ expect(transformer.to(42)).toBe("42");
52
+ });
53
+
54
+ it("truncates rather than emitting a decimal Postgres would reject", () => {
55
+ expect(transformer.to(10.9)).toBe("10");
56
+ expect(transformer.to(-10.9)).toBe("-10");
57
+ });
58
+
59
+ /*
60
+ * The reason these columns are bigint at all: a sample filter on a
61
+ * high-volume project passes 2^31 well within a year, and a silently
62
+ * wrapped diagnostic counter is worse than no counter.
63
+ */
64
+ it("carries a value past the 32-bit range without losing precision", () => {
65
+ expect(transformer.to(4_294_967_296)).toBe("4294967296");
66
+ expect(transformer.to(Number.MAX_SAFE_INTEGER)).toBe("9007199254740991");
67
+ });
68
+
69
+ /*
70
+ * `Math.trunc(NaN).toString()` is the string "NaN", which Postgres
71
+ * rejects with a syntax error naming neither the column nor the caller.
72
+ */
73
+ it("refuses to emit a non-finite value as text", () => {
74
+ expect(transformer.to(NaN)).toBeNull();
75
+ expect(transformer.to(Infinity)).toBeNull();
76
+ expect(transformer.to(-Infinity)).toBeNull();
77
+ });
78
+ });
79
+
80
+ describe("getBigIntDatabaseTransformer().from", () => {
81
+ /*
82
+ * node-postgres hands bigint back as a string — it does not fit a JS
83
+ * number in the general case. Every value stored in these counters is
84
+ * inside MAX_SAFE_INTEGER, and the dashboard renders them as numbers.
85
+ */
86
+ it("parses the string the driver returns into a number", () => {
87
+ expect(transformer.from("0")).toBe(0);
88
+ expect(transformer.from("42")).toBe(42);
89
+ expect(transformer.from("9007199254740991")).toBe(9007199254740991);
90
+ });
91
+
92
+ it("accepts a number unchanged, in case a driver already parsed it", () => {
93
+ expect(transformer.from(7)).toBe(7);
94
+ });
95
+
96
+ it("maps a null column to null rather than 0", () => {
97
+ expect(transformer.from(null)).toBeNull();
98
+ expect(transformer.from(undefined)).toBeNull();
99
+ });
100
+
101
+ it("maps unparsable text to null rather than NaN", () => {
102
+ expect(transformer.from("not a number")).toBeNull();
103
+ expect(transformer.from("")).toBeNull();
104
+ });
105
+
106
+ it("round-trips a counter value", () => {
107
+ for (const value of [0, 1, 999, 4_294_967_296]) {
108
+ expect(transformer.from(transformer.to(value))).toBe(value);
109
+ }
110
+ });
111
+ });
112
+
113
+ /*
114
+ * A miniature of TypeORM's own decision, so the consequence of the
115
+ * transformer's answer is asserted rather than left to a comment. Mirrors
116
+ * PostgresDriver.preparePersistentValue followed by
117
+ * InsertQueryBuilder.createColumnValueExpression.
118
+ */
119
+ describe("the INSERT expression TypeORM derives from this transformer", () => {
120
+ type InsertExpression = "DEFAULT" | "NULL" | string;
121
+
122
+ function insertExpressionFor(value: number | null | undefined): string {
123
+ const prepared: unknown = transformer.to(value);
124
+
125
+ if (prepared === undefined) {
126
+ return "DEFAULT";
127
+ }
128
+
129
+ if (prepared === null) {
130
+ return "NULL";
131
+ }
132
+
133
+ return String(prepared);
134
+ }
135
+
136
+ it("emits DEFAULT for a counter the caller never set", () => {
137
+ const expression: InsertExpression = insertExpressionFor(undefined);
138
+
139
+ expect(expression).toBe("DEFAULT");
140
+ // The old inline transformer produced this, and NOT NULL rejected it.
141
+ expect(expression).not.toBe("NULL");
142
+ });
143
+
144
+ it("emits the value for a counter the caller did set", () => {
145
+ expect(insertExpressionFor(5)).toBe("5");
146
+ });
147
+ });
@@ -0,0 +1,140 @@
1
+ import Recurring from "../../../Types/Events/Recurring";
2
+ import RestrictionTimes from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import Email from "../../../Types/Email";
5
+ import Color from "../../../Types/Color";
6
+ import EventInterval from "../../../Types/Events/EventInterval";
7
+ import PositiveNumber from "../../../Types/PositiveNumber";
8
+ import { ValueTransformer } from "typeorm/decorator/options/ValueTransformer";
9
+ import { describe, expect, it } from "@jest/globals";
10
+
11
+ /*
12
+ * Contract under test — `DatabaseProperty.getDatabaseTransformer()`, the one
13
+ * transformer behind nearly every non-primitive column in the schema.
14
+ *
15
+ * Its `to` has to keep `undefined` and `null` apart. TypeORM applies the
16
+ * transformer BEFORE deciding whether the caller supplied the column:
17
+ *
18
+ * InsertQueryBuilder.createColumnValueExpression
19
+ * -> driver.preparePersistentValue (runs transformer.to)
20
+ * -> if (value === undefined) expression += "DEFAULT"
21
+ *
22
+ * Every `toDatabase` implementation returns null for a falsy input, so
23
+ * before this guard an omitted column arrived as an explicit NULL and the
24
+ * column's DEFAULT was unreachable. On a NOT NULL column with a default that
25
+ * is a not-null constraint violation — an HTTP 500 — on every create that
26
+ * does not mention the column. That is the mechanism behind
27
+ * github.com/OneUptime/oneuptime/issues/3026 (the drop-filter counters), and
28
+ * `OnCallDutyPolicyScheduleLayer.rotation` / `.restrictionTimes` are the same
29
+ * shape: NOT NULL, with a JSON default, written through this transformer.
30
+ *
31
+ * UPDATE is unaffected in both directions — UpdateQueryBuilder drops
32
+ * undefined properties before the transformer runs — so this only ever
33
+ * changes an INSERT from "write NULL" to "use the DEFAULT".
34
+ */
35
+
36
+ interface TransformerCase {
37
+ name: string;
38
+ transformer: ValueTransformer;
39
+ sample: unknown;
40
+ }
41
+
42
+ const CASES: Array<TransformerCase> = [
43
+ {
44
+ name: "Recurring",
45
+ transformer: Recurring.getDatabaseTransformer(),
46
+ sample: Recurring.getDefault(),
47
+ },
48
+ {
49
+ name: "RestrictionTimes",
50
+ transformer: RestrictionTimes.getDatabaseTransformer(),
51
+ sample: RestrictionTimes.getDefault(),
52
+ },
53
+ {
54
+ name: "ObjectID",
55
+ transformer: ObjectID.getDatabaseTransformer(),
56
+ sample: new ObjectID("11111111-1111-4111-8111-111111111111"),
57
+ },
58
+ {
59
+ name: "Email",
60
+ transformer: Email.getDatabaseTransformer(),
61
+ sample: new Email("someone@oneuptime.com"),
62
+ },
63
+ {
64
+ name: "Color",
65
+ transformer: Color.getDatabaseTransformer(),
66
+ sample: new Color("#ff0000"),
67
+ },
68
+ ];
69
+
70
+ describe.each(CASES)(
71
+ "$name.getDatabaseTransformer()",
72
+ ({ transformer, sample }: TransformerCase) => {
73
+ it("passes undefined through so the column DEFAULT applies", () => {
74
+ expect(transformer.to(undefined)).toBeUndefined();
75
+ });
76
+
77
+ it("does not collapse undefined into null", () => {
78
+ expect(transformer.to(undefined)).not.toBeNull();
79
+ });
80
+
81
+ /*
82
+ * An explicit null is a different instruction — "store nothing here" —
83
+ * and still has to reach the column as NULL.
84
+ */
85
+ it("keeps an explicit null as null", () => {
86
+ expect(transformer.to(null)).toBeNull();
87
+ });
88
+
89
+ it("still serializes a real value", () => {
90
+ expect(transformer.to(sample)).not.toBeUndefined();
91
+ expect(transformer.to(sample)).not.toBeNull();
92
+ });
93
+ },
94
+ );
95
+
96
+ describe("the INSERT expression TypeORM derives for a NOT NULL column", () => {
97
+ /*
98
+ * A miniature of PostgresDriver.preparePersistentValue followed by
99
+ * InsertQueryBuilder.createColumnValueExpression, so the consequence is
100
+ * asserted rather than described.
101
+ */
102
+ function insertExpressionFor(
103
+ transformer: ValueTransformer,
104
+ value: unknown,
105
+ ): string {
106
+ const prepared: unknown = transformer.to(value);
107
+
108
+ if (prepared === undefined) {
109
+ return "DEFAULT";
110
+ }
111
+
112
+ if (prepared === null) {
113
+ return "NULL";
114
+ }
115
+
116
+ return "?";
117
+ }
118
+
119
+ it("uses the schedule layer's rotation default instead of NULL", () => {
120
+ expect(
121
+ insertExpressionFor(Recurring.getDatabaseTransformer(), undefined),
122
+ ).toBe("DEFAULT");
123
+ });
124
+
125
+ it("uses the schedule layer's restriction default instead of NULL", () => {
126
+ expect(
127
+ insertExpressionFor(RestrictionTimes.getDatabaseTransformer(), undefined),
128
+ ).toBe("DEFAULT");
129
+ });
130
+
131
+ it("still binds a supplied value", () => {
132
+ const rotation: Recurring = new Recurring();
133
+ rotation.intervalType = EventInterval.Week;
134
+ rotation.intervalCount = new PositiveNumber(2);
135
+
136
+ expect(
137
+ insertExpressionFor(Recurring.getDatabaseTransformer(), rotation),
138
+ ).toBe("?");
139
+ });
140
+ });
@@ -0,0 +1,127 @@
1
+ import {
2
+ coerceNumericColumnValue,
3
+ isNumericTableColumnType,
4
+ } from "../../../Types/Database/NumericColumnValue";
5
+ import TableColumnType from "../../../Types/Database/TableColumnType";
6
+ import { describe, expect, it } from "@jest/globals";
7
+
8
+ /*
9
+ * Contract under test — normalizing a JSON value bound for a number column.
10
+ *
11
+ * HTML has no numeric input event: `<input type="number">` hands back
12
+ * `e.target.value`, a string. So the dashboard posted the JSON string "10"
13
+ * for every number field, and Postgres quietly coerced '10' to 10 on the way
14
+ * in — which is why nobody noticed until a server-side check read the value
15
+ * BEFORE it was stored:
16
+ *
17
+ * BadDataException: Sample percentage is required when the action is
18
+ * "Sample". Enter the percentage of matching logs to keep, between 1 and 99.
19
+ *
20
+ * on a form where the user had typed a percentage. HTTP 400 on a valid
21
+ * submission. github.com/OneUptime/oneuptime/issues/3027
22
+ */
23
+
24
+ describe("isNumericTableColumnType", () => {
25
+ it("covers every column type stored as a plain JS number", () => {
26
+ for (const type of [
27
+ TableColumnType.Number,
28
+ TableColumnType.SmallNumber,
29
+ TableColumnType.BigNumber,
30
+ TableColumnType.PositiveNumber,
31
+ TableColumnType.SmallPositiveNumber,
32
+ TableColumnType.BigPositiveNumber,
33
+ ]) {
34
+ expect(isNumericTableColumnType(type)).toBe(true);
35
+ }
36
+ });
37
+
38
+ /*
39
+ * The types that would be destroyed by coercion. `Port` and `Version` are
40
+ * wrapped objects and `Date` is a Date, however numeric they look.
41
+ */
42
+ it("excludes types that only look numeric", () => {
43
+ for (const type of [
44
+ TableColumnType.Port,
45
+ TableColumnType.Version,
46
+ TableColumnType.Date,
47
+ TableColumnType.ShortText,
48
+ TableColumnType.LongText,
49
+ TableColumnType.Boolean,
50
+ TableColumnType.ObjectID,
51
+ TableColumnType.Entity,
52
+ TableColumnType.EntityArray,
53
+ TableColumnType.JSON,
54
+ ]) {
55
+ expect(isNumericTableColumnType(type)).toBe(false);
56
+ }
57
+ });
58
+
59
+ it("treats a missing type as not numeric", () => {
60
+ expect(isNumericTableColumnType(undefined)).toBe(false);
61
+ expect(isNumericTableColumnType(null)).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe("coerceNumericColumnValue", () => {
66
+ it("converts the string an <input type=number> produces", () => {
67
+ expect(coerceNumericColumnValue("10")).toBe(10);
68
+ expect(coerceNumericColumnValue("1")).toBe(1);
69
+ expect(coerceNumericColumnValue("99")).toBe(99);
70
+ });
71
+
72
+ it("handles the shapes a browser can put in that string", () => {
73
+ expect(coerceNumericColumnValue("0")).toBe(0);
74
+ expect(coerceNumericColumnValue("-5")).toBe(-5);
75
+ expect(coerceNumericColumnValue("2.5")).toBe(2.5);
76
+ expect(coerceNumericColumnValue(" 42 ")).toBe(42);
77
+ expect(coerceNumericColumnValue("1e3")).toBe(1000);
78
+ });
79
+
80
+ it("leaves a value that is already a number alone", () => {
81
+ expect(coerceNumericColumnValue(10)).toBe(10);
82
+ expect(coerceNumericColumnValue(0)).toBe(0);
83
+ });
84
+
85
+ it("leaves null and undefined alone", () => {
86
+ expect(coerceNumericColumnValue(null)).toBeNull();
87
+ expect(coerceNumericColumnValue(undefined)).toBeUndefined();
88
+ });
89
+
90
+ /*
91
+ * `Number("")` is 0. Writing a real 0 for a field the user cleared would
92
+ * be a silent data change, and for a sample percentage specifically, 0 is
93
+ * the value that used to mean "throw away half". Leave it untouched and
94
+ * let the field's own validation speak.
95
+ */
96
+ it("does not turn a cleared field into zero", () => {
97
+ expect(coerceNumericColumnValue("")).toBe("");
98
+ expect(coerceNumericColumnValue(" ")).toBe(" ");
99
+ });
100
+
101
+ it("leaves unparsable text alone so validation can reject it by name", () => {
102
+ for (const garbage of ["abc", "10abc", "NaN", "Infinity", "--1", "1,000"]) {
103
+ expect(coerceNumericColumnValue(garbage)).toBe(garbage);
104
+ }
105
+ });
106
+
107
+ it("never invents a non-finite number", () => {
108
+ const results: Array<unknown> = ["Infinity", "-Infinity", "NaN"].map(
109
+ (value: string) => {
110
+ return coerceNumericColumnValue(value);
111
+ },
112
+ );
113
+
114
+ for (const result of results) {
115
+ expect(typeof result).toBe("string");
116
+ }
117
+ });
118
+
119
+ it("passes non-string, non-number values through untouched", () => {
120
+ const object: Record<string, string> = { a: "1" };
121
+ const array: Array<number> = [1];
122
+
123
+ expect(coerceNumericColumnValue(object)).toBe(object);
124
+ expect(coerceNumericColumnValue(array)).toBe(array);
125
+ expect(coerceNumericColumnValue(true)).toBe(true);
126
+ });
127
+ });
@@ -89,4 +89,41 @@ describe("Decimal", () => {
89
89
  expect(restored.equals(original)).toBe(true);
90
90
  });
91
91
  });
92
+
93
+ /*
94
+ * A decimal column is declared `TableColumnType.Number`, so whatever
95
+ * reaches the transformer may be a Decimal, a string, or a plain number
96
+ * depending on how far the request body got through deserialization.
97
+ */
98
+ describe("getDatabaseTransformer().to", () => {
99
+ const transformer: ReturnType<typeof Decimal.getDatabaseTransformer> =
100
+ Decimal.getDatabaseTransformer();
101
+
102
+ test("stores a Decimal instance as its string form", () => {
103
+ expect(transformer.to(new Decimal(125.5))).toBe("125.5");
104
+ });
105
+
106
+ test("stores a raw string", () => {
107
+ expect(transformer.to("125.5")).toBe("125.5");
108
+ });
109
+
110
+ test("stores a raw number", () => {
111
+ expect(transformer.to(125.5)).toBe("125.5");
112
+ });
113
+
114
+ /*
115
+ * `0` is a perfectly good decimal, and both of its columns are NOT NULL.
116
+ * A truthiness check used to send it to null, which the column rejects.
117
+ */
118
+ test("stores zero rather than turning it into null", () => {
119
+ expect(transformer.to(0)).toBe("0");
120
+ expect(transformer.to("0")).toBe("0");
121
+ expect(transformer.to(new Decimal(0))).toBe("0");
122
+ });
123
+
124
+ test("keeps undefined distinct from null so a column DEFAULT applies", () => {
125
+ expect(transformer.to(undefined)).toBeUndefined();
126
+ expect(transformer.to(null)).toBeNull();
127
+ });
128
+ });
92
129
  });
@@ -0,0 +1,152 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import SnmpVersion, {
3
+ SnmpVersionUtil,
4
+ } from "../../../Types/Monitor/SnmpMonitor/SnmpVersion";
5
+
6
+ /*
7
+ * snmpVersion is a free-text column with two live spellings: the enum VALUES
8
+ * ("1"/"2c"/"3") that the probe contract uses, and the enum KEYS ("V1"/"V2c"/
9
+ * "V3") that every form writes and the column default holds. SnmpVersionUtil is
10
+ * the seam that reconciles them. Reading the column any other way silently takes
11
+ * the wrong branch — a stored "V3" is not === SnmpVersion.V3 ("3"), so a v3
12
+ * device would read as v2c and be polled in cleartext.
13
+ *
14
+ * Unlike the SnmpV3 protocol parsers, parse() cannot return undefined: the probe
15
+ * must pick exactly one session type, so an unreadable value falls back to v2c
16
+ * rather than refusing. These tests pin both the recognized spellings and that
17
+ * deliberate default so a future edit cannot widen or narrow it unnoticed.
18
+ */
19
+
20
+ describe("SnmpVersionUtil.parse", () => {
21
+ test.each([
22
+ ["1", SnmpVersion.V1],
23
+ ["2c", SnmpVersion.V2c],
24
+ ["3", SnmpVersion.V3],
25
+ ])(
26
+ "reads the probe-contract value spelling %j",
27
+ (stored: string, expected: SnmpVersion) => {
28
+ expect(SnmpVersionUtil.parse(stored)).toBe(expected);
29
+ },
30
+ );
31
+
32
+ /*
33
+ * The stored spelling — what forms write and the column default holds. This
34
+ * is the spelling the bare-cast bug got wrong, so it is the one that most
35
+ * needs a test.
36
+ */
37
+ test.each([
38
+ ["V1", SnmpVersion.V1],
39
+ ["V2c", SnmpVersion.V2c],
40
+ ["V3", SnmpVersion.V3],
41
+ ])(
42
+ "reads the stored enum-key spelling %j",
43
+ (stored: string, expected: SnmpVersion) => {
44
+ expect(SnmpVersionUtil.parse(stored)).toBe(expected);
45
+ },
46
+ );
47
+
48
+ test.each([
49
+ ["V1", SnmpVersion.V1],
50
+ ["v1", SnmpVersion.V1],
51
+ [" 1 ", SnmpVersion.V1],
52
+ ["\tv3\n", SnmpVersion.V3],
53
+ ["V3", SnmpVersion.V3],
54
+ ["v3", SnmpVersion.V3],
55
+ ])(
56
+ "tolerates case and surrounding whitespace in %j",
57
+ (stored: string, expected: SnmpVersion) => {
58
+ expect(SnmpVersionUtil.parse(stored)).toBe(expected);
59
+ },
60
+ );
61
+
62
+ /*
63
+ * v2c is the fallback branch, so every v2c spelling AND every value the parser
64
+ * does not recognize must land here. The two are indistinguishable by design:
65
+ * there is no "undefined version", the probe always needs a session type, and
66
+ * v2c is the safe historic default. Anything that stops mapping to v2c here is
67
+ * a behavior change, not a bug fix.
68
+ */
69
+ test.each([
70
+ ["2c", "an exact value"],
71
+ ["V2c", "the stored key"],
72
+ ["v2c", "lowercased key"],
73
+ ["2", "the version number without the c"],
74
+ ["V2", "the key without the c"],
75
+ [undefined, "an undefined column"],
76
+ [null, "a null column"],
77
+ ["", "an empty string"],
78
+ [" ", "whitespace only"],
79
+ ["nonsense", "garbage"],
80
+ ["v4", "a version that does not exist"],
81
+ ["0", "a zero"],
82
+ ])(
83
+ "falls back to v2c for %j (%s)",
84
+ (stored: string | undefined | null, _description: string) => {
85
+ expect(SnmpVersionUtil.parse(stored)).toBe(SnmpVersion.V2c);
86
+ },
87
+ );
88
+ });
89
+
90
+ describe("SnmpVersionUtil.isV3", () => {
91
+ test.each([["3"], ["v3"], ["V3"], [" V3 "], ["\tv3\n"]])(
92
+ "is true for the v3 spelling %j",
93
+ (stored: string) => {
94
+ expect(SnmpVersionUtil.isV3(stored)).toBe(true);
95
+ },
96
+ );
97
+
98
+ /*
99
+ * The consequential direction: everything that is not v3 must read as not-v3,
100
+ * because isV3 gates whether auth/priv credentials are applied. A false
101
+ * positive would be harmless; a false negative strands a v3 device in a v2c
102
+ * branch and polls it without credentials.
103
+ */
104
+ test.each([
105
+ ["1"],
106
+ ["V1"],
107
+ ["2c"],
108
+ ["V2c"],
109
+ ["v2c"],
110
+ ["2"],
111
+ [undefined],
112
+ [null],
113
+ [""],
114
+ [" "],
115
+ ["nonsense"],
116
+ ])(
117
+ "is false for the non-v3 value %j",
118
+ (stored: string | undefined | null) => {
119
+ expect(SnmpVersionUtil.isV3(stored)).toBe(false);
120
+ },
121
+ );
122
+
123
+ test("agrees with parse() for every recognized spelling", () => {
124
+ for (const spelling of ["1", "v1", "V1", "2c", "V2c", "3", "v3", "V3"]) {
125
+ expect(SnmpVersionUtil.isV3(spelling)).toBe(
126
+ SnmpVersionUtil.parse(spelling) === SnmpVersion.V3,
127
+ );
128
+ }
129
+ });
130
+ });
131
+
132
+ describe("every enum member survives a round trip", () => {
133
+ /*
134
+ * Exhaustive over the enum rather than a fixed list, so adding a version
135
+ * without teaching parse() its value AND key spelling fails here instead of
136
+ * silently collapsing to v2c in production.
137
+ */
138
+ test("each value spelling parses back to itself", () => {
139
+ for (const member of Object.values(SnmpVersion)) {
140
+ expect(SnmpVersionUtil.parse(member)).toBe(member);
141
+ }
142
+ });
143
+
144
+ test("each key spelling parses to its member", () => {
145
+ for (const key of Object.keys(SnmpVersion)) {
146
+ // Keys are "V1"/"V2c"/"V3"; the member is looked up by that key.
147
+ const expected: SnmpVersion =
148
+ SnmpVersion[key as keyof typeof SnmpVersion];
149
+ expect(SnmpVersionUtil.parse(key)).toBe(expected);
150
+ }
151
+ });
152
+ });
@@ -36,4 +36,42 @@ describe("Testing class port", () => {
36
36
  const value: Port = new Port("6000");
37
37
  expect(typeof value.port.positiveNumber).toBe("number");
38
38
  });
39
+
40
+ /*
41
+ * A port column is declared `TableColumnType.Number`, so whatever reaches
42
+ * the transformer may be a Port, a string, or a plain number depending on
43
+ * how far the request body got through deserialization. A raw number used
44
+ * to fall off the end of `toDatabase` and return null, which wrote NULL
45
+ * over an SMTP port the user had just typed — silently, since null is a
46
+ * legal value for the column.
47
+ */
48
+ describe("getDatabaseTransformer().to", () => {
49
+ const transformer: ReturnType<typeof Port.getDatabaseTransformer> =
50
+ Port.getDatabaseTransformer();
51
+
52
+ test("stores a Port instance as its number", () => {
53
+ expect(transformer.to(new Port(587))).toBe(587);
54
+ });
55
+
56
+ test("stores a raw string port as a number", () => {
57
+ expect(transformer.to("587")).toBe(587);
58
+ });
59
+
60
+ test("stores a raw number port rather than dropping it", () => {
61
+ expect(transformer.to(587)).toBe(587);
62
+ expect(transformer.to(0)).toBe(0);
63
+ expect(transformer.to(65535)).toBe(65535);
64
+ });
65
+
66
+ test("keeps undefined distinct from null so a column DEFAULT applies", () => {
67
+ expect(transformer.to(undefined)).toBeUndefined();
68
+ expect(transformer.to(null)).toBeNull();
69
+ });
70
+
71
+ test("rejects a port outside the valid range instead of storing null", () => {
72
+ expect(() => {
73
+ return transformer.to(70000);
74
+ }).toThrow(BadDataException);
75
+ });
76
+ });
39
77
  });