@oneuptime/common 9.5.2 → 9.5.3

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 (100) hide show
  1. package/Models/DatabaseModels/Alert.ts +1 -0
  2. package/Models/DatabaseModels/AlertEpisode.ts +1 -0
  3. package/Models/DatabaseModels/AlertEpisodeStateTimeline.ts +1 -0
  4. package/Models/DatabaseModels/AlertStateTimeline.ts +1 -0
  5. package/Models/DatabaseModels/Incident.ts +1 -0
  6. package/Models/DatabaseModels/IncidentEpisode.ts +156 -0
  7. package/Models/DatabaseModels/IncidentEpisodeFeed.ts +2 -0
  8. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +611 -0
  9. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +84 -0
  10. package/Models/DatabaseModels/IncidentGroupingRule.ts +36 -0
  11. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  12. package/Models/DatabaseModels/Index.ts +2 -0
  13. package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
  14. package/Models/DatabaseModels/Project.ts +2 -1
  15. package/Models/DatabaseModels/ProjectCallSMSConfig.ts +1 -0
  16. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
  17. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
  18. package/Models/DatabaseModels/StatusPage.ts +120 -0
  19. package/Server/API/IncidentEpisodePublicNoteAPI.ts +98 -0
  20. package/Server/API/StatusPageAPI.ts +1092 -45
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1770232207959-MigrationName.ts +181 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1770237245069-MigrationName.ts +35 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  24. package/Server/Services/IncidentEpisodePublicNoteService.ts +254 -0
  25. package/Server/Services/IncidentEpisodeService.ts +26 -0
  26. package/Server/Services/Index.ts +2 -0
  27. package/Server/Utils/Monitor/MonitorIncident.ts +6 -0
  28. package/Types/Email/EmailTemplateType.ts +4 -0
  29. package/Types/Icon/IconProp.ts +172 -0
  30. package/Types/Monitor/CriteriaIncident.ts +2 -0
  31. package/Types/Permission.ts +40 -0
  32. package/Types/StatusPage/StatusPageSubscriberNotificationEventType.ts +5 -0
  33. package/UI/Components/Icon/Icon.tsx +1333 -1
  34. package/build/dist/Models/DatabaseModels/Alert.js +1 -0
  35. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  36. package/build/dist/Models/DatabaseModels/AlertEpisode.js +1 -0
  37. package/build/dist/Models/DatabaseModels/AlertEpisode.js.map +1 -1
  38. package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js +1 -0
  39. package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js.map +1 -1
  40. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +1 -0
  41. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
  42. package/build/dist/Models/DatabaseModels/Incident.js +1 -0
  43. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  44. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +161 -0
  45. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  46. package/build/dist/Models/DatabaseModels/IncidentEpisodeFeed.js +2 -0
  47. package/build/dist/Models/DatabaseModels/IncidentEpisodeFeed.js.map +1 -1
  48. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +626 -0
  49. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -0
  50. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +86 -0
  51. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  52. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js +37 -0
  53. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  55. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  56. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  57. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  58. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  59. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  60. package/build/dist/Models/DatabaseModels/Project.js +2 -1
  61. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  62. package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js +1 -0
  63. package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js.map +1 -1
  64. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  65. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  66. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  67. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/StatusPage.js +126 -0
  69. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  70. package/build/dist/Server/API/IncidentEpisodePublicNoteAPI.js +68 -0
  71. package/build/dist/Server/API/IncidentEpisodePublicNoteAPI.js.map +1 -0
  72. package/build/dist/Server/API/StatusPageAPI.js +874 -47
  73. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770232207959-MigrationName.js +68 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770232207959-MigrationName.js.map +1 -0
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770237245069-MigrationName.js +18 -0
  77. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770237245069-MigrationName.js.map +1 -0
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  80. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +223 -0
  81. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -0
  82. package/build/dist/Server/Services/IncidentEpisodeService.js +22 -0
  83. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  84. package/build/dist/Server/Services/Index.js +2 -0
  85. package/build/dist/Server/Services/Index.js.map +1 -1
  86. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +5 -0
  87. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  88. package/build/dist/Types/Email/EmailTemplateType.js +3 -0
  89. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  90. package/build/dist/Types/Icon/IconProp.js +172 -0
  91. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  92. package/build/dist/Types/Monitor/CriteriaIncident.js +1 -0
  93. package/build/dist/Types/Monitor/CriteriaIncident.js.map +1 -1
  94. package/build/dist/Types/Permission.js +34 -0
  95. package/build/dist/Types/Permission.js.map +1 -1
  96. package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js +4 -0
  97. package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js.map +1 -1
  98. package/build/dist/UI/Components/Icon/Icon.js +502 -1
  99. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  100. package/package.json +1 -1
@@ -1,5 +1,9 @@
1
1
  import UserMiddleware from "../Middleware/UserAuthorization";
2
2
  import AcmeChallengeService from "../Services/AcmeChallengeService";
3
+ import IncidentEpisodeService from "../Services/IncidentEpisodeService";
4
+ import IncidentEpisodeMemberService from "../Services/IncidentEpisodeMemberService";
5
+ import IncidentEpisodePublicNoteService from "../Services/IncidentEpisodePublicNoteService";
6
+ import IncidentEpisodeStateTimelineService from "../Services/IncidentEpisodeStateTimelineService";
3
7
  import IncidentPublicNoteService from "../Services/IncidentPublicNoteService";
4
8
  import IncidentService from "../Services/IncidentService";
5
9
  import IncidentStateService from "../Services/IncidentStateService";
@@ -44,7 +48,7 @@ import Email from "../../Types/Email";
44
48
  import BadDataException from "../../Types/Exception/BadDataException";
45
49
  import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
46
50
  import NotFoundException from "../../Types/Exception/NotFoundException";
47
- import { JSONObject } from "../../Types/JSON";
51
+ import { JSONArray, JSONObject } from "../../Types/JSON";
48
52
  import JSONFunctions from "../../Types/JSONFunctions";
49
53
  import ObjectID from "../../Types/ObjectID";
50
54
  import Phone from "../../Types/Phone";
@@ -52,9 +56,14 @@ import PositiveNumber from "../../Types/PositiveNumber";
52
56
  import HashedString from "../../Types/HashedString";
53
57
  import AcmeChallenge from "../../Models/DatabaseModels/AcmeChallenge";
54
58
  import Incident from "../../Models/DatabaseModels/Incident";
