@oneuptime/common 9.5.7 → 9.5.9

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 (178) hide show
  1. package/Models/DatabaseModels/Alert.ts +8 -9
  2. package/Models/DatabaseModels/Incident.ts +5 -5
  3. package/Models/DatabaseModels/IncidentTemplate.ts +4 -3
  4. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -1
  5. package/Models/DatabaseModels/UserOnCallLog.ts +1 -1
  6. package/Models/DatabaseModels/UserPush.ts +2 -1
  7. package/Server/API/UserPushAPI.ts +51 -4
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1770833704656-MigrationName.ts +156 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1770834237090-MigrationName.ts +119 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  11. package/Server/Middleware/UserAuthorization.ts +14 -9
  12. package/Server/Services/AlertEpisodeFeedService.ts +50 -0
  13. package/Server/Services/AlertEpisodeInternalNoteService.ts +162 -0
  14. package/Server/Services/AlertEpisodeMemberService.ts +7 -0
  15. package/Server/Services/AlertEpisodeOwnerTeamService.ts +186 -0
  16. package/Server/Services/AlertEpisodeOwnerUserService.ts +180 -0
  17. package/Server/Services/AlertEpisodeService.ts +68 -0
  18. package/Server/Services/AlertEpisodeStateTimelineService.ts +5 -0
  19. package/Server/Services/AlertService.ts +3 -0
  20. package/Server/Services/IncidentEpisodeFeedService.ts +50 -0
  21. package/Server/Services/IncidentEpisodeInternalNoteService.ts +163 -0
  22. package/Server/Services/IncidentEpisodeMemberService.ts +7 -0
  23. package/Server/Services/IncidentEpisodeOwnerTeamService.ts +189 -0
  24. package/Server/Services/IncidentEpisodeOwnerUserService.ts +183 -0
  25. package/Server/Services/IncidentEpisodePublicNoteService.ts +8 -0
  26. package/Server/Services/IncidentEpisodeService.ts +91 -12
  27. package/Server/Services/IncidentEpisodeStateTimelineService.ts +5 -0
  28. package/Server/Services/IncidentService.ts +5 -0
  29. package/Server/Services/PushNotificationService.ts +129 -27
  30. package/Server/Services/UserNotificationRuleService.ts +13 -3
  31. package/Server/Services/UserPushService.ts +2 -1
  32. package/Server/Services/WorkspaceNotificationRuleService.ts +20 -0
  33. package/Server/Utils/PushNotificationUtil.ts +56 -0
  34. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +1 -1
  35. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +7 -6
  36. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +1 -1
  37. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +7 -6
  38. package/Server/Utils/Workspace/Slack/Actions/Alert.ts +17 -0
  39. package/Server/Utils/Workspace/Slack/Actions/AlertEpisode.ts +27 -12
  40. package/Server/Utils/Workspace/Slack/Actions/Incident.ts +17 -0
  41. package/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.ts +86 -28
  42. package/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.ts +6 -6
  43. package/Server/Utils/Workspace/Slack/Slack.ts +49 -0
  44. package/Server/Utils/Workspace/WorkspaceMessages/Alert.ts +2 -1
  45. package/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.ts +3 -1
  46. package/Server/Utils/Workspace/WorkspaceMessages/Incident.ts +2 -1
  47. package/Server/Utils/Workspace/WorkspaceMessages/IncidentEpisode.ts +3 -1
  48. package/Types/Permission.ts +641 -0
  49. package/Types/PushNotification/PushDeviceType.ts +7 -0
  50. package/Types/PushNotification/PushNotificationRequest.ts +3 -1
  51. package/UI/Components/Detail/Detail.tsx +13 -4
  52. package/UI/Components/Detail/Field.ts +2 -2
  53. package/UI/Components/Dropdown/Dropdown.tsx +38 -7
  54. package/UI/Components/Forms/BasicForm.tsx +35 -5
  55. package/UI/Components/Forms/Fields/PermissionPicker.tsx +261 -0
  56. package/UI/Components/Forms/Types/Field.ts +5 -3
  57. package/UI/Components/ModelDelete/ModelDelete.tsx +4 -1
  58. package/UI/Components/ModelDetail/CardModelDetail.tsx +4 -0
  59. package/UI/Components/ModelDetail/ModelDetail.tsx +4 -1
  60. package/UI/Components/Page/ModelPage.tsx +4 -1
  61. package/UI/Utils/Permission.ts +29 -6
  62. package/build/dist/Models/DatabaseModels/Alert.js +8 -8
  63. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  64. package/build/dist/Models/DatabaseModels/Incident.js +5 -5
  65. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  66. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +3 -3
  67. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -1
  69. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  70. package/build/dist/Models/DatabaseModels/UserOnCallLog.js +1 -1
  71. package/build/dist/Models/DatabaseModels/UserOnCallLog.js.map +1 -1
  72. package/build/dist/Models/DatabaseModels/UserPush.js +2 -1
  73. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  74. package/build/dist/Server/API/UserPushAPI.js +34 -3
  75. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770833704656-MigrationName.js +63 -0
  77. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770833704656-MigrationName.js.map +1 -0
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770834237090-MigrationName.js +46 -0
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770834237090-MigrationName.js.map +1 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  81. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  82. package/build/dist/Server/Middleware/UserAuthorization.js +10 -4
  83. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  84. package/build/dist/Server/Services/AlertEpisodeFeedService.js +33 -0
  85. package/build/dist/Server/Services/AlertEpisodeFeedService.js.map +1 -1
  86. package/build/dist/Server/Services/AlertEpisodeInternalNoteService.js +132 -0
  87. package/build/dist/Server/Services/AlertEpisodeInternalNoteService.js.map +1 -1
  88. package/build/dist/Server/Services/AlertEpisodeMemberService.js +7 -0
  89. package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -1
  90. package/build/dist/Server/Services/AlertEpisodeOwnerTeamService.js +163 -0
  91. package/build/dist/Server/Services/AlertEpisodeOwnerTeamService.js.map +1 -1
  92. package/build/dist/Server/Services/AlertEpisodeOwnerUserService.js +156 -0
  93. package/build/dist/Server/Services/AlertEpisodeOwnerUserService.js.map +1 -1
  94. package/build/dist/Server/Services/AlertEpisodeService.js +53 -0
  95. package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -1
  96. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +4 -0
  97. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -1
  98. package/build/dist/Server/Services/AlertService.js +3 -5
  99. package/build/dist/Server/Services/AlertService.js.map +1 -1
  100. package/build/dist/Server/Services/IncidentEpisodeFeedService.js +33 -0
  101. package/build/dist/Server/Services/IncidentEpisodeFeedService.js.map +1 -1
  102. package/build/dist/Server/Services/IncidentEpisodeInternalNoteService.js +132 -0
  103. package/build/dist/Server/Services/IncidentEpisodeInternalNoteService.js.map +1 -1
  104. package/build/dist/Server/Services/IncidentEpisodeMemberService.js +7 -0
  105. package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
  106. package/build/dist/Server/Services/IncidentEpisodeOwnerTeamService.js +163 -0
  107. package/build/dist/Server/Services/IncidentEpisodeOwnerTeamService.js.map +1 -1
  108. package/build/dist/Server/Services/IncidentEpisodeOwnerUserService.js +156 -0
  109. package/build/dist/Server/Services/IncidentEpisodeOwnerUserService.js.map +1 -1
  110. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +8 -0
  111. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  112. package/build/dist/Server/Services/IncidentEpisodeService.js +72 -10
  113. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  114. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +4 -0
  115. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  116. package/build/dist/Server/Services/IncidentService.js +5 -5
  117. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  118. package/build/dist/Server/Services/PushNotificationService.js +77 -21
  119. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  120. package/build/dist/Server/Services/UserNotificationRuleService.js +12 -9
  121. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  122. package/build/dist/Server/Services/UserPushService.js +2 -1
  123. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  124. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +16 -0
  125. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  126. package/build/dist/Server/Utils/PushNotificationUtil.js +32 -8
  127. package/build/dist/Server/Utils/PushNotificationUtil.js.map +1 -1
  128. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +1 -1
  129. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  130. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +7 -6
  131. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  132. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +1 -1
  133. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  134. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +7 -6
  135. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  136. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +16 -0
  137. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
  138. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js +25 -9
  139. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js.map +1 -1
  140. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +16 -0
  141. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
  142. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js +71 -25
  143. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js.map +1 -1
  144. package/build/dist/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.js +6 -6
  145. package/build/dist/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.js.map +1 -1
  146. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +40 -0
  147. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  148. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/Alert.js +1 -1
  149. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/Alert.js.map +1 -1
  150. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.js +1 -1
  151. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/AlertEpisode.js.map +1 -1
  152. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/Incident.js +1 -1
  153. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/Incident.js.map +1 -1
  154. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/IncidentEpisode.js +1 -1
  155. package/build/dist/Server/Utils/Workspace/WorkspaceMessages/IncidentEpisode.js.map +1 -1
  156. package/build/dist/Types/Permission.js +637 -0
  157. package/build/dist/Types/Permission.js.map +1 -1
  158. package/build/dist/Types/PushNotification/PushDeviceType.js +8 -0
  159. package/build/dist/Types/PushNotification/PushDeviceType.js.map +1 -0
  160. package/build/dist/UI/Components/Detail/Detail.js +7 -1
  161. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  162. package/build/dist/UI/Components/Dropdown/Dropdown.js +17 -2
  163. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  164. package/build/dist/UI/Components/Forms/BasicForm.js +17 -3
  165. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  166. package/build/dist/UI/Components/Forms/Fields/PermissionPicker.js +129 -0
  167. package/build/dist/UI/Components/Forms/Fields/PermissionPicker.js.map +1 -0
  168. package/build/dist/UI/Components/ModelDelete/ModelDelete.js +2 -1
  169. package/build/dist/UI/Components/ModelDelete/ModelDelete.js.map +1 -1
  170. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  171. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  172. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +2 -1
  173. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  174. package/build/dist/UI/Components/Page/ModelPage.js +2 -1
  175. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  176. package/build/dist/UI/Utils/Permission.js +17 -4
  177. package/build/dist/UI/Utils/Permission.js.map +1 -1
  178. package/package.json +2 -1
