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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102import { and, eq, isNull } from "@openstatus/db";import { type Incident, incident, incidentPostmortem,} from "@openstatus/db/src/schema";
import { emitAudit } from "../audit";import { requireScope } from "../auth";import { requireRole } from "../auth/require-role";import { type DB, type ServiceContext, withTransaction } from "../context";import { ConflictError } from "../errors";import { appendIncidentEvent, assertNotClosed, getIncidentInWorkspace,} from "./internal";import { CloseIncidentInput } from "./schemas";
/** The close itself, inside a caller's transaction; checks are the caller's. */export async function closeIncidentInTx( tx: DB, ctx: ServiceContext, existing: Incident,): Promise<Incident> { if (existing.status !== "resolved") { throw new ConflictError( `Incident #${existing.id} must be resolved before it is closed`, ); } const now = new Date(); const updated = await tx .update(incident) .set({ closedAt: now, updatedAt: now }) .where( and( eq(incident.id, existing.id), eq(incident.status, "resolved"), isNull(incident.closedAt), ), ) .returning() .get(); if (!updated) { throw new ConflictError(`Incident #${existing.id} changed concurrently`); } await emitAudit(tx, ctx, { action: "incident.update", entityType: "incident", entityId: updated.id, before: existing, after: updated, }); await appendIncidentEvent(tx, ctx, { incidentId: updated.id, type: "closed", message: "Incident closed", createdAt: now, }); return updated;}
/** * Ends the incident's life: only a resolved one, by an admin, owner or its * commander, and only with an approved postmortem unless explicitly skipped. */// The update and its `incident.update` audit row live in `closeIncidentInTx`.// oxlint-disable-next-line openstatus/services-mutation-guardsexport async function closeIncident(args: { ctx: ServiceContext; input: CloseIncidentInput;}): Promise<Incident> { const { ctx } = args; requireScope(ctx, "write"); const input = CloseIncidentInput.parse(args.input);
return withTransaction(ctx, async (tx) => { const existing = await getIncidentInWorkspace( tx, ctx.workspace.id, input.id, ); assertNotClosed(existing); await requireRole(tx, ctx, ["owner", "admin"], { orUserId: existing.commanderId, }); if (!input.skipPostmortem) { const postmortem = await tx .select({ status: incidentPostmortem.status }) .from(incidentPostmortem) .where(eq(incidentPostmortem.incidentId, existing.id)) .get(); if (postmortem?.status !== "approved") { throw new ConflictError( "Approve the postmortem first, or close without one", ); } } return closeIncidentInTx(tx, ctx, existing); });}