@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,308 @@
1
+ /**
2
+ * WorkflowService trigger denormalization tests.
3
+ *
4
+ * The runner never parses workflow graphs. It finds workflows by the
5
+ * triggerId / triggerArguments columns, which are derived from the graph and
6
+ * written onto the row. Anything that puts a graph on a workflow without
7
+ * populating those columns produces a workflow that looks correct in the
8
+ * builder and silently never fires.
9
+ *
10
+ * That is exactly what creating a workflow with a graph already attached does
11
+ * - importing a JSON export, or duplicating an existing workflow - so the
12
+ * create path has to denormalize the trigger just like the update path does.
13
+ *
14
+ * updateOneById and the outbound call to the workflow service are both spied
15
+ * on, so no database or network is touched.
16
+ */
17
+
18
+ import WorkflowService from "../../../Server/Services/WorkflowService";
19
+ import Workflow from "../../../Models/DatabaseModels/Workflow";
20
+ import { JSONObject } from "../../../Types/JSON";
21
+ import ObjectID from "../../../Types/ObjectID";
22
+ import API from "../../../Utils/API";
23
+ import { afterEach, describe, expect, test, jest } from "@jest/globals";
24
+
25
+ const WORKFLOW_ID: string = "550e8400-e29b-41d4-a716-446655440000";
26
+
27
+ function scheduleGraph(): JSONObject {
28
+ return {
29
+ nodes: [
30
+ {
31
+ id: "node-1",
32
+ data: {
33
+ metadataId: "Schedule",
34
+ componentType: "Trigger",
35
+ nodeType: "Node",
36
+ arguments: { "schedule-cron": "0 0 * * *" },
37
+ },
38
+ },
39
+ {
40
+ id: "node-2",
41
+ data: {
42
+ metadataId: "Webhook",
43
+ componentType: "Component",
44
+ nodeType: "Node",
45
+ arguments: {},
46
+ },
47
+ },
48
+ ],
49
+ edges: [],
50
+ };
51
+ }
52
+
53
+ function createdWorkflow(graph: JSONObject | undefined): Workflow {
54
+ /*
55
+ * webhookSecretKey is set so onCreateSuccess skips its own key-generation
56
+ * update, leaving updateOneById calls attributable to trigger derivation.
57
+ * projectId is left undefined so the fire-and-forget label/owner rule
58
+ * engines are skipped.
59
+ */
60
+ return {
61
+ _id: WORKFLOW_ID,
62
+ id: new ObjectID(WORKFLOW_ID),
63
+ graph: graph,
64
+ webhookSecretKey: "already-set",
65
+ } as unknown as Workflow;
66
+ }
67
+
68
+ function spyOnUpdateOneById(): jest.SpyInstance {
69
+ return jest
70
+ .spyOn(WorkflowService, "updateOneById")
71
+ .mockResolvedValue(undefined as never) as unknown as jest.SpyInstance;
72
+ }
73
+
74
+ function spyOnApiPost(): jest.SpyInstance {
75
+ return jest
76
+ .spyOn(API, "post")
77
+ .mockResolvedValue({} as never) as unknown as jest.SpyInstance;
78
+ }
79
+
80
+ type OnCreateSuccessFunction = (
81
+ onCreate: unknown,
82
+ createdItem: Workflow,
83
+ ) => Promise<Workflow>;
84
+
85
+ function onCreateSuccess(createdItem: Workflow): Promise<Workflow> {
86
+ const hook: OnCreateSuccessFunction = (
87
+ WorkflowService as unknown as { onCreateSuccess: OnCreateSuccessFunction }
88
+ ).onCreateSuccess.bind(WorkflowService);
89
+
90
+ return hook({ createBy: { data: createdItem }, carryForward: null }, {
91
+ ...createdItem,
92
+ } as Workflow);
93
+ }
94
+
95
+ type OnUpdateSuccessFunction = (
96
+ onUpdate: unknown,
97
+ updatedItemIds: Array<ObjectID>,
98
+ ) => Promise<unknown>;
99
+
100
+ function onUpdateSuccess(graph: JSONObject | undefined): Promise<unknown> {
101
+ const hook: OnUpdateSuccessFunction = (
102
+ WorkflowService as unknown as { onUpdateSuccess: OnUpdateSuccessFunction }
103
+ ).onUpdateSuccess.bind(WorkflowService);
104
+
105
+ return hook(
106
+ {
107
+ updateBy: {
108
+ query: { _id: WORKFLOW_ID },
109
+ data: graph === undefined ? {} : { graph: graph },
110
+ },
111
+ },
112
+ [new ObjectID(WORKFLOW_ID)],
113
+ );
114
+ }
115
+
116
+ describe("WorkflowService trigger denormalization on create", () => {
117
+ afterEach(() => {
118
+ jest.restoreAllMocks();
119
+ });
120
+
121
+ test("writes the trigger from the graph when a workflow is created with one", async () => {
122
+ const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
123
+ spyOnApiPost();
124
+
125
+ await onCreateSuccess(createdWorkflow(scheduleGraph()));
126
+
127
+ expect(updateSpy).toHaveBeenCalledTimes(1);
128
+
129
+ const updateArgs: JSONObject = updateSpy.mock.calls[0]![0] as JSONObject;
130
+ const data: JSONObject = updateArgs["data"] as JSONObject;
131
+
132
+ expect(data["triggerId"]).toBe("Schedule");
133
+ expect(data["triggerArguments"]).toEqual({ "schedule-cron": "0 0 * * *" });
134
+
135
+ // must not recurse back through the hooks it is running inside of.
136
+ expect(updateArgs["props"]).toEqual({ isRoot: true, ignoreHooks: true });
137
+ });
138
+
139
+ test("notifies the workflow service so schedule triggers get registered", async () => {
140
+ spyOnUpdateOneById();
141
+ const postSpy: jest.SpyInstance = spyOnApiPost();
142
+
143
+ await onCreateSuccess(createdWorkflow(scheduleGraph()));
144
+
145
+ expect(postSpy).toHaveBeenCalledTimes(1);
146
+
147
+ const postArgs: JSONObject = postSpy.mock.calls[0]![0] as JSONObject;
148
+
149
+ expect(postArgs["url"]!.toString()).toContain(
150
+ `/workflow/update/${WORKFLOW_ID}`,
151
+ );
152
+ });
153
+
154
+ test("clears the trigger when the graph has no trigger node", async () => {
155
+ const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
156
+ spyOnApiPost();
157
+
158
+ await onCreateSuccess(
159
+ createdWorkflow({
160
+ nodes: [
161
+ {
162
+ id: "node-1",
163
+ data: {
164
+ metadataId: "Webhook",
165
+ componentType: "Component",
166
+ nodeType: "Node",
167
+ arguments: {},
168
+ },
169
+ },
170
+ ],
171
+ edges: [],
172
+ }),
173
+ );
174
+
175
+ const data: JSONObject = (updateSpy.mock.calls[0]![0] as JSONObject)[
176
+ "data"
177
+ ] as JSONObject;
178
+
179
+ expect(data["triggerId"]).toBeNull();
180
+ expect(data["triggerArguments"]).toEqual({});
181
+ });
182
+
183
+ test("does no trigger work for a workflow created without a graph", async () => {
184
+ const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
185
+ const postSpy: jest.SpyInstance = spyOnApiPost();
186
+
187
+ await onCreateSuccess(createdWorkflow(undefined));
188
+
189
+ expect(updateSpy).not.toHaveBeenCalled();
190
+ expect(postSpy).not.toHaveBeenCalled();
191
+ });
192
+
193
+ test("does not fail the create when the workflow service is unreachable", async () => {
194
+ const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
195
+
196
+ jest
197
+ .spyOn(API, "post")
198
+ .mockRejectedValue(new Error("connect ECONNREFUSED") as never);
199
+
200
+ /*
201
+ * The row and its trigger are already persisted at this point, so a
202
+ * workflow service outage must not turn a successful import into an error.
203
+ */
204
+ await expect(
205
+ onCreateSuccess(createdWorkflow(scheduleGraph())),
206
+ ).resolves.toBeDefined();
207
+
208
+ expect(updateSpy).toHaveBeenCalledTimes(1);
209
+ });
210
+
211
+ test("derives the trigger for a genuinely imported workflow, which has no webhook secret yet", async () => {
212
+ const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
213
+ spyOnApiPost();
214
+
215
+ /*
216
+ * webhookSecretKey carries create: [] access control, so it is stripped
217
+ * from export files and an imported workflow reaches this hook without
218
+ * one. That means two updates run in sequence - key generation, then
219
+ * trigger derivation - and the trigger write must still happen.
220
+ */
221
+ const imported: Workflow = {
222
+ _id: WORKFLOW_ID,
223
+ id: new ObjectID(WORKFLOW_ID),
224
+ graph: scheduleGraph(),
225
+ } as unknown as Workflow;
226
+
227
+ await onCreateSuccess(imported);
228
+
229
+ expect(updateSpy).toHaveBeenCalledTimes(2);
230
+
231
+ const secretUpdate: JSONObject = (
232
+ updateSpy.mock.calls[0]![0] as JSONObject
233
+ )["data"] as JSONObject;
234
+ const triggerUpdate: JSONObject = (
235
+ updateSpy.mock.calls[1]![0] as JSONObject
236
+ )["data"] as JSONObject;
237
+
238
+ expect(secretUpdate["webhookSecretKey"]).toBeDefined();
239
+ expect(triggerUpdate["triggerId"]).toBe("Schedule");
240
+ });
241
+
242
+ test("writes no trigger for a graph that carries no nodes key", async () => {
243
+ const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
244
+ spyOnApiPost();
245
+
246
+ await onCreateSuccess(createdWorkflow({ edges: [] }));
247
+
248
+ expect(updateSpy).not.toHaveBeenCalled();
249
+ });
250
+ });
251
+
252
+ describe("WorkflowService trigger denormalization on update", () => {
253
+ afterEach(() => {
254
+ jest.restoreAllMocks();
255
+ });
256
+
257
+ test("writes the trigger when a saved graph carries one", async () => {
258
+ const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
259
+ spyOnApiPost();
260
+
261
+ await onUpdateSuccess(scheduleGraph());
262
+
263
+ expect(updateSpy).toHaveBeenCalledTimes(1);
264
+
265
+ const data: JSONObject = (updateSpy.mock.calls[0]![0] as JSONObject)[
266
+ "data"
267
+ ] as JSONObject;
268
+
269
+ expect(data["triggerId"]).toBe("Schedule");
270
+ expect(data["triggerArguments"]).toEqual({ "schedule-cron": "0 0 * * *" });
271
+ });
272
+
273
+ test("clears the trigger when the trigger node is removed in the builder", async () => {
274
+ const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
275
+ spyOnApiPost();
276
+
277
+ await onUpdateSuccess({ nodes: [], edges: [] });
278
+
279
+ const data: JSONObject = (updateSpy.mock.calls[0]![0] as JSONObject)[
280
+ "data"
281
+ ] as JSONObject;
282
+
283
+ expect(data["triggerId"]).toBeNull();
284
+ expect(data["triggerArguments"]).toEqual({});
285
+ });
286
+
287
+ test("touches no trigger columns when an update does not carry the graph", async () => {
288
+ const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
289
+ spyOnApiPost();
290
+
291
+ // e.g. renaming a workflow must leave its trigger alone.
292
+ await onUpdateSuccess(undefined);
293
+
294
+ expect(updateSpy).not.toHaveBeenCalled();
295
+ });
296
+
297
+ test("always notifies the workflow service so trigger changes take effect", async () => {
298
+ spyOnUpdateOneById();
299
+ const postSpy: jest.SpyInstance = spyOnApiPost();
300
+
301
+ await onUpdateSuccess(scheduleGraph());
302
+
303
+ expect(postSpy).toHaveBeenCalledTimes(1);
304
+ expect(
305
+ (postSpy.mock.calls[0]![0] as JSONObject)["url"]!.toString(),
306
+ ).toContain(`/workflow/update/${WORKFLOW_ID}`);
307
+ });
308
+ });
@@ -15,13 +15,21 @@ import {
15
15
  } from "../../../../Server/Utils/AnalyticsDatabase/ClusterConfig";
