Openstatus
www.openstatus.dev
1import { z } from "zod";
2
3import {
4 type SQL,
5 and,
6 asc,
7 desc,
8 eq,
9 gte,
10 inArray,
11 syncMaintenanceToPageComponentDeleteByMaintenance,
12 syncMaintenanceToPageComponentInsertMany,
13} from "@openstatus/db";
14import {
15 maintenance,
16 maintenancesToPageComponents,
17 pageComponent,
18 selectMaintenanceSchema,
19 selectPageComponentSchema,
20} from "@openstatus/db/src/schema";
21
22import { Events } from "@openstatus/analytics";
23import { TRPCError } from "@trpc/server";
24import { createTRPCRouter, protectedProcedure } from "../trpc";
25import { getPeriodDate, periods } from "./utils";
26
27export const maintenanceRouter = createTRPCRouter({
28 delete: protectedProcedure
29 .meta({ track: Events.DeleteMaintenance })
30 .input(z.object({ id: z.number() }))
31 .mutation(async (opts) => {
32 return await opts.ctx.db
33 .delete(maintenance)
34 .where(
35 and(
36 eq(maintenance.id, opts.input.id),
37 eq(maintenance.workspaceId, opts.ctx.workspace.id),
38 ),
39 )
40 .returning();
41 }),
42
43 list: protectedProcedure
44 .input(
45 z
46 .object({
47 period: z.enum(periods).optional(),
48 pageId: z.number().optional(),
49 order: z.enum(["asc", "desc"]).optional(),
50 })
51 .optional(),
52 )
53 .query(async (opts) => {
54 const whereConditions: SQL[] = [
55 eq(maintenance.workspaceId, opts.ctx.workspace.id),
56 ];
57
58 if (opts.input?.period) {
59 whereConditions.push(
60 gte(maintenance.createdAt, getPeriodDate(opts.input.period)),
61 );
62 }
63
64 if (opts.input?.pageId) {
65 whereConditions.push(eq(maintenance.pageId, opts.input.pageId));
66 }
67
68 const query = opts.ctx.db.query.maintenance.findMany({
69 where: and(...whereConditions),
70 orderBy:
71 opts.input?.order === "asc"
72 ? asc(maintenance.createdAt)
73 : desc(maintenance.createdAt),
74 with: {
75 maintenancesToPageComponents: { with: { pageComponent: true } },
76 },
77 });
78
79 const result = await query;
80
81 return selectMaintenanceSchema
82 .extend({
83 pageComponents: z.array(selectPageComponentSchema).prefault([]),
84 })
85 .array()
86 .parse(
87 result.map((m) => ({
88 ...m,
89 pageComponents: m.maintenancesToPageComponents.map(
90 ({ pageComponent }) => pageComponent,
91 ),
92 })),
93 );
94 }),
95
96 new: protectedProcedure
97 .meta({ track: Events.CreateMaintenance })
98 .input(
99 z.object({
100 pageId: z.number(),
101 title: z.string(),
102 message: z.string(),
103 startDate: z.coerce.date(),
104 endDate: z.coerce.date(),
105 pageComponents: z.array(z.number()).optional(),
106 notifySubscribers: z.boolean().nullish(),
107 }),
108 )
109 .mutation(async (opts) => {
110 // Check if the user has access to the monitors
111 if (opts.input.pageComponents?.length) {
112 const whereConditions: SQL[] = [
113 eq(pageComponent.workspaceId, opts.ctx.workspace.id),
114 inArray(pageComponent.id, opts.input.pageComponents),
115 ];
116 const pageComponents = await opts.ctx.db
117 .select()
118 .from(pageComponent)
119 .where(and(...whereConditions))
120 .all();
121
122 if (pageComponents.length !== opts.input.pageComponents.length) {
123 throw new TRPCError({
124 code: "BAD_REQUEST",
125 message: "You do not have access to all the page components",
126 });
127 }
128 }
129
130 const newMaintenance = await opts.ctx.db.transaction(async (tx) => {
131 const newMaintenance = await tx
132 .insert(maintenance)
133 .values({
134 pageId: opts.input.pageId,
135 workspaceId: opts.ctx.workspace.id,
136 title: opts.input.title,
137 message: opts.input.message,
138 from: opts.input.startDate,
139 to: opts.input.endDate,
140 })
141 .returning()
142 .get();
143
144 if (opts.input.pageComponents?.length) {
145 await tx.insert(maintenancesToPageComponents).values(
146 opts.input.pageComponents.map((pageComponentId) => ({
147 maintenanceId: newMaintenance.id,
148 pageComponentId,
149 })),
150 );
151 // Sync to monitors (inverse sync for backward compatibility)
152 await syncMaintenanceToPageComponentInsertMany(
153 tx,
154 newMaintenance.id,
155 opts.input.pageComponents,
156 );
157 }
158
159 return newMaintenance;
160 });
161
162 return {
163 ...newMaintenance,
164 notifySubscribers: opts.input.notifySubscribers,
165 };
166 }),
167
168 update: protectedProcedure
169 .meta({ track: Events.UpdateMaintenance })
170 .input(
171 z.object({
172 id: z.number(),
173 title: z.string(),
174 message: z.string(),
175 startDate: z.coerce.date(),
176 endDate: z.coerce.date(),
177 pageComponents: z.array(z.number()).optional(),
178 }),
179 )
180 .mutation(async (opts) => {
181 // Check if the user has access to the monitors
182 if (opts.input.pageComponents?.length) {
183 const whereConditions: SQL[] = [
184 eq(pageComponent.workspaceId, opts.ctx.workspace.id),
185 inArray(pageComponent.id, opts.input.pageComponents),
186 ];
187 const pageComponents = await opts.ctx.db
188 .select()
189 .from(pageComponent)
190 .where(and(...whereConditions))
191 .all();
192
193 if (pageComponents.length !== opts.input.pageComponents.length) {
194 throw new TRPCError({
195 code: "BAD_REQUEST",
196 message: "You do not have access to all the page components",
197 });
198 }
199 }
200
201 await opts.ctx.db.transaction(async (tx) => {
202 const whereConditions: SQL[] = [
203 eq(maintenance.id, opts.input.id),
204 eq(maintenance.workspaceId, opts.ctx.workspace.id),
205 ];
206
207 // Update the maintenance
208 const _maintenance = await tx
209 .update(maintenance)
210 .set({
211 title: opts.input.title,
212 message: opts.input.message,
213 from: opts.input.startDate,
214 to: opts.input.endDate,
215 workspaceId: opts.ctx.workspace.id,
216 updatedAt: new Date(),
217 })
218 .where(and(...whereConditions))
219 .returning()
220 .get();
221
222 // Delete all existing relations
223 await tx
224 .delete(maintenancesToPageComponents)
225 .where(
226 eq(maintenancesToPageComponents.maintenanceId, _maintenance.id),
227 )
228 .run();
229 // Sync to monitors (inverse sync for backward compatibility)
230 await syncMaintenanceToPageComponentDeleteByMaintenance(
231 tx,
232 _maintenance.id,
233 );
234
235 // Create new relations if page components are provided
236 if (opts.input.pageComponents?.length) {
237 await tx.insert(maintenancesToPageComponents).values(
238 opts.input.pageComponents.map((pageComponentId) => ({
239 maintenanceId: _maintenance.id,
240 pageComponentId,
241 })),
242 );
243 // Sync to monitors (inverse sync for backward compatibility)
244 await syncMaintenanceToPageComponentInsertMany(
245 tx,
246 _maintenance.id,
247 opts.input.pageComponents,
248 );
249 }
250
251 return _maintenance;
252 });
253 }),
254});