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.
8.1 kB ยท 258 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259import { Events } from "@openstatus/analytics";import { NotFoundError } from "@openstatus/services";import { AddStatusReportUpdateInput, CreateStatusReportInput, addStatusReportUpdate, componentImpactsSchema, createStatusReport, deleteStatusReport, deleteStatusReportUpdate, getStatusReport, listStatusReports, updateStatusReport, updateStatusReportUpdate,} from "@openstatus/services/status-report";import { z } from "zod";
import { toServiceCtx, toTRPCError } from "../service-adapter";import { createTRPCRouter, protectedProcedure } from "../trpc";import { periods } from "./utils";
// tRPC-side input schemas. These preserve the existing wire contract exactly;// each procedure adapts between this shape and the canonical service schema.
const createStatusReportTRPCInput = z.object({ title: z.string(), status: CreateStatusReportInput.shape.status, pageId: z.number(), pageComponents: z.array(z.number()), // nullish like notifySubscribers: clients may send null for "absent" componentImpacts: componentImpactsSchema.nullish(), date: z.coerce.date(), message: z.string(), notifySubscribers: z.boolean().nullish(), incidentId: z.number().int().optional(),});
const createStatusReportUpdateTRPCInput = z.object({ id: z.number().optional(), statusReportId: z.number(), status: AddStatusReportUpdateInput.shape.status, message: z.string(), componentImpacts: componentImpactsSchema.nullish(), date: z.coerce.date().optional(), notifySubscribers: z.boolean().nullish(),});
const updateStatusReportUpdateTRPCInput = z.object({ id: z.number(), statusReportId: z.number().optional(), status: AddStatusReportUpdateInput.shape.status.optional(), message: z.string().optional(), componentImpacts: componentImpactsSchema.nullish(), date: z.coerce.date().optional(),});
const updateStatusTRPCInput = z.object({ id: z.number(), pageComponents: z.array(z.number()).optional(), title: z.string(), status: CreateStatusReportInput.shape.status,});
export const statusReportRouter = createTRPCRouter({ create: protectedProcedure .meta({ track: Events.CreateReport }) .input(createStatusReportTRPCInput) .mutation(async ({ ctx, input }) => { try { const serviceCtx = toServiceCtx(ctx); const { initialUpdate } = await createStatusReport({ ctx: serviceCtx, input: { title: input.title, status: input.status, pageId: input.pageId, pageComponentIds: input.pageComponents, componentImpacts: input.componentImpacts ?? undefined, date: input.date, message: input.message, incidentId: input.incidentId, }, }); // Notification is a separate, client-driven step // (subscriberNotification.statusReport) so creation and dispatch stay // split โ dispatch must run on lambda, this router is Edge-served. return { ...initialUpdate, notifySubscribers: input.notifySubscribers }; } catch (err) { toTRPCError(err); } }),
createStatusReportUpdate: protectedProcedure .meta({ track: Events.CreateReportUpdate }) .input(createStatusReportUpdateTRPCInput) .mutation(async ({ ctx, input }) => { try { const serviceCtx = toServiceCtx(ctx); const { statusReportUpdate } = await addStatusReportUpdate({ ctx: serviceCtx, input: { statusReportId: input.statusReportId, status: input.status, message: input.message, componentImpacts: input.componentImpacts ?? undefined, date: input.date, }, }); return { ...statusReportUpdate, notifySubscribers: input.notifySubscribers, }; } catch (err) { toTRPCError(err); } }),
updateStatusReportUpdate: protectedProcedure .meta({ track: Events.UpdateReportUpdate }) .input(updateStatusReportUpdateTRPCInput) .mutation(async ({ ctx, input }) => { try { return await updateStatusReportUpdate({ ctx: toServiceCtx(ctx), input: { id: input.id, status: input.status, message: input.message, componentImpacts: input.componentImpacts ?? undefined, date: input.date, }, }); } catch (err) { toTRPCError(err); } }),
updateStatus: protectedProcedure .meta({ track: Events.UpdateReport }) .input(updateStatusTRPCInput) .mutation(async ({ ctx, input }) => { try { await updateStatusReport({ ctx: toServiceCtx(ctx), input: { id: input.id, title: input.title, status: input.status, pageComponentIds: input.pageComponents, }, }); } catch (err) { toTRPCError(err); } }),
delete: protectedProcedure .meta({ track: Events.DeleteReport }) .input(z.object({ id: z.number() })) .mutation(async ({ ctx, input }) => { try { await deleteStatusReport({ ctx: toServiceCtx(ctx), input: { id: input.id }, }); } catch (err) { // Preserve the pre-migration idempotent behaviour โ the old tRPC // delete silently succeeded when the row was already gone. if (err instanceof NotFoundError) return; toTRPCError(err); } }),
deleteUpdate: protectedProcedure .meta({ track: Events.DeleteReportUpdate }) .input(z.object({ id: z.number() })) .mutation(async ({ ctx, input }) => { try { await deleteStatusReportUpdate({ ctx: toServiceCtx(ctx), input: { id: input.id }, }); } catch (err) { toTRPCError(err); } }),
get: protectedProcedure .input(z.object({ id: z.number() })) .query(async ({ ctx, input }) => { try { const result = await getStatusReport({ ctx: toServiceCtx(ctx), input: { id: input.id }, }); return narrowPage(result); } catch (err) { toTRPCError(err); } }),
list: protectedProcedure .input( z.object({ period: z.enum(periods).optional(), order: z.enum(["asc", "desc"]).optional(), pageId: z.number().optional(), }), ) .query(async ({ ctx, input }) => { try { const { items } = await listStatusReports({ ctx: toServiceCtx(ctx), input: { pageId: input.pageId, period: input.period, order: input.order ?? "desc", // tRPC consumers (dashboard) want the full set โ no paging UI // today. Pass a sentinel ceiling rather than silently truncating; // Connect's separate handler imposes its own max=100. limit: 10_000, offset: 0, statuses: [], }, }); // Legacy data (pre-pageId-required) can have `pageId = null`, so the // list endpoint silently filters those rows rather than 500-ing the // whole response. `get` still throws โ a direct fetch of an orphan // is a real data-integrity signal. return items.filter(hasPage); } catch (err) { toTRPCError(err); } }),});
/** * Narrow the service's nullable `page` field to a required one for the tRPC * return type. Every status report is expected to have a `pageId` (and * therefore a `page`) by the dashboard's schema; a null page here is a * data-integrity signal we surface rather than silently return. */function narrowPage<T extends { id: number; page: unknown }>( report: T,): T & { page: NonNullable<T["page"]> } { if (report.page == null) { throw new Error( `status report ${report.id} has no associated page (data inconsistency)`, ); } return report as T & { page: NonNullable<T["page"]> };}
function hasPage<T extends { page: unknown }>( report: T,): report is T & { page: NonNullable<T["page"]> } { return report.page != null;}