@oneuptime/common 12.0.1 → 12.0.2

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 (205) hide show
  1. package/Models/DatabaseModels/Host.ts +12 -4
  2. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  3. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  4. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  5. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  6. package/Models/DatabaseModels/StatusPage.ts +95 -0
  7. package/Server/API/TeamMemberAPI.ts +85 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  13. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  14. package/Server/Services/BillingService.ts +600 -51
  15. package/Server/Services/CephClusterService.ts +47 -5
  16. package/Server/Services/CephResourceService.ts +43 -4
  17. package/Server/Services/CloudResourceService.ts +47 -5
  18. package/Server/Services/DatabaseService.ts +87 -6
  19. package/Server/Services/DockerHostService.ts +47 -5
  20. package/Server/Services/DockerResourceService.ts +62 -8
  21. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  22. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  23. package/Server/Services/HostService.ts +48 -5
  24. package/Server/Services/IoTDeviceService.ts +1 -17
  25. package/Server/Services/IoTFleetService.ts +47 -5
  26. package/Server/Services/KubernetesClusterService.ts +48 -5
  27. package/Server/Services/KubernetesContainerService.ts +50 -9
  28. package/Server/Services/KubernetesResourceService.ts +53 -7
  29. package/Server/Services/LabelService.ts +106 -1
  30. package/Server/Services/MonitorService.ts +57 -0
  31. package/Server/Services/PodmanHostService.ts +47 -5
  32. package/Server/Services/PodmanResourceService.ts +62 -8
  33. package/Server/Services/ProjectService.ts +165 -35
  34. package/Server/Services/ProxmoxClusterService.ts +47 -5
  35. package/Server/Services/ProxmoxResourceService.ts +43 -4
  36. package/Server/Services/RumApplicationService.ts +48 -5
  37. package/Server/Services/ServerlessFunctionService.ts +48 -5
  38. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  39. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  40. package/Server/Services/StatusPageService.ts +95 -36
  41. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  42. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  43. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  44. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  45. package/Server/Types/Database/QueryHelper.ts +17 -32
  46. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  47. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  48. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  49. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  50. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  51. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  52. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  53. package/Tests/Server/Services/BillingService.test.ts +78 -3
  54. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  55. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  56. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  57. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  58. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  59. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  60. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  61. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  62. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  63. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  64. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  65. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  66. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  67. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  68. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  69. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  70. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  71. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  72. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  73. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  74. package/Tests/Types/Events/Recurring.test.ts +157 -0
  75. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  76. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  77. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  78. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  79. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  80. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  81. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  82. package/Types/Date.ts +112 -0
  83. package/Types/Events/Recurring.ts +99 -1
  84. package/Types/StatusPage/StatusPageReport.ts +11 -0
  85. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  86. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  87. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  88. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  89. package/UI/Utils/TeamMembersByUser.ts +302 -0
  90. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  91. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  92. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  93. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  95. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  97. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  99. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  101. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  103. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  104. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  105. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  116. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  117. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/BillingService.js +466 -36
  119. package/build/dist/Server/Services/BillingService.js.map +1 -1
  120. package/build/dist/Server/Services/CephClusterService.js +42 -5
  121. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  122. package/build/dist/Server/Services/CephResourceService.js +28 -3
  123. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  124. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  125. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +69 -2
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/DockerHostService.js +42 -5
  129. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  130. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  131. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  132. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  133. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  134. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  135. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  136. package/build/dist/Server/Services/HostService.js +43 -5
  137. package/build/dist/Server/Services/HostService.js.map +1 -1
  138. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  139. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  140. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  141. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  142. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  143. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  144. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  145. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  146. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  147. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  148. package/build/dist/Server/Services/LabelService.js +92 -0
  149. package/build/dist/Server/Services/LabelService.js.map +1 -1
  150. package/build/dist/Server/Services/MonitorService.js +47 -0
  151. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  152. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  153. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  154. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  155. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  156. package/build/dist/Server/Services/ProjectService.js +135 -21
  157. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  158. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  159. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  160. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  161. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  162. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  163. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  164. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  165. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  166. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  167. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  168. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  169. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  170. package/build/dist/Server/Services/StatusPageService.js +69 -46
  171. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  172. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  173. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  174. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  175. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  176. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  177. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  178. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  179. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  180. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  181. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  182. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  183. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  184. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  185. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  186. package/build/dist/Types/Date.js +68 -0
  187. package/build/dist/Types/Date.js.map +1 -1
  188. package/build/dist/Types/Events/Recurring.js +63 -0
  189. package/build/dist/Types/Events/Recurring.js.map +1 -1
  190. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  191. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  192. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  193. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  194. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  195. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  196. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  197. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  198. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  199. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  200. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  201. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  202. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  203. package/jest.config.json +2 -0
  204. package/package.json +1 -1
  205. package/tsconfig.json +12 -1
