Openstatus www.openstatus.dev

feat: page-components [status-page] [Part 1] (#1747)

* feat: page-components status-page

* fix: status page and maintenance to monitor transformation

* chore: apply zod schema on noop

* chore: review updates

* Revert " chore: review updates"

This reverts commit 4ba6fcb430d02a890578009cdb5d645d10fda360.

* fix: nullable groupOrder

* chore: review updates

authored by

Maximilian Kaske and committed by
GitHub
d749db5e 47b9f368

+345 -113
+175 -105
packages/api/src/router/statusPage.ts
··· 3 3 import { and, eq, inArray, sql } from "@openstatus/db"; 4 4 import { 5 5 maintenance, 6 - monitorsToPages, 7 6 page, 7 + pageComponent, 8 8 pageConfigurationSchema, 9 9 pageSubscriber, 10 10 selectMaintenancePageSchema, 11 + selectPageComponentWithMonitorRelation, 11 12 selectPublicMonitorSchema, 12 13 selectPublicPageSchemaWithRelation, 13 14 selectStatusReportPageSchema, ··· 25 26 getEvents, 26 27 getUptime, 27 28 getWorstVariant, 29 + isMonitorComponent, 28 30 setDataByType, 31 + transformMaintenanceWithPageComponents, 32 + transformStatusReportWithPageComponents, 33 + transformToMaintenancesToMonitors, 34 + transformToMonitorsToStatusReports, 29 35 } from "./statusPage.utils"; 30 36 import { 31 37 getMetricsLatencyMultiProcedure, ··· 77 83 statusReportUpdates: { 78 84 orderBy: (reports, { desc }) => desc(reports.date), 79 85 }, 80 - monitorsToStatusReports: { with: { monitor: true } }, 86 + statusReportsToPageComponents: { with: { pageComponent: true } }, 81 87 }, 82 88 }, 83 89 maintenances: { 84 90 with: { 85 - maintenancesToMonitors: { with: { monitor: true } }, 91 + maintenancesToPageComponents: { with: { pageComponent: true } }, 86 92 }, 87 93 orderBy: (maintenances, { desc }) => desc(maintenances.from), 88 94 }, 89 - monitorsToPages: { 95 + pageComponents: { 90 96 with: { 91 97 monitor: { 92 98 with: { 93 99 incidents: true, 94 100 }, 95 101 }, 96 - monitorGroup: true, 102 + group: true, 97 103 }, 98 - orderBy: (monitorsToPages, { asc }) => asc(monitorsToPages.order), 104 + orderBy: (pageComponents, { asc }) => asc(pageComponents.order), 99 105 }, 106 + pageComponentGroups: true, 100 107 }, 101 108 }); 102 109 103 110 if (!_page) return null; 104 111 105 112 const ws = selectWorkspaceSchema.safeParse(_page.workspace); 113 + const pageComponents = selectPageComponentWithMonitorRelation 114 + .array() 115 + .parse(_page.pageComponents); 106 116 107 117 const configuration = pageConfigurationSchema.safeParse( 108 118 _page.configuration ?? {}, ··· 116 126 const barType = opts.input.barType ?? configuration.data.type; 117 127 // const cardType = opts.input.cardType ?? configuration.data.value; 118 128 119 - const monitors = _page.monitorsToPages 120 - // NOTE: we cannot nested `where` in drizzle to filter active monitors 121 - .filter((m) => !m.monitor.deletedAt && m.monitor.active) 122 - .map((m) => { 123 - const events = getEvents({ 124 - maintenances: _page.maintenances, 125 - incidents: m.monitor.incidents, 126 - reports: _page.statusReports, 127 - monitorId: m.monitor.id, 128 - }); 129 - const status = 130 - events.some((e) => e.type === "incident" && !e.to) && 131 - barType !== "manual" 132 - ? "error" 133 - : events.some((e) => e.type === "report" && !e.to) 134 - ? "degraded" 135 - : events.some( 136 - (e) => 137 - e.type === "maintenance" && 138 - e.to && 139 - e.from.getTime() <= new Date().getTime() && 140 - e.to.getTime() >= new Date().getTime(), 141 - ) 142 - ? "info" 143 - : "success"; 144 - return { 145 - ...m.monitor, 146 - status, 147 - events, 148 - monitorGroupId: m.monitorGroupId, 149 - order: m.order, 150 - groupOrder: m.groupOrder, 151 - }; 129 + const monitorComponents = pageComponents.filter(isMonitorComponent); 130 + 131 + const monitors = monitorComponents.map((c) => { 132 + const events = getEvents({ 133 + maintenances: _page.maintenances, 134 + incidents: c.monitor.incidents ?? [], 135 + reports: _page.statusReports, 136 + monitorId: c.monitor.id, 152 137 }); 138 + const status = 139 + events.some((e) => e.type === "incident" && !e.to) && 140 + barType !== "manual" 141 + ? "error" 142 + : events.some((e) => e.type === "report" && !e.to) 143 + ? "degraded" 144 + : events.some( 145 + (e) => 146 + e.type === "maintenance" && 147 + e.to && 148 + e.from.getTime() <= new Date().getTime() && 149 + e.to.getTime() >= new Date().getTime(), 150 + ) 151 + ? "info" 152 + : "success"; 153 + return { 154 + ...c.monitor, 155 + status, 156 + events, 157 + monitorGroupId: c.groupId, 158 + order: c.order, 159 + groupOrder: c.groupOrder, 160 + }; 161 + }); 153 162 154 163 const status = 155 164 monitors.some((m) => m.status === "error") && barType !== "manual" ··· 163 172 // Get page-wide events (not tied to specific monitors) 164 173 const pageEvents = getEvents({ 165 174 maintenances: _page.maintenances, 166 - incidents: 167 - _page.monitorsToPages.flatMap((m) => m.monitor.incidents) ?? [], 175 + incidents: monitorComponents.flatMap((c) => c.monitor.incidents ?? []), 168 176 reports: _page.statusReports, 169 177 // No monitorId provided, so we get all events for the page 170 178 }); ··· 198 206 return false; 199 207 }); 200 208 201 - const monitorGroups = Array.from( 202 - new Map( 203 - _page.monitorsToPages.map((m) => [ 204 - m.monitorGroup?.id, 205 - m.monitorGroup, 206 - ]), 207 - ) 208 - .values() 209 - .filter(Boolean), 210 - ); 209 + const monitorGroups = _page.pageComponentGroups; 211 210 212 211 // Create trackers array with grouped and ungrouped monitors 213 212 const groupedMap = new Map< ··· 298 297 299 298 const whiteLabel = ws.data?.limits["white-label"] ?? false; 300 299 300 + // Pre-build a Map for O(1) lookups to avoid N+1 query problem 301 + const monitorByIdMap = new Map( 302 + pageComponents 303 + .filter(isMonitorComponent) 304 + .map((c) => [c.monitorId, c.monitor]), 305 + ); 306 + 307 + // Transform statusReports to include monitorsToStatusReports format 308 + const statusReports = _page.statusReports 309 + .sort((a, b) => { 310 + // Sort reports without updates to the beginning 311 + if ( 312 + a.statusReportUpdates.length === 0 && 313 + b.statusReportUpdates.length === 0 314 + ) 315 + return 0; 316 + if (a.statusReportUpdates.length === 0) return -1; 317 + if (b.statusReportUpdates.length === 0) return -1; 318 + return ( 319 + b.statusReportUpdates[ 320 + b.statusReportUpdates.length - 1 321 + ].date.getTime() - 322 + a.statusReportUpdates[ 323 + a.statusReportUpdates.length - 1 324 + ].date.getTime() 325 + ); 326 + }) 327 + .map((report) => 328 + transformStatusReportWithPageComponents(report, monitorByIdMap), 329 + ); 330 + 331 + // Transform maintenances to include maintenancesToMonitors format 332 + const maintenances = _page.maintenances.map((m) => 333 + transformMaintenanceWithPageComponents(m, monitorByIdMap), 334 + ); 335 + 301 336 return selectPublicPageSchemaWithRelation.parse({ 302 337 ..._page, 303 338 monitors, 304 339 monitorGroups, 305 340 trackers, 306 341 incidents: monitors.flatMap((m) => m.incidents) ?? [], 307 - statusReports: 308 - // NOTE: we need to sort the status reports by the first update date 309 - _page.statusReports.sort((a, b) => { 310 - if (a.statusReportUpdates.length === 0) return -1; 311 - if (b.statusReportUpdates.length === 0) return -1; 312 - return ( 313 - b.statusReportUpdates[ 314 - b.statusReportUpdates.length - 1 315 - ].date.getTime() - 316 - a.statusReportUpdates[ 317 - a.statusReportUpdates.length - 1 318 - ].date.getTime() 319 - ); 320 - }) ?? [], 321 - maintenances: _page.maintenances ?? [], 342 + statusReports, 343 + maintenances, 322 344 workspacePlan: _page.workspace.plan, 323 345 status, 324 346 lastEvents, ··· 347 369 eq(maintenance.id, opts.input.id), 348 370 eq(maintenance.pageId, _page.id), 349 371 ), 350 - with: { maintenancesToMonitors: { with: { monitor: true } } }, 372 + with: { 373 + maintenancesToPageComponents: { 374 + with: { pageComponent: { with: { monitor: true } } }, 375 + }, 376 + }, 351 377 }); 352 378 353 379 if (!_maintenance) return null; 354 380 355 - return selectMaintenancePageSchema.parse(_maintenance); 381 + const pageComponents = selectPageComponentWithMonitorRelation 382 + .array() 383 + .parse( 384 + _maintenance.maintenancesToPageComponents.map((m) => m.pageComponent), 385 + ); 386 + 387 + // Transform to expected format (maintenancesToMonitors) 388 + const maintenancesToMonitors = transformToMaintenancesToMonitors( 389 + _maintenance.id, 390 + pageComponents, 391 + ); 392 + 393 + return selectMaintenancePageSchema.parse({ 394 + ..._maintenance, 395 + maintenancesToMonitors, 396 + }); 356 397 }), 357 398 358 399 getUptime: publicProcedure ··· 376 417 with: { 377 418 maintenances: { 378 419 with: { 379 - maintenancesToMonitors: true, 420 + maintenancesToPageComponents: { with: { pageComponent: true } }, 380 421 }, 381 422 }, 382 423 statusReports: { 383 424 with: { 384 - monitorsToStatusReports: true, 425 + statusReportsToPageComponents: { with: { pageComponent: true } }, 385 426 statusReportUpdates: true, 386 427 }, 387 428 }, 388 - monitorsToPages: { 429 + pageComponents: { 389 430 where: inArray( 390 - monitorsToPages.monitorId, 431 + pageComponent.monitorId, 391 432 opts.input.monitorIds.map(Number), 392 433 ), 393 434 with: { ··· 403 444 404 445 if (!_page) return null; 405 446 406 - const monitors = _page.monitorsToPages.filter( 407 - (m) => !m.monitor.deletedAt && m.monitor.active, 408 - ); 447 + const pageComponents = selectPageComponentWithMonitorRelation 448 + .array() 449 + .parse(_page.pageComponents); 450 + 451 + const monitors = pageComponents.filter(isMonitorComponent); 409 452 410 453 if (monitors.length !== opts.input.monitorIds.length) return null; 411 454 412 455 const monitorsByType = { 413 - http: monitors.filter((m) => m.monitor.jobType === "http"), 414 - tcp: monitors.filter((m) => m.monitor.jobType === "tcp"), 415 - dns: monitors.filter((m) => m.monitor.jobType === "dns"), 456 + http: monitors.filter((c) => c.monitor.jobType === "http"), 457 + tcp: monitors.filter((c) => c.monitor.jobType === "tcp"), 458 + dns: monitors.filter((c) => c.monitor.jobType === "dns"), 416 459 }; 417 460 418 461 const proceduresByType = { ··· 425 468 Object.entries(proceduresByType).map(([type, procedure]) => { 426 469 const monitorIds = monitorsByType[ 427 470 type as keyof typeof proceduresByType 428 - ].map((m) => m.monitor.id.toString()); 471 + ].map((c) => c.monitor.id.toString()); 429 472 if (monitorIds.length === 0) return null; 430 473 // NOTE: if manual mode, don't fetch data from tinybird 431 474 return opts.input.barType === "manual" ··· 475 518 ? 30 476 519 : 45; 477 520 478 - return monitors.map((m) => { 479 - const monitorId = m.monitor.id.toString(); 521 + return monitors.map((c) => { 522 + const monitorId = c.monitor.id.toString(); 480 523 const events = getEvents({ 481 524 maintenances: _page.maintenances, 482 - incidents: m.monitor.incidents, 525 + incidents: c.monitor.incidents ?? [], 483 526 reports: _page.statusReports, 484 - monitorId: m.monitor.id, 527 + monitorId: c.monitor.id, 485 528 }); 486 529 const rawData = statusDataByMonitorId.get(monitorId) || []; 487 530 const filledData = ··· 506 549 }); 507 550 508 551 return { 509 - ...selectPublicMonitorSchema.parse(m.monitor), 552 + ...c.monitor, 510 553 data: processedData, 511 554 uptime, 512 555 }; ··· 561 604 eq(statusReport.pageId, _page.id), 562 605 ), 563 606 with: { 564 - monitorsToStatusReports: { with: { monitor: true } }, 607 + statusReportsToPageComponents: { 608 + with: { pageComponent: { with: { monitor: true } } }, 609 + }, 565 610 statusReportUpdates: { 566 611 orderBy: (reports, { desc }) => desc(reports.date), 567 612 }, ··· 570 615 571 616 if (!_report) return null; 572 617 573 - return selectStatusReportPageSchema.parse(_report); 618 + const pageComponents = selectPageComponentWithMonitorRelation 619 + .array() 620 + .parse( 621 + _report.statusReportsToPageComponents.map((r) => r.pageComponent), 622 + ); 623 + 624 + // Transform to expected format (monitorsToStatusReports) 625 + const monitorsToStatusReports = transformToMonitorsToStatusReports( 626 + _report.id, 627 + pageComponents, 628 + ); 629 + 630 + return selectStatusReportPageSchema.parse({ 631 + ..._report, 632 + monitorsToStatusReports, 633 + }); 574 634 }), 575 635 576 636 getNoopReport: publicProcedure.query(async () => { ··· 581 641 const identifiedDate = new Date(date.setMinutes(date.getMinutes() - 32)); 582 642 const investigatingDate = new Date(date.setMinutes(date.getMinutes() - 4)); 583 643 584 - return { 644 + return selectStatusReportPageSchema.parse({ 585 645 id: 1, 586 646 pageId: 1, 587 647 status: "investigating" as const, ··· 658 718 updatedAt: investigatingDate, 659 719 }, 660 720 ], 661 - }; 721 + }); 662 722 }), 663 723 664 724 getMonitors: publicProcedure ··· 667 727 if (!opts.input.slug) return null; 668 728 669 729 // NOTE: revalidate the public monitors first 670 - const data = await opts.ctx.db.query.page.findFirst({ 730 + const _page = await opts.ctx.db.query.page.findFirst({ 671 731 where: sql`lower(${page.slug}) = ${opts.input.slug} OR lower(${page.customDomain}) = ${opts.input.slug}`, 672 732 with: { 673 - monitorsToPages: { 733 + pageComponents: { 674 734 with: { 675 735 monitor: true, 676 736 }, ··· 678 738 }, 679 739 }); 680 740 681 - if (!data) return null; 741 + if (!_page) return null; 682 742 683 - const publicMonitors = data.monitorsToPages.filter( 684 - (m) => m.monitor.public, 685 - ); 743 + const pageComponents = selectPageComponentWithMonitorRelation 744 + .array() 745 + .parse(_page.pageComponents); 746 + 747 + const publicMonitors = pageComponents 748 + .filter(isMonitorComponent) 749 + .filter((c) => c.monitor?.public); 686 750 687 751 const monitorsByType = { 688 - http: publicMonitors.filter((m) => m.monitor.jobType === "http"), 689 - tcp: publicMonitors.filter((m) => m.monitor.jobType === "tcp"), 690 - dns: publicMonitors.filter((m) => m.monitor.jobType === "dns"), 752 + http: publicMonitors.filter((c) => c.monitor.jobType === "http"), 753 + tcp: publicMonitors.filter((c) => c.monitor.jobType === "tcp"), 754 + dns: publicMonitors.filter((c) => c.monitor.jobType === "dns"), 691 755 }; 692 756 693 757 const proceduresByType = { ··· 704 768 Object.entries(proceduresByType).map(([type, procedure]) => { 705 769 const monitorIds = monitorsByType[ 706 770 type as keyof typeof proceduresByType 707 - ].map((m) => m.monitor.id.toString()); 771 + ].map((c) => c.monitor.id.toString()); 708 772 if (monitorIds.length === 0) return null; 709 773 return procedure({ monitorIds }); 710 774 }), ··· 747 811 }); 748 812 } 749 813 750 - return publicMonitors.map((m) => { 751 - const monitorId = m.monitor.id.toString(); 814 + return publicMonitors.map((c) => { 815 + const monitorId = c.monitor.id.toString(); 752 816 const data = metricsDataByMonitorId.get(monitorId) || []; 753 817 754 818 return { 755 - ...selectPublicMonitorSchema.parse(m.monitor), 819 + ...selectPublicMonitorSchema.parse(c.monitor), 756 820 data, 757 821 }; 758 822 }); ··· 766 830 const _page = await opts.ctx.db.query.page.findFirst({ 767 831 where: sql`lower(${page.slug}) = ${opts.input.slug} OR lower(${page.customDomain}) = ${opts.input.slug}`, 768 832 with: { 769 - monitorsToPages: { 770 - where: eq(monitorsToPages.monitorId, opts.input.id), 833 + pageComponents: { 834 + where: eq(pageComponent.monitorId, opts.input.id), 771 835 with: { 772 836 monitor: true, 773 837 }, ··· 777 841 778 842 if (!_page) return null; 779 843 780 - const _monitor = _page.monitorsToPages.find( 781 - (m) => m.monitorId === opts.input.id && m.monitor.active, 844 + const pageComponents = selectPageComponentWithMonitorRelation 845 + .array() 846 + .parse(_page.pageComponents); 847 + 848 + const monitorComponents = pageComponents.filter(isMonitorComponent); 849 + 850 + const _monitor = monitorComponents.find( 851 + (c) => c.monitor.id === opts.input.id, 782 852 )?.monitor; 783 853 784 854 if (!_monitor) return null;
+137 -7
packages/api/src/router/statusPage.utils.ts
··· 1 1 import type { 2 2 Incident, 3 3 Maintenance, 4 + PageComponent, 5 + PageComponentWithMonitorRelation, 4 6 StatusReport, 5 7 StatusReportUpdate, 6 8 } from "@openstatus/db/src/schema"; 7 9 10 + /** 11 + * Type for a monitor component with a non-null monitor relation 12 + */ 13 + export type MonitorComponentWithNonNullMonitor = 14 + PageComponentWithMonitorRelation & { 15 + type: "monitor"; 16 + monitorId: number; 17 + monitor: NonNullable<PageComponentWithMonitorRelation["monitor"]>; 18 + }; 19 + 20 + /** 21 + * Type guard to check if a pageComponent is a monitor type with a monitor relation 22 + * Works with any object that has the shape of a pageComponent with a valid monitor relation 23 + */ 24 + export function isMonitorComponent( 25 + component: PageComponentWithMonitorRelation, 26 + ): component is MonitorComponentWithNonNullMonitor { 27 + return ( 28 + component.type === "monitor" && 29 + component.monitor !== null && 30 + component.monitor !== undefined && 31 + component.monitor.active === true && 32 + component.monitor.deletedAt === null 33 + ); 34 + } 35 + 36 + /** 37 + * Transforms pageComponents to legacy monitorsToStatusReports format 38 + */ 39 + export function transformToMonitorsToStatusReports( 40 + statusReportId: number, 41 + pageComponents: PageComponentWithMonitorRelation[], 42 + ) { 43 + const monitors = pageComponents.filter(isMonitorComponent); 44 + return monitors.map((m) => ({ 45 + statusReportId, 46 + monitorId: m.monitor.id, 47 + monitor: m.monitor, 48 + })); 49 + } 50 + 51 + /** 52 + * Transforms pageComponents to legacy maintenancesToMonitors format 53 + */ 54 + export function transformToMaintenancesToMonitors( 55 + maintenanceId: number, 56 + pageComponents: PageComponentWithMonitorRelation[], 57 + ) { 58 + const monitors = pageComponents.filter(isMonitorComponent); 59 + return monitors.map((m) => ({ 60 + maintenanceId, 61 + monitorId: m.monitor.id, 62 + monitor: m.monitor, 63 + })); 64 + } 65 + 66 + /** 67 + * Transforms statusReportsToPageComponents relations using a monitorByIdMap for performance 68 + */ 69 + export function transformStatusReportWithPageComponents< 70 + T extends { 71 + id: number; 72 + statusReportsToPageComponents: Array<{ 73 + pageComponent: PageComponent | null; 74 + }>; 75 + }, 76 + >( 77 + report: T, 78 + monitorByIdMap: Map< 79 + number, 80 + NonNullable<PageComponentWithMonitorRelation["monitor"]> 81 + >, 82 + ) { 83 + return { 84 + ...report, 85 + monitorsToStatusReports: report.statusReportsToPageComponents.flatMap( 86 + (r) => { 87 + const pc = r.pageComponent; 88 + if (!pc?.monitorId) return []; 89 + const monitor = monitorByIdMap.get(pc.monitorId); 90 + if (!monitor) return []; 91 + return [ 92 + { statusReportId: report.id, monitorId: pc.monitorId, monitor }, 93 + ]; 94 + }, 95 + ), 96 + }; 97 + } 98 + 99 + /** 100 + * Transforms maintenancesToPageComponents relations using a monitorByIdMap for performance 101 + */ 102 + export function transformMaintenanceWithPageComponents< 103 + T extends { 104 + id: number; 105 + maintenancesToPageComponents: Array<{ 106 + pageComponent: PageComponent | null; 107 + }>; 108 + }, 109 + >( 110 + maintenance: T, 111 + monitorByIdMap: Map< 112 + number, 113 + NonNullable<PageComponentWithMonitorRelation["monitor"]> 114 + >, 115 + ) { 116 + return { 117 + ...maintenance, 118 + maintenancesToMonitors: maintenance.maintenancesToPageComponents.flatMap( 119 + (mp) => { 120 + const pc = mp.pageComponent; 121 + if (!pc?.monitorId) return []; 122 + const monitor = monitorByIdMap.get(pc.monitorId); 123 + if (!monitor) return []; 124 + return [ 125 + { maintenanceId: maintenance.id, monitorId: pc.monitorId, monitor }, 126 + ]; 127 + }, 128 + ), 129 + }; 130 + } 131 + 8 132 type StatusData = { 9 133 day: string; 10 134 count: number; ··· 104 228 pastDays = 45, 105 229 }: { 106 230 maintenances: (Maintenance & { 107 - maintenancesToMonitors: { monitorId: number }[]; 231 + maintenancesToPageComponents: { 232 + pageComponent: PageComponent | null; 233 + }[]; 108 234 })[]; 109 235 incidents: Incident[]; 110 236 reports: (StatusReport & { 111 - monitorsToStatusReports: { monitorId: number }[]; 237 + statusReportsToPageComponents: { 238 + pageComponent: PageComponent | null; 239 + }[]; 112 240 statusReportUpdates: StatusReportUpdate[]; 113 241 })[]; 114 242 monitorId?: number; ··· 118 246 const pastThreshod = new Date(); 119 247 pastThreshod.setDate(pastThreshod.getDate() - pastDays); 120 248 121 - // Filter maintenances - if monitorId is provided, filter by monitor, otherwise include all 249 + // Filter maintenances - if monitorId is provided, filter by monitor via pageComponent, otherwise include all 122 250 maintenances 123 251 .filter((maintenance) => 124 252 monitorId 125 - ? maintenance.maintenancesToMonitors.some( 126 - (m) => m.monitorId === monitorId, 253 + ? maintenance.maintenancesToPageComponents.some( 254 + (m) => m.pageComponent?.monitorId === monitorId, 127 255 ) 128 256 : true, 129 257 ) ··· 154 282 }); 155 283 }); 156 284 157 - // Filter reports - if monitorId is provided, filter by monitor, otherwise include all 285 + // Filter reports - if monitorId is provided, filter by monitor via pageComponent, otherwise include all 158 286 reports 159 287 .filter((report) => 160 288 monitorId 161 - ? report.monitorsToStatusReports.some((m) => m.monitorId === monitorId) 289 + ? report.statusReportsToPageComponents.some( 290 + (m) => m.pageComponent?.monitorId === monitorId, 291 + ) 162 292 : true, 163 293 ) 164 294 .map((report) => {
+4
packages/db/src/schema/maintenances/maintenance.ts
··· 7 7 } from "drizzle-orm/sqlite-core"; 8 8 9 9 import { monitor } from "../monitors"; 10 + import { maintenancesToPageComponents } from "../page_components"; 10 11 import { page } from "../pages"; 11 12 import { workspace } from "../workspaces"; 12 13 ··· 63 64 ); 64 65 65 66 export const maintenanceRelations = relations(maintenance, ({ one, many }) => ({ 67 + // Legacy relation - will be deprecated after migration is complete 66 68 maintenancesToMonitors: many(maintenancesToMonitors), 69 + // New relation using pageComponents architecture 70 + maintenancesToPageComponents: many(maintenancesToPageComponents), 67 71 page: one(page, { 68 72 fields: [maintenance.pageId], 69 73 references: [page.id],
+4
packages/db/src/schema/pages/page.ts
··· 3 3 4 4 import { maintenance } from "../maintenances"; 5 5 import { monitorsToPages } from "../monitors"; 6 + import { pageComponentGroup } from "../page_component_groups"; 7 + import { pageComponent } from "../page_components"; 6 8 import { pageSubscriber } from "../page_subscribers"; 7 9 import { statusReport } from "../status_reports"; 8 10 import { workspace } from "../workspaces"; ··· 69 71 references: [workspace.id], 70 72 }), 71 73 pageSubscribers: many(pageSubscriber), 74 + pageComponents: many(pageComponent), 75 + pageComponentGroups: many(pageComponentGroup), 72 76 }));
+21 -1
packages/db/src/schema/shared.ts
··· 4 4 import { selectMaintenanceSchema } from "./maintenances"; 5 5 import { selectMonitorGroupSchema } from "./monitor_groups"; 6 6 import { selectMonitorSchema } from "./monitors"; 7 + import { selectPageComponentGroupSchema } from "./page_component_groups"; 8 + import { selectPageComponentSchema } from "./page_components"; 7 9 import { selectPageSchema } from "./pages"; 8 10 import { 9 11 selectStatusReportSchema, ··· 99 101 .prefault("success"), 100 102 monitorGroupId: z.number().nullable().optional(), 101 103 order: z.number().default(0).optional(), 102 - groupOrder: z.number().default(0).optional(), 104 + groupOrder: z.number().default(0).nullish(), 103 105 }) 104 106 .transform((data) => ({ 105 107 ...data, ··· 154 156 .transform((val) => val ?? "free"), 155 157 whiteLabel: z.boolean().prefault(false), 156 158 }); 159 + 160 + export const selectPageComponentWithMonitorRelation = 161 + selectPageComponentSchema.extend({ 162 + monitor: selectPublicMonitorBaseSchema 163 + .extend({ 164 + incidents: selectIncidentSchema.array().nullish(), 165 + }) 166 + .transform((data) => ({ 167 + ...data, 168 + name: data.externalName || data.name, 169 + })) 170 + .nullish(), 171 + group: selectPageComponentGroupSchema.nullish(), 172 + }); 173 + 174 + export type PageComponentWithMonitorRelation = z.infer< 175 + typeof selectPageComponentWithMonitorRelation 176 + >; 157 177 158 178 export const selectPublicStatusReportSchemaWithRelation = 159 179 selectStatusReportSchema.extend({
+4
packages/db/src/schema/status_reports/status_reports.ts
··· 7 7 } from "drizzle-orm/sqlite-core"; 8 8 9 9 import { monitor } from "../monitors"; 10 + import { statusReportsToPageComponents } from "../page_components"; 10 11 import { page } from "../pages"; 11 12 import { workspace } from "../workspaces"; 12 13 ··· 55 56 export const StatusReportRelations = relations( 56 57 statusReport, 57 58 ({ one, many }) => ({ 59 + // Legacy relation - will be deprecated after migration is complete 58 60 monitorsToStatusReports: many(monitorsToStatusReport), 61 + // New relation using pageComponents architecture 62 + statusReportsToPageComponents: many(statusReportsToPageComponents), 59 63 page: one(page, { 60 64 fields: [statusReport.pageId], 61 65 references: [page.id],