move methods to checks, wire up service

This commit is contained in:
Alex Holliday committed 2026-08-17 12:26:49 -07:00
1 parent 6f9642c298
commit 3fc7020a90
6 files changed
+74 -43

No files matched your search

@@ -2,6 +2,7 @@ import type {
Check,
ChecksQueryResult,
ChecksSummary,
DailyCheckBucket,
HardwareChecksResult,
PageSpeedChecksResult,
UptimeChecksResult,
@@ -40,6 +41,8 @@ export interface IChecksRepository {
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
findSummaryByTeamId(teamId: string, dateRange: DateRange): Promise<ChecksSummary>;
findUnevaluatedByMonitorId(monitorId: string, since: number): Promise<Check[]>;
getDailyStatusBuckets(monitorIds: string[], days: number, timezone: string | undefined): Promise<DailyCheckBucket[]>;
// update
//delete
deleteByMonitorId(monitorId: string): Promise<number>;
@@ -329,6 +329,44 @@ class MongoChecksRepository implements IChecksRepository {
return docs.map(this.toEntity);
};
getDailyStatusBuckets = async (monitorIds: string[], days: number, timezone: string | undefined) => {
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
// One extra day so the oldest rendered day is complete; the client enumerates exactly `days` days and ignores the partial extra bucket
const windowStart = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
const results = await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": { $in: objectIds },
createdAt: { $gte: windowStart },
},
},
{
$group: {
_id: {
monitorId: "$metadata.monitorId",
day: { $dateTrunc: { date: "$createdAt", unit: "day", timezone } },
},
totalChecks: { $sum: 1 },
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { "_id.day": 1 } },
{
$project: {
_id: 0,
monitorId: { $toString: "$_id.monitorId" },
date: { $dateToString: { date: "$_id.day", format: "%Y-%m-%d", timezone } },
totalChecks: 1,
upChecks: 1,
downChecks: { $subtract: ["$totalChecks", "$upChecks"] },
avgResponseTime: { $round: ["$avgResponseTime", 0] },
},
},
]);
return results;
};
deleteByMonitorId = async (monitorId: string): Promise<number> => {
const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) });
return result.deletedCount;
@@ -1,4 +1,3 @@
import { DailyCheckBucket } from "@/domain/checks/check.type.js";
import type { StatusPage } from "@/domain/status-pages/status-page.type.js";
export interface IStatusPagesRepository {
@@ -9,7 +8,6 @@ export interface IStatusPagesRepository {
findByCustomDomain(customDomain: string): Promise<StatusPage>;
findByTeamId(teamId: string): Promise<StatusPage[]>;
// collection fetch
getDailyStatusBuckets(monitorIds: string[], days: number, timezone: string | undefined): Promise<DailyCheckBucket[]>;
// update
updateById(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
// delete
@@ -114,44 +114,6 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
return this.mapDocuments(statusPages);
};
getDailyStatusBuckets = async (monitorIds: string[], days: number, timezone: string | undefined) => {
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
// One extra day so the oldest rendered day is complete; the client enumerates exactly `days` days and ignores the partial extra bucket
const windowStart = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
const results = await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": { $in: objectIds },
createdAt: { $gte: windowStart },
},
},
{
$group: {
_id: {
monitorId: "$metadata.monitorId",
day: { $dateTrunc: { date: "$createdAt", unit: "day", timezone } },
},
totalChecks: { $sum: 1 },
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { "_id.day": 1 } },
{
$project: {
_id: 0,
monitorId: { $toString: "$_id.monitorId" },
date: { $dateToString: { date: "$_id.day", format: "%Y-%m-%d", timezone } },
totalChecks: 1,
upChecks: 1,
downChecks: { $subtract: ["$totalChecks", "$upChecks"] },
avgResponseTime: { $round: ["$avgResponseTime", 0] },
},
},
]);
return results;
};
updateById = async (
id: string,
teamId: string,
@@ -5,11 +5,14 @@ import {
DEFAULT_STATUS_PAGE_THEME,
DEFAULT_STATUS_PAGE_THEME_MODE,
PublicStatusPagePayload,
STATUS_PAGE_RANGE_DAYS,
StatusPage,
StatusPageRange,
} from "@/domain/status-pages/status-page.type.js";
import { AppError } from "@/utils/AppError.js";
import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js";
import { Monitor } from "@/domain/monitors/monitor.type.js";
import { IChecksRepository } from "@/domain/checks/check.repository.interface.js";
export interface IStatusPageService {
createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
@@ -26,7 +29,8 @@ export class StatusPageService implements IStatusPageService {
constructor(
private statusPagesRepository: IStatusPagesRepository,
private settingsService: ISettingsService,
private monitorsRepository: IMonitorsRepository
private monitorsRepository: IMonitorsRepository,
private checksRepository: IChecksRepository
) {}
private assertCustomDomainAllowed = (customDomain: string | null | undefined) => {
@@ -116,7 +120,11 @@ export class StatusPageService implements IStatusPageService {
return statusPages.map((sp) => this.normalizeTheme(sp));
};
getPublicStatusPagePayload = async (statusPage: StatusPage, requesterTeamId: string | undefined): Promise<PublicStatusPagePayload> => {
getPublicStatusPagePayload = async (
statusPage: StatusPage,
requesterTeamId: string | undefined,
range: StatusPageRange = "latest"
): Promise<PublicStatusPagePayload> => {
if (!statusPage.isPublished) {
if (!requesterTeamId || statusPage.teamId !== requesterTeamId) {
throw new AppError({ message: "Forbidden", status: 403 });
@@ -128,7 +136,26 @@ export class StatusPageService implements IStatusPageService {
const order = new Map(statusPage.monitors.map((id, i) => [id, i]));
const sorted = [...monitors].sort((a, b) => (order.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(b.id) ?? Number.MAX_SAFE_INTEGER));
return { statusPage, monitors: sorted.map((monitor) => this.toPublicMonitor(monitor, showURL)) };
if (range === "latest") {
return { statusPage, monitors: sorted.map((monitor) => this.toPublicMonitor(monitor, showURL)) };
}
const days = STATUS_PAGE_RANGE_DAYS[range];
const bucketTimezone = statusPage.timezone ?? "Etc/UTC";
const buckets = await this.checksRepository.getDailyStatusBuckets(statusPage.monitors, days, bucketTimezone);
const bucketsByMonitor = Map.groupBy(buckets, (bucket) => bucket.monitorId);
return {
statusPage,
range,
bucketTimezone,
checkTTLDays: (await this.settingsService.getDBSettings()).checkTTL,
monitors: sorted.map((monitor) => ({
...this.toPublicMonitor(monitor, showURL),
recentChecks: [],
dailyChecks: bucketsByMonitor.get(monitor.id) ?? [],
})),
};
};
updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
@@ -63,4 +63,7 @@ export type PublicStatusPageMonitor = Pick<Monitor, "id" | "name" | "type" | "st
export interface PublicStatusPagePayload {
statusPage: StatusPage;
monitors: PublicStatusPageMonitor[];
range?: StatusPageRange;
bucketTimezone?: string;
checkTTLDays?: number;
}