Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
7.8 kB ยท 255 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256import { type SQL, and, asc, db as defaultDb, desc, eq, inArray, isNull, sql,} from "@openstatus/db";import { monitorIncidentTable, monitor, monitorTag, monitorTagsToMonitors, notification, notificationsToMonitors, privateLocation, privateLocationToMonitors, selectMonitorIncidentSchema, selectMonitorSchema, selectMonitorTagSchema, selectNotificationSchema, selectPrivateLocationSchema,} from "@openstatus/db/src/schema";
import type { DB, ServiceContext } from "../context";import type { MonitorIncident, Monitor, MonitorTag, Notification, PrivateLocation,} from "../types";import { getMonitorInWorkspace } from "./internal";import { GetMonitorInput, ListMonitorsInput } from "./schemas";
export type MonitorListItem = Monitor & { tags: MonitorTag[]; incidents: MonitorIncident[];};
export type MonitorWithRelations = Monitor & { tags: MonitorTag[]; incidents: MonitorIncident[]; notifications: Notification[]; privateLocations: PrivateLocation[];};
export type ListMonitorsResult = { items: MonitorListItem[]; totalSize: number;};
/** * Batched enrichment for a list of monitors โ loads tags + incidents in * two IN queries (three if notifications / privateLocations are also * requested). Avoids the per-row pattern that pairs badly with dashboards * that don't paginate. */async function enrichMonitorsBatch( db: DB, rows: Array<typeof monitor.$inferSelect>, workspaceId: number, include: { notifications?: boolean; privateLocations?: boolean } = {},): Promise<MonitorWithRelations[]> { if (rows.length === 0) return []; const ids = rows.map((r) => r.id);
// Explicit column selection (not `select()`) keeps the join row shape in // our hands instead of relying on drizzle's auto-derived `row.<table_name>` // keys, which are named after the JS variable and silently break on // schema rename. const [tagRows, incidentRows, notifRows, locRows] = await Promise.all([ db .select({ monitorId: monitorTagsToMonitors.monitorId, tag: monitorTag, }) .from(monitorTag) .innerJoin( monitorTagsToMonitors, eq(monitorTagsToMonitors.monitorTagId, monitorTag.id), ) .where( and( inArray(monitorTagsToMonitors.monitorId, ids), eq(monitorTag.workspaceId, workspaceId), ), ) .all(), db .select() .from(monitorIncidentTable) .where( and( inArray(monitorIncidentTable.monitorId, ids), // Scope to caller's workspace โ defence-in-depth in case an // incident.monitorId somehow points cross-workspace. The // `incident.monitorId` FK doesn't enforce workspace ownership. eq(monitorIncidentTable.workspaceId, workspaceId), ), ) .all(), include.notifications ? db .select({ monitorId: notificationsToMonitors.monitorId, notification, }) .from(notification) .innerJoin( notificationsToMonitors, eq(notificationsToMonitors.notificationId, notification.id), ) .where( and( inArray(notificationsToMonitors.monitorId, ids), eq(notification.workspaceId, workspaceId), ), ) .all() : Promise.resolve([]), include.privateLocations ? db .select({ monitorId: privateLocationToMonitors.monitorId, location: privateLocation, }) .from(privateLocation) .innerJoin( privateLocationToMonitors, eq(privateLocationToMonitors.privateLocationId, privateLocation.id), ) .where( and( inArray(privateLocationToMonitors.monitorId, ids), eq(privateLocation.workspaceId, workspaceId), ), ) .all() : Promise.resolve([]), ]);
const tagsByMonitor = new Map<number, MonitorTag[]>(); for (const row of tagRows) { const tag = selectMonitorTagSchema.parse(row.tag); const arr = tagsByMonitor.get(row.monitorId); if (arr) arr.push(tag); else tagsByMonitor.set(row.monitorId, [tag]); }
const incidentsByMonitor = new Map<number, MonitorIncident[]>(); for (const row of incidentRows) { if (row.monitorId == null) continue; const incident = selectMonitorIncidentSchema.parse(row); const arr = incidentsByMonitor.get(row.monitorId); if (arr) arr.push(incident); else incidentsByMonitor.set(row.monitorId, [incident]); }
const notifsByMonitor = new Map<number, Notification[]>(); for (const row of notifRows) { const parsed = selectNotificationSchema.parse(row.notification); const arr = notifsByMonitor.get(row.monitorId); if (arr) arr.push(parsed); else notifsByMonitor.set(row.monitorId, [parsed]); }
const locsByMonitor = new Map<number, PrivateLocation[]>(); for (const row of locRows) { if (row.monitorId == null) continue; const parsed = selectPrivateLocationSchema.parse(row.location); const arr = locsByMonitor.get(row.monitorId); if (arr) arr.push(parsed); else locsByMonitor.set(row.monitorId, [parsed]); }
return rows.map((r) => ({ ...selectMonitorSchema.parse(r), tags: tagsByMonitor.get(r.id) ?? [], incidents: incidentsByMonitor.get(r.id) ?? [], notifications: notifsByMonitor.get(r.id) ?? [], privateLocations: locsByMonitor.get(r.id) ?? [], }));}
export async function listMonitors(args: { ctx: ServiceContext; input: ListMonitorsInput;}): Promise<ListMonitorsResult> { const { ctx } = args; const input = ListMonitorsInput.parse(args.input); const db = ctx.db ?? defaultDb;
const conditions: SQL[] = [ eq(monitor.workspaceId, ctx.workspace.id), isNull(monitor.deletedAt), ]; const whereClause = and(...conditions);
const rows = await db .select() .from(monitor) .where(whereClause) .orderBy( input.order === "asc" ? asc(monitor.active) : desc(monitor.active), input.order === "asc" ? asc(monitor.createdAt) : desc(monitor.createdAt), ) .limit(input.limit) .offset(input.offset) .all();
// A short page is the last page, so the total is already known and the // extra `count(*)` is only paid when a full page comes back โ or when an // empty page leaves it ambiguous whether we ran off the end. The tRPC // `list` procedure discards `totalSize` entirely. let totalSize = input.offset + rows.length; if (rows.length === input.limit || (rows.length === 0 && input.offset > 0)) { const countRow = await db .select({ count: sql<number>`count(*)` }) .from(monitor) .where(whereClause) .get(); totalSize = countRow?.count ?? totalSize; }
const enriched = await enrichMonitorsBatch(db, rows, ctx.workspace.id); // `list` only exposes tags + incidents to match the tRPC `list` shape. const items: MonitorListItem[] = enriched.map( ({ notifications: _n, privateLocations: _p, ...rest }) => rest, ); return { items, totalSize };}
export async function getMonitor(args: { ctx: ServiceContext; input: GetMonitorInput;}): Promise<MonitorWithRelations> { const { ctx } = args; const input = GetMonitorInput.parse(args.input); const db = ctx.db ?? defaultDb; const record = await getMonitorInWorkspace({ tx: db, id: input.id, workspaceId: ctx.workspace.id, }); const [enriched] = await enrichMonitorsBatch(db, [record], ctx.workspace.id, { notifications: true, privateLocations: true, }); // oxlint-disable-next-line typescript/no-non-null-assertion -- always defined for len === 1 return enriched!;}