@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.
- package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/LogDropFilter.ts +9 -20
- package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
- package/Models/DatabaseModels/StatusPageResource.ts +73 -0
- package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
- package/Server/API/BaseAPI.ts +13 -2
- package/Server/API/DashboardAPI.ts +39 -0
- package/Server/EnvironmentConfig.ts +31 -1
- package/Server/Infrastructure/GlobalCache.ts +115 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertStateTimelineService.ts +20 -0
- package/Server/Services/CephClusterService.ts +26 -86
- package/Server/Services/CloudResourceService.ts +23 -83
- package/Server/Services/DatabaseService.ts +111 -0
- package/Server/Services/DockerHostService.ts +19 -79
- package/Server/Services/DockerSwarmClusterService.ts +27 -88
- package/Server/Services/HostService.ts +46 -99
- package/Server/Services/IncidentStateTimelineService.ts +20 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/IoTFleetService.ts +19 -79
- package/Server/Services/KubernetesClusterService.ts +17 -78
- package/Server/Services/LabelService.ts +57 -10
- package/Server/Services/MetricTypeService.ts +84 -0
- package/Server/Services/MonitorService.ts +89 -14
- package/Server/Services/MonitorStatusTimelineService.ts +29 -0
- package/Server/Services/MonitorTemplateService.ts +65 -0
- package/Server/Services/PodmanHostService.ts +19 -79
- package/Server/Services/ProxmoxClusterService.ts +23 -83
- package/Server/Services/RumApplicationService.ts +19 -80
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
- package/Server/Services/ServerlessFunctionService.ts +24 -85
- package/Server/Services/ServiceService.ts +299 -73
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
- package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
- package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
- package/Server/Services/TelemetryEntityService.ts +1 -0
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
- package/Server/Utils/Monitor/MonitorResource.ts +63 -36
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
- package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
- package/Server/Utils/SingleFlight.ts +83 -0
- package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
- package/Server/Utils/Telemetry/Telemetry.ts +351 -122
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
- package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
- package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
- package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
- package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
- package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
- package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
- package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
- package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
- package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
- package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
- package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
- package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
- package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
- package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
- package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
- package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
- package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
- package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
- package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
- package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
- package/Tests/Types/Decimal.test.ts +37 -0
- package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
- package/Tests/Types/Port.test.ts +38 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
- package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
- package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
- package/Types/Dashboard/DashboardTemplates.ts +397 -0
- package/Types/Database/BigIntColumnTransformer.ts +74 -0
- package/Types/Database/DatabaseProperty.ts +26 -0
- package/Types/Database/NumericColumnValue.ts +128 -0
- package/Types/Decimal.ts +16 -4
- package/Types/Permission.ts +48 -0
- package/Types/Port.ts +16 -4
- package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
- package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
- package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +11 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +39 -0
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +27 -1
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +26 -76
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +23 -73
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +86 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +19 -69
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +58 -102
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +19 -69
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +42 -7
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MetricTypeService.js +74 -0
- package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +81 -9
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +19 -69
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +19 -70
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +236 -61
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
- package/build/dist/Server/Utils/SingleFlight.js +76 -0
- package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
- package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
- package/build/dist/Types/Database/DatabaseProperty.js +25 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/NumericColumnValue.js +97 -0
- package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
- package/build/dist/Types/Decimal.js +14 -3
- package/build/dist/Types/Decimal.js.map +1 -1
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Port.js +15 -5
- package/build/dist/Types/Port.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import AllModelTypes from "../../../Models/DatabaseModels/Index";
|
|
2
|
+
import LogDropFilter from "../../../Models/DatabaseModels/LogDropFilter";
|
|
3
|
+
import TraceDropFilter from "../../../Models/DatabaseModels/TraceDropFilter";
|
|
4
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
5
|
+
import { isNumericTableColumnType } from "../../../Types/Database/NumericColumnValue";
|
|
6
|
+
import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
|
|
7
|
+
import { describe, expect, it } from "@jest/globals";
|
|
8
|
+
import { getMetadataArgsStorage } from "typeorm";
|
|
9
|
+
import { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
|
|
10
|
+
import { ValueTransformer } from "typeorm/decorator/options/ValueTransformer";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Contract under test — two schema-wide invariants about column
|
|
14
|
+
* transformers, not any one model's behaviour. Both are traps that are
|
|
15
|
+
* invisible at the call site: the model reads as a perfectly ordinary
|
|
16
|
+
* column, and the failure surfaces as a Postgres error a long way away.
|
|
17
|
+
*
|
|
18
|
+
* 1. A transformer must not fold `undefined` into `null`.
|
|
19
|
+
*
|
|
20
|
+
* TypeORM runs the transformer BEFORE deciding whether the caller
|
|
21
|
+
* supplied the column:
|
|
22
|
+
*
|
|
23
|
+
* InsertQueryBuilder.createColumnValueExpression
|
|
24
|
+
* -> driver.preparePersistentValue (runs transformer.to)
|
|
25
|
+
* -> if (value === undefined) expression += "DEFAULT"
|
|
26
|
+
*
|
|
27
|
+
* So on a `NOT NULL DEFAULT x` column, answering `null` makes the DEFAULT
|
|
28
|
+
* unreachable and every insert that omits the column dies on the not-null
|
|
29
|
+
* constraint. That is what shipped for the drop-filter drop counters, and
|
|
30
|
+
* creating any log drop filter in the dashboard returned HTTP 500:
|
|
31
|
+
*
|
|
32
|
+
* null value in column "droppedCount" of relation "LogDropFilter"
|
|
33
|
+
* violates not-null constraint
|
|
34
|
+
*
|
|
35
|
+
* github.com/OneUptime/oneuptime/issues/3026
|
|
36
|
+
*
|
|
37
|
+
* 2. A transformer on a number column must accept a raw number.
|
|
38
|
+
*
|
|
39
|
+
* `BaseModel.fromJSON` normalizes strings bound for number columns (see
|
|
40
|
+
* github.com/OneUptime/oneuptime/issues/3027), so a column declared
|
|
41
|
+
* `TableColumnType.Number` now reliably holds a number by the time the
|
|
42
|
+
* transformer sees it — even when its in-memory type is a wrapped object
|
|
43
|
+
* like `Port` or `Decimal`. A transformer that only knew how to handle
|
|
44
|
+
* its own class and a string answered `null` for a number, which wrote
|
|
45
|
+
* NULL over a value the user had just typed.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
interface TransformerColumn {
|
|
49
|
+
entity: string;
|
|
50
|
+
property: string;
|
|
51
|
+
transformer: ValueTransformer;
|
|
52
|
+
defaultValue: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* TypeORM accepts either a single transformer or an array applied in order.
|
|
57
|
+
* `to` runs last-to-first over an array, so the *last* entry is the one that
|
|
58
|
+
* sees the raw entity value.
|
|
59
|
+
*/
|
|
60
|
+
function firstTransformerToRun(
|
|
61
|
+
transformer: ValueTransformer | Array<ValueTransformer>,
|
|
62
|
+
): ValueTransformer {
|
|
63
|
+
if (Array.isArray(transformer)) {
|
|
64
|
+
return transformer[transformer.length - 1] as ValueTransformer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return transformer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function notNullColumnsWithADefaultAndATransformer(): Array<TransformerColumn> {
|
|
71
|
+
const columns: Array<TransformerColumn> = [];
|
|
72
|
+
|
|
73
|
+
for (const column of getMetadataArgsStorage()
|
|
74
|
+
.columns as Array<ColumnMetadataArgs>) {
|
|
75
|
+
const options: ColumnMetadataArgs["options"] = column.options || {};
|
|
76
|
+
|
|
77
|
+
if (!options.transformer) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (options.nullable !== false) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (options.default === undefined || options.default === null) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
columns.push({
|
|
90
|
+
entity:
|
|
91
|
+
(column.target as { name?: string })?.name || String(column.target),
|
|
92
|
+
property: column.propertyName,
|
|
93
|
+
transformer: firstTransformerToRun(options.transformer),
|
|
94
|
+
defaultValue: options.default,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return columns;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe("NOT NULL columns that carry both a DEFAULT and a transformer", () => {
|
|
102
|
+
const columns: Array<TransformerColumn> =
|
|
103
|
+
notNullColumnsWithADefaultAndATransformer();
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
* If this ever finds nothing, the filter above has drifted (a renamed
|
|
107
|
+
* option, a TypeORM upgrade) and the real assertion below is vacuously
|
|
108
|
+
* passing — which is the same as deleting it.
|
|
109
|
+
*/
|
|
110
|
+
it("finds the drop-filter counters, so the scan itself is known to work", () => {
|
|
111
|
+
const scanned: Array<string> = columns.map((column: TransformerColumn) => {
|
|
112
|
+
return `${column.entity}.${column.property}`;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(scanned).toContain("LogDropFilter.droppedCount");
|
|
116
|
+
expect(scanned).toContain("TraceDropFilter.droppedCount");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("never lets a transformer swallow undefined into null", () => {
|
|
120
|
+
const offenders: Array<string> = [];
|
|
121
|
+
|
|
122
|
+
for (const column of columns) {
|
|
123
|
+
if (!column.transformer.to) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (column.transformer.to(undefined) !== undefined) {
|
|
128
|
+
offenders.push(
|
|
129
|
+
`${column.entity}.${column.property} (NOT NULL DEFAULT ${String(
|
|
130
|
+
column.defaultValue,
|
|
131
|
+
)}) maps undefined to ${String(column.transformer.to(undefined))}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/*
|
|
137
|
+
* Named rather than counted: the message has to say which column will
|
|
138
|
+
* 500 on every insert, because the failure surfaces as a Postgres
|
|
139
|
+
* constraint error a long way from the model file.
|
|
140
|
+
*/
|
|
141
|
+
expect(offenders).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* Every column declared as a number type that also carries a transformer.
|
|
147
|
+
*
|
|
148
|
+
* The declared type is not a perfect proxy for the in-memory one — an SMTP
|
|
149
|
+
* port is declared `Number` but typed `Port`, a usage figure is declared
|
|
150
|
+
* `Number` but typed `Decimal` — and those are exactly the columns where a
|
|
151
|
+
* transformer might never have been handed a bare number before.
|
|
152
|
+
*/
|
|
153
|
+
function numericColumnsWithATransformer(): Array<TransformerColumn> {
|
|
154
|
+
const columns: Array<TransformerColumn> = [];
|
|
155
|
+
|
|
156
|
+
for (const modelType of AllModelTypes) {
|
|
157
|
+
const model: BaseModel = new modelType();
|
|
158
|
+
const entity: string = modelType.name;
|
|
159
|
+
|
|
160
|
+
for (const columnName of model.getTableColumns().columns) {
|
|
161
|
+
const metadata: TableColumnMetadata =
|
|
162
|
+
model.getTableColumnMetadata(columnName);
|
|
163
|
+
|
|
164
|
+
if (!metadata || !isNumericTableColumnType(metadata.type)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const column: ColumnMetadataArgs | undefined = (
|
|
169
|
+
getMetadataArgsStorage().columns as Array<ColumnMetadataArgs>
|
|
170
|
+
).find((candidate: ColumnMetadataArgs) => {
|
|
171
|
+
return (
|
|
172
|
+
(candidate.target as { name?: string })?.name === entity &&
|
|
173
|
+
candidate.propertyName === columnName
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!column?.options?.transformer) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
columns.push({
|
|
182
|
+
entity: entity,
|
|
183
|
+
property: columnName,
|
|
184
|
+
transformer: firstTransformerToRun(column.options.transformer),
|
|
185
|
+
defaultValue: column.options.default,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return columns;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
describe("number columns that carry a transformer", () => {
|
|
194
|
+
const columns: Array<TransformerColumn> = numericColumnsWithATransformer();
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* The wrapped-type columns are the whole reason this scan exists. If it
|
|
198
|
+
* stops finding them, the assertion below is passing vacuously.
|
|
199
|
+
*/
|
|
200
|
+
it("finds the wrapped-type number columns, so the scan is known to work", () => {
|
|
201
|
+
const scanned: Array<string> = columns.map((column: TransformerColumn) => {
|
|
202
|
+
return `${column.entity}.${column.property}`;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(scanned).toContain("GlobalConfig.smtpPort");
|
|
206
|
+
expect(scanned).toContain("ProjectSmtpConfig.port");
|
|
207
|
+
expect(scanned).toContain("TelemetryUsageBilling.dataIngestedInGB");
|
|
208
|
+
expect(scanned).toContain("LogDropFilter.droppedCount");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
* `BaseModel.fromJSON` now hands these transformers a real number where it
|
|
213
|
+
* used to hand them the string the form produced. A transformer that
|
|
214
|
+
* answers null for a number writes NULL over a value the user supplied —
|
|
215
|
+
* silently on a nullable column, and as a 500 on a NOT NULL one.
|
|
216
|
+
*/
|
|
217
|
+
it("accepts a raw number rather than answering null", () => {
|
|
218
|
+
const offenders: Array<string> = [];
|
|
219
|
+
|
|
220
|
+
for (const column of columns) {
|
|
221
|
+
if (!column.transformer.to) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* 1 is in range for every number column in the schema; 0 is the value
|
|
227
|
+
* a truthiness check silently drops.
|
|
228
|
+
*/
|
|
229
|
+
for (const sample of [1, 0]) {
|
|
230
|
+
let stored: unknown;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
stored = column.transformer.to(sample);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
offenders.push(
|
|
236
|
+
`${column.entity}.${column.property} threw on ${sample}: ${
|
|
237
|
+
(error as Error).message
|
|
238
|
+
}`,
|
|
239
|
+
);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (stored === null || stored === undefined) {
|
|
244
|
+
offenders.push(
|
|
245
|
+
`${column.entity}.${column.property} maps the number ${sample} to ${String(
|
|
246
|
+
stored,
|
|
247
|
+
)}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
expect(offenders).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("drop filter counter columns", () => {
|
|
258
|
+
/*
|
|
259
|
+
* Spelled out for the two columns the bug actually shipped on, so the
|
|
260
|
+
* regression is legible without reading the scan above.
|
|
261
|
+
*/
|
|
262
|
+
const CASES: Array<{ name: string }> = [
|
|
263
|
+
{ name: LogDropFilter.name },
|
|
264
|
+
{ name: TraceDropFilter.name },
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
describe.each(CASES)("$name.droppedCount", ({ name }: { name: string }) => {
|
|
268
|
+
function droppedCountColumn(): ColumnMetadataArgs {
|
|
269
|
+
const column: ColumnMetadataArgs | undefined = (
|
|
270
|
+
getMetadataArgsStorage().columns as Array<ColumnMetadataArgs>
|
|
271
|
+
).find((candidate: ColumnMetadataArgs) => {
|
|
272
|
+
return (
|
|
273
|
+
(candidate.target as { name?: string })?.name === name &&
|
|
274
|
+
candidate.propertyName === "droppedCount"
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (!column) {
|
|
279
|
+
throw new Error(`${name}.droppedCount is not a registered column`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return column;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
it("is NOT NULL with a zero default, so the UI never renders a blank counter", () => {
|
|
286
|
+
const options: ColumnMetadataArgs["options"] = droppedCountColumn()
|
|
287
|
+
.options as ColumnMetadataArgs["options"];
|
|
288
|
+
|
|
289
|
+
expect(options.nullable).toBe(false);
|
|
290
|
+
expect(options.default).toBe(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/*
|
|
294
|
+
* The exact regression: an insert that leaves the counter alone — which
|
|
295
|
+
* is every create, since only the ingest path writes it — has to reach
|
|
296
|
+
* Postgres as DEFAULT, not as NULL.
|
|
297
|
+
*/
|
|
298
|
+
it("lets a create fall through to that default instead of writing NULL", () => {
|
|
299
|
+
const transformer: ValueTransformer = firstTransformerToRun(
|
|
300
|
+
droppedCountColumn().options.transformer as ValueTransformer,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(transformer.to!(undefined)).toBeUndefined();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
2
|
+
import LogDropFilter from "../../../Models/DatabaseModels/LogDropFilter";
|
|
3
|
+
import TraceDropFilter from "../../../Models/DatabaseModels/TraceDropFilter";
|
|
4
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
5
|
+
import GlobalConfig from "../../../Models/DatabaseModels/GlobalConfig";
|
|
6
|
+
import LogDropFilterAction from "../../../Types/Log/LogDropFilterAction";
|
|
7
|
+
import { coerceNumericColumnsInJSON } from "../../../Types/Database/NumericColumnValue";
|
|
8
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
9
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
10
|
+
import Port from "../../../Types/Port";
|
|
11
|
+
import { describe, expect, it } from "@jest/globals";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Contract under test — what a number column holds after a JSON round trip.
|
|
15
|
+
*
|
|
16
|
+
* `BaseModel.fromJSON` is the single door every model comes through, and it
|
|
17
|
+
* runs on BOTH sides of the wire: ModelForm builds the model it is about to
|
|
18
|
+
* POST with it, and BaseAPI rebuilds that model from the request body with
|
|
19
|
+
* it. That makes it the one place that can fix the following, once.
|
|
20
|
+
*
|
|
21
|
+
* HTML has no numeric input event — `<input type="number">` gives you
|
|
22
|
+
* `e.target.value`, a string — so the dashboard posted "10" for every number
|
|
23
|
+
* field in the product. Postgres coerces '10' to 10 on the way in, so this
|
|
24
|
+
* was invisible everywhere except where something reads the value before it
|
|
25
|
+
* is stored. The drop-filter save hook does exactly that, and a user who
|
|
26
|
+
* typed a perfectly good sample percentage got:
|
|
27
|
+
*
|
|
28
|
+
* HTTP 400 — Sample percentage is required when the action is "Sample".
|
|
29
|
+
*
|
|
30
|
+
* github.com/OneUptime/oneuptime/issues/3027
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
describe("BaseModel.fromJSON on number columns", () => {
|
|
34
|
+
/*
|
|
35
|
+
* The reported payload, as the dashboard actually sends it: every scalar
|
|
36
|
+
* that came from a text-ish input is a string.
|
|
37
|
+
*/
|
|
38
|
+
it("turns the dashboard's sample-filter payload into real numbers", () => {
|
|
39
|
+
const posted: JSONObject = {
|
|
40
|
+
name: "Sample healthcheck logs",
|
|
41
|
+
filterQuery: "body LIKE 'healthcheck'",
|
|
42
|
+
action: LogDropFilterAction.Sample,
|
|
43
|
+
samplePercentage: "10",
|
|
44
|
+
sortOrder: "1",
|
|
45
|
+
isEnabled: true,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
49
|
+
posted,
|
|
50
|
+
LogDropFilter,
|
|
51
|
+
) as LogDropFilter;
|
|
52
|
+
|
|
53
|
+
expect(model.samplePercentage).toBe(10);
|
|
54
|
+
expect(typeof model.samplePercentage).toBe("number");
|
|
55
|
+
expect(model.sortOrder).toBe(1);
|
|
56
|
+
expect(typeof model.sortOrder).toBe("number");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("does the same for trace drop filters", () => {
|
|
60
|
+
const model: TraceDropFilter = BaseModel.fromJSON(
|
|
61
|
+
{
|
|
62
|
+
action: "sample",
|
|
63
|
+
samplePercentage: "25",
|
|
64
|
+
filterQuery: "name = 'GET /health'",
|
|
65
|
+
},
|
|
66
|
+
TraceDropFilter,
|
|
67
|
+
) as TraceDropFilter;
|
|
68
|
+
|
|
69
|
+
expect(model.samplePercentage).toBe(25);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("accepts a percentage that was already a number", () => {
|
|
73
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
74
|
+
{ samplePercentage: 10 },
|
|
75
|
+
LogDropFilter,
|
|
76
|
+
) as LogDropFilter;
|
|
77
|
+
|
|
78
|
+
expect(model.samplePercentage).toBe(10);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps the whole usable percentage range intact", () => {
|
|
82
|
+
for (const percentage of [1, 5, 10, 50, 99]) {
|
|
83
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
84
|
+
{ samplePercentage: String(percentage) },
|
|
85
|
+
LogDropFilter,
|
|
86
|
+
) as LogDropFilter;
|
|
87
|
+
|
|
88
|
+
expect(model.samplePercentage).toBe(percentage);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("leaves an unset percentage unset rather than defaulting it to zero", () => {
|
|
93
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
94
|
+
{ action: LogDropFilterAction.Drop, filterQuery: "severityText='Debug'" },
|
|
95
|
+
LogDropFilter,
|
|
96
|
+
) as LogDropFilter;
|
|
97
|
+
|
|
98
|
+
expect(model.samplePercentage).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("leaves a null percentage null", () => {
|
|
102
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
103
|
+
{ samplePercentage: null },
|
|
104
|
+
LogDropFilter,
|
|
105
|
+
) as LogDropFilter;
|
|
106
|
+
|
|
107
|
+
expect(model.samplePercentage).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* `Number("")` is 0, and 0 is a meaningful sample percentage the validator
|
|
112
|
+
* rejects on purpose. Inventing it from a cleared field would turn a
|
|
113
|
+
* user's blank into a rejection about a value they never typed.
|
|
114
|
+
*/
|
|
115
|
+
it("does not turn a cleared field into zero", () => {
|
|
116
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
117
|
+
{ samplePercentage: "" },
|
|
118
|
+
LogDropFilter,
|
|
119
|
+
) as LogDropFilter;
|
|
120
|
+
|
|
121
|
+
expect(model.samplePercentage).not.toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("leaves unparsable text alone for validation to reject", () => {
|
|
125
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
126
|
+
{ samplePercentage: "abc" },
|
|
127
|
+
LogDropFilter,
|
|
128
|
+
) as LogDropFilter;
|
|
129
|
+
|
|
130
|
+
expect(model.samplePercentage).toBe("abc" as unknown as number);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* This is deliberately not drop-filter specific: the string arrived from a
|
|
135
|
+
* shared form component, so the fix has to hold for every number column or
|
|
136
|
+
* the next hook that reads one before storage repeats issue 3027.
|
|
137
|
+
*/
|
|
138
|
+
it("applies to number columns on unrelated models too", () => {
|
|
139
|
+
const monitor: Monitor = BaseModel.fromJSON(
|
|
140
|
+
{ name: "API", minimumProbeAgreement: "3" },
|
|
141
|
+
Monitor,
|
|
142
|
+
) as Monitor;
|
|
143
|
+
|
|
144
|
+
expect(monitor.minimumProbeAgreement).toBe(3);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("does not touch columns that merely look numeric", () => {
|
|
148
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
149
|
+
{
|
|
150
|
+
name: "12345",
|
|
151
|
+
action: "sample",
|
|
152
|
+
filterQuery: "1234",
|
|
153
|
+
samplePercentage: "10",
|
|
154
|
+
},
|
|
155
|
+
LogDropFilter,
|
|
156
|
+
) as LogDropFilter;
|
|
157
|
+
|
|
158
|
+
expect(model.name).toBe("12345");
|
|
159
|
+
expect(model.filterQuery).toBe("1234");
|
|
160
|
+
expect(model.action).toBe("sample");
|
|
161
|
+
// ...while the number column beside them is still coerced.
|
|
162
|
+
expect(model.samplePercentage).toBe(10);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("still deserializes the columns beside it normally", () => {
|
|
166
|
+
const projectId: string = "22222222-2222-4222-8222-222222222222";
|
|
167
|
+
|
|
168
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
169
|
+
{
|
|
170
|
+
projectId: new ObjectID(projectId).toJSON(),
|
|
171
|
+
samplePercentage: "10",
|
|
172
|
+
},
|
|
173
|
+
LogDropFilter,
|
|
174
|
+
) as LogDropFilter;
|
|
175
|
+
|
|
176
|
+
expect(model.projectId).toBeInstanceOf(ObjectID);
|
|
177
|
+
expect(model.projectId?.toString()).toBe(projectId);
|
|
178
|
+
expect(model.samplePercentage).toBe(10);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* The browser leg of the same trip: ModelForm hands fromJSON the raw form
|
|
183
|
+
* values and then serializes the result as the request body. If coercion
|
|
184
|
+
* did not survive toJSON, the server would still receive a string.
|
|
185
|
+
*/
|
|
186
|
+
it("survives the toJSON that turns the model into a request body", () => {
|
|
187
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
188
|
+
{
|
|
189
|
+
name: "Sample healthcheck logs",
|
|
190
|
+
filterQuery: "body LIKE 'healthcheck'",
|
|
191
|
+
action: LogDropFilterAction.Sample,
|
|
192
|
+
samplePercentage: "10",
|
|
193
|
+
},
|
|
194
|
+
LogDropFilter,
|
|
195
|
+
) as LogDropFilter;
|
|
196
|
+
|
|
197
|
+
const body: JSONObject = BaseModel.toJSON(model, LogDropFilter);
|
|
198
|
+
|
|
199
|
+
expect(body["samplePercentage"]).toBe(10);
|
|
200
|
+
|
|
201
|
+
const rebuilt: LogDropFilter = BaseModel.fromJSON(
|
|
202
|
+
body,
|
|
203
|
+
LogDropFilter,
|
|
204
|
+
) as LogDropFilter;
|
|
205
|
+
|
|
206
|
+
expect(rebuilt.samplePercentage).toBe(10);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/*
|
|
210
|
+
* `droppedCount` is bigint and ingest-owned. The API serializes it as a
|
|
211
|
+
* number, but a client (or a driver) handing back the string form must not
|
|
212
|
+
* leave a string on a column the dashboard does arithmetic on.
|
|
213
|
+
*/
|
|
214
|
+
it("normalizes a bigint counter that arrives as a string", () => {
|
|
215
|
+
const model: LogDropFilter = BaseModel.fromJSON(
|
|
216
|
+
{ droppedCount: "4294967296" },
|
|
217
|
+
LogDropFilter,
|
|
218
|
+
) as LogDropFilter;
|
|
219
|
+
|
|
220
|
+
expect(model.droppedCount).toBe(4294967296);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/*
|
|
224
|
+
* Not every `TableColumnType.Number` column holds a bare number in
|
|
225
|
+
* memory: an SMTP port is declared Number but typed `Port`, and it is
|
|
226
|
+
* written through Port's own transformer. Coercing "587" to 587 there is
|
|
227
|
+
* only safe because that transformer accepts a raw number — so assert the
|
|
228
|
+
* pair, not just the coercion.
|
|
229
|
+
*/
|
|
230
|
+
it("keeps a Port column storable after coercing its string form", () => {
|
|
231
|
+
const config: GlobalConfig = BaseModel.fromJSON(
|
|
232
|
+
{ smtpPort: "587" },
|
|
233
|
+
GlobalConfig,
|
|
234
|
+
) as GlobalConfig;
|
|
235
|
+
|
|
236
|
+
expect(config.smtpPort).toBe(587 as unknown as Port);
|
|
237
|
+
expect(Port.getDatabaseTransformer().to(config.smtpPort)).toBe(587);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
/*
|
|
242
|
+
* The update half. `BaseAPI.updateItem` never builds a model — it goes from
|
|
243
|
+
* the request body straight to a partial entity — so it needs the same
|
|
244
|
+
* normalization applied to a loose patch, or a PATCH and a POST disagree
|
|
245
|
+
* about what "10" is and the save-time hooks see different types depending
|
|
246
|
+
* on which verb the caller used.
|
|
247
|
+
*/
|
|
248
|
+
describe("coerceNumericColumnsInJSON on an update patch", () => {
|
|
249
|
+
it("coerces a number column the patch names", () => {
|
|
250
|
+
const patch: JSONObject = coerceNumericColumnsInJSON(
|
|
251
|
+
{ samplePercentage: "25" },
|
|
252
|
+
new LogDropFilter(),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(patch["samplePercentage"]).toBe(25);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("leaves the columns it does not own alone", () => {
|
|
259
|
+
const patch: JSONObject = coerceNumericColumnsInJSON(
|
|
260
|
+
{
|
|
261
|
+
name: "12345",
|
|
262
|
+
action: "sample",
|
|
263
|
+
filterQuery: "severityText = 'Debug'",
|
|
264
|
+
samplePercentage: "25",
|
|
265
|
+
isEnabled: false,
|
|
266
|
+
},
|
|
267
|
+
new LogDropFilter(),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(patch["name"]).toBe("12345");
|
|
271
|
+
expect(patch["action"]).toBe("sample");
|
|
272
|
+
expect(patch["filterQuery"]).toBe("severityText = 'Debug'");
|
|
273
|
+
expect(patch["isEnabled"]).toBe(false);
|
|
274
|
+
expect(patch["samplePercentage"]).toBe(25);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("ignores keys that are not columns at all", () => {
|
|
278
|
+
const patch: JSONObject = coerceNumericColumnsInJSON(
|
|
279
|
+
{ notAColumn: "10" },
|
|
280
|
+
new LogDropFilter(),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
expect(patch["notAColumn"]).toBe("10");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/*
|
|
287
|
+
* A PartialEntity may carry a `() => string` raw SQL expression instead of
|
|
288
|
+
* a literal. Coercion must not stringify or otherwise disturb one.
|
|
289
|
+
*/
|
|
290
|
+
it("passes a raw SQL expression through untouched", () => {
|
|
291
|
+
const expression: () => string = (): string => {
|
|
292
|
+
return '"samplePercentage" + 1';
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const patch: JSONObject = coerceNumericColumnsInJSON(
|
|
296
|
+
{ samplePercentage: expression as unknown as JSONObject },
|
|
297
|
+
new LogDropFilter(),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(patch["samplePercentage"]).toBe(expression);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("does not invent a value for a cleared field", () => {
|
|
304
|
+
const patch: JSONObject = coerceNumericColumnsInJSON(
|
|
305
|
+
{ samplePercentage: "" },
|
|
306
|
+
new LogDropFilter(),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
expect(patch["samplePercentage"]).not.toBe(0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("keeps an explicit null so a nullable column can be cleared", () => {
|
|
313
|
+
const patch: JSONObject = coerceNumericColumnsInJSON(
|
|
314
|
+
{ samplePercentage: null },
|
|
315
|
+
new LogDropFilter(),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(patch["samplePercentage"]).toBeNull();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
/*
|
|
322
|
+
* The API layer hands this a freshly-constructed model of the entity
|
|
323
|
+
* being updated, so it has to work off an instance rather than the class.
|
|
324
|
+
*/
|
|
325
|
+
it("works off any model instance, not just drop filters", () => {
|
|
326
|
+
const patch: JSONObject = coerceNumericColumnsInJSON(
|
|
327
|
+
{ minimumProbeAgreement: "3" },
|
|
328
|
+
new Monitor(),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(patch["minimumProbeAgreement"]).toBe(3);
|
|
332
|
+
});
|
|
333
|
+
});
|