@oneuptime/common 11.5.11 → 11.5.13
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.
- package/Models/AnalyticsModels/NetworkFlow.ts +12 -1
- package/Models/DatabaseModels/AIInsight.ts +30 -0
- package/Models/DatabaseModels/Project.ts +30 -0
- package/Models/DatabaseModels/TelemetryException.ts +128 -0
- package/Server/API/AIAgentDataAPI.ts +129 -5
- package/Server/API/AIInvestigationAPI.ts +87 -0
- package/Server/API/StatusPageAPI.ts +14 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -2
- package/Server/Services/AIRunService.ts +8 -0
- package/Server/Services/IncidentService.ts +62 -67
- package/Server/Services/MonitorGroupService.ts +15 -2
- package/Server/Services/MonitorService.ts +91 -3
- package/Server/Services/MonitorStatusTimelineService.ts +285 -100
- package/Server/Services/StatusPageService.ts +59 -34
- package/Server/Services/TelemetryExceptionService.ts +320 -24
- package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
- package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
- package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
- package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
- package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
- package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
- package/Server/Utils/Monitor/MonitorResource.ts +43 -14
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +32 -7
- package/Server/Utils/Monitor/MonitorStatusTimelineReconciler.ts +362 -0
- package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
- package/Tests/Server/Services/MonitorStatusTimelineService.test.ts +235 -0
- package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
- package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
- package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
- package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
- package/Tests/Server/Utils/Monitor/MonitorStatusTimelineReconciler.test.ts +555 -0
- package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
- package/Tests/Types/Database/CompareOperatorWireSerialization.test.ts +216 -0
- package/Tests/Types/Database/GreaterThan.test.ts +5 -1
- package/Tests/Types/Database/GreaterThanOrEqual.test.ts +5 -1
- package/Tests/Types/Database/GreaterThanOrNull.test.ts +24 -1
- package/Tests/Types/Database/LessThan.test.ts +5 -1
- package/Tests/Types/Database/LessThanOrEqual.test.ts +24 -1
- package/Tests/Types/Database/LessThanOrNull.test.ts +5 -1
- package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
- package/Tests/Utils/Uptime/UptimeUtil.test.ts +726 -0
- package/Types/AI/CodeFixTaskContext.ts +6 -0
- package/Types/AI/CodeFixTaskType.ts +38 -1
- package/Types/AI/ExceptionAIClassification.ts +27 -0
- package/Types/BaseDatabase/GreaterThan.ts +8 -1
- package/Types/BaseDatabase/GreaterThanOrEqual.ts +8 -1
- package/Types/BaseDatabase/GreaterThanOrNull.ts +9 -1
- package/Types/BaseDatabase/LessThan.ts +8 -1
- package/Types/BaseDatabase/LessThanOrEqual.ts +9 -1
- package/Types/BaseDatabase/LessThanOrNull.ts +8 -1
- package/Types/Log/LogScrubPatternType.ts +7 -0
- package/Types/Trace/TraceScrubPatternType.ts +7 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
- package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
- package/Utils/StatusPage/ResourceUptime.ts +11 -1
- package/Utils/Uptime/UptimeUtil.ts +142 -36
- package/build/dist/Models/AnalyticsModels/NetworkFlow.js +12 -1
- package/build/dist/Models/AnalyticsModels/NetworkFlow.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
- package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +31 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +12 -2
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +9 -2
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +47 -56
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/MonitorGroupService.js +15 -2
- package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +66 -3
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +244 -102
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +57 -33
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
- package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +34 -12
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +25 -8
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimelineReconciler.js +268 -0
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimelineReconciler.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
- package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
- package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
- package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
- package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
- package/build/dist/Types/BaseDatabase/GreaterThan.js +8 -1
- package/build/dist/Types/BaseDatabase/GreaterThan.js.map +1 -1
- package/build/dist/Types/BaseDatabase/GreaterThanOrEqual.js +8 -1
- package/build/dist/Types/BaseDatabase/GreaterThanOrEqual.js.map +1 -1
- package/build/dist/Types/BaseDatabase/GreaterThanOrNull.js +9 -1
- package/build/dist/Types/BaseDatabase/GreaterThanOrNull.js.map +1 -1
- package/build/dist/Types/BaseDatabase/LessThan.js +8 -1
- package/build/dist/Types/BaseDatabase/LessThan.js.map +1 -1
- package/build/dist/Types/BaseDatabase/LessThanOrEqual.js +9 -1
- package/build/dist/Types/BaseDatabase/LessThanOrEqual.js.map +1 -1
- package/build/dist/Types/BaseDatabase/LessThanOrNull.js +8 -1
- package/build/dist/Types/BaseDatabase/LessThanOrNull.js.map +1 -1
- package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
- package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
- package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
- package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
- package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
- package/build/dist/Utils/StatusPage/ResourceUptime.js +4 -1
- package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
- package/build/dist/Utils/Uptime/UptimeUtil.js +86 -33
- package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.ts +0 -101
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.js +0 -40
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import Columns from "../../../UI/Components/ModelTable/Columns";
|
|
2
|
+
import {
|
|
3
|
+
getRelationSelectFromColumns,
|
|
4
|
+
getSelectFromColumns,
|
|
5
|
+
} from "../../../UI/Components/ModelTable/SelectFromColumns";
|
|
6
|
+
import FieldType from "../../../UI/Components/Types/FieldType";
|
|
7
|
+
import Alert from "../../../Models/DatabaseModels/Alert";
|
|
8
|
+
import ProxmoxCluster from "../../../Models/DatabaseModels/ProxmoxCluster";
|
|
9
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
10
|
+
import { describe, expect, test } from "@jest/globals";
|
|
11
|
+
|
|
12
|
+
describe("ModelTable SelectFromColumns", () => {
|
|
13
|
+
describe("getSelectFromColumns", () => {
|
|
14
|
+
test("it selects every field a column declares, not just the first", () => {
|
|
15
|
+
/*
|
|
16
|
+
* Regression for OneUptime/oneuptime#2756: the Proxmox clusters list
|
|
17
|
+
* rendered "0/3 online" for a fully online cluster because only the
|
|
18
|
+
* first key of the column's field map was selected, leaving
|
|
19
|
+
* onlineNodeCount undefined on every row.
|
|
20
|
+
*/
|
|
21
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
22
|
+
{
|
|
23
|
+
field: {
|
|
24
|
+
nodeCount: true,
|
|
25
|
+
onlineNodeCount: true,
|
|
26
|
+
},
|
|
27
|
+
title: "Nodes",
|
|
28
|
+
type: FieldType.Element,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const select: Record<string, unknown> =
|
|
33
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
34
|
+
columns: columns,
|
|
35
|
+
model: new ProxmoxCluster(),
|
|
36
|
+
}) as Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
expect(select["nodeCount"]).toBe(true);
|
|
39
|
+
expect(select["onlineNodeCount"]).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("it always selects _id", () => {
|
|
43
|
+
const select: Record<string, unknown> =
|
|
44
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
45
|
+
columns: [],
|
|
46
|
+
model: new ProxmoxCluster(),
|
|
47
|
+
}) as Record<string, unknown>;
|
|
48
|
+
|
|
49
|
+
expect(select["_id"]).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("it merges fields across multiple columns", () => {
|
|
53
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
54
|
+
{
|
|
55
|
+
field: { name: true },
|
|
56
|
+
title: "Name",
|
|
57
|
+
type: FieldType.Text,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
field: { guestCount: true, storageCount: true },
|
|
61
|
+
title: "Inventory",
|
|
62
|
+
type: FieldType.Element,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const select: Record<string, unknown> =
|
|
67
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
68
|
+
columns: columns,
|
|
69
|
+
model: new ProxmoxCluster(),
|
|
70
|
+
}) as Record<string, unknown>;
|
|
71
|
+
|
|
72
|
+
expect(select["name"]).toBe(true);
|
|
73
|
+
expect(select["guestCount"]).toBe(true);
|
|
74
|
+
expect(select["storageCount"]).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("it skips columns that declare no field", () => {
|
|
78
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
79
|
+
{
|
|
80
|
+
field: undefined as never,
|
|
81
|
+
title: "Actions",
|
|
82
|
+
type: FieldType.Element,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const select: Record<string, unknown> =
|
|
87
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
88
|
+
columns: columns,
|
|
89
|
+
model: new ProxmoxCluster(),
|
|
90
|
+
}) as Record<string, unknown>;
|
|
91
|
+
|
|
92
|
+
expect(Object.keys(select)).toEqual(["_id"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("it throws when the primary field is not a column on the model", () => {
|
|
96
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
97
|
+
{
|
|
98
|
+
field: { notARealColumn: true } as never,
|
|
99
|
+
title: "Nope",
|
|
100
|
+
type: FieldType.Text,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
expect(() => {
|
|
105
|
+
return getSelectFromColumns<ProxmoxCluster>({
|
|
106
|
+
columns: columns,
|
|
107
|
+
model: new ProxmoxCluster(),
|
|
108
|
+
});
|
|
109
|
+
}).toThrow(BadDataException);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("it ignores a non-column secondary field rather than blanking the page", () => {
|
|
113
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
114
|
+
{
|
|
115
|
+
field: { nodeCount: true, notARealColumn: true } as never,
|
|
116
|
+
title: "Nodes",
|
|
117
|
+
type: FieldType.Element,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const consoleError: jest.SpyInstance = jest
|
|
122
|
+
.spyOn(console, "error")
|
|
123
|
+
.mockImplementation(() => {});
|
|
124
|
+
|
|
125
|
+
const select: Record<string, unknown> =
|
|
126
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
127
|
+
columns: columns,
|
|
128
|
+
model: new ProxmoxCluster(),
|
|
129
|
+
}) as Record<string, unknown>;
|
|
130
|
+
|
|
131
|
+
expect(select["nodeCount"]).toBe(true);
|
|
132
|
+
expect(select["notARealColumn"]).toBeUndefined();
|
|
133
|
+
// It must not drop the field silently — that is how #2756 hid.
|
|
134
|
+
expect(consoleError).toHaveBeenCalled();
|
|
135
|
+
|
|
136
|
+
consoleError.mockRestore();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("it drops a secondary field the user cannot read, so the API does not reject the whole request", () => {
|
|
140
|
+
/*
|
|
141
|
+
* checkSelectPermission throws NotAuthorizedException for the entire
|
|
142
|
+
* request when the select names an unreadable column, so widening the
|
|
143
|
+
* select must never add one.
|
|
144
|
+
*/
|
|
145
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
146
|
+
{
|
|
147
|
+
field: {
|
|
148
|
+
nodeCount: true,
|
|
149
|
+
onlineNodeCount: true,
|
|
150
|
+
},
|
|
151
|
+
title: "Nodes",
|
|
152
|
+
type: FieldType.Element,
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const select: Record<string, unknown> =
|
|
157
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
158
|
+
columns: columns,
|
|
159
|
+
model: new ProxmoxCluster(),
|
|
160
|
+
hasPermissionToReadField: (field: string): boolean => {
|
|
161
|
+
return field !== "onlineNodeCount";
|
|
162
|
+
},
|
|
163
|
+
}) as Record<string, unknown>;
|
|
164
|
+
|
|
165
|
+
expect(select["nodeCount"]).toBe(true);
|
|
166
|
+
expect(select["onlineNodeCount"]).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("it keeps the primary field even when the permission callback rejects it, preserving prior behaviour", () => {
|
|
170
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
171
|
+
{
|
|
172
|
+
field: { nodeCount: true, onlineNodeCount: true },
|
|
173
|
+
title: "Nodes",
|
|
174
|
+
type: FieldType.Element,
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const select: Record<string, unknown> =
|
|
179
|
+
getSelectFromColumns<ProxmoxCluster>({
|
|
180
|
+
columns: columns,
|
|
181
|
+
model: new ProxmoxCluster(),
|
|
182
|
+
hasPermissionToReadField: (): boolean => {
|
|
183
|
+
return false;
|
|
184
|
+
},
|
|
185
|
+
}) as Record<string, unknown>;
|
|
186
|
+
|
|
187
|
+
expect(select["nodeCount"]).toBe(true);
|
|
188
|
+
expect(select["onlineNodeCount"]).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("getRelationSelectFromColumns", () => {
|
|
193
|
+
test("it selects every relation a column declares, not just the first", () => {
|
|
194
|
+
const columns: Columns<Alert> = [
|
|
195
|
+
{
|
|
196
|
+
field: {
|
|
197
|
+
monitor: { name: true, _id: true },
|
|
198
|
+
labels: { name: true, color: true },
|
|
199
|
+
},
|
|
200
|
+
title: "Resource",
|
|
201
|
+
type: FieldType.Element,
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
const relationSelect: Record<string, unknown> =
|
|
206
|
+
getRelationSelectFromColumns<Alert>({
|
|
207
|
+
columns: columns,
|
|
208
|
+
model: new Alert(),
|
|
209
|
+
}) as Record<string, unknown>;
|
|
210
|
+
|
|
211
|
+
expect(relationSelect["monitor"]).toEqual({ name: true, _id: true });
|
|
212
|
+
expect(relationSelect["labels"]).toEqual({ name: true, color: true });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("it leaves non-entity fields out of the relation select", () => {
|
|
216
|
+
const columns: Columns<ProxmoxCluster> = [
|
|
217
|
+
{
|
|
218
|
+
field: {
|
|
219
|
+
nodeCount: true,
|
|
220
|
+
onlineNodeCount: true,
|
|
221
|
+
},
|
|
222
|
+
title: "Nodes",
|
|
223
|
+
type: FieldType.Element,
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const relationSelect: Record<string, unknown> =
|
|
228
|
+
getRelationSelectFromColumns<ProxmoxCluster>({
|
|
229
|
+
columns: columns,
|
|
230
|
+
model: new ProxmoxCluster(),
|
|
231
|
+
}) as Record<string, unknown>;
|
|
232
|
+
|
|
233
|
+
expect(relationSelect).toEqual({});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("it merges the same relation declared by two columns", () => {
|
|
237
|
+
const columns: Columns<Alert> = [
|
|
238
|
+
{
|
|
239
|
+
field: { monitor: { name: true } },
|
|
240
|
+
title: "Monitor",
|
|
241
|
+
type: FieldType.Element,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
field: { monitor: { _id: true } },
|
|
245
|
+
title: "Monitor Id",
|
|
246
|
+
type: FieldType.Element,
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
const relationSelect: Record<string, unknown> =
|
|
251
|
+
getRelationSelectFromColumns<Alert>({
|
|
252
|
+
columns: columns,
|
|
253
|
+
model: new Alert(),
|
|
254
|
+
}) as Record<string, unknown>;
|
|
255
|
+
|
|
256
|
+
expect(relationSelect["monitor"]).toEqual({ name: true, _id: true });
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|