Openstatus
www.openstatus.dev
1import { z } from "zod";
2
3import {
4 type SQL,
5 and,
6 asc,
7 desc,
8 eq,
9 gte,
10 syncStatusReportToPageComponentDeleteByStatusReport,
11 syncStatusReportToPageComponentInsertMany,
12} from "@openstatus/db";
13import {
14 insertStatusReportUpdateSchema,
15 selectPageComponentSchema,
16 selectPageSchema,
17 selectStatusReportSchema,
18 selectStatusReportUpdateSchema,
19 statusReport,
20 statusReportStatus,
21 statusReportUpdate,
22 statusReportsToPageComponents,
23} from "@openstatus/db/src/schema";
24
25import { Events } from "@openstatus/analytics";
26import { TRPCError } from "@trpc/server";
27import { createTRPCRouter, protectedProcedure } from "../trpc";
28import { getPeriodDate, periods } from "./utils";
29
30export const statusReportRouter = createTRPCRouter({
31 createStatusReportUpdate: protectedProcedure
32 .meta({ track: Events.CreateReportUpdate })
33 .input(
34 insertStatusReportUpdateSchema.extend({
35 notifySubscribers: z.boolean().nullish(),
36 }),
37 )
38 .mutation(async (opts) => {
39 // update parent status report with latest status
40 const _statusReport = await opts.ctx.db
41 .update(statusReport)
42 .set({ status: opts.input.status, updatedAt: new Date() })
43 .where(
44 and(
45 eq(statusReport.id, opts.input.statusReportId),
46 eq(statusReport.workspaceId, opts.ctx.workspace.id),
47 ),
48 )
49 .returning()
50 .get();
51
52 if (!_statusReport) return;
53
54 const { id, ...statusReportUpdateInput } = opts.input;
55
56 const updatedValue = await opts.ctx.db
57 .insert(statusReportUpdate)
58 .values(statusReportUpdateInput)
59 .returning()
60 .get();
61
62 return {
63 ...selectStatusReportUpdateSchema.parse(updatedValue),
64 notifySubscribers: opts.input.notifySubscribers,
65 };
66 }),
67
68 updateStatusReportUpdate: protectedProcedure
69 .meta({ track: Events.UpdateReportUpdate })
70 .input(insertStatusReportUpdateSchema)
71 .mutation(async (opts) => {
72 const statusReportUpdateInput = opts.input;
73
74 if (!statusReportUpdateInput.id) return;
75
76 const currentStatusReportUpdate = await opts.ctx.db
77 .update(statusReportUpdate)
78 .set({ ...statusReportUpdateInput, updatedAt: new Date() })
79 .where(eq(statusReportUpdate.id, statusReportUpdateInput.id))
80 .returning()
81 .get();
82
83 return selectStatusReportUpdateSchema.parse(currentStatusReportUpdate);
84 }),
85
86 list: protectedProcedure
87 .input(
88 z.object({
89 period: z.enum(periods).optional(),
90 order: z.enum(["asc", "desc"]).optional(),
91 pageId: z.number().optional(),
92 }),
93 )
94 .query(async (opts) => {
95 const whereConditions: SQL[] = [
96 eq(statusReport.workspaceId, opts.ctx.workspace.id),
97 ];
98
99 if (opts.input?.period) {
100 whereConditions.push(
101 gte(statusReport.createdAt, getPeriodDate(opts.input.period)),
102 );
103 }
104
105 if (opts.input?.pageId) {
106 whereConditions.push(eq(statusReport.pageId, opts.input.pageId));
107 }
108
109 const result = await opts.ctx.db.query.statusReport.findMany({
110 where: and(...whereConditions),
111 with: {
112 statusReportUpdates: true,
113 statusReportsToPageComponents: { with: { pageComponent: true } },
114 page: { with: { pageComponents: true } },
115 },
116 orderBy: (statusReport) => [
117 opts.input.order === "asc"
118 ? asc(statusReport.createdAt)
119 : desc(statusReport.createdAt),
120 ],
121 });
122
123 return selectStatusReportSchema
124 .extend({
125 updates: z.array(selectStatusReportUpdateSchema).prefault([]),
126 pageComponents: z.array(selectPageComponentSchema).prefault([]),
127 page: selectPageSchema.extend({
128 pageComponents: z.array(selectPageComponentSchema).prefault([]),
129 }),
130 })
131 .array()
132 .parse(
133 result.map((report) => ({
134 ...report,
135 updates: report.statusReportUpdates,
136 pageComponents: report.statusReportsToPageComponents.map(
137 ({ pageComponent }) => pageComponent,
138 ),
139 })),
140 );
141 }),
142
143 create: protectedProcedure
144 .meta({ track: Events.CreateReport })
145 .input(
146 z.object({
147 title: z.string(),
148 status: z.enum(statusReportStatus),
149 pageId: z.number(),
150 pageComponents: z.array(z.number()),
151 date: z.coerce.date(),
152 message: z.string(),
153 notifySubscribers: z.boolean().nullish(),
154 }),
155 )
156 .mutation(async (opts) => {
157 return opts.ctx.db.transaction(async (tx) => {
158 const newStatusReport = await tx
159 .insert(statusReport)
160 .values({
161 workspaceId: opts.ctx.workspace.id,
162 title: opts.input.title,
163 status: opts.input.status,
164 pageId: opts.input.pageId,
165 })
166 .returning()
167 .get();
168
169 const newStatusReportUpdate = await tx
170 .insert(statusReportUpdate)
171 .values({
172 statusReportId: newStatusReport.id,
173 status: opts.input.status,
174 date: opts.input.date,
175 message: opts.input.message,
176 })
177 .returning()
178 .get();
179
180 if (opts.input.pageComponents.length > 0) {
181 await tx
182 .insert(statusReportsToPageComponents)
183 .values(
184 opts.input.pageComponents.map((pageComponent) => ({
185 pageComponentId: pageComponent,
186 statusReportId: newStatusReport.id,
187 })),
188 )
189 .run();
190 // Reverse sync: page components -> monitors (for backward compatibility)
191 await syncStatusReportToPageComponentInsertMany(
192 tx,
193 newStatusReport.id,
194 opts.input.pageComponents,
195 );
196 }
197
198 return {
199 ...newStatusReportUpdate,
200 notifySubscribers: opts.input.notifySubscribers,
201 };
202 });
203 }),
204
205 updateStatus: protectedProcedure
206 .meta({ track: Events.UpdateReport })
207 .input(
208 z.object({
209 id: z.number(),
210 pageComponents: z.array(z.number()),
211 title: z.string(),
212 status: z.enum(statusReportStatus),
213 }),
214 )
215 .mutation(async (opts) => {
216 await opts.ctx.db.transaction(async (tx) => {
217 await tx
218 .update(statusReport)
219 .set({
220 title: opts.input.title,
221 status: opts.input.status,
222 updatedAt: new Date(),
223 })
224 .where(
225 and(
226 eq(statusReport.id, opts.input.id),
227 eq(statusReport.workspaceId, opts.ctx.workspace.id),
228 ),
229 )
230 .run();
231
232 await tx
233 .delete(statusReportsToPageComponents)
234 .where(
235 eq(statusReportsToPageComponents.statusReportId, opts.input.id),
236 )
237 .run();
238 // Reverse sync: delete from monitors (for backward compatibility)
239 await syncStatusReportToPageComponentDeleteByStatusReport(
240 tx,
241 opts.input.id,
242 );
243
244 if (opts.input.pageComponents.length > 0) {
245 await tx
246 .insert(statusReportsToPageComponents)
247 .values(
248 opts.input.pageComponents.map((pageComponent) => ({
249 pageComponentId: pageComponent,
250 statusReportId: opts.input.id,
251 })),
252 )
253 .run();
254 // Reverse sync: page components -> monitors (for backward compatibility)
255 await syncStatusReportToPageComponentInsertMany(
256 tx,
257 opts.input.id,
258 opts.input.pageComponents,
259 );
260 }
261 });
262 }),
263
264 delete: protectedProcedure
265 .meta({ track: Events.DeleteReport })
266 .input(z.object({ id: z.number() }))
267 .mutation(async (opts) => {
268 const whereConditions: SQL[] = [
269 eq(statusReport.id, opts.input.id),
270 eq(statusReport.workspaceId, opts.ctx.workspace.id),
271 ];
272
273 await opts.ctx.db.transaction(async (tx) => {
274 await tx
275 .delete(statusReport)
276 .where(and(...whereConditions))
277 .run();
278 });
279 }),
280
281 deleteUpdate: protectedProcedure
282 .meta({ track: Events.DeleteReportUpdate })
283 .input(z.object({ id: z.number() }))
284 .mutation(async (opts) => {
285 await opts.ctx.db.transaction(async (tx) => {
286 const update = await tx.query.statusReportUpdate.findFirst({
287 where: eq(statusReportUpdate.id, opts.input.id),
288 with: {
289 statusReport: true,
290 },
291 });
292
293 if (update?.statusReport.workspaceId !== opts.ctx.workspace.id) {
294 throw new TRPCError({
295 code: "FORBIDDEN",
296 message: "You are not allowed to delete this update",
297 });
298 }
299
300 await tx
301 .delete(statusReportUpdate)
302 .where(eq(statusReportUpdate.id, opts.input.id))
303 .run();
304 });
305 }),
306});