@oneuptime/common 11.5.11 → 11.5.12

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 (71) hide show
  1. package/Models/AnalyticsModels/NetworkFlow.ts +12 -1
  2. package/Server/API/StatusPageAPI.ts +14 -1
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
  4. package/Server/Services/IncidentService.ts +62 -67
  5. package/Server/Services/MonitorGroupService.ts +15 -2
  6. package/Server/Services/MonitorService.ts +91 -3
  7. package/Server/Services/MonitorStatusTimelineService.ts +285 -100
  8. package/Server/Services/StatusPageService.ts +59 -34
  9. package/Server/Utils/Monitor/MonitorResource.ts +43 -14
  10. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +32 -7
  11. package/Server/Utils/Monitor/MonitorStatusTimelineReconciler.ts +362 -0
  12. package/Tests/Server/Services/MonitorStatusTimelineService.test.ts +235 -0
  13. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineReconciler.test.ts +555 -0
  14. package/Tests/Types/Database/CompareOperatorWireSerialization.test.ts +216 -0
  15. package/Tests/Types/Database/GreaterThan.test.ts +5 -1
  16. package/Tests/Types/Database/GreaterThanOrEqual.test.ts +5 -1
  17. package/Tests/Types/Database/GreaterThanOrNull.test.ts +24 -1
  18. package/Tests/Types/Database/LessThan.test.ts +5 -1
  19. package/Tests/Types/Database/LessThanOrEqual.test.ts +24 -1
  20. package/Tests/Types/Database/LessThanOrNull.test.ts +5 -1
  21. package/Tests/Utils/Uptime/UptimeUtil.test.ts +726 -0
  22. package/Types/BaseDatabase/GreaterThan.ts +8 -1
  23. package/Types/BaseDatabase/GreaterThanOrEqual.ts +8 -1
  24. package/Types/BaseDatabase/GreaterThanOrNull.ts +9 -1
  25. package/Types/BaseDatabase/LessThan.ts +8 -1
  26. package/Types/BaseDatabase/LessThanOrEqual.ts +9 -1
  27. package/Types/BaseDatabase/LessThanOrNull.ts +8 -1
  28. package/Utils/StatusPage/ResourceUptime.ts +11 -1
  29. package/Utils/Uptime/UptimeUtil.ts +142 -36
  30. package/build/dist/Models/AnalyticsModels/NetworkFlow.js +12 -1
  31. package/build/dist/Models/AnalyticsModels/NetworkFlow.js.map +1 -1
  32. package/build/dist/Server/API/StatusPageAPI.js +12 -2
  33. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  36. package/build/dist/Server/Services/IncidentService.js +47 -56
  37. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  38. package/build/dist/Server/Services/MonitorGroupService.js +15 -2
  39. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  40. package/build/dist/Server/Services/MonitorService.js +66 -3
  41. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  42. package/build/dist/Server/Services/MonitorStatusTimelineService.js +244 -102
  43. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  44. package/build/dist/Server/Services/StatusPageService.js +57 -33
  45. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  46. package/build/dist/Server/Utils/Monitor/MonitorResource.js +34 -12
  47. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  48. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +25 -8
  49. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  50. package/build/dist/Server/Utils/Monitor/MonitorStatusTimelineReconciler.js +268 -0
  51. package/build/dist/Server/Utils/Monitor/MonitorStatusTimelineReconciler.js.map +1 -0
  52. package/build/dist/Types/BaseDatabase/GreaterThan.js +8 -1
  53. package/build/dist/Types/BaseDatabase/GreaterThan.js.map +1 -1
  54. package/build/dist/Types/BaseDatabase/GreaterThanOrEqual.js +8 -1
  55. package/build/dist/Types/BaseDatabase/GreaterThanOrEqual.js.map +1 -1
  56. package/build/dist/Types/BaseDatabase/GreaterThanOrNull.js +9 -1
  57. package/build/dist/Types/BaseDatabase/GreaterThanOrNull.js.map +1 -1
  58. package/build/dist/Types/BaseDatabase/LessThan.js +8 -1
  59. package/build/dist/Types/BaseDatabase/LessThan.js.map +1 -1
  60. package/build/dist/Types/BaseDatabase/LessThanOrEqual.js +9 -1
  61. package/build/dist/Types/BaseDatabase/LessThanOrEqual.js.map +1 -1
  62. package/build/dist/Types/BaseDatabase/LessThanOrNull.js +8 -1
  63. package/build/dist/Types/BaseDatabase/LessThanOrNull.js.map +1 -1
  64. package/build/dist/Utils/StatusPage/ResourceUptime.js +4 -1
  65. package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
  66. package/build/dist/Utils/Uptime/UptimeUtil.js +86 -33
  67. package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
  68. package/package.json +1 -1
  69. package/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.ts +0 -101
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.js +0 -40
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.js.map +0 -1
@@ -0,0 +1,216 @@
1
+ import CompareBase, { CompareType } from "../../../Types/Database/CompareBase";
2
+ import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
3
+ import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
4
+ import GreaterThanOrNull from "../../../Types/BaseDatabase/GreaterThanOrNull";
5
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
6
+ import LessThan from "../../../Types/BaseDatabase/LessThan";
7
+ import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
8
+ import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
9
+ import JSONFunctions from "../../../Types/JSONFunctions";
10
+ import { JSONObject } from "../../../Types/JSON";
11
+ import { describe, expect, it } from "@jest/globals";
12
+
13
+ /*
14
+ * Pins the WIRE CONTRACT of the comparison query operators end to end:
15
+ *
16
+ * browser: JSONFunctions.serialize(query) -> operator.toJSON()
17
+ * HTTP: JSON.stringify / JSON.parse (a raw Date becomes its full ISO string)
18
+ * server: JSONFunctions.deserialize -> Dictionary[_type].fromJSON
19
+ * SQL bind: operator.toString() -> QueryHelper.<op>(value)
20
+ *
21
+ * The bug this file guards against: toJSON used to serialize Date values via
22
+ * toString(), which collapses them to a DATE-ONLY string in the BROWSER'S LOCAL
23
+ * TIMEZONE (OneUptimeDate.asDateForDatabaseQuery). A bound of "now" therefore
24
+ * arrived at the server as midnight of the viewer's calendar date, and every
25
+ * row from later that day was silently excluded from the query - e.g. the
26
+ * Monitor View page dropped a monitor's current open status row whenever the
27
+ * status had changed the same day. Serializing the RAW value (exactly what
28
+ * InBetween always did) keeps the full timestamp across the wire in every
29
+ * timezone.
30
+ *
31
+ * The legacy date-only wire format must STAY accepted: an old client (or a
32
+ * saved query serialized before this change) sends value: "YYYY-MM-DD", and
33
+ * the server must keep treating it as an opaque string bound.
34
+ */
35
+
36
+ const FULL_ISO_TIMESTAMP: string = "2026-07-21T14:35:12.345Z";
37
+ const LEGACY_DATE_ONLY: string = "2026-07-21";
38
+
39
+ type OperatorFactory = (value: CompareType) => CompareBase<CompareType>;
40
+
41
+ type OperatorCase = {
42
+ name: string;
43
+ type: unknown;
44
+ create: OperatorFactory;
45
+ };
46
+
47
+ const OPERATOR_CASES: Array<OperatorCase> = [
48
+ {
49
+ name: "LessThan",
50
+ type: LessThan,
51
+ create: (value: CompareType) => {
52
+ return new LessThan(value);
53
+ },
54
+ },
55
+ {
56
+ name: "LessThanOrEqual",
57
+ type: LessThanOrEqual,
58
+ create: (value: CompareType) => {
59
+ return new LessThanOrEqual(value);
60
+ },
61
+ },
62
+ {
63
+ name: "LessThanOrNull",
64
+ type: LessThanOrNull,
65
+ create: (value: CompareType) => {
66
+ return new LessThanOrNull(value);
67
+ },
68
+ },
69
+ {
70
+ name: "GreaterThan",
71
+ type: GreaterThan,
72
+ create: (value: CompareType) => {
73
+ return new GreaterThan(value);
74
+ },
75
+ },
76
+ {
77
+ name: "GreaterThanOrEqual",
78
+ type: GreaterThanOrEqual,
79
+ create: (value: CompareType) => {
80
+ return new GreaterThanOrEqual(value);
81
+ },
82
+ },
83
+ {
84
+ name: "GreaterThanOrNull",
85
+ type: GreaterThanOrNull,
86
+ create: (value: CompareType) => {
87
+ return new GreaterThanOrNull(value);
88
+ },
89
+ },
90
+ ];
91
+
92
+ type SimulateHttpHopFunction = (query: JSONObject) => JSONObject;
93
+
94
+ /*
95
+ * What actually happens between ModelAPI.getList and BaseAPI: the serialized
96
+ * query is JSON.stringify'd into the request body and JSON.parse'd on the
97
+ * server. This is the step that turns a raw Date into its full ISO string.
98
+ */
99
+ const simulateHttpHop: SimulateHttpHopFunction = (
100
+ query: JSONObject,
101
+ ): JSONObject => {
102
+ return JSON.parse(JSON.stringify(JSONFunctions.serialize(query)));
103
+ };
104
+
105
+ for (const operatorCase of OPERATOR_CASES) {
106
+ describe(`${operatorCase.name} wire serialization`, () => {
107
+ it("sends a Date bound as its full ISO timestamp, not a local date-only string", () => {
108
+ const query: JSONObject = {
109
+ startsAt: operatorCase.create(new Date(FULL_ISO_TIMESTAMP)),
110
+ } as unknown as JSONObject;
111
+
112
+ const wire: JSONObject = simulateHttpHop(query);
113
+
114
+ expect((wire["startsAt"] as JSONObject)["_type"]).toBe(operatorCase.name);
115
+ /*
116
+ * The critical assertion: the full timestamp survives. Under the old
117
+ * toString()-based toJSON this was "2026-07-21" (or "2026-07-22" east
118
+ * of UTC) regardless of the time component.
119
+ */
120
+ expect((wire["startsAt"] as JSONObject)["value"]).toBe(
121
+ FULL_ISO_TIMESTAMP,
122
+ );
123
+ });
124
+
125
+ it("deserializes on the server to the operator with the full-precision bound", () => {
126
+ const query: JSONObject = {
127
+ startsAt: operatorCase.create(new Date(FULL_ISO_TIMESTAMP)),
128
+ } as unknown as JSONObject;
129
+
130
+ const deserialized: JSONObject = JSONFunctions.deserialize(
131
+ simulateHttpHop(query),
132
+ );
133
+
134
+ expect(deserialized["startsAt"]).toBeInstanceOf(operatorCase.type);
135
+
136
+ /*
137
+ * toString() is exactly what QueryUtil passes into QueryHelper as the
138
+ * SQL bind value, so this string IS the query bound. It must be the
139
+ * full ISO timestamp, passed through untouched.
140
+ */
141
+ const operator: CompareBase<CompareType> = deserialized[
142
+ "startsAt"
143
+ ] as unknown as CompareBase<CompareType>;
144
+
145
+ expect(operator.toString()).toBe(FULL_ISO_TIMESTAMP);
146
+ });
147
+
148
+ it("still accepts the legacy date-only wire format as an opaque string", () => {
149
+ const legacyWire: JSONObject = {
150
+ startsAt: {
151
+ _type: operatorCase.name,
152
+ value: LEGACY_DATE_ONLY,
153
+ },
154
+ };
155
+
156
+ const deserialized: JSONObject = JSONFunctions.deserialize(legacyWire);
157
+
158
+ expect(deserialized["startsAt"]).toBeInstanceOf(operatorCase.type);
159
+
160
+ const operator: CompareBase<CompareType> = deserialized[
161
+ "startsAt"
162
+ ] as unknown as CompareBase<CompareType>;
163
+
164
+ expect(operator.toString()).toBe(LEGACY_DATE_ONLY);
165
+ });
166
+
167
+ it("round-trips a number bound unchanged", () => {
168
+ const query: JSONObject = {
169
+ priority: operatorCase.create(42),
170
+ } as unknown as JSONObject;
171
+
172
+ const wire: JSONObject = simulateHttpHop(query);
173
+
174
+ expect((wire["priority"] as JSONObject)["value"]).toBe(42);
175
+
176
+ const deserialized: JSONObject = JSONFunctions.deserialize(wire);
177
+
178
+ const operator: CompareBase<CompareType> = deserialized[
179
+ "priority"
180
+ ] as unknown as CompareBase<CompareType>;
181
+
182
+ expect(operator.toString()).toBe("42");
183
+ });
184
+ });
185
+ }
186
+
187
+ describe("InBetween wire serialization (the reference behavior)", () => {
188
+ it("has always sent full ISO timestamps for both bounds - the operators above now match it", () => {
189
+ const startBound: string = "2026-04-22T00:30:00.000Z";
190
+
191
+ const query: JSONObject = {
192
+ createdAt: new InBetween(
193
+ new Date(startBound),
194
+ new Date(FULL_ISO_TIMESTAMP),
195
+ ),
196
+ } as unknown as JSONObject;
197
+
198
+ const wire: JSONObject = simulateHttpHop(query);
199
+
200
+ expect((wire["createdAt"] as JSONObject)["startValue"]).toBe(startBound);
201
+ expect((wire["createdAt"] as JSONObject)["endValue"]).toBe(
202
+ FULL_ISO_TIMESTAMP,
203
+ );
204
+
205
+ const deserialized: JSONObject = JSONFunctions.deserialize(wire);
206
+
207
+ expect(deserialized["createdAt"]).toBeInstanceOf(InBetween);
208
+
209
+ const operator: InBetween<CompareType> = deserialized[
210
+ "createdAt"
211
+ ] as unknown as InBetween<CompareType>;
212
+
213
+ expect(operator.toStartValueString()).toBe(startBound);
214
+ expect(operator.toEndValueString()).toBe(FULL_ISO_TIMESTAMP);
215
+ });
216
+ });
@@ -25,7 +25,11 @@ describe("GreaterThan", () => {
25
25
  const obj: GreaterThan<number> = new GreaterThan<number>(42);
26
26
  const expectedJSON: JSONObject = {
27
27
  _type: "GreaterThan",
28
- value: "42",
28
+ /*
29
+ * toJSON carries the RAW value (like InBetween), not a stringified one -
30
+ * see the comment on toJSON. For numbers that means the number itself.
31
+ */
32
+ value: 42,
29
33
  };
30
34
  expect(obj.toJSON()).toEqual(expectedJSON);
31
35
  });
