@oneuptime/common 11.5.12 → 11.6.0

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 (193) hide show
  1. package/Models/DatabaseModels/AIInsight.ts +30 -0
  2. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  3. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  4. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  5. package/Models/DatabaseModels/Project.ts +30 -0
  6. package/Models/DatabaseModels/TelemetryException.ts +128 -0
  7. package/Server/API/AIAgentDataAPI.ts +129 -5
  8. package/Server/API/AIInvestigationAPI.ts +87 -0
  9. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  10. package/Server/API/GlobalConfigAPI.ts +62 -1
  11. package/Server/EnvironmentConfig.ts +22 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  16. package/Server/Services/AIRunService.ts +8 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +48 -45
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/TelemetryExceptionService.ts +320 -24
  20. package/Server/Services/WorkflowService.ts +100 -39
  21. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  22. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
  23. package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
  24. package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
  25. package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
  26. package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
  27. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
  28. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  29. package/Server/Utils/Express.ts +13 -0
  30. package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
  31. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  32. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  33. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  34. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  35. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  36. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
  37. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  38. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  39. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
  40. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
  41. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  42. package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
  43. package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
  44. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  45. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  46. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  47. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  48. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  49. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  50. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  51. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  52. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  53. package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
  54. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  55. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  56. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  57. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  58. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  59. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  60. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  61. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  62. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  63. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  64. package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
  65. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  66. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  67. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  68. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  69. package/Tests/Utils/Array.test.ts +156 -0
  70. package/Tests/Utils/CronTab.test.ts +199 -0
  71. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  72. package/Tests/Utils/Number.test.ts +179 -0
  73. package/Tests/Utils/VersionUtil.test.ts +348 -0
  74. package/Tests/jest.setup.ts +1 -0
  75. package/Types/AI/CodeFixTaskContext.ts +6 -0
  76. package/Types/AI/CodeFixTaskType.ts +38 -1
  77. package/Types/AI/ExceptionAIClassification.ts +27 -0
  78. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  79. package/Types/Log/LogScrubPatternType.ts +7 -0
  80. package/Types/Monitor/MonitorStep.ts +24 -4
  81. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  82. package/Types/Trace/TraceScrubPatternType.ts +7 -0
  83. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  84. package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
  85. package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
  86. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  87. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  88. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  89. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  90. package/Utils/CronTab.ts +823 -0
  91. package/Utils/ModelImportExport.ts +21 -6
  92. package/Utils/VersionUtil.ts +260 -0
  93. package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
  94. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  100. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/Project.js +31 -0
  102. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
  104. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  105. package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
  106. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  107. package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
  108. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  109. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  110. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  111. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  112. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  113. package/build/dist/Server/EnvironmentConfig.js +17 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AIRunService.js +9 -2
  124. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  125. package/build/dist/Server/Services/AnalyticsDatabaseService.js +44 -16
  126. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  127. package/build/dist/Server/Services/DatabaseService.js +53 -1
  128. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  129. package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
  130. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  131. package/build/dist/Server/Services/WorkflowService.js +80 -30
  132. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  133. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  134. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  135. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
  136. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
  137. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
  138. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
  139. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
  140. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
  141. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
  142. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
  143. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
  144. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
  145. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
  146. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
  147. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  148. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  149. package/build/dist/Server/Utils/Express.js +12 -0
  150. package/build/dist/Server/Utils/Express.js.map +1 -1
  151. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
  152. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
  153. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  154. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  155. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  156. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  157. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  158. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  159. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  160. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  161. package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
  162. package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
  163. package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
  164. package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
  165. package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
  166. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
  167. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  168. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  169. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  170. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  171. package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
  172. package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
  173. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  174. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  175. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
  176. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  177. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
  178. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
  179. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  180. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  181. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  182. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  183. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  184. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  185. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  186. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  187. package/build/dist/Utils/CronTab.js +609 -0
  188. package/build/dist/Utils/CronTab.js.map +1 -0
  189. package/build/dist/Utils/ModelImportExport.js +20 -6
  190. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  191. package/build/dist/Utils/VersionUtil.js +193 -0
  192. package/build/dist/Utils/VersionUtil.js.map +1 -0
  193. package/package.json +1 -1
