@oneuptime/common 11.5.13 → 11.6.1

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 (156) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
  2. package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
  3. package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
  4. package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
  5. package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
  6. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
  7. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
  8. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  9. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  10. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  11. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  12. package/Server/API/GlobalConfigAPI.ts +62 -1
  13. package/Server/EnvironmentConfig.ts +22 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +61 -46
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/WorkflowService.ts +100 -39
  20. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  21. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  22. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
  23. package/Server/Utils/Express.ts +13 -0
  24. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  25. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  26. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  27. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  28. package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
  29. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  30. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  31. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  32. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
  33. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
  34. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  35. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  36. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  37. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  38. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  39. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  40. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  41. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  42. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  43. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  44. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  45. package/Tests/Types/HashCode.test.ts +41 -0
  46. package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
  47. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  48. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  49. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  50. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  51. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  52. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  53. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  54. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  55. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  56. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  57. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  58. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  59. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  60. package/Tests/Utils/Array.test.ts +156 -0
  61. package/Tests/Utils/CronTab.test.ts +199 -0
  62. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
  63. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  64. package/Tests/Utils/Number.test.ts +179 -0
  65. package/Tests/Utils/ValueFormatter.test.ts +305 -0
  66. package/Tests/Utils/VersionUtil.test.ts +348 -0
  67. package/Tests/jest.setup.ts +1 -0
  68. package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
  69. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  70. package/Types/Monitor/MonitorStep.ts +24 -4
  71. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  72. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  73. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  74. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  75. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  76. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  77. package/Utils/CronTab.ts +823 -0
  78. package/Utils/ModelImportExport.ts +21 -6
  79. package/Utils/VersionUtil.ts +260 -0
  80. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
  81. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
  82. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
  83. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
  84. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
  85. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
  86. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
  87. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
  88. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
  89. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
  90. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
  91. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
  92. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
  93. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  100. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  101. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  102. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  103. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +17 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Services/AnalyticsDatabaseService.js +53 -17
  113. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/DatabaseService.js +53 -1
  115. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  116. package/build/dist/Server/Services/WorkflowService.js +80 -30
  117. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  118. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  119. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  120. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  121. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  122. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
  123. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  124. package/build/dist/Server/Utils/Express.js +12 -0
  125. package/build/dist/Server/Utils/Express.js.map +1 -1
  126. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  127. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  128. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  129. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  131. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  133. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  134. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
  135. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  136. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  137. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  138. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  139. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  140. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  141. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  142. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  143. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  144. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  145. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  146. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  147. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  148. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  149. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  150. package/build/dist/Utils/CronTab.js +609 -0
  151. package/build/dist/Utils/CronTab.js.map +1 -0
  152. package/build/dist/Utils/ModelImportExport.js +20 -6
  153. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  154. package/build/dist/Utils/VersionUtil.js +193 -0
  155. package/build/dist/Utils/VersionUtil.js.map +1 -0
  156. package/package.json +1 -1
