@oneuptime/common 12.0.11 → 12.0.13
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/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
- package/Models/DatabaseModels/NetworkDevice.ts +64 -0
- package/Models/DatabaseModels/StatusPage.ts +59 -0
- package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
- package/Server/API/AIChatAPI.ts +55 -56
- package/Server/API/CommonAPI.ts +89 -0
- package/Server/API/MicrosoftTeamsAPI.ts +20 -9
- package/Server/API/SlackAPI.ts +13 -10
- package/Server/API/StatusPageAPI.ts +2197 -2128
- package/Server/API/TelemetryAPI.ts +72 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/UserAuthorization.ts +11 -2
- package/Server/Services/ApiKeyPermissionService.ts +116 -2
- package/Server/Services/DatabaseService.ts +17 -6
- package/Server/Services/LogAggregationService.ts +34 -0
- package/Server/Services/LogService.ts +21 -0
- package/Server/Services/NetworkSiteService.ts +12 -0
- package/Server/Services/SpanService.ts +21 -0
- package/Server/Services/TraceAggregationService.ts +15 -0
- package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
- package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
- package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
- package/Server/Utils/APIKey/AccessPermission.ts +16 -40
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
- package/Server/Utils/LogRedaction.ts +576 -0
- package/Server/Utils/Logger.ts +123 -46
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
- package/Server/Utils/Monitor/MonitorResource.ts +29 -9
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
- package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
- package/Server/Utils/StartServer.ts +30 -9
- package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
- package/Server/Utils/VM/VMRunner.ts +693 -874
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
- package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
- package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
- package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
- package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
- package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
- package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
- package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
- package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
- package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
- package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
- package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
- package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
- package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
- package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
- package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
- package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
- package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
- package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
- package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
- package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
- package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
- package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
- package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
- package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
- package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
- package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
- package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
- package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
- package/Tests/UI/Components/MasterPage.test.tsx +36 -1
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
- package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
- package/Types/API/Hostname.ts +79 -0
- package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
- package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
- package/Types/Database/DatabaseProperty.ts +38 -0
- package/Types/Database/TableColumn.ts +34 -7
- package/Types/Events/Recurring.ts +16 -1
- package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
- package/Types/Monitor/MonitorStep.ts +13 -1
- package/Types/Monitor/MonitorType.ts +345 -40
- package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
- package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
- package/UI/Components/CardSelect/CardSelect.tsx +714 -105
- package/UI/Components/EmptyState/EmptyState.tsx +8 -1
- package/UI/Components/Footer/Footer.tsx +32 -6
- package/UI/Components/Forms/Fields/FormField.tsx +3 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Loader/PageLoader.tsx +29 -8
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
- package/UI/Components/MasterPage/MasterPage.tsx +24 -11
- package/Utils/Monitor/MonitorMetricType.ts +16 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
- package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
- package/Utils/Telemetry/CrossSignalScope.ts +75 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +48 -35
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +59 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +13 -10
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +745 -673
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +34 -3
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.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/Middleware/UserAuthorization.js +6 -2
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +15 -6
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +2 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogService.js +16 -0
- package/build/dist/Server/Services/LogService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +12 -0
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/SpanService.js +16 -0
- package/build/dist/Server/Services/SpanService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +2 -0
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/LogRedaction.js +449 -0
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Logger.js +98 -45
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -7
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Hostname.js +63 -0
- package/build/dist/Types/API/Hostname.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
- package/build/dist/Types/Database/DatabaseProperty.js +38 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/TableColumn.js +24 -6
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +13 -1
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +9 -1
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +330 -31
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
- package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
- package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
- package/build/dist/UI/Components/Footer/Footer.js +7 -4
- package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
- package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import UserMiddleware from "../Middleware/UserAuthorization";
|
|
2
|
+
import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
|
|
2
3
|
import AcmeChallengeService from "../Services/AcmeChallengeService";
|
|
3
4
|
import IncidentEpisodeService from "../Services/IncidentEpisodeService";
|
|
4
5
|
import IncidentEpisodeMemberService from "../Services/IncidentEpisodeMemberService";
|
|
@@ -36,6 +37,10 @@ import {
|
|
|
36
37
|
NextFunction,
|
|
37
38
|
} from "../Utils/Express";
|
|
38
39
|
import logger, { getLogAttributesFromRequest } from "../Utils/Logger";
|
|
40
|
+
import {
|
|
41
|
+
SEARCH_ENGINE_INDEXING_FLAG_NAME,
|
|
42
|
+
isSearchEngineIndexingEnabled,
|
|
43
|
+
} from "../../Types/StatusPage/SearchEngineIndexing";
|
|
39
44
|
import Response from "../Utils/Response";
|
|
40
45
|
import BaseAPI from "./BaseAPI";
|
|
41
46
|
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
@@ -156,6 +161,29 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
156
161
|
StatusPage,
|
|
157
162
|
StatusPageServiceType
|
|
158
163
|
> {
|
|
164
|
+
/*
|
|
165
|
+
* Post-auth overview responses keyed by resolved statusPageId. The payload
|
|
166
|
+
* is user-independent (authorization is a binary gate checked per-request,
|
|
167
|
+
* before any cache read), so one short-TTL snapshot per page per process
|
|
168
|
+
* serves every viewer. See buildOverviewResponse.
|
|
169
|
+
*/
|
|
170
|
+
private static overviewResponseCache: InMemoryTTLCache<JSONObject> =
|
|
171
|
+
new InMemoryTTLCache<JSONObject>(500);
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* In-flight overview builds keyed by the same cache key, so concurrent
|
|
175
|
+
* cold-cache requests share one build instead of stampeding the database.
|
|
176
|
+
*/
|
|
177
|
+
private static overviewResponseInFlight: Map<string, Promise<JSONObject>> =
|
|
178
|
+
new Map();
|
|
179
|
+
|
|
180
|
+
private static readonly OVERVIEW_CACHE_TTL_MS: number = 15_000;
|
|
181
|
+
|
|
182
|
+
public static clearOverviewResponseCache(): void {
|
|
183
|
+
this.overviewResponseCache.clear();
|
|
184
|
+
this.overviewResponseInFlight.clear();
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
public constructor() {
|
|
160
188
|
super(StatusPage, StatusPageService);
|
|
161
189
|
|
|
@@ -192,6 +220,7 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
192
220
|
pageDescription: true,
|
|
193
221
|
name: true,
|
|
194
222
|
defaultLanguage: true,
|
|
223
|
+
enableSearchEngineIndexing: true,
|
|
195
224
|
},
|
|
196
225
|
props: {
|
|
197
226
|
isRoot: true,
|
|
@@ -212,6 +241,14 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
212
241
|
description: statusPage.pageDescription,
|
|
213
242
|
_id: statusPage._id?.toString(),
|
|
214
243
|
defaultLanguage: statusPage.defaultLanguage || null,
|
|
244
|
+
/*
|
|
245
|
+
* Drives <meta name="robots"> and X-Robots-Tag on the rendered page.
|
|
246
|
+
* Normalised here rather than shipping the raw column, so the two
|
|
247
|
+
* frontend servers never have to decide what a missing value means.
|
|
248
|
+
*/
|
|
249
|
+
[SEARCH_ENGINE_INDEXING_FLAG_NAME]: isSearchEngineIndexingEnabled(
|
|
250
|
+
statusPage.enableSearchEngineIndexing,
|
|
251
|
+
),
|
|
215
252
|
});
|
|
216
253
|
},
|
|
217
254
|
);
|
|
@@ -1553,918 +1590,353 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
1553
1590
|
req.params["statusPageIdOrDomain"] as string,
|
|
1554
1591
|
);
|
|
1555
1592
|
|
|
1593
|
+
/*
|
|
1594
|
+
* checkHasReadAccess MUST stay before any cache read: it is what keeps
|
|
1595
|
+
* private pages, IP whitelists, and master passwords enforced on every
|
|
1596
|
+
* request even when the payload below is served from cache.
|
|
1597
|
+
*/
|
|
1556
1598
|
await this.checkHasReadAccess({
|
|
1557
1599
|
statusPageId: statusPageId,
|
|
1558
1600
|
req: req,
|
|
1559
1601
|
});
|
|
1560
1602
|
|
|
1603
|
+
// Resolved id, so domain-served and id-served views share one entry.
|
|
1604
|
+
const cacheKey: string = statusPageId.toString();
|
|
1605
|
+
|
|
1561
1606
|
/*
|
|
1562
|
-
* The
|
|
1563
|
-
*
|
|
1564
|
-
* fetches the status page row anyway, so let it compute the window
|
|
1565
|
-
* (clamped to 1..90 days, ending now) instead of issuing a separate
|
|
1566
|
-
* StatusPage query here just to read that one column.
|
|
1607
|
+
* The cached JSONObject is shared across requests and must never be
|
|
1608
|
+
* mutated after build.
|
|
1567
1609
|
*/
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
postmortemNote: true,
|
|
1594
|
-
postmortemPostedAt: true,
|
|
1595
|
-
showPostmortemOnStatusPage: true,
|
|
1596
|
-
postmortemAttachments: {
|
|
1597
|
-
_id: true,
|
|
1598
|
-
name: true,
|
|
1599
|
-
},
|
|
1600
|
-
incidentSeverity: {
|
|
1601
|
-
name: true,
|
|
1602
|
-
color: true,
|
|
1603
|
-
},
|
|
1604
|
-
currentIncidentState: {
|
|
1605
|
-
_id: true,
|
|
1606
|
-
name: true,
|
|
1607
|
-
color: true,
|
|
1608
|
-
order: true,
|
|
1609
|
-
},
|
|
1610
|
-
monitors: {
|
|
1611
|
-
_id: true,
|
|
1612
|
-
},
|
|
1613
|
-
};
|
|
1614
|
-
|
|
1615
|
-
if (statusPage.showIncidentLabelsOnStatusPage) {
|
|
1616
|
-
select = {
|
|
1617
|
-
...select,
|
|
1618
|
-
labels: {
|
|
1619
|
-
name: true,
|
|
1620
|
-
color: true,
|
|
1621
|
-
},
|
|
1622
|
-
};
|
|
1610
|
+
let response: JSONObject | undefined =
|
|
1611
|
+
StatusPageAPI.overviewResponseCache.get(cacheKey);
|
|
1612
|
+
|
|
1613
|
+
if (!response) {
|
|
1614
|
+
let inFlight: Promise<JSONObject> | undefined =
|
|
1615
|
+
StatusPageAPI.overviewResponseInFlight.get(cacheKey);
|
|
1616
|
+
|
|
1617
|
+
if (!inFlight) {
|
|
1618
|
+
inFlight = this.buildOverviewResponse(statusPageId);
|
|
1619
|
+
StatusPageAPI.overviewResponseInFlight.set(cacheKey, inFlight);
|
|
1620
|
+
|
|
1621
|
+
inFlight
|
|
1622
|
+
.then((builtResponse: JSONObject) => {
|
|
1623
|
+
StatusPageAPI.overviewResponseCache.set(
|
|
1624
|
+
cacheKey,
|
|
1625
|
+
builtResponse,
|
|
1626
|
+
StatusPageAPI.OVERVIEW_CACHE_TTL_MS,
|
|
1627
|
+
);
|
|
1628
|
+
})
|
|
1629
|
+
.catch(() => {
|
|
1630
|
+
// Failed builds are never cached; the next request rebuilds.
|
|
1631
|
+
})
|
|
1632
|
+
.finally(() => {
|
|
1633
|
+
StatusPageAPI.overviewResponseInFlight.delete(cacheKey);
|
|
1634
|
+
});
|
|
1623
1635
|
}
|
|
1624
1636
|
|
|
1625
|
-
|
|
1626
|
-
await IncidentStateService.getUnresolvedIncidentStates(
|
|
1627
|
-
statusPage.projectId!,
|
|
1628
|
-
{
|
|
1629
|
-
isRoot: true,
|
|
1630
|
-
},
|
|
1631
|
-
);
|
|
1632
|
-
|
|
1633
|
-
const unresolvedIncidentStateIds: Array<ObjectID> =
|
|
1634
|
-
unresolvedIncidentStates.map((state: IncidentState) => {
|
|
1635
|
-
return state.id!;
|
|
1636
|
-
});
|
|
1637
|
-
|
|
1638
|
-
if (statusPage.showIncidentsOnStatusPage) {
|
|
1639
|
-
activeIncidents = await IncidentService.findBy({
|
|
1640
|
-
query: {
|
|
1641
|
-
monitors: monitorsOnStatusPage as any,
|
|
1642
|
-
currentIncidentStateId: QueryHelper.any(
|
|
1643
|
-
unresolvedIncidentStateIds,
|
|
1644
|
-
),
|
|
1645
|
-
isVisibleOnStatusPage: true,
|
|
1646
|
-
projectId: statusPage.projectId!,
|
|
1647
|
-
},
|
|
1648
|
-
select: select,
|
|
1649
|
-
sort: {
|
|
1650
|
-
declaredAt: SortOrder.Descending,
|
|
1651
|
-
createdAt: SortOrder.Descending,
|
|
1652
|
-
},
|
|
1653
|
-
|
|
1654
|
-
skip: 0,
|
|
1655
|
-
limit: LIMIT_PER_PROJECT,
|
|
1656
|
-
props: {
|
|
1657
|
-
isRoot: true,
|
|
1658
|
-
},
|
|
1659
|
-
});
|
|
1660
|
-
}
|
|
1637
|
+
response = await inFlight;
|
|
1661
1638
|
}
|
|
1662
1639
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
return incident.id!;
|
|
1666
|
-
},
|
|
1667
|
-
);
|
|
1640
|
+
// These can serve private-page data on a GET; never let shared caches store them.
|
|
1641
|
+
Response.setNoCacheHeaders(res);
|
|
1668
1642
|
|
|
1669
|
-
|
|
1643
|
+
return Response.sendJsonObjectResponse(req, res, response);
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
next(err);
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1670
1648
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
incidentId: QueryHelper.any(incidentsOnStatusPage),
|
|
1675
|
-
projectId: statusPage.projectId!,
|
|
1676
|
-
},
|
|
1677
|
-
select: {
|
|
1678
|
-
note: true,
|
|
1679
|
-
incidentId: true,
|
|
1680
|
-
postedAt: true,
|
|
1681
|
-
attachments: {
|
|
1682
|
-
_id: true,
|
|
1683
|
-
name: true,
|
|
1684
|
-
},
|
|
1685
|
-
},
|
|
1686
|
-
sort: {
|
|
1687
|
-
postedAt: SortOrder.Descending, // new note first
|
|
1688
|
-
},
|
|
1689
|
-
skip: 0,
|
|
1690
|
-
limit: LIMIT_PER_PROJECT,
|
|
1691
|
-
props: {
|
|
1692
|
-
isRoot: true,
|
|
1693
|
-
},
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1649
|
+
const overviewApiPath: string = `${new this.entityType()
|
|
1650
|
+
.getCrudApiPath()
|
|
1651
|
+
?.toString()}/overview/:statusPageIdOrDomain`;
|
|
1696
1652
|
|
|
1697
|
-
|
|
1653
|
+
this.router.post(
|
|
1654
|
+
overviewApiPath,
|
|
1655
|
+
UserMiddleware.getUserMiddleware,
|
|
1656
|
+
overviewHandler,
|
|
1657
|
+
);
|
|
1698
1658
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
incidentId: true,
|
|
1710
|
-
incidentState: {
|
|
1711
|
-
_id: true,
|
|
1712
|
-
name: true,
|
|
1713
|
-
color: true,
|
|
1714
|
-
isCreatedState: true,
|
|
1715
|
-
isResolvedState: true,
|
|
1716
|
-
isAcknowledgedState: true,
|
|
1717
|
-
},
|
|
1718
|
-
},
|
|
1659
|
+
/*
|
|
1660
|
+
* GET variant of the overview endpoint. Same middleware and same handler
|
|
1661
|
+
* as the POST route above — private status pages are rejected for
|
|
1662
|
+
* unauthenticated callers exactly as they are on POST.
|
|
1663
|
+
*/
|
|
1664
|
+
this.router.get(
|
|
1665
|
+
overviewApiPath,
|
|
1666
|
+
UserMiddleware.getUserMiddleware,
|
|
1667
|
+
overviewHandler,
|
|
1668
|
+
);
|
|
1719
1669
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1670
|
+
this.router.put(
|
|
1671
|
+
`${new this.entityType()
|
|
1672
|
+
.getCrudApiPath()
|
|
1673
|
+
?.toString()}/update-subscription/:statusPageId/:subscriberId`,
|
|
1674
|
+
UserMiddleware.getUserMiddleware,
|
|
1675
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
1676
|
+
try {
|
|
1677
|
+
await this.subscribeToStatusPage(req);
|
|
1678
|
+
return Response.sendEmptySuccessResponse(req, res);
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
next(err);
|
|
1729
1681
|
}
|
|
1682
|
+
},
|
|
1683
|
+
);
|
|
1730
1684
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1685
|
+
this.router.post(
|
|
1686
|
+
`${new this.entityType()
|
|
1687
|
+
.getCrudApiPath()
|
|
1688
|
+
?.toString()}/get-subscription/:statusPageId/:subscriberId`,
|
|
1689
|
+
UserMiddleware.getUserMiddleware,
|
|
1690
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
1691
|
+
try {
|
|
1692
|
+
const subscriber: StatusPageSubscriber =
|
|
1693
|
+
await this.getSubscriber(req);
|
|
1736
1694
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
let unresolvedIncidentStateIds: Array<ObjectID> = [];
|
|
1749
|
-
let hasActiveEpisodes: boolean = false;
|
|
1695
|
+
return Response.sendEntityResponse(
|
|
1696
|
+
req,
|
|
1697
|
+
res,
|
|
1698
|
+
subscriber,
|
|
1699
|
+
StatusPageSubscriber,
|
|
1700
|
+
);
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
next(err);
|
|
1703
|
+
}
|
|
1704
|
+
},
|
|
1705
|
+
);
|
|
1750
1706
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
)
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
);
|
|
1707
|
+
this.router.post(
|
|
1708
|
+
`${new this.entityType()
|
|
1709
|
+
.getCrudApiPath()
|
|
1710
|
+
?.toString()}/subscribe/:statusPageId`,
|
|
1711
|
+
UserMiddleware.getUserMiddleware,
|
|
1712
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
1713
|
+
try {
|
|
1714
|
+
await this.subscribeToStatusPage(req);
|
|
1760
1715
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1716
|
+
return Response.sendEmptySuccessResponse(req, res);
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
next(err);
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
);
|
|
1766
1722
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
},
|
|
1776
|
-
props: {
|
|
1777
|
-
isRoot: true,
|
|
1778
|
-
},
|
|
1779
|
-
});
|
|
1723
|
+
this.router.post(
|
|
1724
|
+
`${new this.entityType()
|
|
1725
|
+
.getCrudApiPath()
|
|
1726
|
+
?.toString()}/manage-subscription/:statusPageId`,
|
|
1727
|
+
UserMiddleware.getUserMiddleware,
|
|
1728
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
1729
|
+
try {
|
|
1730
|
+
await this.manageExistingSubscription(req);
|
|
1780
1731
|
|
|
1781
|
-
|
|
1732
|
+
return Response.sendEmptySuccessResponse(req, res);
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
next(err);
|
|
1782
1735
|
}
|
|
1736
|
+
},
|
|
1737
|
+
);
|
|
1783
1738
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
});
|
|
1739
|
+
/*
|
|
1740
|
+
* Shared handler for the incidents list endpoint. Registered for both
|
|
1741
|
+
* POST and GET (same auth path). It does not read req.body.
|
|
1742
|
+
*/
|
|
1743
|
+
const incidentsListHandler: (
|
|
1744
|
+
req: ExpressRequest,
|
|
1745
|
+
res: ExpressResponse,
|
|
1746
|
+
next: NextFunction,
|
|
1747
|
+
) => Promise<void> = async (
|
|
1748
|
+
req: ExpressRequest,
|
|
1749
|
+
res: ExpressResponse,
|
|
1750
|
+
next: NextFunction,
|
|
1751
|
+
): Promise<void> => {
|
|
1752
|
+
try {
|
|
1753
|
+
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
1754
|
+
req.params["statusPageIdOrDomain"] as string,
|
|
1755
|
+
);
|
|
1802
1756
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1757
|
+
const response: JSONObject = await this.getIncidents(
|
|
1758
|
+
objectId,
|
|
1759
|
+
null,
|
|
1760
|
+
req,
|
|
1761
|
+
);
|
|
1807
1762
|
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
if (incidentIdsForEpisodes.length > 0) {
|
|
1811
|
-
episodeMembers = await IncidentEpisodeMemberService.findBy({
|
|
1812
|
-
query: {
|
|
1813
|
-
incidentId: QueryHelper.any(incidentIdsForEpisodes),
|
|
1814
|
-
projectId: statusPage.projectId!,
|
|
1815
|
-
},
|
|
1816
|
-
select: {
|
|
1817
|
-
incidentEpisodeId: true,
|
|
1818
|
-
incidentId: true,
|
|
1819
|
-
},
|
|
1820
|
-
skip: 0,
|
|
1821
|
-
limit: LIMIT_PER_PROJECT,
|
|
1822
|
-
props: {
|
|
1823
|
-
isRoot: true,
|
|
1824
|
-
},
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1763
|
+
// These can serve private-page data on a GET; never let shared caches store them.
|
|
1764
|
+
Response.setNoCacheHeaders(res);
|
|
1827
1765
|
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1766
|
+
return Response.sendJsonObjectResponse(req, res, response);
|
|
1767
|
+
} catch (err) {
|
|
1768
|
+
next(err);
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1835
1771
|
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
let selectEpisodes: Select<IncidentEpisode> = {
|
|
1840
|
-
createdAt: true,
|
|
1841
|
-
declaredAt: true,
|
|
1842
|
-
updatedAt: true,
|
|
1843
|
-
title: true,
|
|
1844
|
-
description: true,
|
|
1845
|
-
_id: true,
|
|
1846
|
-
episodeNumber: true,
|
|
1847
|
-
incidentSeverity: {
|
|
1848
|
-
name: true,
|
|
1849
|
-
color: true,
|
|
1850
|
-
},
|
|
1851
|
-
currentIncidentState: {
|
|
1852
|
-
name: true,
|
|
1853
|
-
color: true,
|
|
1854
|
-
_id: true,
|
|
1855
|
-
order: true,
|
|
1856
|
-
isCreatedState: true,
|
|
1857
|
-
isAcknowledgedState: true,
|
|
1858
|
-
isResolvedState: true,
|
|
1859
|
-
},
|
|
1860
|
-
incidentCount: true,
|
|
1861
|
-
};
|
|
1772
|
+
const incidentsListApiPath: string = `${new this.entityType()
|
|
1773
|
+
.getCrudApiPath()
|
|
1774
|
+
?.toString()}/incidents/:statusPageIdOrDomain`;
|
|
1862
1775
|
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
color: true,
|
|
1869
|
-
},
|
|
1870
|
-
};
|
|
1871
|
-
}
|
|
1776
|
+
this.router.post(
|
|
1777
|
+
incidentsListApiPath,
|
|
1778
|
+
UserMiddleware.getUserMiddleware,
|
|
1779
|
+
incidentsListHandler,
|
|
1780
|
+
);
|
|
1872
1781
|
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
}),
|
|
1879
|
-
),
|
|
1880
|
-
currentIncidentStateId: QueryHelper.any(
|
|
1881
|
-
unresolvedIncidentStateIds,
|
|
1882
|
-
),
|
|
1883
|
-
isVisibleOnStatusPage: true,
|
|
1884
|
-
projectId: statusPage.projectId!,
|
|
1885
|
-
},
|
|
1886
|
-
select: selectEpisodes,
|
|
1887
|
-
sort: {
|
|
1888
|
-
declaredAt: SortOrder.Descending,
|
|
1889
|
-
createdAt: SortOrder.Descending,
|
|
1890
|
-
},
|
|
1891
|
-
skip: 0,
|
|
1892
|
-
limit: LIMIT_PER_PROJECT,
|
|
1893
|
-
props: {
|
|
1894
|
-
isRoot: true,
|
|
1895
|
-
},
|
|
1896
|
-
});
|
|
1782
|
+
this.router.get(
|
|
1783
|
+
incidentsListApiPath,
|
|
1784
|
+
UserMiddleware.getUserMiddleware,
|
|
1785
|
+
incidentsListHandler,
|
|
1786
|
+
);
|
|
1897
1787
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
) {
|
|
1917
|
-
memberIncidentIds.push(member.incidentId);
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1788
|
+
/*
|
|
1789
|
+
* Shared handler for the scheduled maintenance events list endpoint.
|
|
1790
|
+
* Registered for both POST and GET (same auth path). It does not read
|
|
1791
|
+
* req.body.
|
|
1792
|
+
*/
|
|
1793
|
+
const scheduledMaintenanceEventsListHandler: (
|
|
1794
|
+
req: ExpressRequest,
|
|
1795
|
+
res: ExpressResponse,
|
|
1796
|
+
next: NextFunction,
|
|
1797
|
+
) => Promise<void> = async (
|
|
1798
|
+
req: ExpressRequest,
|
|
1799
|
+
res: ExpressResponse,
|
|
1800
|
+
next: NextFunction,
|
|
1801
|
+
): Promise<void> => {
|
|
1802
|
+
try {
|
|
1803
|
+
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
1804
|
+
req.params["statusPageIdOrDomain"] as string,
|
|
1805
|
+
);
|
|
1920
1806
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
memberIncidents = await IncidentService.findBy({
|
|
1925
|
-
query: {
|
|
1926
|
-
_id: QueryHelper.any(memberIncidentIds),
|
|
1927
|
-
isVisibleOnStatusPage: true,
|
|
1928
|
-
projectId: statusPage.projectId!,
|
|
1929
|
-
},
|
|
1930
|
-
select: {
|
|
1931
|
-
_id: true,
|
|
1932
|
-
monitors: {
|
|
1933
|
-
_id: true,
|
|
1934
|
-
},
|
|
1935
|
-
},
|
|
1936
|
-
skip: 0,
|
|
1937
|
-
limit: LIMIT_PER_PROJECT,
|
|
1938
|
-
props: {
|
|
1939
|
-
isRoot: true,
|
|
1940
|
-
},
|
|
1941
|
-
});
|
|
1942
|
-
}
|
|
1807
|
+
const response: JSONObject = await this.getScheduledMaintenanceEvents(
|
|
1808
|
+
objectId,
|
|
1809
|
+
null,
|
|
1943
1810
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
string,
|
|
1947
|
-
Array<ObjectID>
|
|
1948
|
-
> = new Map();
|
|
1949
|
-
for (const incident of memberIncidents) {
|
|
1950
|
-
const incidentIdStr: string = incident.id!.toString();
|
|
1951
|
-
const monitorIds: Array<ObjectID> = (incident.monitors || [])
|
|
1952
|
-
.map((m: Monitor) => {
|
|
1953
|
-
return new ObjectID(
|
|
1954
|
-
m._id?.toString() || m.id?.toString() || "",
|
|
1955
|
-
);
|
|
1956
|
-
})
|
|
1957
|
-
.filter((id: ObjectID) => {
|
|
1958
|
-
return id.toString() !== "";
|
|
1959
|
-
});
|
|
1960
|
-
incidentMonitorsMap.set(incidentIdStr, monitorIds);
|
|
1961
|
-
}
|
|
1811
|
+
req,
|
|
1812
|
+
);
|
|
1962
1813
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
string,
|
|
1966
|
-
Array<ObjectID>
|
|
1967
|
-
> = new Map();
|
|
1968
|
-
for (const member of episodeMembers) {
|
|
1969
|
-
if (
|
|
1970
|
-
member.incidentEpisodeId &&
|
|
1971
|
-
member.incidentId &&
|
|
1972
|
-
activeEpisodeIds.has(member.incidentEpisodeId.toString())
|
|
1973
|
-
) {
|
|
1974
|
-
const episodeIdStr: string =
|
|
1975
|
-
member.incidentEpisodeId.toString();
|
|
1976
|
-
const incidentIdStr: string = member.incidentId.toString();
|
|
1814
|
+
// These can serve private-page data on a GET; never let shared caches store them.
|
|
1815
|
+
Response.setNoCacheHeaders(res);
|
|
1977
1816
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1817
|
+
return Response.sendJsonObjectResponse(req, res, response);
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
next(err);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1981
1822
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
incidentMonitorsMap.get(incidentIdStr) || [];
|
|
1986
|
-
|
|
1987
|
-
for (const monitorId of incidentMonitors) {
|
|
1988
|
-
if (
|
|
1989
|
-
!episodeMonitors.some((m: ObjectID) => {
|
|
1990
|
-
return m.toString() === monitorId.toString();
|
|
1991
|
-
})
|
|
1992
|
-
) {
|
|
1993
|
-
episodeMonitors.push(monitorId);
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1823
|
+
const scheduledMaintenanceEventsListApiPath: string = `${new this.entityType()
|
|
1824
|
+
.getCrudApiPath()
|
|
1825
|
+
?.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain`;
|
|
1998
1826
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
for (const episodeJson of activeEpisodesJson) {
|
|
2005
|
-
const episodeObj: JSONObject = episodeJson as JSONObject;
|
|
2006
|
-
const episodeId: string | undefined =
|
|
2007
|
-
episodeObj["_id"]?.toString();
|
|
2008
|
-
if (episodeId) {
|
|
2009
|
-
const monitorIds: Array<ObjectID> =
|
|
2010
|
-
episodeMonitorsMap.get(episodeId) || [];
|
|
2011
|
-
episodeObj["monitors"] = monitorIds.map((id: ObjectID) => {
|
|
2012
|
-
return { _id: id.toString() };
|
|
2013
|
-
});
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
1827
|
+
this.router.post(
|
|
1828
|
+
scheduledMaintenanceEventsListApiPath,
|
|
1829
|
+
UserMiddleware.getUserMiddleware,
|
|
1830
|
+
scheduledMaintenanceEventsListHandler,
|
|
1831
|
+
);
|
|
2016
1832
|
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
sort: {
|
|
2041
|
-
postedAt: SortOrder.Descending,
|
|
2042
|
-
},
|
|
2043
|
-
skip: 0,
|
|
2044
|
-
limit: LIMIT_PER_PROJECT,
|
|
2045
|
-
props: {
|
|
2046
|
-
isRoot: true,
|
|
2047
|
-
},
|
|
2048
|
-
});
|
|
2049
|
-
|
|
2050
|
-
// Get episode state timelines
|
|
2051
|
-
episodeStateTimelines =
|
|
2052
|
-
await IncidentEpisodeStateTimelineService.findBy({
|
|
2053
|
-
query: {
|
|
2054
|
-
incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
|
|
2055
|
-
projectId: statusPage.projectId!,
|
|
2056
|
-
},
|
|
2057
|
-
select: {
|
|
2058
|
-
_id: true,
|
|
2059
|
-
createdAt: true,
|
|
2060
|
-
startsAt: true,
|
|
2061
|
-
incidentEpisodeId: true,
|
|
2062
|
-
incidentState: {
|
|
2063
|
-
name: true,
|
|
2064
|
-
color: true,
|
|
2065
|
-
isCreatedState: true,
|
|
2066
|
-
isAcknowledgedState: true,
|
|
2067
|
-
isResolvedState: true,
|
|
2068
|
-
},
|
|
2069
|
-
},
|
|
2070
|
-
sort: {
|
|
2071
|
-
startsAt: SortOrder.Descending,
|
|
2072
|
-
},
|
|
2073
|
-
skip: 0,
|
|
2074
|
-
limit: LIMIT_PER_PROJECT,
|
|
2075
|
-
props: {
|
|
2076
|
-
isRoot: true,
|
|
2077
|
-
},
|
|
2078
|
-
});
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
1833
|
+
this.router.get(
|
|
1834
|
+
scheduledMaintenanceEventsListApiPath,
|
|
1835
|
+
UserMiddleware.getUserMiddleware,
|
|
1836
|
+
scheduledMaintenanceEventsListHandler,
|
|
1837
|
+
);
|
|
1838
|
+
|
|
1839
|
+
/*
|
|
1840
|
+
* Shared handler for the announcements list endpoint. Registered for
|
|
1841
|
+
* both POST and GET (same auth path). It does not read req.body.
|
|
1842
|
+
*/
|
|
1843
|
+
const announcementsListHandler: (
|
|
1844
|
+
req: ExpressRequest,
|
|
1845
|
+
res: ExpressResponse,
|
|
1846
|
+
next: NextFunction,
|
|
1847
|
+
) => Promise<void> = async (
|
|
1848
|
+
req: ExpressRequest,
|
|
1849
|
+
res: ExpressResponse,
|
|
1850
|
+
next: NextFunction,
|
|
1851
|
+
): Promise<void> => {
|
|
1852
|
+
try {
|
|
1853
|
+
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
1854
|
+
req.params["statusPageIdOrDomain"] as string,
|
|
1855
|
+
);
|
|
2083
1856
|
|
|
2084
|
-
|
|
1857
|
+
const response: JSONObject = await this.getAnnouncements(
|
|
1858
|
+
objectId,
|
|
1859
|
+
null,
|
|
2085
1860
|
|
|
2086
|
-
|
|
1861
|
+
req,
|
|
1862
|
+
);
|
|
2087
1863
|
|
|
2088
|
-
let
|
|
1864
|
+
// These can serve private-page data on a GET; never let shared caches store them.
|
|
1865
|
+
Response.setNoCacheHeaders(res);
|
|
2089
1866
|
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
endAnnouncementAt: QueryHelper.greaterThanOrNull(today),
|
|
2096
|
-
projectId: statusPage.projectId!,
|
|
2097
|
-
},
|
|
2098
|
-
select: {
|
|
2099
|
-
createdAt: true,
|
|
2100
|
-
title: true,
|
|
2101
|
-
description: true,
|
|
2102
|
-
_id: true,
|
|
2103
|
-
showAnnouncementAt: true,
|
|
2104
|
-
endAnnouncementAt: true,
|
|
2105
|
-
},
|
|
2106
|
-
skip: 0,
|
|
2107
|
-
limit: LIMIT_PER_PROJECT,
|
|
2108
|
-
props: {
|
|
2109
|
-
isRoot: true,
|
|
2110
|
-
},
|
|
2111
|
-
});
|
|
2112
|
-
}
|
|
1867
|
+
return Response.sendJsonObjectResponse(req, res, response);
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
next(err);
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
2113
1872
|
|
|
2114
|
-
|
|
1873
|
+
const announcementsListApiPath: string = `${new this.entityType()
|
|
1874
|
+
.getCrudApiPath()
|
|
1875
|
+
?.toString()}/announcements/:statusPageIdOrDomain`;
|
|
2115
1876
|
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
endsAt: true,
|
|
2122
|
-
startsAt: true,
|
|
2123
|
-
currentScheduledMaintenanceState: {
|
|
2124
|
-
name: true,
|
|
2125
|
-
color: true,
|
|
2126
|
-
isScheduledState: true,
|
|
2127
|
-
isResolvedState: true,
|
|
2128
|
-
isOngoingState: true,
|
|
2129
|
-
},
|
|
2130
|
-
monitors: {
|
|
2131
|
-
_id: true,
|
|
2132
|
-
},
|
|
2133
|
-
};
|
|
1877
|
+
this.router.post(
|
|
1878
|
+
announcementsListApiPath,
|
|
1879
|
+
UserMiddleware.getUserMiddleware,
|
|
1880
|
+
announcementsListHandler,
|
|
1881
|
+
);
|
|
2134
1882
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
color: true,
|
|
2141
|
-
},
|
|
2142
|
-
};
|
|
2143
|
-
}
|
|
1883
|
+
this.router.get(
|
|
1884
|
+
announcementsListApiPath,
|
|
1885
|
+
UserMiddleware.getUserMiddleware,
|
|
1886
|
+
announcementsListHandler,
|
|
1887
|
+
);
|
|
2144
1888
|
|
|
2145
|
-
|
|
1889
|
+
this.router.post(
|
|
1890
|
+
`${new this.entityType()
|
|
1891
|
+
.getCrudApiPath()
|
|
1892
|
+
?.toString()}/incidents/:statusPageIdOrDomain/:incidentId`,
|
|
1893
|
+
UserMiddleware.getUserMiddleware,
|
|
1894
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
1895
|
+
try {
|
|
1896
|
+
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
1897
|
+
req.params["statusPageIdOrDomain"] as string,
|
|
1898
|
+
);
|
|
2146
1899
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
{
|
|
2150
|
-
query: {
|
|
2151
|
-
currentScheduledMaintenanceState: {
|
|
2152
|
-
isOngoingState: true,
|
|
2153
|
-
} as any,
|
|
2154
|
-
statusPages: statusPageId as any,
|
|
2155
|
-
projectId: statusPage.projectId!,
|
|
2156
|
-
isVisibleOnStatusPage: true,
|
|
2157
|
-
},
|
|
2158
|
-
select: scheduledEventsSelect,
|
|
2159
|
-
sort: {
|
|
2160
|
-
startsAt: SortOrder.Ascending,
|
|
2161
|
-
},
|
|
2162
|
-
skip: 0,
|
|
2163
|
-
limit: LIMIT_PER_PROJECT,
|
|
2164
|
-
props: {
|
|
2165
|
-
isRoot: true,
|
|
2166
|
-
},
|
|
2167
|
-
},
|
|
1900
|
+
const incidentId: ObjectID = new ObjectID(
|
|
1901
|
+
req.params["incidentId"] as string,
|
|
2168
1902
|
);
|
|
2169
|
-
}
|
|
2170
1903
|
|
|
2171
|
-
|
|
1904
|
+
const response: JSONObject = await this.getIncidents(
|
|
1905
|
+
objectId,
|
|
1906
|
+
incidentId,
|
|
1907
|
+
req,
|
|
1908
|
+
);
|
|
2172
1909
|
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
query: {
|
|
2177
|
-
currentScheduledMaintenanceState: {
|
|
2178
|
-
isScheduledState: true,
|
|
2179
|
-
} as any,
|
|
2180
|
-
statusPages: statusPageId as any,
|
|
2181
|
-
projectId: statusPage.projectId!,
|
|
2182
|
-
isVisibleOnStatusPage: true,
|
|
2183
|
-
},
|
|
2184
|
-
select: scheduledEventsSelect,
|
|
2185
|
-
sort: {
|
|
2186
|
-
startsAt: SortOrder.Ascending,
|
|
2187
|
-
},
|
|
2188
|
-
skip: 0,
|
|
2189
|
-
limit: LIMIT_PER_PROJECT,
|
|
2190
|
-
props: {
|
|
2191
|
-
isRoot: true,
|
|
2192
|
-
},
|
|
2193
|
-
});
|
|
1910
|
+
return Response.sendJsonObjectResponse(req, res, response);
|
|
1911
|
+
} catch (err) {
|
|
1912
|
+
next(err);
|
|
2194
1913
|
}
|
|
1914
|
+
},
|
|
1915
|
+
);
|
|
2195
1916
|
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
1917
|
+
this.router.post(
|
|
1918
|
+
`${new this.entityType()
|
|
1919
|
+
.getCrudApiPath()
|
|
1920
|
+
?.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain/:scheduledMaintenanceId`,
|
|
1921
|
+
UserMiddleware.getUserMiddleware,
|
|
1922
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
1923
|
+
try {
|
|
1924
|
+
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
1925
|
+
req.params["statusPageIdOrDomain"] as string,
|
|
1926
|
+
);
|
|
2201
1927
|
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
});
|
|
1928
|
+
const scheduledMaintenanceId: ObjectID = new ObjectID(
|
|
1929
|
+
req.params["scheduledMaintenanceId"] as string,
|
|
1930
|
+
);
|
|
2206
1931
|
|
|
2207
|
-
|
|
2208
|
-
|
|
1932
|
+
const response: JSONObject = await this.getScheduledMaintenanceEvents(
|
|
1933
|
+
objectId,
|
|
1934
|
+
scheduledMaintenanceId,
|
|
2209
1935
|
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
await ScheduledMaintenancePublicNoteService.findBy({
|
|
2213
|
-
query: {
|
|
2214
|
-
scheduledMaintenanceId: QueryHelper.any(
|
|
2215
|
-
scheduledMaintenanceEventsOnStatusPage,
|
|
2216
|
-
),
|
|
2217
|
-
projectId: statusPage.projectId!,
|
|
2218
|
-
},
|
|
2219
|
-
select: {
|
|
2220
|
-
postedAt: true,
|
|
2221
|
-
note: true,
|
|
2222
|
-
scheduledMaintenanceId: true,
|
|
2223
|
-
attachments: {
|
|
2224
|
-
_id: true,
|
|
2225
|
-
name: true,
|
|
2226
|
-
},
|
|
2227
|
-
},
|
|
2228
|
-
sort: {
|
|
2229
|
-
postedAt: SortOrder.Ascending,
|
|
2230
|
-
},
|
|
2231
|
-
skip: 0,
|
|
2232
|
-
limit: LIMIT_PER_PROJECT,
|
|
2233
|
-
props: {
|
|
2234
|
-
isRoot: true,
|
|
2235
|
-
},
|
|
2236
|
-
});
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
let scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline> =
|
|
2240
|
-
[];
|
|
2241
|
-
|
|
2242
|
-
if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
|
|
2243
|
-
scheduledMaintenanceStateTimelines =
|
|
2244
|
-
await ScheduledMaintenanceStateTimelineService.findBy({
|
|
2245
|
-
query: {
|
|
2246
|
-
scheduledMaintenanceId: QueryHelper.any(
|
|
2247
|
-
scheduledMaintenanceEventsOnStatusPage,
|
|
2248
|
-
),
|
|
2249
|
-
projectId: statusPage.projectId!,
|
|
2250
|
-
},
|
|
2251
|
-
select: {
|
|
2252
|
-
_id: true,
|
|
2253
|
-
createdAt: true,
|
|
2254
|
-
startsAt: true,
|
|
2255
|
-
scheduledMaintenanceId: true,
|
|
2256
|
-
scheduledMaintenanceState: {
|
|
2257
|
-
_id: true,
|
|
2258
|
-
color: true,
|
|
2259
|
-
name: true,
|
|
2260
|
-
isScheduledState: true,
|
|
2261
|
-
isResolvedState: true,
|
|
2262
|
-
isOngoingState: true,
|
|
2263
|
-
},
|
|
2264
|
-
},
|
|
2265
|
-
|
|
2266
|
-
sort: {
|
|
2267
|
-
startsAt: SortOrder.Descending, // newer state changes first
|
|
2268
|
-
},
|
|
2269
|
-
skip: 0,
|
|
2270
|
-
limit: LIMIT_PER_PROJECT,
|
|
2271
|
-
props: {
|
|
2272
|
-
isRoot: true,
|
|
2273
|
-
},
|
|
2274
|
-
});
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
// get all status page bar chart rules
|
|
2278
|
-
const statusPageHistoryChartBarColorRules: Array<StatusPageHistoryChartBarColorRule> =
|
|
2279
|
-
await StatusPageHistoryChartBarColorRuleService.findBy({
|
|
2280
|
-
query: {
|
|
2281
|
-
statusPageId: statusPageId,
|
|
2282
|
-
},
|
|
2283
|
-
select: {
|
|
2284
|
-
_id: true,
|
|
2285
|
-
barColor: true,
|
|
2286
|
-
order: true,
|
|
2287
|
-
statusPageId: true,
|
|
2288
|
-
uptimePercentGreaterThanOrEqualTo: true,
|
|
2289
|
-
},
|
|
2290
|
-
sort: {
|
|
2291
|
-
order: SortOrder.Ascending,
|
|
2292
|
-
},
|
|
2293
|
-
skip: 0,
|
|
2294
|
-
limit: LIMIT_PER_PROJECT,
|
|
2295
|
-
props: {
|
|
2296
|
-
isRoot: true,
|
|
2297
|
-
},
|
|
2298
|
-
});
|
|
2299
|
-
|
|
2300
|
-
/*
|
|
2301
|
-
* Fetch all incidents (active + resolved) in the timeline date range
|
|
2302
|
-
* for the uptime bar tooltip and click-through
|
|
2303
|
-
*/
|
|
2304
|
-
let timelineIncidents: Array<Incident> = [];
|
|
2305
|
-
if (
|
|
2306
|
-
monitorsOnStatusPage.length > 0 &&
|
|
2307
|
-
statusPage.showIncidentsOnStatusPage
|
|
2308
|
-
) {
|
|
2309
|
-
timelineIncidents = await IncidentService.findBy({
|
|
2310
|
-
query: {
|
|
2311
|
-
monitors: monitorsOnStatusPage as any,
|
|
2312
|
-
declaredAt: QueryHelper.inBetween(startDate, endDate),
|
|
2313
|
-
isVisibleOnStatusPage: true,
|
|
2314
|
-
projectId: statusPage.projectId!,
|
|
2315
|
-
},
|
|
2316
|
-
select: {
|
|
2317
|
-
_id: true,
|
|
2318
|
-
title: true,
|
|
2319
|
-
declaredAt: true,
|
|
2320
|
-
incidentSeverity: {
|
|
2321
|
-
name: true,
|
|
2322
|
-
color: true,
|
|
2323
|
-
},
|
|
2324
|
-
currentIncidentState: {
|
|
2325
|
-
_id: true,
|
|
2326
|
-
name: true,
|
|
2327
|
-
color: true,
|
|
2328
|
-
},
|
|
2329
|
-
monitors: {
|
|
2330
|
-
_id: true,
|
|
2331
|
-
},
|
|
2332
|
-
},
|
|
2333
|
-
sort: {
|
|
2334
|
-
declaredAt: SortOrder.Descending,
|
|
2335
|
-
},
|
|
2336
|
-
skip: 0,
|
|
2337
|
-
limit: LIMIT_PER_PROJECT,
|
|
2338
|
-
props: {
|
|
2339
|
-
isRoot: true,
|
|
2340
|
-
},
|
|
2341
|
-
});
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
const overallStatus: MonitorStatus | null =
|
|
2345
|
-
StatusPageService.getOverallMonitorStatus({
|
|
2346
|
-
statusPageResources,
|
|
2347
|
-
monitorStatuses,
|
|
2348
|
-
monitorGroupCurrentStatuses,
|
|
2349
|
-
});
|
|
2350
|
-
|
|
2351
|
-
const response: JSONObject = {
|
|
2352
|
-
overallStatus: overallStatus
|
|
2353
|
-
? BaseModel.toJSON(overallStatus, MonitorStatus)
|
|
2354
|
-
: null,
|
|
2355
|
-
|
|
2356
|
-
scheduledMaintenanceEventsPublicNotes: BaseModel.toJSONArray(
|
|
2357
|
-
scheduledMaintenanceEventsPublicNotes,
|
|
2358
|
-
ScheduledMaintenancePublicNote,
|
|
2359
|
-
),
|
|
2360
|
-
statusPageHistoryChartBarColorRules: BaseModel.toJSONArray(
|
|
2361
|
-
statusPageHistoryChartBarColorRules,
|
|
2362
|
-
StatusPageHistoryChartBarColorRule,
|
|
2363
|
-
),
|
|
2364
|
-
scheduledMaintenanceEvents: BaseModel.toJSONArray(
|
|
2365
|
-
scheduledMaintenanceEvents,
|
|
2366
|
-
ScheduledMaintenance,
|
|
2367
|
-
),
|
|
2368
|
-
activeAnnouncements: BaseModel.toJSONArray(
|
|
2369
|
-
activeAnnouncements,
|
|
2370
|
-
StatusPageAnnouncement,
|
|
2371
|
-
),
|
|
2372
|
-
incidentPublicNotes: BaseModel.toJSONArray(
|
|
2373
|
-
incidentPublicNotes,
|
|
2374
|
-
IncidentPublicNote,
|
|
2375
|
-
),
|
|
2376
|
-
|
|
2377
|
-
activeIncidents: BaseModel.toJSONArray(activeIncidents, Incident),
|
|
2378
|
-
|
|
2379
|
-
activeEpisodes: activeEpisodesJson,
|
|
2380
|
-
episodePublicNotes: BaseModel.toJSONArray(
|
|
2381
|
-
episodePublicNotes,
|
|
2382
|
-
IncidentEpisodePublicNote,
|
|
2383
|
-
),
|
|
2384
|
-
episodeStateTimelines: BaseModel.toJSONArray(
|
|
2385
|
-
episodeStateTimelines,
|
|
2386
|
-
IncidentEpisodeStateTimeline,
|
|
2387
|
-
),
|
|
2388
|
-
|
|
2389
|
-
monitorStatusTimelines: BaseModel.toJSONArray(
|
|
2390
|
-
monitorStatusTimelines,
|
|
2391
|
-
MonitorStatusTimeline,
|
|
2392
|
-
),
|
|
2393
|
-
resourceGroups: BaseModel.toJSONArray(
|
|
2394
|
-
statusPageGroups,
|
|
2395
|
-
StatusPageGroup,
|
|
2396
|
-
),
|
|
2397
|
-
monitorStatuses: BaseModel.toJSONArray(
|
|
2398
|
-
monitorStatuses,
|
|
2399
|
-
MonitorStatus,
|
|
2400
|
-
),
|
|
2401
|
-
statusPageResources: BaseModel.toJSONArray(
|
|
2402
|
-
statusPageResources,
|
|
2403
|
-
StatusPageResource,
|
|
2404
|
-
),
|
|
2405
|
-
incidentStateTimelines: BaseModel.toJSONArray(
|
|
2406
|
-
incidentStateTimelines,
|
|
2407
|
-
IncidentStateTimeline,
|
|
2408
|
-
),
|
|
2409
|
-
statusPage: (() => {
|
|
2410
|
-
const statusPageJson: JSONObject = BaseModel.toJSONObject(
|
|
2411
|
-
statusPage,
|
|
2412
|
-
StatusPage,
|
|
2413
|
-
);
|
|
2414
|
-
delete statusPageJson["projectId"];
|
|
2415
|
-
return statusPageJson;
|
|
2416
|
-
})(),
|
|
2417
|
-
scheduledMaintenanceStateTimelines: BaseModel.toJSONArray(
|
|
2418
|
-
scheduledMaintenanceStateTimelines,
|
|
2419
|
-
ScheduledMaintenanceStateTimeline,
|
|
2420
|
-
),
|
|
2421
|
-
|
|
2422
|
-
monitorGroupCurrentStatuses: JSONFunctions.serialize(
|
|
2423
|
-
monitorGroupCurrentStatuses,
|
|
2424
|
-
),
|
|
2425
|
-
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
|
2426
|
-
timelineIncidents: BaseModel.toJSONArray(timelineIncidents, Incident),
|
|
2427
|
-
};
|
|
2428
|
-
|
|
2429
|
-
// These can serve private-page data on a GET; never let shared caches store them.
|
|
2430
|
-
Response.setNoCacheHeaders(res);
|
|
2431
|
-
|
|
2432
|
-
return Response.sendJsonObjectResponse(req, res, response);
|
|
2433
|
-
} catch (err) {
|
|
2434
|
-
next(err);
|
|
2435
|
-
}
|
|
2436
|
-
};
|
|
2437
|
-
|
|
2438
|
-
const overviewApiPath: string = `${new this.entityType()
|
|
2439
|
-
.getCrudApiPath()
|
|
2440
|
-
?.toString()}/overview/:statusPageIdOrDomain`;
|
|
2441
|
-
|
|
2442
|
-
this.router.post(
|
|
2443
|
-
overviewApiPath,
|
|
2444
|
-
UserMiddleware.getUserMiddleware,
|
|
2445
|
-
overviewHandler,
|
|
2446
|
-
);
|
|
2447
|
-
|
|
2448
|
-
/*
|
|
2449
|
-
* GET variant of the overview endpoint. Same middleware and same handler
|
|
2450
|
-
* as the POST route above — private status pages are rejected for
|
|
2451
|
-
* unauthenticated callers exactly as they are on POST.
|
|
2452
|
-
*/
|
|
2453
|
-
this.router.get(
|
|
2454
|
-
overviewApiPath,
|
|
2455
|
-
UserMiddleware.getUserMiddleware,
|
|
2456
|
-
overviewHandler,
|
|
2457
|
-
);
|
|
1936
|
+
req,
|
|
1937
|
+
);
|
|
2458
1938
|
|
|
2459
|
-
|
|
2460
|
-
`${new this.entityType()
|
|
2461
|
-
.getCrudApiPath()
|
|
2462
|
-
?.toString()}/update-subscription/:statusPageId/:subscriberId`,
|
|
2463
|
-
UserMiddleware.getUserMiddleware,
|
|
2464
|
-
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
2465
|
-
try {
|
|
2466
|
-
await this.subscribeToStatusPage(req);
|
|
2467
|
-
return Response.sendEmptySuccessResponse(req, res);
|
|
1939
|
+
return Response.sendJsonObjectResponse(req, res, response);
|
|
2468
1940
|
} catch (err) {
|
|
2469
1941
|
next(err);
|
|
2470
1942
|
}
|
|
@@ -2474,35 +1946,51 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
2474
1946
|
this.router.post(
|
|
2475
1947
|
`${new this.entityType()
|
|
2476
1948
|
.getCrudApiPath()
|
|
2477
|
-
?.toString()}/
|
|
1949
|
+
?.toString()}/announcements/:statusPageIdOrDomain/:announcementId`,
|
|
2478
1950
|
UserMiddleware.getUserMiddleware,
|
|
2479
1951
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
2480
1952
|
try {
|
|
2481
|
-
const
|
|
2482
|
-
|
|
1953
|
+
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
1954
|
+
req.params["statusPageIdOrDomain"] as string,
|
|
1955
|
+
);
|
|
1956
|
+
|
|
1957
|
+
const announcementId: ObjectID = new ObjectID(
|
|
1958
|
+
req.params["announcementId"] as string,
|
|
1959
|
+
);
|
|
1960
|
+
|
|
1961
|
+
const response: JSONObject = await this.getAnnouncements(
|
|
1962
|
+
objectId,
|
|
1963
|
+
announcementId,
|
|
2483
1964
|
|
|
2484
|
-
return Response.sendEntityResponse(
|
|
2485
1965
|
req,
|
|
2486
|
-
res,
|
|
2487
|
-
subscriber,
|
|
2488
|
-
StatusPageSubscriber,
|
|
2489
1966
|
);
|
|
1967
|
+
|
|
1968
|
+
return Response.sendJsonObjectResponse(req, res, response);
|
|
2490
1969
|
} catch (err) {
|
|
2491
1970
|
next(err);
|
|
2492
1971
|
}
|
|
2493
1972
|
},
|
|
2494
1973
|
);
|
|
2495
1974
|
|
|
1975
|
+
// Episodes endpoints
|
|
2496
1976
|
this.router.post(
|
|
2497
1977
|
`${new this.entityType()
|
|
2498
1978
|
.getCrudApiPath()
|
|
2499
|
-
?.toString()}/
|
|
1979
|
+
?.toString()}/episodes/:statusPageIdOrDomain`,
|
|
2500
1980
|
UserMiddleware.getUserMiddleware,
|
|
2501
1981
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
2502
1982
|
try {
|
|
2503
|
-
await
|
|
1983
|
+
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
1984
|
+
req.params["statusPageIdOrDomain"] as string,
|
|
1985
|
+
);
|
|
2504
1986
|
|
|
2505
|
-
|
|
1987
|
+
const response: JSONObject = await this.getEpisodes(
|
|
1988
|
+
objectId,
|
|
1989
|
+
null,
|
|
1990
|
+
req,
|
|
1991
|
+
);
|
|
1992
|
+
|
|
1993
|
+
return Response.sendJsonObjectResponse(req, res, response);
|
|
2506
1994
|
} catch (err) {
|
|
2507
1995
|
next(err);
|
|
2508
1996
|
}
|
|
@@ -2512,319 +2000,42 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
2512
2000
|
this.router.post(
|
|
2513
2001
|
`${new this.entityType()
|
|
2514
2002
|
.getCrudApiPath()
|
|
2515
|
-
?.toString()}/
|
|
2003
|
+
?.toString()}/episodes/:statusPageIdOrDomain/:episodeId`,
|
|
2516
2004
|
UserMiddleware.getUserMiddleware,
|
|
2517
2005
|
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
2518
2006
|
try {
|
|
2519
|
-
await
|
|
2007
|
+
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
2008
|
+
req.params["statusPageIdOrDomain"] as string,
|
|
2009
|
+
);
|
|
2520
2010
|
|
|
2521
|
-
|
|
2011
|
+
const episodeId: ObjectID = new ObjectID(
|
|
2012
|
+
req.params["episodeId"] as string,
|
|
2013
|
+
);
|
|
2014
|
+
|
|
2015
|
+
const response: JSONObject = await this.getEpisodes(
|
|
2016
|
+
objectId,
|
|
2017
|
+
episodeId,
|
|
2018
|
+
req,
|
|
2019
|
+
);
|
|
2020
|
+
|
|
2021
|
+
return Response.sendJsonObjectResponse(req, res, response);
|
|
2522
2022
|
} catch (err) {
|
|
2523
2023
|
next(err);
|
|
2524
2024
|
}
|
|
2525
2025
|
},
|
|
2526
2026
|
);
|
|
2027
|
+
}
|
|
2527
2028
|
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
res: ExpressResponse,
|
|
2539
|
-
next: NextFunction,
|
|
2540
|
-
): Promise<void> => {
|
|
2541
|
-
try {
|
|
2542
|
-
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
2543
|
-
req.params["statusPageIdOrDomain"] as string,
|
|
2544
|
-
);
|
|
2545
|
-
|
|
2546
|
-
const response: JSONObject = await this.getIncidents(
|
|
2547
|
-
objectId,
|
|
2548
|
-
null,
|
|
2549
|
-
req,
|
|
2550
|
-
);
|
|
2551
|
-
|
|
2552
|
-
// These can serve private-page data on a GET; never let shared caches store them.
|
|
2553
|
-
Response.setNoCacheHeaders(res);
|
|
2554
|
-
|
|
2555
|
-
return Response.sendJsonObjectResponse(req, res, response);
|
|
2556
|
-
} catch (err) {
|
|
2557
|
-
next(err);
|
|
2558
|
-
}
|
|
2559
|
-
};
|
|
2560
|
-
|
|
2561
|
-
const incidentsListApiPath: string = `${new this.entityType()
|
|
2562
|
-
.getCrudApiPath()
|
|
2563
|
-
?.toString()}/incidents/:statusPageIdOrDomain`;
|
|
2564
|
-
|
|
2565
|
-
this.router.post(
|
|
2566
|
-
incidentsListApiPath,
|
|
2567
|
-
UserMiddleware.getUserMiddleware,
|
|
2568
|
-
incidentsListHandler,
|
|
2569
|
-
);
|
|
2570
|
-
|
|
2571
|
-
this.router.get(
|
|
2572
|
-
incidentsListApiPath,
|
|
2573
|
-
UserMiddleware.getUserMiddleware,
|
|
2574
|
-
incidentsListHandler,
|
|
2575
|
-
);
|
|
2576
|
-
|
|
2577
|
-
/*
|
|
2578
|
-
* Shared handler for the scheduled maintenance events list endpoint.
|
|
2579
|
-
* Registered for both POST and GET (same auth path). It does not read
|
|
2580
|
-
* req.body.
|
|
2581
|
-
*/
|
|
2582
|
-
const scheduledMaintenanceEventsListHandler: (
|
|
2583
|
-
req: ExpressRequest,
|
|
2584
|
-
res: ExpressResponse,
|
|
2585
|
-
next: NextFunction,
|
|
2586
|
-
) => Promise<void> = async (
|
|
2587
|
-
req: ExpressRequest,
|
|
2588
|
-
res: ExpressResponse,
|
|
2589
|
-
next: NextFunction,
|
|
2590
|
-
): Promise<void> => {
|
|
2591
|
-
try {
|
|
2592
|
-
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
2593
|
-
req.params["statusPageIdOrDomain"] as string,
|
|
2594
|
-
);
|
|
2595
|
-
|
|
2596
|
-
const response: JSONObject = await this.getScheduledMaintenanceEvents(
|
|
2597
|
-
objectId,
|
|
2598
|
-
null,
|
|
2599
|
-
|
|
2600
|
-
req,
|
|
2601
|
-
);
|
|
2602
|
-
|
|
2603
|
-
// These can serve private-page data on a GET; never let shared caches store them.
|
|
2604
|
-
Response.setNoCacheHeaders(res);
|
|
2605
|
-
|
|
2606
|
-
return Response.sendJsonObjectResponse(req, res, response);
|
|
2607
|
-
} catch (err) {
|
|
2608
|
-
next(err);
|
|
2609
|
-
}
|
|
2610
|
-
};
|
|
2611
|
-
|
|
2612
|
-
const scheduledMaintenanceEventsListApiPath: string = `${new this.entityType()
|
|
2613
|
-
.getCrudApiPath()
|
|
2614
|
-
?.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain`;
|
|
2615
|
-
|
|
2616
|
-
this.router.post(
|
|
2617
|
-
scheduledMaintenanceEventsListApiPath,
|
|
2618
|
-
UserMiddleware.getUserMiddleware,
|
|
2619
|
-
scheduledMaintenanceEventsListHandler,
|
|
2620
|
-
);
|
|
2621
|
-
|
|
2622
|
-
this.router.get(
|
|
2623
|
-
scheduledMaintenanceEventsListApiPath,
|
|
2624
|
-
UserMiddleware.getUserMiddleware,
|
|
2625
|
-
scheduledMaintenanceEventsListHandler,
|
|
2626
|
-
);
|
|
2627
|
-
|
|
2628
|
-
/*
|
|
2629
|
-
* Shared handler for the announcements list endpoint. Registered for
|
|
2630
|
-
* both POST and GET (same auth path). It does not read req.body.
|
|
2631
|
-
*/
|
|
2632
|
-
const announcementsListHandler: (
|
|
2633
|
-
req: ExpressRequest,
|
|
2634
|
-
res: ExpressResponse,
|
|
2635
|
-
next: NextFunction,
|
|
2636
|
-
) => Promise<void> = async (
|
|
2637
|
-
req: ExpressRequest,
|
|
2638
|
-
res: ExpressResponse,
|
|
2639
|
-
next: NextFunction,
|
|
2640
|
-
): Promise<void> => {
|
|
2641
|
-
try {
|
|
2642
|
-
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
2643
|
-
req.params["statusPageIdOrDomain"] as string,
|
|
2644
|
-
);
|
|
2645
|
-
|
|
2646
|
-
const response: JSONObject = await this.getAnnouncements(
|
|
2647
|
-
objectId,
|
|
2648
|
-
null,
|
|
2649
|
-
|
|
2650
|
-
req,
|
|
2651
|
-
);
|
|
2652
|
-
|
|
2653
|
-
// These can serve private-page data on a GET; never let shared caches store them.
|
|
2654
|
-
Response.setNoCacheHeaders(res);
|
|
2655
|
-
|
|
2656
|
-
return Response.sendJsonObjectResponse(req, res, response);
|
|
2657
|
-
} catch (err) {
|
|
2658
|
-
next(err);
|
|
2659
|
-
}
|
|
2660
|
-
};
|
|
2661
|
-
|
|
2662
|
-
const announcementsListApiPath: string = `${new this.entityType()
|
|
2663
|
-
.getCrudApiPath()
|
|
2664
|
-
?.toString()}/announcements/:statusPageIdOrDomain`;
|
|
2665
|
-
|
|
2666
|
-
this.router.post(
|
|
2667
|
-
announcementsListApiPath,
|
|
2668
|
-
UserMiddleware.getUserMiddleware,
|
|
2669
|
-
announcementsListHandler,
|
|
2670
|
-
);
|
|
2671
|
-
|
|
2672
|
-
this.router.get(
|
|
2673
|
-
announcementsListApiPath,
|
|
2674
|
-
UserMiddleware.getUserMiddleware,
|
|
2675
|
-
announcementsListHandler,
|
|
2676
|
-
);
|
|
2677
|
-
|
|
2678
|
-
this.router.post(
|
|
2679
|
-
`${new this.entityType()
|
|
2680
|
-
.getCrudApiPath()
|
|
2681
|
-
?.toString()}/incidents/:statusPageIdOrDomain/:incidentId`,
|
|
2682
|
-
UserMiddleware.getUserMiddleware,
|
|
2683
|
-
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
2684
|
-
try {
|
|
2685
|
-
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
2686
|
-
req.params["statusPageIdOrDomain"] as string,
|
|
2687
|
-
);
|
|
2688
|
-
|
|
2689
|
-
const incidentId: ObjectID = new ObjectID(
|
|
2690
|
-
req.params["incidentId"] as string,
|
|
2691
|
-
);
|
|
2692
|
-
|
|
2693
|
-
const response: JSONObject = await this.getIncidents(
|
|
2694
|
-
objectId,
|
|
2695
|
-
incidentId,
|
|
2696
|
-
req,
|
|
2697
|
-
);
|
|
2698
|
-
|
|
2699
|
-
return Response.sendJsonObjectResponse(req, res, response);
|
|
2700
|
-
} catch (err) {
|
|
2701
|
-
next(err);
|
|
2702
|
-
}
|
|
2703
|
-
},
|
|
2704
|
-
);
|
|
2705
|
-
|
|
2706
|
-
this.router.post(
|
|
2707
|
-
`${new this.entityType()
|
|
2708
|
-
.getCrudApiPath()
|
|
2709
|
-
?.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain/:scheduledMaintenanceId`,
|
|
2710
|
-
UserMiddleware.getUserMiddleware,
|
|
2711
|
-
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
2712
|
-
try {
|
|
2713
|
-
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
2714
|
-
req.params["statusPageIdOrDomain"] as string,
|
|
2715
|
-
);
|
|
2716
|
-
|
|
2717
|
-
const scheduledMaintenanceId: ObjectID = new ObjectID(
|
|
2718
|
-
req.params["scheduledMaintenanceId"] as string,
|
|
2719
|
-
);
|
|
2720
|
-
|
|
2721
|
-
const response: JSONObject = await this.getScheduledMaintenanceEvents(
|
|
2722
|
-
objectId,
|
|
2723
|
-
scheduledMaintenanceId,
|
|
2724
|
-
|
|
2725
|
-
req,
|
|
2726
|
-
);
|
|
2727
|
-
|
|
2728
|
-
return Response.sendJsonObjectResponse(req, res, response);
|
|
2729
|
-
} catch (err) {
|
|
2730
|
-
next(err);
|
|
2731
|
-
}
|
|
2732
|
-
},
|
|
2733
|
-
);
|
|
2734
|
-
|
|
2735
|
-
this.router.post(
|
|
2736
|
-
`${new this.entityType()
|
|
2737
|
-
.getCrudApiPath()
|
|
2738
|
-
?.toString()}/announcements/:statusPageIdOrDomain/:announcementId`,
|
|
2739
|
-
UserMiddleware.getUserMiddleware,
|
|
2740
|
-
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
2741
|
-
try {
|
|
2742
|
-
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
2743
|
-
req.params["statusPageIdOrDomain"] as string,
|
|
2744
|
-
);
|
|
2745
|
-
|
|
2746
|
-
const announcementId: ObjectID = new ObjectID(
|
|
2747
|
-
req.params["announcementId"] as string,
|
|
2748
|
-
);
|
|
2749
|
-
|
|
2750
|
-
const response: JSONObject = await this.getAnnouncements(
|
|
2751
|
-
objectId,
|
|
2752
|
-
announcementId,
|
|
2753
|
-
|
|
2754
|
-
req,
|
|
2755
|
-
);
|
|
2756
|
-
|
|
2757
|
-
return Response.sendJsonObjectResponse(req, res, response);
|
|
2758
|
-
} catch (err) {
|
|
2759
|
-
next(err);
|
|
2760
|
-
}
|
|
2761
|
-
},
|
|
2762
|
-
);
|
|
2763
|
-
|
|
2764
|
-
// Episodes endpoints
|
|
2765
|
-
this.router.post(
|
|
2766
|
-
`${new this.entityType()
|
|
2767
|
-
.getCrudApiPath()
|
|
2768
|
-
?.toString()}/episodes/:statusPageIdOrDomain`,
|
|
2769
|
-
UserMiddleware.getUserMiddleware,
|
|
2770
|
-
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
2771
|
-
try {
|
|
2772
|
-
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
2773
|
-
req.params["statusPageIdOrDomain"] as string,
|
|
2774
|
-
);
|
|
2775
|
-
|
|
2776
|
-
const response: JSONObject = await this.getEpisodes(
|
|
2777
|
-
objectId,
|
|
2778
|
-
null,
|
|
2779
|
-
req,
|
|
2780
|
-
);
|
|
2781
|
-
|
|
2782
|
-
return Response.sendJsonObjectResponse(req, res, response);
|
|
2783
|
-
} catch (err) {
|
|
2784
|
-
next(err);
|
|
2785
|
-
}
|
|
2786
|
-
},
|
|
2787
|
-
);
|
|
2788
|
-
|
|
2789
|
-
this.router.post(
|
|
2790
|
-
`${new this.entityType()
|
|
2791
|
-
.getCrudApiPath()
|
|
2792
|
-
?.toString()}/episodes/:statusPageIdOrDomain/:episodeId`,
|
|
2793
|
-
UserMiddleware.getUserMiddleware,
|
|
2794
|
-
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
2795
|
-
try {
|
|
2796
|
-
const objectId: ObjectID = await resolveStatusPageIdOrThrow(
|
|
2797
|
-
req.params["statusPageIdOrDomain"] as string,
|
|
2798
|
-
);
|
|
2799
|
-
|
|
2800
|
-
const episodeId: ObjectID = new ObjectID(
|
|
2801
|
-
req.params["episodeId"] as string,
|
|
2802
|
-
);
|
|
2803
|
-
|
|
2804
|
-
const response: JSONObject = await this.getEpisodes(
|
|
2805
|
-
objectId,
|
|
2806
|
-
episodeId,
|
|
2807
|
-
req,
|
|
2808
|
-
);
|
|
2809
|
-
|
|
2810
|
-
return Response.sendJsonObjectResponse(req, res, response);
|
|
2811
|
-
} catch (err) {
|
|
2812
|
-
next(err);
|
|
2813
|
-
}
|
|
2814
|
-
},
|
|
2815
|
-
);
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
|
-
@CaptureSpan()
|
|
2819
|
-
public async getScheduledMaintenanceEvents(
|
|
2820
|
-
statusPageId: ObjectID,
|
|
2821
|
-
scheduledMaintenanceId: ObjectID | null,
|
|
2822
|
-
req: ExpressRequest,
|
|
2823
|
-
): Promise<JSONObject> {
|
|
2824
|
-
await this.checkHasReadAccess({
|
|
2825
|
-
statusPageId: statusPageId,
|
|
2826
|
-
req: req,
|
|
2827
|
-
});
|
|
2029
|
+
@CaptureSpan()
|
|
2030
|
+
public async getScheduledMaintenanceEvents(
|
|
2031
|
+
statusPageId: ObjectID,
|
|
2032
|
+
scheduledMaintenanceId: ObjectID | null,
|
|
2033
|
+
req: ExpressRequest,
|
|
2034
|
+
): Promise<JSONObject> {
|
|
2035
|
+
await this.checkHasReadAccess({
|
|
2036
|
+
statusPageId: statusPageId,
|
|
2037
|
+
req: req,
|
|
2038
|
+
});
|
|
2828
2039
|
|
|
2829
2040
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
2830
2041
|
query: {
|
|
@@ -3757,403 +2968,681 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
3757
2968
|
getLogAttributesFromRequest(req as any),
|
|
3758
2969
|
);
|
|
3759
2970
|
throw new BadDataException(
|
|
3760
|
-
"Email subscribers not enabled for this status page.",
|
|
2971
|
+
"Email subscribers not enabled for this status page.",
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
if (req.body.data["subscriberPhone"] && !statusPage.enableSmsSubscribers) {
|
|
2976
|
+
logger.debug(
|
|
2977
|
+
`SMS subscribers not enabled for status page with ID: ${objectId}`,
|
|
2978
|
+
getLogAttributesFromRequest(req as any),
|
|
2979
|
+
);
|
|
2980
|
+
throw new BadDataException(
|
|
2981
|
+
"SMS subscribers not enabled for this status page.",
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// if no email or phone, throw error.
|
|
2986
|
+
|
|
2987
|
+
if (
|
|
2988
|
+
req.body.data["slackWorkspaceName"] &&
|
|
2989
|
+
!statusPage.enableSlackSubscribers
|
|
2990
|
+
) {
|
|
2991
|
+
logger.debug(
|
|
2992
|
+
`Slack subscribers not enabled for status page with ID: ${objectId}`,
|
|
2993
|
+
getLogAttributesFromRequest(req as any),
|
|
2994
|
+
);
|
|
2995
|
+
throw new BadDataException(
|
|
2996
|
+
"Slack subscribers not enabled for this status page.",
|
|
2997
|
+
);
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
if (
|
|
3001
|
+
req.body.data["microsoftTeamsWorkspaceName"] &&
|
|
3002
|
+
!statusPage.enableMicrosoftTeamsSubscribers
|
|
3003
|
+
) {
|
|
3004
|
+
logger.debug(
|
|
3005
|
+
`Microsoft Teams subscribers not enabled for status page with ID: ${objectId}`,
|
|
3006
|
+
getLogAttributesFromRequest(req as any),
|
|
3007
|
+
);
|
|
3008
|
+
throw new BadDataException(
|
|
3009
|
+
"Microsoft Teams subscribers not enabled for this status page.",
|
|
3010
|
+
);
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
if (
|
|
3014
|
+
req.body.data["subscriberWebhook"] &&
|
|
3015
|
+
!statusPage.enableWebhookSubscribers
|
|
3016
|
+
) {
|
|
3017
|
+
logger.debug(
|
|
3018
|
+
`Webhook subscribers not enabled for status page with ID: ${objectId}`,
|
|
3019
|
+
getLogAttributesFromRequest(req as any),
|
|
3020
|
+
);
|
|
3021
|
+
throw new BadDataException(
|
|
3022
|
+
"Webhook subscribers not enabled for this status page.",
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
if (
|
|
3027
|
+
!req.params["subscriberId"] &&
|
|
3028
|
+
!req.body.data["subscriberEmail"] &&
|
|
3029
|
+
!req.body.data["subscriberPhone"] &&
|
|
3030
|
+
!req.body.data["slackWorkspaceName"] &&
|
|
3031
|
+
!req.body.data["microsoftTeamsWorkspaceName"] &&
|
|
3032
|
+
!req.body.data["subscriberWebhook"]
|
|
3033
|
+
) {
|
|
3034
|
+
logger.debug(
|
|
3035
|
+
`No email, phone, slack workspace name, Microsoft Teams workspace name, or webhook URL provided for subscription to status page with ID: ${objectId}`,
|
|
3036
|
+
getLogAttributesFromRequest(req as any),
|
|
3037
|
+
);
|
|
3038
|
+
throw new BadDataException(
|
|
3039
|
+
"Email, phone, slack workspace name, Microsoft Teams workspace name, or webhook URL is required to subscribe to this status page.",
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
const email: Email | undefined = req.body.data["subscriberEmail"]
|
|
3044
|
+
? new Email(req.body.data["subscriberEmail"] as string)
|
|
3045
|
+
: undefined;
|
|
3046
|
+
|
|
3047
|
+
const phone: Phone | undefined = req.body.data["subscriberPhone"]
|
|
3048
|
+
? new Phone(req.body.data["subscriberPhone"] as string)
|
|
3049
|
+
: undefined;
|
|
3050
|
+
|
|
3051
|
+
const slackIncomingWebhookUrl: string | undefined = req.body.data[
|
|
3052
|
+
"slackIncomingWebhookUrl"
|
|
3053
|
+
]
|
|
3054
|
+
? (req.body.data["slackIncomingWebhookUrl"] as string)
|
|
3055
|
+
: undefined;
|
|
3056
|
+
|
|
3057
|
+
const slackWorkspaceName: string | undefined = req.body.data[
|
|
3058
|
+
"slackWorkspaceName"
|
|
3059
|
+
]
|
|
3060
|
+
? (req.body.data["slackWorkspaceName"] as string)
|
|
3061
|
+
: undefined;
|
|
3062
|
+
|
|
3063
|
+
const microsoftTeamsIncomingWebhookUrl: string | undefined = req.body.data[
|
|
3064
|
+
"microsoftTeamsIncomingWebhookUrl"
|
|
3065
|
+
]
|
|
3066
|
+
? (req.body.data["microsoftTeamsIncomingWebhookUrl"] as string)
|
|
3067
|
+
: undefined;
|
|
3068
|
+
|
|
3069
|
+
const microsoftTeamsWorkspaceName: string | undefined = req.body.data[
|
|
3070
|
+
"microsoftTeamsWorkspaceName"
|
|
3071
|
+
]
|
|
3072
|
+
? (req.body.data["microsoftTeamsWorkspaceName"] as string)
|
|
3073
|
+
: undefined;
|
|
3074
|
+
|
|
3075
|
+
const subscriberWebhookUrl: string | undefined = req.body.data[
|
|
3076
|
+
"subscriberWebhook"
|
|
3077
|
+
]
|
|
3078
|
+
? (req.body.data["subscriberWebhook"] as string)
|
|
3079
|
+
: undefined;
|
|
3080
|
+
|
|
3081
|
+
let statusPageSubscriber: StatusPageSubscriber | null = null;
|
|
3082
|
+
|
|
3083
|
+
let isUpdate: boolean = false;
|
|
3084
|
+
|
|
3085
|
+
if (!req.params["subscriberId"]) {
|
|
3086
|
+
logger.debug(
|
|
3087
|
+
`Creating new subscriber for status page with ID: ${objectId}`,
|
|
3088
|
+
getLogAttributesFromRequest(req as any),
|
|
3089
|
+
);
|
|
3090
|
+
statusPageSubscriber = new StatusPageSubscriber();
|
|
3091
|
+
} else {
|
|
3092
|
+
const subscriberId: ObjectID = new ObjectID(
|
|
3093
|
+
req.params["subscriberId"] as string,
|
|
3094
|
+
);
|
|
3095
|
+
|
|
3096
|
+
logger.debug(
|
|
3097
|
+
`Updating existing subscriber with ID: ${subscriberId} for status page with ID: ${objectId}`,
|
|
3098
|
+
getLogAttributesFromRequest(req as any),
|
|
3099
|
+
);
|
|
3100
|
+
/*
|
|
3101
|
+
* Scope the lookup to the status page in the route. This endpoint only
|
|
3102
|
+
* checks read access on that status page, so without the statusPageId
|
|
3103
|
+
* clause anyone holding a subscriber UUID could pass any status page they
|
|
3104
|
+
* can read and edit a subscriber belonging to a different — possibly
|
|
3105
|
+
* private — status page. A subscriber on another page must be
|
|
3106
|
+
* indistinguishable from one that does not exist, so that the shared
|
|
3107
|
+
* "Subscriber not found" below cannot be used to probe for valid UUIDs.
|
|
3108
|
+
*/
|
|
3109
|
+
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
|
|
3110
|
+
query: {
|
|
3111
|
+
_id: subscriberId.toString(),
|
|
3112
|
+
statusPageId: objectId,
|
|
3113
|
+
},
|
|
3114
|
+
props: {
|
|
3115
|
+
isRoot: true,
|
|
3116
|
+
},
|
|
3117
|
+
});
|
|
3118
|
+
|
|
3119
|
+
if (!statusPageSubscriber) {
|
|
3120
|
+
logger.debug(
|
|
3121
|
+
`Subscriber not found with ID: ${subscriberId} on status page with ID: ${objectId}`,
|
|
3122
|
+
getLogAttributesFromRequest(req as any),
|
|
3123
|
+
);
|
|
3124
|
+
throw new BadDataException("Subscriber not found");
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
isUpdate = true;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
if (email) {
|
|
3131
|
+
logger.debug(
|
|
3132
|
+
`Setting subscriber email: ${email}`,
|
|
3133
|
+
getLogAttributesFromRequest(req as any),
|
|
3134
|
+
);
|
|
3135
|
+
statusPageSubscriber.subscriberEmail = email;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
if (phone) {
|
|
3139
|
+
logger.debug(
|
|
3140
|
+
`Setting subscriber phone: ${phone}`,
|
|
3141
|
+
getLogAttributesFromRequest(req as any),
|
|
3142
|
+
);
|
|
3143
|
+
statusPageSubscriber.subscriberPhone = phone;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
if (slackIncomingWebhookUrl) {
|
|
3147
|
+
logger.debug(
|
|
3148
|
+
`Setting subscriber slack: ${slackIncomingWebhookUrl}`,
|
|
3149
|
+
getLogAttributesFromRequest(req as any),
|
|
3150
|
+
);
|
|
3151
|
+
statusPageSubscriber.slackIncomingWebhookUrl = URL.fromString(
|
|
3152
|
+
slackIncomingWebhookUrl,
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
if (slackWorkspaceName) {
|
|
3157
|
+
logger.debug(
|
|
3158
|
+
`Setting subscriber slack workspace name: ${slackWorkspaceName}`,
|
|
3159
|
+
getLogAttributesFromRequest(req as any),
|
|
3160
|
+
);
|
|
3161
|
+
statusPageSubscriber.slackWorkspaceName = slackWorkspaceName;
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
if (microsoftTeamsIncomingWebhookUrl) {
|
|
3165
|
+
logger.debug(
|
|
3166
|
+
`Setting subscriber Microsoft Teams webhook: ${microsoftTeamsIncomingWebhookUrl}`,
|
|
3167
|
+
getLogAttributesFromRequest(req as any),
|
|
3168
|
+
);
|
|
3169
|
+
statusPageSubscriber.microsoftTeamsIncomingWebhookUrl = URL.fromString(
|
|
3170
|
+
microsoftTeamsIncomingWebhookUrl,
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
if (microsoftTeamsWorkspaceName) {
|
|
3175
|
+
logger.debug(
|
|
3176
|
+
`Setting subscriber Microsoft Teams workspace name: ${microsoftTeamsWorkspaceName}`,
|
|
3177
|
+
getLogAttributesFromRequest(req as any),
|
|
3178
|
+
);
|
|
3179
|
+
statusPageSubscriber.microsoftTeamsWorkspaceName =
|
|
3180
|
+
microsoftTeamsWorkspaceName;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
if (subscriberWebhookUrl) {
|
|
3184
|
+
logger.debug(
|
|
3185
|
+
`Setting subscriber webhook URL: ${subscriberWebhookUrl}`,
|
|
3186
|
+
getLogAttributesFromRequest(req as any),
|
|
3187
|
+
);
|
|
3188
|
+
statusPageSubscriber.subscriberWebhook =
|
|
3189
|
+
URL.fromString(subscriberWebhookUrl);
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
if (
|
|
3193
|
+
!isUpdate &&
|
|
3194
|
+
req.body.data["statusPageResources"] &&
|
|
3195
|
+
!statusPage.allowSubscribersToChooseResources
|
|
3196
|
+
) {
|
|
3197
|
+
logger.debug(
|
|
3198
|
+
`Subscribers not allowed to choose resources for status page with ID: ${objectId}`,
|
|
3199
|
+
getLogAttributesFromRequest(req as any),
|
|
3200
|
+
);
|
|
3201
|
+
throw new BadDataException(
|
|
3202
|
+
"Subscribers are not allowed to choose resources for this status page.",
|
|
3761
3203
|
);
|
|
3762
3204
|
}
|
|
3763
3205
|
|
|
3764
|
-
if (
|
|
3206
|
+
if (
|
|
3207
|
+
!isUpdate &&
|
|
3208
|
+
req.body.data["statusPageEventTypes"] &&
|
|
3209
|
+
!statusPage.allowSubscribersToChooseEventTypes
|
|
3210
|
+
) {
|
|
3765
3211
|
logger.debug(
|
|
3766
|
-
`
|
|
3212
|
+
`Subscribers not allowed to choose event types for status page with ID: ${objectId}`,
|
|
3767
3213
|
getLogAttributesFromRequest(req as any),
|
|
3768
3214
|
);
|
|
3769
3215
|
throw new BadDataException(
|
|
3770
|
-
"
|
|
3216
|
+
"Subscribers are not allowed to choose event types for this status page.",
|
|
3771
3217
|
);
|
|
3772
3218
|
}
|
|
3773
3219
|
|
|
3774
|
-
|
|
3220
|
+
statusPageSubscriber.statusPageId = objectId;
|
|
3221
|
+
statusPageSubscriber.sendYouHaveSubscribedMessage = true;
|
|
3222
|
+
statusPageSubscriber.projectId = statusPage.projectId!;
|
|
3223
|
+
statusPageSubscriber.isSubscribedToAllResources = Boolean(
|
|
3224
|
+
req.body.data["isSubscribedToAllResources"],
|
|
3225
|
+
);
|
|
3226
|
+
|
|
3227
|
+
statusPageSubscriber.isSubscribedToAllEventTypes = Boolean(
|
|
3228
|
+
req.body.data["isSubscribedToAllEventTypes"],
|
|
3229
|
+
);
|
|
3775
3230
|
|
|
3776
3231
|
if (
|
|
3777
|
-
req.body.data["
|
|
3778
|
-
|
|
3232
|
+
req.body.data["statusPageResources"] &&
|
|
3233
|
+
req.body.data["statusPageResources"].length > 0
|
|
3779
3234
|
) {
|
|
3780
3235
|
logger.debug(
|
|
3781
|
-
`
|
|
3236
|
+
`Setting subscriber resources: ${JSON.stringify(req.body.data["statusPageResources"])}`,
|
|
3782
3237
|
getLogAttributesFromRequest(req as any),
|
|
3783
3238
|
);
|
|
3784
|
-
|
|
3785
|
-
"
|
|
3786
|
-
|
|
3239
|
+
statusPageSubscriber.statusPageResources = req.body.data[
|
|
3240
|
+
"statusPageResources"
|
|
3241
|
+
] as Array<StatusPageResource>;
|
|
3787
3242
|
}
|
|
3788
3243
|
|
|
3789
3244
|
if (
|
|
3790
|
-
req.body.data["
|
|
3791
|
-
|
|
3245
|
+
req.body.data["statusPageEventTypes"] &&
|
|
3246
|
+
req.body.data["statusPageEventTypes"].length > 0
|
|
3792
3247
|
) {
|
|
3793
3248
|
logger.debug(
|
|
3794
|
-
`
|
|
3249
|
+
`Setting subscriber event types: ${JSON.stringify(req.body.data["statusPageEventTypes"])}`,
|
|
3795
3250
|
getLogAttributesFromRequest(req as any),
|
|
3796
3251
|
);
|
|
3797
|
-
|
|
3798
|
-
"
|
|
3799
|
-
|
|
3252
|
+
statusPageSubscriber.statusPageEventTypes = req.body.data[
|
|
3253
|
+
"statusPageEventTypes"
|
|
3254
|
+
] as Array<StatusPageEventType>;
|
|
3800
3255
|
}
|
|
3801
3256
|
|
|
3802
|
-
if (
|
|
3803
|
-
|
|
3804
|
-
!statusPage.enableWebhookSubscribers
|
|
3805
|
-
) {
|
|
3257
|
+
if (isUpdate) {
|
|
3258
|
+
// check isUnsubscribed is set to false.
|
|
3806
3259
|
logger.debug(
|
|
3807
|
-
`
|
|
3260
|
+
`Updating subscriber with ID: ${statusPageSubscriber.id}`,
|
|
3808
3261
|
getLogAttributesFromRequest(req as any),
|
|
3809
3262
|
);
|
|
3810
|
-
|
|
3811
|
-
|
|
3263
|
+
statusPageSubscriber.isUnsubscribed = Boolean(
|
|
3264
|
+
req.body.data["isUnsubscribed"],
|
|
3812
3265
|
);
|
|
3813
|
-
}
|
|
3814
3266
|
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3267
|
+
await StatusPageSubscriberService.updateOneById({
|
|
3268
|
+
id: statusPageSubscriber.id!,
|
|
3269
|
+
data: {
|
|
3270
|
+
statusPageResources: statusPageSubscriber.statusPageResources!,
|
|
3271
|
+
isSubscribedToAllResources:
|
|
3272
|
+
statusPageSubscriber.isSubscribedToAllResources!,
|
|
3273
|
+
statusPageEventTypes: statusPageSubscriber.statusPageEventTypes!,
|
|
3274
|
+
isSubscribedToAllEventTypes:
|
|
3275
|
+
statusPageSubscriber.isSubscribedToAllEventTypes!,
|
|
3276
|
+
isUnsubscribed: statusPageSubscriber.isUnsubscribed,
|
|
3277
|
+
} as any,
|
|
3278
|
+
props: {
|
|
3279
|
+
isRoot: true,
|
|
3280
|
+
},
|
|
3281
|
+
});
|
|
3282
|
+
} else {
|
|
3823
3283
|
logger.debug(
|
|
3824
|
-
`
|
|
3284
|
+
`Creating new subscriber: ${JSON.stringify(statusPageSubscriber)}`,
|
|
3825
3285
|
getLogAttributesFromRequest(req as any),
|
|
3826
3286
|
);
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3287
|
+
await StatusPageSubscriberService.create({
|
|
3288
|
+
data: statusPageSubscriber,
|
|
3289
|
+
props: {
|
|
3290
|
+
isRoot: true,
|
|
3291
|
+
},
|
|
3292
|
+
});
|
|
3830
3293
|
}
|
|
3831
3294
|
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
? new Phone(req.body.data["subscriberPhone"] as string)
|
|
3838
|
-
: undefined;
|
|
3839
|
-
|
|
3840
|
-
const slackIncomingWebhookUrl: string | undefined = req.body.data[
|
|
3841
|
-
"slackIncomingWebhookUrl"
|
|
3842
|
-
]
|
|
3843
|
-
? (req.body.data["slackIncomingWebhookUrl"] as string)
|
|
3844
|
-
: undefined;
|
|
3845
|
-
|
|
3846
|
-
const slackWorkspaceName: string | undefined = req.body.data[
|
|
3847
|
-
"slackWorkspaceName"
|
|
3848
|
-
]
|
|
3849
|
-
? (req.body.data["slackWorkspaceName"] as string)
|
|
3850
|
-
: undefined;
|
|
3851
|
-
|
|
3852
|
-
const microsoftTeamsIncomingWebhookUrl: string | undefined = req.body.data[
|
|
3853
|
-
"microsoftTeamsIncomingWebhookUrl"
|
|
3854
|
-
]
|
|
3855
|
-
? (req.body.data["microsoftTeamsIncomingWebhookUrl"] as string)
|
|
3856
|
-
: undefined;
|
|
3295
|
+
logger.debug(
|
|
3296
|
+
`Subscription process completed for status page with ID: ${objectId}`,
|
|
3297
|
+
getLogAttributesFromRequest(req as any),
|
|
3298
|
+
);
|
|
3299
|
+
}
|
|
3857
3300
|
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3301
|
+
@CaptureSpan()
|
|
3302
|
+
public async getSubscriber(
|
|
3303
|
+
req: ExpressRequest,
|
|
3304
|
+
): Promise<StatusPageSubscriber> {
|
|
3305
|
+
const objectId: ObjectID = new ObjectID(
|
|
3306
|
+
req.params["statusPageId"] as string,
|
|
3307
|
+
);
|
|
3863
3308
|
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
: undefined;
|
|
3309
|
+
await this.checkHasReadAccess({
|
|
3310
|
+
statusPageId: objectId,
|
|
3311
|
+
req: req,
|
|
3312
|
+
});
|
|
3869
3313
|
|
|
3870
|
-
|
|
3314
|
+
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
3315
|
+
query: {
|
|
3316
|
+
_id: objectId.toString(),
|
|
3317
|
+
},
|
|
3318
|
+
select: {
|
|
3319
|
+
_id: true,
|
|
3320
|
+
projectId: true,
|
|
3321
|
+
},
|
|
3322
|
+
props: {
|
|
3323
|
+
isRoot: true,
|
|
3324
|
+
},
|
|
3325
|
+
});
|
|
3871
3326
|
|
|
3872
|
-
|
|
3327
|
+
if (!statusPage) {
|
|
3328
|
+
throw new BadDataException("Status Page not found");
|
|
3329
|
+
}
|
|
3873
3330
|
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
getLogAttributesFromRequest(req as any),
|
|
3878
|
-
);
|
|
3879
|
-
statusPageSubscriber = new StatusPageSubscriber();
|
|
3880
|
-
} else {
|
|
3881
|
-
const subscriberId: ObjectID = new ObjectID(
|
|
3882
|
-
req.params["subscriberId"] as string,
|
|
3883
|
-
);
|
|
3331
|
+
const subscriberId: ObjectID = new ObjectID(
|
|
3332
|
+
req.params["subscriberId"] as string,
|
|
3333
|
+
);
|
|
3884
3334
|
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
getLogAttributesFromRequest(req as any),
|
|
3888
|
-
);
|
|
3889
|
-
/*
|
|
3890
|
-
* Scope the lookup to the status page in the route. This endpoint only
|
|
3891
|
-
* checks read access on that status page, so without the statusPageId
|
|
3892
|
-
* clause anyone holding a subscriber UUID could pass any status page they
|
|
3893
|
-
* can read and edit a subscriber belonging to a different — possibly
|
|
3894
|
-
* private — status page. A subscriber on another page must be
|
|
3895
|
-
* indistinguishable from one that does not exist, so that the shared
|
|
3896
|
-
* "Subscriber not found" below cannot be used to probe for valid UUIDs.
|
|
3897
|
-
*/
|
|
3898
|
-
statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
|
|
3335
|
+
const statusPageSubscriber: StatusPageSubscriber | null =
|
|
3336
|
+
await StatusPageSubscriberService.findOneBy({
|
|
3899
3337
|
query: {
|
|
3900
3338
|
_id: subscriberId.toString(),
|
|
3901
|
-
statusPageId:
|
|
3339
|
+
statusPageId: statusPage.id!,
|
|
3340
|
+
},
|
|
3341
|
+
select: {
|
|
3342
|
+
isUnsubscribed: true,
|
|
3343
|
+
subscriberEmail: true,
|
|
3344
|
+
subscriberPhone: true,
|
|
3345
|
+
slackWorkspaceName: true,
|
|
3346
|
+
statusPageId: true,
|
|
3347
|
+
statusPageResources: true,
|
|
3348
|
+
isSubscribedToAllResources: true,
|
|
3349
|
+
statusPageEventTypes: true,
|
|
3350
|
+
isSubscribedToAllEventTypes: true,
|
|
3902
3351
|
},
|
|
3903
3352
|
props: {
|
|
3904
3353
|
isRoot: true,
|
|
3905
3354
|
},
|
|
3906
3355
|
});
|
|
3907
3356
|
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3357
|
+
if (!statusPageSubscriber) {
|
|
3358
|
+
throw new BadDataException("Subscriber not found");
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
return statusPageSubscriber;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
@CaptureSpan()
|
|
3365
|
+
public async getIncidents(
|
|
3366
|
+
statusPageId: ObjectID,
|
|
3367
|
+
incidentId: ObjectID | null,
|
|
3368
|
+
req: ExpressRequest,
|
|
3369
|
+
): Promise<JSONObject> {
|
|
3370
|
+
await this.checkHasReadAccess({
|
|
3371
|
+
statusPageId: statusPageId,
|
|
3372
|
+
req: req,
|
|
3373
|
+
});
|
|
3374
|
+
|
|
3375
|
+
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
3376
|
+
query: {
|
|
3377
|
+
_id: statusPageId.toString(),
|
|
3378
|
+
},
|
|
3379
|
+
select: {
|
|
3380
|
+
_id: true,
|
|
3381
|
+
projectId: true,
|
|
3382
|
+
showIncidentHistoryInDays: true,
|
|
3383
|
+
showIncidentLabelsOnStatusPage: true,
|
|
3384
|
+
showIncidentsOnStatusPage: true,
|
|
3385
|
+
},
|
|
3386
|
+
props: {
|
|
3387
|
+
isRoot: true,
|
|
3388
|
+
},
|
|
3389
|
+
});
|
|
3390
|
+
|
|
3391
|
+
if (!statusPage) {
|
|
3392
|
+
throw new BadDataException("Status Page not found");
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
if (!statusPage.showIncidentsOnStatusPage) {
|
|
3396
|
+
throw new BadDataException(
|
|
3397
|
+
"Incidents are not enabled on this status page.",
|
|
3398
|
+
);
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
// get monitors on status page.
|
|
3402
|
+
const statusPageResources: Array<StatusPageResource> =
|
|
3403
|
+
await StatusPageService.getStatusPageResources({
|
|
3404
|
+
statusPageId: statusPageId,
|
|
3405
|
+
});
|
|
3406
|
+
|
|
3407
|
+
const { monitorsOnStatusPage, monitorsInGroup } =
|
|
3408
|
+
await StatusPageService.getMonitorIdsOnStatusPage({
|
|
3409
|
+
statusPageId: statusPageId,
|
|
3410
|
+
// reuse the resources fetched above instead of re-querying them
|
|
3411
|
+
statusPageResources: statusPageResources,
|
|
3412
|
+
});
|
|
3915
3413
|
|
|
3916
|
-
|
|
3917
|
-
}
|
|
3414
|
+
const today: Date = OneUptimeDate.getCurrentDate();
|
|
3918
3415
|
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
getLogAttributesFromRequest(req as any),
|
|
3923
|
-
);
|
|
3924
|
-
statusPageSubscriber.subscriberEmail = email;
|
|
3925
|
-
}
|
|
3416
|
+
const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
|
|
3417
|
+
statusPage.showIncidentHistoryInDays || 14,
|
|
3418
|
+
);
|
|
3926
3419
|
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
}
|
|
3420
|
+
let incidentQuery: Query<Incident> = {
|
|
3421
|
+
monitors: monitorsOnStatusPage as any,
|
|
3422
|
+
projectId: statusPage.projectId!,
|
|
3423
|
+
createdAt: QueryHelper.inBetween(historyDays, today),
|
|
3424
|
+
isVisibleOnStatusPage: true,
|
|
3425
|
+
};
|
|
3934
3426
|
|
|
3935
|
-
if (
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
);
|
|
3427
|
+
if (incidentId) {
|
|
3428
|
+
incidentQuery = {
|
|
3429
|
+
monitors: monitorsOnStatusPage as any,
|
|
3430
|
+
projectId: statusPage.projectId!,
|
|
3431
|
+
_id: incidentId.toString(),
|
|
3432
|
+
isVisibleOnStatusPage: true,
|
|
3433
|
+
};
|
|
3943
3434
|
}
|
|
3944
3435
|
|
|
3945
|
-
if
|
|
3946
|
-
|
|
3947
|
-
`Setting subscriber slack workspace name: ${slackWorkspaceName}`,
|
|
3948
|
-
getLogAttributesFromRequest(req as any),
|
|
3949
|
-
);
|
|
3950
|
-
statusPageSubscriber.slackWorkspaceName = slackWorkspaceName;
|
|
3951
|
-
}
|
|
3436
|
+
// check if status page has active incident.
|
|
3437
|
+
let incidents: Array<Incident> = [];
|
|
3952
3438
|
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3439
|
+
let selectIncidents: Select<Incident> = {
|
|
3440
|
+
createdAt: true,
|
|
3441
|
+
declaredAt: true,
|
|
3442
|
+
updatedAt: true,
|
|
3443
|
+
title: true,
|
|
3444
|
+
description: true,
|
|
3445
|
+
_id: true,
|
|
3446
|
+
postmortemNote: true,
|
|
3447
|
+
postmortemPostedAt: true,
|
|
3448
|
+
showPostmortemOnStatusPage: true,
|
|
3449
|
+
postmortemAttachments: {
|
|
3450
|
+
_id: true,
|
|
3451
|
+
name: true,
|
|
3452
|
+
},
|
|
3453
|
+
incidentSeverity: {
|
|
3454
|
+
name: true,
|
|
3455
|
+
color: true,
|
|
3456
|
+
},
|
|
3457
|
+
currentIncidentState: {
|
|
3458
|
+
name: true,
|
|
3459
|
+
color: true,
|
|
3460
|
+
_id: true,
|
|
3461
|
+
order: true,
|
|
3462
|
+
},
|
|
3463
|
+
monitors: {
|
|
3464
|
+
_id: true,
|
|
3465
|
+
},
|
|
3466
|
+
};
|
|
3962
3467
|
|
|
3963
|
-
if (
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3468
|
+
if (statusPage.showIncidentLabelsOnStatusPage) {
|
|
3469
|
+
selectIncidents = {
|
|
3470
|
+
...selectIncidents,
|
|
3471
|
+
labels: {
|
|
3472
|
+
name: true,
|
|
3473
|
+
color: true,
|
|
3474
|
+
},
|
|
3475
|
+
};
|
|
3970
3476
|
}
|
|
3971
3477
|
|
|
3972
|
-
if (
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3478
|
+
if (monitorsOnStatusPage.length > 0) {
|
|
3479
|
+
incidents = await IncidentService.findBy({
|
|
3480
|
+
query: incidentQuery,
|
|
3481
|
+
select: selectIncidents,
|
|
3482
|
+
sort: {
|
|
3483
|
+
declaredAt: SortOrder.Descending,
|
|
3484
|
+
createdAt: SortOrder.Descending,
|
|
3485
|
+
},
|
|
3486
|
+
skip: 0,
|
|
3487
|
+
limit: LIMIT_PER_PROJECT,
|
|
3488
|
+
props: {
|
|
3489
|
+
isRoot: true,
|
|
3490
|
+
},
|
|
3491
|
+
});
|
|
3980
3492
|
|
|
3981
|
-
|
|
3982
|
-
!isUpdate &&
|
|
3983
|
-
req.body.data["statusPageResources"] &&
|
|
3984
|
-
!statusPage.allowSubscribersToChooseResources
|
|
3985
|
-
) {
|
|
3986
|
-
logger.debug(
|
|
3987
|
-
`Subscribers not allowed to choose resources for status page with ID: ${objectId}`,
|
|
3988
|
-
getLogAttributesFromRequest(req as any),
|
|
3989
|
-
);
|
|
3990
|
-
throw new BadDataException(
|
|
3991
|
-
"Subscribers are not allowed to choose resources for this status page.",
|
|
3992
|
-
);
|
|
3993
|
-
}
|
|
3493
|
+
let activeIncidents: Array<Incident> = [];
|
|
3994
3494
|
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
getLogAttributesFromRequest(req as any),
|
|
4003
|
-
);
|
|
4004
|
-
throw new BadDataException(
|
|
4005
|
-
"Subscribers are not allowed to choose event types for this status page.",
|
|
4006
|
-
);
|
|
4007
|
-
}
|
|
3495
|
+
const unresolvedIncidentStates: Array<IncidentState> =
|
|
3496
|
+
await IncidentStateService.getUnresolvedIncidentStates(
|
|
3497
|
+
statusPage.projectId!,
|
|
3498
|
+
{
|
|
3499
|
+
isRoot: true,
|
|
3500
|
+
},
|
|
3501
|
+
);
|
|
4008
3502
|
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
req.body.data["isSubscribedToAllResources"],
|
|
4014
|
-
);
|
|
3503
|
+
const unresolvbedIncidentStateIds: Array<ObjectID> =
|
|
3504
|
+
unresolvedIncidentStates.map((state: IncidentState) => {
|
|
3505
|
+
return state.id!;
|
|
3506
|
+
});
|
|
4015
3507
|
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
3508
|
+
// If there is no particular incident id to fetch then fetch active incidents.
|
|
3509
|
+
if (!incidentId) {
|
|
3510
|
+
activeIncidents = await IncidentService.findBy({
|
|
3511
|
+
query: {
|
|
3512
|
+
monitors: monitorsOnStatusPage as any,
|
|
3513
|
+
isVisibleOnStatusPage: true,
|
|
3514
|
+
currentIncidentStateId: QueryHelper.any(
|
|
3515
|
+
unresolvbedIncidentStateIds,
|
|
3516
|
+
),
|
|
3517
|
+
projectId: statusPage.projectId!,
|
|
3518
|
+
},
|
|
3519
|
+
select: selectIncidents,
|
|
3520
|
+
sort: {
|
|
3521
|
+
declaredAt: SortOrder.Descending,
|
|
3522
|
+
createdAt: SortOrder.Descending,
|
|
3523
|
+
},
|
|
4019
3524
|
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
);
|
|
4028
|
-
statusPageSubscriber.statusPageResources = req.body.data[
|
|
4029
|
-
"statusPageResources"
|
|
4030
|
-
] as Array<StatusPageResource>;
|
|
4031
|
-
}
|
|
3525
|
+
skip: 0,
|
|
3526
|
+
limit: LIMIT_PER_PROJECT,
|
|
3527
|
+
props: {
|
|
3528
|
+
isRoot: true,
|
|
3529
|
+
},
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
4032
3532
|
|
|
4033
|
-
|
|
4034
|
-
req.body.data["statusPageEventTypes"] &&
|
|
4035
|
-
req.body.data["statusPageEventTypes"].length > 0
|
|
4036
|
-
) {
|
|
4037
|
-
logger.debug(
|
|
4038
|
-
`Setting subscriber event types: ${JSON.stringify(req.body.data["statusPageEventTypes"])}`,
|
|
4039
|
-
getLogAttributesFromRequest(req as any),
|
|
4040
|
-
);
|
|
4041
|
-
statusPageSubscriber.statusPageEventTypes = req.body.data[
|
|
4042
|
-
"statusPageEventTypes"
|
|
4043
|
-
] as Array<StatusPageEventType>;
|
|
4044
|
-
}
|
|
3533
|
+
incidents = [...activeIncidents, ...incidents];
|
|
4045
3534
|
|
|
4046
|
-
|
|
4047
|
-
// check isUnsubscribed is set to false.
|
|
4048
|
-
logger.debug(
|
|
4049
|
-
`Updating subscriber with ID: ${statusPageSubscriber.id}`,
|
|
4050
|
-
getLogAttributesFromRequest(req as any),
|
|
4051
|
-
);
|
|
4052
|
-
statusPageSubscriber.isUnsubscribed = Boolean(
|
|
4053
|
-
req.body.data["isUnsubscribed"],
|
|
4054
|
-
);
|
|
3535
|
+
// get distinct by id.
|
|
4055
3536
|
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
3537
|
+
incidents = ArrayUtil.distinctByFieldName(incidents, "_id");
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
const incidentsOnStatusPage: Array<ObjectID> = incidents.map(
|
|
3541
|
+
(incident: Incident) => {
|
|
3542
|
+
return incident.id!;
|
|
3543
|
+
},
|
|
3544
|
+
);
|
|
3545
|
+
|
|
3546
|
+
let incidentPublicNotes: Array<IncidentPublicNote> = [];
|
|
3547
|
+
|
|
3548
|
+
if (incidentsOnStatusPage.length > 0) {
|
|
3549
|
+
incidentPublicNotes = await IncidentPublicNoteService.findBy({
|
|
3550
|
+
query: {
|
|
3551
|
+
incidentId: QueryHelper.any(incidentsOnStatusPage),
|
|
3552
|
+
projectId: statusPage.projectId!,
|
|
4069
3553
|
},
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
3554
|
+
select: {
|
|
3555
|
+
postedAt: true,
|
|
3556
|
+
note: true,
|
|
3557
|
+
incidentId: true,
|
|
3558
|
+
attachments: {
|
|
3559
|
+
_id: true,
|
|
3560
|
+
name: true,
|
|
3561
|
+
},
|
|
3562
|
+
},
|
|
3563
|
+
sort: {
|
|
3564
|
+
postedAt: SortOrder.Descending, // new note first
|
|
3565
|
+
},
|
|
3566
|
+
skip: 0,
|
|
3567
|
+
limit: LIMIT_PER_PROJECT,
|
|
4078
3568
|
props: {
|
|
4079
3569
|
isRoot: true,
|
|
4080
3570
|
},
|
|
4081
3571
|
});
|
|
4082
3572
|
}
|
|
4083
3573
|
|
|
4084
|
-
|
|
4085
|
-
`Subscription process completed for status page with ID: ${objectId}`,
|
|
4086
|
-
getLogAttributesFromRequest(req as any),
|
|
4087
|
-
);
|
|
4088
|
-
}
|
|
4089
|
-
|
|
4090
|
-
@CaptureSpan()
|
|
4091
|
-
public async getSubscriber(
|
|
4092
|
-
req: ExpressRequest,
|
|
4093
|
-
): Promise<StatusPageSubscriber> {
|
|
4094
|
-
const objectId: ObjectID = new ObjectID(
|
|
4095
|
-
req.params["statusPageId"] as string,
|
|
4096
|
-
);
|
|
4097
|
-
|
|
4098
|
-
await this.checkHasReadAccess({
|
|
4099
|
-
statusPageId: objectId,
|
|
4100
|
-
req: req,
|
|
4101
|
-
});
|
|
3574
|
+
let incidentStateTimelines: Array<IncidentStateTimeline> = [];
|
|
4102
3575
|
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
3576
|
+
if (incidentsOnStatusPage.length > 0) {
|
|
3577
|
+
incidentStateTimelines = await IncidentStateTimelineService.findBy({
|
|
3578
|
+
query: {
|
|
3579
|
+
incidentId: QueryHelper.any(incidentsOnStatusPage),
|
|
3580
|
+
projectId: statusPage.projectId!,
|
|
3581
|
+
},
|
|
3582
|
+
select: {
|
|
3583
|
+
_id: true,
|
|
3584
|
+
createdAt: true,
|
|
3585
|
+
startsAt: true,
|
|
3586
|
+
incidentId: true,
|
|
3587
|
+
incidentState: {
|
|
3588
|
+
name: true,
|
|
3589
|
+
color: true,
|
|
3590
|
+
},
|
|
3591
|
+
},
|
|
3592
|
+
sort: {
|
|
3593
|
+
startsAt: SortOrder.Descending, // newer state changes first
|
|
3594
|
+
},
|
|
4115
3595
|
|
|
4116
|
-
|
|
4117
|
-
|
|
3596
|
+
skip: 0,
|
|
3597
|
+
limit: LIMIT_PER_PROJECT,
|
|
3598
|
+
props: {
|
|
3599
|
+
isRoot: true,
|
|
3600
|
+
},
|
|
3601
|
+
});
|
|
4118
3602
|
}
|
|
4119
3603
|
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
const statusPageSubscriber: StatusPageSubscriber | null =
|
|
4125
|
-
await StatusPageSubscriberService.findOneBy({
|
|
3604
|
+
// get all the incident states for this project.
|
|
3605
|
+
const incidentStates: Array<IncidentState> =
|
|
3606
|
+
await IncidentStateService.findBy({
|
|
4126
3607
|
query: {
|
|
4127
|
-
|
|
4128
|
-
statusPageId: statusPage.id!,
|
|
3608
|
+
projectId: statusPage.projectId!,
|
|
4129
3609
|
},
|
|
4130
3610
|
select: {
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
subscriberPhone: true,
|
|
4134
|
-
slackWorkspaceName: true,
|
|
4135
|
-
statusPageId: true,
|
|
4136
|
-
statusPageResources: true,
|
|
4137
|
-
isSubscribedToAllResources: true,
|
|
4138
|
-
statusPageEventTypes: true,
|
|
4139
|
-
isSubscribedToAllEventTypes: true,
|
|
3611
|
+
isResolvedState: true,
|
|
3612
|
+
order: true,
|
|
4140
3613
|
},
|
|
3614
|
+
limit: LIMIT_PER_PROJECT,
|
|
3615
|
+
skip: 0,
|
|
4141
3616
|
props: {
|
|
4142
3617
|
isRoot: true,
|
|
4143
3618
|
},
|
|
4144
3619
|
});
|
|
4145
3620
|
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
3621
|
+
const response: JSONObject = {
|
|
3622
|
+
incidentPublicNotes: BaseModel.toJSONArray(
|
|
3623
|
+
incidentPublicNotes,
|
|
3624
|
+
IncidentPublicNote,
|
|
3625
|
+
),
|
|
3626
|
+
incidentStates: BaseModel.toJSONArray(incidentStates, IncidentState),
|
|
3627
|
+
incidents: BaseModel.toJSONArray(incidents, Incident),
|
|
3628
|
+
statusPageResources: BaseModel.toJSONArray(
|
|
3629
|
+
statusPageResources,
|
|
3630
|
+
StatusPageResource,
|
|
3631
|
+
),
|
|
3632
|
+
incidentStateTimelines: BaseModel.toJSONArray(
|
|
3633
|
+
incidentStateTimelines,
|
|
3634
|
+
IncidentStateTimeline,
|
|
3635
|
+
),
|
|
3636
|
+
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
|
3637
|
+
};
|
|
4149
3638
|
|
|
4150
|
-
return
|
|
3639
|
+
return response;
|
|
4151
3640
|
}
|
|
4152
3641
|
|
|
4153
3642
|
@CaptureSpan()
|
|
4154
|
-
public async
|
|
3643
|
+
public async getEpisodes(
|
|
4155
3644
|
statusPageId: ObjectID,
|
|
4156
|
-
|
|
3645
|
+
episodeId: ObjectID | null,
|
|
4157
3646
|
req: ExpressRequest,
|
|
4158
3647
|
): Promise<JSONObject> {
|
|
4159
3648
|
await this.checkHasReadAccess({
|
|
@@ -4168,9 +3657,9 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4168
3657
|
select: {
|
|
4169
3658
|
_id: true,
|
|
4170
3659
|
projectId: true,
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
3660
|
+
showEpisodeHistoryInDays: true,
|
|
3661
|
+
showEpisodesOnStatusPage: true,
|
|
3662
|
+
showEpisodeLabelsOnStatusPage: true,
|
|
4174
3663
|
},
|
|
4175
3664
|
props: {
|
|
4176
3665
|
isRoot: true,
|
|
@@ -4181,9 +3670,9 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4181
3670
|
throw new BadDataException("Status Page not found");
|
|
4182
3671
|
}
|
|
4183
3672
|
|
|
4184
|
-
if (!statusPage.
|
|
3673
|
+
if (!statusPage.showEpisodesOnStatusPage) {
|
|
4185
3674
|
throw new BadDataException(
|
|
4186
|
-
"
|
|
3675
|
+
"Episodes are not enabled on this status page.",
|
|
4187
3676
|
);
|
|
4188
3677
|
}
|
|
4189
3678
|
|
|
@@ -4203,42 +3692,133 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4203
3692
|
const today: Date = OneUptimeDate.getCurrentDate();
|
|
4204
3693
|
|
|
4205
3694
|
const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
|
|
4206
|
-
statusPage.
|
|
3695
|
+
statusPage.showEpisodeHistoryInDays || 14,
|
|
4207
3696
|
);
|
|
4208
3697
|
|
|
4209
|
-
|
|
3698
|
+
/*
|
|
3699
|
+
* Get incidents that have monitors on this status page
|
|
3700
|
+
* Note: We don't filter by incident.isVisibleOnStatusPage here because
|
|
3701
|
+
* episode visibility is independent of incident visibility.
|
|
3702
|
+
* An episode should show if episode.isVisibleOnStatusPage is true,
|
|
3703
|
+
* regardless of whether its member incidents are visible.
|
|
3704
|
+
*/
|
|
3705
|
+
const incidentQuery: Query<Incident> = {
|
|
4210
3706
|
monitors: monitorsOnStatusPage as any,
|
|
4211
3707
|
projectId: statusPage.projectId!,
|
|
4212
3708
|
createdAt: QueryHelper.inBetween(historyDays, today),
|
|
3709
|
+
};
|
|
3710
|
+
|
|
3711
|
+
let incidents: Array<Incident> = [];
|
|
3712
|
+
|
|
3713
|
+
if (monitorsOnStatusPage.length > 0) {
|
|
3714
|
+
incidents = await IncidentService.findBy({
|
|
3715
|
+
query: incidentQuery,
|
|
3716
|
+
select: {
|
|
3717
|
+
_id: true,
|
|
3718
|
+
},
|
|
3719
|
+
skip: 0,
|
|
3720
|
+
limit: LIMIT_PER_PROJECT,
|
|
3721
|
+
props: {
|
|
3722
|
+
isRoot: true,
|
|
3723
|
+
},
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
const incidentIds: Array<ObjectID> = incidents.map((incident: Incident) => {
|
|
3728
|
+
return incident.id!;
|
|
3729
|
+
});
|
|
3730
|
+
|
|
3731
|
+
// Get episode members that link to these incidents
|
|
3732
|
+
let episodeMembers: Array<IncidentEpisodeMember> = [];
|
|
3733
|
+
|
|
3734
|
+
if (incidentIds.length > 0) {
|
|
3735
|
+
episodeMembers = await IncidentEpisodeMemberService.findBy({
|
|
3736
|
+
query: {
|
|
3737
|
+
incidentId: QueryHelper.any(incidentIds),
|
|
3738
|
+
projectId: statusPage.projectId!,
|
|
3739
|
+
},
|
|
3740
|
+
select: {
|
|
3741
|
+
incidentEpisodeId: true,
|
|
3742
|
+
incidentId: true,
|
|
3743
|
+
},
|
|
3744
|
+
skip: 0,
|
|
3745
|
+
limit: LIMIT_PER_PROJECT,
|
|
3746
|
+
props: {
|
|
3747
|
+
isRoot: true,
|
|
3748
|
+
},
|
|
3749
|
+
});
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
// Get unique episode IDs
|
|
3753
|
+
const episodeIdsFromMembers: Set<string> = new Set();
|
|
3754
|
+
for (const member of episodeMembers) {
|
|
3755
|
+
if (member.incidentEpisodeId) {
|
|
3756
|
+
episodeIdsFromMembers.add(member.incidentEpisodeId.toString());
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
let episodeQuery: Query<IncidentEpisode> = {
|
|
3761
|
+
_id: QueryHelper.any(
|
|
3762
|
+
Array.from(episodeIdsFromMembers).map((id: string) => {
|
|
3763
|
+
return new ObjectID(id);
|
|
3764
|
+
}),
|
|
3765
|
+
),
|
|
3766
|
+
projectId: statusPage.projectId!,
|
|
4213
3767
|
isVisibleOnStatusPage: true,
|
|
4214
3768
|
};
|
|
4215
3769
|
|
|
4216
|
-
if (
|
|
4217
|
-
|
|
4218
|
-
|
|
3770
|
+
if (episodeId) {
|
|
3771
|
+
episodeQuery = {
|
|
3772
|
+
_id: episodeId.toString(),
|
|
4219
3773
|
projectId: statusPage.projectId!,
|
|
4220
|
-
_id: incidentId.toString(),
|
|
4221
3774
|
isVisibleOnStatusPage: true,
|
|
4222
3775
|
};
|
|
3776
|
+
|
|
3777
|
+
// When viewing a specific episode, also fetch its members directly
|
|
3778
|
+
const episodeMembersForSpecificEpisode: Array<IncidentEpisodeMember> =
|
|
3779
|
+
await IncidentEpisodeMemberService.findBy({
|
|
3780
|
+
query: {
|
|
3781
|
+
incidentEpisodeId: episodeId,
|
|
3782
|
+
projectId: statusPage.projectId!,
|
|
3783
|
+
},
|
|
3784
|
+
select: {
|
|
3785
|
+
incidentEpisodeId: true,
|
|
3786
|
+
incidentId: true,
|
|
3787
|
+
},
|
|
3788
|
+
skip: 0,
|
|
3789
|
+
limit: LIMIT_PER_PROJECT,
|
|
3790
|
+
props: {
|
|
3791
|
+
isRoot: true,
|
|
3792
|
+
},
|
|
3793
|
+
});
|
|
3794
|
+
|
|
3795
|
+
// Merge with existing episode members
|
|
3796
|
+
for (const member of episodeMembersForSpecificEpisode) {
|
|
3797
|
+
if (
|
|
3798
|
+
!episodeMembers.some((m: IncidentEpisodeMember) => {
|
|
3799
|
+
return (
|
|
3800
|
+
m.incidentEpisodeId?.toString() ===
|
|
3801
|
+
member.incidentEpisodeId?.toString() &&
|
|
3802
|
+
m.incidentId?.toString() === member.incidentId?.toString()
|
|
3803
|
+
);
|
|
3804
|
+
})
|
|
3805
|
+
) {
|
|
3806
|
+
episodeMembers.push(member);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
4223
3809
|
}
|
|
4224
3810
|
|
|
4225
|
-
//
|
|
4226
|
-
let
|
|
3811
|
+
// Get episodes
|
|
3812
|
+
let episodes: Array<IncidentEpisode> = [];
|
|
4227
3813
|
|
|
4228
|
-
let
|
|
3814
|
+
let selectEpisodes: Select<IncidentEpisode> = {
|
|
4229
3815
|
createdAt: true,
|
|
4230
3816
|
declaredAt: true,
|
|
4231
3817
|
updatedAt: true,
|
|
4232
3818
|
title: true,
|
|
4233
3819
|
description: true,
|
|
4234
3820
|
_id: true,
|
|
4235
|
-
|
|
4236
|
-
postmortemPostedAt: true,
|
|
4237
|
-
showPostmortemOnStatusPage: true,
|
|
4238
|
-
postmortemAttachments: {
|
|
4239
|
-
_id: true,
|
|
4240
|
-
name: true,
|
|
4241
|
-
},
|
|
3821
|
+
episodeNumber: true,
|
|
4242
3822
|
incidentSeverity: {
|
|
4243
3823
|
name: true,
|
|
4244
3824
|
color: true,
|
|
@@ -4248,15 +3828,16 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4248
3828
|
color: true,
|
|
4249
3829
|
_id: true,
|
|
4250
3830
|
order: true,
|
|
3831
|
+
isCreatedState: true,
|
|
3832
|
+
isAcknowledgedState: true,
|
|
3833
|
+
isResolvedState: true,
|
|
4251
3834
|
},
|
|
4252
|
-
|
|
4253
|
-
_id: true,
|
|
4254
|
-
},
|
|
3835
|
+
incidentCount: true,
|
|
4255
3836
|
};
|
|
4256
3837
|
|
|
4257
|
-
if (statusPage.
|
|
4258
|
-
|
|
4259
|
-
...
|
|
3838
|
+
if (statusPage.showEpisodeLabelsOnStatusPage) {
|
|
3839
|
+
selectEpisodes = {
|
|
3840
|
+
...selectEpisodes,
|
|
4260
3841
|
labels: {
|
|
4261
3842
|
name: true,
|
|
4262
3843
|
color: true,
|
|
@@ -4264,10 +3845,10 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4264
3845
|
};
|
|
4265
3846
|
}
|
|
4266
3847
|
|
|
4267
|
-
if (
|
|
4268
|
-
|
|
4269
|
-
query:
|
|
4270
|
-
select:
|
|
3848
|
+
if (episodeIdsFromMembers.size > 0 || episodeId) {
|
|
3849
|
+
episodes = await IncidentEpisodeService.findBy({
|
|
3850
|
+
query: episodeQuery,
|
|
3851
|
+
select: selectEpisodes,
|
|
4271
3852
|
sort: {
|
|
4272
3853
|
declaredAt: SortOrder.Descending,
|
|
4273
3854
|
createdAt: SortOrder.Descending,
|
|
@@ -4278,9 +3859,10 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4278
3859
|
isRoot: true,
|
|
4279
3860
|
},
|
|
4280
3861
|
});
|
|
3862
|
+
}
|
|
4281
3863
|
|
|
4282
|
-
|
|
4283
|
-
|
|
3864
|
+
// If no specific episode, also fetch active (unresolved) episodes
|
|
3865
|
+
if (!episodeId && episodeIdsFromMembers.size > 0) {
|
|
4284
3866
|
const unresolvedIncidentStates: Array<IncidentState> =
|
|
4285
3867
|
await IncidentStateService.getUnresolvedIncidentStates(
|
|
4286
3868
|
statusPage.projectId!,
|
|
@@ -4289,68 +3871,145 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4289
3871
|
},
|
|
4290
3872
|
);
|
|
4291
3873
|
|
|
4292
|
-
const
|
|
3874
|
+
const unresolvedIncidentStateIds: Array<ObjectID> =
|
|
4293
3875
|
unresolvedIncidentStates.map((state: IncidentState) => {
|
|
4294
3876
|
return state.id!;
|
|
4295
3877
|
});
|
|
4296
3878
|
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
activeIncidents = await IncidentService.findBy({
|
|
3879
|
+
const activeEpisodes: Array<IncidentEpisode> =
|
|
3880
|
+
await IncidentEpisodeService.findBy({
|
|
4300
3881
|
query: {
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
3882
|
+
_id: QueryHelper.any(
|
|
3883
|
+
Array.from(episodeIdsFromMembers).map((id: string) => {
|
|
3884
|
+
return new ObjectID(id);
|
|
3885
|
+
}),
|
|
4305
3886
|
),
|
|
3887
|
+
isVisibleOnStatusPage: true,
|
|
3888
|
+
currentIncidentStateId: QueryHelper.any(unresolvedIncidentStateIds),
|
|
4306
3889
|
projectId: statusPage.projectId!,
|
|
4307
3890
|
},
|
|
4308
|
-
select:
|
|
3891
|
+
select: selectEpisodes,
|
|
4309
3892
|
sort: {
|
|
4310
3893
|
declaredAt: SortOrder.Descending,
|
|
4311
3894
|
createdAt: SortOrder.Descending,
|
|
4312
3895
|
},
|
|
4313
|
-
|
|
4314
3896
|
skip: 0,
|
|
4315
3897
|
limit: LIMIT_PER_PROJECT,
|
|
4316
3898
|
props: {
|
|
4317
3899
|
isRoot: true,
|
|
4318
3900
|
},
|
|
4319
3901
|
});
|
|
4320
|
-
}
|
|
4321
|
-
|
|
4322
|
-
incidents = [...activeIncidents, ...incidents];
|
|
4323
3902
|
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
incidents = ArrayUtil.distinctByFieldName(incidents, "_id");
|
|
3903
|
+
episodes = [...activeEpisodes, ...episodes];
|
|
3904
|
+
episodes = ArrayUtil.distinctByFieldName(episodes, "_id");
|
|
4327
3905
|
}
|
|
4328
3906
|
|
|
4329
|
-
const
|
|
4330
|
-
(
|
|
4331
|
-
return
|
|
3907
|
+
const episodesOnStatusPage: Array<ObjectID> = episodes.map(
|
|
3908
|
+
(episode: IncidentEpisode) => {
|
|
3909
|
+
return episode.id!;
|
|
4332
3910
|
},
|
|
4333
3911
|
);
|
|
4334
3912
|
|
|
4335
|
-
|
|
3913
|
+
/*
|
|
3914
|
+
* Build a map of episode ID -> monitor IDs from episode members
|
|
3915
|
+
* Collect all unique incident IDs from episode members
|
|
3916
|
+
*/
|
|
3917
|
+
const memberIncidentIds: Array<ObjectID> = [];
|
|
3918
|
+
for (const member of episodeMembers) {
|
|
3919
|
+
if (
|
|
3920
|
+
member.incidentId &&
|
|
3921
|
+
!memberIncidentIds.some((id: ObjectID) => {
|
|
3922
|
+
return id.toString() === member.incidentId!.toString();
|
|
3923
|
+
})
|
|
3924
|
+
) {
|
|
3925
|
+
memberIncidentIds.push(member.incidentId);
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
4336
3928
|
|
|
4337
|
-
|
|
4338
|
-
|
|
3929
|
+
// Fetch incidents with their monitors
|
|
3930
|
+
let memberIncidents: Array<Incident> = [];
|
|
3931
|
+
if (memberIncidentIds.length > 0) {
|
|
3932
|
+
memberIncidents = await IncidentService.findBy({
|
|
4339
3933
|
query: {
|
|
4340
|
-
|
|
3934
|
+
_id: QueryHelper.any(memberIncidentIds),
|
|
3935
|
+
projectId: statusPage.projectId!,
|
|
3936
|
+
},
|
|
3937
|
+
select: {
|
|
3938
|
+
_id: true,
|
|
3939
|
+
monitors: {
|
|
3940
|
+
_id: true,
|
|
3941
|
+
},
|
|
3942
|
+
},
|
|
3943
|
+
skip: 0,
|
|
3944
|
+
limit: LIMIT_PER_PROJECT,
|
|
3945
|
+
props: {
|
|
3946
|
+
isRoot: true,
|
|
3947
|
+
},
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
// Build a map of incident ID -> monitors
|
|
3952
|
+
const incidentMonitorsMap: Map<string, Array<ObjectID>> = new Map();
|
|
3953
|
+
for (const incident of memberIncidents) {
|
|
3954
|
+
const incidentIdStr: string = incident.id!.toString();
|
|
3955
|
+
const monitorIds: Array<ObjectID> = (incident.monitors || [])
|
|
3956
|
+
.map((m: Monitor) => {
|
|
3957
|
+
return new ObjectID(m._id?.toString() || m.id?.toString() || "");
|
|
3958
|
+
})
|
|
3959
|
+
.filter((id: ObjectID) => {
|
|
3960
|
+
return id.toString() !== "";
|
|
3961
|
+
});
|
|
3962
|
+
incidentMonitorsMap.set(incidentIdStr, monitorIds);
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
// Build episode monitors map from members and incident monitors
|
|
3966
|
+
const episodeMonitorsMap: Map<string, Array<ObjectID>> = new Map();
|
|
3967
|
+
for (const member of episodeMembers) {
|
|
3968
|
+
if (member.incidentEpisodeId && member.incidentId) {
|
|
3969
|
+
const episodeIdStr: string = member.incidentEpisodeId.toString();
|
|
3970
|
+
const incidentIdStr: string = member.incidentId.toString();
|
|
3971
|
+
|
|
3972
|
+
if (!episodeMonitorsMap.has(episodeIdStr)) {
|
|
3973
|
+
episodeMonitorsMap.set(episodeIdStr, []);
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
const episodeMonitors: Array<ObjectID> =
|
|
3977
|
+
episodeMonitorsMap.get(episodeIdStr)!;
|
|
3978
|
+
const incidentMonitors: Array<ObjectID> =
|
|
3979
|
+
incidentMonitorsMap.get(incidentIdStr) || [];
|
|
3980
|
+
|
|
3981
|
+
for (const monitorId of incidentMonitors) {
|
|
3982
|
+
if (
|
|
3983
|
+
!episodeMonitors.some((m: ObjectID) => {
|
|
3984
|
+
return m.toString() === monitorId.toString();
|
|
3985
|
+
})
|
|
3986
|
+
) {
|
|
3987
|
+
episodeMonitors.push(monitorId);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
// Get public notes for episodes
|
|
3994
|
+
let episodePublicNotes: Array<IncidentEpisodePublicNote> = [];
|
|
3995
|
+
|
|
3996
|
+
if (episodesOnStatusPage.length > 0) {
|
|
3997
|
+
episodePublicNotes = await IncidentEpisodePublicNoteService.findBy({
|
|
3998
|
+
query: {
|
|
3999
|
+
incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
|
|
4341
4000
|
projectId: statusPage.projectId!,
|
|
4342
4001
|
},
|
|
4343
4002
|
select: {
|
|
4344
4003
|
postedAt: true,
|
|
4345
4004
|
note: true,
|
|
4346
|
-
|
|
4005
|
+
incidentEpisodeId: true,
|
|
4347
4006
|
attachments: {
|
|
4348
4007
|
_id: true,
|
|
4349
4008
|
name: true,
|
|
4350
4009
|
},
|
|
4351
4010
|
},
|
|
4352
4011
|
sort: {
|
|
4353
|
-
postedAt: SortOrder.Descending,
|
|
4012
|
+
postedAt: SortOrder.Descending,
|
|
4354
4013
|
},
|
|
4355
4014
|
skip: 0,
|
|
4356
4015
|
limit: LIMIT_PER_PROJECT,
|
|
@@ -4360,28 +4019,31 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4360
4019
|
});
|
|
4361
4020
|
}
|
|
4362
4021
|
|
|
4363
|
-
|
|
4022
|
+
// Get state timelines for episodes
|
|
4023
|
+
let episodeStateTimelines: Array<IncidentEpisodeStateTimeline> = [];
|
|
4364
4024
|
|
|
4365
|
-
if (
|
|
4366
|
-
|
|
4025
|
+
if (episodesOnStatusPage.length > 0) {
|
|
4026
|
+
episodeStateTimelines = await IncidentEpisodeStateTimelineService.findBy({
|
|
4367
4027
|
query: {
|
|
4368
|
-
|
|
4028
|
+
incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
|
|
4369
4029
|
projectId: statusPage.projectId!,
|
|
4370
4030
|
},
|
|
4371
4031
|
select: {
|
|
4372
4032
|
_id: true,
|
|
4373
4033
|
createdAt: true,
|
|
4374
4034
|
startsAt: true,
|
|
4375
|
-
|
|
4035
|
+
incidentEpisodeId: true,
|
|
4376
4036
|
incidentState: {
|
|
4377
4037
|
name: true,
|
|
4378
4038
|
color: true,
|
|
4039
|
+
isCreatedState: true,
|
|
4040
|
+
isAcknowledgedState: true,
|
|
4041
|
+
isResolvedState: true,
|
|
4379
4042
|
},
|
|
4380
4043
|
},
|
|
4381
4044
|
sort: {
|
|
4382
|
-
startsAt: SortOrder.Descending,
|
|
4045
|
+
startsAt: SortOrder.Descending,
|
|
4383
4046
|
},
|
|
4384
|
-
|
|
4385
4047
|
skip: 0,
|
|
4386
4048
|
limit: LIMIT_PER_PROJECT,
|
|
4387
4049
|
props: {
|
|
@@ -4390,7 +4052,7 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4390
4052
|
});
|
|
4391
4053
|
}
|
|
4392
4054
|
|
|
4393
|
-
//
|
|
4055
|
+
// Get all incident states for this project
|
|
4394
4056
|
const incidentStates: Array<IncidentState> =
|
|
4395
4057
|
await IncidentStateService.findBy({
|
|
4396
4058
|
query: {
|
|
@@ -4407,20 +4069,37 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4407
4069
|
},
|
|
4408
4070
|
});
|
|
4409
4071
|
|
|
4072
|
+
// Serialize episodes and add monitors to each
|
|
4073
|
+
const episodesJson: JSONArray = BaseModel.toJSONArray(
|
|
4074
|
+
episodes,
|
|
4075
|
+
IncidentEpisode,
|
|
4076
|
+
);
|
|
4077
|
+
for (const episodeJson of episodesJson) {
|
|
4078
|
+
const episodeObj: JSONObject = episodeJson as JSONObject;
|
|
4079
|
+
const episodeId: string | undefined = episodeObj["_id"]?.toString();
|
|
4080
|
+
if (episodeId) {
|
|
4081
|
+
const monitorIds: Array<ObjectID> =
|
|
4082
|
+
episodeMonitorsMap.get(episodeId) || [];
|
|
4083
|
+
episodeObj["monitors"] = monitorIds.map((id: ObjectID) => {
|
|
4084
|
+
return { _id: id.toString() };
|
|
4085
|
+
});
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4410
4089
|
const response: JSONObject = {
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4090
|
+
episodePublicNotes: BaseModel.toJSONArray(
|
|
4091
|
+
episodePublicNotes,
|
|
4092
|
+
IncidentEpisodePublicNote,
|
|
4414
4093
|
),
|
|
4415
4094
|
incidentStates: BaseModel.toJSONArray(incidentStates, IncidentState),
|
|
4416
|
-
|
|
4095
|
+
episodes: episodesJson,
|
|
4417
4096
|
statusPageResources: BaseModel.toJSONArray(
|
|
4418
4097
|
statusPageResources,
|
|
4419
4098
|
StatusPageResource,
|
|
4420
4099
|
),
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4100
|
+
episodeStateTimelines: BaseModel.toJSONArray(
|
|
4101
|
+
episodeStateTimelines,
|
|
4102
|
+
IncidentEpisodeStateTimeline,
|
|
4424
4103
|
),
|
|
4425
4104
|
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
|
4426
4105
|
};
|
|
@@ -4429,26 +4108,54 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4429
4108
|
}
|
|
4430
4109
|
|
|
4431
4110
|
@CaptureSpan()
|
|
4432
|
-
public async
|
|
4433
|
-
statusPageId: ObjectID
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4111
|
+
public async getStatusPageResourcesAndTimelines(data: {
|
|
4112
|
+
statusPageId: ObjectID;
|
|
4113
|
+
/*
|
|
4114
|
+
* When omitted, the timeline window is computed from the status page's
|
|
4115
|
+
* own showUptimeHistoryInDays (clamped to 1..90 days, ending now) —
|
|
4116
|
+
* the status page row is fetched below anyway, so callers that want the
|
|
4117
|
+
* page-configured window (the overview endpoint) don't need their own
|
|
4118
|
+
* StatusPage query just to read that column.
|
|
4119
|
+
*/
|
|
4120
|
+
startDateForMonitorTimeline?: Date | undefined;
|
|
4121
|
+
endDateForMonitorTimeline?: Date | undefined;
|
|
4122
|
+
}): Promise<{
|
|
4123
|
+
statusPageResources: StatusPageResource[];
|
|
4124
|
+
monitorStatuses: MonitorStatus[];
|
|
4125
|
+
monitorStatusTimelines: MonitorStatusTimeline[];
|
|
4126
|
+
monitorGroupCurrentStatuses: Dictionary<ObjectID>;
|
|
4127
|
+
statusPageGroups: StatusPageGroup[];
|
|
4128
|
+
statusPage: StatusPage;
|
|
4129
|
+
monitorsOnStatusPage: ObjectID[];
|
|
4130
|
+
monitorsInGroup: Dictionary<ObjectID[]>;
|
|
4131
|
+
startDateForMonitorTimeline: Date;
|
|
4132
|
+
endDateForMonitorTimeline: Date;
|
|
4133
|
+
}> {
|
|
4134
|
+
const objectId: ObjectID = data.statusPageId;
|
|
4441
4135
|
|
|
4442
4136
|
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
|
|
4443
4137
|
query: {
|
|
4444
|
-
_id:
|
|
4138
|
+
_id: objectId.toString(),
|
|
4445
4139
|
},
|
|
4446
4140
|
select: {
|
|
4447
4141
|
_id: true,
|
|
4448
4142
|
projectId: true,
|
|
4449
|
-
|
|
4450
|
-
|
|
4143
|
+
isPublicStatusPage: true,
|
|
4144
|
+
overviewPageDescription: true,
|
|
4145
|
+
showIncidentLabelsOnStatusPage: true,
|
|
4146
|
+
showScheduledEventLabelsOnStatusPage: true,
|
|
4451
4147
|
showEpisodeLabelsOnStatusPage: true,
|
|
4148
|
+
downtimeMonitorStatuses: {
|
|
4149
|
+
_id: true,
|
|
4150
|
+
},
|
|
4151
|
+
defaultBarColor: true,
|
|
4152
|
+
showOverallUptimePercentOnStatusPage: true,
|
|
4153
|
+
overallUptimePercentPrecision: true,
|
|
4154
|
+
showAnnouncementsOnStatusPage: true,
|
|
4155
|
+
showIncidentsOnStatusPage: true,
|
|
4156
|
+
showEpisodesOnStatusPage: true,
|
|
4157
|
+
showScheduledMaintenanceEventsOnStatusPage: true,
|
|
4158
|
+
showUptimeHistoryInDays: true,
|
|
4452
4159
|
},
|
|
4453
4160
|
props: {
|
|
4454
4161
|
isRoot: true,
|
|
@@ -4459,51 +4166,42 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4459
4166
|
throw new BadDataException("Status Page not found");
|
|
4460
4167
|
}
|
|
4461
4168
|
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
}
|
|
4467
|
-
|
|
4468
|
-
// get monitors on status page.
|
|
4469
|
-
const statusPageResources: Array<StatusPageResource> =
|
|
4470
|
-
await StatusPageService.getStatusPageResources({
|
|
4471
|
-
statusPageId: statusPageId,
|
|
4472
|
-
});
|
|
4169
|
+
let startDateForMonitorTimeline: Date | undefined =
|
|
4170
|
+
data.startDateForMonitorTimeline;
|
|
4171
|
+
let endDateForMonitorTimeline: Date | undefined =
|
|
4172
|
+
data.endDateForMonitorTimeline;
|
|
4473
4173
|
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
statusPageId: statusPageId,
|
|
4477
|
-
// reuse the resources fetched above instead of re-querying them
|
|
4478
|
-
statusPageResources: statusPageResources,
|
|
4479
|
-
});
|
|
4174
|
+
if (!startDateForMonitorTimeline || !endDateForMonitorTimeline) {
|
|
4175
|
+
let uptimeHistoryDays: number = statusPage.showUptimeHistoryInDays || 90;
|
|
4480
4176
|
|
|
4481
|
-
|
|
4177
|
+
if (uptimeHistoryDays > 90) {
|
|
4178
|
+
uptimeHistoryDays = 90;
|
|
4179
|
+
}
|
|
4482
4180
|
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4181
|
+
if (uptimeHistoryDays < 1) {
|
|
4182
|
+
uptimeHistoryDays = 1;
|
|
4183
|
+
}
|
|
4486
4184
|
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
* An episode should show if episode.isVisibleOnStatusPage is true,
|
|
4492
|
-
* regardless of whether its member incidents are visible.
|
|
4493
|
-
*/
|
|
4494
|
-
const incidentQuery: Query<Incident> = {
|
|
4495
|
-
monitors: monitorsOnStatusPage as any,
|
|
4496
|
-
projectId: statusPage.projectId!,
|
|
4497
|
-
createdAt: QueryHelper.inBetween(historyDays, today),
|
|
4498
|
-
};
|
|
4185
|
+
startDateForMonitorTimeline =
|
|
4186
|
+
OneUptimeDate.getSomeDaysAgo(uptimeHistoryDays);
|
|
4187
|
+
endDateForMonitorTimeline = OneUptimeDate.getCurrentDate();
|
|
4188
|
+
}
|
|
4499
4189
|
|
|
4500
|
-
|
|
4190
|
+
//get monitor statuses
|
|
4501
4191
|
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
query:
|
|
4192
|
+
const monitorStatuses: Array<MonitorStatus> =
|
|
4193
|
+
await MonitorStatusService.findBy({
|
|
4194
|
+
query: {
|
|
4195
|
+
projectId: statusPage.projectId!,
|
|
4196
|
+
},
|
|
4505
4197
|
select: {
|
|
4506
|
-
|
|
4198
|
+
name: true,
|
|
4199
|
+
color: true,
|
|
4200
|
+
priority: true,
|
|
4201
|
+
isOperationalState: true,
|
|
4202
|
+
},
|
|
4203
|
+
sort: {
|
|
4204
|
+
priority: SortOrder.Ascending,
|
|
4507
4205
|
},
|
|
4508
4206
|
skip: 0,
|
|
4509
4207
|
limit: LIMIT_PER_PROJECT,
|
|
@@ -4511,24 +4209,71 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4511
4209
|
isRoot: true,
|
|
4512
4210
|
},
|
|
4513
4211
|
});
|
|
4514
|
-
}
|
|
4515
4212
|
|
|
4516
|
-
|
|
4517
|
-
return incident.id!;
|
|
4518
|
-
});
|
|
4213
|
+
// get resource groups.
|
|
4519
4214
|
|
|
4520
|
-
|
|
4521
|
-
|
|
4215
|
+
const groups: Array<StatusPageGroup> = await StatusPageGroupService.findBy({
|
|
4216
|
+
query: {
|
|
4217
|
+
statusPageId: objectId,
|
|
4218
|
+
},
|
|
4219
|
+
select: {
|
|
4220
|
+
name: true,
|
|
4221
|
+
order: true,
|
|
4222
|
+
description: true,
|
|
4223
|
+
isExpandedByDefault: true,
|
|
4224
|
+
showCurrentStatus: true,
|
|
4225
|
+
showUptimePercent: true,
|
|
4226
|
+
uptimePercentPrecision: true,
|
|
4227
|
+
viewMode: true,
|
|
4228
|
+
rowAxisLabel: true,
|
|
4229
|
+
columnAxisLabel: true,
|
|
4230
|
+
rowAxisValues: true,
|
|
4231
|
+
columnAxisValues: true,
|
|
4232
|
+
parentStatusPageGroupId: true,
|
|
4233
|
+
},
|
|
4234
|
+
sort: {
|
|
4235
|
+
order: SortOrder.Ascending,
|
|
4236
|
+
},
|
|
4237
|
+
skip: 0,
|
|
4238
|
+
limit: LIMIT_PER_PROJECT,
|
|
4239
|
+
props: {
|
|
4240
|
+
isRoot: true,
|
|
4241
|
+
},
|
|
4242
|
+
});
|
|
4522
4243
|
|
|
4523
|
-
|
|
4524
|
-
|
|
4244
|
+
// get monitors on status page.
|
|
4245
|
+
const statusPageResources: Array<StatusPageResource> =
|
|
4246
|
+
await StatusPageResourceService.findBy({
|
|
4525
4247
|
query: {
|
|
4526
|
-
|
|
4527
|
-
projectId: statusPage.projectId!,
|
|
4248
|
+
statusPageId: objectId,
|
|
4528
4249
|
},
|
|
4529
4250
|
select: {
|
|
4530
|
-
|
|
4531
|
-
|
|
4251
|
+
statusPageGroupId: true,
|
|
4252
|
+
statusPageGroup: {
|
|
4253
|
+
name: true,
|
|
4254
|
+
viewMode: true,
|
|
4255
|
+
rowAxisLabel: true,
|
|
4256
|
+
columnAxisLabel: true,
|
|
4257
|
+
},
|
|
4258
|
+
monitorId: true,
|
|
4259
|
+
displayTooltip: true,
|
|
4260
|
+
displayDescription: true,
|
|
4261
|
+
displayName: true,
|
|
4262
|
+
showStatusHistoryChart: true,
|
|
4263
|
+
showCurrentStatus: true,
|
|
4264
|
+
order: true,
|
|
4265
|
+
monitor: {
|
|
4266
|
+
_id: true,
|
|
4267
|
+
currentMonitorStatusId: true,
|
|
4268
|
+
},
|
|
4269
|
+
monitorGroupId: true,
|
|
4270
|
+
showUptimePercent: true,
|
|
4271
|
+
uptimePercentPrecision: true,
|
|
4272
|
+
rowAxisValue: true,
|
|
4273
|
+
columnAxisValue: true,
|
|
4274
|
+
},
|
|
4275
|
+
sort: {
|
|
4276
|
+
order: SortOrder.Ascending,
|
|
4532
4277
|
},
|
|
4533
4278
|
skip: 0,
|
|
4534
4279
|
limit: LIMIT_PER_PROJECT,
|
|
@@ -4536,122 +4281,218 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4536
4281
|
isRoot: true,
|
|
4537
4282
|
},
|
|
4538
4283
|
});
|
|
4539
|
-
}
|
|
4540
4284
|
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4285
|
+
const monitorGroupIds: Array<ObjectID> = statusPageResources
|
|
4286
|
+
.map((resource: StatusPageResource) => {
|
|
4287
|
+
return resource.monitorGroupId!;
|
|
4288
|
+
})
|
|
4289
|
+
.filter((id: ObjectID) => {
|
|
4290
|
+
return Boolean(id); // remove nulls
|
|
4291
|
+
});
|
|
4292
|
+
|
|
4293
|
+
// get monitors in the group.
|
|
4294
|
+
const monitorGroupCurrentStatuses: Dictionary<ObjectID> = {};
|
|
4295
|
+
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
|
4296
|
+
|
|
4297
|
+
// get monitor status charts.
|
|
4298
|
+
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
|
|
4299
|
+
.map((monitor: StatusPageResource) => {
|
|
4300
|
+
return monitor.monitorId!;
|
|
4301
|
+
})
|
|
4302
|
+
.filter((id: ObjectID) => {
|
|
4303
|
+
return Boolean(id); // remove nulls
|
|
4304
|
+
});
|
|
4305
|
+
|
|
4306
|
+
const monitorsOnStatusPageForTimeline: Array<ObjectID> = statusPageResources
|
|
4307
|
+
.filter((monitor: StatusPageResource) => {
|
|
4308
|
+
return monitor.showStatusHistoryChart || monitor.showUptimePercent;
|
|
4309
|
+
})
|
|
4310
|
+
.map((monitor: StatusPageResource) => {
|
|
4311
|
+
return monitor.monitorId!;
|
|
4312
|
+
})
|
|
4313
|
+
.filter((id: ObjectID) => {
|
|
4314
|
+
return Boolean(id); // remove nulls
|
|
4315
|
+
});
|
|
4316
|
+
|
|
4317
|
+
/*
|
|
4318
|
+
* Batched: this loop used to issue 4 queries per monitor group per page
|
|
4319
|
+
* view (3 inside MonitorGroupService.getCurrentStatus + one duplicate
|
|
4320
|
+
* group-resource fetch), on the hottest public endpoint in the product.
|
|
4321
|
+
* One shared fetch + the already-loaded `monitorStatuses` now serve every
|
|
4322
|
+
* group.
|
|
4323
|
+
*/
|
|
4324
|
+
const monitorGroupResourcesByGroupId: Dictionary<
|
|
4325
|
+
Array<MonitorGroupResource>
|
|
4326
|
+
> =
|
|
4327
|
+
await MonitorGroupService.getMonitorGroupResourcesByGroupIds(
|
|
4328
|
+
monitorGroupIds,
|
|
4329
|
+
);
|
|
4330
|
+
|
|
4331
|
+
const monitorGroupStatuses: Dictionary<MonitorStatus> =
|
|
4332
|
+
await MonitorGroupService.getCurrentStatusesForMonitorGroups({
|
|
4333
|
+
monitorGroupIds: monitorGroupIds,
|
|
4334
|
+
monitorStatuses: monitorStatuses,
|
|
4335
|
+
monitorGroupResources: monitorGroupResourcesByGroupId,
|
|
4336
|
+
});
|
|
4337
|
+
|
|
4338
|
+
for (const monitorGroupId of monitorGroupIds) {
|
|
4339
|
+
// get current status of monitors in the group.
|
|
4340
|
+
|
|
4341
|
+
const currentStatus: MonitorStatus | undefined =
|
|
4342
|
+
monitorGroupStatuses[monitorGroupId.toString()];
|
|
4343
|
+
|
|
4344
|
+
if (currentStatus) {
|
|
4345
|
+
monitorGroupCurrentStatuses[monitorGroupId.toString()] =
|
|
4346
|
+
currentStatus.id!;
|
|
4546
4347
|
}
|
|
4547
|
-
}
|
|
4548
4348
|
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4349
|
+
// get monitors in the group.
|
|
4350
|
+
|
|
4351
|
+
const monitorsInGroupIds: Array<ObjectID> = (
|
|
4352
|
+
monitorGroupResourcesByGroupId[monitorGroupId.toString()] || []
|
|
4353
|
+
)
|
|
4354
|
+
.map((resource: MonitorGroupResource) => {
|
|
4355
|
+
return resource.monitorId!;
|
|
4356
|
+
})
|
|
4357
|
+
.filter((id: ObjectID) => {
|
|
4358
|
+
return Boolean(id); // remove nulls
|
|
4359
|
+
});
|
|
4360
|
+
|
|
4361
|
+
const shouldShowTimelineForThisGroup: boolean = Boolean(
|
|
4362
|
+
statusPageResources.find((resource: StatusPageResource) => {
|
|
4363
|
+
return (
|
|
4364
|
+
resource.monitorGroupId?.toString() === monitorGroupId.toString() &&
|
|
4365
|
+
(resource.showStatusHistoryChart || resource.showUptimePercent)
|
|
4366
|
+
);
|
|
4553
4367
|
}),
|
|
4554
|
-
)
|
|
4555
|
-
projectId: statusPage.projectId!,
|
|
4556
|
-
isVisibleOnStatusPage: true,
|
|
4557
|
-
};
|
|
4368
|
+
);
|
|
4558
4369
|
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
isVisibleOnStatusPage: true,
|
|
4564
|
-
};
|
|
4370
|
+
for (const monitorId of monitorsInGroupIds) {
|
|
4371
|
+
if (!monitorId) {
|
|
4372
|
+
continue;
|
|
4373
|
+
}
|
|
4565
4374
|
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
incidentId: true,
|
|
4576
|
-
},
|
|
4577
|
-
skip: 0,
|
|
4578
|
-
limit: LIMIT_PER_PROJECT,
|
|
4579
|
-
props: {
|
|
4580
|
-
isRoot: true,
|
|
4581
|
-
},
|
|
4582
|
-
});
|
|
4375
|
+
if (
|
|
4376
|
+
!monitorsOnStatusPage.find((item: ObjectID) => {
|
|
4377
|
+
return item.toString() === monitorId.toString();
|
|
4378
|
+
})
|
|
4379
|
+
) {
|
|
4380
|
+
monitorsOnStatusPage.push(monitorId);
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
// add this to the timeline event for this group.
|
|
4583
4384
|
|
|
4584
|
-
// Merge with existing episode members
|
|
4585
|
-
for (const member of episodeMembersForSpecificEpisode) {
|
|
4586
4385
|
if (
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
member.incidentEpisodeId?.toString() &&
|
|
4591
|
-
m.incidentId?.toString() === member.incidentId?.toString()
|
|
4592
|
-
);
|
|
4386
|
+
shouldShowTimelineForThisGroup &&
|
|
4387
|
+
!monitorsOnStatusPageForTimeline.find((item: ObjectID) => {
|
|
4388
|
+
return item.toString() === monitorId.toString();
|
|
4593
4389
|
})
|
|
4594
4390
|
) {
|
|
4595
|
-
|
|
4391
|
+
monitorsOnStatusPageForTimeline.push(monitorId);
|
|
4596
4392
|
}
|
|
4597
4393
|
}
|
|
4394
|
+
|
|
4395
|
+
monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
|
|
4598
4396
|
}
|
|
4599
4397
|
|
|
4600
|
-
|
|
4601
|
-
|
|
4398
|
+
const monitorStatusTimelines: Array<MonitorStatusTimeline> =
|
|
4399
|
+
await StatusPageService.getMonitorStatusTimelineForStatusPage({
|
|
4400
|
+
monitorIds: monitorsOnStatusPageForTimeline,
|
|
4401
|
+
startDate: startDateForMonitorTimeline,
|
|
4402
|
+
endDate: endDateForMonitorTimeline,
|
|
4403
|
+
});
|
|
4602
4404
|
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
name: true,
|
|
4617
|
-
color: true,
|
|
4618
|
-
_id: true,
|
|
4619
|
-
order: true,
|
|
4620
|
-
isCreatedState: true,
|
|
4621
|
-
isAcknowledgedState: true,
|
|
4622
|
-
isResolvedState: true,
|
|
4623
|
-
},
|
|
4624
|
-
incidentCount: true,
|
|
4405
|
+
// return everything.
|
|
4406
|
+
|
|
4407
|
+
return {
|
|
4408
|
+
statusPageResources,
|
|
4409
|
+
monitorStatuses,
|
|
4410
|
+
monitorGroupCurrentStatuses,
|
|
4411
|
+
statusPageGroups: groups,
|
|
4412
|
+
monitorStatusTimelines,
|
|
4413
|
+
statusPage,
|
|
4414
|
+
monitorsOnStatusPage,
|
|
4415
|
+
monitorsInGroup,
|
|
4416
|
+
startDateForMonitorTimeline,
|
|
4417
|
+
endDateForMonitorTimeline,
|
|
4625
4418
|
};
|
|
4419
|
+
}
|
|
4626
4420
|
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4421
|
+
/*
|
|
4422
|
+
* Builds the full overview payload for one status page. Everything below
|
|
4423
|
+
* is a pure function of the statusPageId — every query runs with isRoot
|
|
4424
|
+
* props and nothing is read from the request — which is what makes the
|
|
4425
|
+
* result safe to cache and share across requests. The returned JSONObject
|
|
4426
|
+
* must never be mutated after build.
|
|
4427
|
+
*/
|
|
4428
|
+
@CaptureSpan()
|
|
4429
|
+
public async buildOverviewResponse(
|
|
4430
|
+
statusPageId: ObjectID,
|
|
4431
|
+
): Promise<JSONObject> {
|
|
4432
|
+
/*
|
|
4433
|
+
* The timeline window comes from the status page's configured
|
|
4434
|
+
* showUptimeHistoryInDays. getStatusPageResourcesAndTimelines
|
|
4435
|
+
* fetches the status page row anyway, so let it compute the window
|
|
4436
|
+
* (clamped to 1..90 days, ending now) instead of issuing a separate
|
|
4437
|
+
* StatusPage query here just to read that one column.
|
|
4438
|
+
*/
|
|
4439
|
+
const {
|
|
4440
|
+
monitorStatuses,
|
|
4441
|
+
monitorGroupCurrentStatuses,
|
|
4442
|
+
statusPageResources,
|
|
4443
|
+
statusPage,
|
|
4444
|
+
monitorsOnStatusPage,
|
|
4445
|
+
monitorStatusTimelines,
|
|
4446
|
+
statusPageGroups,
|
|
4447
|
+
monitorsInGroup,
|
|
4448
|
+
startDateForMonitorTimeline: startDate,
|
|
4449
|
+
endDateForMonitorTimeline: endDate,
|
|
4450
|
+
} = await this.getStatusPageResourcesAndTimelines({
|
|
4451
|
+
statusPageId: statusPageId,
|
|
4452
|
+
});
|
|
4453
|
+
|
|
4454
|
+
// check if status page has active incident.
|
|
4455
|
+
let activeIncidents: Array<Incident> = [];
|
|
4456
|
+
if (monitorsOnStatusPage.length > 0) {
|
|
4457
|
+
let select: Select<Incident> = {
|
|
4458
|
+
createdAt: true,
|
|
4459
|
+
declaredAt: true,
|
|
4460
|
+
updatedAt: true,
|
|
4461
|
+
title: true,
|
|
4462
|
+
description: true,
|
|
4463
|
+
_id: true,
|
|
4464
|
+
postmortemNote: true,
|
|
4465
|
+
postmortemPostedAt: true,
|
|
4466
|
+
showPostmortemOnStatusPage: true,
|
|
4467
|
+
postmortemAttachments: {
|
|
4468
|
+
_id: true,
|
|
4469
|
+
name: true,
|
|
4470
|
+
},
|
|
4471
|
+
incidentSeverity: {
|
|
4631
4472
|
name: true,
|
|
4632
4473
|
color: true,
|
|
4633
4474
|
},
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
query: episodeQuery,
|
|
4640
|
-
select: selectEpisodes,
|
|
4641
|
-
sort: {
|
|
4642
|
-
declaredAt: SortOrder.Descending,
|
|
4643
|
-
createdAt: SortOrder.Descending,
|
|
4475
|
+
currentIncidentState: {
|
|
4476
|
+
_id: true,
|
|
4477
|
+
name: true,
|
|
4478
|
+
color: true,
|
|
4479
|
+
order: true,
|
|
4644
4480
|
},
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
props: {
|
|
4648
|
-
isRoot: true,
|
|
4481
|
+
monitors: {
|
|
4482
|
+
_id: true,
|
|
4649
4483
|
},
|
|
4650
|
-
}
|
|
4651
|
-
|
|
4484
|
+
};
|
|
4485
|
+
|
|
4486
|
+
if (statusPage.showIncidentLabelsOnStatusPage) {
|
|
4487
|
+
select = {
|
|
4488
|
+
...select,
|
|
4489
|
+
labels: {
|
|
4490
|
+
name: true,
|
|
4491
|
+
color: true,
|
|
4492
|
+
},
|
|
4493
|
+
};
|
|
4494
|
+
}
|
|
4652
4495
|
|
|
4653
|
-
// If no specific episode, also fetch active (unresolved) episodes
|
|
4654
|
-
if (!episodeId && episodeIdsFromMembers.size > 0) {
|
|
4655
4496
|
const unresolvedIncidentStates: Array<IncidentState> =
|
|
4656
4497
|
await IncidentStateService.getUnresolvedIncidentStates(
|
|
4657
4498
|
statusPage.projectId!,
|
|
@@ -4665,140 +4506,54 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4665
4506
|
return state.id!;
|
|
4666
4507
|
});
|
|
4667
4508
|
|
|
4668
|
-
|
|
4669
|
-
await
|
|
4509
|
+
if (statusPage.showIncidentsOnStatusPage) {
|
|
4510
|
+
activeIncidents = await IncidentService.findBy({
|
|
4670
4511
|
query: {
|
|
4671
|
-
|
|
4672
|
-
Array.from(episodeIdsFromMembers).map((id: string) => {
|
|
4673
|
-
return new ObjectID(id);
|
|
4674
|
-
}),
|
|
4675
|
-
),
|
|
4676
|
-
isVisibleOnStatusPage: true,
|
|
4512
|
+
monitors: monitorsOnStatusPage as any,
|
|
4677
4513
|
currentIncidentStateId: QueryHelper.any(unresolvedIncidentStateIds),
|
|
4514
|
+
isVisibleOnStatusPage: true,
|
|
4678
4515
|
projectId: statusPage.projectId!,
|
|
4679
|
-
},
|
|
4680
|
-
select:
|
|
4681
|
-
sort: {
|
|
4682
|
-
declaredAt: SortOrder.Descending,
|
|
4683
|
-
createdAt: SortOrder.Descending,
|
|
4684
|
-
},
|
|
4685
|
-
skip: 0,
|
|
4686
|
-
limit: LIMIT_PER_PROJECT,
|
|
4687
|
-
props: {
|
|
4688
|
-
isRoot: true,
|
|
4689
|
-
},
|
|
4690
|
-
});
|
|
4691
|
-
|
|
4692
|
-
episodes = [...activeEpisodes, ...episodes];
|
|
4693
|
-
episodes = ArrayUtil.distinctByFieldName(episodes, "_id");
|
|
4694
|
-
}
|
|
4695
|
-
|
|
4696
|
-
const episodesOnStatusPage: Array<ObjectID> = episodes.map(
|
|
4697
|
-
(episode: IncidentEpisode) => {
|
|
4698
|
-
return episode.id!;
|
|
4699
|
-
},
|
|
4700
|
-
);
|
|
4701
|
-
|
|
4702
|
-
/*
|
|
4703
|
-
* Build a map of episode ID -> monitor IDs from episode members
|
|
4704
|
-
* Collect all unique incident IDs from episode members
|
|
4705
|
-
*/
|
|
4706
|
-
const memberIncidentIds: Array<ObjectID> = [];
|
|
4707
|
-
for (const member of episodeMembers) {
|
|
4708
|
-
if (
|
|
4709
|
-
member.incidentId &&
|
|
4710
|
-
!memberIncidentIds.some((id: ObjectID) => {
|
|
4711
|
-
return id.toString() === member.incidentId!.toString();
|
|
4712
|
-
})
|
|
4713
|
-
) {
|
|
4714
|
-
memberIncidentIds.push(member.incidentId);
|
|
4715
|
-
}
|
|
4716
|
-
}
|
|
4717
|
-
|
|
4718
|
-
// Fetch incidents with their monitors
|
|
4719
|
-
let memberIncidents: Array<Incident> = [];
|
|
4720
|
-
if (memberIncidentIds.length > 0) {
|
|
4721
|
-
memberIncidents = await IncidentService.findBy({
|
|
4722
|
-
query: {
|
|
4723
|
-
_id: QueryHelper.any(memberIncidentIds),
|
|
4724
|
-
projectId: statusPage.projectId!,
|
|
4725
|
-
},
|
|
4726
|
-
select: {
|
|
4727
|
-
_id: true,
|
|
4728
|
-
monitors: {
|
|
4729
|
-
_id: true,
|
|
4730
|
-
},
|
|
4731
|
-
},
|
|
4732
|
-
skip: 0,
|
|
4733
|
-
limit: LIMIT_PER_PROJECT,
|
|
4734
|
-
props: {
|
|
4735
|
-
isRoot: true,
|
|
4736
|
-
},
|
|
4737
|
-
});
|
|
4738
|
-
}
|
|
4739
|
-
|
|
4740
|
-
// Build a map of incident ID -> monitors
|
|
4741
|
-
const incidentMonitorsMap: Map<string, Array<ObjectID>> = new Map();
|
|
4742
|
-
for (const incident of memberIncidents) {
|
|
4743
|
-
const incidentIdStr: string = incident.id!.toString();
|
|
4744
|
-
const monitorIds: Array<ObjectID> = (incident.monitors || [])
|
|
4745
|
-
.map((m: Monitor) => {
|
|
4746
|
-
return new ObjectID(m._id?.toString() || m.id?.toString() || "");
|
|
4747
|
-
})
|
|
4748
|
-
.filter((id: ObjectID) => {
|
|
4749
|
-
return id.toString() !== "";
|
|
4750
|
-
});
|
|
4751
|
-
incidentMonitorsMap.set(incidentIdStr, monitorIds);
|
|
4752
|
-
}
|
|
4753
|
-
|
|
4754
|
-
// Build episode monitors map from members and incident monitors
|
|
4755
|
-
const episodeMonitorsMap: Map<string, Array<ObjectID>> = new Map();
|
|
4756
|
-
for (const member of episodeMembers) {
|
|
4757
|
-
if (member.incidentEpisodeId && member.incidentId) {
|
|
4758
|
-
const episodeIdStr: string = member.incidentEpisodeId.toString();
|
|
4759
|
-
const incidentIdStr: string = member.incidentId.toString();
|
|
4760
|
-
|
|
4761
|
-
if (!episodeMonitorsMap.has(episodeIdStr)) {
|
|
4762
|
-
episodeMonitorsMap.set(episodeIdStr, []);
|
|
4763
|
-
}
|
|
4764
|
-
|
|
4765
|
-
const episodeMonitors: Array<ObjectID> =
|
|
4766
|
-
episodeMonitorsMap.get(episodeIdStr)!;
|
|
4767
|
-
const incidentMonitors: Array<ObjectID> =
|
|
4768
|
-
incidentMonitorsMap.get(incidentIdStr) || [];
|
|
4516
|
+
},
|
|
4517
|
+
select: select,
|
|
4518
|
+
sort: {
|
|
4519
|
+
declaredAt: SortOrder.Descending,
|
|
4520
|
+
createdAt: SortOrder.Descending,
|
|
4521
|
+
},
|
|
4769
4522
|
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
episodeMonitors.push(monitorId);
|
|
4777
|
-
}
|
|
4778
|
-
}
|
|
4523
|
+
skip: 0,
|
|
4524
|
+
limit: LIMIT_PER_PROJECT,
|
|
4525
|
+
props: {
|
|
4526
|
+
isRoot: true,
|
|
4527
|
+
},
|
|
4528
|
+
});
|
|
4779
4529
|
}
|
|
4780
4530
|
}
|
|
4781
4531
|
|
|
4782
|
-
|
|
4783
|
-
|
|
4532
|
+
const incidentsOnStatusPage: Array<ObjectID> = activeIncidents.map(
|
|
4533
|
+
(incident: Incident) => {
|
|
4534
|
+
return incident.id!;
|
|
4535
|
+
},
|
|
4536
|
+
);
|
|
4784
4537
|
|
|
4785
|
-
|
|
4786
|
-
|
|
4538
|
+
let incidentPublicNotes: Array<IncidentPublicNote> = [];
|
|
4539
|
+
|
|
4540
|
+
if (incidentsOnStatusPage.length > 0) {
|
|
4541
|
+
incidentPublicNotes = await IncidentPublicNoteService.findBy({
|
|
4787
4542
|
query: {
|
|
4788
|
-
|
|
4543
|
+
incidentId: QueryHelper.any(incidentsOnStatusPage),
|
|
4789
4544
|
projectId: statusPage.projectId!,
|
|
4790
4545
|
},
|
|
4791
4546
|
select: {
|
|
4792
|
-
postedAt: true,
|
|
4793
4547
|
note: true,
|
|
4794
|
-
|
|
4548
|
+
incidentId: true,
|
|
4549
|
+
postedAt: true,
|
|
4795
4550
|
attachments: {
|
|
4796
4551
|
_id: true,
|
|
4797
4552
|
name: true,
|
|
4798
4553
|
},
|
|
4799
4554
|
},
|
|
4800
4555
|
sort: {
|
|
4801
|
-
postedAt: SortOrder.Descending,
|
|
4556
|
+
postedAt: SortOrder.Descending, // new note first
|
|
4802
4557
|
},
|
|
4803
4558
|
skip: 0,
|
|
4804
4559
|
limit: LIMIT_PER_PROJECT,
|
|
@@ -4808,30 +4563,402 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4808
4563
|
});
|
|
4809
4564
|
}
|
|
4810
4565
|
|
|
4811
|
-
|
|
4812
|
-
let episodeStateTimelines: Array<IncidentEpisodeStateTimeline> = [];
|
|
4566
|
+
let incidentStateTimelines: Array<IncidentStateTimeline> = [];
|
|
4813
4567
|
|
|
4814
|
-
if (
|
|
4815
|
-
|
|
4568
|
+
if (incidentsOnStatusPage.length > 0) {
|
|
4569
|
+
incidentStateTimelines = await IncidentStateTimelineService.findBy({
|
|
4816
4570
|
query: {
|
|
4817
|
-
|
|
4571
|
+
incidentId: QueryHelper.any(incidentsOnStatusPage),
|
|
4818
4572
|
projectId: statusPage.projectId!,
|
|
4819
4573
|
},
|
|
4820
4574
|
select: {
|
|
4821
4575
|
_id: true,
|
|
4822
4576
|
createdAt: true,
|
|
4823
4577
|
startsAt: true,
|
|
4824
|
-
|
|
4578
|
+
incidentId: true,
|
|
4825
4579
|
incidentState: {
|
|
4580
|
+
_id: true,
|
|
4581
|
+
name: true,
|
|
4582
|
+
color: true,
|
|
4583
|
+
isCreatedState: true,
|
|
4584
|
+
isResolvedState: true,
|
|
4585
|
+
isAcknowledgedState: true,
|
|
4586
|
+
},
|
|
4587
|
+
},
|
|
4588
|
+
|
|
4589
|
+
sort: {
|
|
4590
|
+
startsAt: SortOrder.Descending, // newer state changes first
|
|
4591
|
+
},
|
|
4592
|
+
skip: 0,
|
|
4593
|
+
limit: LIMIT_PER_PROJECT,
|
|
4594
|
+
props: {
|
|
4595
|
+
isRoot: true,
|
|
4596
|
+
},
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
// Fetch active episodes (similar to incidents)
|
|
4601
|
+
let activeEpisodes: Array<IncidentEpisode> = [];
|
|
4602
|
+
let activeEpisodesJson: JSONArray = [];
|
|
4603
|
+
let episodePublicNotes: Array<IncidentEpisodePublicNote> = [];
|
|
4604
|
+
let episodeStateTimelines: Array<IncidentEpisodeStateTimeline> = [];
|
|
4605
|
+
|
|
4606
|
+
/*
|
|
4607
|
+
* Cheap guard before the expensive part: the block below scans every
|
|
4608
|
+
* incident ever attached to this page's monitors (a many-to-many
|
|
4609
|
+
* join, up to LIMIT_PER_PROJECT rows) just to discover episode
|
|
4610
|
+
* membership — on every overview view, even though most pages have
|
|
4611
|
+
* zero active episodes most of the time. One indexed COUNT of the
|
|
4612
|
+
* project's unresolved, visible episodes lets us skip all of it in
|
|
4613
|
+
* the common case. Behavior-preserving: the final activeEpisodes
|
|
4614
|
+
* query applies exactly these three constraints, so count == 0
|
|
4615
|
+
* implies the block's outputs stay empty.
|
|
4616
|
+
*/
|
|
4617
|
+
let unresolvedIncidentStateIds: Array<ObjectID> = [];
|
|
4618
|
+
let hasActiveEpisodes: boolean = false;
|
|
4619
|
+
|
|
4620
|
+
if (
|
|
4621
|
+
statusPage.showEpisodesOnStatusPage &&
|
|
4622
|
+
monitorsOnStatusPage.length > 0
|
|
4623
|
+
) {
|
|
4624
|
+
const unresolvedIncidentStates: Array<IncidentState> =
|
|
4625
|
+
await IncidentStateService.getUnresolvedIncidentStates(
|
|
4626
|
+
statusPage.projectId!,
|
|
4627
|
+
{ isRoot: true },
|
|
4628
|
+
);
|
|
4629
|
+
|
|
4630
|
+
unresolvedIncidentStateIds = unresolvedIncidentStates.map(
|
|
4631
|
+
(state: IncidentState) => {
|
|
4632
|
+
return state.id!;
|
|
4633
|
+
},
|
|
4634
|
+
);
|
|
4635
|
+
|
|
4636
|
+
const activeEpisodeCount: PositiveNumber =
|
|
4637
|
+
await IncidentEpisodeService.countBy({
|
|
4638
|
+
query: {
|
|
4639
|
+
projectId: statusPage.projectId!,
|
|
4640
|
+
isVisibleOnStatusPage: true,
|
|
4641
|
+
currentIncidentStateId: QueryHelper.any(unresolvedIncidentStateIds),
|
|
4642
|
+
},
|
|
4643
|
+
props: {
|
|
4644
|
+
isRoot: true,
|
|
4645
|
+
},
|
|
4646
|
+
});
|
|
4647
|
+
|
|
4648
|
+
hasActiveEpisodes = activeEpisodeCount.toNumber() > 0;
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
if (hasActiveEpisodes) {
|
|
4652
|
+
// First, get incidents that have monitors on status page
|
|
4653
|
+
const incidentsForEpisodes: Array<Incident> =
|
|
4654
|
+
await IncidentService.findBy({
|
|
4655
|
+
query: {
|
|
4656
|
+
monitors: monitorsOnStatusPage as any,
|
|
4657
|
+
isVisibleOnStatusPage: true,
|
|
4658
|
+
projectId: statusPage.projectId!,
|
|
4659
|
+
},
|
|
4660
|
+
select: {
|
|
4661
|
+
_id: true,
|
|
4662
|
+
},
|
|
4663
|
+
skip: 0,
|
|
4664
|
+
limit: LIMIT_PER_PROJECT,
|
|
4665
|
+
props: {
|
|
4666
|
+
isRoot: true,
|
|
4667
|
+
},
|
|
4668
|
+
});
|
|
4669
|
+
|
|
4670
|
+
const incidentIdsForEpisodes: Array<ObjectID> = incidentsForEpisodes.map(
|
|
4671
|
+
(incident: Incident) => {
|
|
4672
|
+
return incident.id!;
|
|
4673
|
+
},
|
|
4674
|
+
);
|
|
4675
|
+
|
|
4676
|
+
// Get episode members for these incidents
|
|
4677
|
+
let episodeMembers: Array<IncidentEpisodeMember> = [];
|
|
4678
|
+
if (incidentIdsForEpisodes.length > 0) {
|
|
4679
|
+
episodeMembers = await IncidentEpisodeMemberService.findBy({
|
|
4680
|
+
query: {
|
|
4681
|
+
incidentId: QueryHelper.any(incidentIdsForEpisodes),
|
|
4682
|
+
projectId: statusPage.projectId!,
|
|
4683
|
+
},
|
|
4684
|
+
select: {
|
|
4685
|
+
incidentEpisodeId: true,
|
|
4686
|
+
incidentId: true,
|
|
4687
|
+
},
|
|
4688
|
+
skip: 0,
|
|
4689
|
+
limit: LIMIT_PER_PROJECT,
|
|
4690
|
+
props: {
|
|
4691
|
+
isRoot: true,
|
|
4692
|
+
},
|
|
4693
|
+
});
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
// Get unique episode IDs
|
|
4697
|
+
const episodeIdsFromMembers: Set<string> = new Set();
|
|
4698
|
+
for (const member of episodeMembers) {
|
|
4699
|
+
if (member.incidentEpisodeId) {
|
|
4700
|
+
episodeIdsFromMembers.add(member.incidentEpisodeId.toString());
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
|
|
4704
|
+
// Fetch active (unresolved) episodes
|
|
4705
|
+
if (episodeIdsFromMembers.size > 0) {
|
|
4706
|
+
// unresolvedIncidentStateIds was fetched by the guard above.
|
|
4707
|
+
let selectEpisodes: Select<IncidentEpisode> = {
|
|
4708
|
+
createdAt: true,
|
|
4709
|
+
declaredAt: true,
|
|
4710
|
+
updatedAt: true,
|
|
4711
|
+
title: true,
|
|
4712
|
+
description: true,
|
|
4713
|
+
_id: true,
|
|
4714
|
+
episodeNumber: true,
|
|
4715
|
+
incidentSeverity: {
|
|
4826
4716
|
name: true,
|
|
4827
4717
|
color: true,
|
|
4718
|
+
},
|
|
4719
|
+
currentIncidentState: {
|
|
4720
|
+
name: true,
|
|
4721
|
+
color: true,
|
|
4722
|
+
_id: true,
|
|
4723
|
+
order: true,
|
|
4828
4724
|
isCreatedState: true,
|
|
4829
4725
|
isAcknowledgedState: true,
|
|
4830
4726
|
isResolvedState: true,
|
|
4831
4727
|
},
|
|
4728
|
+
incidentCount: true,
|
|
4729
|
+
};
|
|
4730
|
+
|
|
4731
|
+
if (statusPage.showEpisodeLabelsOnStatusPage) {
|
|
4732
|
+
selectEpisodes = {
|
|
4733
|
+
...selectEpisodes,
|
|
4734
|
+
labels: {
|
|
4735
|
+
name: true,
|
|
4736
|
+
color: true,
|
|
4737
|
+
},
|
|
4738
|
+
};
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
activeEpisodes = await IncidentEpisodeService.findBy({
|
|
4742
|
+
query: {
|
|
4743
|
+
_id: QueryHelper.any(
|
|
4744
|
+
Array.from(episodeIdsFromMembers).map((id: string) => {
|
|
4745
|
+
return new ObjectID(id);
|
|
4746
|
+
}),
|
|
4747
|
+
),
|
|
4748
|
+
currentIncidentStateId: QueryHelper.any(unresolvedIncidentStateIds),
|
|
4749
|
+
isVisibleOnStatusPage: true,
|
|
4750
|
+
projectId: statusPage.projectId!,
|
|
4751
|
+
},
|
|
4752
|
+
select: selectEpisodes,
|
|
4753
|
+
sort: {
|
|
4754
|
+
declaredAt: SortOrder.Descending,
|
|
4755
|
+
createdAt: SortOrder.Descending,
|
|
4756
|
+
},
|
|
4757
|
+
skip: 0,
|
|
4758
|
+
limit: LIMIT_PER_PROJECT,
|
|
4759
|
+
props: {
|
|
4760
|
+
isRoot: true,
|
|
4761
|
+
},
|
|
4762
|
+
});
|
|
4763
|
+
|
|
4764
|
+
// Build episode monitors map
|
|
4765
|
+
if (activeEpisodes.length > 0) {
|
|
4766
|
+
// Collect all incident IDs from episode members for active episodes
|
|
4767
|
+
const activeEpisodeIds: Set<string> = new Set(
|
|
4768
|
+
activeEpisodes.map((e: IncidentEpisode) => {
|
|
4769
|
+
return e.id!.toString();
|
|
4770
|
+
}),
|
|
4771
|
+
);
|
|
4772
|
+
|
|
4773
|
+
const memberIncidentIds: Array<ObjectID> = [];
|
|
4774
|
+
for (const member of episodeMembers) {
|
|
4775
|
+
if (
|
|
4776
|
+
member.incidentEpisodeId &&
|
|
4777
|
+
activeEpisodeIds.has(member.incidentEpisodeId.toString()) &&
|
|
4778
|
+
member.incidentId &&
|
|
4779
|
+
!memberIncidentIds.some((id: ObjectID) => {
|
|
4780
|
+
return id.toString() === member.incidentId!.toString();
|
|
4781
|
+
})
|
|
4782
|
+
) {
|
|
4783
|
+
memberIncidentIds.push(member.incidentId);
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
// Fetch incidents with monitors
|
|
4788
|
+
let memberIncidents: Array<Incident> = [];
|
|
4789
|
+
if (memberIncidentIds.length > 0) {
|
|
4790
|
+
memberIncidents = await IncidentService.findBy({
|
|
4791
|
+
query: {
|
|
4792
|
+
_id: QueryHelper.any(memberIncidentIds),
|
|
4793
|
+
isVisibleOnStatusPage: true,
|
|
4794
|
+
projectId: statusPage.projectId!,
|
|
4795
|
+
},
|
|
4796
|
+
select: {
|
|
4797
|
+
_id: true,
|
|
4798
|
+
monitors: {
|
|
4799
|
+
_id: true,
|
|
4800
|
+
},
|
|
4801
|
+
},
|
|
4802
|
+
skip: 0,
|
|
4803
|
+
limit: LIMIT_PER_PROJECT,
|
|
4804
|
+
props: {
|
|
4805
|
+
isRoot: true,
|
|
4806
|
+
},
|
|
4807
|
+
});
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
// Build incident -> monitors map
|
|
4811
|
+
const incidentMonitorsMap: Map<string, Array<ObjectID>> = new Map();
|
|
4812
|
+
for (const incident of memberIncidents) {
|
|
4813
|
+
const incidentIdStr: string = incident.id!.toString();
|
|
4814
|
+
const monitorIds: Array<ObjectID> = (incident.monitors || [])
|
|
4815
|
+
.map((m: Monitor) => {
|
|
4816
|
+
return new ObjectID(
|
|
4817
|
+
m._id?.toString() || m.id?.toString() || "",
|
|
4818
|
+
);
|
|
4819
|
+
})
|
|
4820
|
+
.filter((id: ObjectID) => {
|
|
4821
|
+
return id.toString() !== "";
|
|
4822
|
+
});
|
|
4823
|
+
incidentMonitorsMap.set(incidentIdStr, monitorIds);
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4826
|
+
// Build episode -> monitors map
|
|
4827
|
+
const episodeMonitorsMap: Map<string, Array<ObjectID>> = new Map();
|
|
4828
|
+
for (const member of episodeMembers) {
|
|
4829
|
+
if (
|
|
4830
|
+
member.incidentEpisodeId &&
|
|
4831
|
+
member.incidentId &&
|
|
4832
|
+
activeEpisodeIds.has(member.incidentEpisodeId.toString())
|
|
4833
|
+
) {
|
|
4834
|
+
const episodeIdStr: string = member.incidentEpisodeId.toString();
|
|
4835
|
+
const incidentIdStr: string = member.incidentId.toString();
|
|
4836
|
+
|
|
4837
|
+
if (!episodeMonitorsMap.has(episodeIdStr)) {
|
|
4838
|
+
episodeMonitorsMap.set(episodeIdStr, []);
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4841
|
+
const episodeMonitors: Array<ObjectID> =
|
|
4842
|
+
episodeMonitorsMap.get(episodeIdStr)!;
|
|
4843
|
+
const incidentMonitors: Array<ObjectID> =
|
|
4844
|
+
incidentMonitorsMap.get(incidentIdStr) || [];
|
|
4845
|
+
|
|
4846
|
+
for (const monitorId of incidentMonitors) {
|
|
4847
|
+
if (
|
|
4848
|
+
!episodeMonitors.some((m: ObjectID) => {
|
|
4849
|
+
return m.toString() === monitorId.toString();
|
|
4850
|
+
})
|
|
4851
|
+
) {
|
|
4852
|
+
episodeMonitors.push(monitorId);
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
// Serialize episodes and add monitors
|
|
4859
|
+
activeEpisodesJson = BaseModel.toJSONArray(
|
|
4860
|
+
activeEpisodes,
|
|
4861
|
+
IncidentEpisode,
|
|
4862
|
+
);
|
|
4863
|
+
for (const episodeJson of activeEpisodesJson) {
|
|
4864
|
+
const episodeObj: JSONObject = episodeJson as JSONObject;
|
|
4865
|
+
const episodeId: string | undefined = episodeObj["_id"]?.toString();
|
|
4866
|
+
if (episodeId) {
|
|
4867
|
+
const monitorIds: Array<ObjectID> =
|
|
4868
|
+
episodeMonitorsMap.get(episodeId) || [];
|
|
4869
|
+
episodeObj["monitors"] = monitorIds.map((id: ObjectID) => {
|
|
4870
|
+
return { _id: id.toString() };
|
|
4871
|
+
});
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
|
|
4875
|
+
// Get episode public notes
|
|
4876
|
+
const episodesOnStatusPage: Array<ObjectID> = activeEpisodes.map(
|
|
4877
|
+
(episode: IncidentEpisode) => {
|
|
4878
|
+
return episode.id!;
|
|
4879
|
+
},
|
|
4880
|
+
);
|
|
4881
|
+
|
|
4882
|
+
if (episodesOnStatusPage.length > 0) {
|
|
4883
|
+
episodePublicNotes = await IncidentEpisodePublicNoteService.findBy({
|
|
4884
|
+
query: {
|
|
4885
|
+
incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
|
|
4886
|
+
projectId: statusPage.projectId!,
|
|
4887
|
+
},
|
|
4888
|
+
select: {
|
|
4889
|
+
postedAt: true,
|
|
4890
|
+
note: true,
|
|
4891
|
+
incidentEpisodeId: true,
|
|
4892
|
+
attachments: {
|
|
4893
|
+
_id: true,
|
|
4894
|
+
name: true,
|
|
4895
|
+
},
|
|
4896
|
+
},
|
|
4897
|
+
sort: {
|
|
4898
|
+
postedAt: SortOrder.Descending,
|
|
4899
|
+
},
|
|
4900
|
+
skip: 0,
|
|
4901
|
+
limit: LIMIT_PER_PROJECT,
|
|
4902
|
+
props: {
|
|
4903
|
+
isRoot: true,
|
|
4904
|
+
},
|
|
4905
|
+
});
|
|
4906
|
+
|
|
4907
|
+
// Get episode state timelines
|
|
4908
|
+
episodeStateTimelines =
|
|
4909
|
+
await IncidentEpisodeStateTimelineService.findBy({
|
|
4910
|
+
query: {
|
|
4911
|
+
incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
|
|
4912
|
+
projectId: statusPage.projectId!,
|
|
4913
|
+
},
|
|
4914
|
+
select: {
|
|
4915
|
+
_id: true,
|
|
4916
|
+
createdAt: true,
|
|
4917
|
+
startsAt: true,
|
|
4918
|
+
incidentEpisodeId: true,
|
|
4919
|
+
incidentState: {
|
|
4920
|
+
name: true,
|
|
4921
|
+
color: true,
|
|
4922
|
+
isCreatedState: true,
|
|
4923
|
+
isAcknowledgedState: true,
|
|
4924
|
+
isResolvedState: true,
|
|
4925
|
+
},
|
|
4926
|
+
},
|
|
4927
|
+
sort: {
|
|
4928
|
+
startsAt: SortOrder.Descending,
|
|
4929
|
+
},
|
|
4930
|
+
skip: 0,
|
|
4931
|
+
limit: LIMIT_PER_PROJECT,
|
|
4932
|
+
props: {
|
|
4933
|
+
isRoot: true,
|
|
4934
|
+
},
|
|
4935
|
+
});
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
|
|
4941
|
+
// check if status page has active announcement.
|
|
4942
|
+
|
|
4943
|
+
const today: Date = OneUptimeDate.getCurrentDate();
|
|
4944
|
+
|
|
4945
|
+
let activeAnnouncements: Array<StatusPageAnnouncement> = [];
|
|
4946
|
+
|
|
4947
|
+
if (statusPage.showAnnouncementsOnStatusPage) {
|
|
4948
|
+
activeAnnouncements = await StatusPageAnnouncementService.findBy({
|
|
4949
|
+
query: {
|
|
4950
|
+
statusPages: statusPageId as any,
|
|
4951
|
+
showAnnouncementAt: QueryHelper.lessThan(today),
|
|
4952
|
+
endAnnouncementAt: QueryHelper.greaterThanOrNull(today),
|
|
4953
|
+
projectId: statusPage.projectId!,
|
|
4832
4954
|
},
|
|
4833
|
-
|
|
4834
|
-
|
|
4955
|
+
select: {
|
|
4956
|
+
createdAt: true,
|
|
4957
|
+
title: true,
|
|
4958
|
+
description: true,
|
|
4959
|
+
_id: true,
|
|
4960
|
+
showAnnouncementAt: true,
|
|
4961
|
+
endAnnouncementAt: true,
|
|
4835
4962
|
},
|
|
4836
4963
|
skip: 0,
|
|
4837
4964
|
limit: LIMIT_PER_PROJECT,
|
|
@@ -4841,156 +4968,180 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4841
4968
|
});
|
|
4842
4969
|
}
|
|
4843
4970
|
|
|
4844
|
-
//
|
|
4845
|
-
|
|
4846
|
-
|
|
4971
|
+
// check if status page has active scheduled events.
|
|
4972
|
+
|
|
4973
|
+
let scheduledEventsSelect: Select<ScheduledMaintenance> = {
|
|
4974
|
+
createdAt: true,
|
|
4975
|
+
title: true,
|
|
4976
|
+
description: true,
|
|
4977
|
+
_id: true,
|
|
4978
|
+
endsAt: true,
|
|
4979
|
+
startsAt: true,
|
|
4980
|
+
currentScheduledMaintenanceState: {
|
|
4981
|
+
name: true,
|
|
4982
|
+
color: true,
|
|
4983
|
+
isScheduledState: true,
|
|
4984
|
+
isResolvedState: true,
|
|
4985
|
+
isOngoingState: true,
|
|
4986
|
+
},
|
|
4987
|
+
monitors: {
|
|
4988
|
+
_id: true,
|
|
4989
|
+
},
|
|
4990
|
+
};
|
|
4991
|
+
|
|
4992
|
+
if (statusPage.showScheduledEventLabelsOnStatusPage) {
|
|
4993
|
+
scheduledEventsSelect = {
|
|
4994
|
+
...scheduledEventsSelect,
|
|
4995
|
+
labels: {
|
|
4996
|
+
name: true,
|
|
4997
|
+
color: true,
|
|
4998
|
+
},
|
|
4999
|
+
};
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
let scheduledMaintenanceEvents: Array<ScheduledMaintenance> = [];
|
|
5003
|
+
|
|
5004
|
+
if (statusPage.showScheduledMaintenanceEventsOnStatusPage) {
|
|
5005
|
+
scheduledMaintenanceEvents = await ScheduledMaintenanceService.findBy({
|
|
4847
5006
|
query: {
|
|
5007
|
+
currentScheduledMaintenanceState: {
|
|
5008
|
+
isOngoingState: true,
|
|
5009
|
+
} as any,
|
|
5010
|
+
statusPages: statusPageId as any,
|
|
4848
5011
|
projectId: statusPage.projectId!,
|
|
5012
|
+
isVisibleOnStatusPage: true,
|
|
4849
5013
|
},
|
|
4850
|
-
select:
|
|
4851
|
-
|
|
4852
|
-
|
|
5014
|
+
select: scheduledEventsSelect,
|
|
5015
|
+
sort: {
|
|
5016
|
+
startsAt: SortOrder.Ascending,
|
|
4853
5017
|
},
|
|
4854
|
-
limit: LIMIT_PER_PROJECT,
|
|
4855
5018
|
skip: 0,
|
|
5019
|
+
limit: LIMIT_PER_PROJECT,
|
|
4856
5020
|
props: {
|
|
4857
5021
|
isRoot: true,
|
|
4858
5022
|
},
|
|
4859
5023
|
});
|
|
4860
|
-
|
|
4861
|
-
// Serialize episodes and add monitors to each
|
|
4862
|
-
const episodesJson: JSONArray = BaseModel.toJSONArray(
|
|
4863
|
-
episodes,
|
|
4864
|
-
IncidentEpisode,
|
|
4865
|
-
);
|
|
4866
|
-
for (const episodeJson of episodesJson) {
|
|
4867
|
-
const episodeObj: JSONObject = episodeJson as JSONObject;
|
|
4868
|
-
const episodeId: string | undefined = episodeObj["_id"]?.toString();
|
|
4869
|
-
if (episodeId) {
|
|
4870
|
-
const monitorIds: Array<ObjectID> =
|
|
4871
|
-
episodeMonitorsMap.get(episodeId) || [];
|
|
4872
|
-
episodeObj["monitors"] = monitorIds.map((id: ObjectID) => {
|
|
4873
|
-
return { _id: id.toString() };
|
|
4874
|
-
});
|
|
4875
|
-
}
|
|
4876
5024
|
}
|
|
4877
5025
|
|
|
4878
|
-
|
|
4879
|
-
episodePublicNotes: BaseModel.toJSONArray(
|
|
4880
|
-
episodePublicNotes,
|
|
4881
|
-
IncidentEpisodePublicNote,
|
|
4882
|
-
),
|
|
4883
|
-
incidentStates: BaseModel.toJSONArray(incidentStates, IncidentState),
|
|
4884
|
-
episodes: episodesJson,
|
|
4885
|
-
statusPageResources: BaseModel.toJSONArray(
|
|
4886
|
-
statusPageResources,
|
|
4887
|
-
StatusPageResource,
|
|
4888
|
-
),
|
|
4889
|
-
episodeStateTimelines: BaseModel.toJSONArray(
|
|
4890
|
-
episodeStateTimelines,
|
|
4891
|
-
IncidentEpisodeStateTimeline,
|
|
4892
|
-
),
|
|
4893
|
-
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
|
4894
|
-
};
|
|
4895
|
-
|
|
4896
|
-
return response;
|
|
4897
|
-
}
|
|
5026
|
+
let futureScheduledMaintenanceEvents: Array<ScheduledMaintenance> = [];
|
|
4898
5027
|
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
endDateForMonitorTimeline: Date;
|
|
4922
|
-
}> {
|
|
4923
|
-
const objectId: ObjectID = data.statusPageId;
|
|
5028
|
+
if (statusPage.showScheduledMaintenanceEventsOnStatusPage) {
|
|
5029
|
+
futureScheduledMaintenanceEvents =
|
|
5030
|
+
await ScheduledMaintenanceService.findBy({
|
|
5031
|
+
query: {
|
|
5032
|
+
currentScheduledMaintenanceState: {
|
|
5033
|
+
isScheduledState: true,
|
|
5034
|
+
} as any,
|
|
5035
|
+
statusPages: statusPageId as any,
|
|
5036
|
+
projectId: statusPage.projectId!,
|
|
5037
|
+
isVisibleOnStatusPage: true,
|
|
5038
|
+
},
|
|
5039
|
+
select: scheduledEventsSelect,
|
|
5040
|
+
sort: {
|
|
5041
|
+
startsAt: SortOrder.Ascending,
|
|
5042
|
+
},
|
|
5043
|
+
skip: 0,
|
|
5044
|
+
limit: LIMIT_PER_PROJECT,
|
|
5045
|
+
props: {
|
|
5046
|
+
isRoot: true,
|
|
5047
|
+
},
|
|
5048
|
+
});
|
|
5049
|
+
}
|
|
4924
5050
|
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
_id: objectId.toString(),
|
|
4928
|
-
},
|
|
4929
|
-
select: {
|
|
4930
|
-
_id: true,
|
|
4931
|
-
projectId: true,
|
|
4932
|
-
isPublicStatusPage: true,
|
|
4933
|
-
overviewPageDescription: true,
|
|
4934
|
-
showIncidentLabelsOnStatusPage: true,
|
|
4935
|
-
showScheduledEventLabelsOnStatusPage: true,
|
|
4936
|
-
showEpisodeLabelsOnStatusPage: true,
|
|
4937
|
-
downtimeMonitorStatuses: {
|
|
4938
|
-
_id: true,
|
|
4939
|
-
},
|
|
4940
|
-
defaultBarColor: true,
|
|
4941
|
-
showOverallUptimePercentOnStatusPage: true,
|
|
4942
|
-
overallUptimePercentPrecision: true,
|
|
4943
|
-
showAnnouncementsOnStatusPage: true,
|
|
4944
|
-
showIncidentsOnStatusPage: true,
|
|
4945
|
-
showEpisodesOnStatusPage: true,
|
|
4946
|
-
showScheduledMaintenanceEventsOnStatusPage: true,
|
|
4947
|
-
showUptimeHistoryInDays: true,
|
|
4948
|
-
},
|
|
4949
|
-
props: {
|
|
4950
|
-
isRoot: true,
|
|
4951
|
-
},
|
|
5051
|
+
futureScheduledMaintenanceEvents.forEach((event: ScheduledMaintenance) => {
|
|
5052
|
+
scheduledMaintenanceEvents.push(event);
|
|
4952
5053
|
});
|
|
4953
5054
|
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
5055
|
+
const scheduledMaintenanceEventsOnStatusPage: Array<ObjectID> =
|
|
5056
|
+
scheduledMaintenanceEvents.map((event: ScheduledMaintenance) => {
|
|
5057
|
+
return event.id!;
|
|
5058
|
+
});
|
|
4957
5059
|
|
|
4958
|
-
let
|
|
4959
|
-
|
|
4960
|
-
let endDateForMonitorTimeline: Date | undefined =
|
|
4961
|
-
data.endDateForMonitorTimeline;
|
|
5060
|
+
let scheduledMaintenanceEventsPublicNotes: Array<ScheduledMaintenancePublicNote> =
|
|
5061
|
+
[];
|
|
4962
5062
|
|
|
4963
|
-
if (
|
|
4964
|
-
|
|
5063
|
+
if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
|
|
5064
|
+
scheduledMaintenanceEventsPublicNotes =
|
|
5065
|
+
await ScheduledMaintenancePublicNoteService.findBy({
|
|
5066
|
+
query: {
|
|
5067
|
+
scheduledMaintenanceId: QueryHelper.any(
|
|
5068
|
+
scheduledMaintenanceEventsOnStatusPage,
|
|
5069
|
+
),
|
|
5070
|
+
projectId: statusPage.projectId!,
|
|
5071
|
+
},
|
|
5072
|
+
select: {
|
|
5073
|
+
postedAt: true,
|
|
5074
|
+
note: true,
|
|
5075
|
+
scheduledMaintenanceId: true,
|
|
5076
|
+
attachments: {
|
|
5077
|
+
_id: true,
|
|
5078
|
+
name: true,
|
|
5079
|
+
},
|
|
5080
|
+
},
|
|
5081
|
+
sort: {
|
|
5082
|
+
postedAt: SortOrder.Ascending,
|
|
5083
|
+
},
|
|
5084
|
+
skip: 0,
|
|
5085
|
+
limit: LIMIT_PER_PROJECT,
|
|
5086
|
+
props: {
|
|
5087
|
+
isRoot: true,
|
|
5088
|
+
},
|
|
5089
|
+
});
|
|
5090
|
+
}
|
|
4965
5091
|
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
}
|
|
5092
|
+
let scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline> =
|
|
5093
|
+
[];
|
|
4969
5094
|
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
5095
|
+
if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
|
|
5096
|
+
scheduledMaintenanceStateTimelines =
|
|
5097
|
+
await ScheduledMaintenanceStateTimelineService.findBy({
|
|
5098
|
+
query: {
|
|
5099
|
+
scheduledMaintenanceId: QueryHelper.any(
|
|
5100
|
+
scheduledMaintenanceEventsOnStatusPage,
|
|
5101
|
+
),
|
|
5102
|
+
projectId: statusPage.projectId!,
|
|
5103
|
+
},
|
|
5104
|
+
select: {
|
|
5105
|
+
_id: true,
|
|
5106
|
+
createdAt: true,
|
|
5107
|
+
startsAt: true,
|
|
5108
|
+
scheduledMaintenanceId: true,
|
|
5109
|
+
scheduledMaintenanceState: {
|
|
5110
|
+
_id: true,
|
|
5111
|
+
color: true,
|
|
5112
|
+
name: true,
|
|
5113
|
+
isScheduledState: true,
|
|
5114
|
+
isResolvedState: true,
|
|
5115
|
+
isOngoingState: true,
|
|
5116
|
+
},
|
|
5117
|
+
},
|
|
4973
5118
|
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
5119
|
+
sort: {
|
|
5120
|
+
startsAt: SortOrder.Descending, // newer state changes first
|
|
5121
|
+
},
|
|
5122
|
+
skip: 0,
|
|
5123
|
+
limit: LIMIT_PER_PROJECT,
|
|
5124
|
+
props: {
|
|
5125
|
+
isRoot: true,
|
|
5126
|
+
},
|
|
5127
|
+
});
|
|
4977
5128
|
}
|
|
4978
5129
|
|
|
4979
|
-
//get
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
await MonitorStatusService.findBy({
|
|
5130
|
+
// get all status page bar chart rules
|
|
5131
|
+
const statusPageHistoryChartBarColorRules: Array<StatusPageHistoryChartBarColorRule> =
|
|
5132
|
+
await StatusPageHistoryChartBarColorRuleService.findBy({
|
|
4983
5133
|
query: {
|
|
4984
|
-
|
|
5134
|
+
statusPageId: statusPageId,
|
|
4985
5135
|
},
|
|
4986
5136
|
select: {
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
5137
|
+
_id: true,
|
|
5138
|
+
barColor: true,
|
|
5139
|
+
order: true,
|
|
5140
|
+
statusPageId: true,
|
|
5141
|
+
uptimePercentGreaterThanOrEqualTo: true,
|
|
4991
5142
|
},
|
|
4992
5143
|
sort: {
|
|
4993
|
-
|
|
5144
|
+
order: SortOrder.Ascending,
|
|
4994
5145
|
},
|
|
4995
5146
|
skip: 0,
|
|
4996
5147
|
limit: LIMIT_PER_PROJECT,
|
|
@@ -4999,70 +5150,41 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
4999
5150
|
},
|
|
5000
5151
|
});
|
|
5001
5152
|
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
isExpandedByDefault: true,
|
|
5013
|
-
showCurrentStatus: true,
|
|
5014
|
-
showUptimePercent: true,
|
|
5015
|
-
uptimePercentPrecision: true,
|
|
5016
|
-
viewMode: true,
|
|
5017
|
-
rowAxisLabel: true,
|
|
5018
|
-
columnAxisLabel: true,
|
|
5019
|
-
rowAxisValues: true,
|
|
5020
|
-
columnAxisValues: true,
|
|
5021
|
-
parentStatusPageGroupId: true,
|
|
5022
|
-
},
|
|
5023
|
-
sort: {
|
|
5024
|
-
order: SortOrder.Ascending,
|
|
5025
|
-
},
|
|
5026
|
-
skip: 0,
|
|
5027
|
-
limit: LIMIT_PER_PROJECT,
|
|
5028
|
-
props: {
|
|
5029
|
-
isRoot: true,
|
|
5030
|
-
},
|
|
5031
|
-
});
|
|
5032
|
-
|
|
5033
|
-
// get monitors on status page.
|
|
5034
|
-
const statusPageResources: Array<StatusPageResource> =
|
|
5035
|
-
await StatusPageResourceService.findBy({
|
|
5153
|
+
/*
|
|
5154
|
+
* Fetch all incidents (active + resolved) in the timeline date range
|
|
5155
|
+
* for the uptime bar tooltip and click-through
|
|
5156
|
+
*/
|
|
5157
|
+
let timelineIncidents: Array<Incident> = [];
|
|
5158
|
+
if (
|
|
5159
|
+
monitorsOnStatusPage.length > 0 &&
|
|
5160
|
+
statusPage.showIncidentsOnStatusPage
|
|
5161
|
+
) {
|
|
5162
|
+
timelineIncidents = await IncidentService.findBy({
|
|
5036
5163
|
query: {
|
|
5037
|
-
|
|
5164
|
+
monitors: monitorsOnStatusPage as any,
|
|
5165
|
+
declaredAt: QueryHelper.inBetween(startDate, endDate),
|
|
5166
|
+
isVisibleOnStatusPage: true,
|
|
5167
|
+
projectId: statusPage.projectId!,
|
|
5038
5168
|
},
|
|
5039
5169
|
select: {
|
|
5040
|
-
|
|
5041
|
-
|
|
5170
|
+
_id: true,
|
|
5171
|
+
title: true,
|
|
5172
|
+
declaredAt: true,
|
|
5173
|
+
incidentSeverity: {
|
|
5042
5174
|
name: true,
|
|
5043
|
-
|
|
5044
|
-
rowAxisLabel: true,
|
|
5045
|
-
columnAxisLabel: true,
|
|
5175
|
+
color: true,
|
|
5046
5176
|
},
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
order: true,
|
|
5054
|
-
monitor: {
|
|
5177
|
+
currentIncidentState: {
|
|
5178
|
+
_id: true,
|
|
5179
|
+
name: true,
|
|
5180
|
+
color: true,
|
|
5181
|
+
},
|
|
5182
|
+
monitors: {
|
|
5055
5183
|
_id: true,
|
|
5056
|
-
currentMonitorStatusId: true,
|
|
5057
5184
|
},
|
|
5058
|
-
monitorGroupId: true,
|
|
5059
|
-
showUptimePercent: true,
|
|
5060
|
-
uptimePercentPrecision: true,
|
|
5061
|
-
rowAxisValue: true,
|
|
5062
|
-
columnAxisValue: true,
|
|
5063
5185
|
},
|
|
5064
5186
|
sort: {
|
|
5065
|
-
|
|
5187
|
+
declaredAt: SortOrder.Descending,
|
|
5066
5188
|
},
|
|
5067
5189
|
skip: 0,
|
|
5068
5190
|
limit: LIMIT_PER_PROJECT,
|
|
@@ -5070,141 +5192,88 @@ export default class StatusPageAPI extends BaseAPI<
|
|
|
5070
5192
|
isRoot: true,
|
|
5071
5193
|
},
|
|
5072
5194
|
});
|
|
5195
|
+
}
|
|
5073
5196
|
|
|
5074
|
-
const
|
|
5075
|
-
.
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
return Boolean(id); // remove nulls
|
|
5080
|
-
});
|
|
5081
|
-
|
|
5082
|
-
// get monitors in the group.
|
|
5083
|
-
const monitorGroupCurrentStatuses: Dictionary<ObjectID> = {};
|
|
5084
|
-
const monitorsInGroup: Dictionary<Array<ObjectID>> = {};
|
|
5085
|
-
|
|
5086
|
-
// get monitor status charts.
|
|
5087
|
-
const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
|
|
5088
|
-
.map((monitor: StatusPageResource) => {
|
|
5089
|
-
return monitor.monitorId!;
|
|
5090
|
-
})
|
|
5091
|
-
.filter((id: ObjectID) => {
|
|
5092
|
-
return Boolean(id); // remove nulls
|
|
5093
|
-
});
|
|
5094
|
-
|
|
5095
|
-
const monitorsOnStatusPageForTimeline: Array<ObjectID> = statusPageResources
|
|
5096
|
-
.filter((monitor: StatusPageResource) => {
|
|
5097
|
-
return monitor.showStatusHistoryChart || monitor.showUptimePercent;
|
|
5098
|
-
})
|
|
5099
|
-
.map((monitor: StatusPageResource) => {
|
|
5100
|
-
return monitor.monitorId!;
|
|
5101
|
-
})
|
|
5102
|
-
.filter((id: ObjectID) => {
|
|
5103
|
-
return Boolean(id); // remove nulls
|
|
5104
|
-
});
|
|
5105
|
-
|
|
5106
|
-
/*
|
|
5107
|
-
* Batched: this loop used to issue 4 queries per monitor group per page
|
|
5108
|
-
* view (3 inside MonitorGroupService.getCurrentStatus + one duplicate
|
|
5109
|
-
* group-resource fetch), on the hottest public endpoint in the product.
|
|
5110
|
-
* One shared fetch + the already-loaded `monitorStatuses` now serve every
|
|
5111
|
-
* group.
|
|
5112
|
-
*/
|
|
5113
|
-
const monitorGroupResourcesByGroupId: Dictionary<
|
|
5114
|
-
Array<MonitorGroupResource>
|
|
5115
|
-
> =
|
|
5116
|
-
await MonitorGroupService.getMonitorGroupResourcesByGroupIds(
|
|
5117
|
-
monitorGroupIds,
|
|
5118
|
-
);
|
|
5119
|
-
|
|
5120
|
-
const monitorGroupStatuses: Dictionary<MonitorStatus> =
|
|
5121
|
-
await MonitorGroupService.getCurrentStatusesForMonitorGroups({
|
|
5122
|
-
monitorGroupIds: monitorGroupIds,
|
|
5123
|
-
monitorStatuses: monitorStatuses,
|
|
5124
|
-
monitorGroupResources: monitorGroupResourcesByGroupId,
|
|
5197
|
+
const overallStatus: MonitorStatus | null =
|
|
5198
|
+
StatusPageService.getOverallMonitorStatus({
|
|
5199
|
+
statusPageResources,
|
|
5200
|
+
monitorStatuses,
|
|
5201
|
+
monitorGroupCurrentStatuses,
|
|
5125
5202
|
});
|
|
5126
5203
|
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
monitorGroupStatuses[monitorGroupId.toString()];
|
|
5132
|
-
|
|
5133
|
-
if (currentStatus) {
|
|
5134
|
-
monitorGroupCurrentStatuses[monitorGroupId.toString()] =
|
|
5135
|
-
currentStatus.id!;
|
|
5136
|
-
}
|
|
5137
|
-
|
|
5138
|
-
// get monitors in the group.
|
|
5139
|
-
|
|
5140
|
-
const monitorsInGroupIds: Array<ObjectID> = (
|
|
5141
|
-
monitorGroupResourcesByGroupId[monitorGroupId.toString()] || []
|
|
5142
|
-
)
|
|
5143
|
-
.map((resource: MonitorGroupResource) => {
|
|
5144
|
-
return resource.monitorId!;
|
|
5145
|
-
})
|
|
5146
|
-
.filter((id: ObjectID) => {
|
|
5147
|
-
return Boolean(id); // remove nulls
|
|
5148
|
-
});
|
|
5149
|
-
|
|
5150
|
-
const shouldShowTimelineForThisGroup: boolean = Boolean(
|
|
5151
|
-
statusPageResources.find((resource: StatusPageResource) => {
|
|
5152
|
-
return (
|
|
5153
|
-
resource.monitorGroupId?.toString() === monitorGroupId.toString() &&
|
|
5154
|
-
(resource.showStatusHistoryChart || resource.showUptimePercent)
|
|
5155
|
-
);
|
|
5156
|
-
}),
|
|
5157
|
-
);
|
|
5158
|
-
|
|
5159
|
-
for (const monitorId of monitorsInGroupIds) {
|
|
5160
|
-
if (!monitorId) {
|
|
5161
|
-
continue;
|
|
5162
|
-
}
|
|
5163
|
-
|
|
5164
|
-
if (
|
|
5165
|
-
!monitorsOnStatusPage.find((item: ObjectID) => {
|
|
5166
|
-
return item.toString() === monitorId.toString();
|
|
5167
|
-
})
|
|
5168
|
-
) {
|
|
5169
|
-
monitorsOnStatusPage.push(monitorId);
|
|
5170
|
-
}
|
|
5171
|
-
|
|
5172
|
-
// add this to the timeline event for this group.
|
|
5204
|
+
const response: JSONObject = {
|
|
5205
|
+
overallStatus: overallStatus
|
|
5206
|
+
? BaseModel.toJSON(overallStatus, MonitorStatus)
|
|
5207
|
+
: null,
|
|
5173
5208
|
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5209
|
+
scheduledMaintenanceEventsPublicNotes: BaseModel.toJSONArray(
|
|
5210
|
+
scheduledMaintenanceEventsPublicNotes,
|
|
5211
|
+
ScheduledMaintenancePublicNote,
|
|
5212
|
+
),
|
|
5213
|
+
statusPageHistoryChartBarColorRules: BaseModel.toJSONArray(
|
|
5214
|
+
statusPageHistoryChartBarColorRules,
|
|
5215
|
+
StatusPageHistoryChartBarColorRule,
|
|
5216
|
+
),
|
|
5217
|
+
scheduledMaintenanceEvents: BaseModel.toJSONArray(
|
|
5218
|
+
scheduledMaintenanceEvents,
|
|
5219
|
+
ScheduledMaintenance,
|
|
5220
|
+
),
|
|
5221
|
+
activeAnnouncements: BaseModel.toJSONArray(
|
|
5222
|
+
activeAnnouncements,
|
|
5223
|
+
StatusPageAnnouncement,
|
|
5224
|
+
),
|
|
5225
|
+
incidentPublicNotes: BaseModel.toJSONArray(
|
|
5226
|
+
incidentPublicNotes,
|
|
5227
|
+
IncidentPublicNote,
|
|
5228
|
+
),
|
|
5183
5229
|
|
|
5184
|
-
|
|
5185
|
-
}
|
|
5230
|
+
activeIncidents: BaseModel.toJSONArray(activeIncidents, Incident),
|
|
5186
5231
|
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5232
|
+
activeEpisodes: activeEpisodesJson,
|
|
5233
|
+
episodePublicNotes: BaseModel.toJSONArray(
|
|
5234
|
+
episodePublicNotes,
|
|
5235
|
+
IncidentEpisodePublicNote,
|
|
5236
|
+
),
|
|
5237
|
+
episodeStateTimelines: BaseModel.toJSONArray(
|
|
5238
|
+
episodeStateTimelines,
|
|
5239
|
+
IncidentEpisodeStateTimeline,
|
|
5240
|
+
),
|
|
5193
5241
|
|
|
5194
|
-
|
|
5242
|
+
monitorStatusTimelines: BaseModel.toJSONArray(
|
|
5243
|
+
monitorStatusTimelines,
|
|
5244
|
+
MonitorStatusTimeline,
|
|
5245
|
+
),
|
|
5246
|
+
resourceGroups: BaseModel.toJSONArray(statusPageGroups, StatusPageGroup),
|
|
5247
|
+
monitorStatuses: BaseModel.toJSONArray(monitorStatuses, MonitorStatus),
|
|
5248
|
+
statusPageResources: BaseModel.toJSONArray(
|
|
5249
|
+
statusPageResources,
|
|
5250
|
+
StatusPageResource,
|
|
5251
|
+
),
|
|
5252
|
+
incidentStateTimelines: BaseModel.toJSONArray(
|
|
5253
|
+
incidentStateTimelines,
|
|
5254
|
+
IncidentStateTimeline,
|
|
5255
|
+
),
|
|
5256
|
+
statusPage: (() => {
|
|
5257
|
+
const statusPageJson: JSONObject = BaseModel.toJSONObject(
|
|
5258
|
+
statusPage,
|
|
5259
|
+
StatusPage,
|
|
5260
|
+
);
|
|
5261
|
+
delete statusPageJson["projectId"];
|
|
5262
|
+
return statusPageJson;
|
|
5263
|
+
})(),
|
|
5264
|
+
scheduledMaintenanceStateTimelines: BaseModel.toJSONArray(
|
|
5265
|
+
scheduledMaintenanceStateTimelines,
|
|
5266
|
+
ScheduledMaintenanceStateTimeline,
|
|
5267
|
+
),
|
|
5195
5268
|
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
monitorStatusTimelines,
|
|
5202
|
-
statusPage,
|
|
5203
|
-
monitorsOnStatusPage,
|
|
5204
|
-
monitorsInGroup,
|
|
5205
|
-
startDateForMonitorTimeline,
|
|
5206
|
-
endDateForMonitorTimeline,
|
|
5269
|
+
monitorGroupCurrentStatuses: JSONFunctions.serialize(
|
|
5270
|
+
monitorGroupCurrentStatuses,
|
|
5271
|
+
),
|
|
5272
|
+
monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
|
|
5273
|
+
timelineIncidents: BaseModel.toJSONArray(timelineIncidents, Incident),
|
|
5207
5274
|
};
|
|
5275
|
+
|
|
5276
|
+
return response;
|
|
5208
5277
|
}
|
|
5209
5278
|
|
|
5210
5279
|
private async getStatusPageAnnouncementAttachment(
|