Openstatus
www.openstatus.dev
1import { z } from "zod";
2
3import { type SQL, and, asc, desc, eq, gte, schema } from "@openstatus/db";
4import {
5 incidentTable,
6 selectIncidentSchema,
7 selectMonitorSchema,
8} from "@openstatus/db/src/schema";
9
10import { Events } from "@openstatus/analytics";
11import { TRPCError } from "@trpc/server";
12import { createTRPCRouter, protectedProcedure } from "../trpc";
13import { getPeriodDate, periods } from "./utils";
14
15export const incidentRouter = createTRPCRouter({
16 delete: protectedProcedure
17 .meta({ track: Events.DeleteIncident })
18 .input(z.object({ id: z.number() }))
19 .mutation(async (opts) => {
20 const incidentToDelete = await opts.ctx.db
21 .select()
22 .from(schema.incidentTable)
23 .where(
24 and(
25 eq(schema.incidentTable.id, opts.input.id),
26 eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),
27 ),
28 )
29 .get();
30 if (!incidentToDelete) return;
31
32 await opts.ctx.db
33 .delete(schema.incidentTable)
34 .where(eq(schema.incidentTable.id, incidentToDelete.id))
35 .run();
36 }),
37
38 list: protectedProcedure
39 .input(
40 z
41 .object({
42 period: z.enum(periods).optional(),
43 monitorId: z.number().nullish(),
44 order: z.enum(["asc", "desc"]).optional(),
45 })
46 .optional(),
47 )
48 .query(async (opts) => {
49 const whereConditions: SQL[] = [
50 eq(incidentTable.workspaceId, opts.ctx.workspace.id),
51 ];
52
53 if (opts.input?.period) {
54 whereConditions.push(
55 gte(incidentTable.startedAt, getPeriodDate(opts.input.period)),
56 );
57 }
58
59 if (opts.input?.monitorId) {
60 whereConditions.push(eq(incidentTable.monitorId, opts.input.monitorId));
61 }
62
63 const result = await opts.ctx.db.query.incidentTable.findMany({
64 where: and(...whereConditions),
65 with: {
66 monitor: true,
67 },
68 orderBy:
69 opts.input?.order === "asc"
70 ? asc(incidentTable.startedAt)
71 : desc(incidentTable.startedAt),
72 });
73
74 return selectIncidentSchema
75 .extend({
76 monitor: selectMonitorSchema,
77 })
78 .array()
79 .parse(result);
80 }),
81
82 acknowledge: protectedProcedure
83 .meta({ track: Events.AcknowledgeIncident })
84 .input(z.object({ id: z.number() }))
85 .mutation(async (opts) => {
86 const currentIncident = await opts.ctx.db
87 .select()
88 .from(schema.incidentTable)
89 .where(
90 and(
91 eq(schema.incidentTable.id, opts.input.id),
92 eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),
93 ),
94 )
95 .get();
96 if (!currentIncident) {
97 throw new TRPCError({
98 code: "NOT_FOUND",
99 message: "Incident not found",
100 });
101 }
102 if (currentIncident.acknowledgedAt) {
103 throw new TRPCError({
104 code: "BAD_REQUEST",
105 message: "Incident already acknowledged",
106 });
107 }
108 await opts.ctx.db
109 .update(schema.incidentTable)
110 .set({
111 acknowledgedAt: new Date(),
112 acknowledgedBy: opts.ctx.user.id,
113 updatedAt: new Date(),
114 })
115 .where(
116 and(
117 eq(schema.incidentTable.id, opts.input.id),
118 eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),
119 ),
120 );
121 return true;
122 }),
123
124 resolve: protectedProcedure
125 .meta({ track: Events.ResolveIncident })
126 .input(z.object({ id: z.number() }))
127 .mutation(async (opts) => {
128 const currentIncident = await opts.ctx.db
129 .select()
130 .from(schema.incidentTable)
131 .where(
132 and(
133 eq(schema.incidentTable.id, opts.input.id),
134 eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),
135 ),
136 )
137 .get();
138 if (!currentIncident) {
139 throw new TRPCError({
140 code: "NOT_FOUND",
141 message: "Incident not found",
142 });
143 }
144 if (currentIncident.resolvedAt) {
145 throw new TRPCError({
146 code: "BAD_REQUEST",
147 message: "Incident already resolved",
148 });
149 }
150 await opts.ctx.db
151 .update(schema.incidentTable)
152 .set({
153 resolvedAt: new Date(),
154 resolvedBy: opts.ctx.user.id,
155 updatedAt: new Date(),
156 })
157 .where(
158 and(
159 eq(schema.incidentTable.id, opts.input.id),
160 eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id),
161 ),
162 );
163 return true;
164 }),
165});