@oneuptime/common 12.0.18 → 12.0.20

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 (262) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +23 -0
  2. package/Models/DatabaseModels/Index.ts +4 -2
  3. package/Models/DatabaseModels/Monitor.ts +39 -9
  4. package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
  5. package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
  6. package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
  7. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
  8. package/Models/DatabaseModels/UserSlack.ts +341 -0
  9. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
  10. package/Server/API/EnterpriseLicenseAPI.ts +94 -19
  11. package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
  12. package/Server/API/UserSlackAPI.ts +125 -0
  13. package/Server/EnvironmentConfig.ts +29 -107
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  19. package/Server/Infrastructure/Queue.ts +7 -0
  20. package/Server/Services/EnterpriseLicenseService.ts +60 -0
  21. package/Server/Services/Index.ts +6 -2
  22. package/Server/Services/LlmLogService.ts +25 -9
  23. package/Server/Services/OnCallReadinessService.ts +86 -2
  24. package/Server/Services/ProjectService.ts +171 -0
  25. package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
  26. package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
  27. package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
  28. package/Server/Services/UserNotificationRuleService.ts +589 -11
  29. package/Server/Services/UserNotificationSettingService.ts +138 -0
  30. package/Server/Services/UserService.ts +28 -0
  31. package/Server/Services/UserSlackService.ts +218 -0
  32. package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
  33. package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
  34. package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
  35. package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
  36. package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
  37. package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
  38. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
  39. package/Server/Utils/Response.ts +2 -2
  40. package/Server/Utils/StartServer.ts +2 -2
  41. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
  42. package/Server/Views/Partials/AnalyticsConsent.ejs +533 -0
  43. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
  44. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
  45. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
  46. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +302 -0
  47. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
  48. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
  49. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
  50. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
  51. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
  52. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
  53. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
  54. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
  55. package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
  56. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
  57. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
  58. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
  59. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
  60. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
  61. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
  62. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
  63. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
  64. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
  65. package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
  66. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
  67. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
  68. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
  69. package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
  70. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
  71. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
  72. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
  73. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
  74. package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
  75. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
  76. package/Tests/Server/Services/UserSlackService.test.ts +407 -0
  77. package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
  78. package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
  79. package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
  80. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
  81. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
  82. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
  83. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
  84. package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
  85. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
  86. package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
  87. package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
  88. package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
  89. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
  90. package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
  91. package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
  92. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
  93. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
  94. package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
  95. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
  96. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
  97. package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
  98. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +155 -0
  99. package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
  100. package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
  101. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
  102. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
  103. package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
  104. package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
  105. package/Types/Analytics/RevenueEvent.ts +1 -0
  106. package/Types/Marketing/MarketingEvent.ts +140 -0
  107. package/UI/Components/Charts/Area/AreaChart.tsx +8 -1
  108. package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
  109. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +19 -4
  110. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +20 -2
  111. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +19 -4
  112. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +105 -0
  113. package/UI/Components/Charts/Line/LineChart.tsx +8 -1
  114. package/UI/Components/Detail/Detail.tsx +18 -1
  115. package/UI/Utils/NotificationMethodUtil.ts +41 -1
  116. package/UI/Utils/PermissionGate.ts +56 -0
  117. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
  118. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
  119. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  120. package/build/dist/Models/DatabaseModels/Index.js +4 -2
  121. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
  123. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
  125. package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
  126. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
  127. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
  129. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
  131. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
  133. package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
  134. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  135. package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
  136. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  137. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
  138. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
  139. package/build/dist/Server/API/UserSlackAPI.js +81 -0
  140. package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
  141. package/build/dist/Server/EnvironmentConfig.js +25 -70
  142. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
  151. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  152. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  153. package/build/dist/Server/Infrastructure/Queue.js +7 -0
  154. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  155. package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
  156. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  157. package/build/dist/Server/Services/Index.js +6 -2
  158. package/build/dist/Server/Services/Index.js.map +1 -1
  159. package/build/dist/Server/Services/LlmLogService.js +25 -9
  160. package/build/dist/Server/Services/LlmLogService.js.map +1 -1
  161. package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
  162. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
  163. package/build/dist/Server/Services/ProjectService.js +144 -0
  164. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  165. package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
  166. package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
  167. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
  168. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  169. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
  170. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  171. package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
  172. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  173. package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
  174. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  175. package/build/dist/Server/Services/UserService.js +26 -1
  176. package/build/dist/Server/Services/UserService.js.map +1 -1
  177. package/build/dist/Server/Services/UserSlackService.js +195 -0
  178. package/build/dist/Server/Services/UserSlackService.js.map +1 -0
  179. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
  180. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  181. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
  182. package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
  183. package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
  184. package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
  185. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
  186. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
  187. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
  188. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
  189. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
  190. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
  192. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
  193. package/build/dist/Server/Utils/Response.js +2 -2
  194. package/build/dist/Server/Utils/Response.js.map +1 -1
  195. package/build/dist/Server/Utils/StartServer.js +2 -2
  196. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  197. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
  198. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  199. package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
  200. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
  201. package/build/dist/Types/Marketing/MarketingEvent.js +82 -0
  202. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
  203. package/build/dist/UI/Components/Charts/Area/AreaChart.js +9 -1
  204. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  205. package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
  206. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  207. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +22 -13
  208. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  209. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +27 -15
  210. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  211. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +27 -18
  212. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  213. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +71 -0
  214. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
  215. package/build/dist/UI/Components/Charts/Line/LineChart.js +9 -1
  216. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  217. package/build/dist/UI/Components/Detail/Detail.js +17 -1
  218. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  219. package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
  220. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
  221. package/build/dist/UI/Utils/PermissionGate.js +32 -0
  222. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  223. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
  224. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
  225. package/package.json +1 -1
  226. package/Models/DatabaseModels/MarketingConversion.ts +0 -410
  227. package/Server/Services/MarketingConversionService.ts +0 -10
  228. package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
  229. package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
  230. package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
  231. package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
  232. package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
  233. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
  234. package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
  235. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
  236. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
  237. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
  238. package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
  239. package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
  240. package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
  241. package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
  242. package/Types/Marketing/MarketingConversion.ts +0 -53
  243. package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
  244. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
  245. package/build/dist/Server/Services/MarketingConversionService.js +0 -9
  246. package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
  247. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
  248. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
  249. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
  250. package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
  251. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
  252. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
  253. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
  254. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
  255. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
  256. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
  257. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
  258. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
  259. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
  260. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
  261. package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
  262. package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