16
16
  import UpdateBy from "../../../../Server/Types/AnalyticsDatabase/UpdateBy";
17
17
  import "../../TestingUtils/Init";
18
- import AnalyticsBaseModel from "../../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
18
+ import AnalyticsBaseModel, {
19
+ AnalyticsBaseModelType,
20
+ } from "../../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
19
21
  import Route from "../../../../Types/API/Route";
20
22
  import AnalyticsTableEngine from "../../../../Types/AnalyticsDatabase/AnalyticsTableEngine";
21
23
  import AnalyticsTableColumn, {
22
24
  SkipIndexType,
23
25
  } from "../../../../Types/AnalyticsDatabase/TableColumn";
24
26
  import TableColumnType from "../../../../Types/AnalyticsDatabase/TableColumnType";
27
+ import MetricItemAggMV1m from "../../../../Models/AnalyticsModels/MetricItemAggMV1m";
28
+ import MetricItemAggMV1mByHostV2 from "../../../../Models/AnalyticsModels/MetricItemAggMV1mByHostV2";
29
+ import MetricItemAggMV1mByService from "../../../../Models/AnalyticsModels/MetricItemAggMV1mByService";
30
+ import MetricItemAggMV1mByK8sCluster from "../../../../Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster";
31
+ import MetricItemAggMV1mByContainer from "../../../../Models/AnalyticsModels/MetricItemAggMV1mByContainer";
32
+ import MetricBaselineHourly from "../../../../Models/AnalyticsModels/MetricBaselineHourly";
25
33
 
