@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.
- package/Models/DatabaseModels/Alert.ts +1 -0
- package/Models/DatabaseModels/AlertEpisode.ts +1 -0
- package/Models/DatabaseModels/AlertEpisodeStateTimeline.ts +1 -0
- package/Models/DatabaseModels/AlertStateTimeline.ts +1 -0
- package/Models/DatabaseModels/Incident.ts +1 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +156 -0
- package/Models/DatabaseModels/IncidentEpisodeFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +611 -0
- package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +84 -0
- package/Models/DatabaseModels/IncidentGroupingRule.ts +36 -0
- package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
- package/Models/DatabaseModels/Project.ts +2 -1
- package/Models/DatabaseModels/ProjectCallSMSConfig.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
- package/Models/DatabaseModels/StatusPage.ts +120 -0
- package/Server/API/IncidentEpisodePublicNoteAPI.ts +98 -0
- package/Server/API/StatusPageAPI.ts +1092 -45
- package/Server/Infrastructure/Postgres/SchemaMigrations/1770232207959-MigrationName.ts +181 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1770237245069-MigrationName.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentEpisodePublicNoteService.ts +254 -0
- package/Server/Services/IncidentEpisodeService.ts +26 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +6 -0
- package/Types/Email/EmailTemplateType.ts +4 -0
- package/Types/Icon/IconProp.ts +172 -0
- package/Types/Monitor/CriteriaIncident.ts +2 -0
- package/Types/Permission.ts +40 -0
- package/Types/StatusPage/StatusPageSubscriberNotificationEventType.ts +5 -0
- package/UI/Components/Icon/Icon.tsx +1333 -1
- package/build/dist/Models/DatabaseModels/Alert.js +1 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertEpisode.js +1 -0
- package/build/dist/Models/DatabaseModels/AlertEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +1 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +161 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +626 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +86 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js +37 -0
- package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +2 -1
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js +1 -0
- package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +126 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/IncidentEpisodePublicNoteAPI.js +68 -0
- package/build/dist/Server/API/IncidentEpisodePublicNoteAPI.js.map +1 -0
- package/build/dist/Server/API/StatusPageAPI.js +874 -47
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770232207959-MigrationName.js +68 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770232207959-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770237245069-MigrationName.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770237245069-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +223 -0
- package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -0
- package/build/dist/Server/Services/IncidentEpisodeService.js +22 -0
- package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +5 -0
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +3 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Icon/IconProp.js +172 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaIncident.js +1 -0
- package/build/dist/Types/Monitor/CriteriaIncident.js.map +1 -1
- package/build/dist/Types/Permission.js +34 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js +4 -0
- package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +502 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- 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
|
|
3537
|
-
statusPageId: ObjectID
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
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:
|
|
3952
|
+
_id: statusPageId.toString(),
|
|
3555
3953
|
},
|
|
3556
3954
|
select: {
|
|
3557
3955
|
_id: true,
|
|
3558
3956
|
projectId: true,
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
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
|
-
|
|
3970
|
+
if (!statusPage.showEpisodesOnStatusPage) {
|
|
3971
|
+
throw new BadDataException(
|
|
3972
|
+
"Episodes are not enabled on this status page.",
|
|
3973
|
+
);
|
|
3974
|
+
}
|
|
3583
3975
|
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4022
|
+
const incidentIds: Array<ObjectID> = incidents.map((incident: Incident) => {
|
|
4023
|
+
return incident.id!;
|
|
4024
|
+
});
|
|
3606
4025
|
|
|
3607
|
-
|
|
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;
|