@@ -0,0 +1,140 @@
1
+ import { JSONObject } from "../JSON";
2
+
3
+ /*
4
+ * The outbound marketing event contract.
5
+ *
6
+ * OneUptime does not keep a conversion ledger. The moments worth
7
+ * measuring are emitted as signed webhooks at the instant they happen and are
8
+ * not stored here afterwards, so this interface — not a database table — is
9
+ * the whole record of what a conversion looks like.
10
+ *
11
+ * Two consequences follow from having no store, and both are the receiver's
12
+ * job rather than ours:
13
+ *
14
+ * - DEDUPLICATION. `eventId` is stable for a given real-world occurrence, so
15
+ * a redelivery carries the id the first attempt did. Retries are expected
16
+ * (the queue retries, and Cal retries into us), so a receiver that keys on
17
+ * anything else will double-count.
18
+ * - ORDERING. Events are delivered per-occurrence with no sequence number.
19
+ * A signup and a plan change seconds apart may arrive either way round;
20
+ * order by `occurredAt`, never by arrival.
21
+ *
22
+ * Names are the same snake_case the browser funnel uses
23
+ * (Common/Types/Analytics/RevenueEvent.ts) so one event means one thing on
24
+ * both sides. Adding a property is safe; changing what an existing one means
25
+ * requires a new schema version.
26
+ */
27
+ export enum MarketingEventType {
28
+ SignUp = "sign_up",
29
+ MeetingBooked = "meeting_booked",
30
+ /*
31
+ * A project's first paid subscription, created together with the project.
32
+ *
33
+ * Deliberately not a subscription_upgraded. An upgrade is a movement between
34
+ * two tiers and reports expansion; this has no previous tier at all and is
35
+ * new business. Reporting one as the other mixes net-new with expansion
36
+ * revenue, and nothing downstream could separate them again.
37
+ */
38
+ SubscriptionStarted = "subscription_started",
39
+ SubscriptionUpgraded = "subscription_upgraded",
40
+ SubscriptionDowngraded = "subscription_downgraded",
41
+ /*
42
+ * A sales-led licence was issued, carrying its annual contract value.
43
+ *
44
+ * This is the only event that reports enterprise revenue, and the only one
45
+ * whose email is typed in by a human rather than captured from a session —
46
+ * which is exactly what makes it joinable: set EnterpriseLicense.email to
47
+ * the address the customer booked with and this event shares an identity
48
+ * with the meeting_booked that preceded it, months earlier.
49
+ */
50
+ EnterpriseLicenseIssued = "enterprise_license_issued",
51
+ }
52
+
53
+ /*
54
+ * Which conversation a booked meeting actually is.
55
+ *
56
+ * All three Cal embeds book the same event type (oneuptimehq/demo), so without
57
+ * this the webhook cannot tell a free user's support call from a net-new
58
+ * enterprise demo — they arrive as one undifferentiated `meeting_booked`, and
59
+ * anything bidding on or reporting against that count values them identically.
60
+ *
61
+ * The browser event has carried this since the embeds were instrumented; the
62
+ * webhook did not, which meant the authoritative record was the one that could
63
+ * not make the distinction.
64
+ *
65
+ * `Unknown` is a real state, not a failure: a booking made through a Cal link
66
+ * that was not one of the instrumented embeds genuinely has no kind, and is
67
+ * still a booking worth emitting.
68
+ */
69
+ export enum MeetingBookingKind {
70
+ EnterpriseDemo = "enterprise_demo",
71
+ SupportCall = "support_call",
72
+ ArchitectureAssessment = "architecture_assessment",
73
+ Unknown = "unknown",
74
+ }
75
+
76
+ /*
77
+ * The Cal booking-metadata key the embeds write and the webhook reads. Shared
78
+ * so a rename cannot land on one side only - the failure mode there is silent,
79
+ * because a booking with no recognised kind still succeeds.
80
+ */
81
+ export const BOOKING_KIND_METADATA_KEY: string = "ou_booking_kind";
82
+
83
+ export const MeetingBookingKinds: Array<string> =
84
+ Object.values(MeetingBookingKind);
85
+
86
+ export const MARKETING_EVENT_SCHEMA_VERSION: number = 1;
87
+
88
+ /*
89
+ * The campaign the converting visitor carried.
90
+ *
91
+ * Copied from the User or Project row for a signup or a plan change, and read
92
+ * out of Cal booking metadata for a booked meeting. Every field is optional:
93
+ * a conversion with no attribution at all is still a conversion, and is still
94
+ * emitted.
95
+ */
96
+ export interface MarketingEventAttribution extends JSONObject {
97
+ utmSource?: string | undefined;
98
+ utmMedium?: string | undefined;
99
+ utmCampaign?: string | undefined;
100
+ utmTerm?: string | undefined;
101
+ utmContent?: string | undefined;
102
+ utmUrl?: string | undefined;
103
+ clickIds: JSONObject;
104
+ firstTouch: JSONObject;
105
+ }
106
+
107
+ export interface MarketingEvent extends JSONObject {
108
+ schemaVersion: number;
109
+ /*
110
+ * Stable per real-world occurrence, and the receiver's deduplication key:
111
+ *
112
+ * sign_up:{userId}
113
+ * meeting_booked:{calBookingUid}
114
+ * subscription_started:{projectId}
115
+ * subscription_upgraded:{projectId}:{occurredAt}
116
+ * subscription_downgraded:{projectId}:{occurredAt}
117
+ * enterprise_license_issued:{enterpriseLicenseId}
118
+ *
119
+ * Four of these are naturally unique — a user signs up once, a booking has
120
+ * one uid, a project has one first subscription, a licence row is issued
121
+ * once — so Cal retrying a delivery or the queue retrying a job cannot
122
+ * produce a second conversion. A plan change can legitimately recur for one
123
+ * project, so its id carries the instant it happened.
124
+ */
125
+ eventId: string;
126
+ eventType: MarketingEventType;
127
+ // ISO 8601, UTC. The moment the conversion happened, not the moment it was sent.
128
+ occurredAt: string;
129
+ /*
130
+ * Identity. Both forms are sent: the address for a direct CRM join, and its
131
+ * SHA-256 (trimmed and lowercased before hashing, per the usual ad-platform
132
+ * normalisation) for anywhere a digest is wanted instead. Absent only when
133
+ * OneUptime genuinely has no address for the conversion.
134
+ */
135
+ email?: string | undefined;
136
+ emailHash?: string | undefined;
137
+ attribution: MarketingEventAttribution;
138
+ // Event-specific detail. See the per-event builders in MarketingEventUtil.
139
+ data: JSONObject;
140
+ }
@@ -181,7 +181,14 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
181
181
 