@@ -0,0 +1,311 @@
1
+ import NotificationRuleCondition, {
2
+ ConditionType,
3
+ NotificationRuleConditionCheckOn,
4
+ NotificationRuleConditionUtil,
5
+ } from "../../../../Types/Workspace/NotificationRules/NotificationRuleCondition";
6
+ import NotificationRuleEventType from "../../../../Types/Workspace/NotificationRules/EventType";
7
+ import IncidentNotificationRule from "../../../../Types/Workspace/NotificationRules/NotificationRuleTypes/IncidentNotificationRule";
8
+ import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
9
+
10
+ describe("NotificationRuleConditionUtil", () => {
11
+ const makeRule: (
12
+ overrides?: Partial<IncidentNotificationRule>,
13
+ ) => IncidentNotificationRule = (
14
+ overrides: Partial<IncidentNotificationRule> = {},
15
+ ): IncidentNotificationRule => {
16
+ return {
17
+ filters: [],
18
+ shouldCreateNewChannel: true,
19
+ newChannelTemplateName: "incident-{{id}}",
20
+ shouldPostToExistingChannel: false,
21
+ ...overrides,
22
+ } as unknown as IncidentNotificationRule;
23
+ };
24
+
25
+ const validate: (rule: IncidentNotificationRule) => string | null = (
26
+ rule: IncidentNotificationRule,
27
+ ): string | null => {
28
+ return NotificationRuleConditionUtil.getValidationError({
29
+ notificationRule: rule,
30
+ eventType: NotificationRuleEventType.Incident,
31
+ workspaceType: WorkspaceType.Slack,
32
+ });
33
+ };
34
+
35
+ describe("getValidationError - filters", () => {
36
+ test("requires checkOn on every filter", () => {
37
+ const rule: IncidentNotificationRule = makeRule({
38
+ filters: [
39
+ {
40
+ conditionType: ConditionType.EqualTo,
41
+ value: "x",
42
+ } as unknown as NotificationRuleCondition,
43
+ ],
44
+ });
45
+ expect(validate(rule)).toBe("Check On is required");
46
+ });
47
+
48
+ test("requires a condition type", () => {
49
+ const rule: IncidentNotificationRule = makeRule({
50
+ filters: [
51
+ {
52
+ checkOn: NotificationRuleConditionCheckOn.IncidentTitle,
53
+ conditionType: undefined,
54
+ value: "x",
55
+ },
56
+ ],
57
+ });
58
+ expect(validate(rule)).toBe(
59
+ `Filter Condition is required for ${NotificationRuleConditionCheckOn.IncidentTitle}`,
60
+ );
61
+ });
62
+
63
+ test("requires a value", () => {
64
+ const rule: IncidentNotificationRule = makeRule({
65
+ filters: [
66
+ {
67
+ checkOn: NotificationRuleConditionCheckOn.IncidentTitle,
68
+ conditionType: ConditionType.EqualTo,
69
+ value: undefined,
70
+ },
71
+ ],
72
+ });
73
+ expect(validate(rule)).toBe(
74
+ `Value is required for ${NotificationRuleConditionCheckOn.IncidentTitle}`,
75
+ );
76
+ });
77
+
78
+ test("rejects an empty-array value", () => {
79
+ const rule: IncidentNotificationRule = makeRule({
80
+ filters: [
81
+ {
82
+ checkOn: NotificationRuleConditionCheckOn.IncidentLabels,
83
+ conditionType: ConditionType.ContainsAny,
84
+ value: [],
85
+ },
86
+ ],
87
+ });
88
+ expect(validate(rule)).toBe(
89
+ `Value is required for ${NotificationRuleConditionCheckOn.IncidentLabels}`,
90
+ );
91
+ });
92
+ });
93
+
94
+ describe("getValidationError - channel selection", () => {
95
+ test("requires either create-new or post-to-existing channel", () => {
96
+ const rule: IncidentNotificationRule = makeRule({
97
+ shouldCreateNewChannel: false,
98
+ shouldPostToExistingChannel: false,
99
+ });
100
+ expect(validate(rule)).toBe(
101
+ "Please select either create slack channel or post to existing Slack channel",
102
+ );
103
+ });
104
+
105
+ test("requires an existing channel name when posting to an existing channel", () => {
106
+ const rule: IncidentNotificationRule = makeRule({
107
+ shouldCreateNewChannel: false,
108
+ shouldPostToExistingChannel: true,
109
+ existingChannelNames: " ",
110
+ });
111
+ expect(validate(rule)).toBe("Existing Slack channel name is required");
112
+ });
113
+
114
+ test("requires a new channel name when creating a channel", () => {
115
+ const rule: IncidentNotificationRule = makeRule({
116
+ shouldCreateNewChannel: true,
117
+ newChannelTemplateName: " ",
118
+ });
119
+ expect(validate(rule)).toBe("New Slack channel name is required");
120
+ });
121
+
122
+ test("returns null for a fully valid rule", () => {
123
+ expect(validate(makeRule())).toBeNull();
124
+ });
125
+
126
+ test("skips channel validation for event types that do not post to channels", () => {
127
+ const rule: IncidentNotificationRule = makeRule({
128
+ shouldCreateNewChannel: false,
129
+ shouldPostToExistingChannel: false,
130
+ });
131
+ // Monitor events are not in the channel-required set.
132
+ expect(
133
+ NotificationRuleConditionUtil.getValidationError({
134
+ notificationRule: rule,
135
+ eventType: NotificationRuleEventType.Monitor,
136
+ workspaceType: WorkspaceType.Slack,
137
+ }),
138
+ ).toBeNull();
139
+ });
140
+ });
141
+
142
+ describe("hasValueField", () => {
143
+ test.each([
144
+ ConditionType.IsEmpty,
145
+ ConditionType.IsNotEmpty,
146
+ ConditionType.True,
147
+ ConditionType.False,
148
+ ])(
149
+ "returns false for the valueless condition %s",
150
+ (conditionType: ConditionType) => {
151
+ expect(
152
+ NotificationRuleConditionUtil.hasValueField({
153
+ checkOn: NotificationRuleConditionCheckOn.IncidentTitle,
154
+ conditionType,
155
+ }),
156
+ ).toBe(false);
157
+ },
158
+ );
159
+
160
+ test.each([ConditionType.EqualTo, ConditionType.Contains])(
161
+ "returns true for the value-based condition %s",
162
+ (conditionType: ConditionType) => {
163
+ expect(
164
+ NotificationRuleConditionUtil.hasValueField({
165
+ checkOn: NotificationRuleConditionCheckOn.IncidentTitle,
166
+ conditionType,
167
+ }),
168
+ ).toBe(true);
169
+ },
170
+ );
171
+ });
172
+
173
+ describe("isDropdownValueField", () => {
174
+ test("returns false when checkOn or conditionType is missing", () => {
175
+ expect(
176
+ NotificationRuleConditionUtil.isDropdownValueField({
177
+ checkOn: undefined,
178
+ conditionType: ConditionType.ContainsAny,
179
+ }),
180
+ ).toBe(false);
181
+ expect(
182
+ NotificationRuleConditionUtil.isDropdownValueField({
183
+ checkOn: NotificationRuleConditionCheckOn.IncidentState,
184
+ conditionType: undefined,
185
+ }),
186
+ ).toBe(false);
187
+ });
188
+
189
+ test.each([
190
+ NotificationRuleConditionCheckOn.IncidentState,
191
+ NotificationRuleConditionCheckOn.IncidentSeverity,
192
+ NotificationRuleConditionCheckOn.MonitorLabels,
193
+ NotificationRuleConditionCheckOn.Monitors,
194
+ ])(
195
+ "returns true for the dropdown checkOn %s",
196
+ (checkOn: NotificationRuleConditionCheckOn) => {
197
+ expect(
198
+ NotificationRuleConditionUtil.isDropdownValueField({
199
+ checkOn,
200
+ conditionType: ConditionType.ContainsAny,
201
+ }),
202
+ ).toBe(true);
203
+ },
204
+ );
205
+
206
+ test.each([
207
+ NotificationRuleConditionCheckOn.IncidentTitle,
208
+ NotificationRuleConditionCheckOn.IncidentDescription,
209
+ ])(
210
+ "returns false for the free-text checkOn %s",
211
+ (checkOn: NotificationRuleConditionCheckOn) => {
212
+ expect(
213
+ NotificationRuleConditionUtil.isDropdownValueField({
214
+ checkOn,
215
+ conditionType: ConditionType.EqualTo,
216
+ }),
217
+ ).toBe(false);
218
+ },
219
+ );
220
+ });
221
+
222
+ describe("getCheckOnByEventType", () => {
223
+ test("returns incident checkOns for the Incident event", () => {
224
+ const checkOns: Array<NotificationRuleConditionCheckOn> =
225
+ NotificationRuleConditionUtil.getCheckOnByEventType(
226
+ NotificationRuleEventType.Incident,
227
+ );
228
+ expect(checkOns).toContain(
229
+ NotificationRuleConditionCheckOn.IncidentTitle,
230
+ );
231
+ expect(checkOns).toContain(NotificationRuleConditionCheckOn.Monitors);
232
+ expect(checkOns).not.toContain(
233
+ NotificationRuleConditionCheckOn.AlertTitle,
234
+ );
235
+ });
236
+
237
+ test("returns monitor checkOns for the Monitor event", () => {
238
+ const checkOns: Array<NotificationRuleConditionCheckOn> =
239
+ NotificationRuleConditionUtil.getCheckOnByEventType(
240
+ NotificationRuleEventType.Monitor,
241
+ );
242
+ expect(checkOns).toContain(NotificationRuleConditionCheckOn.MonitorName);
243
+ expect(checkOns).toContain(
244
+ NotificationRuleConditionCheckOn.MonitorStatus,
245
+ );
246
+ });
247
+
248
+ test("returns an empty list for an unhandled event type", () => {
249
+ expect(
250
+ NotificationRuleConditionUtil.getCheckOnByEventType(
251
+ NotificationRuleEventType.OnCallDutyPolicy,
252
+ ),
253
+ ).toEqual([]);
254
+ });
255
+ });
256
+
257
+ describe("getConditionTypeByCheckOn", () => {
258
+ test("free-text fields offer the string comparison operators", () => {
259
+ const types: Array<ConditionType> =
260
+ NotificationRuleConditionUtil.getConditionTypeByCheckOn(
261
+ NotificationRuleConditionCheckOn.IncidentTitle,
262
+ );
263
+ expect(types).toContain(ConditionType.EqualTo);
264
+ expect(types).toContain(ConditionType.Contains);
265
+ expect(types).toContain(ConditionType.StartsWith);
266
+ expect(types).not.toContain(ConditionType.ContainsAll);
267
+ });
268
+
269
+ test("state/severity fields offer ContainsAny / NotContains", () => {
270
+ expect(
271
+ NotificationRuleConditionUtil.getConditionTypeByCheckOn(
272
+ NotificationRuleConditionCheckOn.IncidentState,
273
+ ),
274
+ ).toEqual([ConditionType.ContainsAny, ConditionType.NotContains]);
275
+ });
276
+
277
+ test("label fields additionally offer ContainsAll", () => {
278
+ const types: Array<ConditionType> =
279
+ NotificationRuleConditionUtil.getConditionTypeByCheckOn(
280
+ NotificationRuleConditionCheckOn.IncidentLabels,
281
+ );
282
+ expect(types).toContain(ConditionType.ContainsAll);
283
+ });
284
+
285
+ test("returns an empty list for an unhandled checkOn", () => {
286
+ expect(
287
+ NotificationRuleConditionUtil.getConditionTypeByCheckOn(
288
+ "Unhandled" as NotificationRuleConditionCheckOn,
289
+ ),
290
+ ).toEqual([]);
291
+ });
292
+ });
293
+
294
+ describe("getDropdownOptionsByCheckOn", () => {
295
+ test("returns an empty list when the backing collections are empty", () => {
296
+ expect(
297
+ NotificationRuleConditionUtil.getDropdownOptionsByCheckOn({
298
+ alertSeverities: [],
299
+ alertStates: [],
300
+ incidentSeverities: [],
301
+ monitorStatus: [],
302
+ incidentStates: [],
303
+ scheduledMaintenanceStates: [],
304
+ labels: [],
305
+ monitors: [],
306
+ checkOn: NotificationRuleConditionCheckOn.IncidentSeverity,
307
+ }),
308
+ ).toEqual([]);
309
+ });
310
+ });
311
+ });
@@ -0,0 +1,259 @@
1
+ import Columns from "../../../UI/Components/ModelTable/Columns";
2
+ import {
3
+ getRelationSelectFromColumns,
4
+ getSelectFromColumns,
5
+ } from "../../../UI/Components/ModelTable/SelectFromColumns";
6
+ import FieldType from "../../../UI/Components/Types/FieldType";
7
+ import Alert from "../../../Models/DatabaseModels/Alert";
8
+ import ProxmoxCluster from "../../../Models/DatabaseModels/ProxmoxCluster";
9
+ import BadDataException from "../../../Types/Exception/BadDataException";
10
+ import { describe, expect, test } from "@jest/globals";
11
+
12
+ describe("ModelTable SelectFromColumns", () => {
13
+ describe("getSelectFromColumns", () => {
14
+ test("it selects every field a column declares, not just the first", () => {
15
+ /*
16
+ * Regression for OneUptime/oneuptime#2756: the Proxmox clusters list
17
+ * rendered "0/3 online" for a fully online cluster because only the
18
+ * first key of the column's field map was selected, leaving
19
+ * onlineNodeCount undefined on every row.
20
+ */
21
+ const columns: Columns<ProxmoxCluster> = [
22
+ {
23
+ field: {
24
+ nodeCount: true,
25
+ onlineNodeCount: true,
26
+ },
27
+ title: "Nodes",
28
+ type: FieldType.Element,
29
+ },
30
+ ];
31
+
32
+ const select: Record<string, unknown> =
33
+ getSelectFromColumns<ProxmoxCluster>({
34
+ columns: columns,
35
+ model: new ProxmoxCluster(),
36
+ }) as Record<string, unknown>;
37
+
38
+ expect(select["nodeCount"]).toBe(true);
39
+ expect(select["onlineNodeCount"]).toBe(true);
40
+ });
41
+
42
+ test("it always selects _id", () => {
43
+ const select: Record<string, unknown> =
44
+ getSelectFromColumns<ProxmoxCluster>({
45
+ columns: [],
46
+ model: new ProxmoxCluster(),
47
+ }) as Record<string, unknown>;
48
+
49
+ expect(select["_id"]).toBe(true);
50
+ });
51
+
52
+ test("it merges fields across multiple columns", () => {
53
+ const columns: Columns<ProxmoxCluster> = [
54
+ {
55
+ field: { name: true },
56
+ title: "Name",
57
+ type: FieldType.Text,
58
+ },
59
+ {
60
+ field: { guestCount: true, storageCount: true },
61
+ title: "Inventory",
62
+ type: FieldType.Element,
63
+ },
64
+ ];
65
+
66
+ const select: Record<string, unknown> =
67
+ getSelectFromColumns<ProxmoxCluster>({
68
+ columns: columns,
69
+ model: new ProxmoxCluster(),
70
+ }) as Record<string, unknown>;
71
+
72
+ expect(select["name"]).toBe(true);
73
+ expect(select["guestCount"]).toBe(true);
74
+ expect(select["storageCount"]).toBe(true);
75
+ });
76
+
77
+ test("it skips columns that declare no field", () => {
78
+ const columns: Columns<ProxmoxCluster> = [
79
+ {
80
+ field: undefined as never,
81
+ title: "Actions",
82
+ type: FieldType.Element,
83
+ },
84
+ ];
85
+
86
+ const select: Record<string, unknown> =
87
+ getSelectFromColumns<ProxmoxCluster>({
88
+ columns: columns,
89
+ model: new ProxmoxCluster(),
90
+ }) as Record<string, unknown>;
91
+
92
+ expect(Object.keys(select)).toEqual(["_id"]);
93
+ });
94
+
95
+ test("it throws when the primary field is not a column on the model", () => {
96
+ const columns: Columns<ProxmoxCluster> = [
97
+ {
98
+ field: { notARealColumn: true } as never,
99
+ title: "Nope",
100
+ type: FieldType.Text,
101
+ },
102
+ ];
103
+
104
+ expect(() => {
105
+ return getSelectFromColumns<ProxmoxCluster>({
106
+ columns: columns,
107
+ model: new ProxmoxCluster(),
108
+ });
109
+ }).toThrow(BadDataException);
110
+ });
111
+
112
+ test("it ignores a non-column secondary field rather than blanking the page", () => {
113
+ const columns: Columns<ProxmoxCluster> = [
114
+ {
115
+ field: { nodeCount: true, notARealColumn: true } as never,
116
+ title: "Nodes",
117
+ type: FieldType.Element,
118
+ },
119
+ ];
120
+
121
+ const consoleError: jest.SpyInstance = jest
122
+ .spyOn(console, "error")
123
+ .mockImplementation(() => {});
124
+
125
+ const select: Record<string, unknown> =
126
+ getSelectFromColumns<ProxmoxCluster>({
127
+ columns: columns,
128
+ model: new ProxmoxCluster(),
129
+ }) as Record<string, unknown>;
130
+
131
+ expect(select["nodeCount"]).toBe(true);
132
+ expect(select["notARealColumn"]).toBeUndefined();
133
+ // It must not drop the field silently — that is how #2756 hid.
134
+ expect(consoleError).toHaveBeenCalled();
135
+
136
+ consoleError.mockRestore();
137
+ });
138
+
139
+ test("it drops a secondary field the user cannot read, so the API does not reject the whole request", () => {
140
+ /*
141
+ * checkSelectPermission throws NotAuthorizedException for the entire
142
+ * request when the select names an unreadable column, so widening the
143
+ * select must never add one.
144
+ */
145
+ const columns: Columns<ProxmoxCluster> = [
146
+ {
147
+ field: {
148
+ nodeCount: true,
149
+ onlineNodeCount: true,
150
+ },
151
+ title: "Nodes",
152
+ type: FieldType.Element,
153
+ },
154
+ ];
155
+
156
+ const select: Record<string, unknown> =
157
+ getSelectFromColumns<ProxmoxCluster>({
158
+ columns: columns,
159
+ model: new ProxmoxCluster(),
160
+ hasPermissionToReadField: (field: string): boolean => {
161
+ return field !== "onlineNodeCount";
162
+ },
163
+ }) as Record<string, unknown>;
164
+
165
+ expect(select["nodeCount"]).toBe(true);
166
+ expect(select["onlineNodeCount"]).toBeUndefined();
167
+ });
168
+
169
+ test("it keeps the primary field even when the permission callback rejects it, preserving prior behaviour", () => {
170
+ const columns: Columns<ProxmoxCluster> = [
171
+ {
172
+ field: { nodeCount: true, onlineNodeCount: true },
173
+ title: "Nodes",
174
+ type: FieldType.Element,
175
+ },
176
+ ];
177
+
178
+ const select: Record<string, unknown> =
179
+ getSelectFromColumns<ProxmoxCluster>({
180
+ columns: columns,
181
+ model: new ProxmoxCluster(),
182
+ hasPermissionToReadField: (): boolean => {
183
+ return false;
184
+ },
185
+ }) as Record<string, unknown>;
186
+
187
+ expect(select["nodeCount"]).toBe(true);
188
+ expect(select["onlineNodeCount"]).toBeUndefined();
189
+ });
190
+ });
191
+
192
+ describe("getRelationSelectFromColumns", () => {
193
+ test("it selects every relation a column declares, not just the first", () => {
194
+ const columns: Columns<Alert> = [
195
+ {
196
+ field: {
197
+ monitor: { name: true, _id: true },
198
+ labels: { name: true, color: true },
199
+ },
200
+ title: "Resource",
201
+ type: FieldType.Element,
202
+ },
203
+ ];
204
+
205
+ const relationSelect: Record<string, unknown> =
206
+ getRelationSelectFromColumns<Alert>({
207
+ columns: columns,
208
+ model: new Alert(),
209
+ }) as Record<string, unknown>;
210
+
211
+ expect(relationSelect["monitor"]).toEqual({ name: true, _id: true });
212
+ expect(relationSelect["labels"]).toEqual({ name: true, color: true });
213
+ });
214
+
215
+ test("it leaves non-entity fields out of the relation select", () => {
216
+ const columns: Columns<ProxmoxCluster> = [
217
+ {
218
+ field: {
219
+ nodeCount: true,
220
+ onlineNodeCount: true,
221
+ },
222
+ title: "Nodes",
223
+ type: FieldType.Element,
224
+ },
225
+ ];
226
+
227
+ const relationSelect: Record<string, unknown> =
228
+ getRelationSelectFromColumns<ProxmoxCluster>({
229
+ columns: columns,
230
+ model: new ProxmoxCluster(),
231
+ }) as Record<string, unknown>;
232
+
233
+ expect(relationSelect).toEqual({});
234
+ });
235
+
236
+ test("it merges the same relation declared by two columns", () => {
237
+ const columns: Columns<Alert> = [
238
+ {
239
+ field: { monitor: { name: true } },
240
+ title: "Monitor",
241
+ type: FieldType.Element,
242
+ },
243
+ {
244
+ field: { monitor: { _id: true } },
245
+ title: "Monitor Id",
246
+ type: FieldType.Element,
247
+ },
248
+ ];
249
+
250
+ const relationSelect: Record<string, unknown> =
251
+ getRelationSelectFromColumns<Alert>({
252
+ columns: columns,
253
+ model: new Alert(),
254
+ }) as Record<string, unknown>;
255
+
256
+ expect(relationSelect["monitor"]).toEqual({ name: true, _id: true });
257
+ });
258
+ });
259
+ });