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.
12 kB ยท 381 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382import { and, eq, inArray, isNull } from "@openstatus/db";import { monitor, selectMonitorSchema } from "@openstatus/db/src/schema";
import { emitAudit } from "../audit";import { requireScope } from "../auth";import { type ServiceContext, withTransaction } from "../context";import type { Monitor } from "../types";import { getMonitorInWorkspace, headersToDbJson, serialiseAssertions,} from "./internal";import { BulkUpdateMonitorsInput, UpdateMonitorConfigInput, UpdateMonitorFollowRedirectsInput, UpdateMonitorGeneralInput, UpdateMonitorOtelInput, UpdateMonitorPublicInput, UpdateMonitorResponseTimeInput, UpdateMonitorRetryInput,} from "./schemas";import { assertMonitorUrlSafe } from "./url-safety";
/** * Apply a whole-object patch in a single UPDATE. `undefined` fields are * left untouched; `degradedAfter: null` clears the column. */export async function updateMonitorConfig(args: { ctx: ServiceContext; input: UpdateMonitorConfigInput;}): Promise<Monitor> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorConfigInput.parse(args.input);
return withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
if (input.url !== undefined) { // `jobType` is absent from this patch, so the stored type decides. assertMonitorUrlSafe({ jobType: existing.jobType, url: input.url }); }
const values: Record<string, unknown> = { updatedAt: new Date() }; if (input.name !== undefined) values.name = input.name; if (input.url !== undefined) values.url = input.url; if (input.method !== undefined) values.method = input.method; if (input.headers !== undefined) { values.headers = headersToDbJson(input.headers); } if (input.body !== undefined) values.body = input.body; if (input.assertions !== undefined) { values.assertions = serialiseAssertions(input.assertions); } if (input.active !== undefined) values.active = input.active; if (input.periodicity !== undefined) values.periodicity = input.periodicity; if (input.regions !== undefined) values.regions = input.regions.join(","); if (input.description !== undefined) values.description = input.description; if (input.public !== undefined) values.public = input.public; if (input.timeout !== undefined) values.timeout = input.timeout; if (input.degradedAfter !== undefined) { values.degradedAfter = input.degradedAfter; } if (input.retry !== undefined) values.retry = input.retry; if (input.followRedirects !== undefined) { values.followRedirects = input.followRedirects; } if (input.grpcService !== undefined) values.grpcService = input.grpcService; if (input.grpcTls !== undefined) values.grpcTls = input.grpcTls; if (input.otelEndpoint !== undefined) { values.otelEndpoint = input.otelEndpoint; } if (input.otelHeaders !== undefined) { values.otelHeaders = headersToDbJson(input.otelHeaders); }
const updated = await tx .update(monitor) .set(values) .where(eq(monitor.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: existing, after: updated, });
return selectMonitorSchema.parse(updated); });}
/** * Update a monitor's "general" fields โ name / endpoint / method / headers / * body / assertions / active. Mirrors the tRPC `updateGeneral` surface and * intentionally allows jobType switching (e.g. HTTP โ TCP) to preserve the * existing dashboard flow. */export async function updateMonitorGeneral(args: { ctx: ServiceContext; input: UpdateMonitorGeneralInput;}): Promise<Monitor> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorGeneralInput.parse(args.input); assertMonitorUrlSafe({ jobType: input.jobType, url: input.url });
return withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
const updated = await tx .update(monitor) .set({ name: input.name, jobType: input.jobType, url: input.url, method: input.method, headers: headersToDbJson(input.headers), body: input.body, active: input.active, assertions: serialiseAssertions(input.assertions), ...(input.grpcService !== undefined ? { grpcService: input.grpcService } : {}), ...(input.grpcTls !== undefined ? { grpcTls: input.grpcTls } : {}), updatedAt: new Date(), }) .where(eq(monitor.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: existing, after: updated, });
return selectMonitorSchema.parse(updated); });}
export async function updateMonitorRetry(args: { ctx: ServiceContext; input: UpdateMonitorRetryInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorRetryInput.parse(args.input); await withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, }); const updated = await tx .update(monitor) .set({ retry: input.retry, updatedAt: new Date() }) .where(eq(monitor.id, existing.id)) .returning() .get(); await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: existing, after: updated, }); });}
export async function updateMonitorFollowRedirects(args: { ctx: ServiceContext; input: UpdateMonitorFollowRedirectsInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorFollowRedirectsInput.parse(args.input); await withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, }); const updated = await tx .update(monitor) .set({ followRedirects: input.followRedirects, updatedAt: new Date() }) .where(eq(monitor.id, existing.id)) .returning() .get(); await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: existing, after: updated, }); });}
export async function updateMonitorOtel(args: { ctx: ServiceContext; input: UpdateMonitorOtelInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorOtelInput.parse(args.input); await withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, }); const updated = await tx .update(monitor) .set({ otelEndpoint: input.otelEndpoint, otelHeaders: headersToDbJson(input.otelHeaders), updatedAt: new Date(), }) .where(eq(monitor.id, existing.id)) .returning() .get(); await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: existing, after: updated, }); });}
export async function updateMonitorPublic(args: { ctx: ServiceContext; input: UpdateMonitorPublicInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorPublicInput.parse(args.input); await withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, }); const updated = await tx .update(monitor) .set({ public: input.public, updatedAt: new Date() }) .where(eq(monitor.id, existing.id)) .returning() .get(); await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: existing, after: updated, }); });}
export async function updateMonitorResponseTime(args: { ctx: ServiceContext; input: UpdateMonitorResponseTimeInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorResponseTimeInput.parse(args.input); await withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, }); const updated = await tx .update(monitor) .set({ timeout: input.timeout, degradedAfter: input.degradedAfter, updatedAt: new Date(), }) .where(eq(monitor.id, existing.id)) .returning() .get(); await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: existing, after: updated, }); });}
/** * Batched update of `public` / `active` across multiple monitors. All ids * must be in the caller's workspace and not soft-deleted; no per-row * not-found check (matches the pre-migration behaviour โ missing ids are * silently ignored). */export async function bulkUpdateMonitors(args: { ctx: ServiceContext; input: BulkUpdateMonitorsInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = BulkUpdateMonitorsInput.parse(args.input); if (input.public === undefined && input.active === undefined) return;
await withTransaction(ctx, async (tx) => { // Fetch current rows so per-monitor audit entries carry a real // `before` snapshot โ the diff is the only way readers can tell // which flag actually flipped for each row. const existingRows = await tx .select() .from(monitor) .where( and( inArray(monitor.id, input.ids), eq(monitor.workspaceId, ctx.workspace.id), isNull(monitor.deletedAt), ), ) .all(); if (existingRows.length === 0) return;
const set: Record<string, unknown> = { updatedAt: new Date() }; if (input.public !== undefined) set.public = input.public; if (input.active !== undefined) set.active = input.active;
// `.returning()` the full rows that actually matched so the audit // loop below attributes only what we wrote and carries the new // snapshot for each monitor. const updatedRows = await tx .update(monitor) .set(set) .where( and( inArray( monitor.id, existingRows.map((r) => r.id), ), eq(monitor.workspaceId, ctx.workspace.id), isNull(monitor.deletedAt), ), ) .returning() .all();
const existingById = new Map(existingRows.map((r) => [r.id, r])); for (const updated of updatedRows) { // `updatedRows` is a strict subset of `existingRows` (same WHERE // + ids derived from `existingRows`), so the lookup always hits. // If it ever doesn't, the update violated that invariant and we // shouldn't emit an audit row at all. const before = existingById.get(updated.id); if (!before) continue; await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: updated.id, before, after: updated, }); } });}