182
182
  return (
183
183
  <div
184
- className="relative flex flex-1"
184
+ /*
185
+ * `isolate` opens a stacking context so the recharts tooltip's
186
+ * z-index competes only inside this chart — without it the tooltip
187
+ * (zIndex 10, pinned to the chart's top edge) ties with the app
188
+ * header's sticky z-10 in the root context and paints over the
189
+ * navbar on half-scrolled charts.
190
+ */
191
+ className="relative isolate flex flex-1"
185
192
  style={props.heightInPx ? { height: `${props.heightInPx}px` } : undefined}
186
193
  >
187
194
  <AreaChart
@@ -115,7 +115,14 @@ const BarChartElement: FunctionComponent<BarInternalProps> = (
115
115
 
116
116
  return (
117
117
  <div
118
- className="relative flex flex-1"
118
+ /*
119
+ * `isolate` opens a stacking context so the recharts tooltip's
120
+ * z-index competes only inside this chart — without it the tooltip
121
+ * (zIndex 10, pinned to the chart's top edge) ties with the app
122
+ * header's sticky z-10 in the root context and paints over the
123
+ * navbar on half-scrolled charts.
124
+ */
125
+ className="relative isolate flex flex-1"
119
126
  style={props.heightInPx ? { height: `${props.heightInPx}px` } : undefined}
120
127
  >
121
128
  <BarChart
@@ -39,6 +39,10 @@ import {
39
39
  import { cx } from "../Utils/Cx";
40
40
  import { getYAxisDomain } from "../Utils/GetYAxisDomain";
41
41
  import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey";
42
+ import {
43
+ PreparedTooltipEntries,
44
+ prepareTooltipEntries,
45
+ } from "../Utils/TooltipEntries";
42
46
  import ChartCurve from "../../Types/ChartCurve";
43
47
 
44
48
  /*
@@ -475,9 +479,15 @@ const ChartTooltip: ({
475
479
  valueFormatter,
476
480
  }: ChartTooltipProps): React.JSX.Element | null => {
477
481
  if (active && payload && payload.length) {
478
- const legendPayload: PayloadItem[] = payload.filter((item: PayloadItem) => {
479
- return item.type !== "none";
480
- });
482
+ /*
483
+ * Highest value at the hovered timestamp first, capped — on a grouped
484
+ * chart (one series per host/pod), the spiking series must be the
485
+ * first line of the tooltip, not buried at its alphabetical position.
486
+ */
487
+ const {
488
+ entries: legendPayload,
489
+ overflowCount,
490
+ }: PreparedTooltipEntries<PayloadItem> = prepareTooltipEntries(payload);
481
491
  return (
482
492
  <div
483
493
  className={cx(
@@ -531,6 +541,11 @@ const ChartTooltip: ({
531
541
  );
532
542
  },
533
543
  )}
544
+ {overflowCount > 0 ? (
545
+ <p className={cx("pt-1 text-xs", "text-gray-400")}>
546
+ +{overflowCount} more series — highest values shown
547
+ </p>
548
+ ) : null}
534
549
  </div>
535
550
  </div>
536
551
  );
@@ -1426,4 +1441,4 @@ const AreaChart: React.ForwardRefExoticComponent<
1426
1441
 
1427
1442
  AreaChart.displayName = "AreaChart";
1428
1443
 
1429
- export { AreaChart, type AreaChartEventProps, type TooltipProps };
1444
+ export { AreaChart, ChartTooltip, type AreaChartEventProps, type TooltipProps };
@@ -33,6 +33,10 @@ import {
33
33
  } from "../Utils/ChartColors";
34
34
  import { cx } from "../Utils/Cx";
35
35
  import { getYAxisDomain } from "../Utils/GetYAxisDomain";
36
+ import {
37
+ PreparedTooltipEntries,
38
+ prepareTooltipEntries,
39
+ } from "../Utils/TooltipEntries";
36
40
  import { useOnWindowResize } from "../Utils/UseWindowOnResize";
37
41
 
38
42
  /*
@@ -562,6 +566,15 @@ const ChartTooltip: React.FunctionComponent<ChartTooltipProps> = ({
562
566
  valueFormatter,
563
567
  }: ChartTooltipProps): React.ReactElement | null => {
564
568
  if (active && payload && payload.length) {
569
+ /*
570
+ * Highest value at the hovered bucket first, capped — on a grouped
571
+ * chart (one series per host/pod), the spiking series must be the
572
+ * first line of the tooltip, not buried at its alphabetical position.
573
+ */
574
+ const {
575
+ entries: sortedPayload,
576
+ overflowCount,
577
+ }: PreparedTooltipEntries<PayloadItem> = prepareTooltipEntries(payload);
565
578
  return (
566
579
  <div
567
580
  className={cx(
@@ -586,7 +599,7 @@ const ChartTooltip: React.FunctionComponent<ChartTooltipProps> = ({
586
599
  </p>
587
600
  </div>
588
601
  <div className={cx("space-y-1 px-4 py-2")}>
589
- {payload.map(
602
+ {sortedPayload.map(
590
603
  ({ value, category, color }: PayloadItem, index: number) => {
591
604
  return (
592
605
  <div
@@ -631,6 +644,11 @@ const ChartTooltip: React.FunctionComponent<ChartTooltipProps> = ({
631
644
  );
632
645
  },
633
646
  )}
647
+ {overflowCount > 0 ? (
648
+ <p className={cx("pt-1 text-xs", "text-gray-400")}>
649
+ +{overflowCount} more series — highest values shown
650
+ </p>
651
+ ) : null}
634
652
  </div>
635
653
  </div>
636
654
  );
@@ -1194,4 +1212,4 @@ const BarChart: React.ForwardRefExoticComponent<
1194
1212
 
1195
1213
  BarChart.displayName = "BarChart";
1196
1214
 
1197
- export { BarChart, type BarChartEventProps, type TooltipProps };
1215
+ export { BarChart, ChartTooltip, type BarChartEventProps, type TooltipProps };
@@ -40,6 +40,10 @@ import {
40
40
  import { cx } from "../Utils/Cx";
41
41
  import { getYAxisDomain } from "../Utils/GetYAxisDomain";
42
42
  import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey";
43
+ import {
44
+ PreparedTooltipEntries,
45
+ prepareTooltipEntries,
46
+ } from "../Utils/TooltipEntries";
43
47
  import ChartCurve from "../../Types/ChartCurve";
44
48
 
45
49
  /*
@@ -478,9 +482,15 @@ const ChartTooltip: ({
478
482
  valueFormatter,
479
483
  }: ChartTooltipProps): React.JSX.Element | null => {
480
484
  if (active && payload && payload.length) {
481
- const legendPayload: PayloadItem[] = payload.filter((item: PayloadItem) => {
482
- return item.type !== "none";
483
- });
485
+ /*
486
+ * Highest value at the hovered timestamp first, capped — on a grouped
487
+ * chart (one series per host/pod), the spiking series must be the
488
+ * first line of the tooltip, not buried at its alphabetical position.
489
+ */
490
+ const {
491
+ entries: legendPayload,
492
+ overflowCount,
493
+ }: PreparedTooltipEntries<PayloadItem> = prepareTooltipEntries(payload);
484
494
  return (
485
495
  <div
486
496
  className={cx(
@@ -550,6 +560,11 @@ const ChartTooltip: ({
550
560
  );
551
561
  },
552
562
  )}
563
+ {overflowCount > 0 ? (
564
+ <p className={cx("pt-1 text-xs", "text-gray-400")}>
565
+ +{overflowCount} more series — highest values shown
566
+ </p>
567
+ ) : null}
553
568
  </div>
554
569
  </div>
555
570
  );
@@ -1419,4 +1434,4 @@ const LineChart: React.ForwardRefExoticComponent<
1419
1434
 
1420
1435
  LineChart.displayName = "LineChart";
1421
1436
 
1422
- export { LineChart, type LineChartEventProps, type TooltipProps };
1437
+ export { ChartTooltip, LineChart, type LineChartEventProps, type TooltipProps };
@@ -0,0 +1,105 @@
1
+ /*
2
+ * Shared ordering/capping for the hover tooltip of the Area / Line / Bar
3
+ * charts. With a grouped metric (say 20 hosts on one chart) the recharts
4
+ * payload arrives in series-render order — effectively alphabetical — so
5
+ * the series that is actually spiking at the hovered timestamp is buried
6
+ * mid-list. Sorting by the value AT THE HOVERED POINT puts the top series
7
+ * first, and capping the list keeps a high-cardinality chart's tooltip
8
+ * readable (the "+N more" line reports what was elided).
9
+ */
10
+
11
+ /*
12
+ * The slice of a recharts tooltip payload item the ordering needs. The
13
+ * three chart implementations each declare their own structurally-equal
14
+ * PayloadItem, so this stays generic instead of importing any of them.
15
+ */
16
+ export interface SortableTooltipItem {
17
+ category: string;
18
+ value: number;
19
+ type?: string | undefined;
20
+ }
21
+
22
+ export const DEFAULT_TOOLTIP_MAX_ENTRIES: number = 10;
23
+
24
+ export interface PreparedTooltipEntries<T extends SortableTooltipItem> {
25
+ /** Entries to render, highest value first, capped at maxEntries. */
26
+ entries: Array<T>;
27
+ /** How many entries the cap hid (0 when everything fits). */
28
+ overflowCount: number;
29
+ /** Entry count after filtering, before capping. */
30
+ totalCount: number;
31
+ }
32
+
33
+ /*
34
+ * Natural compare so host names order like cpu0 < cpu2 < cpu10 instead of
35
+ * lexicographically — mirrors the series chips' name ordering.
36
+ */
37
+ function compareCategoryNames(a: string, b: string): number {
38
+ return String(a).localeCompare(String(b), undefined, {
39
+ numeric: true,
40
+ sensitivity: "base",
41
+ });
42
+ }
43
+
44
+ export function prepareTooltipEntries<T extends SortableTooltipItem>(
45
+ payload: Array<T> | undefined | null,
46
+ maxEntries: number = DEFAULT_TOOLTIP_MAX_ENTRIES,
47
+ ): PreparedTooltipEntries<T> {
48
+ /*
49
+ * `type === "none"` entries are series excluded from legends/tooltips
50
+ * (recharts tooltipType) — every chart filtered them before this
51
+ * module existed, so the filter lives here now to stay uniform.
52
+ */
53
+ const visibleEntries: Array<T> = (payload || []).filter((item: T) => {
54
+ return item.type !== "none";
55
+ });
56
+
57
+ const sortedEntries: Array<T> = visibleEntries
58
+ .slice()
59
+ .sort((a: T, b: T): number => {
60
+ /*
61
+ * Non-finite values (missing points, the anomaly band's [low, high]
62
+ * array) sort last — they carry no "how high is this series right
63
+ * now" signal, so they must never displace a real reading.
64
+ */
65
+ const aValue: number | null =
66
+ typeof a.value === "number" && Number.isFinite(a.value)
67
+ ? a.value
68
+ : null;
69
+ const bValue: number | null =
70
+ typeof b.value === "number" && Number.isFinite(b.value)
71
+ ? b.value
72
+ : null;
73
+
74
+ if (aValue === null && bValue === null) {
75
+ return compareCategoryNames(a.category, b.category);
76
+ }
77
+ if (aValue === null) {
78
+ return 1;
79
+ }
80
+ if (bValue === null) {
81
+ return -1;
82
+ }
83
+ if (bValue !== aValue) {
84
+ return bValue - aValue;
85
+ }
86
+ return compareCategoryNames(a.category, b.category);
87
+ });
88
+
89
+ const totalCount: number = sortedEntries.length;
90
+
91
+ const cap: number =
92
+ Number.isInteger(maxEntries) && maxEntries > 0
93
+ ? maxEntries
94
+ : DEFAULT_TOOLTIP_MAX_ENTRIES;
95
+
96
+ if (totalCount <= cap) {
97
+ return { entries: sortedEntries, overflowCount: 0, totalCount };
98
+ }
99
+
100
+ return {
101
+ entries: sortedEntries.slice(0, cap),
102
+ overflowCount: totalCount - cap,
103
+ totalCount,
104
+ };
105
+ }
@@ -161,7 +161,14 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
161
161
 
162
162
  return (
163
163
  <div
164
- className="relative flex flex-1"
164
+ /*
165
+ * `isolate` opens a stacking context so the recharts tooltip's
166
+ * z-index competes only inside this chart — without it the tooltip
167
+ * (zIndex 10, pinned to the chart's top edge) ties with the app
168
+ * header's sticky z-10 in the root context and paints over the
169
+ * navbar on half-scrolled charts.
170
+ */
171
+ className="relative isolate flex flex-1"
165
172
  style={props.heightInPx ? { height: `${props.heightInPx}px` } : undefined}
166
173
  >
167
174
  <LineChart
@@ -715,7 +715,24 @@ const Detail: DetailFunction = <T extends GenericObject>(
715
715
  }
716
716
 
717
717
  if (field.getElement) {
718
- data = field.getElement(props.item);
718
+ /*
719
+ * getField runs inside Detail's own render, so a throw out of a field
720
+ * renderer unwinds the whole route to the app level ErrorBoundary and
721
+ * replaces the page with "Something went wrong". One misbehaving field
722
+ * must never cost the entire page: log the misconfiguration and keep
723
+ * whatever this field type already rendered - the entity badge, or ""
724
+ * so the placeholder below can stand in.
725
+ */
726
+ try {
727
+ data = field.getElement(props.item);
728
+ } catch (err) {
729
+ Logger.error(
730
+ "Detail: getElement for '" +
731
+ fieldKeyStr +
732
+ "' threw and was ignored. " +
733
+ (err instanceof Error ? err.message : String(err)),
734
+ );
735
+ }
719
736
  }
720
737
 
721
738
  let className: string = "sm:col-span-1";
@@ -7,6 +7,8 @@ import UserNotificationRule from "../../Models/DatabaseModels/UserNotificationRu
7
7
  import UserPush from "../../Models/DatabaseModels/UserPush";
8
8
  import UserSMS from "../../Models/DatabaseModels/UserSMS";
9
9
  import UserTelegram from "../../Models/DatabaseModels/UserTelegram";
10
+ import UserSlack from "../../Models/DatabaseModels/UserSlack";
11
+ import UserMicrosoftTeams from "../../Models/DatabaseModels/UserMicrosoftTeams";
10
12
  import UserWebhook from "../../Models/DatabaseModels/UserWebhook";
11
13
  import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
12
14
  import ObjectID from "../../Types/ObjectID";
@@ -37,6 +39,8 @@ export interface NotificationMethodModels {
37
39
  userPush: Array<UserPush>;
38
40
  userWhatsApps: Array<UserWhatsApp>;
39
41
  userTelegrams: Array<UserTelegram>;
42
+ userSlacks: Array<UserSlack>;
43
+ userMicrosoftTeamsAccounts: Array<UserMicrosoftTeams>;
40
44
  userWebhooks: Array<UserWebhook>;
41
45
  }
42
46
 
@@ -74,6 +78,16 @@ const NOTIFICATION_METHOD_RELATIONS: Array<NotificationMethodRelation> = [
74
78
  title: "Telegram",
75
79
  columns: ["telegramUserHandle", "telegramChatId"],
76
80
  },
81
+ {
82
+ relationName: "userSlack",
83
+ title: "Slack",
84
+ columns: ["slackUserName", "slackUserId"],
85
+ },
86
+ {
87
+ relationName: "userMicrosoftTeams",
88
+ title: "Microsoft Teams",
89
+ columns: ["microsoftTeamsUserName", "microsoftTeamsUserId"],
90
+ },
77
91
  /*
78
92
  * Only `name` is read here. A webhook URL is a bearer credential for
79
93
  * Slack/Discord/Teams style hooks, so it is deliberately never selected into
@@ -185,6 +199,24 @@ export default class NotificationMethodUtil {
185
199
  );
186
200
  }
187
201
 
202
+ if (model instanceof UserSlack) {
203
+ return (
204
+ "Slack: " +
205
+ (NotificationMethodUtil.readColumn(model, "slackUserName") ||
206
+ NotificationMethodUtil.readColumn(model, "slackUserId") ||
207
+ "Unknown Account")
208
+ );
209
+ }
210
+
211
+ if (model instanceof UserMicrosoftTeams) {
212
+ return (
213
+ "Microsoft Teams: " +
214
+ (NotificationMethodUtil.readColumn(model, "microsoftTeamsUserName") ||
215
+ NotificationMethodUtil.readColumn(model, "microsoftTeamsUserId") ||
216
+ "Unknown Account")
217
+ );
218
+ }
219
+
188
220
  const identifier: string =
189
221
  NotificationMethodUtil.readColumn(model, "phone") ||
190
222
  NotificationMethodUtil.readColumn(model, "email") ||
@@ -207,7 +239,7 @@ export default class NotificationMethodUtil {
207
239
 
208
240
  /*
209
241
  * 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.
242
+ * dropdown is single-select, so at most one of the nine foreign keys is set.
211
243
  * An id that matches nothing leaves the rule untouched, and the server then
212
244
  * rejects the create as having no notification method.
213
245
  */
@@ -258,6 +290,12 @@ export default class NotificationMethodUtil {
258
290
  assignById(models.userTelegrams, (id: ObjectID) => {
259
291
  rule.userTelegramId = id;
260
292
  });
293
+ assignById(models.userSlacks, (id: ObjectID) => {
294
+ rule.userSlackId = id;
295
+ });
296
+ assignById(models.userMicrosoftTeamsAccounts, (id: ObjectID) => {
297
+ rule.userMicrosoftTeamsId = id;
298
+ });
261
299
  assignById(models.userWebhooks, (id: ObjectID) => {
262
300
  rule.userWebhookId = id;
263
301
  });
@@ -279,6 +317,8 @@ export default class NotificationMethodUtil {
279
317
  ...models.userPush,
280
318
  ...models.userWhatsApps,
281
319
  ...models.userTelegrams,
320
+ ...models.userSlacks,
321
+ ...models.userMicrosoftTeamsAccounts,
282
322
  ...models.userWebhooks,
283
323
  ];
284
324
  }
@@ -4,6 +4,7 @@ import Permission, {
4
4
  PermissionHelper,
5
5
  PermissionProps,
6
6
  } from "../../Types/Permission";
7
+ import { ColumnAccessControl } from "../../Types/BaseDatabase/AccessControl";
7
8
  import PermissionUtil from "./Permission";
8
9
  import User from "./User";
9
10
 
@@ -52,6 +53,15 @@ export interface PermissionCheckableModel {
52
53
  getDeletePermissions: () => Array<Permission>;
53
54
  }
54
55
 
56
+ /*
57
+ * A model that declares column-level access control. Separate from
58
+ * PermissionCheckableModel because column gating answers a different question
59
+ * ("may this field be SELECTED?") and only the database models carry it.
60
+ */
61
+ export interface ColumnPermissionCheckableModel {
62
+ getColumnAccessControlForAllColumns: () => Dictionary<ColumnAccessControl>;
63
+ }
64
+
55
65
  export interface PermissionGateOptions {
56
66
  /*
57
67
  * Overrides the permissions read from storage. Only used by tests and by
@@ -270,6 +280,52 @@ export default class PermissionGate {
270
280
  };
271
281
  }
272
282
 
283
+ /*
284
+ * Whether the signed-in user may SELECT a column.
285
+ *
286
+ * Column access control is enforced differently from record access control:
287
+ * asking for a column you cannot read is not degraded, it is fatal.
288
+ * ColumnPermission throws `User is not allowed to read on <column> column of
289
+ * <model>` and the whole request fails, so one unreadable field in a select
290
+ * takes down the entire page rather than blanking one value. That is why
291
+ * ModelAction has no column-level members (see the comment on the enum) and
292
+ * why callers must ask this BEFORE building the select, not after.
293
+ *
294
+ * Deliberately fails closed. When the permission snapshot has not landed yet
295
+ * this returns false and the caller omits the field: the page renders
296
+ * without that one value, which is recoverable. Failing open would send the
297
+ * column anyway and hard-fail the request, which is not.
298
+ *
299
+ * A column with no declared read ACL is readable - there is nothing to
300
+ * enforce, and ColumnPermission agrees.
301
+ */
302
+ public static canReadColumn(
303
+ model: ColumnPermissionCheckableModel,
304
+ columnName: string,
305
+ options?: PermissionGateOptions | undefined,
306
+ ): boolean {
307
+ if (User.isMasterAdmin()) {
308
+ return true;
309
+ }
310
+
311
+ const accessControl: ColumnAccessControl | undefined =
312
+ model.getColumnAccessControlForAllColumns()[columnName];
313
+
314
+ const columnPermissions: Array<Permission> = accessControl?.read || [];
315
+
316
+ if (columnPermissions.length === 0) {
317
+ return true;
318
+ }
319
+
320
+ const userPermissions: Array<Permission> =
321
+ options?.permissions ?? PermissionUtil.getAllPermissions();
322
+
323
+ return PermissionHelper.doesPermissionsIntersect(
324
+ userPermissions,
325
+ columnPermissions,
326
+ );
327
+ }
328
+
273
329
  /* Test seam - the props lookup is memoized for the lifetime of the page. */
274
330
  public static clearPermissionPropsCache(): void {
275
331
  permissionPropsCache = null;