@@ -27,7 +27,11 @@ describe("GreaterThanOrEqual", () => {
27
27
  const obj: GreaterThanOrEqual<number> = new GreaterThanOrEqual<number>(42);
28
28
  const expectedJSON: JSONObject = {
29
29
  _type: "GreaterThanOrEqual",
30
- value: "42",
30
+ /*
31
+ * toJSON carries the RAW value (like InBetween), not a stringified one -
32
+ * see the comment on toJSON. For numbers that means the number itself.
33
+ */
34
+ value: 42,
31
35
  };
32
36
  expect(obj.toJSON()).toEqual(expectedJSON);
33
37
  });
@@ -23,13 +23,36 @@ describe("GreaterThanOrNull", () => {
23
23
 
24
24
  it("should generate the correct JSON representation using toJSON", () => {
25
25
  const obj: GreaterThanOrNull<number> = new GreaterThanOrNull<number>(42);
26
+ /*
27
+ * toJSON carries the RAW value (like InBetween), not a stringified one -
28
+ * see the comment on toJSON. For numbers that means the number itself.
29
+ */
26
30
  const expectedJSON: JSONObject = {
27
31
  _type: "GreaterThanOrNull",
28
- value: "42",
32
+ value: 42,
29
33
  };
30
34
  expect(obj.toJSON()).toEqual(expectedJSON);
31
35
  });
32
36
 
37
+ it("keeps the full timestamp when serializing a Date value", () => {
38
+ const value: Date = new Date("2026-07-21T14:35:12.345Z");
39
+ const obj: GreaterThanOrNull<Date> = new GreaterThanOrNull<Date>(value);
40
+
41
+ /*
42
+ * The raw Date is serialized, so JSON.stringify emits the full ISO
43
+ * timestamp. The old toString()-based toJSON collapsed it to a
44
+ * local-timezone date-only string, which shifted query bounds sent from
45
+ * the browser by up to a day and silently dropped same-day rows.
46
+ */
47
+ expect(obj.toJSON()).toEqual({
48
+ _type: "GreaterThanOrNull",
49
+ value: value,
50
+ });
51
+ expect(JSON.parse(JSON.stringify(obj.toJSON()))["value"]).toBe(
52
+ "2026-07-21T14:35:12.345Z",
53
+ );
54
+ });
55
+
33
56
  it("should create a GreaterThanOrNull object from valid JSON input", () => {
34
57
  const jsonInput: JSONObject = {
35
58
  _type: "GreaterThanOrNull",
@@ -25,7 +25,11 @@ describe("LessThan", () => {
25
25
  const obj: LessThan<number> = new LessThan<number>(42);
26
26
  const expectedJSON: JSONObject = {
27
27
  _type: "LessThan",
28
- value: "42",
28
+ /*
29
+ * toJSON carries the RAW value (like InBetween), not a stringified one -
30
+ * see the comment on toJSON. For numbers that means the number itself.
31
+ */
32
+ value: 42,
29
33
  };
30
34
  expect(obj.toJSON()).toEqual(expectedJSON);
31
35
  });
@@ -23,13 +23,36 @@ describe("LessThanOrEqual", () => {
23
23
 
24
24
  it("should generate the correct JSON representation using toJSON", () => {
25
25
  const obj: LessThanOrEqual<number> = new LessThanOrEqual<number>(42);
26
+ /*
27
+ * toJSON carries the RAW value (like InBetween), not a stringified one -
28
+ * see the comment on toJSON. For numbers that means the number itself.
29
+ */
26
30
  const expectedJSON: JSONObject = {
27
31
  _type: "LessThanOrEqual",
28
- value: "42",
32
+ value: 42,
29
33
  };
30
34
  expect(obj.toJSON()).toEqual(expectedJSON);
31
35
  });
32
36
 
37
+ it("keeps the full timestamp when serializing a Date value", () => {
38
+ const value: Date = new Date("2026-07-21T14:35:12.345Z");
39
+ const obj: LessThanOrEqual<Date> = new LessThanOrEqual<Date>(value);
40
+
41
+ /*
42
+ * The raw Date is serialized, so JSON.stringify emits the full ISO
43
+ * timestamp. The old toString()-based toJSON collapsed it to a
44
+ * local-timezone date-only string, which shifted query bounds sent from
45
+ * the browser by up to a day and silently dropped same-day rows.
46
+ */
47
+ expect(obj.toJSON()).toEqual({
48
+ _type: "LessThanOrEqual",
49
+ value: value,
50
+ });
51
+ expect(JSON.parse(JSON.stringify(obj.toJSON()))["value"]).toBe(
52
+ "2026-07-21T14:35:12.345Z",
53
+ );
54
+ });
55
+
33
56
  it("should create a LessThanOrEqual object from valid JSON input", () => {
34
57
  const jsonInput: JSONObject = {
35
58
  _type: "LessThanOrEqual",
@@ -25,7 +25,11 @@ describe("LessThanOrNull", () => {
25
25
  const obj: LessThanOrNull<number> = new LessThanOrNull<number>(42);
26
26
  const expectedJSON: JSONObject = {
27
27
  _type: "LessThanOrNull",
28
- value: "42",
28
+ /*
29
+ * toJSON carries the RAW value (like InBetween), not a stringified one -
30
+ * see the comment on toJSON. For numbers that means the number itself.
31
+ */
32
+ value: 42,
29
33
  };
30
34
  expect(obj.toJSON()).toEqual(expectedJSON);
31
35
  });