@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,176 @@
1
+ /*
2
+ * Pure resolution of breadcrumb destinations.
3
+ *
4
+ * A breadcrumb trail mirrors the URL segments of the current page: crumb `i`
5
+ * conceptually corresponds to the URL truncated to `i + 3` segments (the
6
+ * leading "", "dashboard" and the project id account for the first three).
7
+ *
8
+ * Historically each crumb linked to that truncated URL directly. That is wrong
9
+ * for the many intermediate segments that are only section *prefixes* with no
10
+ * page of their own — `/user-settings`, `/on-call-duty`, `/<product>/settings`,
11
+ * `/ai`, `/llm`. Linking to such a prefix navigates to a route that matches
12
+ * nothing and renders a blank page. Leaf detail pages (`/traces/view/:id`) have
13
+ * the mirror problem: the last crumb's truncated URL drops the id.
14
+ *
15
+ * This module contains the pure, framework-agnostic decision of *what* each
16
+ * crumb should point at, expressed as a `BreadcrumbTarget` descriptor. It has no
17
+ * dependency on the router or the app's route table so it can be unit-tested in
18
+ * isolation. Callers translate the descriptor into a concrete route.
19
+ */
20
+
21
+ export enum BreadcrumbTargetKind {
22
+ // Link to the project home (URL truncated to its first 3 segments).
23
+ Home = "home",
24
+ // The current page — rendered as plain text, never a link.
25
+ CurrentPage = "current",
26
+ // Link to an ancestor page that lies on the current path, at `depth` segments.
27
+ Ancestor = "ancestor",
28
+ // Link to a real page on a different branch (the section's landing page).
29
+ SectionLanding = "landing",
30
+ }
31
+
32
+ export interface BreadcrumbTarget {
33
+ kind: BreadcrumbTargetKind;
34
+ /*
35
+ * Number of URL segments to keep (including the leading empty segment) when
36
+ * building the link. Present for Home and Ancestor.
37
+ */
38
+ depth?: number | undefined;
39
+ /*
40
+ * Fully-qualified route pattern (may still contain `:params`) of the section
41
+ * landing page. Present only for SectionLanding.
42
+ */
43
+ routePattern?: string | undefined;
44
+ }
45
+
46
+ export interface ResolveBreadcrumbTrailOptions {
47
+ // Route pattern of the page the trail is for, e.g. "/dashboard/:projectId/x".
48
+ pagePattern: string;
49
+ // Number of crumbs in the trail.
50
+ crumbCount: number;
51
+ // Every real, navigable route pattern, in declaration order.
52
+ realRoutePatterns: ReadonlyArray<string>;
53
+ }
54
+
55
+ /*
56
+ * Strip a trailing slash so "/a/b/" and "/a/b" compare equal. The root "/" is
57
+ * left intact.
58
+ */
59
+ export function normalizeRoutePath(path: string): string {
60
+ if (path.length > 1 && path.endsWith("/")) {
61
+ return path.replace(/\/+$/, "");
62
+ }
63
+ return path;
64
+ }
65
+
66
+ /*
67
+ * Resolve the destination of a single crumb.
68
+ *
69
+ * - First crumb ("Project") -> Home.
70
+ * - Last crumb -> CurrentPage (plain text).
71
+ * - Intermediate crumb -> the nearest real ancestor route on the
72
+ * current path; else the section's
73
+ * landing page; else CurrentPage. It
74
+ * never points at a non-existent route.
75
+ */
76
+ export function resolveBreadcrumbTarget(options: {
77
+ index: number;
78
+ crumbCount: number;
79
+ pagePattern: string;
80
+ realRoutePatterns: ReadonlyArray<string>;
81
+ }): BreadcrumbTarget {
82
+ const { index, crumbCount, pagePattern, realRoutePatterns } = options;
83
+
84
+ if (index <= 0) {
85
+ return { kind: BreadcrumbTargetKind.Home, depth: 3 };
86
+ }
87
+
88
+ if (index >= crumbCount - 1) {
89
+ return { kind: BreadcrumbTargetKind.CurrentPage };
90
+ }
91
+
92
+ const normalizedPage: string = normalizeRoutePath(pagePattern);
93
+ const pageSegments: Array<string> = normalizedPage.split("/");
94
+ const realRouteSet: Set<string> = new Set(
95
+ realRoutePatterns.map(normalizeRoutePath),
96
+ );
97
+
98
+ // Depth (segment count) of this crumb's own URL prefix.
99
+ const crumbDepth: number = index + 3;
100
+
101
+ /*
102
+ * Walk from this crumb's prefix depth toward — but not including — the current
103
+ * page and link to the nearest ancestor that is a real page. This links
104
+ * "Insights" to /ai/insights on an /ai/insights/settings page, and keeps
105
+ * section list pages (e.g. "Traces" -> /traces) linkable as before.
106
+ */
107
+ for (let depth: number = crumbDepth; depth < pageSegments.length; depth++) {
108
+ const ancestorPattern: string = pageSegments.slice(0, depth).join("/");
109
+ if (realRouteSet.has(ancestorPattern)) {
110
+ return { kind: BreadcrumbTargetKind.Ancestor, depth };
111
+ }
112
+ }
113
+
114
+ /*
115
+ * The crumb sits at or beyond the current page's own depth — either a "packed"
116
+ * trail with more crumbs than URL segments (e.g. Project > Profiler > Overview
117
+ * on /profiles, where "Profiler" and "Overview" share the single /profiles
118
+ * segment), or exactly the page depth. There is no distinct ancestor page to
119
+ * point at, so render it as plain text like the current page's own crumb
120
+ * rather than inventing a link to a sibling.
121
+ */
122
+ if (crumbDepth >= pageSegments.length) {
123
+ return { kind: BreadcrumbTargetKind.CurrentPage };
124
+ }
125
+
126
+ /*
127
+ * The prefix is a bare section root with no page above the current page (e.g.
128
+ * /user-settings). Fall back to the section's landing page — the first
129
+ * declared real route under the prefix — so the crumb still navigates
130
+ * somewhere useful.
131
+ */
132
+ const prefixWithSlash: string =
133
+ pageSegments.slice(0, crumbDepth).join("/") + "/";
134
+ const landingPattern: string | undefined = realRoutePatterns
135
+ .map(normalizeRoutePath)
136
+ .find((pattern: string): boolean => {
137
+ return pattern.startsWith(prefixWithSlash);
138
+ });
139
+
140
+ // No page under the prefix, or the landing is the current page -> plain text.
141
+ if (!landingPattern || landingPattern === normalizedPage) {
142
+ return { kind: BreadcrumbTargetKind.CurrentPage };
143
+ }
144
+
145
+ /*
146
+ * The landing is a real page on a different branch of this section (e.g.
147
+ * "On-Call Duty" -> /policies while viewing /schedules). A landing that lay on
148
+ * the current path would already have been returned as an Ancestor by the loop
149
+ * above, so at this point it is always off-path; the caller fills in the
150
+ * project id.
151
+ */
152
+ return {
153
+ kind: BreadcrumbTargetKind.SectionLanding,
154
+ routePattern: landingPattern,
155
+ };
156
+ }
157
+
158
+ // Resolve the destination of every crumb in a trail.
159
+ export function resolveBreadcrumbTrail(
160
+ options: ResolveBreadcrumbTrailOptions,
161
+ ): Array<BreadcrumbTarget> {
162
+ const { pagePattern, crumbCount, realRoutePatterns } = options;
163
+
164
+ const targets: Array<BreadcrumbTarget> = [];
165
+ for (let index: number = 0; index < crumbCount; index++) {
166
+ targets.push(
167
+ resolveBreadcrumbTarget({
168
+ index,
169
+ crumbCount,
170
+ pagePattern,
171
+ realRoutePatterns,
172
+ }),
173
+ );
174
+ }
175
+ return targets;
176
+ }
@@ -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
+ }