Openstatus
www.openstatus.dev
1import { and, eq, inArray, sql } from "drizzle-orm";
2
3import type { db } from "./db";
4import {
5 maintenance,
6 maintenancesToMonitors,
7 maintenancesToPageComponents,
8 monitor,
9 monitorsToPages,
10 monitorsToStatusReport,
11 pageComponent,
12 pageComponentGroup,
13 statusReport,
14 statusReportsToPageComponents,
15} from "./schema";
16
17// Type that works with both LibSQLDatabase and SQLiteTransaction
18// Using any to allow both db instance and transaction to be passed
19// biome-ignore lint/suspicious/noExplicitAny: Compatible with both db and transaction
20type DB = typeof db;
21// Extract transaction type from the callback parameter of db.transaction()
22type Transaction = Parameters<Parameters<DB["transaction"]>[0]>[0];
23
24// ============================================================================
25// Monitor Group <-> Page Component Group Sync
26// ============================================================================
27
28/**
29 * Syncs a monitor group insert to page_component_groups
30 */
31export async function syncMonitorGroupInsert(
32 db: DB | Transaction,
33 data: {
34 id: number;
35 workspaceId: number;
36 pageId: number;
37 name: string;
38 },
39) {
40 await db
41 .insert(pageComponentGroup)
42 .values({
43 id: data.id,
44 workspaceId: data.workspaceId,
45 pageId: data.pageId,
46 name: data.name,
47 })
48 .onConflictDoNothing();
49}
50
51/**
52 * Syncs a monitor group delete to page_component_groups
53 */
54export async function syncMonitorGroupDelete(
55 db: DB | Transaction,
56 monitorGroupId: number,
57) {
58 await db
59 .delete(pageComponentGroup)
60 .where(eq(pageComponentGroup.id, monitorGroupId));
61}
62
63/**
64 * Syncs multiple monitor group deletes to page_component_groups
65 */
66export async function syncMonitorGroupDeleteMany(
67 db: DB | Transaction,
68 monitorGroupIds: number[],
69) {
70 if (monitorGroupIds.length === 0) return;
71 await db
72 .delete(pageComponentGroup)
73 .where(inArray(pageComponentGroup.id, monitorGroupIds));
74}
75
76// ============================================================================
77// Monitors to Pages <-> Page Component Sync
78// ============================================================================
79
80/**
81 * Syncs a monitors_to_pages insert to page_component
82 * Requires monitor data to get name and workspace_id
83 */
84export async function syncMonitorsToPageInsert(
85 db: DB | Transaction,
86 data: {
87 monitorId: number;
88 pageId: number;
89 order?: number;
90 monitorGroupId?: number | null;
91 groupOrder?: number;
92 },
93) {
94 // Get monitor data for name and workspace_id (only active monitors)
95 const monitorData = await db
96 .select({
97 id: monitor.id,
98 name: monitor.name,
99 externalName: monitor.externalName,
100 workspaceId: monitor.workspaceId,
101 active: monitor.active,
102 })
103 .from(monitor)
104 .where(eq(monitor.id, data.monitorId))
105 .get();
106
107 // Skip if monitor doesn't exist, has no workspace, or is inactive
108 if (!monitorData || !monitorData.workspaceId || !monitorData.active) return;
109
110 await db
111 .insert(pageComponent)
112 .values({
113 workspaceId: monitorData.workspaceId,
114 pageId: data.pageId,
115 type: "monitor",
116 monitorId: data.monitorId,
117 name: monitorData.externalName || monitorData.name,
118 order: data.order ?? 0,
119 groupId: data.monitorGroupId ?? null,
120 groupOrder: data.groupOrder ?? 0,
121 })
122 .onConflictDoNothing();
123}
124
125/**
126 * Syncs multiple monitors_to_pages inserts to page_component
127 */
128export async function syncMonitorsToPageInsertMany(
129 db: DB | Transaction,
130 items: Array<{
131 monitorId: number;
132 pageId: number;
133 order?: number;
134 monitorGroupId?: number | null;
135 groupOrder?: number;
136 }>,
137) {
138 if (items.length === 0) return;
139
140 // Get all monitor data in one query (only active monitors)
141 const monitorIds = [...new Set(items.map((item) => item.monitorId))];
142 const monitors = await db
143 .select({
144 id: monitor.id,
145 name: monitor.name,
146 externalName: monitor.externalName,
147 workspaceId: monitor.workspaceId,
148 active: monitor.active,
149 })
150 .from(monitor)
151 .where(and(inArray(monitor.id, monitorIds), eq(monitor.active, true)));
152
153 const monitorMap = new Map(monitors.map((m) => [m.id, m]));
154
155 const values = items
156 .map((item) => {
157 const m = monitorMap.get(item.monitorId);
158 // Skip if monitor doesn't exist, has no workspace, or is inactive
159 if (!m || !m.workspaceId || !m.active) return null;
160 return {
161 workspaceId: m.workspaceId,
162 pageId: item.pageId,
163 type: "monitor" as const,
164 monitorId: item.monitorId,
165 name: m.externalName || m.name,
166 order: item.order ?? 0,
167 groupId: item.monitorGroupId ?? null,
168 groupOrder: item.groupOrder ?? 0,
169 };
170 })
171 .filter((v): v is NonNullable<typeof v> => v !== null);
172
173 if (values.length === 0) return;
174
175 await db.insert(pageComponent).values(values).onConflictDoNothing();
176}
177
178/**
179 * Syncs multiple monitors_to_pages upserts to page_component
180 * Updates order, groupId, groupOrder for existing components, inserts new ones
181 */
182export async function syncMonitorsToPageUpsertMany(
183 db: DB | Transaction,
184 items: Array<{
185 monitorId: number;
186 pageId: number;
187 order?: number;
188 monitorGroupId?: number | null;
189 groupOrder?: number;
190 }>,
191) {
192 if (items.length === 0) return;
193
194 // Get all monitor data in one query (only active monitors)
195 const monitorIds = [...new Set(items.map((item) => item.monitorId))];
196 const monitors = await db
197 .select({
198 id: monitor.id,
199 name: monitor.name,
200 externalName: monitor.externalName,
201 workspaceId: monitor.workspaceId,
202 active: monitor.active,
203 })
204 .from(monitor)
205 .where(and(inArray(monitor.id, monitorIds), eq(monitor.active, true)));
206
207 const monitorMap = new Map(monitors.map((m) => [m.id, m]));
208
209 const values = items
210 .map((item) => {
211 const m = monitorMap.get(item.monitorId);
212 // Skip if monitor doesn't exist, has no workspace, or is inactive
213 if (!m || !m.workspaceId || !m.active) return null;
214 return {
215 workspaceId: m.workspaceId,
216 pageId: item.pageId,
217 type: "monitor" as const,
218 monitorId: item.monitorId,
219 name: m.externalName || m.name,
220 order: item.order ?? 0,
221 groupId: item.monitorGroupId ?? null,
222 groupOrder: item.groupOrder ?? 0,
223 };
224 })
225 .filter((v): v is NonNullable<typeof v> => v !== null);
226
227 if (values.length === 0) return;
228
229 // Use onConflictDoUpdate to update existing page components
230 // The unique constraint is on (pageId, monitorId)
231 await db
232 .insert(pageComponent)
233 .values(values)
234 .onConflictDoUpdate({
235 target: [pageComponent.pageId, pageComponent.monitorId],
236 set: {
237 order: sql.raw("excluded.`order`"),
238 groupId: sql.raw("excluded.`group_id`"),
239 groupOrder: sql.raw("excluded.`group_order`"),
240 },
241 });
242}
243
244/**
245 * Syncs a monitors_to_pages delete to page_component
246 */
247export async function syncMonitorsToPageDelete(
248 db: DB | Transaction,
249 data: { monitorId: number; pageId: number },
250) {
251 await db
252 .delete(pageComponent)
253 .where(
254 and(
255 eq(pageComponent.monitorId, data.monitorId),
256 eq(pageComponent.pageId, data.pageId),
257 ),
258 );
259}
260
261/**
262 * REVERSE SYNC: Syncs a page_component delete to monitors_to_pages
263 * Used when pageComponent is the primary table and monitorsToPages is kept for backwards compatibility
264 */
265export async function syncPageComponentToMonitorsToPageDelete(
266 db: DB | Transaction,
267 data: { monitorId: number; pageId: number },
268) {
269 await db
270 .delete(monitorsToPages)
271 .where(
272 and(
273 eq(monitorsToPages.monitorId, data.monitorId),
274 eq(monitorsToPages.pageId, data.pageId),
275 ),
276 );
277}
278
279/**
280 * Syncs monitors_to_pages deletes for a specific page to page_component
281 */
282export async function syncMonitorsToPageDeleteByPage(
283 db: DB | Transaction,
284 pageId: number,
285) {
286 await db
287 .delete(pageComponent)
288 .where(
289 and(eq(pageComponent.pageId, pageId), eq(pageComponent.type, "monitor")),
290 );
291}
292
293/**
294 * Syncs monitors_to_pages deletes for specific monitors to page_component
295 */
296export async function syncMonitorsToPageDeleteByMonitors(
297 db: DB | Transaction,
298 monitorIds: number[],
299) {
300 if (monitorIds.length === 0) return;
301 await db
302 .delete(pageComponent)
303 .where(inArray(pageComponent.monitorId, monitorIds));
304}
305
306/**
307 * REVERSE SYNC: Syncs page_component inserts to monitors_to_pages
308 * Used when pageComponents is the primary table and monitorsToPages is kept for backwards compatibility
309 */
310export async function syncPageComponentToMonitorsToPageInsertMany(
311 db: DB | Transaction,
312 items: Array<{
313 monitorId: number;
314 pageId: number;
315 order?: number;
316 monitorGroupId?: number | null;
317 groupOrder?: number;
318 }>,
319) {
320 if (items.length === 0) return;
321
322 await db
323 .insert(monitorsToPages)
324 .values(
325 items.map((item) => ({
326 monitorId: item.monitorId,
327 pageId: item.pageId,
328 order: item.order ?? 0,
329 monitorGroupId: item.monitorGroupId ?? null,
330 groupOrder: item.groupOrder ?? 0,
331 })),
332 )
333 .onConflictDoNothing();
334}
335
336// ============================================================================
337// Status Report to Monitors <-> Status Report to Page Components Sync
338// ============================================================================
339
340/**
341 * Syncs a status_report_to_monitors insert to status_report_to_page_component
342 */
343export async function syncStatusReportToMonitorInsert(
344 db: DB | Transaction,
345 data: { statusReportId: number; monitorId: number },
346) {
347 // Get the status report's page_id
348 const report = await db
349 .select({ pageId: statusReport.pageId })
350 .from(statusReport)
351 .where(eq(statusReport.id, data.statusReportId))
352 .get();
353
354 // Find matching page_components
355 const components = await db
356 .select({ id: pageComponent.id })
357 .from(pageComponent)
358 .where(
359 and(
360 eq(pageComponent.monitorId, data.monitorId),
361 report?.pageId
362 ? eq(pageComponent.pageId, report.pageId)
363 : // If no page_id on status report, match all page_components with this monitor
364 eq(pageComponent.monitorId, data.monitorId),
365 ),
366 );
367
368 if (components.length === 0) return;
369
370 await db
371 .insert(statusReportsToPageComponents)
372 .values(
373 components.map((c) => ({
374 statusReportId: data.statusReportId,
375 pageComponentId: c.id,
376 })),
377 )
378 .onConflictDoNothing();
379}
380
381/**
382 * Syncs multiple status_report_to_monitors inserts to status_report_to_page_component
383 */
384export async function syncStatusReportToMonitorInsertMany(
385 db: DB | Transaction,
386 statusReportId: number,
387 monitorIds: number[],
388) {
389 if (monitorIds.length === 0) return;
390
391 // Get the status report's page_id
392 const report = await db
393 .select({ pageId: statusReport.pageId })
394 .from(statusReport)
395 .where(eq(statusReport.id, statusReportId))
396 .get();
397
398 // Find matching page_components for all monitors
399 const components = await db
400 .select({ id: pageComponent.id, monitorId: pageComponent.monitorId })
401 .from(pageComponent)
402 .where(
403 and(
404 inArray(pageComponent.monitorId, monitorIds),
405 report?.pageId
406 ? eq(pageComponent.pageId, report.pageId)
407 : // If no page_id, we need to be careful - get components that match the monitor
408 inArray(pageComponent.monitorId, monitorIds),
409 ),
410 );
411
412 if (components.length === 0) return;
413
414 await db
415 .insert(statusReportsToPageComponents)
416 .values(
417 components.map((c) => ({
418 statusReportId,
419 pageComponentId: c.id,
420 })),
421 )
422 .onConflictDoNothing();
423}
424
425/**
426 * Syncs a status_report_to_monitors delete to status_report_to_page_component
427 */
428export async function syncStatusReportToMonitorDelete(
429 db: DB | Transaction,
430 data: { statusReportId: number; monitorId: number },
431) {
432 // Find page_components with this monitor
433 const components = await db
434 .select({ id: pageComponent.id })
435 .from(pageComponent)
436 .where(eq(pageComponent.monitorId, data.monitorId));
437
438 if (components.length === 0) return;
439
440 await db.delete(statusReportsToPageComponents).where(
441 and(
442 eq(statusReportsToPageComponents.statusReportId, data.statusReportId),
443 inArray(
444 statusReportsToPageComponents.pageComponentId,
445 components.map((c) => c.id),
446 ),
447 ),
448 );
449}
450
451/**
452 * Syncs status_report_to_monitors deletes for a specific status report
453 */
454export async function syncStatusReportToMonitorDeleteByStatusReport(
455 db: DB | Transaction,
456 statusReportId: number,
457) {
458 await db
459 .delete(statusReportsToPageComponents)
460 .where(eq(statusReportsToPageComponents.statusReportId, statusReportId));
461}
462
463/**
464 * Syncs status_report_to_monitors deletes for specific monitors
465 */
466export async function syncStatusReportToMonitorDeleteByMonitors(
467 db: DB | Transaction,
468 monitorIds: number[],
469) {
470 if (monitorIds.length === 0) return;
471
472 // Find page_components with these monitors
473 const components = await db
474 .select({ id: pageComponent.id })
475 .from(pageComponent)
476 .where(inArray(pageComponent.monitorId, monitorIds));
477
478 if (components.length === 0) return;
479
480 await db.delete(statusReportsToPageComponents).where(
481 inArray(
482 statusReportsToPageComponents.pageComponentId,
483 components.map((c) => c.id),
484 ),
485 );
486}
487
488/**
489 * Syncs status_report_to_page_component inserts to status_report_to_monitors
490 * This is the inverse of syncStatusReportToMonitorInsertMany
491 */
492export async function syncStatusReportToPageComponentInsertMany(
493 db: DB | Transaction,
494 statusReportId: number,
495 pageComponentIds: number[],
496) {
497 if (pageComponentIds.length === 0) return;
498
499 // Find monitor IDs from the page components
500 // Only get components that have a monitorId (not static components)
501 const components = await db
502 .select({ monitorId: pageComponent.monitorId })
503 .from(pageComponent)
504 .where(
505 and(
506 inArray(pageComponent.id, pageComponentIds),
507 eq(pageComponent.type, "monitor"),
508 ),
509 );
510
511 if (components.length === 0) return;
512
513 // Extract unique monitor IDs (filter out nulls)
514 const monitorIds = [
515 ...new Set(
516 components
517 .map((c) => c.monitorId)
518 .filter((id): id is number => id !== null),
519 ),
520 ];
521
522 if (monitorIds.length === 0) return;
523
524 // Insert into monitorsToStatusReport
525 await db
526 .insert(monitorsToStatusReport)
527 .values(
528 monitorIds.map((monitorId) => ({
529 statusReportId,
530 monitorId,
531 })),
532 )
533 .onConflictDoNothing();
534}
535
536/**
537 * Syncs status_report_to_page_component deletes to status_report_to_monitors
538 * This is the inverse of syncStatusReportToMonitorDeleteByStatusReport
539 * When page components are removed from a status report, remove the corresponding monitors
540 */
541export async function syncStatusReportToPageComponentDeleteByStatusReport(
542 db: DB | Transaction,
543 statusReportId: number,
544) {
545 await db
546 .delete(monitorsToStatusReport)
547 .where(eq(monitorsToStatusReport.statusReportId, statusReportId));
548}
549
550// ============================================================================
551// Maintenance to Monitor <-> Maintenance to Page Component Sync
552// ============================================================================
553
554/**
555 * Syncs a maintenance_to_monitor insert to maintenance_to_page_component
556 */
557export async function syncMaintenanceToMonitorInsert(
558 db: DB | Transaction,
559 data: { maintenanceId: number; monitorId: number },
560) {
561 // Get the maintenance's page_id
562 const maint = await db
563 .select({ pageId: maintenance.pageId })
564 .from(maintenance)
565 .where(eq(maintenance.id, data.maintenanceId))
566 .get();
567
568 // Find matching page_components
569 const components = await db
570 .select({ id: pageComponent.id })
571 .from(pageComponent)
572 .where(
573 and(
574 eq(pageComponent.monitorId, data.monitorId),
575 maint?.pageId
576 ? eq(pageComponent.pageId, maint.pageId)
577 : eq(pageComponent.monitorId, data.monitorId),
578 ),
579 );
580
581 if (components.length === 0) return;
582
583 await db
584 .insert(maintenancesToPageComponents)
585 .values(
586 components.map((c) => ({
587 maintenanceId: data.maintenanceId,
588 pageComponentId: c.id,
589 })),
590 )
591 .onConflictDoNothing();
592}
593
594/**
595 * Syncs multiple maintenance_to_monitor inserts to maintenance_to_page_component
596 */
597export async function syncMaintenanceToMonitorInsertMany(
598 db: DB | Transaction,
599 maintenanceId: number,
600 monitorIds: number[],
601) {
602 if (monitorIds.length === 0) return;
603
604 // Get the maintenance's page_id
605 const maint = await db
606 .select({ pageId: maintenance.pageId })
607 .from(maintenance)
608 .where(eq(maintenance.id, maintenanceId))
609 .get();
610
611 // Find matching page_components for all monitors
612 const components = await db
613 .select({ id: pageComponent.id, monitorId: pageComponent.monitorId })
614 .from(pageComponent)
615 .where(
616 and(
617 inArray(pageComponent.monitorId, monitorIds),
618 maint?.pageId
619 ? eq(pageComponent.pageId, maint.pageId)
620 : inArray(pageComponent.monitorId, monitorIds),
621 ),
622 );
623
624 if (components.length === 0) return;
625
626 await db
627 .insert(maintenancesToPageComponents)
628 .values(
629 components.map((c) => ({
630 maintenanceId,
631 pageComponentId: c.id,
632 })),
633 )
634 .onConflictDoNothing();
635}
636
637/**
638 * Syncs a maintenance_to_monitor delete to maintenance_to_page_component
639 */
640export async function syncMaintenanceToMonitorDelete(
641 db: DB | Transaction,
642 data: { maintenanceId: number; monitorId: number },
643) {
644 // Find page_components with this monitor
645 const components = await db
646 .select({ id: pageComponent.id })
647 .from(pageComponent)
648 .where(eq(pageComponent.monitorId, data.monitorId));
649
650 if (components.length === 0) return;
651
652 await db.delete(maintenancesToPageComponents).where(
653 and(
654 eq(maintenancesToPageComponents.maintenanceId, data.maintenanceId),
655 inArray(
656 maintenancesToPageComponents.pageComponentId,
657 components.map((c) => c.id),
658 ),
659 ),
660 );
661}
662
663/**
664 * Syncs maintenance_to_monitor deletes for a specific maintenance
665 */
666export async function syncMaintenanceToMonitorDeleteByMaintenance(
667 db: DB | Transaction,
668 maintenanceId: number,
669) {
670 await db
671 .delete(maintenancesToPageComponents)
672 .where(eq(maintenancesToPageComponents.maintenanceId, maintenanceId));
673}
674
675/**
676 * Syncs maintenance_to_monitor deletes for specific monitors
677 */
678export async function syncMaintenanceToMonitorDeleteByMonitors(
679 db: DB | Transaction,
680 monitorIds: number[],
681) {
682 if (monitorIds.length === 0) return;
683
684 // Find page_components with these monitors
685 const components = await db
686 .select({ id: pageComponent.id })
687 .from(pageComponent)
688 .where(inArray(pageComponent.monitorId, monitorIds));
689
690 if (components.length === 0) return;
691
692 await db.delete(maintenancesToPageComponents).where(
693 inArray(
694 maintenancesToPageComponents.pageComponentId,
695 components.map((c) => c.id),
696 ),
697 );
698}
699
700/**
701 * Syncs maintenance_to_page_component inserts to maintenance_to_monitors
702 * This is the inverse of syncMaintenanceToMonitorInsertMany
703 */
704export async function syncMaintenanceToPageComponentInsertMany(
705 db: DB | Transaction,
706 maintenanceId: number,
707 pageComponentIds: number[],
708) {
709 if (pageComponentIds.length === 0) return;
710
711 // Find monitor IDs from the page components
712 // Only get components that have a monitorId (not static components)
713 const components = await db
714 .select({ monitorId: pageComponent.monitorId })
715 .from(pageComponent)
716 .where(
717 and(
718 inArray(pageComponent.id, pageComponentIds),
719 eq(pageComponent.type, "monitor"),
720 ),
721 );
722
723 if (components.length === 0) return;
724
725 // Extract unique monitor IDs (filter out nulls)
726 const monitorIds = [
727 ...new Set(
728 components
729 .map((c) => c.monitorId)
730 .filter((id): id is number => id !== null),
731 ),
732 ];
733
734 if (monitorIds.length === 0) return;
735
736 // Insert into maintenancesToMonitors
737 await db
738 .insert(maintenancesToMonitors)
739 .values(
740 monitorIds.map((monitorId) => ({
741 maintenanceId,
742 monitorId,
743 })),
744 )
745 .onConflictDoNothing();
746}
747
748/**
749 * Syncs maintenance_to_page_component deletes to maintenance_to_monitors
750 * This is the inverse of syncMaintenanceToMonitorDeleteByMaintenance
751 * When page components are removed from a maintenance, remove the corresponding monitors
752 */
753export async function syncMaintenanceToPageComponentDeleteByMaintenance(
754 db: DB | Transaction,
755 maintenanceId: number,
756) {
757 await db
758 .delete(maintenancesToMonitors)
759 .where(eq(maintenancesToMonitors.maintenanceId, maintenanceId));
760}