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.
5.4 kB ยท 159 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160import { and, eq, inArray, isNull } from "@openstatus/db";import { monitor, monitorTagsToMonitors, notificationsToMonitors, pageComponent, privateLocationToMonitors,} from "@openstatus/db/src/schema";
import { emitAudit } from "../audit";import { requireScope } from "../auth";import { type ServiceContext, withTransaction } from "../context";import { NotFoundError } from "../errors";import { DeleteMonitorInput, DeleteMonitorsInput } from "./schemas";
/** * Soft-delete a single monitor. Sets `deleted_at` + `active = false` and * tears down the tag / notification / page-component associations in the * same transaction. */export async function deleteMonitor(args: { ctx: ServiceContext; input: DeleteMonitorInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = DeleteMonitorInput.parse(args.input);
await withTransaction(ctx, async (tx) => { // Filter out already soft-deleted rows โ otherwise a second call would // re-run the side effects and emit a duplicate audit. const existing = await tx .select() .from(monitor) .where( and( eq(monitor.id, input.id), eq(monitor.workspaceId, ctx.workspace.id), isNull(monitor.deletedAt), ), ) .get(); if (!existing) throw new NotFoundError("monitor", input.id);
await tx .update(monitor) .set({ deletedAt: new Date(), active: false }) .where(eq(monitor.id, existing.id)); await tx .delete(monitorTagsToMonitors) .where(eq(monitorTagsToMonitors.monitorId, existing.id)); await tx .delete(notificationsToMonitors) .where(eq(notificationsToMonitors.monitorId, existing.id)); await tx .delete(pageComponent) .where(eq(pageComponent.monitorId, existing.id)); // Also tear down `privateLocationToMonitors` โ this join table was // previously left behind on soft-delete, leaving orphaned // `(privateLocationId, monitorId)` rows pointing at a tombstoned // monitor. Scheduling queries that read this join can then try to // route probes to a deleted monitor. await tx .delete(privateLocationToMonitors) .where(eq(privateLocationToMonitors.monitorId, existing.id));
await emitAudit(tx, ctx, { action: "monitor.delete", entityType: "monitor", entityId: existing.id, before: existing, }); });}
/** * Bulk soft-delete. Requires all ids resolve to monitors in the caller's * workspace (cross-workspace or already-deleted ids fail the pre-check). */export async function deleteMonitors(args: { ctx: ServiceContext; input: DeleteMonitorsInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = DeleteMonitorsInput.parse(args.input);
await withTransaction(ctx, async (tx) => { const ids = Array.from(new Set(input.ids)); const existing = await tx .select() .from(monitor) .where( and( inArray(monitor.id, ids), eq(monitor.workspaceId, ctx.workspace.id), isNull(monitor.deletedAt), ), ) .all(); if (existing.length !== ids.length) { // Report the first *actually missing* id rather than always // blaming `ids[0]`, which misled callers whenever a mix of valid // and invalid ids was passed (valid `ids[0]` still appeared in // the error even though a later id was the real miss). const foundIds = new Set(existing.map((r) => r.id)); const missingId = ids.find((id) => !foundIds.has(id)) ?? -1; throw new NotFoundError("monitor", missingId); }
// Layered defense: repeat `workspaceId` on every mutation that has // the column, even though the pre-check above already validated // all ids belong to the caller's workspace (same transaction, // deduped ids). The belt-and-braces `AND workspace_id = ?` closes // the copy-paste footgun if this block ever gets reused in a // context without the pre-check. `monitorTagsToMonitors` and // `notificationsToMonitors` don't carry `workspaceId` โ they're // scoped through the `monitor` FK's cascade, so the pre-check is // the only practical guard there. await tx .update(monitor) .set({ deletedAt: new Date(), active: false }) .where( and( inArray(monitor.id, ids), eq(monitor.workspaceId, ctx.workspace.id), ), ); await tx .delete(monitorTagsToMonitors) .where(inArray(monitorTagsToMonitors.monitorId, ids)); await tx .delete(notificationsToMonitors) .where(inArray(notificationsToMonitors.monitorId, ids)); await tx .delete(pageComponent) .where( and( inArray(pageComponent.monitorId, ids), eq(pageComponent.workspaceId, ctx.workspace.id), ), ); // Match the single-delete cleanup โ bulk soft-delete also has to // strip `privateLocationToMonitors` so scheduling queries don't // keep routing probes to tombstoned monitors. await tx .delete(privateLocationToMonitors) .where(inArray(privateLocationToMonitors.monitorId, ids));
for (const row of existing) { await emitAudit(tx, ctx, { action: "monitor.delete", entityType: "monitor", entityId: row.id, before: row, }); } });}