26
34
  const CLUSTER_ENV_KEY: string = "CLICKHOUSE_CLUSTER_NAME";
27
35
  const SHARDING_ENV_KEY: string = "CLICKHOUSE_SHARDING_KEY";
@@ -167,12 +175,38 @@ describe("ClickHouse cluster-aware schema (always-on)", () => {
167
175
  required: true,
168
176
  type: TableColumnType.ObjectID,
169
177
  }),
178
+ new AnalyticsTableColumn({
179
+ key: "bucketTime",
180
+ title: "Bucket",
181
+ description: "Bucket",
182
+ required: true,
183
+ type: TableColumnType.Date,
184
+ }),
185
+ new AnalyticsTableColumn({
186
+ key: "retentionDate",
187
+ title: "Retention",
188
+ description: "Retention",
189
+ required: true,
190
+ type: TableColumnType.Date,
191
+ simpleAggregateFunction: "max",
192
+ }),
193
+ new AnalyticsTableColumn({
194
+ key: "valueState",
195
+ title: "Value",
196
+ description: "Value",
197
+ required: true,
198
+ type: TableColumnType.AggregateFunction,
199
+ aggregateFunctionDefinition: "sum, Float64",
200
+ }),
170
201
  ],
171
202
  crudApiPath: new Route("agg"),
