Openstatus www.openstatus.dev

refactor: move report within status page (#908)

* refactor: move report within status page

* chore: improve report header

* fix: empty state action button

* ๐Ÿš€ migrations

* ๐Ÿš€ server

* ๐Ÿงน clean clean clean

* ๐Ÿงน clean

* ๐Ÿงช

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿš€

* ๐Ÿ™ revert

* ๐Ÿš€ migrations

* ๐Ÿ˜ญ

* ๐Ÿ˜ญ

* ๐Ÿ˜ small update

* ๐Ÿ™ fix missing status to monitor

* ๐Ÿ”ฅ fix migration

* ๐Ÿš€ fix tests

* ๐Ÿš€ test fixed

* fix: billing monitor number

* fix: missing pageId on status report form

* fix: missing pageId

* chore: remove comment

---------

Co-authored-by: Thibault Le Ouay <thibaultleouay@gmail.Com>

authored by

Maximilian Kaske
Thibault Le Ouay
and committed by
GitHub
f7ea9b6a aae294eb

+2622 -704
+13 -22
apps/server/src/public/status.ts
··· 1 1 import { Hono } from "hono"; 2 2 import { endTime, setMetric, startTime } from "hono/timing"; 3 3 4 - import { and, db, eq, gte, inArray, isNull, lte } from "@openstatus/db"; 4 + import { and, db, eq, gte, inArray, isNull, lte, ne } from "@openstatus/db"; 5 5 import { 6 6 incidentTable, 7 7 maintenance, ··· 9 9 monitorsToPages, 10 10 monitorsToStatusReport, 11 11 page, 12 - pagesToStatusReports, 13 12 statusReport, 14 13 } from "@openstatus/db/src/schema"; 15 14 import { Status, Tracker } from "@openstatus/tracker"; ··· 53 52 } = await getStatusPageData(currentPage.id); 54 53 endTime(c, "database"); 55 54 56 - console.log(maintenanceData); 57 - 58 - const statusReports = [ 59 - ...pageStatusReportData, 60 - ...monitorStatusReportData, 61 - ].map((item) => { 55 + const statusReports = [...monitorStatusReportData].map((item) => { 62 56 return item.status_report; 63 57 }); 58 + 59 + statusReports.push(...pageStatusReportData); 64 60 65 61 const tracker = new Tracker({ 66 62 incidents: ongoingIncidents, ··· 110 106 .where(inArray(monitorsToStatusReport.monitorId, monitorIds)) 111 107 .all(); 112 108 113 - const pageStatusReportDataQuery = db 114 - .select() 115 - .from(pagesToStatusReports) 116 - .innerJoin( 117 - statusReport, 118 - and( 119 - eq(pagesToStatusReports.statusReportId, statusReport.id), 120 - eq(pagesToStatusReports.pageId, pageId), 121 - ), 122 - ) 123 - .all(); 124 - 125 109 const ongoingIncidentsQuery = db 126 110 .select() 127 111 .from(incidentTable) ··· 144 128 ), 145 129 ); 146 130 131 + const pageStatusReportDataQuery = db 132 + .select() 133 + .from(statusReport) 134 + .where( 135 + and(eq(statusReport.pageId, pageId), ne(statusReport.status, "resolved")), 136 + ); 137 + 147 138 const [ 139 + pageStatusReportData, 148 140 monitorStatusReportData, 149 - pageStatusReportData, 150 141 ongoingIncidents, 151 142 maintenanceData, 152 143 ] = await Promise.all([ 153 - monitorStatusReportQuery, 154 144 pageStatusReportDataQuery, 145 + monitorStatusReportQuery, 155 146 ongoingIncidentsQuery, 156 147 ongoingMaintenancesQuery, 157 148 ]);
+15 -23
apps/server/src/v1/statusReportUpdates/post.ts
··· 4 4 import { 5 5 page, 6 6 pageSubscriber, 7 - pagesToStatusReports, 8 7 statusReport, 9 8 statusReportUpdate, 10 9 } from "@openstatus/db/src/schema"; ··· 81 80 82 81 // send email 83 82 84 - if (workspacePlan.title !== "Hobby") { 85 - const allPages = await db 83 + if (workspacePlan.limits.notifications && _statusReport.pageId) { 84 + const subscribers = await db 86 85 .select() 87 - .from(pagesToStatusReports) 88 - .where(eq(pagesToStatusReports.statusReportId, _statusReport.id)) 86 + .from(pageSubscriber) 87 + .where( 88 + and( 89 + eq(pageSubscriber.pageId, _statusReport.pageId), 90 + isNotNull(pageSubscriber.acceptedAt) 91 + ) 92 + ) 89 93 .all(); 90 94 91 - for (const currentPage of allPages) { 92 - const subscribers = await db 93 - .select() 94 - .from(pageSubscriber) 95 - .where( 96 - and( 97 - eq(pageSubscriber.pageId, currentPage.pageId), 98 - isNotNull(pageSubscriber.acceptedAt) 99 - ) 100 - ) 101 - .all(); 102 - 103 - const pageInfo = await db 104 - .select() 105 - .from(page) 106 - .where(eq(page.id, currentPage.pageId)) 107 - .get(); 108 - if (!pageInfo) continue; 95 + const pageInfo = await db 96 + .select() 97 + .from(page) 98 + .where(eq(page.id, _statusReport.pageId)) 99 + .get(); 100 + if (pageInfo) { 109 101 const subscribersEmails = subscribers.map( 110 102 (subscriber) => subscriber.email 111 103 );
+2 -9
apps/server/src/v1/statusReports/get.ts
··· 38 38 with: { 39 39 statusReportUpdates: true, 40 40 monitorsToStatusReports: true, 41 - pagesToStatusReports: true, 42 41 }, 43 42 where: and( 44 43 eq(statusReport.workspaceId, Number(workspaceId)), ··· 50 49 throw new HTTPException(404, { message: "Not Found" }); 51 50 } 52 51 53 - const { 54 - statusReportUpdates, 55 - monitorsToStatusReports, 56 - pagesToStatusReports, 57 - } = _statusUpdate; 52 + const { statusReportUpdates, monitorsToStatusReports } = _statusUpdate; 58 53 59 54 // most recent report information 60 55 const { message, date } = ··· 67 62 monitorIds: monitorsToStatusReports.length 68 63 ? monitorsToStatusReports.map((monitor) => monitor.monitorId) 69 64 : null, 70 - pageIds: pagesToStatusReports.length 71 - ? pagesToStatusReports.map((page) => page.pageId) 72 - : null, 65 + 73 66 statusReportUpdateIds: statusReportUpdates.map((update) => update.id), 74 67 }); 75 68
-2
apps/server/src/v1/statusReports/get_all.ts
··· 35 35 with: { 36 36 statusReportUpdates: true, 37 37 monitorsToStatusReports: true, 38 - pagesToStatusReports: true, 39 38 }, 40 39 where: eq(statusReport.workspaceId, Number(workspaceId)), 41 40 }); ··· 48 47 _statusReports.map((r) => ({ 49 48 ...r, 50 49 statusReportUpdateIds: r.statusReportUpdates.map((u) => u.id), 51 - pageIds: r.pagesToStatusReports.map((p) => p.pageId), 52 50 monitorIds: r.monitorsToStatusReports.map((m) => m.monitorId), 53 51 })) 54 52 );
+27 -50
apps/server/src/v1/statusReports/post.ts
··· 6 6 monitorsToStatusReport, 7 7 page, 8 8 pageSubscriber, 9 - pagesToStatusReports, 10 9 statusReport, 11 10 statusReportUpdate, 12 11 } from "@openstatus/db/src/schema"; ··· 62 61 const workspaceId = c.get("workspaceId"); 63 62 const workspacePlan = c.get("workspacePlan"); 64 63 65 - const { pageIds, monitorIds, date, ...rest } = input; 64 + const { monitorIds, date, ...rest } = input; 66 65 67 66 if (monitorIds?.length) { 68 67 const _monitors = await db ··· 82 81 } 83 82 } 84 83 85 - if (pageIds?.length) { 84 + 85 + if(rest.pageId){ 86 86 const _pages = await db 87 - .select() 88 - .from(page) 89 - .where( 90 - and( 91 - eq(page.workspaceId, Number(workspaceId)), 92 - inArray(page.id, pageIds) 93 - ) 87 + .select() 88 + .from(page) 89 + .where( 90 + and( 91 + eq(page.workspaceId, Number(workspaceId)), 92 + eq(page.id, rest.pageId) 94 93 ) 95 - .all(); 94 + ) 95 + .all(); 96 96 97 - if (_pages.length !== pageIds.length) { 98 - throw new HTTPException(400, { message: "Page not found" }); 99 - } 97 + if (_pages.length !== 1) { 98 + throw new HTTPException(400, { message: "Page not found" }); 99 + } 100 100 } 101 101 102 102 const _newStatusReport = await db ··· 118 118 .returning() 119 119 .get(); 120 120 121 - if (pageIds?.length) { 122 - await db 123 - .insert(pagesToStatusReports) 124 - .values( 125 - pageIds.map((id) => { 126 - return { 127 - pageId: id, 128 - statusReportId: _newStatusReport.id, 129 - }; 130 - }) 131 - ) 132 - .returning(); 133 - } 134 - 135 121 if (monitorIds?.length) { 136 122 await db 137 123 .insert(monitorsToStatusReport) ··· 146 132 .returning(); 147 133 } 148 134 149 - if (workspacePlan.title !== "Hobby") { 150 - const allPages = await db 135 + if (workspacePlan.limits.notifications && _newStatusReport.pageId) { 136 + const subscribers = await db 151 137 .select() 152 - .from(pagesToStatusReports) 138 + .from(pageSubscriber) 153 139 .where( 154 - eq(pagesToStatusReports.statusReportId, Number(_newStatusReport.id)) 140 + and( 141 + eq(pageSubscriber.pageId, _newStatusReport.pageId), 142 + isNotNull(pageSubscriber.acceptedAt) 143 + ) 155 144 ) 156 145 .all(); 157 - for (const currentPage of allPages) { 158 - const subscribers = await db 159 - .select() 160 - .from(pageSubscriber) 161 - .where( 162 - and( 163 - eq(pageSubscriber.pageId, currentPage.pageId), 164 - isNotNull(pageSubscriber.acceptedAt) 165 - ) 166 - ) 167 - .all(); 168 - const pageInfo = await db 169 - .select() 170 - .from(page) 171 - .where(eq(page.id, currentPage.pageId)) 172 - .get(); 173 - if (!pageInfo) continue; 146 + const pageInfo = await db 147 + .select() 148 + .from(page) 149 + .where(eq(page.id, _newStatusReport.pageId)) 150 + .get(); 151 + if (pageInfo) { 174 152 const subscribersEmails = subscribers.map( 175 153 (subscriber) => subscriber.email 176 154 ); ··· 187 165 const data = StatusReportSchema.parse({ 188 166 ..._newStatusReport, 189 167 monitorIds, 190 - pageIds, 191 168 statusReportUpdateIds: [_newStatusReportUpdate.id], 192 169 }); 193 170
+6 -10
apps/server/src/v1/statusReports/schema.ts
··· 1 1 import { z } from "@hono/zod-openapi"; 2 2 3 - import { statusReportStatusSchema } from "@openstatus/db/src/schema"; 3 + import { page, statusReportStatusSchema } from "@openstatus/db/src/schema"; 4 4 5 5 export const ParamsSchema = z.object({ 6 6 id: z ··· 49 49 description: "id of monitors this report needs to refer", 50 50 }) 51 51 .nullable(), 52 - pageIds: z 53 - .array(z.number()) 54 - .optional() 55 - .nullable() 56 - .default([]) 57 - .openapi({ 58 - description: "id of status pages this report needs to refer", 59 - }) 60 - .nullable(), 52 + 53 + pageId: z.number().optional().nullable().openapi({ 54 + description: "The id of the page this status report belongs to", 55 + 56 + }), 61 57 }); 62 58 63 59 export type StatusReportSchema = z.infer<typeof StatusReportSchema>;
+7 -7
apps/server/src/v1/statusReports/statusReports.test.ts
··· 15 15 status: "monitoring", 16 16 statusReportUpdateIds: expect.arrayContaining([1, 3]), // depending on the order of the updates 17 17 monitorIds: null, 18 - pageIds: [1], 18 + pageId: 1, 19 19 }); 20 20 }); 21 21 ··· 34 34 status: "monitoring", 35 35 statusReportUpdateIds: expect.arrayContaining([1, 3]), // depending on the order of the updates 36 36 monitorIds: [], 37 - pageIds: [1], 37 + pageId: 1, 38 38 }, 39 39 { 40 40 id: 2, ··· 42 42 status: "investigating", 43 43 statusReportUpdateIds: expect.arrayContaining([2]), // depending on the order of the updates 44 44 monitorIds: [1, 2], 45 - pageIds: [1], 45 + pageId: 1, 46 46 }, 47 47 ], 48 48 }); ··· 60 60 title: "New Status Report", 61 61 message: "Message", 62 62 monitorIds: [1], 63 - pageIds: [1], 63 + pageId: 1, 64 64 }), 65 65 }); 66 66 const json = await res.json(); ··· 73 73 status: "investigating", 74 74 statusReportUpdateIds: [expect.any(Number)], 75 75 monitorIds: [1], 76 - pageIds: [1], 76 + pageId: 1, 77 77 }); 78 78 }); 79 79 ··· 118 118 title: "New Status Report", 119 119 message: "Message", 120 120 monitorIds: [100], 121 - pageIds: [1], 121 + pageId: 1, 122 122 }), 123 123 }); 124 124 ··· 137 137 title: "New Status Report", 138 138 message: "Message", 139 139 monitorIds: [1], 140 - pageIds: [100], 140 + pageId: 100, 141 141 }), 142 142 }); 143 143
+16 -23
apps/server/src/v1/statusReports/update/post.ts
··· 3 3 import { 4 4 page, 5 5 pageSubscriber, 6 - pagesToStatusReports, 7 6 statusReport, 8 7 statusReportUpdate, 9 8 } from "@openstatus/db/src/schema"; ··· 77 76 .returning() 78 77 .get(); 79 78 80 - if (workspacePlan.title !== "Hobby") { 81 - const allPages = await db 79 + if (workspacePlan.limits.notifications && _statusReport.pageId) { 80 + const subscribers = await db 82 81 .select() 83 - .from(pagesToStatusReports) 84 - .where(eq(pagesToStatusReports.statusReportId, Number(id))) 85 - .all(); 86 - for (const currentPage of allPages) { 87 - const subscribers = await db 88 - .select() 89 - .from(pageSubscriber) 90 - .where( 91 - and( 92 - eq(pageSubscriber.pageId, currentPage.pageId), 93 - isNotNull(pageSubscriber.acceptedAt) 94 - ) 82 + .from(pageSubscriber) 83 + .where( 84 + and( 85 + eq(pageSubscriber.pageId, _statusReport.pageId), 86 + isNotNull(pageSubscriber.acceptedAt) 95 87 ) 96 - .all(); 97 - const pageInfo = await db 98 - .select() 99 - .from(page) 100 - .where(eq(page.id, currentPage.pageId)) 101 - .get(); 102 - if (!pageInfo) continue; 88 + ) 89 + .all(); 90 + const pageInfo = await db 91 + .select() 92 + .from(page) 93 + .where(eq(page.id, _statusReport.pageId)) 94 + .get(); 95 + if (pageInfo) { 103 96 const subscribersEmails = subscribers.map( 104 97 (subscriber) => subscriber.email 105 98 ); ··· 113 106 } 114 107 } 115 108 116 - const data = StatusReportUpdateSchema.parse(_statusReportUpdate); 109 + const data = StatusReportSchema.parse(_statusReportUpdate); 117 110 118 111 return c.json(data, 200); 119 112 });
+1
apps/web/src/app/(content)/features/mock.ts
··· 1033 1033 status: "resolved" as const, 1034 1034 title: "Downtime", 1035 1035 workspaceId: 1, 1036 + pageId: 0, 1036 1037 createdAt: new Date("2024-07-09T21:22:43.000Z"), 1037 1038 updatedAt: new Date("2024-07-09T21:23:17.000Z"), 1038 1039 statusReportUpdates: [
+42
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/(overview)/page.tsx
··· 1 + import Link from "next/link"; 2 + import * as React from "react"; 3 + 4 + import { Button } from "@openstatus/ui"; 5 + 6 + import { EmptyState } from "@/components/dashboard/empty-state"; 7 + import { columns } from "@/components/data-table/status-report/columns"; 8 + import { DataTable } from "@/components/data-table/status-report/data-table"; 9 + import { api } from "@/trpc/server"; 10 + 11 + export default async function MonitorPage({ 12 + params, 13 + }: { 14 + params: { id: string }; 15 + }) { 16 + const reports = await api.statusReport.getStatusReportByPageId.query({ 17 + id: Number.parseInt(params.id), 18 + }); 19 + 20 + if (reports?.length === 0) 21 + return ( 22 + <EmptyState 23 + icon="siren" 24 + title="No status reports" 25 + description="Create your first status report" 26 + action={ 27 + <Button asChild> 28 + <Link href="./reports/new">Create</Link> 29 + </Button> 30 + } 31 + /> 32 + ); 33 + 34 + return ( 35 + <div className="space-y-3"> 36 + <Button size="sm" asChild> 37 + <Link href="./reports/new">Create Status Report</Link> 38 + </Button> 39 + <DataTable columns={columns} data={reports} /> 40 + </div> 41 + ); 42 + }
+80
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/[reportId]/overview/_components/header.tsx
··· 1 + "use client"; 2 + 3 + import { StatusReportUpdateForm } from "@/components/forms/status-report-update/form"; 4 + import { StatusBadge } from "@/components/status-update/status-badge"; 5 + import { formatDate } from "@/lib/utils"; 6 + import type { 7 + Monitor, 8 + StatusReport, 9 + StatusReportUpdate, 10 + } from "@openstatus/db/src/schema"; 11 + import { 12 + Badge, 13 + Button, 14 + Dialog, 15 + DialogContent, 16 + DialogDescription, 17 + DialogHeader, 18 + DialogTitle, 19 + DialogTrigger, 20 + Separator, 21 + } from "@openstatus/ui"; 22 + import { useState } from "react"; 23 + 24 + export function Header({ 25 + report, 26 + monitors, 27 + }: { 28 + report: StatusReport & { statusReportUpdates: StatusReportUpdate[] }; 29 + monitors?: Pick<Monitor, "name" | "id">[]; 30 + }) { 31 + const [open, setOpen] = useState(false); 32 + 33 + const firstUpdate = report.statusReportUpdates?.[0]; 34 + const lastUpdate = 35 + report.statusReportUpdates?.[report.statusReportUpdates?.length - 1]; 36 + 37 + return ( 38 + <div className="space-y-3"> 39 + <div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"> 40 + <div className="space-y-2"> 41 + <h3 className="font-cal text-lg">{report.title}</h3> 42 + <div className="flex flex-wrap items-center gap-2 text-muted-foreground text-sm"> 43 + <span className="font-mono"> 44 + {firstUpdate?.date 45 + ? formatDate(firstUpdate?.date) 46 + : "Missing date"} 47 + </span> 48 + <span className="text-muted-foreground/50 text-xs">โ€ข</span> 49 + <StatusBadge status={report.status} /> 50 + <span className="text-muted-foreground/50 text-xs">โ€ข</span> 51 + {monitors?.map(({ name, id }) => ( 52 + <Badge key={id} variant="outline"> 53 + {name} 54 + </Badge> 55 + ))} 56 + </div> 57 + </div> 58 + 59 + <Dialog open={open} onOpenChange={setOpen}> 60 + <DialogTrigger asChild> 61 + <Button size="sm">Create Status Update Report</Button> 62 + </DialogTrigger> 63 + <DialogContent className="max-h-screen overflow-y-scroll sm:max-w-[650px]"> 64 + <DialogHeader> 65 + <DialogTitle>Edit Status Report</DialogTitle> 66 + <DialogDescription> 67 + Update your status report with new information. 68 + </DialogDescription> 69 + </DialogHeader> 70 + <StatusReportUpdateForm 71 + statusReportId={report.id} 72 + onSubmit={() => setOpen(false)} 73 + /> 74 + </DialogContent> 75 + </Dialog> 76 + </div> 77 + <Separator /> 78 + </div> 79 + ); 80 + }
+26
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/[reportId]/overview/loading.tsx
··· 1 + import { Separator, Skeleton } from "@openstatus/ui"; 2 + 3 + export default function Loading() { 4 + return ( 5 + <div className="col-span-full flex flex-col gap-6"> 6 + <div className="space-y-3"> 7 + <div className="flex items-end justify-between"> 8 + <div className="flex-1 space-y-2"> 9 + <Skeleton className="h-7 w-full max-w-[200px]" /> 10 + <div className="flex flex-wrap items-center gap-2 text-muted-foreground text-sm"> 11 + <Skeleton className="h-5 w-full max-w-[100px]" /> 12 + <span className="text-muted-foreground/50 text-xs">โ€ข</span> 13 + <Skeleton className="h-[22px] w-24 rounded-full" /> 14 + <span className="text-muted-foreground/50 text-xs">โ€ข</span> 15 + <Skeleton className="h-[22px] w-16 rounded-full" /> 16 + </div> 17 + </div> 18 + <Skeleton className="h-8 w-48" /> 19 + </div> 20 + <Separator /> 21 + </div> 22 + <Skeleton className="h-48 w-full" /> 23 + <Skeleton className="h-48 w-full" /> 24 + </div> 25 + ); 26 + }
+9
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/[reportId]/page.tsx
··· 1 + import { redirect } from "next/navigation"; 2 + 3 + export default function Page({ 4 + params, 5 + }: { 6 + params: { workspaceSlug: string; reportId: string }; 7 + }) { 8 + return redirect(`./${params.reportId}/overview`); 9 + }
-24
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/(overview)/layout.tsx
··· 1 - import Link from "next/link"; 2 - import type { ReactNode } from "react"; 3 - 4 - import { Button } from "@openstatus/ui"; 5 - 6 - import { Header } from "@/components/dashboard/header"; 7 - import AppPageLayout from "@/components/layout/app-page-layout"; 8 - 9 - export default async function Layout({ children }: { children: ReactNode }) { 10 - return ( 11 - <AppPageLayout> 12 - <Header 13 - title="Status Reports" 14 - description="Overview of all your status reports and updates." 15 - actions={ 16 - <Button asChild> 17 - <Link href="./status-reports/new">Create</Link> 18 - </Button> 19 - } 20 - /> 21 - {children} 22 - </AppPageLayout> 23 - ); 24 - }
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/(overview)/loading.tsx apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/(overview)/loading.tsx
-29
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/(overview)/page.tsx
··· 1 - import Link from "next/link"; 2 - import * as React from "react"; 3 - 4 - import { Button } from "@openstatus/ui"; 5 - 6 - import { EmptyState } from "@/components/dashboard/empty-state"; 7 - import { columns } from "@/components/data-table/status-report/columns"; 8 - import { DataTable } from "@/components/data-table/status-report/data-table"; 9 - import { api } from "@/trpc/server"; 10 - 11 - export default async function MonitorPage() { 12 - const reports = await api.statusReport.getStatusReportByWorkspace.query(); 13 - 14 - if (reports?.length === 0) 15 - return ( 16 - <EmptyState 17 - icon="siren" 18 - title="No status reports" 19 - description="Create your first status report" 20 - action={ 21 - <Button asChild> 22 - <Link href="./status-reports/new">Create</Link> 23 - </Button> 24 - } 25 - /> 26 - ); 27 - 28 - return <DataTable columns={columns} data={reports} />; 29 - }
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/_components/status-update-button.tsx apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/[reportId]/_components/status-update-button.tsx
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/edit/loading.tsx apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/[reportId]/edit/loading.tsx
+5 -7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/edit/page.tsx apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/[reportId]/edit/page.tsx
··· 4 4 export default async function EditPage({ 5 5 params, 6 6 }: { 7 - params: { workspaceSlug: string; id: string }; 7 + params: { workspaceSlug: string; id: string; reportId: string }; 8 8 }) { 9 9 const statusUpdate = await api.statusReport.getStatusReportById.query({ 10 - id: Number.parseInt(params.id), 10 + id: Number.parseInt(params.reportId), 11 + pageId: Number.parseInt(params.id), 11 12 }); 12 13 13 14 const monitors = await api.monitor.getMonitorsByWorkspace.query(); 14 15 15 - const pages = await api.page.getPagesByWorkspace.query(); 16 - 17 16 return ( 18 17 <StatusReportForm 19 18 monitors={monitors} 20 - pages={pages} 21 19 defaultValues={ 22 20 // TODO: we should move the mapping to the trpc layer 23 21 // so we don't have to do this in the UI ··· 25 23 { 26 24 ...statusUpdate, 27 25 monitors: statusUpdate?.monitorsToStatusReports.map( 28 - ({ monitorId }) => monitorId, 26 + ({ monitorId }) => monitorId 29 27 ), 30 - pages: statusUpdate?.pagesToStatusReports.map(({ pageId }) => pageId), 31 28 message: "", 32 29 } 33 30 } 31 + pageId={Number.parseInt(params.id)} 34 32 defaultSection="connect" 35 33 /> 36 34 );
-34
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/layout.tsx
··· 1 - import { notFound } from "next/navigation"; 2 - 3 - import { Header } from "@/components/dashboard/header"; 4 - import AppPageWithSidebarLayout from "@/components/layout/app-page-with-sidebar-layout"; 5 - import { api } from "@/trpc/server"; 6 - import { StatusUpdateButton } from "./_components/status-update-button"; 7 - 8 - export default async function Layout({ 9 - children, 10 - params, 11 - }: { 12 - children: React.ReactNode; 13 - params: { workspaceSlug: string; id: string }; 14 - }) { 15 - const id = params.id; 16 - 17 - const statusReport = await api.statusReport.getStatusReportById.query({ 18 - id: Number(id), 19 - }); 20 - 21 - if (!statusReport) { 22 - return notFound(); 23 - } 24 - 25 - return ( 26 - <AppPageWithSidebarLayout id="status-reports"> 27 - <Header 28 - title={statusReport.title} 29 - actions={<StatusUpdateButton statusReportId={Number(id)} />} 30 - /> 31 - {children} 32 - </AppPageWithSidebarLayout> 33 - ); 34 - }
-22
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/overview/loading.tsx
··· 1 - import { Separator, Skeleton } from "@openstatus/ui"; 2 - 3 - export default function Loading() { 4 - return ( 5 - <div className="col-span-full flex flex-col gap-6"> 6 - <div className="grid grid-cols-5 gap-3 text-sm"> 7 - <Skeleton className="col-start-1 h-5 max-w-[100px]" /> 8 - <Skeleton className="col-span-4 h-5 w-full max-w-[200px]" /> 9 - <Skeleton className="col-start-1 h-5 max-w-[100px]" /> 10 - <Skeleton className="col-span-4 h-5 w-full max-w-[100px]" /> 11 - <Skeleton className="col-start-1 h-5 max-w-[100px]" /> 12 - <div className="col-span-4 flex gap-2"> 13 - <Skeleton className="h-5 w-full max-w-[60px]" /> 14 - <Skeleton className="h-5 w-full max-w-[60px]" /> 15 - </div> 16 - </div> 17 - <Separator /> 18 - <Skeleton className="h-48 w-full" /> 19 - <Skeleton className="h-48 w-full" /> 20 - </div> 21 - ); 22 - }
+6 -12
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/overview/page.tsx apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/[reportId]/overview/page.tsx
··· 2 2 import { notFound } from "next/navigation"; 3 3 import * as React from "react"; 4 4 5 - import { Button, Separator } from "@openstatus/ui"; 5 + import { Button } from "@openstatus/ui"; 6 6 7 7 import { EmptyState } from "@/components/dashboard/empty-state"; 8 8 import { Events } from "@/components/status-update/events"; 9 - import { Summary } from "@/components/status-update/summary"; 10 9 import { api } from "@/trpc/server"; 10 + import { Header } from "./_components/header"; 11 11 12 12 export default async function OverviewPage({ 13 13 params, 14 14 }: { 15 - params: { workspaceSlug: string; id: string }; 15 + params: { workspaceSlug: string; id: string; reportId: string }; 16 16 }) { 17 17 const report = await api.statusReport.getStatusReportById.query({ 18 - id: Number.parseInt(params.id), 18 + id: Number.parseInt(params.reportId), 19 + pageId: Number.parseInt(params.id), 19 20 }); 20 21 21 22 if (!report) return notFound(); ··· 24 25 25 26 return ( 26 27 <> 27 - <Summary report={report} monitors={monitors} /> 28 - <Separator /> 28 + <Header report={report} monitors={monitors} /> 29 29 {report.statusReportUpdates.length > 0 ? ( 30 30 <Events statusReportUpdates={report.statusReportUpdates} editable /> 31 31 ) : ( ··· 33 33 icon="megaphone" 34 34 title="No status report updates" 35 35 description="Create your first update" 36 - action={ 37 - <Button asChild> 38 - {/* TODO: check if correct */} 39 - <Link href={`./${params.id}/update/edit`}>Create</Link> 40 - </Button> 41 - } 42 36 /> 43 37 )} 44 38 </>
-9
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/[id]/page.tsx
··· 1 - import { redirect } from "next/navigation"; 2 - 3 - export default function Page({ 4 - params, 5 - }: { 6 - params: { workspaceSlug: string; id: string }; 7 - }) { 8 - return redirect(`./${params.id}/overview`); 9 - }
-19
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/edit/loading.tsx
··· 1 - import { Skeleton } from "@openstatus/ui"; 2 - 3 - import { Header } from "@/components/dashboard/header"; 4 - import { SkeletonForm } from "@/components/forms/skeleton-form"; 5 - 6 - export default function Loading() { 7 - return ( 8 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 9 - <div className="col-span-full flex w-full justify-between"> 10 - <Header.Skeleton> 11 - <Skeleton className="h-9 w-20" /> 12 - </Header.Skeleton> 13 - </div> 14 - <div className="col-span-full"> 15 - <SkeletonForm /> 16 - </div> 17 - </div> 18 - ); 19 - }
-71
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/edit/page.tsx
··· 1 - import { notFound } from "next/navigation"; 2 - import * as z from "zod"; 3 - 4 - import { Header } from "@/components/dashboard/header"; 5 - import { StatusReportForm } from "@/components/forms/status-report-form"; 6 - import AppPageLayout from "@/components/layout/app-page-layout"; 7 - import { api } from "@/trpc/server"; 8 - 9 - /** 10 - * allowed URL search params 11 - */ 12 - const searchParamsSchema = z.object({ 13 - id: z.coerce.number().optional(), 14 - }); 15 - 16 - export default async function EditPage({ 17 - // biome-ignore lint/correctness/noUnusedVariables: <explanation> 18 - params, 19 - searchParams, 20 - }: { 21 - params: { workspaceSlug: string }; 22 - searchParams: { [key: string]: string | string[] | undefined }; 23 - }) { 24 - const search = searchParamsSchema.safeParse(searchParams); 25 - 26 - if (!search.success) { 27 - return notFound(); 28 - } 29 - 30 - const { id } = search.data; 31 - 32 - const statusUpdate = id 33 - ? await api.statusReport.getStatusReportById.query({ 34 - id, 35 - }) 36 - : undefined; 37 - 38 - const monitors = await api.monitor.getMonitorsByWorkspace.query(); 39 - 40 - const pages = await api.page.getPagesByWorkspace.query(); 41 - 42 - return ( 43 - <AppPageLayout> 44 - <Header 45 - title="Status Report" 46 - description="Create a public report for your incident" 47 - /> 48 - <StatusReportForm 49 - monitors={monitors} 50 - pages={pages} 51 - defaultValues={ 52 - statusUpdate 53 - ? // TODO: we should move the mapping to the trpc layer 54 - // so we don't have to do this in the UI 55 - // it should be something like defaultValues={statusReport} 56 - { 57 - ...statusUpdate, 58 - monitors: statusUpdate?.monitorsToStatusReports.map( 59 - ({ monitorId }) => monitorId, 60 - ), 61 - pages: statusUpdate?.pagesToStatusReports.map( 62 - ({ pageId }) => pageId, 63 - ), 64 - message: "", 65 - } 66 - : undefined 67 - } 68 - /> 69 - </AppPageLayout> 70 - ); 71 - }
-18
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/new/layout.tsx
··· 1 - import { Header } from "@/components/dashboard/header"; 2 - import AppPageLayout from "@/components/layout/app-page-layout"; 3 - 4 - export default async function Layout({ 5 - children, 6 - }: { 7 - children: React.ReactNode; 8 - }) { 9 - return ( 10 - <AppPageLayout> 11 - <Header 12 - title="Status Report" 13 - description="Create a public report for your incident." 14 - /> 15 - {children} 16 - </AppPageLayout> 17 - ); 18 - }
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/new/loading.tsx apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/new/loading.tsx
+6 -4
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-reports/new/page.tsx apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/[id]/reports/new/page.tsx
··· 1 1 import { StatusReportForm } from "@/components/forms/status-report/form"; 2 2 import { api } from "@/trpc/server"; 3 3 4 - export default async function NewPage() { 4 + export default async function NewPage({ 5 + params, 6 + }: { 7 + params: { id: string; reportId: string }; 8 + }) { 5 9 const monitors = await api.monitor.getMonitorsByWorkspace.query(); 6 - 7 - const pages = await api.page.getPagesByWorkspace.query(); 8 10 9 11 return ( 10 12 <StatusReportForm 11 13 monitors={monitors} 12 - pages={pages} 13 14 nextUrl={"./"} 14 15 defaultSection="update-message" 16 + pageId={Number.parseInt(params.id)} 15 17 /> 16 18 ); 17 19 }
+1 -4
apps/web/src/components/data-table/status-report/columns.tsx
··· 21 21 cell: ({ row }) => { 22 22 const id = row.original.id; 23 23 return ( 24 - <Link 25 - href={`./status-reports/${id}/overview`} 26 - className="hover:underline" 27 - > 24 + <Link href={`./reports/${id}/overview`} className="hover:underline"> 28 25 <span className="truncate">{row.getValue("title")}</span> 29 26 </Link> 30 27 );
+2 -2
apps/web/src/components/data-table/status-report/data-table-row-actions.tsx
··· 69 69 </Button> 70 70 </DropdownMenuTrigger> 71 71 <DropdownMenuContent align="end"> 72 - <Link href={`./status-reports/${statusReport.id}/edit`}> 72 + <Link href={`./reports/${statusReport.id}/edit`}> 73 73 <DropdownMenuItem>Edit</DropdownMenuItem> 74 74 </Link> 75 - <Link href={`./status-reports/${statusReport.id}/overview`}> 75 + <Link href={`./reports/${statusReport.id}/overview`}> 76 76 <DropdownMenuItem>View</DropdownMenuItem> 77 77 </Link> 78 78 <AlertDialogTrigger asChild>
+11 -68
apps/web/src/components/forms/status-report-form.tsx
··· 51 51 interface Props { 52 52 defaultValues?: InsertStatusReport; 53 53 monitors?: Monitor[]; 54 - pages?: Page[]; 55 54 nextUrl?: string; 55 + pageId: number; 56 56 } 57 57 58 58 export function StatusReportForm({ 59 59 defaultValues, 60 60 monitors, 61 - pages, 62 61 nextUrl, 62 + pageId, 63 63 }: Props) { 64 64 const form = useForm<InsertStatusReport>({ 65 65 resolver: zodResolver(insertStatusReportSchema), ··· 69 69 title: defaultValues.title, 70 70 status: defaultValues.status, 71 71 monitors: defaultValues.monitors, 72 - pages: defaultValues.pages, 73 72 // include update on creation 74 73 message: defaultValues.message, 75 74 date: defaultValues.date, ··· 86 85 startTransition(async () => { 87 86 try { 88 87 if (defaultValues) { 89 - await api.statusReport.updateStatusReport.mutate({ ...props }); 88 + await api.statusReport.updateStatusReport.mutate({ 89 + pageId, 90 + ...props, 91 + }); 90 92 } else { 91 93 const { message, date, status, ...rest } = props; 92 94 const statusReport = await api.statusReport.createStatusReport.mutate( 93 95 { 94 96 status, 95 97 message, 98 + pageId, 96 99 ...rest, 97 - }, 100 + } 98 101 ); 99 102 // include update on creation 100 103 if (statusReport?.id) { ··· 224 227 ]) 225 228 : field.onChange( 226 229 field.value?.filter( 227 - (value) => value !== item.id, 228 - ), 230 + (value) => value !== item.id 231 + ) 229 232 ); 230 233 }} 231 234 /> ··· 240 243 "rounded-full p-1", 241 244 item.active 242 245 ? "bg-green-500" 243 - : "bg-red-500", 246 + : "bg-red-500" 244 247 )} 245 248 /> 246 - </div> 247 - <p className="truncate text-muted-foreground text-sm"> 248 - {item.description} 249 - </p> 250 - </div> 251 - </FormItem> 252 - ); 253 - }} 254 - /> 255 - ))} 256 - </div> 257 - <FormMessage /> 258 - </FormItem> 259 - )} 260 - /> 261 - <FormField 262 - control={form.control} 263 - name="pages" 264 - render={() => ( 265 - <FormItem className="sm:col-span-full"> 266 - <div className="mb-4"> 267 - <FormLabel>Pages</FormLabel> 268 - <FormDescription> 269 - Select the pages that you want to refer the incident to. 270 - </FormDescription> 271 - </div> 272 - <div className="grid grid-cols-2 gap-4 sm:grid-cols-4"> 273 - {pages?.map((item) => ( 274 - <FormField 275 - key={item.id} 276 - control={form.control} 277 - name="pages" 278 - render={({ field }) => { 279 - return ( 280 - <FormItem 281 - key={item.id} 282 - className="flex flex-row items-start space-x-3 space-y-0" 283 - > 284 - <FormControl> 285 - <Checkbox 286 - checked={field.value?.includes(item.id)} 287 - onCheckedChange={(checked) => { 288 - return checked 289 - ? field.onChange([ 290 - ...(field.value || []), 291 - item.id, 292 - ]) 293 - : field.onChange( 294 - field.value?.filter( 295 - (value) => value !== item.id, 296 - ), 297 - ); 298 - }} 299 - /> 300 - </FormControl> 301 - <div className="grid gap-1.5 leading-none"> 302 - <div className="flex items-center gap-2"> 303 - <FormLabel className="font-normal"> 304 - {item.title} 305 - </FormLabel> 306 249 </div> 307 250 <p className="truncate text-muted-foreground text-sm"> 308 251 {item.description}
+9 -6
apps/web/src/components/forms/status-report/form.tsx
··· 30 30 defaultSection?: string; 31 31 defaultValues?: InsertStatusReport; 32 32 monitors?: Monitor[]; 33 - pages?: Page[]; 34 33 nextUrl?: string; 34 + pageId: number; 35 35 } 36 36 37 37 export function StatusReportForm({ 38 38 defaultSection, 39 39 defaultValues, 40 40 monitors, 41 - pages, 42 41 nextUrl, 42 + pageId, 43 43 }: Props) { 44 44 const form = useForm<InsertStatusReport>({ 45 45 resolver: zodResolver(insertStatusReportSchema), ··· 49 49 title: defaultValues.title, 50 50 status: defaultValues.status, 51 51 monitors: defaultValues.monitors, 52 - pages: defaultValues.pages, 53 52 // include update on creation 54 53 message: defaultValues.message, 55 54 date: defaultValues.date, ··· 67 66 startTransition(async () => { 68 67 try { 69 68 if (defaultValues) { 70 - await api.statusReport.updateStatusReport.mutate({ ...props }); 69 + await api.statusReport.updateStatusReport.mutate({ 70 + pageId, 71 + ...props, 72 + }); 71 73 } else { 72 74 const { message, date, status, ...rest } = props; 73 75 const statusReport = await api.statusReport.createStatusReport.mutate( 74 76 { 75 77 status, 76 78 message, 79 + pageId, 77 80 ...rest, 78 - }, 81 + } 79 82 ); 80 83 // include update on creation 81 84 if (statusReport?.id) { ··· 133 136 </TabsContent> 134 137 ) : null} 135 138 <TabsContent value="connect"> 136 - <SectionConnect form={form} monitors={monitors} pages={pages} /> 139 + <SectionConnect form={form} monitors={monitors} /> 137 140 </TabsContent> 138 141 </Tabs> 139 142 <SaveButton
+3 -58
apps/web/src/components/forms/status-report/section-connect.tsx
··· 21 21 22 22 interface Props { 23 23 form: UseFormReturn<InsertStatusReport>; 24 - pages?: Page[]; 25 24 monitors?: Monitor[]; 26 25 } 27 26 28 - export function SectionConnect({ form, pages, monitors }: Props) { 27 + export function SectionConnect({ form, monitors }: Props) { 29 28 return ( 30 29 <div className="grid w-full gap-4"> 31 30 <div className="flex flex-col gap-3"> 32 31 <FormField 33 32 control={form.control} 34 - name="pages" 35 - render={() => ( 36 - <FormItem> 37 - <div className="mb-4"> 38 - <FormLabel>Pages</FormLabel> 39 - <FormDescription> 40 - Select the pages that you want to refer the incident to. 41 - </FormDescription> 42 - </div> 43 - <div className="grid grid-cols-1 grid-rows-1 gap-6 md:grid-cols-3 sm:grid-cols-2"> 44 - {pages?.map((item) => ( 45 - <FormField 46 - key={item.id} 47 - control={form.control} 48 - name="pages" 49 - render={({ field }) => { 50 - return ( 51 - <FormItem key={item.id} className="h-full w-full"> 52 - <FormControl className="w-full"> 53 - <CheckboxLabel 54 - id={String(item.id)} 55 - name="page" 56 - checked={field.value?.includes(item.id)} 57 - onCheckedChange={(checked) => { 58 - return checked 59 - ? field.onChange([ 60 - ...(field.value || []), 61 - item.id, 62 - ]) 63 - : field.onChange( 64 - field.value?.filter( 65 - (value) => value !== item.id, 66 - ), 67 - ); 68 - }} 69 - > 70 - {item.title} 71 - </CheckboxLabel> 72 - </FormControl> 73 - </FormItem> 74 - ); 75 - }} 76 - /> 77 - ))} 78 - </div> 79 - {!pages || pages.length === 0 ? ( 80 - <FormDescription>Missing status pages.</FormDescription> 81 - ) : null} 82 - <FormMessage /> 83 - </FormItem> 84 - )} 85 - /> 86 - <FormField 87 - control={form.control} 88 33 name="monitors" 89 34 render={() => ( 90 35 <FormItem> ··· 118 63 ]) 119 64 : field.onChange( 120 65 field.value?.filter( 121 - (value) => value !== item.id, 122 - ), 66 + (value) => value !== item.id 67 + ) 123 68 ); 124 69 }} 125 70 >
+7 -8
apps/web/src/config/pages.ts
··· 89 89 segment: "edit", 90 90 }, 91 91 { 92 + title: "Status Reports", 93 + description: "Inform your users about recent reports", 94 + href: "/status-pages/[id]/reports", 95 + icon: "megaphone", 96 + segment: "reports", 97 + }, 98 + { 92 99 title: "Domain", 93 100 description: "Where you can see the domain settings.", 94 101 href: "/status-pages/[id]/domain", ··· 174 181 icon: "panel-top", 175 182 segment: "status-pages", 176 183 children: statusPagesPagesConfig, 177 - }, 178 - { 179 - title: "Status Reports", 180 - description: "War room where you handle the incidents.", 181 - href: "/status-reports", 182 - icon: "megaphone", 183 - segment: "status-reports", 184 - children: statusReportsPagesConfig, 185 184 }, 186 185 { 187 186 title: "Notifications",
+13 -17
packages/api/src/router/page.ts
··· 1 1 import { TRPCError } from "@trpc/server"; 2 2 import { z } from "zod"; 3 3 4 - import { and, eq, gte, inArray, isNull, lte, or, sql } from "@openstatus/db"; 4 + import { 5 + and, 6 + eq, 7 + gte, 8 + inArray, 9 + isNotNull, 10 + isNull, 11 + lte, 12 + or, 13 + sql, 14 + } from "@openstatus/db"; 5 15 import { 6 16 incidentTable, 7 17 insertPageSchema, ··· 10 20 monitorsToPages, 11 21 monitorsToStatusReport, 12 22 page, 13 - pagesToStatusReports, 14 23 selectPageSchemaWithMonitorsRelation, 15 24 selectPublicPageSchemaWithRelation, 16 25 statusReport, ··· 269 278 .all() 270 279 : []; 271 280 272 - const statusReportsToPagesResult = await opts.ctx.db 273 - .select() 274 - .from(pagesToStatusReports) 275 - .where(eq(pagesToStatusReports.pageId, result.id)) 276 - .all(); 277 - 278 281 const monitorStatusReportIds = monitorsToStatusReportResult.map( 279 282 ({ statusReportId }) => statusReportId, 280 283 ); 281 284 282 - const pageStatusReportIds = statusReportsToPagesResult.map( 283 - ({ statusReportId }) => statusReportId, 284 - ); 285 - 286 - const statusReportIds = Array.from( 287 - new Set([...monitorStatusReportIds, ...pageStatusReportIds]), 288 - ); 285 + const statusReportIds = Array.from(new Set([...monitorStatusReportIds])); 289 286 290 287 const statusReports = 291 288 statusReportIds.length > 0 292 289 ? await opts.ctx.db.query.statusReport.findMany({ 293 - where: or(inArray(statusReport.id, statusReportIds)), 290 + where: eq(statusReport.pageId, result.id), 294 291 with: { 295 292 statusReportUpdates: { 296 293 orderBy: (reports, { desc }) => desc(reports.date), 297 294 }, 298 295 monitorsToStatusReports: { with: { monitor: true } }, 299 - pagesToStatusReports: true, 300 296 }, 301 297 }) 302 298 : [];
+57 -82
packages/api/src/router/statusReport.ts
··· 7 7 monitorsToStatusReport, 8 8 page, 9 9 pageSubscriber, 10 - pagesToStatusReports, 11 10 selectMonitorSchema, 12 11 selectPublicStatusReportSchemaWithRelation, 13 12 selectStatusReportSchema, ··· 25 24 createStatusReport: protectedProcedure 26 25 .input(insertStatusReportSchema) 27 26 .mutation(async (opts) => { 28 - const { id, monitors, pages, date, message, ...statusReportInput } = 29 - opts.input; 27 + const { id, monitors, date, message, ...statusReportInput } = opts.input; 30 28 31 29 const newStatusReport = await opts.ctx.db 32 30 .insert(statusReport) ··· 50 48 .get(); 51 49 } 52 50 53 - if (pages.length > 0) { 54 - await opts.ctx.db 55 - .insert(pagesToStatusReports) 56 - .values( 57 - pages.map((page) => ({ 58 - pageId: page, 59 - statusReportId: newStatusReport.id, 60 - })), 61 - ) 62 - .returning() 63 - .get(); 64 - } 65 - 66 51 return newStatusReport; 67 52 }), 68 53 ··· 70 55 .input(insertStatusReportUpdateSchema) 71 56 .mutation(async (opts) => { 72 57 // update parent status report with latest status 73 - await opts.ctx.db 58 + const _statusReport = await opts.ctx.db 74 59 .update(statusReport) 75 60 .set({ status: opts.input.status, updatedAt: new Date() }) 76 61 .where( ··· 97 82 .from(workspace) 98 83 .where(eq(workspace.id, opts.ctx.workspace.id)) 99 84 .get(); 100 - if (currentWorkspace?.plan !== "pro") { 101 - const allPages = await opts.ctx.db 85 + if (currentWorkspace?.plan !== "pro" && _statusReport.pageId) { 86 + const subscribers = await opts.ctx.db 102 87 .select() 103 - .from(pagesToStatusReports) 88 + .from(pageSubscriber) 104 89 .where( 105 - eq( 106 - pagesToStatusReports.statusReportId, 107 - updatedValue.statusReportId, 90 + and( 91 + eq(pageSubscriber.pageId, _statusReport.pageId), 92 + isNotNull(pageSubscriber.acceptedAt), 108 93 ), 109 94 ) 110 95 .all(); 111 - for (const currentPage of allPages) { 112 - const subscribers = await opts.ctx.db 113 - .select() 114 - .from(pageSubscriber) 115 - .where( 116 - and( 117 - eq(pageSubscriber.pageId, currentPage.pageId), 118 - isNotNull(pageSubscriber.acceptedAt), 119 - ), 120 - ) 121 - .all(); 122 - const pageInfo = await opts.ctx.db 123 - .select() 124 - .from(page) 125 - .where(eq(page.id, currentPage.pageId)) 126 - .get(); 127 - if (!pageInfo) continue; 96 + const pageInfo = await opts.ctx.db 97 + .select() 98 + .from(page) 99 + .where(eq(page.id, _statusReport.pageId)) 100 + .get(); 101 + if (pageInfo) { 128 102 const subscribersEmails = subscribers.map( 129 103 (subscriber) => subscriber.email, 130 104 ); ··· 143 117 updateStatusReport: protectedProcedure 144 118 .input(insertStatusReportSchema) 145 119 .mutation(async (opts) => { 146 - const { monitors, pages, ...statusReportInput } = opts.input; 120 + const { monitors, ...statusReportInput } = opts.input; 147 121 148 122 if (!statusReportInput.id) return; 149 123 ··· 201 175 .run(); 202 176 } 203 177 204 - const currentPagesToStatusReports = await opts.ctx.db 205 - .select() 206 - .from(pagesToStatusReports) 207 - .where(eq(pagesToStatusReports.statusReportId, currentStatusReport.id)) 208 - .all(); 209 - 210 - const addedPages = pages?.filter( 211 - (x) => 212 - !currentPagesToStatusReports.map(({ pageId }) => pageId)?.includes(x), 213 - ); 214 - 215 - if (addedPages.length) { 216 - const values = addedPages.map((pageId) => ({ 217 - pageId, 218 - statusReportId: currentStatusReport.id, 219 - })); 220 - 221 - await opts.ctx.db.insert(pagesToStatusReports).values(values).run(); 222 - } 223 - 224 - const removedPages = currentPagesToStatusReports 225 - .map(({ pageId }) => pageId) 226 - .filter((x) => !pages?.includes(x)); 227 - 228 - if (removedPages.length) { 229 - await opts.ctx.db 230 - .delete(pagesToStatusReports) 231 - .where( 232 - and( 233 - eq(pagesToStatusReports.statusReportId, currentStatusReport.id), 234 - inArray(pagesToStatusReports.pageId, removedPages), 235 - ), 236 - ) 237 - .run(); 238 - } 239 - 240 178 return currentStatusReport; 241 179 }), 242 180 ··· 296 234 }), 297 235 298 236 getStatusReportById: protectedProcedure 299 - .input(z.object({ id: z.number() })) 237 + .input(z.object({ id: z.number(), pageId: z.number().optional() })) 300 238 .query(async (opts) => { 301 239 const selectPublicStatusReportSchemaWithRelation = 302 240 selectStatusReportSchema.extend({ ··· 309 247 monitor: selectMonitorSchema, 310 248 }), 311 249 ) 312 - .default([]), 313 - pagesToStatusReports: z 314 - .array(z.object({ statusReportId: z.number(), pageId: z.number() })) 315 250 .default([]), 316 251 statusReportUpdates: z.array(selectStatusReportUpdateSchema), 317 252 date: z.date().default(new Date()), ··· 321 256 where: and( 322 257 eq(statusReport.id, opts.input.id), 323 258 eq(statusReport.workspaceId, opts.ctx.workspace.id), 259 + // only allow to fetch status report if it belongs to the page 260 + opts.input.pageId 261 + ? eq(statusReport.pageId, opts.input.pageId) 262 + : undefined, 324 263 ), 325 264 with: { 326 265 monitorsToStatusReports: { with: { monitor: true } }, 327 - pagesToStatusReports: true, 328 266 statusReportUpdates: { 329 267 orderBy: (statusReportUpdate, { desc }) => [ 330 268 desc(statusReportUpdate.createdAt), ··· 377 315 return z.array(selectStatusSchemaWithRelation).parse(result); 378 316 }), 379 317 318 + getStatusReportByPageId: protectedProcedure 319 + .input(z.object({ id: z.number() })) 320 + .query(async (opts) => { 321 + // FIXME: can we get rid of that? 322 + const selectStatusSchemaWithRelation = selectStatusReportSchema.extend({ 323 + status: statusReportStatusSchema.default("investigating"), // TODO: remove! 324 + monitorsToStatusReports: z 325 + .array( 326 + z.object({ 327 + statusReportId: z.number(), 328 + monitorId: z.number(), 329 + monitor: selectMonitorSchema, 330 + }), 331 + ) 332 + .default([]), 333 + statusReportUpdates: z.array(selectStatusReportUpdateSchema), 334 + }); 335 + 336 + const result = await opts.ctx.db.query.statusReport.findMany({ 337 + where: and( 338 + eq(statusReport.workspaceId, opts.ctx.workspace.id), 339 + eq(statusReport.pageId, opts.input.id), 340 + ), 341 + with: { 342 + monitorsToStatusReports: { with: { monitor: true } }, 343 + statusReportUpdates: { 344 + orderBy: (statusReportUpdate, { desc }) => [ 345 + desc(statusReportUpdate.createdAt), 346 + ], 347 + }, 348 + }, 349 + orderBy: (statusReport, { desc }) => [desc(statusReport.updatedAt)], 350 + }); 351 + return z.array(selectStatusSchemaWithRelation).parse(result); 352 + }), 353 + 380 354 getPublicStatusReportById: publicProcedure 381 355 .input(z.object({ slug: z.string().toLowerCase(), id: z.number() })) 382 356 .query(async (opts) => { ··· 389 363 const _statusReport = await opts.ctx.db.query.statusReport.findFirst({ 390 364 where: and( 391 365 eq(statusReport.id, opts.input.id), 366 + eq(statusReport.pageId, result.id), 392 367 eq(statusReport.workspaceId, result.workspaceId), 393 368 ), 394 369 with: {
+7 -2
packages/api/src/router/workspace.ts
··· 3 3 import * as randomWordSlugs from "random-word-slugs"; 4 4 import { z } from "zod"; 5 5 6 - import { and, eq, sql } from "@openstatus/db"; 6 + import { and, eq, isNotNull, sql } from "@openstatus/db"; 7 7 import { 8 8 application, 9 9 monitor, ··· 206 206 const monitors = await tx 207 207 .select({ count: sql<number>`count(*)` }) 208 208 .from(monitor) 209 - .where(eq(monitor.workspaceId, opts.ctx.workspace.id)); 209 + .where( 210 + and( 211 + eq(monitor.workspaceId, opts.ctx.workspace.id), 212 + isNotNull(monitor.deletedAt), 213 + ), 214 + ); 210 215 const pages = await tx 211 216 .select({ count: sql<number>`count(*)` }) 212 217 .from(page)
+3
packages/db/drizzle/0034_serious_shard.sql
··· 1 + ALTER TABLE `status_report` ADD `page_id` integer REFERENCES page(id);--> statement-breakpoint 2 + 3 + UPDATE `status_report` SET `page_id` = `t`.`page_id` from (select `page_id`, `status_report_id` from `status_reports_to_pages`) `t` where `t`.`status_report_id` = `id` ;
+2186
packages/db/drizzle/meta/0034_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "145f2782-3cb0-4066-9842-881a17636999", 5 + "prevId": "447f81c0-1837-4e85-bb0e-9ecce75e95d9", 6 + "tables": { 7 + "status_report_to_monitors": { 8 + "name": "status_report_to_monitors", 9 + "columns": { 10 + "monitor_id": { 11 + "name": "monitor_id", 12 + "type": "integer", 13 + "primaryKey": false, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "status_report_id": { 18 + "name": "status_report_id", 19 + "type": "integer", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "created_at": { 25 + "name": "created_at", 26 + "type": "integer", 27 + "primaryKey": false, 28 + "notNull": false, 29 + "autoincrement": false, 30 + "default": "(strftime('%s', 'now'))" 31 + } 32 + }, 33 + "indexes": {}, 34 + "foreignKeys": { 35 + "status_report_to_monitors_monitor_id_monitor_id_fk": { 36 + "name": "status_report_to_monitors_monitor_id_monitor_id_fk", 37 + "tableFrom": "status_report_to_monitors", 38 + "tableTo": "monitor", 39 + "columnsFrom": [ 40 + "monitor_id" 41 + ], 42 + "columnsTo": [ 43 + "id" 44 + ], 45 + "onDelete": "cascade", 46 + "onUpdate": "no action" 47 + }, 48 + "status_report_to_monitors_status_report_id_status_report_id_fk": { 49 + "name": "status_report_to_monitors_status_report_id_status_report_id_fk", 50 + "tableFrom": "status_report_to_monitors", 51 + "tableTo": "status_report", 52 + "columnsFrom": [ 53 + "status_report_id" 54 + ], 55 + "columnsTo": [ 56 + "id" 57 + ], 58 + "onDelete": "cascade", 59 + "onUpdate": "no action" 60 + } 61 + }, 62 + "compositePrimaryKeys": { 63 + "status_report_to_monitors_monitor_id_status_report_id_pk": { 64 + "columns": [ 65 + "monitor_id", 66 + "status_report_id" 67 + ], 68 + "name": "status_report_to_monitors_monitor_id_status_report_id_pk" 69 + } 70 + }, 71 + "uniqueConstraints": {} 72 + }, 73 + "status_report": { 74 + "name": "status_report", 75 + "columns": { 76 + "id": { 77 + "name": "id", 78 + "type": "integer", 79 + "primaryKey": true, 80 + "notNull": true, 81 + "autoincrement": false 82 + }, 83 + "status": { 84 + "name": "status", 85 + "type": "text", 86 + "primaryKey": false, 87 + "notNull": true, 88 + "autoincrement": false 89 + }, 90 + "title": { 91 + "name": "title", 92 + "type": "text(256)", 93 + "primaryKey": false, 94 + "notNull": true, 95 + "autoincrement": false 96 + }, 97 + "workspace_id": { 98 + "name": "workspace_id", 99 + "type": "integer", 100 + "primaryKey": false, 101 + "notNull": false, 102 + "autoincrement": false 103 + }, 104 + "page_id": { 105 + "name": "page_id", 106 + "type": "integer", 107 + "primaryKey": false, 108 + "notNull": false, 109 + "autoincrement": false 110 + }, 111 + "created_at": { 112 + "name": "created_at", 113 + "type": "integer", 114 + "primaryKey": false, 115 + "notNull": false, 116 + "autoincrement": false, 117 + "default": "(strftime('%s', 'now'))" 118 + }, 119 + "updated_at": { 120 + "name": "updated_at", 121 + "type": "integer", 122 + "primaryKey": false, 123 + "notNull": false, 124 + "autoincrement": false, 125 + "default": "(strftime('%s', 'now'))" 126 + } 127 + }, 128 + "indexes": {}, 129 + "foreignKeys": { 130 + "status_report_workspace_id_workspace_id_fk": { 131 + "name": "status_report_workspace_id_workspace_id_fk", 132 + "tableFrom": "status_report", 133 + "tableTo": "workspace", 134 + "columnsFrom": [ 135 + "workspace_id" 136 + ], 137 + "columnsTo": [ 138 + "id" 139 + ], 140 + "onDelete": "no action", 141 + "onUpdate": "no action" 142 + }, 143 + "status_report_page_id_page_id_fk": { 144 + "name": "status_report_page_id_page_id_fk", 145 + "tableFrom": "status_report", 146 + "tableTo": "page", 147 + "columnsFrom": [ 148 + "page_id" 149 + ], 150 + "columnsTo": [ 151 + "id" 152 + ], 153 + "onDelete": "no action", 154 + "onUpdate": "no action" 155 + } 156 + }, 157 + "compositePrimaryKeys": {}, 158 + "uniqueConstraints": {} 159 + }, 160 + "status_report_update": { 161 + "name": "status_report_update", 162 + "columns": { 163 + "id": { 164 + "name": "id", 165 + "type": "integer", 166 + "primaryKey": true, 167 + "notNull": true, 168 + "autoincrement": false 169 + }, 170 + "status": { 171 + "name": "status", 172 + "type": "text(4)", 173 + "primaryKey": false, 174 + "notNull": true, 175 + "autoincrement": false 176 + }, 177 + "date": { 178 + "name": "date", 179 + "type": "integer", 180 + "primaryKey": false, 181 + "notNull": true, 182 + "autoincrement": false 183 + }, 184 + "message": { 185 + "name": "message", 186 + "type": "text", 187 + "primaryKey": false, 188 + "notNull": true, 189 + "autoincrement": false 190 + }, 191 + "status_report_id": { 192 + "name": "status_report_id", 193 + "type": "integer", 194 + "primaryKey": false, 195 + "notNull": true, 196 + "autoincrement": false 197 + }, 198 + "created_at": { 199 + "name": "created_at", 200 + "type": "integer", 201 + "primaryKey": false, 202 + "notNull": false, 203 + "autoincrement": false, 204 + "default": "(strftime('%s', 'now'))" 205 + }, 206 + "updated_at": { 207 + "name": "updated_at", 208 + "type": "integer", 209 + "primaryKey": false, 210 + "notNull": false, 211 + "autoincrement": false, 212 + "default": "(strftime('%s', 'now'))" 213 + } 214 + }, 215 + "indexes": {}, 216 + "foreignKeys": { 217 + "status_report_update_status_report_id_status_report_id_fk": { 218 + "name": "status_report_update_status_report_id_status_report_id_fk", 219 + "tableFrom": "status_report_update", 220 + "tableTo": "status_report", 221 + "columnsFrom": [ 222 + "status_report_id" 223 + ], 224 + "columnsTo": [ 225 + "id" 226 + ], 227 + "onDelete": "cascade", 228 + "onUpdate": "no action" 229 + } 230 + }, 231 + "compositePrimaryKeys": {}, 232 + "uniqueConstraints": {} 233 + }, 234 + "integration": { 235 + "name": "integration", 236 + "columns": { 237 + "id": { 238 + "name": "id", 239 + "type": "integer", 240 + "primaryKey": true, 241 + "notNull": true, 242 + "autoincrement": false 243 + }, 244 + "name": { 245 + "name": "name", 246 + "type": "text(256)", 247 + "primaryKey": false, 248 + "notNull": true, 249 + "autoincrement": false 250 + }, 251 + "workspace_id": { 252 + "name": "workspace_id", 253 + "type": "integer", 254 + "primaryKey": false, 255 + "notNull": false, 256 + "autoincrement": false 257 + }, 258 + "credential": { 259 + "name": "credential", 260 + "type": "text", 261 + "primaryKey": false, 262 + "notNull": false, 263 + "autoincrement": false 264 + }, 265 + "external_id": { 266 + "name": "external_id", 267 + "type": "text", 268 + "primaryKey": false, 269 + "notNull": true, 270 + "autoincrement": false 271 + }, 272 + "created_at": { 273 + "name": "created_at", 274 + "type": "integer", 275 + "primaryKey": false, 276 + "notNull": false, 277 + "autoincrement": false, 278 + "default": "(strftime('%s', 'now'))" 279 + }, 280 + "updated_at": { 281 + "name": "updated_at", 282 + "type": "integer", 283 + "primaryKey": false, 284 + "notNull": false, 285 + "autoincrement": false, 286 + "default": "(strftime('%s', 'now'))" 287 + }, 288 + "data": { 289 + "name": "data", 290 + "type": "text", 291 + "primaryKey": false, 292 + "notNull": true, 293 + "autoincrement": false 294 + } 295 + }, 296 + "indexes": {}, 297 + "foreignKeys": { 298 + "integration_workspace_id_workspace_id_fk": { 299 + "name": "integration_workspace_id_workspace_id_fk", 300 + "tableFrom": "integration", 301 + "tableTo": "workspace", 302 + "columnsFrom": [ 303 + "workspace_id" 304 + ], 305 + "columnsTo": [ 306 + "id" 307 + ], 308 + "onDelete": "no action", 309 + "onUpdate": "no action" 310 + } 311 + }, 312 + "compositePrimaryKeys": {}, 313 + "uniqueConstraints": {} 314 + }, 315 + "page": { 316 + "name": "page", 317 + "columns": { 318 + "id": { 319 + "name": "id", 320 + "type": "integer", 321 + "primaryKey": true, 322 + "notNull": true, 323 + "autoincrement": false 324 + }, 325 + "workspace_id": { 326 + "name": "workspace_id", 327 + "type": "integer", 328 + "primaryKey": false, 329 + "notNull": true, 330 + "autoincrement": false 331 + }, 332 + "title": { 333 + "name": "title", 334 + "type": "text", 335 + "primaryKey": false, 336 + "notNull": true, 337 + "autoincrement": false 338 + }, 339 + "description": { 340 + "name": "description", 341 + "type": "text", 342 + "primaryKey": false, 343 + "notNull": true, 344 + "autoincrement": false 345 + }, 346 + "icon": { 347 + "name": "icon", 348 + "type": "text(256)", 349 + "primaryKey": false, 350 + "notNull": false, 351 + "autoincrement": false, 352 + "default": "''" 353 + }, 354 + "slug": { 355 + "name": "slug", 356 + "type": "text(256)", 357 + "primaryKey": false, 358 + "notNull": true, 359 + "autoincrement": false 360 + }, 361 + "custom_domain": { 362 + "name": "custom_domain", 363 + "type": "text(256)", 364 + "primaryKey": false, 365 + "notNull": true, 366 + "autoincrement": false 367 + }, 368 + "published": { 369 + "name": "published", 370 + "type": "integer", 371 + "primaryKey": false, 372 + "notNull": false, 373 + "autoincrement": false, 374 + "default": false 375 + }, 376 + "password": { 377 + "name": "password", 378 + "type": "text(256)", 379 + "primaryKey": false, 380 + "notNull": false, 381 + "autoincrement": false 382 + }, 383 + "password_protected": { 384 + "name": "password_protected", 385 + "type": "integer", 386 + "primaryKey": false, 387 + "notNull": false, 388 + "autoincrement": false, 389 + "default": false 390 + }, 391 + "created_at": { 392 + "name": "created_at", 393 + "type": "integer", 394 + "primaryKey": false, 395 + "notNull": false, 396 + "autoincrement": false, 397 + "default": "(strftime('%s', 'now'))" 398 + }, 399 + "updated_at": { 400 + "name": "updated_at", 401 + "type": "integer", 402 + "primaryKey": false, 403 + "notNull": false, 404 + "autoincrement": false, 405 + "default": "(strftime('%s', 'now'))" 406 + } 407 + }, 408 + "indexes": { 409 + "page_slug_unique": { 410 + "name": "page_slug_unique", 411 + "columns": [ 412 + "slug" 413 + ], 414 + "isUnique": true 415 + } 416 + }, 417 + "foreignKeys": { 418 + "page_workspace_id_workspace_id_fk": { 419 + "name": "page_workspace_id_workspace_id_fk", 420 + "tableFrom": "page", 421 + "tableTo": "workspace", 422 + "columnsFrom": [ 423 + "workspace_id" 424 + ], 425 + "columnsTo": [ 426 + "id" 427 + ], 428 + "onDelete": "cascade", 429 + "onUpdate": "no action" 430 + } 431 + }, 432 + "compositePrimaryKeys": {}, 433 + "uniqueConstraints": {} 434 + }, 435 + "monitor": { 436 + "name": "monitor", 437 + "columns": { 438 + "id": { 439 + "name": "id", 440 + "type": "integer", 441 + "primaryKey": true, 442 + "notNull": true, 443 + "autoincrement": false 444 + }, 445 + "job_type": { 446 + "name": "job_type", 447 + "type": "text", 448 + "primaryKey": false, 449 + "notNull": true, 450 + "autoincrement": false, 451 + "default": "'other'" 452 + }, 453 + "periodicity": { 454 + "name": "periodicity", 455 + "type": "text", 456 + "primaryKey": false, 457 + "notNull": true, 458 + "autoincrement": false, 459 + "default": "'other'" 460 + }, 461 + "status": { 462 + "name": "status", 463 + "type": "text", 464 + "primaryKey": false, 465 + "notNull": true, 466 + "autoincrement": false, 467 + "default": "'active'" 468 + }, 469 + "active": { 470 + "name": "active", 471 + "type": "integer", 472 + "primaryKey": false, 473 + "notNull": false, 474 + "autoincrement": false, 475 + "default": false 476 + }, 477 + "regions": { 478 + "name": "regions", 479 + "type": "text", 480 + "primaryKey": false, 481 + "notNull": true, 482 + "autoincrement": false, 483 + "default": "''" 484 + }, 485 + "url": { 486 + "name": "url", 487 + "type": "text(2048)", 488 + "primaryKey": false, 489 + "notNull": true, 490 + "autoincrement": false 491 + }, 492 + "name": { 493 + "name": "name", 494 + "type": "text(256)", 495 + "primaryKey": false, 496 + "notNull": true, 497 + "autoincrement": false, 498 + "default": "''" 499 + }, 500 + "description": { 501 + "name": "description", 502 + "type": "text", 503 + "primaryKey": false, 504 + "notNull": true, 505 + "autoincrement": false, 506 + "default": "''" 507 + }, 508 + "headers": { 509 + "name": "headers", 510 + "type": "text", 511 + "primaryKey": false, 512 + "notNull": false, 513 + "autoincrement": false, 514 + "default": "''" 515 + }, 516 + "body": { 517 + "name": "body", 518 + "type": "text", 519 + "primaryKey": false, 520 + "notNull": false, 521 + "autoincrement": false, 522 + "default": "''" 523 + }, 524 + "method": { 525 + "name": "method", 526 + "type": "text", 527 + "primaryKey": false, 528 + "notNull": false, 529 + "autoincrement": false, 530 + "default": "'GET'" 531 + }, 532 + "workspace_id": { 533 + "name": "workspace_id", 534 + "type": "integer", 535 + "primaryKey": false, 536 + "notNull": false, 537 + "autoincrement": false 538 + }, 539 + "timeout": { 540 + "name": "timeout", 541 + "type": "integer", 542 + "primaryKey": false, 543 + "notNull": true, 544 + "autoincrement": false, 545 + "default": 45000 546 + }, 547 + "degraded_after": { 548 + "name": "degraded_after", 549 + "type": "integer", 550 + "primaryKey": false, 551 + "notNull": false, 552 + "autoincrement": false 553 + }, 554 + "assertions": { 555 + "name": "assertions", 556 + "type": "text", 557 + "primaryKey": false, 558 + "notNull": false, 559 + "autoincrement": false 560 + }, 561 + "public": { 562 + "name": "public", 563 + "type": "integer", 564 + "primaryKey": false, 565 + "notNull": false, 566 + "autoincrement": false, 567 + "default": false 568 + }, 569 + "created_at": { 570 + "name": "created_at", 571 + "type": "integer", 572 + "primaryKey": false, 573 + "notNull": false, 574 + "autoincrement": false, 575 + "default": "(strftime('%s', 'now'))" 576 + }, 577 + "updated_at": { 578 + "name": "updated_at", 579 + "type": "integer", 580 + "primaryKey": false, 581 + "notNull": false, 582 + "autoincrement": false, 583 + "default": "(strftime('%s', 'now'))" 584 + }, 585 + "deleted_at": { 586 + "name": "deleted_at", 587 + "type": "integer", 588 + "primaryKey": false, 589 + "notNull": false, 590 + "autoincrement": false 591 + } 592 + }, 593 + "indexes": {}, 594 + "foreignKeys": { 595 + "monitor_workspace_id_workspace_id_fk": { 596 + "name": "monitor_workspace_id_workspace_id_fk", 597 + "tableFrom": "monitor", 598 + "tableTo": "workspace", 599 + "columnsFrom": [ 600 + "workspace_id" 601 + ], 602 + "columnsTo": [ 603 + "id" 604 + ], 605 + "onDelete": "no action", 606 + "onUpdate": "no action" 607 + } 608 + }, 609 + "compositePrimaryKeys": {}, 610 + "uniqueConstraints": {} 611 + }, 612 + "monitors_to_pages": { 613 + "name": "monitors_to_pages", 614 + "columns": { 615 + "monitor_id": { 616 + "name": "monitor_id", 617 + "type": "integer", 618 + "primaryKey": false, 619 + "notNull": true, 620 + "autoincrement": false 621 + }, 622 + "page_id": { 623 + "name": "page_id", 624 + "type": "integer", 625 + "primaryKey": false, 626 + "notNull": true, 627 + "autoincrement": false 628 + }, 629 + "created_at": { 630 + "name": "created_at", 631 + "type": "integer", 632 + "primaryKey": false, 633 + "notNull": false, 634 + "autoincrement": false, 635 + "default": "(strftime('%s', 'now'))" 636 + }, 637 + "order": { 638 + "name": "order", 639 + "type": "integer", 640 + "primaryKey": false, 641 + "notNull": false, 642 + "autoincrement": false, 643 + "default": 0 644 + } 645 + }, 646 + "indexes": {}, 647 + "foreignKeys": { 648 + "monitors_to_pages_monitor_id_monitor_id_fk": { 649 + "name": "monitors_to_pages_monitor_id_monitor_id_fk", 650 + "tableFrom": "monitors_to_pages", 651 + "tableTo": "monitor", 652 + "columnsFrom": [ 653 + "monitor_id" 654 + ], 655 + "columnsTo": [ 656 + "id" 657 + ], 658 + "onDelete": "cascade", 659 + "onUpdate": "no action" 660 + }, 661 + "monitors_to_pages_page_id_page_id_fk": { 662 + "name": "monitors_to_pages_page_id_page_id_fk", 663 + "tableFrom": "monitors_to_pages", 664 + "tableTo": "page", 665 + "columnsFrom": [ 666 + "page_id" 667 + ], 668 + "columnsTo": [ 669 + "id" 670 + ], 671 + "onDelete": "cascade", 672 + "onUpdate": "no action" 673 + } 674 + }, 675 + "compositePrimaryKeys": { 676 + "monitors_to_pages_monitor_id_page_id_pk": { 677 + "columns": [ 678 + "monitor_id", 679 + "page_id" 680 + ], 681 + "name": "monitors_to_pages_monitor_id_page_id_pk" 682 + } 683 + }, 684 + "uniqueConstraints": {} 685 + }, 686 + "account": { 687 + "name": "account", 688 + "columns": { 689 + "user_id": { 690 + "name": "user_id", 691 + "type": "integer", 692 + "primaryKey": false, 693 + "notNull": true, 694 + "autoincrement": false 695 + }, 696 + "type": { 697 + "name": "type", 698 + "type": "text", 699 + "primaryKey": false, 700 + "notNull": true, 701 + "autoincrement": false 702 + }, 703 + "provider": { 704 + "name": "provider", 705 + "type": "text", 706 + "primaryKey": false, 707 + "notNull": true, 708 + "autoincrement": false 709 + }, 710 + "provider_account_id": { 711 + "name": "provider_account_id", 712 + "type": "text", 713 + "primaryKey": false, 714 + "notNull": true, 715 + "autoincrement": false 716 + }, 717 + "refresh_token": { 718 + "name": "refresh_token", 719 + "type": "text", 720 + "primaryKey": false, 721 + "notNull": false, 722 + "autoincrement": false 723 + }, 724 + "access_token": { 725 + "name": "access_token", 726 + "type": "text", 727 + "primaryKey": false, 728 + "notNull": false, 729 + "autoincrement": false 730 + }, 731 + "expires_at": { 732 + "name": "expires_at", 733 + "type": "integer", 734 + "primaryKey": false, 735 + "notNull": false, 736 + "autoincrement": false 737 + }, 738 + "token_type": { 739 + "name": "token_type", 740 + "type": "text", 741 + "primaryKey": false, 742 + "notNull": false, 743 + "autoincrement": false 744 + }, 745 + "scope": { 746 + "name": "scope", 747 + "type": "text", 748 + "primaryKey": false, 749 + "notNull": false, 750 + "autoincrement": false 751 + }, 752 + "id_token": { 753 + "name": "id_token", 754 + "type": "text", 755 + "primaryKey": false, 756 + "notNull": false, 757 + "autoincrement": false 758 + }, 759 + "session_state": { 760 + "name": "session_state", 761 + "type": "text", 762 + "primaryKey": false, 763 + "notNull": false, 764 + "autoincrement": false 765 + } 766 + }, 767 + "indexes": {}, 768 + "foreignKeys": { 769 + "account_user_id_user_id_fk": { 770 + "name": "account_user_id_user_id_fk", 771 + "tableFrom": "account", 772 + "tableTo": "user", 773 + "columnsFrom": [ 774 + "user_id" 775 + ], 776 + "columnsTo": [ 777 + "id" 778 + ], 779 + "onDelete": "cascade", 780 + "onUpdate": "no action" 781 + } 782 + }, 783 + "compositePrimaryKeys": { 784 + "account_provider_provider_account_id_pk": { 785 + "columns": [ 786 + "provider", 787 + "provider_account_id" 788 + ], 789 + "name": "account_provider_provider_account_id_pk" 790 + } 791 + }, 792 + "uniqueConstraints": {} 793 + }, 794 + "session": { 795 + "name": "session", 796 + "columns": { 797 + "session_token": { 798 + "name": "session_token", 799 + "type": "text", 800 + "primaryKey": true, 801 + "notNull": true, 802 + "autoincrement": false 803 + }, 804 + "user_id": { 805 + "name": "user_id", 806 + "type": "integer", 807 + "primaryKey": false, 808 + "notNull": true, 809 + "autoincrement": false 810 + }, 811 + "expires": { 812 + "name": "expires", 813 + "type": "integer", 814 + "primaryKey": false, 815 + "notNull": true, 816 + "autoincrement": false 817 + } 818 + }, 819 + "indexes": {}, 820 + "foreignKeys": { 821 + "session_user_id_user_id_fk": { 822 + "name": "session_user_id_user_id_fk", 823 + "tableFrom": "session", 824 + "tableTo": "user", 825 + "columnsFrom": [ 826 + "user_id" 827 + ], 828 + "columnsTo": [ 829 + "id" 830 + ], 831 + "onDelete": "cascade", 832 + "onUpdate": "no action" 833 + } 834 + }, 835 + "compositePrimaryKeys": {}, 836 + "uniqueConstraints": {} 837 + }, 838 + "user": { 839 + "name": "user", 840 + "columns": { 841 + "id": { 842 + "name": "id", 843 + "type": "integer", 844 + "primaryKey": true, 845 + "notNull": true, 846 + "autoincrement": false 847 + }, 848 + "tenant_id": { 849 + "name": "tenant_id", 850 + "type": "text(256)", 851 + "primaryKey": false, 852 + "notNull": false, 853 + "autoincrement": false 854 + }, 855 + "first_name": { 856 + "name": "first_name", 857 + "type": "text", 858 + "primaryKey": false, 859 + "notNull": false, 860 + "autoincrement": false, 861 + "default": "''" 862 + }, 863 + "last_name": { 864 + "name": "last_name", 865 + "type": "text", 866 + "primaryKey": false, 867 + "notNull": false, 868 + "autoincrement": false, 869 + "default": "''" 870 + }, 871 + "photo_url": { 872 + "name": "photo_url", 873 + "type": "text", 874 + "primaryKey": false, 875 + "notNull": false, 876 + "autoincrement": false, 877 + "default": "''" 878 + }, 879 + "name": { 880 + "name": "name", 881 + "type": "text", 882 + "primaryKey": false, 883 + "notNull": false, 884 + "autoincrement": false 885 + }, 886 + "email": { 887 + "name": "email", 888 + "type": "text", 889 + "primaryKey": false, 890 + "notNull": false, 891 + "autoincrement": false, 892 + "default": "''" 893 + }, 894 + "emailVerified": { 895 + "name": "emailVerified", 896 + "type": "integer", 897 + "primaryKey": false, 898 + "notNull": false, 899 + "autoincrement": false 900 + }, 901 + "created_at": { 902 + "name": "created_at", 903 + "type": "integer", 904 + "primaryKey": false, 905 + "notNull": false, 906 + "autoincrement": false, 907 + "default": "(strftime('%s', 'now'))" 908 + }, 909 + "updated_at": { 910 + "name": "updated_at", 911 + "type": "integer", 912 + "primaryKey": false, 913 + "notNull": false, 914 + "autoincrement": false, 915 + "default": "(strftime('%s', 'now'))" 916 + } 917 + }, 918 + "indexes": { 919 + "user_tenant_id_unique": { 920 + "name": "user_tenant_id_unique", 921 + "columns": [ 922 + "tenant_id" 923 + ], 924 + "isUnique": true 925 + } 926 + }, 927 + "foreignKeys": {}, 928 + "compositePrimaryKeys": {}, 929 + "uniqueConstraints": {} 930 + }, 931 + "users_to_workspaces": { 932 + "name": "users_to_workspaces", 933 + "columns": { 934 + "user_id": { 935 + "name": "user_id", 936 + "type": "integer", 937 + "primaryKey": false, 938 + "notNull": true, 939 + "autoincrement": false 940 + }, 941 + "workspace_id": { 942 + "name": "workspace_id", 943 + "type": "integer", 944 + "primaryKey": false, 945 + "notNull": true, 946 + "autoincrement": false 947 + }, 948 + "role": { 949 + "name": "role", 950 + "type": "text", 951 + "primaryKey": false, 952 + "notNull": true, 953 + "autoincrement": false, 954 + "default": "'member'" 955 + }, 956 + "created_at": { 957 + "name": "created_at", 958 + "type": "integer", 959 + "primaryKey": false, 960 + "notNull": false, 961 + "autoincrement": false, 962 + "default": "(strftime('%s', 'now'))" 963 + } 964 + }, 965 + "indexes": {}, 966 + "foreignKeys": { 967 + "users_to_workspaces_user_id_user_id_fk": { 968 + "name": "users_to_workspaces_user_id_user_id_fk", 969 + "tableFrom": "users_to_workspaces", 970 + "tableTo": "user", 971 + "columnsFrom": [ 972 + "user_id" 973 + ], 974 + "columnsTo": [ 975 + "id" 976 + ], 977 + "onDelete": "no action", 978 + "onUpdate": "no action" 979 + }, 980 + "users_to_workspaces_workspace_id_workspace_id_fk": { 981 + "name": "users_to_workspaces_workspace_id_workspace_id_fk", 982 + "tableFrom": "users_to_workspaces", 983 + "tableTo": "workspace", 984 + "columnsFrom": [ 985 + "workspace_id" 986 + ], 987 + "columnsTo": [ 988 + "id" 989 + ], 990 + "onDelete": "no action", 991 + "onUpdate": "no action" 992 + } 993 + }, 994 + "compositePrimaryKeys": { 995 + "users_to_workspaces_user_id_workspace_id_pk": { 996 + "columns": [ 997 + "user_id", 998 + "workspace_id" 999 + ], 1000 + "name": "users_to_workspaces_user_id_workspace_id_pk" 1001 + } 1002 + }, 1003 + "uniqueConstraints": {} 1004 + }, 1005 + "verification_token": { 1006 + "name": "verification_token", 1007 + "columns": { 1008 + "identifier": { 1009 + "name": "identifier", 1010 + "type": "text", 1011 + "primaryKey": false, 1012 + "notNull": true, 1013 + "autoincrement": false 1014 + }, 1015 + "token": { 1016 + "name": "token", 1017 + "type": "text", 1018 + "primaryKey": false, 1019 + "notNull": true, 1020 + "autoincrement": false 1021 + }, 1022 + "expires": { 1023 + "name": "expires", 1024 + "type": "integer", 1025 + "primaryKey": false, 1026 + "notNull": true, 1027 + "autoincrement": false 1028 + } 1029 + }, 1030 + "indexes": {}, 1031 + "foreignKeys": {}, 1032 + "compositePrimaryKeys": { 1033 + "verification_token_identifier_token_pk": { 1034 + "columns": [ 1035 + "identifier", 1036 + "token" 1037 + ], 1038 + "name": "verification_token_identifier_token_pk" 1039 + } 1040 + }, 1041 + "uniqueConstraints": {} 1042 + }, 1043 + "page_subscriber": { 1044 + "name": "page_subscriber", 1045 + "columns": { 1046 + "id": { 1047 + "name": "id", 1048 + "type": "integer", 1049 + "primaryKey": true, 1050 + "notNull": true, 1051 + "autoincrement": false 1052 + }, 1053 + "email": { 1054 + "name": "email", 1055 + "type": "text", 1056 + "primaryKey": false, 1057 + "notNull": true, 1058 + "autoincrement": false 1059 + }, 1060 + "page_id": { 1061 + "name": "page_id", 1062 + "type": "integer", 1063 + "primaryKey": false, 1064 + "notNull": true, 1065 + "autoincrement": false 1066 + }, 1067 + "token": { 1068 + "name": "token", 1069 + "type": "text", 1070 + "primaryKey": false, 1071 + "notNull": false, 1072 + "autoincrement": false 1073 + }, 1074 + "accepted_at": { 1075 + "name": "accepted_at", 1076 + "type": "integer", 1077 + "primaryKey": false, 1078 + "notNull": false, 1079 + "autoincrement": false 1080 + }, 1081 + "expires_at": { 1082 + "name": "expires_at", 1083 + "type": "integer", 1084 + "primaryKey": false, 1085 + "notNull": false, 1086 + "autoincrement": false 1087 + }, 1088 + "created_at": { 1089 + "name": "created_at", 1090 + "type": "integer", 1091 + "primaryKey": false, 1092 + "notNull": false, 1093 + "autoincrement": false, 1094 + "default": "(strftime('%s', 'now'))" 1095 + }, 1096 + "updated_at": { 1097 + "name": "updated_at", 1098 + "type": "integer", 1099 + "primaryKey": false, 1100 + "notNull": false, 1101 + "autoincrement": false, 1102 + "default": "(strftime('%s', 'now'))" 1103 + } 1104 + }, 1105 + "indexes": {}, 1106 + "foreignKeys": { 1107 + "page_subscriber_page_id_page_id_fk": { 1108 + "name": "page_subscriber_page_id_page_id_fk", 1109 + "tableFrom": "page_subscriber", 1110 + "tableTo": "page", 1111 + "columnsFrom": [ 1112 + "page_id" 1113 + ], 1114 + "columnsTo": [ 1115 + "id" 1116 + ], 1117 + "onDelete": "no action", 1118 + "onUpdate": "no action" 1119 + } 1120 + }, 1121 + "compositePrimaryKeys": {}, 1122 + "uniqueConstraints": {} 1123 + }, 1124 + "workspace": { 1125 + "name": "workspace", 1126 + "columns": { 1127 + "id": { 1128 + "name": "id", 1129 + "type": "integer", 1130 + "primaryKey": true, 1131 + "notNull": true, 1132 + "autoincrement": false 1133 + }, 1134 + "slug": { 1135 + "name": "slug", 1136 + "type": "text", 1137 + "primaryKey": false, 1138 + "notNull": true, 1139 + "autoincrement": false 1140 + }, 1141 + "name": { 1142 + "name": "name", 1143 + "type": "text", 1144 + "primaryKey": false, 1145 + "notNull": false, 1146 + "autoincrement": false 1147 + }, 1148 + "stripe_id": { 1149 + "name": "stripe_id", 1150 + "type": "text(256)", 1151 + "primaryKey": false, 1152 + "notNull": false, 1153 + "autoincrement": false 1154 + }, 1155 + "subscription_id": { 1156 + "name": "subscription_id", 1157 + "type": "text", 1158 + "primaryKey": false, 1159 + "notNull": false, 1160 + "autoincrement": false 1161 + }, 1162 + "plan": { 1163 + "name": "plan", 1164 + "type": "text", 1165 + "primaryKey": false, 1166 + "notNull": false, 1167 + "autoincrement": false 1168 + }, 1169 + "ends_at": { 1170 + "name": "ends_at", 1171 + "type": "integer", 1172 + "primaryKey": false, 1173 + "notNull": false, 1174 + "autoincrement": false 1175 + }, 1176 + "paid_until": { 1177 + "name": "paid_until", 1178 + "type": "integer", 1179 + "primaryKey": false, 1180 + "notNull": false, 1181 + "autoincrement": false 1182 + }, 1183 + "created_at": { 1184 + "name": "created_at", 1185 + "type": "integer", 1186 + "primaryKey": false, 1187 + "notNull": false, 1188 + "autoincrement": false, 1189 + "default": "(strftime('%s', 'now'))" 1190 + }, 1191 + "updated_at": { 1192 + "name": "updated_at", 1193 + "type": "integer", 1194 + "primaryKey": false, 1195 + "notNull": false, 1196 + "autoincrement": false, 1197 + "default": "(strftime('%s', 'now'))" 1198 + }, 1199 + "dsn": { 1200 + "name": "dsn", 1201 + "type": "text", 1202 + "primaryKey": false, 1203 + "notNull": false, 1204 + "autoincrement": false 1205 + } 1206 + }, 1207 + "indexes": { 1208 + "workspace_slug_unique": { 1209 + "name": "workspace_slug_unique", 1210 + "columns": [ 1211 + "slug" 1212 + ], 1213 + "isUnique": true 1214 + }, 1215 + "workspace_stripe_id_unique": { 1216 + "name": "workspace_stripe_id_unique", 1217 + "columns": [ 1218 + "stripe_id" 1219 + ], 1220 + "isUnique": true 1221 + }, 1222 + "workspace_id_dsn_unique": { 1223 + "name": "workspace_id_dsn_unique", 1224 + "columns": [ 1225 + "id", 1226 + "dsn" 1227 + ], 1228 + "isUnique": true 1229 + } 1230 + }, 1231 + "foreignKeys": {}, 1232 + "compositePrimaryKeys": {}, 1233 + "uniqueConstraints": {} 1234 + }, 1235 + "notification": { 1236 + "name": "notification", 1237 + "columns": { 1238 + "id": { 1239 + "name": "id", 1240 + "type": "integer", 1241 + "primaryKey": true, 1242 + "notNull": true, 1243 + "autoincrement": false 1244 + }, 1245 + "name": { 1246 + "name": "name", 1247 + "type": "text", 1248 + "primaryKey": false, 1249 + "notNull": true, 1250 + "autoincrement": false 1251 + }, 1252 + "provider": { 1253 + "name": "provider", 1254 + "type": "text", 1255 + "primaryKey": false, 1256 + "notNull": true, 1257 + "autoincrement": false 1258 + }, 1259 + "data": { 1260 + "name": "data", 1261 + "type": "text", 1262 + "primaryKey": false, 1263 + "notNull": false, 1264 + "autoincrement": false, 1265 + "default": "'{}'" 1266 + }, 1267 + "workspace_id": { 1268 + "name": "workspace_id", 1269 + "type": "integer", 1270 + "primaryKey": false, 1271 + "notNull": false, 1272 + "autoincrement": false 1273 + }, 1274 + "created_at": { 1275 + "name": "created_at", 1276 + "type": "integer", 1277 + "primaryKey": false, 1278 + "notNull": false, 1279 + "autoincrement": false, 1280 + "default": "(strftime('%s', 'now'))" 1281 + }, 1282 + "updated_at": { 1283 + "name": "updated_at", 1284 + "type": "integer", 1285 + "primaryKey": false, 1286 + "notNull": false, 1287 + "autoincrement": false, 1288 + "default": "(strftime('%s', 'now'))" 1289 + } 1290 + }, 1291 + "indexes": {}, 1292 + "foreignKeys": { 1293 + "notification_workspace_id_workspace_id_fk": { 1294 + "name": "notification_workspace_id_workspace_id_fk", 1295 + "tableFrom": "notification", 1296 + "tableTo": "workspace", 1297 + "columnsFrom": [ 1298 + "workspace_id" 1299 + ], 1300 + "columnsTo": [ 1301 + "id" 1302 + ], 1303 + "onDelete": "no action", 1304 + "onUpdate": "no action" 1305 + } 1306 + }, 1307 + "compositePrimaryKeys": {}, 1308 + "uniqueConstraints": {} 1309 + }, 1310 + "notifications_to_monitors": { 1311 + "name": "notifications_to_monitors", 1312 + "columns": { 1313 + "monitor_id": { 1314 + "name": "monitor_id", 1315 + "type": "integer", 1316 + "primaryKey": false, 1317 + "notNull": true, 1318 + "autoincrement": false 1319 + }, 1320 + "notification_id": { 1321 + "name": "notification_id", 1322 + "type": "integer", 1323 + "primaryKey": false, 1324 + "notNull": true, 1325 + "autoincrement": false 1326 + }, 1327 + "created_at": { 1328 + "name": "created_at", 1329 + "type": "integer", 1330 + "primaryKey": false, 1331 + "notNull": false, 1332 + "autoincrement": false, 1333 + "default": "(strftime('%s', 'now'))" 1334 + } 1335 + }, 1336 + "indexes": {}, 1337 + "foreignKeys": { 1338 + "notifications_to_monitors_monitor_id_monitor_id_fk": { 1339 + "name": "notifications_to_monitors_monitor_id_monitor_id_fk", 1340 + "tableFrom": "notifications_to_monitors", 1341 + "tableTo": "monitor", 1342 + "columnsFrom": [ 1343 + "monitor_id" 1344 + ], 1345 + "columnsTo": [ 1346 + "id" 1347 + ], 1348 + "onDelete": "cascade", 1349 + "onUpdate": "no action" 1350 + }, 1351 + "notifications_to_monitors_notification_id_notification_id_fk": { 1352 + "name": "notifications_to_monitors_notification_id_notification_id_fk", 1353 + "tableFrom": "notifications_to_monitors", 1354 + "tableTo": "notification", 1355 + "columnsFrom": [ 1356 + "notification_id" 1357 + ], 1358 + "columnsTo": [ 1359 + "id" 1360 + ], 1361 + "onDelete": "cascade", 1362 + "onUpdate": "no action" 1363 + } 1364 + }, 1365 + "compositePrimaryKeys": { 1366 + "notifications_to_monitors_monitor_id_notification_id_pk": { 1367 + "columns": [ 1368 + "monitor_id", 1369 + "notification_id" 1370 + ], 1371 + "name": "notifications_to_monitors_monitor_id_notification_id_pk" 1372 + } 1373 + }, 1374 + "uniqueConstraints": {} 1375 + }, 1376 + "monitor_status": { 1377 + "name": "monitor_status", 1378 + "columns": { 1379 + "monitor_id": { 1380 + "name": "monitor_id", 1381 + "type": "integer", 1382 + "primaryKey": false, 1383 + "notNull": true, 1384 + "autoincrement": false 1385 + }, 1386 + "region": { 1387 + "name": "region", 1388 + "type": "text", 1389 + "primaryKey": false, 1390 + "notNull": true, 1391 + "autoincrement": false, 1392 + "default": "''" 1393 + }, 1394 + "status": { 1395 + "name": "status", 1396 + "type": "text", 1397 + "primaryKey": false, 1398 + "notNull": true, 1399 + "autoincrement": false, 1400 + "default": "'active'" 1401 + }, 1402 + "created_at": { 1403 + "name": "created_at", 1404 + "type": "integer", 1405 + "primaryKey": false, 1406 + "notNull": false, 1407 + "autoincrement": false, 1408 + "default": "(strftime('%s', 'now'))" 1409 + }, 1410 + "updated_at": { 1411 + "name": "updated_at", 1412 + "type": "integer", 1413 + "primaryKey": false, 1414 + "notNull": false, 1415 + "autoincrement": false, 1416 + "default": "(strftime('%s', 'now'))" 1417 + } 1418 + }, 1419 + "indexes": { 1420 + "monitor_status_idx": { 1421 + "name": "monitor_status_idx", 1422 + "columns": [ 1423 + "monitor_id", 1424 + "region" 1425 + ], 1426 + "isUnique": false 1427 + } 1428 + }, 1429 + "foreignKeys": { 1430 + "monitor_status_monitor_id_monitor_id_fk": { 1431 + "name": "monitor_status_monitor_id_monitor_id_fk", 1432 + "tableFrom": "monitor_status", 1433 + "tableTo": "monitor", 1434 + "columnsFrom": [ 1435 + "monitor_id" 1436 + ], 1437 + "columnsTo": [ 1438 + "id" 1439 + ], 1440 + "onDelete": "cascade", 1441 + "onUpdate": "no action" 1442 + } 1443 + }, 1444 + "compositePrimaryKeys": { 1445 + "monitor_status_monitor_id_region_pk": { 1446 + "columns": [ 1447 + "monitor_id", 1448 + "region" 1449 + ], 1450 + "name": "monitor_status_monitor_id_region_pk" 1451 + } 1452 + }, 1453 + "uniqueConstraints": {} 1454 + }, 1455 + "invitation": { 1456 + "name": "invitation", 1457 + "columns": { 1458 + "id": { 1459 + "name": "id", 1460 + "type": "integer", 1461 + "primaryKey": true, 1462 + "notNull": true, 1463 + "autoincrement": false 1464 + }, 1465 + "email": { 1466 + "name": "email", 1467 + "type": "text", 1468 + "primaryKey": false, 1469 + "notNull": true, 1470 + "autoincrement": false 1471 + }, 1472 + "role": { 1473 + "name": "role", 1474 + "type": "text", 1475 + "primaryKey": false, 1476 + "notNull": true, 1477 + "autoincrement": false, 1478 + "default": "'member'" 1479 + }, 1480 + "workspace_id": { 1481 + "name": "workspace_id", 1482 + "type": "integer", 1483 + "primaryKey": false, 1484 + "notNull": true, 1485 + "autoincrement": false 1486 + }, 1487 + "token": { 1488 + "name": "token", 1489 + "type": "text", 1490 + "primaryKey": false, 1491 + "notNull": true, 1492 + "autoincrement": false 1493 + }, 1494 + "expires_at": { 1495 + "name": "expires_at", 1496 + "type": "integer", 1497 + "primaryKey": false, 1498 + "notNull": true, 1499 + "autoincrement": false 1500 + }, 1501 + "created_at": { 1502 + "name": "created_at", 1503 + "type": "integer", 1504 + "primaryKey": false, 1505 + "notNull": false, 1506 + "autoincrement": false, 1507 + "default": "(strftime('%s', 'now'))" 1508 + }, 1509 + "accepted_at": { 1510 + "name": "accepted_at", 1511 + "type": "integer", 1512 + "primaryKey": false, 1513 + "notNull": false, 1514 + "autoincrement": false 1515 + } 1516 + }, 1517 + "indexes": {}, 1518 + "foreignKeys": {}, 1519 + "compositePrimaryKeys": {}, 1520 + "uniqueConstraints": {} 1521 + }, 1522 + "incident": { 1523 + "name": "incident", 1524 + "columns": { 1525 + "id": { 1526 + "name": "id", 1527 + "type": "integer", 1528 + "primaryKey": true, 1529 + "notNull": true, 1530 + "autoincrement": false 1531 + }, 1532 + "title": { 1533 + "name": "title", 1534 + "type": "text", 1535 + "primaryKey": false, 1536 + "notNull": true, 1537 + "autoincrement": false, 1538 + "default": "''" 1539 + }, 1540 + "summary": { 1541 + "name": "summary", 1542 + "type": "text", 1543 + "primaryKey": false, 1544 + "notNull": true, 1545 + "autoincrement": false, 1546 + "default": "''" 1547 + }, 1548 + "status": { 1549 + "name": "status", 1550 + "type": "text", 1551 + "primaryKey": false, 1552 + "notNull": true, 1553 + "autoincrement": false, 1554 + "default": "'triage'" 1555 + }, 1556 + "monitor_id": { 1557 + "name": "monitor_id", 1558 + "type": "integer", 1559 + "primaryKey": false, 1560 + "notNull": false, 1561 + "autoincrement": false 1562 + }, 1563 + "workspace_id": { 1564 + "name": "workspace_id", 1565 + "type": "integer", 1566 + "primaryKey": false, 1567 + "notNull": false, 1568 + "autoincrement": false 1569 + }, 1570 + "started_at": { 1571 + "name": "started_at", 1572 + "type": "integer", 1573 + "primaryKey": false, 1574 + "notNull": true, 1575 + "autoincrement": false, 1576 + "default": "(strftime('%s', 'now'))" 1577 + }, 1578 + "acknowledged_at": { 1579 + "name": "acknowledged_at", 1580 + "type": "integer", 1581 + "primaryKey": false, 1582 + "notNull": false, 1583 + "autoincrement": false 1584 + }, 1585 + "acknowledged_by": { 1586 + "name": "acknowledged_by", 1587 + "type": "integer", 1588 + "primaryKey": false, 1589 + "notNull": false, 1590 + "autoincrement": false 1591 + }, 1592 + "resolved_at": { 1593 + "name": "resolved_at", 1594 + "type": "integer", 1595 + "primaryKey": false, 1596 + "notNull": false, 1597 + "autoincrement": false 1598 + }, 1599 + "resolved_by": { 1600 + "name": "resolved_by", 1601 + "type": "integer", 1602 + "primaryKey": false, 1603 + "notNull": false, 1604 + "autoincrement": false 1605 + }, 1606 + "incident_screenshot_url": { 1607 + "name": "incident_screenshot_url", 1608 + "type": "text", 1609 + "primaryKey": false, 1610 + "notNull": false, 1611 + "autoincrement": false 1612 + }, 1613 + "recovery_screenshot_url": { 1614 + "name": "recovery_screenshot_url", 1615 + "type": "text", 1616 + "primaryKey": false, 1617 + "notNull": false, 1618 + "autoincrement": false 1619 + }, 1620 + "auto_resolved": { 1621 + "name": "auto_resolved", 1622 + "type": "integer", 1623 + "primaryKey": false, 1624 + "notNull": false, 1625 + "autoincrement": false, 1626 + "default": false 1627 + }, 1628 + "created_at": { 1629 + "name": "created_at", 1630 + "type": "integer", 1631 + "primaryKey": false, 1632 + "notNull": false, 1633 + "autoincrement": false, 1634 + "default": "(strftime('%s', 'now'))" 1635 + }, 1636 + "updated_at": { 1637 + "name": "updated_at", 1638 + "type": "integer", 1639 + "primaryKey": false, 1640 + "notNull": false, 1641 + "autoincrement": false, 1642 + "default": "(strftime('%s', 'now'))" 1643 + } 1644 + }, 1645 + "indexes": { 1646 + "incident_monitor_id_started_at_unique": { 1647 + "name": "incident_monitor_id_started_at_unique", 1648 + "columns": [ 1649 + "monitor_id", 1650 + "started_at" 1651 + ], 1652 + "isUnique": true 1653 + } 1654 + }, 1655 + "foreignKeys": { 1656 + "incident_monitor_id_monitor_id_fk": { 1657 + "name": "incident_monitor_id_monitor_id_fk", 1658 + "tableFrom": "incident", 1659 + "tableTo": "monitor", 1660 + "columnsFrom": [ 1661 + "monitor_id" 1662 + ], 1663 + "columnsTo": [ 1664 + "id" 1665 + ], 1666 + "onDelete": "set default", 1667 + "onUpdate": "no action" 1668 + }, 1669 + "incident_workspace_id_workspace_id_fk": { 1670 + "name": "incident_workspace_id_workspace_id_fk", 1671 + "tableFrom": "incident", 1672 + "tableTo": "workspace", 1673 + "columnsFrom": [ 1674 + "workspace_id" 1675 + ], 1676 + "columnsTo": [ 1677 + "id" 1678 + ], 1679 + "onDelete": "no action", 1680 + "onUpdate": "no action" 1681 + }, 1682 + "incident_acknowledged_by_user_id_fk": { 1683 + "name": "incident_acknowledged_by_user_id_fk", 1684 + "tableFrom": "incident", 1685 + "tableTo": "user", 1686 + "columnsFrom": [ 1687 + "acknowledged_by" 1688 + ], 1689 + "columnsTo": [ 1690 + "id" 1691 + ], 1692 + "onDelete": "no action", 1693 + "onUpdate": "no action" 1694 + }, 1695 + "incident_resolved_by_user_id_fk": { 1696 + "name": "incident_resolved_by_user_id_fk", 1697 + "tableFrom": "incident", 1698 + "tableTo": "user", 1699 + "columnsFrom": [ 1700 + "resolved_by" 1701 + ], 1702 + "columnsTo": [ 1703 + "id" 1704 + ], 1705 + "onDelete": "no action", 1706 + "onUpdate": "no action" 1707 + } 1708 + }, 1709 + "compositePrimaryKeys": {}, 1710 + "uniqueConstraints": {} 1711 + }, 1712 + "monitor_tag": { 1713 + "name": "monitor_tag", 1714 + "columns": { 1715 + "id": { 1716 + "name": "id", 1717 + "type": "integer", 1718 + "primaryKey": true, 1719 + "notNull": true, 1720 + "autoincrement": false 1721 + }, 1722 + "workspace_id": { 1723 + "name": "workspace_id", 1724 + "type": "integer", 1725 + "primaryKey": false, 1726 + "notNull": true, 1727 + "autoincrement": false 1728 + }, 1729 + "name": { 1730 + "name": "name", 1731 + "type": "text", 1732 + "primaryKey": false, 1733 + "notNull": true, 1734 + "autoincrement": false 1735 + }, 1736 + "color": { 1737 + "name": "color", 1738 + "type": "text", 1739 + "primaryKey": false, 1740 + "notNull": true, 1741 + "autoincrement": false 1742 + }, 1743 + "created_at": { 1744 + "name": "created_at", 1745 + "type": "integer", 1746 + "primaryKey": false, 1747 + "notNull": false, 1748 + "autoincrement": false, 1749 + "default": "(strftime('%s', 'now'))" 1750 + }, 1751 + "updated_at": { 1752 + "name": "updated_at", 1753 + "type": "integer", 1754 + "primaryKey": false, 1755 + "notNull": false, 1756 + "autoincrement": false, 1757 + "default": "(strftime('%s', 'now'))" 1758 + } 1759 + }, 1760 + "indexes": {}, 1761 + "foreignKeys": { 1762 + "monitor_tag_workspace_id_workspace_id_fk": { 1763 + "name": "monitor_tag_workspace_id_workspace_id_fk", 1764 + "tableFrom": "monitor_tag", 1765 + "tableTo": "workspace", 1766 + "columnsFrom": [ 1767 + "workspace_id" 1768 + ], 1769 + "columnsTo": [ 1770 + "id" 1771 + ], 1772 + "onDelete": "cascade", 1773 + "onUpdate": "no action" 1774 + } 1775 + }, 1776 + "compositePrimaryKeys": {}, 1777 + "uniqueConstraints": {} 1778 + }, 1779 + "monitor_tag_to_monitor": { 1780 + "name": "monitor_tag_to_monitor", 1781 + "columns": { 1782 + "monitor_id": { 1783 + "name": "monitor_id", 1784 + "type": "integer", 1785 + "primaryKey": false, 1786 + "notNull": true, 1787 + "autoincrement": false 1788 + }, 1789 + "monitor_tag_id": { 1790 + "name": "monitor_tag_id", 1791 + "type": "integer", 1792 + "primaryKey": false, 1793 + "notNull": true, 1794 + "autoincrement": false 1795 + }, 1796 + "created_at": { 1797 + "name": "created_at", 1798 + "type": "integer", 1799 + "primaryKey": false, 1800 + "notNull": false, 1801 + "autoincrement": false, 1802 + "default": "(strftime('%s', 'now'))" 1803 + } 1804 + }, 1805 + "indexes": {}, 1806 + "foreignKeys": { 1807 + "monitor_tag_to_monitor_monitor_id_monitor_id_fk": { 1808 + "name": "monitor_tag_to_monitor_monitor_id_monitor_id_fk", 1809 + "tableFrom": "monitor_tag_to_monitor", 1810 + "tableTo": "monitor", 1811 + "columnsFrom": [ 1812 + "monitor_id" 1813 + ], 1814 + "columnsTo": [ 1815 + "id" 1816 + ], 1817 + "onDelete": "cascade", 1818 + "onUpdate": "no action" 1819 + }, 1820 + "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk": { 1821 + "name": "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk", 1822 + "tableFrom": "monitor_tag_to_monitor", 1823 + "tableTo": "monitor_tag", 1824 + "columnsFrom": [ 1825 + "monitor_tag_id" 1826 + ], 1827 + "columnsTo": [ 1828 + "id" 1829 + ], 1830 + "onDelete": "cascade", 1831 + "onUpdate": "no action" 1832 + } 1833 + }, 1834 + "compositePrimaryKeys": { 1835 + "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk": { 1836 + "columns": [ 1837 + "monitor_id", 1838 + "monitor_tag_id" 1839 + ], 1840 + "name": "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk" 1841 + } 1842 + }, 1843 + "uniqueConstraints": {} 1844 + }, 1845 + "application": { 1846 + "name": "application", 1847 + "columns": { 1848 + "id": { 1849 + "name": "id", 1850 + "type": "integer", 1851 + "primaryKey": true, 1852 + "notNull": true, 1853 + "autoincrement": false 1854 + }, 1855 + "name": { 1856 + "name": "name", 1857 + "type": "text", 1858 + "primaryKey": false, 1859 + "notNull": false, 1860 + "autoincrement": false 1861 + }, 1862 + "dsn": { 1863 + "name": "dsn", 1864 + "type": "text", 1865 + "primaryKey": false, 1866 + "notNull": false, 1867 + "autoincrement": false 1868 + }, 1869 + "workspace_id": { 1870 + "name": "workspace_id", 1871 + "type": "integer", 1872 + "primaryKey": false, 1873 + "notNull": false, 1874 + "autoincrement": false 1875 + }, 1876 + "created_at": { 1877 + "name": "created_at", 1878 + "type": "integer", 1879 + "primaryKey": false, 1880 + "notNull": false, 1881 + "autoincrement": false, 1882 + "default": "(strftime('%s', 'now'))" 1883 + }, 1884 + "updated_at": { 1885 + "name": "updated_at", 1886 + "type": "integer", 1887 + "primaryKey": false, 1888 + "notNull": false, 1889 + "autoincrement": false, 1890 + "default": "(strftime('%s', 'now'))" 1891 + } 1892 + }, 1893 + "indexes": { 1894 + "application_dsn_unique": { 1895 + "name": "application_dsn_unique", 1896 + "columns": [ 1897 + "dsn" 1898 + ], 1899 + "isUnique": true 1900 + } 1901 + }, 1902 + "foreignKeys": { 1903 + "application_workspace_id_workspace_id_fk": { 1904 + "name": "application_workspace_id_workspace_id_fk", 1905 + "tableFrom": "application", 1906 + "tableTo": "workspace", 1907 + "columnsFrom": [ 1908 + "workspace_id" 1909 + ], 1910 + "columnsTo": [ 1911 + "id" 1912 + ], 1913 + "onDelete": "no action", 1914 + "onUpdate": "no action" 1915 + } 1916 + }, 1917 + "compositePrimaryKeys": {}, 1918 + "uniqueConstraints": {} 1919 + }, 1920 + "maintenance": { 1921 + "name": "maintenance", 1922 + "columns": { 1923 + "id": { 1924 + "name": "id", 1925 + "type": "integer", 1926 + "primaryKey": true, 1927 + "notNull": true, 1928 + "autoincrement": false 1929 + }, 1930 + "title": { 1931 + "name": "title", 1932 + "type": "text(256)", 1933 + "primaryKey": false, 1934 + "notNull": true, 1935 + "autoincrement": false 1936 + }, 1937 + "message": { 1938 + "name": "message", 1939 + "type": "text", 1940 + "primaryKey": false, 1941 + "notNull": true, 1942 + "autoincrement": false 1943 + }, 1944 + "from": { 1945 + "name": "from", 1946 + "type": "integer", 1947 + "primaryKey": false, 1948 + "notNull": true, 1949 + "autoincrement": false 1950 + }, 1951 + "to": { 1952 + "name": "to", 1953 + "type": "integer", 1954 + "primaryKey": false, 1955 + "notNull": true, 1956 + "autoincrement": false 1957 + }, 1958 + "workspace_id": { 1959 + "name": "workspace_id", 1960 + "type": "integer", 1961 + "primaryKey": false, 1962 + "notNull": false, 1963 + "autoincrement": false 1964 + }, 1965 + "page_id": { 1966 + "name": "page_id", 1967 + "type": "integer", 1968 + "primaryKey": false, 1969 + "notNull": false, 1970 + "autoincrement": false 1971 + }, 1972 + "created_at": { 1973 + "name": "created_at", 1974 + "type": "integer", 1975 + "primaryKey": false, 1976 + "notNull": false, 1977 + "autoincrement": false, 1978 + "default": "(strftime('%s', 'now'))" 1979 + }, 1980 + "updated_at": { 1981 + "name": "updated_at", 1982 + "type": "integer", 1983 + "primaryKey": false, 1984 + "notNull": false, 1985 + "autoincrement": false, 1986 + "default": "(strftime('%s', 'now'))" 1987 + } 1988 + }, 1989 + "indexes": {}, 1990 + "foreignKeys": { 1991 + "maintenance_workspace_id_workspace_id_fk": { 1992 + "name": "maintenance_workspace_id_workspace_id_fk", 1993 + "tableFrom": "maintenance", 1994 + "tableTo": "workspace", 1995 + "columnsFrom": [ 1996 + "workspace_id" 1997 + ], 1998 + "columnsTo": [ 1999 + "id" 2000 + ], 2001 + "onDelete": "no action", 2002 + "onUpdate": "no action" 2003 + }, 2004 + "maintenance_page_id_page_id_fk": { 2005 + "name": "maintenance_page_id_page_id_fk", 2006 + "tableFrom": "maintenance", 2007 + "tableTo": "page", 2008 + "columnsFrom": [ 2009 + "page_id" 2010 + ], 2011 + "columnsTo": [ 2012 + "id" 2013 + ], 2014 + "onDelete": "no action", 2015 + "onUpdate": "no action" 2016 + } 2017 + }, 2018 + "compositePrimaryKeys": {}, 2019 + "uniqueConstraints": {} 2020 + }, 2021 + "maintenance_to_monitor": { 2022 + "name": "maintenance_to_monitor", 2023 + "columns": { 2024 + "monitor_id": { 2025 + "name": "monitor_id", 2026 + "type": "integer", 2027 + "primaryKey": false, 2028 + "notNull": true, 2029 + "autoincrement": false 2030 + }, 2031 + "maintenance_id": { 2032 + "name": "maintenance_id", 2033 + "type": "integer", 2034 + "primaryKey": false, 2035 + "notNull": true, 2036 + "autoincrement": false 2037 + }, 2038 + "created_at": { 2039 + "name": "created_at", 2040 + "type": "integer", 2041 + "primaryKey": false, 2042 + "notNull": false, 2043 + "autoincrement": false, 2044 + "default": "(strftime('%s', 'now'))" 2045 + } 2046 + }, 2047 + "indexes": {}, 2048 + "foreignKeys": { 2049 + "maintenance_to_monitor_monitor_id_monitor_id_fk": { 2050 + "name": "maintenance_to_monitor_monitor_id_monitor_id_fk", 2051 + "tableFrom": "maintenance_to_monitor", 2052 + "tableTo": "monitor", 2053 + "columnsFrom": [ 2054 + "monitor_id" 2055 + ], 2056 + "columnsTo": [ 2057 + "id" 2058 + ], 2059 + "onDelete": "cascade", 2060 + "onUpdate": "no action" 2061 + }, 2062 + "maintenance_to_monitor_maintenance_id_maintenance_id_fk": { 2063 + "name": "maintenance_to_monitor_maintenance_id_maintenance_id_fk", 2064 + "tableFrom": "maintenance_to_monitor", 2065 + "tableTo": "maintenance", 2066 + "columnsFrom": [ 2067 + "maintenance_id" 2068 + ], 2069 + "columnsTo": [ 2070 + "id" 2071 + ], 2072 + "onDelete": "cascade", 2073 + "onUpdate": "no action" 2074 + } 2075 + }, 2076 + "compositePrimaryKeys": { 2077 + "maintenance_to_monitor_monitor_id_maintenance_id_pk": { 2078 + "columns": [ 2079 + "maintenance_id", 2080 + "monitor_id" 2081 + ], 2082 + "name": "maintenance_to_monitor_monitor_id_maintenance_id_pk" 2083 + } 2084 + }, 2085 + "uniqueConstraints": {} 2086 + }, 2087 + "check": { 2088 + "name": "check", 2089 + "columns": { 2090 + "id": { 2091 + "name": "id", 2092 + "type": "integer", 2093 + "primaryKey": true, 2094 + "notNull": true, 2095 + "autoincrement": true 2096 + }, 2097 + "regions": { 2098 + "name": "regions", 2099 + "type": "text", 2100 + "primaryKey": false, 2101 + "notNull": true, 2102 + "autoincrement": false, 2103 + "default": "''" 2104 + }, 2105 + "url": { 2106 + "name": "url", 2107 + "type": "text(4096)", 2108 + "primaryKey": false, 2109 + "notNull": true, 2110 + "autoincrement": false 2111 + }, 2112 + "headers": { 2113 + "name": "headers", 2114 + "type": "text", 2115 + "primaryKey": false, 2116 + "notNull": false, 2117 + "autoincrement": false, 2118 + "default": "''" 2119 + }, 2120 + "body": { 2121 + "name": "body", 2122 + "type": "text", 2123 + "primaryKey": false, 2124 + "notNull": false, 2125 + "autoincrement": false, 2126 + "default": "''" 2127 + }, 2128 + "method": { 2129 + "name": "method", 2130 + "type": "text", 2131 + "primaryKey": false, 2132 + "notNull": false, 2133 + "autoincrement": false, 2134 + "default": "'GET'" 2135 + }, 2136 + "count_requests": { 2137 + "name": "count_requests", 2138 + "type": "integer", 2139 + "primaryKey": false, 2140 + "notNull": false, 2141 + "autoincrement": false, 2142 + "default": 1 2143 + }, 2144 + "workspace_id": { 2145 + "name": "workspace_id", 2146 + "type": "integer", 2147 + "primaryKey": false, 2148 + "notNull": false, 2149 + "autoincrement": false 2150 + }, 2151 + "created_at": { 2152 + "name": "created_at", 2153 + "type": "integer", 2154 + "primaryKey": false, 2155 + "notNull": false, 2156 + "autoincrement": false, 2157 + "default": "(strftime('%s', 'now'))" 2158 + } 2159 + }, 2160 + "indexes": {}, 2161 + "foreignKeys": { 2162 + "check_workspace_id_workspace_id_fk": { 2163 + "name": "check_workspace_id_workspace_id_fk", 2164 + "tableFrom": "check", 2165 + "tableTo": "workspace", 2166 + "columnsFrom": [ 2167 + "workspace_id" 2168 + ], 2169 + "columnsTo": [ 2170 + "id" 2171 + ], 2172 + "onDelete": "no action", 2173 + "onUpdate": "no action" 2174 + } 2175 + }, 2176 + "compositePrimaryKeys": {}, 2177 + "uniqueConstraints": {} 2178 + } 2179 + }, 2180 + "enums": {}, 2181 + "_meta": { 2182 + "schemas": {}, 2183 + "tables": {}, 2184 + "columns": {} 2185 + } 2186 + }
+7
packages/db/drizzle/meta/_journal.json
··· 239 239 "when": 1719740057514, 240 240 "tag": "0033_solid_colossus", 241 241 "breakpoints": true 242 + }, 243 + { 244 + "idx": 34, 245 + "version": "6", 246 + "when": 1720727898360, 247 + "tag": "0034_serious_shard", 248 + "breakpoints": true 242 249 } 243 250 ] 244 251 }
+3 -5
packages/db/src/schema/pages/page.ts
··· 3 3 4 4 import { maintenance } from "../maintenances"; 5 5 import { monitorsToPages } from "../monitors"; 6 - import { pagesToStatusReports } from "../status_reports"; 7 6 import { workspace } from "../workspaces"; 8 7 9 8 export const page = sqliteTable("page", { ··· 23 22 // Password protecting the status page - no specific restriction on password 24 23 password: text("password", { length: 256 }), 25 24 passwordProtected: integer("password_protected", { mode: "boolean" }).default( 26 - false, 25 + false 27 26 ), 28 27 29 28 createdAt: integer("created_at", { mode: "timestamp" }).default( 30 - sql`(strftime('%s', 'now'))`, 29 + sql`(strftime('%s', 'now'))` 31 30 ), 32 31 updatedAt: integer("updated_at", { mode: "timestamp" }).default( 33 - sql`(strftime('%s', 'now'))`, 32 + sql`(strftime('%s', 'now'))` 34 33 ), 35 34 }); 36 35 37 36 export const pageRelations = relations(page, ({ many, one }) => ({ 38 37 monitorsToPages: many(monitorsToPages), 39 - pagesToStatusReports: many(pagesToStatusReports), 40 38 maintenancesToPages: many(maintenance), 41 39 workspace: one(workspace, { 42 40 fields: [page.workspaceId],
+46 -40
packages/db/src/schema/status_reports/status_reports.ts
··· 24 24 25 25 workspaceId: integer("workspace_id").references(() => workspace.id), 26 26 27 + pageId: integer("page_id").references(() => page.id), 28 + 27 29 createdAt: integer("created_at", { mode: "timestamp" }).default( 28 - sql`(strftime('%s', 'now'))`, 30 + sql`(strftime('%s', 'now'))` 29 31 ), 30 32 updatedAt: integer("updated_at", { mode: "timestamp" }).default( 31 - sql`(strftime('%s', 'now'))`, 33 + sql`(strftime('%s', 'now'))` 32 34 ), 33 35 }); 34 36 ··· 43 45 .references(() => statusReport.id, { onDelete: "cascade" }) 44 46 .notNull(), 45 47 createdAt: integer("created_at", { mode: "timestamp" }).default( 46 - sql`(strftime('%s', 'now'))`, 48 + sql`(strftime('%s', 'now'))` 47 49 ), 48 50 updatedAt: integer("updated_at", { mode: "timestamp" }).default( 49 - sql`(strftime('%s', 'now'))`, 51 + sql`(strftime('%s', 'now'))` 50 52 ), 51 53 }); 52 54 ··· 54 56 statusReport, 55 57 ({ one, many }) => ({ 56 58 monitorsToStatusReports: many(monitorsToStatusReport), 57 - pagesToStatusReports: many(pagesToStatusReports), 59 + page: one(page, { 60 + fields: [statusReport.pageId], 61 + references: [page.id], 62 + }), 58 63 statusReportUpdates: many(statusReportUpdate), 59 64 workspace: one(workspace, { 60 65 fields: [statusReport.workspaceId], 61 66 references: [workspace.id], 62 67 }), 63 - }), 68 + }) 64 69 ); 65 70 66 71 export const statusReportUpdateRelations = relations( ··· 70 75 fields: [statusReportUpdate.statusReportId], 71 76 references: [statusReport.id], 72 77 }), 73 - }), 78 + }) 74 79 ); 75 80 76 81 export const monitorsToStatusReport = sqliteTable( ··· 83 88 .notNull() 84 89 .references(() => statusReport.id, { onDelete: "cascade" }), 85 90 createdAt: integer("created_at", { mode: "timestamp" }).default( 86 - sql`(strftime('%s', 'now'))`, 91 + sql`(strftime('%s', 'now'))` 87 92 ), 88 93 }, 89 94 (t) => ({ 90 95 pk: primaryKey(t.monitorId, t.statusReportId), 91 - }), 96 + }) 92 97 ); 93 98 94 99 export const monitorsToStatusReportRelations = relations( ··· 102 107 fields: [monitorsToStatusReport.statusReportId], 103 108 references: [statusReport.id], 104 109 }), 105 - }), 110 + }) 106 111 ); 107 112 108 - export const pagesToStatusReports = sqliteTable( 109 - "status_reports_to_pages", 110 - { 111 - pageId: integer("page_id") 112 - .notNull() 113 - .references(() => page.id, { onDelete: "cascade" }), 114 - statusReportId: integer("status_report_id") 115 - .notNull() 116 - .references(() => statusReport.id, { onDelete: "cascade" }), 117 - createdAt: integer("created_at", { mode: "timestamp" }).default( 118 - sql`(strftime('%s', 'now'))`, 119 - ), 120 - }, 121 - (t) => ({ 122 - pk: primaryKey(t.pageId, t.statusReportId), 123 - }), 124 - ); 113 + // FIXME: We might have to drop foreign key constraints for the following tables 114 + // export const pagesToStatusReports = sqliteTable( 115 + // "status_reports_to_pages", 116 + // { 117 + // pageId: integer("page_id") 118 + // .notNull() 119 + // .references(() => page.id, { onDelete: "cascade" }), 120 + // statusReportId: integer("status_report_id") 121 + // .notNull() 122 + // .references(() => statusReport.id, { onDelete: "cascade" }), 123 + // createdAt: integer("created_at", { mode: "timestamp" }).default( 124 + // sql`(strftime('%s', 'now'))` 125 + // ), 126 + // }, 127 + // (t) => ({ 128 + // pk: primaryKey(t.pageId, t.statusReportId), 129 + // }) 130 + // ); 125 131 126 - export const pagesToStatusReportsRelations = relations( 127 - pagesToStatusReports, 128 - ({ one }) => ({ 129 - page: one(page, { 130 - fields: [pagesToStatusReports.pageId], 131 - references: [page.id], 132 - }), 133 - statusReport: one(statusReport, { 134 - fields: [pagesToStatusReports.statusReportId], 135 - references: [statusReport.id], 136 - }), 137 - }), 138 - ); 132 + // export const pagesToStatusReportsRelations = relations( 133 + // pagesToStatusReports, 134 + // ({ one }) => ({ 135 + // page: one(page, { 136 + // fields: [pagesToStatusReports.pageId], 137 + // references: [page.id], 138 + // }), 139 + // statusReport: one(statusReport, { 140 + // fields: [pagesToStatusReports.statusReportId], 141 + // references: [statusReport.id], 142 + // }), 143 + // }) 144 + // );
+2 -3
packages/db/src/schema/status_reports/validation.ts
··· 13 13 statusReportUpdate, 14 14 { 15 15 status: statusReportStatusSchema, 16 - }, 16 + } 17 17 ); 18 18 19 19 export const insertStatusReportSchema = createInsertSchema(statusReport, { ··· 25 25 * relationship to monitors and pages 26 26 */ 27 27 monitors: z.number().array().optional().default([]), 28 - pages: z.number().array().optional().default([]), 29 28 }) 30 29 .extend({ 31 30 /** ··· 42 41 statusReportUpdate, 43 42 { 44 43 status: statusReportStatusSchema, 45 - }, 44 + } 46 45 ); 47 46 48 47 export type InsertStatusReport = z.infer<typeof insertStatusReportSchema>;
+2 -12
packages/db/src/seed.mts
··· 11 11 notification, 12 12 notificationsToMonitors, 13 13 page, 14 - pagesToStatusReports, 15 14 statusReport, 16 15 statusReportUpdate, 17 16 user, ··· 143 142 .values({ 144 143 id: 1, 145 144 workspaceId: 1, 145 + pageId:1, 146 146 title: "Test Status Report", 147 147 status: "investigating", 148 148 updatedAt: new Date(), ··· 165 165 .values({ 166 166 id: 2, 167 167 workspaceId: 1, 168 + pageId:1, 168 169 title: "Test Status Report", 169 170 status: "investigating", 170 171 updatedAt: new Date(), ··· 190 191 { 191 192 monitorId: 2, 192 193 statusReportId: 2, 193 - }, 194 - ]); 195 - 196 - await db.insert(pagesToStatusReports).values([ 197 - { 198 - pageId: 1, 199 - statusReportId: 2, 200 - }, 201 - { 202 - pageId: 1, 203 - statusReportId: 1, 204 194 }, 205 195 ]); 206 196
+2 -2
utils/api-bruno/checker.bru
··· 24 24 "url":"https://openstat.us/404", 25 25 "status": "active", 26 26 "cronTimestamp":1699088595307 27 - "pageIds":["1"] 27 + "pageId":1 28 28 } 29 29 } 30 30 ··· 36 36 "url":"https://openstat.us/404", 37 37 "status": "active", 38 38 "cronTimestamp":1699088595307, 39 - "pageIds":["1"] 39 + "pageId":1 40 40 } 41 41 }