@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,310 @@
1
+ import { DropdownOption } from "../Components/Dropdown/Dropdown";
2
+ import Select from "../../Types/BaseDatabase/Select";
3
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
4
+ import UserCall from "../../Models/DatabaseModels/UserCall";
5
+ import UserEmail from "../../Models/DatabaseModels/UserEmail";
6
+ import UserNotificationRule from "../../Models/DatabaseModels/UserNotificationRule";
7
+ import UserPush from "../../Models/DatabaseModels/UserPush";
8
+ import UserSMS from "../../Models/DatabaseModels/UserSMS";
9
+ import UserTelegram from "../../Models/DatabaseModels/UserTelegram";
10
+ import UserWebhook from "../../Models/DatabaseModels/UserWebhook";
11
+ import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
12
+ import ObjectID from "../../Types/ObjectID";
13
+
14
+ /*
15
+ * The four on-call rule pages (alert, incident, alert episode, incident
16
+ * episode) each let a user pick one of their notification methods from a single
17
+ * dropdown. Every method lives in its own model, so building the dropdown and
18
+ * mapping the chosen option back onto the rule used to be ~140 lines duplicated
19
+ * verbatim across all four pages. Adding a method meant four identical edits,
20
+ * and missing one silently dropped that method from a rule type.
21
+ */
22
+
23
+ /*
24
+ * Anything that exposes column values by name. DatabaseBaseModel satisfies
25
+ * this; keeping the surface this small is what makes the display helpers
26
+ * testable without constructing decorated model instances.
27
+ */
28
+ export interface ColumnValueReader {
29
+ getColumnValue: (columnName: string) => unknown;
30
+ }
31
+
32
+ /* The per-user notification methods a rule can point at. */
33
+ export interface NotificationMethodModels {
34
+ userCalls: Array<UserCall>;
35
+ userEmails: Array<UserEmail>;
36
+ userSMSs: Array<UserSMS>;
37
+ userPush: Array<UserPush>;
38
+ userWhatsApps: Array<UserWhatsApp>;
39
+ userTelegrams: Array<UserTelegram>;
40
+ userWebhooks: Array<UserWebhook>;
41
+ }
42
+
43
+ /* One rendered "Notification Method" line, e.g. `Email: jane@example.com`. */
44
+ export interface NotificationMethodDisplayItem {
45
+ /* The method name shown before the colon, e.g. "Email". */
46
+ title: string;
47
+ /* The identifier of the method itself, e.g. the address or device name. */
48
+ value: string;
49
+ }
50
+
51
+ /*
52
+ * A relation on UserNotificationRule / UserOnCallLogTimeline, and how to turn
53
+ * it into a display line. `columns` is tried in order so a method can fall back
54
+ * to a secondary identifier (a Telegram chat id when the handle is unknown).
55
+ */
56
+ interface NotificationMethodRelation {
57
+ relationName: string;
58
+ title: string;
59
+ columns: Array<string>;
60
+ }
61
+
62
+ /*
63
+ * Display order for the "Notification Method" table cell. A rule only ever has
64
+ * one method set, so this order is cosmetic, but it is kept stable so the cell
65
+ * reads identically wherever it is rendered.
66
+ */
67
+ const NOTIFICATION_METHOD_RELATIONS: Array<NotificationMethodRelation> = [
68
+ { relationName: "userEmail", title: "Email", columns: ["email"] },
69
+ { relationName: "userCall", title: "Call", columns: ["phone"] },
70
+ { relationName: "userSms", title: "SMS", columns: ["phone"] },
71
+ { relationName: "userWhatsApp", title: "WhatsApp", columns: ["phone"] },
72
+ {
73
+ relationName: "userTelegram",
74
+ title: "Telegram",
75
+ columns: ["telegramUserHandle", "telegramChatId"],
76
+ },
77
+ /*
78
+ * Only `name` is read here. A webhook URL is a bearer credential for
79
+ * Slack/Discord/Teams style hooks, so it is deliberately never selected into
80
+ * the browser or rendered. `name` is NOT NULL on UserWebhook.
81
+ */
82
+ { relationName: "userWebhook", title: "Webhook", columns: ["name"] },
83
+ { relationName: "userPush", title: "Push", columns: ["deviceName"] },
84
+ ];
85
+
86
+ export default class NotificationMethodUtil {
87
+ /*
88
+ * The relation -> column selection the on-call rule tables need in order to
89
+ * render the "Notification Method" cell. Shared so every table that renders
90
+ * NotificationMethodView selects the same fields; a table that under-selects
91
+ * silently renders a blank cell.
92
+ */
93
+ public static getSelectForNotificationMethods<
94
+ TBaseModel extends BaseModel,
95
+ >(): Select<TBaseModel> {
96
+ const select: Record<string, Record<string, boolean>> = {};
97
+
98
+ for (const relation of NOTIFICATION_METHOD_RELATIONS) {
99
+ const columns: Record<string, boolean> = {};
100
+
101
+ for (const column of relation.columns) {
102
+ columns[column] = true;
103
+ }
104
+
105
+ select[relation.relationName] = columns;
106
+ }
107
+
108
+ return select as Select<TBaseModel>;
109
+ }
110
+
111
+ /*
112
+ * Turns a rule (or an on-call log timeline entry) into the lines to render in
113
+ * the "Notification Method" column. Returns an empty array when no method
114
+ * relation was loaded or populated.
115
+ */
116
+ public static getDisplayItems(
117
+ item: ColumnValueReader,
118
+ ): Array<NotificationMethodDisplayItem> {
119
+ const displayItems: Array<NotificationMethodDisplayItem> = [];
120
+
121
+ for (const relation of NOTIFICATION_METHOD_RELATIONS) {
122
+ const relationValue: unknown = item.getColumnValue(relation.relationName);
123
+
124
+ if (!relationValue || typeof relationValue !== "object") {
125
+ continue;
126
+ }
127
+
128
+ const value: string | undefined = NotificationMethodUtil.firstNonEmpty(
129
+ relationValue as Record<string, unknown>,
130
+ relation.columns,
131
+ );
132
+
133
+ if (value) {
134
+ displayItems.push({ title: relation.title, value: value });
135
+ }
136
+ }
137
+
138
+ return displayItems;
139
+ }
140
+
141
+ /*
142
+ * The dropdown shown on the on-call rule create form. Every method the user
143
+ * has configured becomes one option, prefixed with the method name so a phone
144
+ * number used for both SMS and WhatsApp stays distinguishable.
145
+ */
146
+ public static getDropdownOptions(
147
+ models: NotificationMethodModels,
148
+ ): Array<DropdownOption> {
149
+ return NotificationMethodUtil.toOrderedList(models).map(
150
+ (model: BaseModel) => {
151
+ return {
152
+ label: NotificationMethodUtil.getLabel(model),
153
+ value: model.id!.toString(),
154
+ };
155
+ },
156
+ );
157
+ }
158
+
159
+ /*
160
+ * The label for a single method, e.g. `SMS: +15550100`. Exported separately so
161
+ * the dropdown and any future surface stay in agreement.
162
+ */
163
+ public static getLabel(model: BaseModel): string {
164
+ if (model instanceof UserWebhook) {
165
+ return (
166
+ "Webhook: " +
167
+ (NotificationMethodUtil.readColumn(model, "name") || "Unknown Webhook")
168
+ );
169
+ }
170
+
171
+ if (model instanceof UserPush) {
172
+ return (
173
+ "Push: " +
174
+ (NotificationMethodUtil.readColumn(model, "deviceName") ||
175
+ "Unknown Device")
176
+ );
177
+ }
178
+
179
+ if (model instanceof UserTelegram) {
180
+ return (
181
+ "Telegram: " +
182
+ (NotificationMethodUtil.readColumn(model, "telegramUserHandle") ||
183
+ NotificationMethodUtil.readColumn(model, "telegramChatId") ||
184
+ "Unknown Chat")
185
+ );
186
+ }
187
+
188
+ const identifier: string =
189
+ NotificationMethodUtil.readColumn(model, "phone") ||
190
+ NotificationMethodUtil.readColumn(model, "email") ||
191
+ "";
192
+
193
+ if (model instanceof UserCall) {
194
+ return "Call: " + identifier;
195
+ }
196
+
197
+ if (model instanceof UserSMS) {
198
+ return "SMS: " + identifier;
199
+ }
200
+
201
+ if (model instanceof UserWhatsApp) {
202
+ return "WhatsApp: " + identifier;
203
+ }
204
+
205
+ return "Email: " + identifier;
206
+ }
207
+
208
+ /*
209
+ * Maps the id chosen in the dropdown back onto the rule being created. The
210
+ * dropdown is single-select, so at most one of the seven foreign keys is set.
211
+ * An id that matches nothing leaves the rule untouched, and the server then
212
+ * rejects the create as having no notification method.
213
+ */
214
+ public static setSelectedMethodOnRule(
215
+ rule: UserNotificationRule,
216
+ selectedValue: unknown,
217
+ models: NotificationMethodModels,
218
+ ): UserNotificationRule {
219
+ if (!selectedValue) {
220
+ return rule;
221
+ }
222
+
223
+ const selectedId: string = selectedValue.toString();
224
+
225
+ const assignById: (
226
+ candidates: Array<BaseModel>,
227
+ assign: (id: ObjectID) => void,
228
+ ) => void = (
229
+ candidates: Array<BaseModel>,
230
+ assign: (id: ObjectID) => void,
231
+ ): void => {
232
+ const match: BaseModel | undefined = candidates.find(
233
+ (candidate: BaseModel) => {
234
+ return candidate.id?.toString() === selectedId;
235
+ },
236
+ );
237
+
238
+ if (match) {
239
+ assign(match.id!);
240
+ }
241
+ };
242
+
243
+ assignById(models.userEmails, (id: ObjectID) => {
244
+ rule.userEmailId = id;
245
+ });
246
+ assignById(models.userSMSs, (id: ObjectID) => {
247
+ rule.userSmsId = id;
248
+ });
249
+ assignById(models.userCalls, (id: ObjectID) => {
250
+ rule.userCallId = id;
251
+ });
252
+ assignById(models.userPush, (id: ObjectID) => {
253
+ rule.userPushId = id;
254
+ });
255
+ assignById(models.userWhatsApps, (id: ObjectID) => {
256
+ rule.userWhatsAppId = id;
257
+ });
258
+ assignById(models.userTelegrams, (id: ObjectID) => {
259
+ rule.userTelegramId = id;
260
+ });
261
+ assignById(models.userWebhooks, (id: ObjectID) => {
262
+ rule.userWebhookId = id;
263
+ });
264
+
265
+ return rule;
266
+ }
267
+
268
+ /*
269
+ * Dropdown ordering: methods are grouped by type in a fixed order so the list
270
+ * does not reshuffle between page loads.
271
+ */
272
+ private static toOrderedList(
273
+ models: NotificationMethodModels,
274
+ ): Array<BaseModel> {
275
+ return [
276
+ ...models.userCalls,
277
+ ...models.userEmails,
278
+ ...models.userSMSs,
279
+ ...models.userPush,
280
+ ...models.userWhatsApps,
281
+ ...models.userTelegrams,
282
+ ...models.userWebhooks,
283
+ ];
284
+ }
285
+
286
+ private static readColumn(model: BaseModel, columnName: string): string {
287
+ const value: unknown = model.getColumnValue(columnName);
288
+
289
+ if (value === undefined || value === null || value === "") {
290
+ return "";
291
+ }
292
+
293
+ return value.toString();
294
+ }
295
+
296
+ private static firstNonEmpty(
297
+ source: Record<string, unknown>,
298
+ columns: Array<string>,
299
+ ): string | undefined {
300
+ for (const column of columns) {
301
+ const value: unknown = source[column];
302
+
303
+ if (value !== undefined && value !== null && value !== "") {
304
+ return value.toString();
305
+ }
306
+ }
307
+
308
+ return undefined;
309
+ }
310
+ }