59
+ import IncidentEpisode from "../../Models/DatabaseModels/IncidentEpisode";
60
+ import IncidentEpisodeMember from "../../Models/DatabaseModels/IncidentEpisodeMember";
61
+ import IncidentEpisodePublicNote from "../../Models/DatabaseModels/IncidentEpisodePublicNote";
62
+ import IncidentEpisodeStateTimeline from "../../Models/DatabaseModels/IncidentEpisodeStateTimeline";
55
63
  import IncidentPublicNote from "../../Models/DatabaseModels/IncidentPublicNote";
56
64
  import IncidentState from "../../Models/DatabaseModels/IncidentState";
57
65
  import IncidentStateTimeline from "../../Models/DatabaseModels/IncidentStateTimeline";
66
+ import Monitor from "../../Models/DatabaseModels/Monitor";
58
67
  import MonitorGroupResource from "../../Models/DatabaseModels/MonitorGroupResource";
59
68
  import MonitorStatus from "../../Models/DatabaseModels/MonitorStatus";
60
69
  import MonitorStatusTimeline from "../../Models/DatabaseModels/MonitorStatusTimeline";
@@ -409,6 +418,20 @@ export default class StatusPageAPI extends BaseAPI<
409
418
  },
410
419
  );
411
420
 
421
+ this.router.get(
422
+ `${new this.entityType()
423
+ .getCrudApiPath()
424
+ ?.toString()}/incident-episode-public-note/attachment/:statusPageId/:episodeId/:noteId/:fileId`,
425
+ UserMiddleware.getUserMiddleware,
426
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
427
+ try {
428
+ await this.getIncidentEpisodePublicNoteAttachment(req, res);
429
+ } catch (err) {
430
+ next(err);
431
+ }
432
+ },
433
+ );
434
+
412
435
  this.router.get(
413
436
  `${new this.entityType()
414
437
  .getCrudApiPath()
@@ -1579,6 +1602,324 @@ export default class StatusPageAPI extends BaseAPI<
1579
1602
  });
1580
1603
  }
1581
1604
 
1605
+ // Fetch active episodes (similar to incidents)
1606
+ let activeEpisodes: Array<IncidentEpisode> = [];
1607
+ let activeEpisodesJson: JSONArray = [];
1608
+ let episodePublicNotes: Array<IncidentEpisodePublicNote> = [];
1609
+ let episodeStateTimelines: Array<IncidentEpisodeStateTimeline> = [];
1610
+
1611
+ if (
1612
+ statusPage.showEpisodesOnStatusPage &&
1613
+ monitorsOnStatusPage.length > 0
1614
+ ) {
1615
+ // First, get incidents that have monitors on status page
1616
+ const incidentsForEpisodes: Array<Incident> =
1617
+ await IncidentService.findBy({
1618
+ query: {
1619
+ monitors: monitorsOnStatusPage as any,
1620
+ projectId: statusPage.projectId!,
1621
+ },
1622
+ select: {
1623
+ _id: true,
1624
+ },
1625
+ skip: 0,
1626
+ limit: LIMIT_PER_PROJECT,
1627
+ props: {
1628
+ isRoot: true,
1629
+ },
1630
+ });
1631
+
1632
+ const incidentIdsForEpisodes: Array<ObjectID> =
1633
+ incidentsForEpisodes.map((incident: Incident) => {
1634
+ return incident.id!;
1635
+ });
1636
+
1637
+ // Get episode members for these incidents
1638
+ let episodeMembers: Array<IncidentEpisodeMember> = [];
1639
+ if (incidentIdsForEpisodes.length > 0) {
1640
+ episodeMembers = await IncidentEpisodeMemberService.findBy({
1641
+ query: {
1642
+ incidentId: QueryHelper.any(incidentIdsForEpisodes),
1643
+ projectId: statusPage.projectId!,
1644
+ },
1645
+ select: {
1646
+ incidentEpisodeId: true,
1647
+ incidentId: true,
1648
+ },
1649
+ skip: 0,
1650
+ limit: LIMIT_PER_PROJECT,
1651
+ props: {
1652
+ isRoot: true,
1653
+ },
1654
+ });
1655
+ }
1656
+
1657
+ // Get unique episode IDs
1658
+ const episodeIdsFromMembers: Set<string> = new Set();
1659
+ for (const member of episodeMembers) {
1660
+ if (member.incidentEpisodeId) {
1661
+ episodeIdsFromMembers.add(member.incidentEpisodeId.toString());
1662
+ }
1663
+ }
1664
+
1665
+ // Fetch active (unresolved) episodes
1666
+ if (episodeIdsFromMembers.size > 0) {
1667
+ const unresolvedIncidentStates: Array<IncidentState> =
1668
+ await IncidentStateService.getUnresolvedIncidentStates(
1669
+ statusPage.projectId!,
1670
+ { isRoot: true },
1671
+ );
1672
+
1673
+ const unresolvedIncidentStateIds: Array<ObjectID> =
1674
+ unresolvedIncidentStates.map((state: IncidentState) => {
1675
+ return state.id!;
1676
+ });
1677
+
1678
+ let selectEpisodes: Select<IncidentEpisode> = {
1679
+ createdAt: true,
1680
+ declaredAt: true,
1681
+ updatedAt: true,
1682
+ title: true,
1683
+ description: true,
1684
+ _id: true,
1685
+ episodeNumber: true,
1686
+ incidentSeverity: {
1687
+ name: true,
1688
+ color: true,
1689
+ },
1690
+ currentIncidentState: {
1691
+ name: true,
1692
+ color: true,
1693
+ _id: true,
1694
+ order: true,
1695
+ isCreatedState: true,
1696
+ isAcknowledgedState: true,
1697
+ isResolvedState: true,
1698
+ },
1699
+ incidentCount: true,
1700
+ };
1701
+
1702
+ if (statusPage.showEpisodeLabelsOnStatusPage) {
1703
+ selectEpisodes = {
1704
+ ...selectEpisodes,
1705
+ labels: {
1706
+ name: true,
1707
+ color: true,
1708
+ },
1709
+ };
1710
+ }
1711
+
1712
+ activeEpisodes = await IncidentEpisodeService.findBy({
1713
+ query: {
1714
+ _id: QueryHelper.any(
1715
+ Array.from(episodeIdsFromMembers).map((id: string) => {
1716
+ return new ObjectID(id);
1717
+ }),
1718
+ ),
1719
+ currentIncidentStateId: QueryHelper.any(
1720
+ unresolvedIncidentStateIds,
1721
+ ),
1722
+ isVisibleOnStatusPage: true,
1723
+ projectId: statusPage.projectId!,
1724
+ },
1725
+ select: selectEpisodes,
1726
+ sort: {
1727
+ declaredAt: SortOrder.Descending,
1728
+ createdAt: SortOrder.Descending,
1729
+ },
1730
+ skip: 0,
1731
+ limit: LIMIT_PER_PROJECT,
1732
+ props: {
1733
+ isRoot: true,
1734
+ },
1735
+ });
1736
+
1737
+ // Build episode monitors map
1738
+ if (activeEpisodes.length > 0) {
1739
+ // Collect all incident IDs from episode members for active episodes
1740
+ const activeEpisodeIds: Set<string> = new Set(
1741
+ activeEpisodes.map((e: IncidentEpisode) => {
1742
+ return e.id!.toString();
1743
+ }),
1744
+ );
1745
+
1746
+ const memberIncidentIds: Array<ObjectID> = [];
1747
+ for (const member of episodeMembers) {
1748
+ if (
1749
+ member.incidentEpisodeId &&
1750
+ activeEpisodeIds.has(member.incidentEpisodeId.toString()) &&
1751
+ member.incidentId &&
1752
+ !memberIncidentIds.some((id: ObjectID) => {
1753
+ return id.toString() === member.incidentId!.toString();
1754
+ })
1755
+ ) {
1756
+ memberIncidentIds.push(member.incidentId);
1757
+ }
1758
+ }
1759
+
1760
+ // Fetch incidents with monitors
1761
+ let memberIncidents: Array<Incident> = [];
1762
+ if (memberIncidentIds.length > 0) {
1763
+ memberIncidents = await IncidentService.findBy({
1764
+ query: {
1765
+ _id: QueryHelper.any(memberIncidentIds),
1766
+ projectId: statusPage.projectId!,
1767
+ },
1768
+ select: {
1769
+ _id: true,
1770
+ monitors: {
1771
+ _id: true,
1772
+ },
1773
+ },
1774
+ skip: 0,
1775
+ limit: LIMIT_PER_PROJECT,
1776
+ props: {
1777
+ isRoot: true,
1778
+ },
1779
+ });
1780
+ }
1781
+
1782
+ // Build incident -> monitors map
1783
+ const incidentMonitorsMap: Map<
1784
+ string,
1785
+ Array<ObjectID>
1786
+ > = new Map();
1787
+ for (const incident of memberIncidents) {
1788
+ const incidentIdStr: string = incident.id!.toString();
1789
+ const monitorIds: Array<ObjectID> = (incident.monitors || [])
1790
+ .map((m: Monitor) => {
1791
+ return new ObjectID(
1792
+ m._id?.toString() || m.id?.toString() || "",
1793
+ );
1794
+ })
1795
+ .filter((id: ObjectID) => {
1796
+ return id.toString() !== "";
1797
+ });
1798
+ incidentMonitorsMap.set(incidentIdStr, monitorIds);
1799
+ }
1800
+
1801
+ // Build episode -> monitors map
1802
+ const episodeMonitorsMap: Map<
1803
+ string,
1804
+ Array<ObjectID>
1805
+ > = new Map();
1806
+ for (const member of episodeMembers) {
1807
+ if (
1808
+ member.incidentEpisodeId &&
1809
+ member.incidentId &&
1810
+ activeEpisodeIds.has(member.incidentEpisodeId.toString())
1811
+ ) {
1812
+ const episodeIdStr: string =
1813
+ member.incidentEpisodeId.toString();
1814
+ const incidentIdStr: string = member.incidentId.toString();
1815
+
1816
+ if (!episodeMonitorsMap.has(episodeIdStr)) {
1817
+ episodeMonitorsMap.set(episodeIdStr, []);
1818
+ }
1819
+
1820
+ const episodeMonitors: Array<ObjectID> =
1821
+ episodeMonitorsMap.get(episodeIdStr)!;
1822
+ const incidentMonitors: Array<ObjectID> =
1823
+ incidentMonitorsMap.get(incidentIdStr) || [];
1824
+
1825
+ for (const monitorId of incidentMonitors) {
1826
+ if (
1827
+ !episodeMonitors.some((m: ObjectID) => {
1828
+ return m.toString() === monitorId.toString();
1829
+ })
1830
+ ) {
1831
+ episodeMonitors.push(monitorId);
1832
+ }
1833
+ }
1834
+ }
1835
+ }
1836
+
1837
+ // Serialize episodes and add monitors
1838
+ activeEpisodesJson = BaseModel.toJSONArray(
1839
+ activeEpisodes,
1840
+ IncidentEpisode,
1841
+ );
1842
+ for (const episodeJson of activeEpisodesJson) {
1843
+ const episodeObj: JSONObject = episodeJson as JSONObject;
1844
+ const episodeId: string | undefined =
1845
+ episodeObj["_id"]?.toString();
1846
+ if (episodeId) {
1847
+ const monitorIds: Array<ObjectID> =
1848
+ episodeMonitorsMap.get(episodeId) || [];
1849
+ episodeObj["monitors"] = monitorIds.map((id: ObjectID) => {
1850
+ return { _id: id.toString() };
1851
+ });
1852
+ }
1853
+ }
1854
+
1855
+ // Get episode public notes
1856
+ const episodesOnStatusPage: Array<ObjectID> =
1857
+ activeEpisodes.map((episode: IncidentEpisode) => {
1858
+ return episode.id!;
1859
+ });
1860
+
1861
+ if (episodesOnStatusPage.length > 0) {
1862
+ episodePublicNotes =
1863
+ await IncidentEpisodePublicNoteService.findBy({
1864
+ query: {
1865
+ incidentEpisodeId:
1866
+ QueryHelper.any(episodesOnStatusPage),
1867
+ projectId: statusPage.projectId!,
1868
+ },
1869
+ select: {
1870
+ postedAt: true,
1871
+ note: true,
1872
+ incidentEpisodeId: true,
1873
+ attachments: {
1874
+ _id: true,
1875
+ name: true,
1876
+ },
1877
+ },
1878
+ sort: {
1879
+ postedAt: SortOrder.Descending,
1880
+ },
1881
+ skip: 0,
1882
+ limit: LIMIT_PER_PROJECT,
1883
+ props: {
1884
+ isRoot: true,
1885
+ },
1886
+ });
1887
+
1888
+ // Get episode state timelines
1889
+ episodeStateTimelines =
1890
+ await IncidentEpisodeStateTimelineService.findBy({
1891
+ query: {
1892
+ incidentEpisodeId:
1893
+ QueryHelper.any(episodesOnStatusPage),
1894
+ projectId: statusPage.projectId!,
1895
+ },
1896
+ select: {
1897
+ _id: true,
1898
+ createdAt: true,
1899
+ startsAt: true,
1900
+ incidentEpisodeId: true,
1901
+ incidentState: {
1902
+ name: true,
1903
+ color: true,
1904
+ isCreatedState: true,
1905
+ isAcknowledgedState: true,
1906
+ isResolvedState: true,
1907
+ },
1908
+ },
1909
+ sort: {
1910
+ startsAt: SortOrder.Descending,
1911
+ },
1912
+ skip: 0,
1913
+ limit: LIMIT_PER_PROJECT,
1914
+ props: {
1915
+ isRoot: true,
1916
+ },
1917
+ });
1918
+ }
1919
+ }
1920
+ }
1921
+ }
1922
+
1582
1923
  // check if status page has active announcement.
1583
1924
 
1584
1925
  const today: Date = OneUptimeDate.getCurrentDate();
@@ -1830,6 +2171,16 @@ export default class StatusPageAPI extends BaseAPI<
1830
2171
 
1831
2172
  activeIncidents: BaseModel.toJSONArray(activeIncidents, Incident),
1832
2173
 
2174
+ activeEpisodes: activeEpisodesJson,
2175
+ episodePublicNotes: BaseModel.toJSONArray(
2176
+ episodePublicNotes,
2177
+ IncidentEpisodePublicNote,
2178
+ ),
2179
+ episodeStateTimelines: BaseModel.toJSONArray(
2180
+ episodeStateTimelines,
2181
+ IncidentEpisodeStateTimeline,
2182
+ ),
2183
+
1833
2184
  monitorStatusTimelines: BaseModel.toJSONArray(
1834
2185
  monitorStatusTimelines,
1835
2186
  MonitorStatusTimeline,
@@ -2097,6 +2448,59 @@ export default class StatusPageAPI extends BaseAPI<
2097
2448
  }
2098
2449
  },
2099
2450
  );
2451
+
2452
+ // Episodes endpoints
2453
+ this.router.post(
2454
+ `${new this.entityType()
2455
+ .getCrudApiPath()
2456
+ ?.toString()}/episodes/:statusPageIdOrDomain`,
2457
+ UserMiddleware.getUserMiddleware,
2458
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
2459
+ try {
2460
+ const objectId: ObjectID = await resolveStatusPageIdOrThrow(
2461
+ req.params["statusPageIdOrDomain"] as string,
2462
+ );
2463
+
2464
+ const response: JSONObject = await this.getEpisodes(
2465
+ objectId,
2466
+ null,
2467
+ req,
2468
+ );
2469
+
2470
+ return Response.sendJsonObjectResponse(req, res, response);
2471
+ } catch (err) {
2472
+ next(err);
2473
+ }
2474
+ },
2475
+ );
2476
+
2477
+ this.router.post(
2478
+ `${new this.entityType()
2479
+ .getCrudApiPath()
2480
+ ?.toString()}/episodes/:statusPageIdOrDomain/:episodeId`,
2481
+ UserMiddleware.getUserMiddleware,
2482
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
2483
+ try {
2484
+ const objectId: ObjectID = await resolveStatusPageIdOrThrow(
2485
+ req.params["statusPageIdOrDomain"] as string,
2486
+ );
2487
+
2488
+ const episodeId: ObjectID = new ObjectID(
2489
+ req.params["episodeId"] as string,
2490
+ );
2491
+
2492
+ const response: JSONObject = await this.getEpisodes(
2493
+ objectId,
2494
+ episodeId,
2495
+ req,
2496
+ );
2497
+
2498
+ return Response.sendJsonObjectResponse(req, res, response);
2499
+ } catch (err) {
2500
+ next(err);
2501
+ }
2502
+ },
2503
+ );
2100
2504
  }
2101
2505
 
2102
2506
  @CaptureSpan()
@@ -3533,42 +3937,26 @@ export default class StatusPageAPI extends BaseAPI<
3533
3937
  }
3534
3938
 
3535
3939
  @CaptureSpan()
3536
- public async getStatusPageResourcesAndTimelines(data: {
3537
- statusPageId: ObjectID;
3538
- startDateForMonitorTimeline: Date;
3539
- endDateForMonitorTimeline: Date;
3540
- }): Promise<{
3541
- statusPageResources: StatusPageResource[];
3542
- monitorStatuses: MonitorStatus[];
3543
- monitorStatusTimelines: MonitorStatusTimeline[];
3544
- monitorGroupCurrentStatuses: Dictionary<ObjectID>;
3545
- statusPageGroups: StatusPageGroup[];
3546
- statusPage: StatusPage;
3547
- monitorsOnStatusPage: ObjectID[];
3548
- monitorsInGroup: Dictionary<ObjectID[]>;
3549
- }> {
3550
- const objectId: ObjectID = data.statusPageId;
3940
+ public async getEpisodes(
3941
+ statusPageId: ObjectID,
3942
+ episodeId: ObjectID | null,
3943
+ req: ExpressRequest,
3944
+ ): Promise<JSONObject> {
3945
+ await this.checkHasReadAccess({
3946
+ statusPageId: statusPageId,
3947
+ req: req,
3948
+ });
3551
3949
 
3552
3950
  const statusPage: StatusPage | null = await StatusPageService.findOneBy({
3553
3951
  query: {
3554
- _id: objectId.toString(),
3952
+ _id: statusPageId.toString(),
3555
3953
  },
3556
3954
  select: {
3557
3955
  _id: true,
3558
3956
  projectId: true,
3559
- isPublicStatusPage: true,
3560
- overviewPageDescription: true,
3561
- showIncidentLabelsOnStatusPage: true,
3562
- showScheduledEventLabelsOnStatusPage: true,
3563
- downtimeMonitorStatuses: {
3564
- _id: true,
3565
- },
3566
- defaultBarColor: true,
3567
- showOverallUptimePercentOnStatusPage: true,
3568
- overallUptimePercentPrecision: true,
3569
- showAnnouncementsOnStatusPage: true,
3570
- showIncidentsOnStatusPage: true,
3571
- showScheduledMaintenanceEventsOnStatusPage: true,
3957
+ showEpisodeHistoryInDays: true,
3958
+ showEpisodesOnStatusPage: true,
3959
+ showEpisodeLabelsOnStatusPage: true,
3572
3960
  },
3573
3961
  props: {
3574
3962
  isRoot: true,
@@ -3579,21 +3967,49 @@ export default class StatusPageAPI extends BaseAPI<
3579
3967
  throw new BadDataException("Status Page not found");
3580
3968
  }
3581
3969
 
3582
- //get monitor statuses
3970
+ if (!statusPage.showEpisodesOnStatusPage) {
3971
+ throw new BadDataException(
3972
+ "Episodes are not enabled on this status page.",
3973
+ );
3974
+ }
3583
3975
 
3584
- const monitorStatuses: Array<MonitorStatus> =
3585
- await MonitorStatusService.findBy({
3586
- query: {
3587
- projectId: statusPage.projectId!,
3588
- },
3976
+ // get monitors on status page.
3977
+ const statusPageResources: Array<StatusPageResource> =
3978
+ await StatusPageService.getStatusPageResources({
3979
+ statusPageId: statusPageId,
3980
+ });
3981
+
3982
+ const { monitorsOnStatusPage, monitorsInGroup } =
3983
+ await StatusPageService.getMonitorIdsOnStatusPage({
3984
+ statusPageId: statusPageId,
3985
+ });
3986
+
3987
+ const today: Date = OneUptimeDate.getCurrentDate();
3988
+
3989
+ const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
3990
+ statusPage.showEpisodeHistoryInDays || 14,
3991
+ );
3992
+
3993
+ /*
3994
+ * Get incidents that have monitors on this status page
3995
+ * Note: We don't filter by incident.isVisibleOnStatusPage here because
3996
+ * episode visibility is independent of incident visibility.
3997
+ * An episode should show if episode.isVisibleOnStatusPage is true,
3998
+ * regardless of whether its member incidents are visible.
3999
+ */
4000
+ const incidentQuery: Query<Incident> = {
4001
+ monitors: monitorsOnStatusPage as any,
4002
+ projectId: statusPage.projectId!,
4003
+ createdAt: QueryHelper.inBetween(historyDays, today),
4004
+ };
4005
+
4006
+ let incidents: Array<Incident> = [];
4007
+
4008
+ if (monitorsOnStatusPage.length > 0) {
4009
+ incidents = await IncidentService.findBy({
4010
+ query: incidentQuery,
3589
4011
  select: {
3590
- name: true,
3591
- color: true,
3592
- priority: true,
3593
- isOperationalState: true,
3594
- },
3595
- sort: {
3596
- priority: SortOrder.Ascending,
4012
+ _id: true,
3597
4013
  },
3598
4014
  skip: 0,
3599
4015
  limit: LIMIT_PER_PROJECT,
@@ -3601,10 +4017,466 @@ export default class StatusPageAPI extends BaseAPI<
3601
4017
  isRoot: true,
3602
4018
  },
3603
4019
  });
4020
+ }
3604
4021
 
3605
- // get resource groups.
4022
+ const incidentIds: Array<ObjectID> = incidents.map((incident: Incident) => {
4023
+ return incident.id!;
4024
+ });
3606
4025
 
3607
- const groups: Array<StatusPageGroup> = await StatusPageGroupService.findBy({
4026
+ // Get episode members that link to these incidents
4027
+ let episodeMembers: Array<IncidentEpisodeMember> = [];
4028
+
4029
+ if (incidentIds.length > 0) {
4030
+ episodeMembers = await IncidentEpisodeMemberService.findBy({
4031
+ query: {
4032
+ incidentId: QueryHelper.any(incidentIds),
4033
+ projectId: statusPage.projectId!,
4034
+ },
4035
+ select: {
4036
+ incidentEpisodeId: true,
4037
+ incidentId: true,
4038
+ },
4039
+ skip: 0,
4040
+ limit: LIMIT_PER_PROJECT,
4041
+ props: {
4042
+ isRoot: true,
4043
+ },
4044
+ });
4045
+ }
4046
+
4047
+ // Get unique episode IDs
4048
+ const episodeIdsFromMembers: Set<string> = new Set();
4049
+ for (const member of episodeMembers) {
4050
+ if (member.incidentEpisodeId) {
4051
+ episodeIdsFromMembers.add(member.incidentEpisodeId.toString());
4052
+ }
4053
+ }
4054
+
4055
+ let episodeQuery: Query<IncidentEpisode> = {
4056
+ _id: QueryHelper.any(
4057
+ Array.from(episodeIdsFromMembers).map((id: string) => {
4058
+ return new ObjectID(id);
4059
+ }),
4060
+ ),
4061
+ projectId: statusPage.projectId!,
4062
+ isVisibleOnStatusPage: true,
4063
+ };
4064
+
4065
+ if (episodeId) {
4066
+ episodeQuery = {
4067
+ _id: episodeId.toString(),
4068
+ projectId: statusPage.projectId!,
4069
+ isVisibleOnStatusPage: true,
4070
+ };
4071
+
4072
+ // When viewing a specific episode, also fetch its members directly
4073
+ const episodeMembersForSpecificEpisode: Array<IncidentEpisodeMember> =
4074
+ await IncidentEpisodeMemberService.findBy({
4075
+ query: {
4076
+ incidentEpisodeId: episodeId,
4077
+ projectId: statusPage.projectId!,
4078
+ },
4079
+ select: {
4080
+ incidentEpisodeId: true,
4081
+ incidentId: true,
4082
+ },
4083
+ skip: 0,
4084
+ limit: LIMIT_PER_PROJECT,
4085
+ props: {
4086
+ isRoot: true,
4087
+ },
4088
+ });
4089
+
4090
+ // Merge with existing episode members
4091
+ for (const member of episodeMembersForSpecificEpisode) {
4092
+ if (
4093
+ !episodeMembers.some((m: IncidentEpisodeMember) => {
4094
+ return (
4095
+ m.incidentEpisodeId?.toString() ===
4096
+ member.incidentEpisodeId?.toString() &&
4097
+ m.incidentId?.toString() === member.incidentId?.toString()
4098
+ );
4099
+ })
4100
+ ) {
4101
+ episodeMembers.push(member);
4102
+ }
4103
+ }
4104
+ }
4105
+
4106
+ // Get episodes
4107
+ let episodes: Array<IncidentEpisode> = [];
4108
+
4109
+ let selectEpisodes: Select<IncidentEpisode> = {
4110
+ createdAt: true,
4111
+ declaredAt: true,
4112
+ updatedAt: true,
4113
+ title: true,
4114
+ description: true,
4115
+ _id: true,
4116
+ episodeNumber: true,
4117
+ incidentSeverity: {
4118
+ name: true,
4119
+ color: true,
4120
+ },
4121
+ currentIncidentState: {
4122
+ name: true,
4123
+ color: true,
4124
+ _id: true,
4125
+ order: true,
4126
+ isCreatedState: true,
4127
+ isAcknowledgedState: true,
4128
+ isResolvedState: true,
4129
+ },
4130
+ incidentCount: true,
4131
+ };
4132
+
4133
+ if (statusPage.showEpisodeLabelsOnStatusPage) {
4134
+ selectEpisodes = {
4135
+ ...selectEpisodes,
4136
+ labels: {
4137
+ name: true,
4138
+ color: true,
4139
+ },
4140
+ };
4141
+ }
4142
+
4143
+ if (episodeIdsFromMembers.size > 0 || episodeId) {
4144
+ episodes = await IncidentEpisodeService.findBy({
4145
+ query: episodeQuery,
4146
+ select: selectEpisodes,
4147
+ sort: {
4148
+ declaredAt: SortOrder.Descending,
4149
+ createdAt: SortOrder.Descending,
4150
+ },
4151
+ skip: 0,
4152
+ limit: LIMIT_PER_PROJECT,
4153
+ props: {
4154
+ isRoot: true,
4155
+ },
4156
+ });
4157
+ }
4158
+
4159
+ // If no specific episode, also fetch active (unresolved) episodes
4160
+ if (!episodeId && episodeIdsFromMembers.size > 0) {
4161
+ const unresolvedIncidentStates: Array<IncidentState> =
4162
+ await IncidentStateService.getUnresolvedIncidentStates(
4163
+ statusPage.projectId!,
4164
+ {
4165
+ isRoot: true,
4166
+ },
4167
+ );
4168
+
4169
+ const unresolvedIncidentStateIds: Array<ObjectID> =
4170
+ unresolvedIncidentStates.map((state: IncidentState) => {
4171
+ return state.id!;
4172
+ });
4173
+
4174
+ const activeEpisodes: Array<IncidentEpisode> =
4175
+ await IncidentEpisodeService.findBy({
4176
+ query: {
4177
+ _id: QueryHelper.any(
4178
+ Array.from(episodeIdsFromMembers).map((id: string) => {
4179
+ return new ObjectID(id);
4180
+ }),
4181
+ ),
4182
+ isVisibleOnStatusPage: true,
4183
+ currentIncidentStateId: QueryHelper.any(unresolvedIncidentStateIds),
4184
+ projectId: statusPage.projectId!,
4185
+ },
4186
+ select: selectEpisodes,
4187
+ sort: {
4188
+ declaredAt: SortOrder.Descending,
4189
+ createdAt: SortOrder.Descending,
4190
+ },
4191
+ skip: 0,
4192
+ limit: LIMIT_PER_PROJECT,
4193
+ props: {
4194
+ isRoot: true,
4195
+ },
4196
+ });
4197
+
4198
+ episodes = [...activeEpisodes, ...episodes];
4199
+ episodes = ArrayUtil.distinctByFieldName(episodes, "_id");
4200
+ }
4201
+
4202
+ const episodesOnStatusPage: Array<ObjectID> = episodes.map(
4203
+ (episode: IncidentEpisode) => {
4204
+ return episode.id!;
4205
+ },
4206
+ );
4207
+
4208
+ /*
4209
+ * Build a map of episode ID -> monitor IDs from episode members
4210
+ * Collect all unique incident IDs from episode members
4211
+ */
4212
+ const memberIncidentIds: Array<ObjectID> = [];
4213
+ for (const member of episodeMembers) {
4214
+ if (
4215
+ member.incidentId &&
4216
+ !memberIncidentIds.some((id: ObjectID) => {
4217
+ return id.toString() === member.incidentId!.toString();
4218
+ })
4219
+ ) {
4220
+ memberIncidentIds.push(member.incidentId);
4221
+ }
4222
+ }
4223
+
4224
+ // Fetch incidents with their monitors
4225
+ let memberIncidents: Array<Incident> = [];
4226
+ if (memberIncidentIds.length > 0) {
4227
+ memberIncidents = await IncidentService.findBy({
4228
+ query: {
4229
+ _id: QueryHelper.any(memberIncidentIds),
4230
+ projectId: statusPage.projectId!,
4231
+ },
4232
+ select: {
4233
+ _id: true,
4234
+ monitors: {
4235
+ _id: true,
4236
+ },
4237
+ },
4238
+ skip: 0,
4239
+ limit: LIMIT_PER_PROJECT,
4240
+ props: {
4241
+ isRoot: true,
4242
+ },
4243
+ });
4244
+ }
4245
+
4246
+ // Build a map of incident ID -> monitors
4247
+ const incidentMonitorsMap: Map<string, Array<ObjectID>> = new Map();
4248
+ for (const incident of memberIncidents) {
4249
+ const incidentIdStr: string = incident.id!.toString();
4250
+ const monitorIds: Array<ObjectID> = (incident.monitors || [])
4251
+ .map((m: Monitor) => {
4252
+ return new ObjectID(m._id?.toString() || m.id?.toString() || "");
4253
+ })
4254
+ .filter((id: ObjectID) => {
4255
+ return id.toString() !== "";
4256
+ });
4257
+ incidentMonitorsMap.set(incidentIdStr, monitorIds);
4258
+ }
4259
+
4260
+ // Build episode monitors map from members and incident monitors
4261
+ const episodeMonitorsMap: Map<string, Array<ObjectID>> = new Map();
4262
+ for (const member of episodeMembers) {
4263
+ if (member.incidentEpisodeId && member.incidentId) {
4264
+ const episodeIdStr: string = member.incidentEpisodeId.toString();
4265
+ const incidentIdStr: string = member.incidentId.toString();
4266
+
4267
+ if (!episodeMonitorsMap.has(episodeIdStr)) {
4268
+ episodeMonitorsMap.set(episodeIdStr, []);
4269
+ }
4270
+
4271
+ const episodeMonitors: Array<ObjectID> =
4272
+ episodeMonitorsMap.get(episodeIdStr)!;
4273
+ const incidentMonitors: Array<ObjectID> =
4274
+ incidentMonitorsMap.get(incidentIdStr) || [];
4275
+
4276
+ for (const monitorId of incidentMonitors) {
4277
+ if (
4278
+ !episodeMonitors.some((m: ObjectID) => {
4279
+ return m.toString() === monitorId.toString();
4280
+ })
4281
+ ) {
4282
+ episodeMonitors.push(monitorId);
4283
+ }
4284
+ }
4285
+ }
4286
+ }
4287
+
4288
+ // Get public notes for episodes
4289
+ let episodePublicNotes: Array<IncidentEpisodePublicNote> = [];
4290
+
4291
+ if (episodesOnStatusPage.length > 0) {
4292
+ episodePublicNotes = await IncidentEpisodePublicNoteService.findBy({
4293
+ query: {
4294
+ incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
4295
+ projectId: statusPage.projectId!,
4296
+ },
4297
+ select: {
4298
+ postedAt: true,
4299
+ note: true,
4300
+ incidentEpisodeId: true,
4301
+ attachments: {
4302
+ _id: true,
4303
+ name: true,
4304
+ },
4305
+ },
4306
+ sort: {
4307
+ postedAt: SortOrder.Descending,
4308
+ },
4309
+ skip: 0,
4310
+ limit: LIMIT_PER_PROJECT,
4311
+ props: {
4312
+ isRoot: true,
4313
+ },
4314
+ });
4315
+ }
4316
+
4317
+ // Get state timelines for episodes
4318
+ let episodeStateTimelines: Array<IncidentEpisodeStateTimeline> = [];
4319
+
4320
+ if (episodesOnStatusPage.length > 0) {
4321
+ episodeStateTimelines = await IncidentEpisodeStateTimelineService.findBy({
4322
+ query: {
4323
+ incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
4324
+ projectId: statusPage.projectId!,
4325
+ },
4326
+ select: {
4327
+ _id: true,
4328
+ createdAt: true,
4329
+ startsAt: true,
4330
+ incidentEpisodeId: true,
4331
+ incidentState: {
4332
+ name: true,
4333
+ color: true,
4334
+ isCreatedState: true,
4335
+ isAcknowledgedState: true,
4336
+ isResolvedState: true,
4337
+ },
4338
+ },
4339
+ sort: {
4340
+ startsAt: SortOrder.Descending,
4341
+ },
4342
+ skip: 0,
4343
+ limit: LIMIT_PER_PROJECT,
4344
+ props: {
4345
+ isRoot: true,
4346
+ },
4347
+ });
4348
+ }
4349
+
4350
+ // Get all incident states for this project
4351
+ const incidentStates: Array<IncidentState> =
4352
+ await IncidentStateService.findBy({
4353
+ query: {
4354
+ projectId: statusPage.projectId!,
4355
+ },
4356
+ select: {
4357
+ isResolvedState: true,
4358
+ order: true,
4359
+ },
4360
+ limit: LIMIT_PER_PROJECT,
4361
+ skip: 0,
4362
+ props: {
4363
+ isRoot: true,
4364
+ },
4365
+ });
4366
+
4367
+ // Serialize episodes and add monitors to each
4368
+ const episodesJson: JSONArray = BaseModel.toJSONArray(
4369
+ episodes,
4370
+ IncidentEpisode,
4371
+ );
4372
+ for (const episodeJson of episodesJson) {
4373
+ const episodeObj: JSONObject = episodeJson as JSONObject;
4374
+ const episodeId: string | undefined = episodeObj["_id"]?.toString();
4375
+ if (episodeId) {
4376
+ const monitorIds: Array<ObjectID> =
4377
+ episodeMonitorsMap.get(episodeId) || [];
4378
+ episodeObj["monitors"] = monitorIds.map((id: ObjectID) => {
4379
+ return { _id: id.toString() };
4380
+ });
4381
+ }
4382
+ }
4383
+
4384
+ const response: JSONObject = {
4385
+ episodePublicNotes: BaseModel.toJSONArray(
4386
+ episodePublicNotes,
4387
+ IncidentEpisodePublicNote,
4388
+ ),
4389
+ incidentStates: BaseModel.toJSONArray(incidentStates, IncidentState),
4390
+ episodes: episodesJson,
4391
+ statusPageResources: BaseModel.toJSONArray(
4392
+ statusPageResources,
4393
+ StatusPageResource,
4394
+ ),
4395
+ episodeStateTimelines: BaseModel.toJSONArray(
4396
+ episodeStateTimelines,
4397
+ IncidentEpisodeStateTimeline,
4398
+ ),
4399
+ monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
4400
+ };
4401
+
4402
+ return response;
4403
+ }
4404
+
4405
+ @CaptureSpan()
4406
+ public async getStatusPageResourcesAndTimelines(data: {
4407
+ statusPageId: ObjectID;
4408
+ startDateForMonitorTimeline: Date;
4409
+ endDateForMonitorTimeline: Date;
4410
+ }): Promise<{
4411
+ statusPageResources: StatusPageResource[];
4412
+ monitorStatuses: MonitorStatus[];
4413
+ monitorStatusTimelines: MonitorStatusTimeline[];
4414
+ monitorGroupCurrentStatuses: Dictionary<ObjectID>;
4415
+ statusPageGroups: StatusPageGroup[];
4416
+ statusPage: StatusPage;
4417
+ monitorsOnStatusPage: ObjectID[];
4418
+ monitorsInGroup: Dictionary<ObjectID[]>;
4419
+ }> {
4420
+ const objectId: ObjectID = data.statusPageId;
4421
+
4422
+ const statusPage: StatusPage | null = await StatusPageService.findOneBy({
4423
+ query: {
4424
+ _id: objectId.toString(),
4425
+ },
4426
+ select: {
4427
+ _id: true,
4428
+ projectId: true,
4429
+ isPublicStatusPage: true,
4430
+ overviewPageDescription: true,
4431
+ showIncidentLabelsOnStatusPage: true,
4432
+ showScheduledEventLabelsOnStatusPage: true,
4433
+ showEpisodeLabelsOnStatusPage: true,
4434
+ downtimeMonitorStatuses: {
4435
+ _id: true,
4436
+ },
4437
+ defaultBarColor: true,
4438
+ showOverallUptimePercentOnStatusPage: true,
4439
+ overallUptimePercentPrecision: true,
4440
+ showAnnouncementsOnStatusPage: true,
4441
+ showIncidentsOnStatusPage: true,
4442
+ showEpisodesOnStatusPage: true,
4443
+ showScheduledMaintenanceEventsOnStatusPage: true,
4444
+ },
4445
+ props: {
4446
+ isRoot: true,
4447
+ },
4448
+ });
4449
+
4450
+ if (!statusPage) {
4451
+ throw new BadDataException("Status Page not found");
4452
+ }
4453
+
4454
+ //get monitor statuses
4455
+
4456
+ const monitorStatuses: Array<MonitorStatus> =
4457
+ await MonitorStatusService.findBy({
4458
+ query: {
4459
+ projectId: statusPage.projectId!,
4460
+ },
4461
+ select: {
4462
+ name: true,
4463
+ color: true,
4464
+ priority: true,
4465
+ isOperationalState: true,
4466
+ },
4467
+ sort: {
4468
+ priority: SortOrder.Ascending,
4469
+ },
4470
+ skip: 0,
4471
+ limit: LIMIT_PER_PROJECT,
4472
+ props: {
4473
+ isRoot: true,
4474
+ },
4475
+ });
4476
+
4477
+ // get resource groups.
4478
+
4479
+ const groups: Array<StatusPageGroup> = await StatusPageGroupService.findBy({
3608
4480
  query: {
3609
4481
  statusPageId: objectId,
3610
4482
  },
@@ -4240,6 +5112,181 @@ export default class StatusPageAPI extends BaseAPI<
4240
5112
  return Response.sendFileResponse(req, res, attachment);
4241
5113
  }
4242
5114
 
5115
+ private async getIncidentEpisodePublicNoteAttachment(
5116
+ req: ExpressRequest,
5117
+ res: ExpressResponse,
5118
+ ): Promise<void> {
5119
+ const statusPageIdParam: string | undefined = req.params["statusPageId"];
5120
+ const episodeIdParam: string | undefined = req.params["episodeId"];
5121
+ const noteIdParam: string | undefined = req.params["noteId"];
5122
+ const fileIdParam: string | undefined = req.params["fileId"];
5123
+
5124
+ if (!statusPageIdParam || !episodeIdParam || !noteIdParam || !fileIdParam) {
5125
+ throw new NotFoundException("Attachment not found");
5126
+ }
5127
+
5128
+ let statusPageId: ObjectID;
5129
+ let episodeId: ObjectID;
5130
+ let noteId: ObjectID;
5131
+ let fileId: ObjectID;
5132
+
5133
+ try {
5134
+ statusPageId = new ObjectID(statusPageIdParam);
5135
+ episodeId = new ObjectID(episodeIdParam);
5136
+ noteId = new ObjectID(noteIdParam);
5137
+ fileId = new ObjectID(fileIdParam);
5138
+ } catch {
5139
+ throw new NotFoundException("Attachment not found");
5140
+ }
5141
+
5142
+ await this.checkHasReadAccess({
5143
+ statusPageId: statusPageId,
5144
+ req: req,
5145
+ });
5146
+
5147
+ const statusPage: StatusPage | null = await StatusPageService.findOneBy({
5148
+ query: {
5149
+ _id: statusPageId.toString(),
5150
+ },
5151
+ select: {
5152
+ _id: true,
5153
+ projectId: true,
5154
+ showEpisodesOnStatusPage: true,
5155
+ },
5156
+ props: {
5157
+ isRoot: true,
5158
+ },
5159
+ });
5160
+
5161
+ if (!statusPage || !statusPage.projectId) {
5162
+ throw new NotFoundException("Attachment not found");
5163
+ }
5164
+
5165
+ if (!statusPage.showEpisodesOnStatusPage) {
5166
+ throw new NotFoundException("Attachment not found");
5167
+ }
5168
+
5169
+ const { monitorsOnStatusPage } =
5170
+ await StatusPageService.getMonitorIdsOnStatusPage({
5171
+ statusPageId: statusPageId,
5172
+ });
5173
+
5174
+ if (!monitorsOnStatusPage || monitorsOnStatusPage.length === 0) {
5175
+ throw new NotFoundException("Attachment not found");
5176
+ }
5177
+
5178
+ // Get episode members (incidents) that are linked to monitors on the status page
5179
+ const episodeMembers: Array<IncidentEpisodeMember> =
5180
+ await IncidentEpisodeMemberService.findBy({
5181
+ query: {
5182
+ incidentEpisodeId: episodeId,
5183
+ projectId: statusPage.projectId!,
5184
+ },
5185
+ select: {
5186
+ incidentId: true,
5187
+ },
5188
+ limit: LIMIT_PER_PROJECT,
5189
+ skip: 0,
5190
+ props: {
5191
+ isRoot: true,
5192
+ },
5193
+ });
5194
+
5195
+ if (episodeMembers.length === 0) {
5196
+ throw new NotFoundException("Attachment not found");
5197
+ }
5198
+
5199
+ const incidentIds: Array<ObjectID> = episodeMembers
5200
+ .map((member: IncidentEpisodeMember) => {
5201
+ return member.incidentId;
5202
+ })
5203
+ .filter((id: ObjectID | undefined): id is ObjectID => {
5204
+ return Boolean(id);
5205
+ });
5206
+
5207
+ // Check if any of the incidents are linked to monitors on the status page
5208
+ const incident: Incident | null = await IncidentService.findOneBy({
5209
+ query: {
5210
+ _id: QueryHelper.any(incidentIds),
5211
+ projectId: statusPage.projectId!,
5212
+ isVisibleOnStatusPage: true,
5213
+ monitors: monitorsOnStatusPage as any,
5214
+ },
5215
+ select: {
5216
+ _id: true,
5217
+ },
5218
+ props: {
5219
+ isRoot: true,
5220
+ },
5221
+ });
5222
+
5223
+ if (!incident) {
5224
+ throw new NotFoundException("Attachment not found");
5225
+ }
5226
+
5227
+ // Verify the episode exists and is visible
5228
+ const episode: IncidentEpisode | null =
5229
+ await IncidentEpisodeService.findOneBy({
5230
+ query: {
5231
+ _id: episodeId.toString(),
5232
+ projectId: statusPage.projectId!,
5233
+ isVisibleOnStatusPage: true,
5234
+ },
5235
+ select: {
5236
+ _id: true,
5237
+ },
5238
+ props: {
5239
+ isRoot: true,
5240
+ },
5241
+ });
5242
+
5243
+ if (!episode) {
5244
+ throw new NotFoundException("Attachment not found");
5245
+ }
5246
+
5247
+ const episodePublicNote: IncidentEpisodePublicNote | null =
5248
+ await IncidentEpisodePublicNoteService.findOneBy({
5249
+ query: {
5250
+ _id: noteId.toString(),
5251
+ incidentEpisodeId: episodeId.toString(),
5252
+ projectId: statusPage.projectId!,
5253
+ },
5254
+ select: {
5255
+ attachments: {
5256
+ _id: true,
5257
+ file: true,
5258
+ fileType: true,
5259
+ name: true,
5260
+ },
5261
+ },
5262
+ props: {
5263
+ isRoot: true,
5264
+ },
5265
+ });
5266
+
5267
+ if (!episodePublicNote) {
5268
+ throw new NotFoundException("Attachment not found");
5269
+ }
5270
+
5271
+ const attachment: File | undefined = episodePublicNote.attachments?.find(
5272
+ (file: File) => {
5273
+ const attachmentId: string | null = file._id
5274
+ ? file._id.toString()
5275
+ : file.id
5276
+ ? file.id.toString()
5277
+ : null;
5278
+ return attachmentId === fileId.toString();
5279
+ },
5280
+ );
5281
+
5282
+ if (!attachment || !attachment.file) {
5283
+ throw new NotFoundException("Attachment not found");
5284
+ }
5285
+
5286
+ Response.setNoCacheHeaders(res);
5287
+ return Response.sendFileResponse(req, res, attachment);
5288
+ }
5289
+
4243
5290
  public async checkHasReadAccess(data: {
4244
5291
  statusPageId: ObjectID;
4245
5292
  req: ExpressRequest;