Openstatus www.openstatus.dev
at main 760 lines 21 kB view raw
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}