Openstatus www.openstatus.dev
at main 306 lines 9.1 kB view raw
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});