@@ -33,6 +33,8 @@ import User from "../../Models/DatabaseModels/User";
33
33
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
34
34
  import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
35
35
  import WorkspaceType from "../../Types/Workspace/WorkspaceType";
36
+ import IncidentEpisodeWorkspaceMessages from "../Utils/Workspace/WorkspaceMessages/IncidentEpisode";
37
+ import { MessageBlocksByWorkspaceType } from "./WorkspaceNotificationRuleService";
36
38
  import IncidentService from "./IncidentService";
37
39
  import OnCallDutyPolicyService from "./OnCallDutyPolicyService";
38
40
  import OnCallDutyPolicy from "../../Models/DatabaseModels/OnCallDutyPolicy";
@@ -147,6 +149,17 @@ export class Service extends DatabaseService<Model> {
147
149
 
148
150
  // Create initial state timeline entry
149
151
  Promise.resolve()
152
+ .then(async () => {
153
+ try {
154
+ if (createdItem.projectId && createdItem.id) {
155
+ await this.handleEpisodeWorkspaceOperationsAsync(createdItem);
156
+ }
157
+ } catch (error) {
158
+ logger.error(
159
+ `Workspace operations failed in IncidentEpisodeService.onCreateSuccess: ${error}`,
160
+ );
161
+ }
162
+ })
150
163
  .then(async () => {
151
164
  try {
152
165
  await this.changeEpisodeState({
@@ -193,6 +206,51 @@ export class Service extends DatabaseService<Model> {
193
206
  return createdItem;
194
207
  }
195
208
 
209
+ @CaptureSpan()
210
+ private async handleEpisodeWorkspaceOperationsAsync(
211
+ createdItem: Model,
212
+ ): Promise<void> {
213
+ try {
214
+ if (!createdItem.projectId || !createdItem.id) {
215
+ throw new BadDataException(
216
+ "projectId and id are required for workspace operations",
217
+ );
218
+ }
219
+
220
+ const workspaceResult: {
221
+ channelsCreated: Array<NotificationRuleWorkspaceChannel>;
222
+ } | null =
223
+ await IncidentEpisodeWorkspaceMessages.createChannelsAndInviteUsersToChannels(
224
+ {
225
+ projectId: createdItem.projectId,
226
+ incidentEpisodeId: createdItem.id,
227
+ episodeNumber: createdItem.episodeNumber || 0,
228
+ ...(createdItem.episodeNumberWithPrefix
229
+ ? {
230
+ episodeNumberWithPrefix: createdItem.episodeNumberWithPrefix,
231
+ }
232
+ : {}),
233
+ },
234
+ );
235
+
236
+ if (workspaceResult && workspaceResult.channelsCreated?.length > 0) {
237
+ await this.updateOneById({
238
+ id: createdItem.id,
239
+ data: {
240
+ postUpdatesToWorkspaceChannels:
241
+ workspaceResult.channelsCreated || [],
242
+ },
243
+ props: {
244
+ isRoot: true,
245
+ },
246
+ });
247
+ }
248
+ } catch (error) {
249
+ logger.error(`Error in handleEpisodeWorkspaceOperationsAsync: ${error}`);
250
+ throw error;
251
+ }
252
+ }
253
+
196
254
  @CaptureSpan()
197
255
  private async createEpisodeCreatedFeed(episode: Model): Promise<void> {
198
256
  if (!episode.id || !episode.projectId) {
@@ -213,6 +271,14 @@ export class Service extends DatabaseService<Model> {
213
271
  feedInfoInMarkdown += `This episode was manually created.\n\n`;
214
272
  }
215
273
 
274
+ const episodeCreateMessageBlocks: Array<MessageBlocksByWorkspaceType> =
275
+ await IncidentEpisodeWorkspaceMessages.getIncidentEpisodeCreateMessageBlocks(
276
+ {
277
+ incidentEpisodeId: episode.id,
278
+ projectId: episode.projectId,
279
+ },
280
+ );
281
+
216
282
  await IncidentEpisodeFeedService.createIncidentEpisodeFeedItem({
217
283
  incidentEpisodeId: episode.id,
218
284
  projectId: episode.projectId,
@@ -220,6 +286,10 @@ export class Service extends DatabaseService<Model> {
220
286
  displayColor: Red500,
221
287
  feedInfoInMarkdown: feedInfoInMarkdown,
222
288
  userId: episode.createdByUserId || undefined,
289
+ workspaceNotification: {
290
+ appendMessageBlocks: episodeCreateMessageBlocks,
291
+ sendWorkspaceNotification: true,
292
+ },
223
293
  });
224
294
  }
225
295
 
@@ -952,21 +1022,30 @@ export class Service extends DatabaseService<Model> {
952
1022
  }
953
1023
 
954
1024
  @CaptureSpan()
955
- public getWorkspaceChannelForEpisode(
956
- episode: Model,
957
- workspaceType: WorkspaceType,
958
- ): Array<NotificationRuleWorkspaceChannel> {
959
- if (
960
- !episode.postUpdatesToWorkspaceChannels ||
961
- !Array.isArray(episode.postUpdatesToWorkspaceChannels) ||
962
- episode.postUpdatesToWorkspaceChannels.length === 0
963
- ) {
964
- return [];
1025
+ public async getWorkspaceChannelForEpisode(data: {
1026
+ episodeId: ObjectID;
1027
+ workspaceType?: WorkspaceType | null;
1028
+ }): Promise<Array<NotificationRuleWorkspaceChannel>> {
1029
+ const episode: Model | null = await this.findOneById({
1030
+ id: data.episodeId,
1031
+ select: {
1032
+ postUpdatesToWorkspaceChannels: true,
1033
+ },
1034
+ props: {
1035
+ isRoot: true,
1036
+ },
1037
+ });
1038
+
1039
+ if (!episode) {
1040
+ throw new BadDataException("Incident Episode not found.");
965
1041
  }
966
1042
 
967
- return episode.postUpdatesToWorkspaceChannels.filter(
1043
+ return (episode.postUpdatesToWorkspaceChannels || []).filter(
968
1044
  (channel: NotificationRuleWorkspaceChannel) => {
969
- return channel.workspaceType === workspaceType;
1045
+ if (!data.workspaceType) {
1046
+ return true;
1047
+ }
1048
+ return channel.workspaceType === data.workspaceType;
970
1049
  },
971
1050
  );
972
1051
  }
@@ -383,6 +383,11 @@ export class Service extends DatabaseService<IncidentEpisodeStateTimeline> {
383
383
  ? `**Cause:** \n${createdItem.rootCause}`
384
384
  : undefined,
385
385
  userId: createdItem.createdByUserId || onCreate.createBy.props.userId,
386
+ workspaceNotification: {
387
+ sendWorkspaceNotification: true,
388
+ notifyUserId:
389
+ createdItem.createdByUserId || onCreate.createBy.props.userId,
390
+ },
386
391
  });
387
392
 
388
393
  return createdItem;
@@ -843,6 +843,11 @@ export class Service extends DatabaseService<Model> {
843
843
  projectId: createdItem.projectId,
844
844
  incidentId: createdItem.id,
845
845
  incidentNumber: createdItem.incidentNumber!,
846
+ ...(createdItem.incidentNumberWithPrefix
847
+ ? {
848
+ incidentNumberWithPrefix: createdItem.incidentNumberWithPrefix,
849
+ }
850
+ : {}),
846
851
  });
847
852
 
848
853
  if (workspaceResult && workspaceResult.channelsCreated?.length > 0) {
@@ -1,5 +1,6 @@
1
1
  import PushNotificationRequest from "../../Types/PushNotification/PushNotificationRequest";
2
2
  import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
3
+ import PushDeviceType from "../../Types/PushNotification/PushDeviceType";
3
4
  import ObjectID from "../../Types/ObjectID";
4
5
  import logger from "../Utils/Logger";
5
6
  import UserPushService from "./UserPushService";
@@ -11,6 +12,7 @@ import {
11
12
  VapidSubject,
12
13
  } from "../EnvironmentConfig";
13
14
  import webpush from "web-push";
15
+ import { Expo, ExpoPushMessage, ExpoPushTicket } from "expo-server-sdk";
14
16
  import PushNotificationUtil from "../Utils/PushNotificationUtil";
15
17
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
16
18
  import UserPush from "../../Models/DatabaseModels/UserPush";
@@ -41,6 +43,7 @@ export interface PushNotificationOptions {
41
43
 
42
44
  export default class PushNotificationService {
43
45
  public static isWebPushInitialized = false;
46
+ private static expoClient: Expo = new Expo();
44
47
 
45
48
  public static initializeWebPush(): void {
46
49
  if (this.isWebPushInitialized) {
@@ -76,13 +79,8 @@ export default class PushNotificationService {
76
79
  throw new Error("No devices provided");
77
80
  }
78
81
 
79
- if (request.deviceType !== "web") {
80
- logger.error(`Unsupported device type: ${request.deviceType}`);
81
- throw new Error("Only web push notifications are supported");
82
- }
83
-
84
82
  logger.info(
85
- `Sending web push notifications to ${request.devices.length} devices`,
83
+ `Sending ${request.deviceType} push notifications to ${request.devices.length} devices`,
86
84
  );
87
85
  logger.info(`Notification message: ${JSON.stringify(request.message)}`);
88
86
 
@@ -98,9 +96,25 @@ export default class PushNotificationService {
98
96
  const promises: Promise<void>[] = [];
99
97
 
100
98
  for (const device of request.devices) {
101
- promises.push(
102
- this.sendWebPushNotification(device.token, request.message, options),
103
- );
99
+ if (request.deviceType === PushDeviceType.Web) {
100
+ promises.push(
101
+ this.sendWebPushNotification(device.token, request.message, options),
102
+ );
103
+ } else if (
104
+ request.deviceType === PushDeviceType.iOS ||
105
+ request.deviceType === PushDeviceType.Android
106
+ ) {
107
+ promises.push(
108
+ this.sendExpoPushNotification(
109
+ device.token,
110
+ request.message,
111
+ request.deviceType,
112
+ options,
113
+ ),
114
+ );
115
+ } else {
116
+ logger.error(`Unsupported device type: ${request.deviceType}`);
117
+ }
104
118
  }
105
119
 
106
120
  const results: Array<any> = await Promise.allSettled(promises);
@@ -314,6 +328,81 @@ export default class PushNotificationService {
314
328
  }
315
329
  }
316
330
 
331
+ private static async sendExpoPushNotification(
332
+ expoPushToken: string,
333
+ message: PushNotificationMessage,
334
+ deviceType: PushDeviceType,
335
+ _options: PushNotificationOptions,
336
+ ): Promise<void> {
337
+ if (!Expo.isExpoPushToken(expoPushToken)) {
338
+ throw new Error(
339
+ `Invalid Expo push token for ${deviceType} device: ${expoPushToken}`,
340
+ );
341
+ }
342
+
343
+ try {
344
+ const dataPayload: { [key: string]: string } = {};
345
+ if (message.data) {
346
+ for (const key of Object.keys(message.data)) {
347
+ dataPayload[key] = String(message.data[key]);
348
+ }
349
+ }
350
+ if (message.url || message.clickAction) {
351
+ dataPayload["url"] = message.url || message.clickAction || "";
352
+ }
353
+
354
+ const channelId: string =
355
+ deviceType === PushDeviceType.Android ? "oncall_high" : "default";
356
+
357
+ const expoPushMessage: ExpoPushMessage = {
358
+ to: expoPushToken,
359
+ title: message.title,
360
+ body: message.body,
361
+ data: dataPayload,
362
+ sound: "default",
363
+ priority: "high",
364
+ channelId: channelId,
365
+ };
366
+
367
+ const tickets: ExpoPushTicket[] =
368
+ await this.expoClient.sendPushNotificationsAsync([expoPushMessage]);
369
+
370
+ const ticket: ExpoPushTicket | undefined = tickets[0];
371
+
372
+ if (ticket && ticket.status === "error") {
373
+ const errorTicket: ExpoPushTicket & {
374
+ message?: string;
375
+ details?: { error?: string };
376
+ } = ticket as ExpoPushTicket & {
377
+ message?: string;
378
+ details?: { error?: string };
379
+ };
380
+ logger.error(
381
+ `Expo push notification error for ${deviceType} device: ${errorTicket.message}`,
382
+ );
383
+
384
+ if (errorTicket.details?.error === "DeviceNotRegistered") {
385
+ logger.info(
386
+ "Expo push token is no longer valid (DeviceNotRegistered)",
387
+ );
388
+ }
389
+
390
+ throw new Error(
391
+ `Expo push notification failed: ${errorTicket.message}`,
392
+ );
393
+ }
394
+
395
+ logger.info(
396
+ `Expo push notification sent successfully to ${deviceType} device`,
397
+ );
398
+ } catch (error: any) {
399
+ logger.error(
400
+ `Failed to send Expo push notification to ${deviceType} device: ${error.message}`,
401
+ );
402
+ throw error;
403
+ }
404
+ }
405
+
317
406
  public static async sendPushNotificationToUser(
318
407
  userId: ObjectID,
319
408
  projectId: ObjectID,
@@ -342,33 +431,46 @@ export default class PushNotificationService {
342
431
 
343
432
  if (userPushDevices.length === 0) {
344
433
  logger.info(
345
- `No verified web push devices found for user ${userId.toString()}`,
434
+ `No verified push devices found for user ${userId.toString()}`,
346
435
  );
347
436
  return;
348
437
  }
349
438
 
350
- // Get web devices with tokens and names
351
- const webDevices: Array<{ token: string; name?: string }> = [];
439
+ // Group devices by type
440
+ const devicesByType: Map<
441
+ string,
442
+ Array<{ token: string; name?: string }>
443
+ > = new Map();
352
444
 
353
445
  for (const device of userPushDevices) {
354
- if (device.deviceType === "web") {
355
- webDevices.push({
356
- token: device.deviceToken!,
357
- name: device.deviceName || "Unknown Device",
358
- });
446
+ const type: string = device.deviceType || PushDeviceType.Web;
447
+ if (!devicesByType.has(type)) {
448
+ devicesByType.set(type, []);
359
449
  }
450
+ devicesByType.get(type)!.push({
451
+ token: device.deviceToken!,
452
+ name: device.deviceName || "Unknown Device",
453
+ });
360
454
  }
361
455
 
362
- // Send notifications to web devices
363
- if (webDevices.length > 0) {
364
- await this.sendPushNotification(
365
- {
366
- devices: webDevices,
367
- message: message,
368
- deviceType: "web",
369
- },
370
- options,
371
- );
456
+ // Send notifications to each device type group
457
+ const sendPromises: Promise<void>[] = [];
458
+
459
+ for (const [deviceType, devices] of devicesByType.entries()) {
460
+ if (devices.length > 0) {
461
+ sendPromises.push(
462
+ this.sendPushNotification(
463
+ {
464
+ devices: devices,
465
+ message: message,
466
+ deviceType: deviceType as PushDeviceType,
467
+ },
468
+ options,
469
+ ),
470
+ );
471
+ }
372
472
  }
473
+
474
+ await Promise.allSettled(sendPromises);
373
475
  }
374
476
  }
@@ -29,6 +29,7 @@ import EmailTemplateType from "../../Types/Email/EmailTemplateType";
29
29
  import BadDataException from "../../Types/Exception/BadDataException";
30
30
  import NotificationRuleType from "../../Types/NotificationRule/NotificationRuleType";
31
31
  import ObjectID from "../../Types/ObjectID";
32
+ import PushDeviceType from "../../Types/PushNotification/PushDeviceType";
32
33
  import Phone from "../../Types/Phone";
33
34
  import SMS from "../../Types/SMS/SMS";
34
35
  import WhatsAppMessage from "../../Types/WhatsApp/WhatsAppMessage";
@@ -1101,6 +1102,8 @@ export class Service extends DatabaseService<Model> {
1101
1102
  ...(alert.alertNumber !== undefined && {
1102
1103
  alertNumber: alert.alertNumber,
1103
1104
  }),
1105
+ alertId: alert.id!.toString(),
1106
+ projectId: alert.projectId!.toString(),
1104
1107
  });
1105
1108
 
1106
1109
  // send push notification.
@@ -1115,7 +1118,8 @@ export class Service extends DatabaseService<Model> {
1115
1118
  },
1116
1119
  ],
1117
1120
  message: pushMessage,
1118
- deviceType: notificationRuleItem.userPush.deviceType!,
1121
+ deviceType: notificationRuleItem.userPush
1122
+ .deviceType! as PushDeviceType,
1119
1123
  },
1120
1124
  {
1121
1125
  projectId: options.projectId,
@@ -1175,6 +1179,8 @@ export class Service extends DatabaseService<Model> {
1175
1179
  ...(incident.incidentNumber !== undefined && {
1176
1180
  incidentNumber: incident.incidentNumber,
1177
1181
  }),
1182
+ incidentId: incident.id!.toString(),
1183
+ projectId: incident.projectId!.toString(),
1178
1184
  });
1179
1185
 
1180
1186
  // send push notification.
@@ -1189,7 +1195,8 @@ export class Service extends DatabaseService<Model> {
1189
1195
  },
1190
1196
  ],
1191
1197
  message: pushMessage,
1192
- deviceType: notificationRuleItem.userPush.deviceType!,
1198
+ deviceType: notificationRuleItem.userPush
1199
+ .deviceType! as PushDeviceType,
1193
1200
  },
1194
1201
  {
1195
1202
  projectId: options.projectId,
@@ -1251,6 +1258,8 @@ export class Service extends DatabaseService<Model> {
1251
1258
  ...(alertEpisode.episodeNumberWithPrefix && {
1252
1259
  episodeNumberWithPrefix: alertEpisode.episodeNumberWithPrefix,
1253
1260
  }),
1261
+ alertEpisodeId: alertEpisode.id!.toString(),
1262
+ projectId: alertEpisode.projectId!.toString(),
1254
1263
  });
1255
1264
 
1256
1265
  PushNotificationService.sendPushNotification(
@@ -1264,7 +1273,8 @@ export class Service extends DatabaseService<Model> {
1264
1273
  },
1265
1274
  ],
1266
1275
  message: pushMessage,
1267
- deviceType: notificationRuleItem.userPush.deviceType!,
1276
+ deviceType: notificationRuleItem.userPush
1277
+ .deviceType! as PushDeviceType,
1268
1278
  },
1269
1279
  {
1270
1280
  projectId: options.projectId,
@@ -4,6 +4,7 @@ import { OnCreate, OnDelete } from "../Types/Database/Hooks";
4
4
  import DatabaseService from "./DatabaseService";
5
5
  import BadDataException from "../../Types/Exception/BadDataException";
6
6
  import PositiveNumber from "../../Types/PositiveNumber";
7
+ import PushDeviceType from "../../Types/PushNotification/PushDeviceType";
7
8
  import UserPush from "../../Models/DatabaseModels/UserPush";
8
9
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
9
10
 
@@ -25,7 +26,7 @@ export class Service extends DatabaseService<UserPush> {
25
26
  }
26
27
 
27
28
  // Validate device type
28
- const validDeviceTypes: string[] = ["web", "android", "ios"];
29
+ const validDeviceTypes: string[] = Object.values(PushDeviceType);
29
30
  if (!validDeviceTypes.includes(createBy.data.deviceType)) {
30
31
  throw new BadDataException(
31
32
  "Device type must be one of: " + validDeviceTypes.join(", "),
@@ -52,6 +52,7 @@ import WorkspaceNotificationActionType from "../../Types/Workspace/WorkspaceNoti
52
52
  import ExceptionMessages from "../../Types/Exception/ExceptionMessages";
53
53
  import IncidentEpisode from "../../Models/DatabaseModels/IncidentEpisode";
54
54
  import IncidentEpisodeService from "./IncidentEpisodeService";
55
+ import AlertEpisodeService from "./AlertEpisodeService";
55
56
 
56
57
  export interface MessageBlocksByWorkspaceType {
57
58
  workspaceType: WorkspaceType;
@@ -735,6 +736,25 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
735
736
  );
736
737
  }
737
738
 
739
+ // incident episodes
740
+ if (data.notificationFor.incidentEpisodeId) {
741
+ monitorChannels =
742
+ await IncidentEpisodeService.getWorkspaceChannelForEpisode({
743
+ episodeId: data.notificationFor.incidentEpisodeId,
744
+ workspaceType: data.workspaceType,
745
+ });
746
+ }
747
+
748
+ // alert episodes
749
+ if (data.notificationFor.alertEpisodeId) {
750
+ monitorChannels = await AlertEpisodeService.getWorkspaceChannelForEpisode(
751
+ {
752
+ episodeId: data.notificationFor.alertEpisodeId,
753
+ workspaceType: data.workspaceType,
754
+ },
755
+ );
756
+ }
757
+
738
758
  logger.debug("Workspace channels found:");
739
759
  logger.debug(monitorChannels);
740
760