@@ -0,0 +1,122 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
3
+ import AnalyticsTableEngine from "../../../Types/AnalyticsDatabase/AnalyticsTableEngine";
4
+ import AnalyticsTableColumn from "../../../Types/AnalyticsDatabase/TableColumn";
5
+ import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
6
+
7
+ type ModelOptions = ConstructorParameters<typeof AnalyticsBaseModel>[0];
8
+
9
+ function projectIdColumn(): AnalyticsTableColumn {
10
+ return new AnalyticsTableColumn({
11
+ key: "projectId",
12
+ title: "Project ID",
13
+ description: "Tenant",
14
+ required: true,
15
+ type: TableColumnType.Text,
16
+ });
17
+ }
18
+
19
+ function modelOptions(overrides: Partial<ModelOptions> = {}): ModelOptions {
20
+ return {
21
+ tableName: "TestAnalyticsTable",
22
+ singularName: "Test row",
23
+ pluralName: "Test rows",
24
+ tableColumns: [projectIdColumn()],
25
+ primaryKeys: ["projectId"],
26
+ sortKeys: ["projectId"],
27
+ partitionKey: "projectId",
28
+ tableEngine: AnalyticsTableEngine.MergeTree,
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ describe("AnalyticsBaseModel base-column policy", () => {
34
+ test("adds _id and createdAt by default for regular analytics models", () => {
35
+ const suppliedColumns: Array<AnalyticsTableColumn> = [projectIdColumn()];
36
+ const model: AnalyticsBaseModel = new AnalyticsBaseModel(
37
+ modelOptions({ tableColumns: suppliedColumns }),
38
+ );
39
+
40
+ expect(
41
+ model.tableColumns.map((column: AnalyticsTableColumn) => {
42
+ return column.key;
43
+ }),
44
+ ).toEqual(["projectId", "_id", "createdAt"]);
45
+ expect(model.getTableColumn("_id")?.type).toBe(TableColumnType.ObjectID);
46
+ expect(model.getTableColumn("_id")?.codec).toEqual({
47
+ codec: "ZSTD",
48
+ level: 1,
49
+ });
50
+ expect(model.getTableColumn("createdAt")?.type).toBe(TableColumnType.Date);
51
+
52
+ // Construction copies the caller's array instead of mutating it.
53
+ expect(
54
+ suppliedColumns.map((column: AnalyticsTableColumn) => {
55
+ return column.key;
56
+ }),
57
+ ).toEqual(["projectId"]);
58
+ });
59
+
60
+ test("omits synthetic columns when includeBaseColumns is false", () => {
61
+ const model: AnalyticsBaseModel = new AnalyticsBaseModel(
62
+ modelOptions({
63
+ includeBaseColumns: false,
64
+ defaultSortColumn: "projectId",
65
+ }),
66
+ );
67
+
68
+ expect(
69
+ model.tableColumns.map((column: AnalyticsTableColumn) => {
70
+ return column.key;
71
+ }),
72
+ ).toEqual(["projectId"]);
73
+ expect(model.getTableColumn("_id")).toBeNull();
74
+ expect(model.getTableColumn("createdAt")).toBeNull();
75
+ expect(model.defaultSortColumn).toBe("projectId");
76
+ });
77
+
78
+ test("requires a real default sort column when base columns are omitted", () => {
79
+ expect(() => {
80
+ return new AnalyticsBaseModel(
81
+ modelOptions({
82
+ includeBaseColumns: false,
83
+ }),
84
+ );
85
+ }).toThrow(
86
+ "defaultSortColumn is required when includeBaseColumns is false",
87
+ );
88
+ });
89
+
90
+ test("rejects createdAt as a strict model's default sort because it is absent", () => {
91
+ expect(() => {
92
+ return new AnalyticsBaseModel(
93
+ modelOptions({
94
+ includeBaseColumns: false,
95
+ defaultSortColumn: "createdAt",
96
+ }),
97
+ );
98
+ }).toThrow("defaultSortColumn createdAt is not part of tableColumns");
99
+ });
100
+
101
+ test("still validates primary and sort keys against the strict column set", () => {
102
+ expect(() => {
103
+ return new AnalyticsBaseModel(
104
+ modelOptions({
105
+ includeBaseColumns: false,
106
+ defaultSortColumn: "projectId",
107
+ sortKeys: ["projectId", "createdAt"],
108
+ }),
109
+ );
110
+ }).toThrow("Sort key createdAt is not part of tableColumns");
111
+
112
+ expect(() => {
113
+ return new AnalyticsBaseModel(
114
+ modelOptions({
115
+ includeBaseColumns: false,
116
+ defaultSortColumn: "projectId",
117
+ primaryKeys: ["_id"],
118
+ }),
119
+ );
120
+ }).toThrow("Primary key _id is not part of tableColumns");
121
+ });
122
+ });
@@ -0,0 +1,232 @@
1
+ import NetworkDeviceDiscoveryScanService from "../../../Server/Services/NetworkDeviceDiscoveryScanService";
2
+ import ModelPermission from "../../../Server/Types/Database/Permissions/Index";
3
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
4
+ import NetworkDeviceDiscoveryScan from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
5
+ import DatabaseBaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
6
+ import Probe from "../../../Models/DatabaseModels/Probe";
7
+ import ObjectID from "../../../Types/ObjectID";
8
+ import OneUptimeDate from "../../../Types/Date";
9
+ import { JSONObject } from "../../../Types/JSON";
10
+ import Select from "../../../Server/Types/Database/Select";
11
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
12
+
13
+ type ScanUpdateData = UpdateBy<NetworkDeviceDiscoveryScan>["data"];
14
+
15
+ /*
16
+ * Regression tests for the bug that left every SNMP discovery scan stuck in
17
+ * "Pending": update `data` passed as a full model instance carries the
18
+ * non-column own properties of DatabaseBaseModel (column initializers plus
19
+ * `isPermissionIf = {}`). _updateBy used to turn every data key into a select
20
+ * column for its internal find, and the unknown `isPermissionIf` column made
21
+ * that find throw `TableColumnMetadata not found for isPermissionIf column`,
22
+ * failing the whole update — so the probe-ingest list endpoint 400'd before
23
+ * it could claim a scan.
24
+ *
25
+ * sanitizeUpdateData is the fix: model instances are stripped down to their
26
+ * set table columns before the update pipeline runs.
27
+ */
28
+
29
+ describe("DatabaseService.sanitizeUpdateData — model instances become plain column data", () => {
30
+ test("a fresh model instance really does carry non-column own properties (the trap this guards against)", () => {
31
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
32
+
33
+ /*
34
+ * If this stops holding, the base model changed shape and these tests
35
+ * (and sanitizeUpdateData itself) should be revisited.
36
+ */
37
+ expect(Object.keys(scan)).toContain("isPermissionIf");
38
+ });
39
+
40
+ test("keeps only the columns that were actually set", () => {
41
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
42
+ scan.status = "In Progress";
43
+ scan.startedAt = OneUptimeDate.getCurrentDate();
44
+
45
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
46
+ "sanitizeUpdateData"
47
+ ](scan as unknown as ScanUpdateData);
48
+
49
+ expect(Object.keys(data as JSONObject).sort()).toEqual([
50
+ "startedAt",
51
+ "status",
52
+ ]);
53
+ expect((data as JSONObject)["status"]).toBe("In Progress");
54
+ expect((data as JSONObject)["startedAt"]).toBeInstanceOf(Date);
55
+ });
56
+
57
+ test("the result is a plain object, not a model instance", () => {
58
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
59
+ scan.status = "Completed";
60
+
61
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
62
+ "sanitizeUpdateData"
63
+ ](scan as unknown as ScanUpdateData);
64
+
65
+ expect(data).not.toBeInstanceOf(DatabaseBaseModel);
66
+ expect(Object.getPrototypeOf(data)).toBe(Object.prototype);
67
+ });
68
+
69
+ test("drops isPermissionIf and every other non-column own property", () => {
70
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
71
+ scan.status = "Completed";
72
+
73
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
74
+ "sanitizeUpdateData"
75
+ ](scan as unknown as ScanUpdateData);
76
+
77
+ expect(Object.keys(data as JSONObject)).not.toContain("isPermissionIf");
78
+ });
79
+
80
+ test("drops columns left undefined so they cannot become writes", () => {
81
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
82
+ scan.status = "Failed";
83
+ // cidr, snmpVersion, statusMessage etc. stay undefined.
84
+
85
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
86
+ "sanitizeUpdateData"
87
+ ](scan as unknown as ScanUpdateData);
88
+
89
+ expect(Object.keys(data as JSONObject)).toEqual(["status"]);
90
+ });
91
+
92
+ test("preserves explicit null so columns can still be cleared", () => {
93
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
94
+ scan.status = "Pending";
95
+ (scan as any).statusMessage = null;
96
+ (scan as any).nextScanAt = null;
97
+
98
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
99
+ "sanitizeUpdateData"
100
+ ](scan as unknown as ScanUpdateData);
101
+
102
+ expect((data as JSONObject)["statusMessage"]).toBeNull();
103
+ expect((data as JSONObject)["nextScanAt"]).toBeNull();
104
+ });
105
+
106
+ test("drops _id, createdAt, updatedAt and version even when set (loaded-model updates must not redirect the write or forge timestamps)", () => {
107
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan(
108
+ ObjectID.generate(),
109
+ );
110
+ scan.status = "Completed";
111
+ (scan as any).createdAt = OneUptimeDate.getCurrentDate();
112
+ (scan as any).updatedAt = OneUptimeDate.getCurrentDate();
113
+ (scan as any).version = 7;
114
+
115
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
116
+ "sanitizeUpdateData"
117
+ ](scan as unknown as ScanUpdateData);
118
+
119
+ expect(Object.keys(data as JSONObject)).toEqual(["status"]);
120
+ });
121
+
122
+ test("keeps entity relation columns that were set", () => {
123
+ const probeId: ObjectID = ObjectID.generate();
124
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
125
+ scan.status = "Pending";
126
+ scan.probe = new Probe(probeId);
127
+
128
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
129
+ "sanitizeUpdateData"
130
+ ](scan as unknown as ScanUpdateData);
131
+
132
+ expect((data as JSONObject)["probe"]).toBeInstanceOf(Probe);
133
+ expect(((data as JSONObject)["probe"] as Probe).id?.toString()).toBe(
134
+ probeId.toString(),
135
+ );
136
+ });
137
+
138
+ test("plain objects pass through by reference, unknown keys and all (typos must keep failing loudly)", () => {
139
+ const plain: JSONObject = {
140
+ status: "Completed",
141
+ someTypoColumn: 1,
142
+ };
143
+
144
+ const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
145
+ "sanitizeUpdateData"
146
+ ](plain as unknown as ScanUpdateData);
147
+
148
+ expect(data).toBe(plain);
149
+ expect(Object.keys(data as JSONObject)).toContain("someTypoColumn");
150
+ });
151
+ });
152
+
153
+ describe("DatabaseService._updateBy — updateOneById with a model instance no longer poisons the internal find", () => {
154
+ afterEach(() => {
155
+ jest.restoreAllMocks();
156
+ });
157
+
158
+ test("select columns for the internal find contain only real columns from the data", async () => {
159
+ const scanId: ObjectID = ObjectID.generate();
160
+
161
+ const foundItem: NetworkDeviceDiscoveryScan =
162
+ new NetworkDeviceDiscoveryScan();
163
+ foundItem._id = scanId.toString();
164
+
165
+ // eslint-disable-next-line @typescript-eslint/typedef
166
+ const findBySpy = jest
167
+ .spyOn(NetworkDeviceDiscoveryScanService as any, "_findBy")
168
+ .mockResolvedValue([foundItem] as never);
169
+
170
+ // eslint-disable-next-line @typescript-eslint/typedef
171
+ const saveMock = jest.fn((item: unknown) => {
172
+ return Promise.resolve(item);
173
+ });
174
+ jest
175
+ .spyOn(NetworkDeviceDiscoveryScanService, "getRepository")
176
+ .mockReturnValue({ save: saveMock } as never);
177
+
178
+ // Permission layer is not under test here and needs no DB.
179
+ jest
180
+ .spyOn(ModelPermission, "checkUpdatePermissionByModel")
181
+ .mockResolvedValue(undefined as never);
182
+ jest
183
+ .spyOn(ModelPermission, "checkUpdateQueryPermissions")
184
+ .mockImplementation(((
185
+ _modelType: unknown,
186
+ query: unknown,
187
+ ): Promise<unknown> => {
188
+ return Promise.resolve(query);
189
+ }) as never);
190
+
191
+ const inProgress: NetworkDeviceDiscoveryScan =
192
+ new NetworkDeviceDiscoveryScan();
193
+ inProgress.status = "In Progress";
194
+ inProgress.startedAt = OneUptimeDate.getCurrentDate();
195
+
196
+ /*
197
+ * Before the fix this call rejected with "TableColumnMetadata not found
198
+ * for isPermissionIf column" — reproducing exactly why discovery scans
199
+ * never left Pending.
200
+ */
201
+ await expect(
202
+ NetworkDeviceDiscoveryScanService.updateOneById({
203
+ id: scanId,
204
+ data: inProgress as unknown as UpdateBy<NetworkDeviceDiscoveryScan>["data"],
205
+ props: {
206
+ isRoot: true,
207
+ },
208
+ }),
209
+ ).resolves.toBeUndefined();
210
+
211
+ expect(findBySpy).toHaveBeenCalledTimes(1);
212
+ const findArgs: { select: Select<NetworkDeviceDiscoveryScan> } = (
213
+ findBySpy.mock.calls[0] as Array<any>
214
+ )[0];
215
+ const selectedColumns: Array<string> = Object.keys(findArgs.select);
216
+
217
+ // The data columns plus _id and the tenant column — nothing else.
218
+ expect(selectedColumns.sort()).toEqual(
219
+ ["_id", "projectId", "startedAt", "status"].sort(),
220
+ );
221
+ expect(selectedColumns).not.toContain("isPermissionIf");
222
+
223
+ // The persisted payload is the plain data plus the located row's _id.
224
+ expect(saveMock).toHaveBeenCalledTimes(1);
225
+ const savedItem: JSONObject = saveMock.mock.calls[0]![0] as JSONObject;
226
+ expect(Object.keys(savedItem).sort()).toEqual(
227
+ ["_id", "startedAt", "status"].sort(),
228
+ );
229
+ expect(savedItem["status"]).toBe("In Progress");
230
+ expect(savedItem["_id"]).toBe(scanId.toString());
231
+ });
232
+ });
@@ -0,0 +1,107 @@
1
+ import NetworkDeviceDiscoveryScan from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
2
+ import NetworkDeviceDiscoveryScanService from "../../../Server/Services/NetworkDeviceDiscoveryScanService";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import { afterEach, describe, expect, test } from "@jest/globals";
5
+
6
+ /*
7
+ * Regression tests for the second layer of the discovery-scan "stuck
8
+ * Pending" bug (the first layer — model instances poisoning the internal
9
+ * find's select columns — is covered in
10
+ * DatabaseServiceSanitizeUpdateData.test.ts).
11
+ *
12
+ * DatabaseService._updateBy builds the row it saves as a merge of the
13
+ * located row's _id and the caller's update data. With the old merge order
14
+ * `{ _id: item._id, ...data }`, update data carrying an own `_id: undefined`
15
+ * property (a fresh model instance before sanitizeUpdateData existed, or a
16
+ * plain object that spells it out) clobbered the located row's id — TypeORM
17
+ * save() saw no primary key, INSERTed a new row instead of updating, and
18
+ * died on the first NOT NULL column.
19
+ *
20
+ * These tests pin the fixed merge order: the located row's _id must win no
21
+ * matter what shape the update data takes. _findBy is mocked out, so this
22
+ * exercises the save-payload merge in isolation.
23
+ */
24
+
25
+ type SavedRow = { _id?: string; status?: string; startedAt?: Date };
26
+
27
+ function mockPersistence(existingRowId: string): jest.Mock {
28
+ const existing: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
29
+ existing._id = existingRowId;
30
+
31
+ jest
32
+ .spyOn(NetworkDeviceDiscoveryScanService as any, "_findBy")
33
+ .mockResolvedValue([existing] as never);
34
+
35
+ const save: jest.Mock = jest
36
+ .fn()
37
+ .mockImplementation(async (row: unknown): Promise<unknown> => {
38
+ return row;
39
+ });
40
+
41
+ jest
42
+ .spyOn(NetworkDeviceDiscoveryScanService, "getRepository")
43
+ .mockReturnValue({ save } as any);
44
+
45
+ return save;
46
+ }
47
+
48
+ describe("DatabaseService.updateOneById — saved row keeps the located row's _id", () => {
49
+ afterEach(() => {
50
+ jest.restoreAllMocks();
51
+ });
52
+
53
+ test("when data is a fresh model instance (own undefined _id property)", async () => {
54
+ const rowId: string = ObjectID.generate().toString();
55
+ const save: jest.Mock = mockPersistence(rowId);
56
+
57
+ const data: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
58
+ data.status = "In Progress";
59
+ data.startedAt = new Date();
60
+
61
+ // The bug's precondition: the instance really does carry _id: undefined.
62
+ expect(Object.keys(data)).toContain("_id");
63
+
64
+ await NetworkDeviceDiscoveryScanService.updateOneById({
65
+ id: new ObjectID(rowId),
66
+ data: data as never,
67
+ props: { isRoot: true },
68
+ });
69
+
70
+ expect(save).toHaveBeenCalledTimes(1);
71
+ const saved: SavedRow = save.mock.calls[0]![0] as SavedRow;
72
+ expect(saved._id).toBe(rowId);
73
+ expect(saved.status).toBe("In Progress");
74
+ });
75
+
76
+ test("when data is a plain partial object", async () => {
77
+ const rowId: string = ObjectID.generate().toString();
78
+ const save: jest.Mock = mockPersistence(rowId);
79
+
80
+ await NetworkDeviceDiscoveryScanService.updateOneById({
81
+ id: new ObjectID(rowId),
82
+ data: { status: "In Progress" } as never,
83
+ props: { isRoot: true },
84
+ });
85
+
86
+ expect(save).toHaveBeenCalledTimes(1);
87
+ const saved: SavedRow = save.mock.calls[0]![0] as SavedRow;
88
+ expect(saved._id).toBe(rowId);
89
+ expect(saved.status).toBe("In Progress");
90
+ });
91
+
92
+ test("when data is a plain object that spells out _id: undefined", async () => {
93
+ const rowId: string = ObjectID.generate().toString();
94
+ const save: jest.Mock = mockPersistence(rowId);
95
+
96
+ await NetworkDeviceDiscoveryScanService.updateOneById({
97
+ id: new ObjectID(rowId),
98
+ data: { _id: undefined, status: "In Progress" } as never,
99
+ props: { isRoot: true },
100
+ });
101
+
102
+ expect(save).toHaveBeenCalledTimes(1);
103
+ const saved: SavedRow = save.mock.calls[0]![0] as SavedRow;
104
+ expect(saved._id).toBe(rowId);
105
+ expect(saved.status).toBe("In Progress");
106
+ });
107
+ });