172
- primaryKeys: ["projectId"],
173
- sortKeys: ["projectId"],
203
+ primaryKeys: ["projectId", "bucketTime"],
204
+ sortKeys: ["projectId", "bucketTime"],
174
205
  partitionKey: "toYYYYMM(bucketTime)",
175
206
  tableEngine: AnalyticsTableEngine.AggregatingMergeTree,
207
+ ttlExpression: "retentionDate DELETE",
208
+ includeBaseColumns: false,
209
+ defaultSortColumn: "bucketTime",
176
210
  });
177
211
  }
178
212
  }
@@ -215,6 +249,133 @@ describe("ClickHouse cluster-aware schema (always-on)", () => {
215
249
  expect(fullText(stmt)).toContain("MetricItemAggMV1mLocal");
216
250
  });
217
251
 
252
+ test("AggregatingMergeTree CREATE uses real measures without the dimension bypass", () => {
253
+ const stmt: Statement = aggregatingGen.toTableCreateStatement();
254
+ expect(stmt.query).toContain("SimpleAggregateFunction(max, DateTime)");
255
+ expect(stmt.query).toContain("AggregateFunction(sum, Float64)");
256
+ expect(fullText(stmt)).not.toContain('"_id"');
257
+ expect(fullText(stmt)).not.toContain('"createdAt"');
258
+ expect(stmt.query).not.toContain("allow_dimensions_outside_sorting_key");
259
+ });
260
+
261
+ test("all metric aggregate models satisfy the strict off-key measure invariant", () => {
262
+ const models: Array<AnalyticsBaseModel> = [
263
+ new MetricItemAggMV1m(),
264
+ new MetricItemAggMV1mByHostV2(),
265
+ new MetricItemAggMV1mByService(),
266
+ new MetricItemAggMV1mByK8sCluster(),
267
+ new MetricItemAggMV1mByContainer(),
268
+ new MetricBaselineHourly(),
269
+ ];
270
+
271
+ for (const model of models) {
272
+ expect(model.tableEngine).toBe(
273
+ AnalyticsTableEngine.AggregatingMergeTree,
274
+ );
275
+ expect(model.getTableColumn("_id")).toBeNull();
276
+ expect(model.getTableColumn("createdAt")).toBeNull();
277
+ expect(model.getTableColumn("updatedAt")).toBeNull();
278
+ expect(model.defaultSortColumn).toBeTruthy();
279
+ expect(model.sortKeys).toContain(model.defaultSortColumn);
280
+ expect(model.tableSettings).not.toContain(
281
+ "allow_dimensions_outside_sorting_key",
282
+ );
283
+
284
+ for (const column of model.tableColumns) {
285
+ if (model.sortKeys.includes(column.key)) {
286
+ continue;
287
+ }
288
+ expect(
289
+ column.type === TableColumnType.AggregateFunction ||
290
+ Boolean(column.simpleAggregateFunction),
291
+ ).toBe(true);
292
+ }
293
+
294
+ const retentionDate: AnalyticsTableColumn | null =
295
+ model.getTableColumn("retentionDate");
296
+ if (retentionDate) {
297
+ expect(retentionDate.simpleAggregateFunction).toBe("max");
298
+ expect(model.materializedViews[0]?.query).toContain(
299
+ "maxSimpleState(retentionDate) AS retentionDate",
300
+ );
301
+ }
302
+ }
303
+ });
304
+
305
+ test("every concrete metric aggregate model generates strict ClickHouse DDL", () => {
306
+ const modelTypes: Array<AnalyticsBaseModelType> = [
307
+ MetricItemAggMV1m,
308
+ MetricItemAggMV1mByHostV2,
309
+ MetricItemAggMV1mByService,
310
+ MetricItemAggMV1mByK8sCluster,
311
+ MetricItemAggMV1mByContainer,
312
+ MetricBaselineHourly,
313
+ ];
314
+
315
+ for (const modelType of modelTypes) {
316
+ const model: AnalyticsBaseModel = new modelType();
317
+ const generator: StatementGenerator<AnalyticsBaseModel> =
318
+ new StatementGenerator<AnalyticsBaseModel>({
319
+ modelType,
320
+ database: ClickhouseAppInstance,
321
+ });
322
+ const statement: Statement = generator.toTableCreateStatement();
323
+ const ddl: string = fullText(statement);
324
+
325
+ expect(statement.query).toContain(
326
+ "ENGINE = ReplicatedAggregatingMergeTree",
327
+ );
328
+ expect(ddl).toContain(`${model.tableName}Local`);
329
+ expect(ddl).not.toContain('"_id"');
330
+ expect(ddl).not.toContain('"createdAt"');
331
+ expect(ddl).not.toContain('"updatedAt"');
332
+ expect(statement.query).not.toContain(
333
+ "allow_dimensions_outside_sorting_key",
334
+ );
335
+
336
+ if (model.getTableColumn("retentionDate")) {
337
+ expect(statement.query).toContain(
338
+ "retentionDate SimpleAggregateFunction(max, DateTime)",
339
+ );
340
+ expect(statement.query).toContain("TTL retentionDate DELETE");
341
+ } else {
342
+ expect(statement.query).not.toContain("retentionDate");
343
+ }
344
+ }
345
+ });
346
+
347
+ test("retention materialized views emit the state type their targets declare", () => {
348
+ const models: Array<AnalyticsBaseModel> = [
349
+ new MetricItemAggMV1m(),
350
+ new MetricItemAggMV1mByHostV2(),
351
+ new MetricItemAggMV1mByService(),
352
+ new MetricItemAggMV1mByK8sCluster(),
353
+ new MetricItemAggMV1mByContainer(),
354
+ ];
355
+
356
+ for (const model of models) {
357
+ const query: string = model.materializedViews[0]?.query || "";
358
+ expect(query).toContain(
359
+ "maxSimpleState(retentionDate) AS retentionDate",
360
+ );
361
+ expect(query).not.toContain("max(retentionDate) AS retentionDate");
362
+ expect(
363
+ model.getTableColumn("retentionDate")?.simpleAggregateFunction,
364
+ ).toBe("max");
365
+ }
366
+
367
+ const baseline: MetricBaselineHourly = new MetricBaselineHourly();
368
+ expect(baseline.getTableColumn("retentionDate")).toBeNull();
369
+ expect(baseline.materializedViews[0]?.query).not.toContain(
370
+ "retentionDate",
371
+ );
372
+ });
373
+
374
+ test("plain MergeTree CREATE does NOT add the aggregating-only setting", () => {
375
+ const stmt: Statement = spanGen.toTableCreateStatement();
376
+ expect(stmt.query).not.toContain("allow_dimensions_outside_sorting_key");
377
+ });
378
+
218
379
  test("Distributed wrapper uses the model sharding key and local table", () => {
219
380
  const q: string = spanGen.toDistributedTableCreateStatement().query;
220
381
  expect(q).toContain("ON CLUSTER 'oneuptime'");
@@ -1102,6 +1102,105 @@ describe("StatementGenerator", () => {
1102
1102
  );
1103
1103
  });
1104
1104
 
1105
+ test("emits AggregateFunction and scalar SimpleAggregateFunction measures", () => {
1106
+ const statement: Statement = generator.toColumnsCreateStatement([
1107
+ new AnalyticsTableColumn({
1108
+ key: "retentionDate",
1109
+ title: "<title>",
1110
+ description: "<description>",
1111
+ required: false,
1112
+ type: TableColumnType.Date,
1113
+ simpleAggregateFunction: "max",
1114
+ }),
1115
+ new AnalyticsTableColumn({
1116
+ key: "valueState",
1117
+ title: "<title>",
1118
+ description: "<description>",
1119
+ required: true,
1120
+ type: TableColumnType.AggregateFunction,
1121
+ aggregateFunctionDefinition: "sum, Float64",
1122
+ }),
1123
+ ]);
1124
+
1125
+ expectStatement(
1126
+ statement,
1127
+ SQL`retentionDate SimpleAggregateFunction(max, DateTime), valueState AggregateFunction(sum, Float64)`,
1128
+ );
1129
+ });
1130
+
1131
+ test("supports SimpleAggregateFunction on non-DateTime scalar types and codecs", () => {
1132
+ const statement: Statement = generator.toColumnsCreateStatement([
1133
+ new AnalyticsTableColumn({
1134
+ key: "sampleCount",
1135
+ title: "<title>",
1136
+ description: "<description>",
1137
+ required: true,
1138
+ type: TableColumnType.UInt64,
1139
+ simpleAggregateFunction: "sum",
1140
+ codec: { codec: "ZSTD", level: 1 },
1141
+ }),
1142
+ ]);
1143
+
1144
+ expectStatement(
1145
+ statement,
1146
+ SQL`sampleCount SimpleAggregateFunction(sum, UInt64) CODEC(ZSTD(1))`,
1147
+ );
1148
+ });
1149
+
1150
+ test("rejects a column that declares both aggregate representations", () => {
1151
+ expect(() => {
1152
+ return generator.toColumnsCreateStatement([
1153
+ new AnalyticsTableColumn({
1154
+ key: "ambiguousState",
1155
+ title: "<title>",
1156
+ description: "<description>",
1157
+ required: true,
1158
+ type: TableColumnType.AggregateFunction,
1159
+ aggregateFunctionDefinition: "sum, Float64",
1160
+ simpleAggregateFunction: "sum",
1161
+ }),
1162
+ ]);
1163
+ }).toThrow(
1164
+ "Column ambiguousState cannot declare both AggregateFunction and SimpleAggregateFunction",
1165
+ );
1166
+ });
1167
+
1168
+ test.each(["", " max", "max ", "max)", "max, DateTime"])(
1169
+ "rejects unsafe SimpleAggregateFunction name %p",
1170
+ (functionName: string) => {
1171
+ expect(() => {
1172
+ return generator.toColumnsCreateStatement([
1173
+ new AnalyticsTableColumn({
1174
+ key: "invalidMeasure",
1175
+ title: "<title>",
1176
+ description: "<description>",
1177
+ required: true,
1178
+ type: TableColumnType.Date,
1179
+ simpleAggregateFunction: functionName,
1180
+ }),
1181
+ ]);
1182
+ }).toThrow(
1183
+ `Column invalidMeasure has invalid simpleAggregateFunction "${functionName}"`,
1184
+ );
1185
+ },
1186
+ );
1187
+
1188
+ test("rejects AggregateFunction without its state definition", () => {
1189
+ expect(() => {
1190
+ return generator.toColumnsCreateStatement([
1191
+ new AnalyticsTableColumn({
1192
+ key: "missingStateDefinition",
1193
+ title: "<title>",
1194
+ description: "<description>",
1195
+ required: true,
1196
+ type: TableColumnType.AggregateFunction,
1197
+ }),
1198
+ ]);
1199
+ }).toThrow(
1200
+ "Column missingStateDefinition is AggregateFunction but missing aggregateFunctionDefinition",
1201
+ );
1202
+ });
1203
+
1105
1204
  test("emits single and pipelined CODEC clauses", () => {
1106
1205
  const statement: Statement = generator.toColumnsCreateStatement([
1107
1206
  new AnalyticsTableColumn({