@@ -0,0 +1,586 @@
1
+ import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
2
+ import CephResource from "../../../Models/DatabaseModels/CephResource";
3
+ import DockerResource from "../../../Models/DatabaseModels/DockerResource";
4
+ import DockerSwarmResource from "../../../Models/DatabaseModels/DockerSwarmResource";
5
+ import IoTDevice from "../../../Models/DatabaseModels/IoTDevice";
6
+ import KubernetesContainer from "../../../Models/DatabaseModels/KubernetesContainer";
7
+ import KubernetesResource from "../../../Models/DatabaseModels/KubernetesResource";
8
+ import PodmanResource from "../../../Models/DatabaseModels/PodmanResource";
9
+ import ProxmoxResource from "../../../Models/DatabaseModels/ProxmoxResource";
10
+ import CephResourceService from "../../../Server/Services/CephResourceService";
11
+ import DockerResourceService from "../../../Server/Services/DockerResourceService";
12
+ import DockerSwarmResourceService from "../../../Server/Services/DockerSwarmResourceService";
13
+ import IoTDeviceService from "../../../Server/Services/IoTDeviceService";
14
+ import KubernetesContainerService from "../../../Server/Services/KubernetesContainerService";
15
+ import KubernetesResourceService from "../../../Server/Services/KubernetesResourceService";
16
+ import PodmanResourceService from "../../../Server/Services/PodmanResourceService";
17
+ import ProxmoxResourceService from "../../../Server/Services/ProxmoxResourceService";
18
+ import { getMaxLengthFromTableColumnType } from "../../../Types/Database/ColumnLength";
19
+ import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
20
+ import ObjectID from "../../../Types/ObjectID";
21
+ import { describe, expect, test } from "@jest/globals";
22
+
23
+ /*
24
+ * The contract that actually prevents issue #3006 from recurring:
25
+ * EVERY bounded varchar column an inventory service writes through its
26
+ * hand-built bulk INSERT must be clamped to the width the entity
27
+ * declares.
28
+ *
29
+ * Rather than hand-listing column positions (which rot the moment a
30
+ * column is added), each case here feeds a row whose every text field is
31
+ * absurdly oversized, then reads the INSERT's own column list out of the
32
+ * emitted SQL and looks each column's bound up in the entity metadata.
33
+ * Any bounded column whose bound parameter still exceeds it is a row
34
+ * that Postgres would reject with 22001 — aborting the whole 500-row
35
+ * chunk and silently dropping every other resource in the flush.
36
+ *
37
+ * This is what fails if someone widens a column without updating the
38
+ * sanitizer, narrows one without updating it, or adds a new bounded
39
+ * column to an INSERT and forgets to clamp it at all.
40
+ */
41
+
42
+ const PROJECT_ID: ObjectID = ObjectID.generate();
43
+ const PARENT_ID: ObjectID = ObjectID.generate();
44
+ const OBSERVED_AT: Date = new Date("2026-08-05T00:00:00.000Z");
45
+
46
+ // Longer than the widest bounded varchar in the schema (500), by a lot.
47
+ const OVERSIZED: string = "x".repeat(4000);
48
+
49
+ interface RepositoryHolder {
50
+ getRepository: () => unknown;
51
+ }
52
+
53
+ interface ColumnAudit {
54
+ /** Bounded columns whose parameter is a string — the ones at risk. */
55
+ checked: Array<string>;
56
+ /** Bounded columns whose parameter would still blow the column up. */
57
+ violations: Array<string>;
58
+ /** Bounded columns clamped to exactly their declared bound. */
59
+ clampedToBound: Array<string>;
60
+ }
61
+
62
+ interface UpsertCase {
63
+ label: string;
64
+ service: RepositoryHolder;
65
+ model: BaseModel;
66
+ /** Bounded string columns the INSERT is expected to carry. */
67
+ boundedColumnCount: number;
68
+ /** jsonb columns the INSERT carries as a serialized payload. */
69
+ jsonPayloadColumnCount: number;
70
+ invoke: () => Promise<void>;
71
+ }
72
+
73
+ function mockQueryRunner(service: RepositoryHolder): jest.Mock {
74
+ const query: jest.Mock = jest.fn().mockResolvedValue([]);
75
+ jest
76
+ .spyOn(service, "getRepository")
77
+ .mockReturnValue({ manager: { query } } as any);
78
+ return query;
79
+ }
80
+
81
+ /**
82
+ * Pull the column list straight out of `INSERT INTO "X" (...)`, so the
83
+ * audit follows the statement the service actually built rather than a
84
+ * copy of it maintained here.
85
+ */
86
+ function insertColumnList(sql: string): Array<string> {
87
+ const match: RegExpMatchArray | null = sql.match(
88
+ /INSERT INTO\s+"[A-Za-z]+"\s*\(([^)]+)\)/,
89
+ );
90
+
91
+ if (!match || !match[1]) {
92
+ throw new Error(`Could not parse an INSERT column list out of: ${sql}`);
93
+ }
94
+
95
+ return match[1]
96
+ .split(",")
97
+ .map((column: string) => {
98
+ return column.trim().replace(/^"|"$/g, "");
99
+ })
100
+ .filter((column: string) => {
101
+ return column.length > 0;
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Audit the first VALUES row of a bulk INSERT against the entity's
107
+ * declared column widths. Parameters are positional, so the first
108
+ * `columns.length` params are row one.
109
+ */
110
+ function auditFirstRow(
111
+ model: BaseModel,
112
+ sql: string,
113
+ params: Array<unknown>,
114
+ ): ColumnAudit {
115
+ const audit: ColumnAudit = {
116
+ checked: [],
117
+ violations: [],
118
+ clampedToBound: [],
119
+ };
120
+
121
+ insertColumnList(sql).forEach((column: string, index: number) => {
122
+ const metadata: TableColumnMetadata | undefined =
123
+ model.getTableColumnMetadata(column);
124
+
125
+ if (!metadata) {
126
+ return;
127
+ }
128
+
129
+ const maxLength: number | undefined = getMaxLengthFromTableColumnType(
130
+ metadata.type,
131
+ );
132
+ const value: unknown = params[index];
133
+
134
+ if (maxLength === undefined || typeof value !== "string") {
135
+ return;
136
+ }
137
+
138
+ audit.checked.push(column);
139
+
140
+ if (value.length > maxLength) {
141
+ audit.violations.push(
142
+ `"${column}" is varchar(${maxLength}) but the parameter carries ${value.length} characters`,
143
+ );
144
+ }
145
+
146
+ if (value.length === maxLength) {
147
+ audit.clampedToBound.push(column);
148
+ }
149
+ });
150
+
151
+ return audit;
152
+ }
153
+
154
+ const CASES: Array<UpsertCase> = [
155
+ {
156
+ label: "KubernetesResourceService.bulkUpsert",
157
+ service: KubernetesResourceService,
158
+ model: new KubernetesResource(),
159
+ boundedColumnCount: 5,
160
+ jsonPayloadColumnCount: 1,
161
+ invoke: async (): Promise<void> => {
162
+ await KubernetesResourceService.bulkUpsert({
163
+ projectId: PROJECT_ID,
164
+ kubernetesClusterId: PARENT_ID,
165
+ resources: [
166
+ {
167
+ kind: OVERSIZED,
168
+ namespaceKey: OVERSIZED,
169
+ name: OVERSIZED,
170
+ uid: OVERSIZED,
171
+ phase: OVERSIZED,
172
+ isReady: true,
173
+ hasMemoryPressure: null,
174
+ hasDiskPressure: null,
175
+ hasPidPressure: null,
176
+ labels: { app: OVERSIZED },
177
+ annotations: null,
178
+ ownerReferences: null,
179
+ spec: null,
180
+ containerCount: 1,
181
+ status: null,
182
+ lastSeenAt: OBSERVED_AT,
183
+ resourceCreationTimestamp: null,
184
+ },
185
+ ],
186
+ });
187
+ },
188
+ },
189
+ {
190
+ label: "KubernetesContainerService.bulkUpsert",
191
+ service: KubernetesContainerService,
192
+ model: new KubernetesContainer(),
193
+ boundedColumnCount: 6,
194
+ jsonPayloadColumnCount: 0,
195
+ invoke: async (): Promise<void> => {
196
+ await KubernetesContainerService.bulkUpsert({
197
+ projectId: PROJECT_ID,
198
+ kubernetesClusterId: PARENT_ID,
199
+ containers: [
200
+ {
201
+ podNamespaceKey: OVERSIZED,
202
+ podName: OVERSIZED,
203
+ name: OVERSIZED,
204
+ image: OVERSIZED,
205
+ state: OVERSIZED,
206
+ reason: OVERSIZED,
207
+ isReady: true,
208
+ restartCount: 0,
209
+ memoryLimitBytes: 1024,
210
+ lastSeenAt: OBSERVED_AT,
211
+ },
212
+ ],
213
+ });
214
+ },
215
+ },
216
+ {
217
+ label: "DockerResourceService.bulkUpsert",
218
+ service: DockerResourceService,
219
+ model: new DockerResource(),
220
+ boundedColumnCount: 5,
221
+ jsonPayloadColumnCount: 1,
222
+ invoke: async (): Promise<void> => {
223
+ await DockerResourceService.bulkUpsert({
224
+ projectId: PROJECT_ID,
225
+ dockerHostId: PARENT_ID,
226
+ resources: [
227
+ {
228
+ kind: OVERSIZED,
229
+ name: OVERSIZED,
230
+ containerId: OVERSIZED,
231
+ imageName: OVERSIZED,
232
+ state: OVERSIZED,
233
+ labels: { role: OVERSIZED },
234
+ resourceCreationTimestamp: null,
235
+ lastSeenAt: OBSERVED_AT,
236
+ },
237
+ ],
238
+ });
239
+ },
240
+ },
241
+ {
242
+ label: "DockerResourceService.bulkUpsertContainers",
243
+ service: DockerResourceService,
244
+ model: new DockerResource(),
245
+ boundedColumnCount: 5,
246
+ jsonPayloadColumnCount: 0,
247
+ invoke: async (): Promise<void> => {
248
+ await DockerResourceService.bulkUpsertContainers({
249
+ projectId: PROJECT_ID,
250
+ dockerHostId: PARENT_ID,
251
+ containers: [
252
+ {
253
+ containerName: OVERSIZED,
254
+ containerId: OVERSIZED,
255
+ imageName: OVERSIZED,
256
+ state: OVERSIZED,
257
+ cpuPercent: 1,
258
+ memoryBytes: 1024,
259
+ observedAt: OBSERVED_AT,
260
+ },
261
+ ],
262
+ });
263
+ },
264
+ },
265
+ {
266
+ label: "PodmanResourceService.bulkUpsert",
267
+ service: PodmanResourceService,
268
+ model: new PodmanResource(),
269
+ boundedColumnCount: 5,
270
+ jsonPayloadColumnCount: 1,
271
+ invoke: async (): Promise<void> => {
272
+ await PodmanResourceService.bulkUpsert({
273
+ projectId: PROJECT_ID,
274
+ podmanHostId: PARENT_ID,
275
+ resources: [
276
+ {
277
+ kind: OVERSIZED,
278
+ name: OVERSIZED,
279
+ containerId: OVERSIZED,
280
+ imageName: OVERSIZED,
281
+ state: OVERSIZED,
282
+ labels: { role: OVERSIZED },
283
+ resourceCreationTimestamp: null,
284
+ lastSeenAt: OBSERVED_AT,
285
+ },
286
+ ],
287
+ });
288
+ },
289
+ },
290
+ {
291
+ label: "PodmanResourceService.bulkUpsertContainers",
292
+ service: PodmanResourceService,
293
+ model: new PodmanResource(),
294
+ boundedColumnCount: 5,
295
+ jsonPayloadColumnCount: 0,
296
+ invoke: async (): Promise<void> => {
297
+ await PodmanResourceService.bulkUpsertContainers({
298
+ projectId: PROJECT_ID,
299
+ podmanHostId: PARENT_ID,
300
+ containers: [
301
+ {
302
+ containerName: OVERSIZED,
303
+ containerId: OVERSIZED,
304
+ imageName: OVERSIZED,
305
+ state: OVERSIZED,
306
+ cpuPercent: 1,
307
+ memoryBytes: 1024,
308
+ observedAt: OBSERVED_AT,
309
+ },
310
+ ],
311
+ });
312
+ },
313
+ },
314
+ {
315
+ label: "DockerSwarmResourceService.bulkUpsert",
316
+ service: DockerSwarmResourceService,
317
+ model: new DockerSwarmResource(),
318
+ boundedColumnCount: 11,
319
+ jsonPayloadColumnCount: 1,
320
+ invoke: async (): Promise<void> => {
321
+ await DockerSwarmResourceService.bulkUpsert({
322
+ projectId: PROJECT_ID,
323
+ dockerSwarmClusterId: PARENT_ID,
324
+ resources: [
325
+ {
326
+ kind: OVERSIZED,
327
+ externalId: OVERSIZED,
328
+ name: OVERSIZED,
329
+ state: OVERSIZED,
330
+ role: OVERSIZED,
331
+ serviceMode: OVERSIZED,
332
+ desiredReplicas: 1,
333
+ runningReplicas: 1,
334
+ image: OVERSIZED,
335
+ stackName: OVERSIZED,
336
+ serviceName: OVERSIZED,
337
+ nodeHostname: OVERSIZED,
338
+ driver: OVERSIZED,
339
+ isReady: true,
340
+ attributes: { note: OVERSIZED },
341
+ lastSeenAt: OBSERVED_AT,
342
+ },
343
+ ],
344
+ });
345
+ },
346
+ },
347
+ {
348
+ label: "ProxmoxResourceService.bulkUpsert",
349
+ service: ProxmoxResourceService,
350
+ model: new ProxmoxResource(),
351
+ boundedColumnCount: 6,
352
+ jsonPayloadColumnCount: 0,
353
+ invoke: async (): Promise<void> => {
354
+ await ProxmoxResourceService.bulkUpsert({
355
+ projectId: PROJECT_ID,
356
+ proxmoxClusterId: PARENT_ID,
357
+ resources: [
358
+ {
359
+ kind: OVERSIZED,
360
+ externalId: OVERSIZED,
361
+ name: OVERSIZED,
362
+ vmid: 100,
363
+ guestType: OVERSIZED,
364
+ parentNodeName: OVERSIZED,
365
+ isUp: true,
366
+ haState: OVERSIZED,
367
+ onboot: null,
368
+ isBackedUp: null,
369
+ uptimeSeconds: 10,
370
+ lastSeenAt: OBSERVED_AT,
371
+ },
372
+ ],
373
+ });
374
+ },
375
+ },
376
+ {
377
+ label: "CephResourceService.bulkUpsert",
378
+ service: CephResourceService,
379
+ model: new CephResource(),
380
+ boundedColumnCount: 6,
381
+ jsonPayloadColumnCount: 0,
382
+ invoke: async (): Promise<void> => {
383
+ await CephResourceService.bulkUpsert({
384
+ projectId: PROJECT_ID,
385
+ cephClusterId: PARENT_ID,
386
+ resources: [
387
+ {
388
+ kind: OVERSIZED,
389
+ externalId: OVERSIZED,
390
+ name: OVERSIZED,
391
+ hostname: OVERSIZED,
392
+ daemonVersion: OVERSIZED,
393
+ deviceClass: OVERSIZED,
394
+ isUp: true,
395
+ isIn: true,
396
+ inQuorum: null,
397
+ lastSeenAt: OBSERVED_AT,
398
+ },
399
+ ],
400
+ });
401
+ },
402
+ },
403
+ {
404
+ label: "IoTDeviceService.bulkUpsert",
405
+ service: IoTDeviceService,
406
+ model: new IoTDevice(),
407
+ boundedColumnCount: 5,
408
+ jsonPayloadColumnCount: 0,
409
+ invoke: async (): Promise<void> => {
410
+ await IoTDeviceService.bulkUpsert({
411
+ projectId: PROJECT_ID,
412
+ iotFleetId: PARENT_ID,
413
+ devices: [
414
+ {
415
+ kind: OVERSIZED,
416
+ externalId: OVERSIZED,
417
+ name: OVERSIZED,
418
+ deviceType: OVERSIZED,
419
+ firmwareVersion: OVERSIZED,
420
+ isUp: true,
421
+ uptimeSeconds: 10,
422
+ lastSeenAt: OBSERVED_AT,
423
+ },
424
+ ],
425
+ });
426
+ },
427
+ },
428
+ ];
429
+
430
+ afterEach(() => {
431
+ jest.restoreAllMocks();
432
+ });
433
+
434
+ describe("every bulk INSERT fits the widths its entity declares", () => {
435
+ test.each(CASES)(
436
+ "$label clamps every bounded column",
437
+ async (upsertCase: UpsertCase) => {
438
+ const query: jest.Mock = mockQueryRunner(upsertCase.service);
439
+ await upsertCase.invoke();
440
+
441
+ expect(query).toHaveBeenCalledTimes(1);
442
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
443
+ const audit: ColumnAudit = auditFirstRow(upsertCase.model, sql, params);
444
+
445
+ expect(audit.violations).toEqual([]);
446
+ },
447
+ );
448
+
449
+ test.each(CASES)(
450
+ "$label audits the bounded columns it is supposed to",
451
+ async (upsertCase: UpsertCase) => {
452
+ /*
453
+ * Guards the guard: without this, a regex that matched nothing
454
+ * would make the assertion above pass vacuously. Bump the count
455
+ * deliberately when a bounded column joins the INSERT — and clamp
456
+ * it in the service while you are there.
457
+ */
458
+ const query: jest.Mock = mockQueryRunner(upsertCase.service);
459
+ await upsertCase.invoke();
460
+
461
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
462
+ const audit: ColumnAudit = auditFirstRow(upsertCase.model, sql, params);
463
+
464
+ expect(audit.checked).toHaveLength(upsertCase.boundedColumnCount);
465
+ },
466
+ );
467
+
468
+ test.each(CASES)(
469
+ "$label actually clipped the oversized values, rather than dropping them",
470
+ async (upsertCase: UpsertCase) => {
471
+ const query: jest.Mock = mockQueryRunner(upsertCase.service);
472
+ await upsertCase.invoke();
473
+
474
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
475
+ const audit: ColumnAudit = auditFirstRow(upsertCase.model, sql, params);
476
+
477
+ /*
478
+ * Every input was oversized, so every bounded column that came
479
+ * from the input must now sit exactly on its bound.
480
+ */
481
+ expect(audit.clampedToBound.length).toBeGreaterThan(0);
482
+ expect(params).not.toContain(OVERSIZED);
483
+ },
484
+ );
485
+
486
+ test.each(CASES)(
487
+ "$label leaves its jsonb payloads unclamped",
488
+ async (upsertCase: UpsertCase) => {
489
+ /*
490
+ * jsonb payloads (labels / annotations / attributes / status) have
491
+ * no varchar bound, so clamping them would corrupt real data for
492
+ * no reason.
493
+ *
494
+ * jsonPayloadColumnCount is asserted explicitly because without it
495
+ * this case executes ZERO assertions for the services that carry no
496
+ * jsonb column — and "no assertions ran" reads exactly like
497
+ * "everything checked out".
498
+ */
499
+ const query: jest.Mock = mockQueryRunner(upsertCase.service);
500
+ await upsertCase.invoke();
501
+
502
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
503
+ const columns: Array<string> = insertColumnList(sql);
504
+ const jsonPayloadColumns: Array<string> = [];
505
+
506
+ columns.forEach((column: string, index: number) => {
507
+ const metadata: TableColumnMetadata | undefined =
508
+ upsertCase.model.getTableColumnMetadata(column);
509
+ const value: unknown = params[index];
510
+
511
+ if (
512
+ !metadata ||
513
+ typeof value !== "string" ||
514
+ getMaxLengthFromTableColumnType(metadata.type) !== undefined
515
+ ) {
516
+ return;
517
+ }
518
+
519
+ /*
520
+ * Unbounded and string-valued: a serialized JSON blob. It must
521
+ * still carry the full oversized payload we fed in.
522
+ */
523
+ if (value.startsWith("{")) {
524
+ jsonPayloadColumns.push(column);
525
+ expect(value).toContain(OVERSIZED);
526
+ expect(value.length).toBeGreaterThan(OVERSIZED.length);
527
+ }
528
+ });
529
+
530
+ expect(jsonPayloadColumns).toHaveLength(
531
+ upsertCase.jsonPayloadColumnCount,
532
+ );
533
+ },
534
+ );
535
+ });
536
+
537
+ describe("the parity audit itself", () => {
538
+ test("flags a column whose parameter exceeds the declared width", () => {
539
+ /*
540
+ * A test for the test: prove the sweep above is capable of failing.
541
+ * KubernetesResource."kind" is varchar(100).
542
+ */
543
+ const audit: ColumnAudit = auditFirstRow(
544
+ new KubernetesResource(),
545
+ 'INSERT INTO "KubernetesResource" ("kind", "name") VALUES ($1, $2)',
546
+ [OVERSIZED, "api"],
547
+ );
548
+
549
+ expect(audit.violations).toHaveLength(1);
550
+ expect(audit.violations[0]).toContain('"kind"');
551
+ expect(audit.violations[0]).toContain("varchar(100)");
552
+ });
553
+
554
+ test("accepts a column sitting exactly on its declared width", () => {
555
+ const audit: ColumnAudit = auditFirstRow(
556
+ new KubernetesResource(),
557
+ 'INSERT INTO "KubernetesResource" ("kind", "name") VALUES ($1, $2)',
558
+ ["k".repeat(100), "n".repeat(500)],
559
+ );
560
+
561
+ expect(audit.violations).toEqual([]);
562
+ expect(audit.checked).toEqual(["kind", "name"]);
563
+ expect(audit.clampedToBound).toEqual(["kind", "name"]);
564
+ });
565
+
566
+ test("reads the widened name bound off KubernetesResource, not a constant", () => {
567
+ /*
568
+ * 500 here comes from the entity; if the model regressed to
569
+ * ShortText this would flag a violation instead.
570
+ */
571
+ const audit: ColumnAudit = auditFirstRow(
572
+ new KubernetesResource(),
573
+ 'INSERT INTO "KubernetesResource" ("name") VALUES ($1)',
574
+ ["n".repeat(500)],
575
+ );
576
+
577
+ expect(audit.violations).toEqual([]);
578
+ expect(audit.clampedToBound).toEqual(["name"]);
579
+ });
580
+
581
+ test("throws rather than passing vacuously when it cannot find a column list", () => {
582
+ expect(() => {
583
+ return auditFirstRow(new KubernetesResource(), "UPDATE something", []);
584
+ }).toThrow("Could not parse an INSERT column list");
585
+ });
586
+ });