import type { ClickHouseClient } from "@clickhouse/client"; import { db, schema } from "@superlog/db"; import { eq } from "drizzle-orm"; import type { Context, Hono } from "hono"; import { HTTPException } from "hono/http-exception"; import { alertInputSchema, createAlertRecord, deleteAlertRecord, getAlertWithFirings, listAlertsForProject, previewAlert, testAlertById, updateAlertRecord, } from "./alerts-service.js"; import { resolveActiveOrgContext } from "./org-context.js"; type Vars = { userId: string; orgId: string | null }; export { evaluateAlertQuery } from "./alerts-service.js"; export function mountAlerts(app: Hono<{ Variables: Vars }>, opts: { ch: ClickHouseClient }) { const requireAccess = async (c: Context<{ Variables: Vars }>, projectId: string) => { const project = await db.query.projects.findFirst({ where: eq(schema.projects.id, projectId), }); if (!project) throw new HTTPException(404, { message: "project not found" }); const ctx = await resolveActiveOrgContext({ userId: c.var.userId, preferredOrgId: c.var.orgId, }); if (project.orgId !== ctx.org.id) throw new HTTPException(403, { message: "forbidden" }); return { project, user: ctx.user }; }; app.get("/api/projects/:projectId/alerts", async (c) => { const projectId = c.req.param("projectId"); await requireAccess(c, projectId); return c.json(await listAlertsForProject(projectId)); }); app.post("/api/projects/:projectId/alerts", async (c) => { const projectId = c.req.param("projectId"); const { user } = await requireAccess(c, projectId); const body = await c.req.json().catch(() => ({})); const parsed = alertInputSchema.safeParse(body); if (!parsed.success) throw new HTTPException(400, { message: "invalid body" }); return c.json(await createAlertRecord(projectId, user.id, parsed.data)); }); app.get("/api/projects/:projectId/alerts/:id", async (c) => { const projectId = c.req.param("projectId"); const id = c.req.param("id"); await requireAccess(c, projectId); const alert = await getAlertWithFirings(projectId, id); if (!alert) throw new HTTPException(404, { message: "alert not found" }); return c.json(alert); }); app.patch("/api/projects/:projectId/alerts/:id", async (c) => { const projectId = c.req.param("projectId"); const id = c.req.param("id"); await requireAccess(c, projectId); const body = await c.req.json().catch(() => ({})); const parsed = alertInputSchema.partial().safeParse(body); if (!parsed.success) throw new HTTPException(400, { message: "invalid body" }); const updated = await updateAlertRecord(projectId, id, parsed.data); if (!updated) throw new HTTPException(404, { message: "alert not found" }); return c.json(updated); }); app.delete("/api/projects/:projectId/alerts/:id", async (c) => { const projectId = c.req.param("projectId"); const id = c.req.param("id"); const { user } = await requireAccess(c, projectId); await deleteAlertRecord(projectId, id, user.id); return c.json({ ok: true }); }); app.post("/api/projects/:projectId/alerts/preview", async (c) => { const projectId = c.req.param("projectId"); await requireAccess(c, projectId); const body = await c.req.json().catch(() => ({})); const parsed = alertInputSchema.safeParse(body); if (!parsed.success) throw new HTTPException(400, { message: "invalid body" }); return c.json(await previewAlert(opts.ch, projectId, parsed.data)); }); app.post("/api/projects/:projectId/alerts/:id/test", async (c) => { const projectId = c.req.param("projectId"); const id = c.req.param("id"); await requireAccess(c, projectId); const result = await testAlertById(opts.ch, projectId, id); if (!result) throw new HTTPException(404, { message: "alert not found" }); return c.json(result); }); }