diff --git a/apps/dashboard/src/components/forms/components/form-components.tsx b/apps/dashboard/src/components/forms/components/form-components.tsx index 6644b1f8..b928c2d7 100644 --- a/apps/dashboard/src/components/forms/components/form-components.tsx +++ b/apps/dashboard/src/components/forms/components/form-components.tsx @@ -92,6 +92,15 @@ import { } from "@/components/ui/sortable"; import { cn } from "@/lib/utils"; +import { type ThirdPartyEntry, ThirdPartyPicker } from "./third-party-picker"; + +function externalKey(c: { + externalServiceId?: number | null; + externalServiceComponentId?: number | null; +}): string { + return `${c.externalServiceId}:${c.externalServiceComponentId ?? "all"}`; +} + type PageComponent = RouterOutputs["pageComponent"]["list"][number]; type Monitor = RouterOutputs["monitor"]["list"][number]; type Workspace = RouterOutputs["workspace"]["get"]; @@ -105,10 +114,12 @@ type ComponentGroup = { const componentSchema = z.object({ id: z.number(), monitorId: z.number().nullish(), + externalServiceId: z.number().nullish(), + externalServiceComponentId: z.number().nullish(), order: z.number(), name: z.string().min(1, { message: "Name is required" }), description: z.string().optional(), - type: z.enum(["monitor", "static"]), + type: z.enum(["monitor", "static", "external"]), }); const schema = z.object({ @@ -126,15 +137,19 @@ const schema = z.object({ ), }); +type ComponentFormData = { + id: number; + order: number; + name?: string; + type?: "monitor" | "static" | "external"; + monitorId?: number | null; + externalServiceId?: number | null; + externalServiceComponentId?: number | null; +}; + const getSortedComponents = ( components: PageComponent[], - componentData: { - id: number; - order: number; - name?: string; - type?: "monitor" | "static"; - monitorId?: number | null; - }[], + componentData: ComponentFormData[], monitors: Monitor[], ) => { const orderMap = new Map(componentData?.map((c) => [c.id, c.order]) ?? []); @@ -157,6 +172,8 @@ const getSortedComponents = ( name: c.name ?? "", type: c.type ?? "static", monitorId: c.monitorId ?? null, + externalServiceId: c.externalServiceId ?? null, + externalServiceComponentId: c.externalServiceComponentId ?? null, monitor: monitor ?? null, groupId: null, groupOrder: null, @@ -176,25 +193,13 @@ const getSortedComponents = ( const getSortedItems = ( components: PageComponent[], - componentData: { - id: number; - order: number; - name?: string; - type?: "monitor" | "static"; - monitorId?: number | null; - }[], + componentData: ComponentFormData[], groups: Array<{ id: number; order: number; name: string; defaultOpen: boolean; - components: Array<{ - id: number; - order: number; - name?: string; - type?: "monitor" | "static"; - monitorId?: number | null; - }>; + components: ComponentFormData[]; }>, monitors: Monitor[], ): (PageComponent | ComponentGroup)[] => { @@ -219,6 +224,8 @@ const getSortedItems = ( name: c.name ?? "", type: c.type ?? "static", monitorId: c.monitorId ?? null, + externalServiceId: c.externalServiceId ?? null, + externalServiceComponentId: c.externalServiceComponentId ?? null, monitor: monitor ?? null, groupId: null, groupOrder: null, @@ -290,6 +297,34 @@ export function FormComponents({ const watchComponents = form.watch("components"); const watchGroups = form.watch("groups"); const [openUpgradeDialog, setOpenUpgradeDialog] = useState(false); + const [thirdPartyOpen, setThirdPartyOpen] = useState(false); + + const existingExternalKeys = new Set( + [ + ...(watchComponents ?? []), + ...(watchGroups ?? []).flatMap((g) => g.components), + ] + .filter((c) => c.type === "external") + .map(externalKey), + ); + + const handleAddThirdParty = useCallback( + (entries: ThirdPartyEntry[]) => { + const current = form.getValues("components") ?? []; + const additions = entries.map((entry, i) => ({ + id: Date.now() + i, + monitorId: null, + externalServiceId: entry.externalServiceId, + externalServiceComponentId: entry.externalServiceComponentId, + order: current.length + i, + name: entry.name, + description: "", + type: "external" as const, + })); + form.setValue("components", [...current, ...additions]); + }, + [form], + ); const [data, setData] = useState<(PageComponent | ComponentGroup)[]>( getSortedItems( allPageComponents, @@ -348,6 +383,8 @@ export function FormComponents({ return { id: item.id, monitorId: item.monitorId, + externalServiceId: item.externalServiceId, + externalServiceComponentId: item.externalServiceComponentId, order: index, name: existingComponent?.name ?? item.name, description: existingComponent?.description ?? "", @@ -619,7 +656,13 @@ export function FormComponents({ - + { + e.preventDefault(); + if (!validateLimit()) return; + setThirdPartyOpen(true); + }} + > Add Third-Party Component @@ -708,6 +751,12 @@ export function FormComponents({ open={openUpgradeDialog} onOpenChange={setOpenUpgradeDialog} /> + ); } @@ -803,7 +852,12 @@ function ComponentRow({ )}
- {component.monitor && component.type === "monitor" ? ( + {component.type === "external" ? ( + + {" "} + Third-party + + ) : component.monitor && component.type === "monitor" ? ( e.stopPropagation()} @@ -921,6 +975,34 @@ function ComponentGroupRow({ const watchComponents = form.watch("components"); const watchGroups = form.watch("groups"); const [data, setData] = useState(group.components); + const [thirdPartyOpen, setThirdPartyOpen] = useState(false); + + const existingExternalKeys = new Set( + [...watchComponents, ...watchGroups.flatMap((g) => g.components)] + .filter((c) => c.type === "external") + .map(externalKey), + ); + + const handleAddThirdParty = useCallback( + (entries: ThirdPartyEntry[]) => { + const current = form.getValues(`groups.${groupIndex}.components`) ?? []; + const additions = entries.map((entry, i) => ({ + id: Date.now() + i, + monitorId: null, + externalServiceId: entry.externalServiceId, + externalServiceComponentId: entry.externalServiceComponentId, + order: current.length + i, + name: entry.name, + description: "", + type: "external" as const, + })); + form.setValue(`groups.${groupIndex}.components`, [ + ...current, + ...additions, + ]); + }, + [form, groupIndex], + ); // Calculate taken monitor IDs (in main list or other groups) const takenMonitorIds = new Set([ @@ -948,6 +1030,8 @@ function ComponentGroupRow({ return { id: c.id, monitorId: c.monitorId, + externalServiceId: c.externalServiceId, + externalServiceComponentId: c.externalServiceComponentId, order: index, name: existingComponent?.name ?? c.name, description: existingComponent?.description ?? "", @@ -1137,7 +1221,13 @@ function ComponentGroupRow({ - + { + e.preventDefault(); + if (!validateLimit()) return; + setThirdPartyOpen(true); + }} + > Add Third-Party Component @@ -1258,6 +1348,12 @@ function ComponentGroupRow({ )}
+ ); } diff --git a/apps/dashboard/src/components/forms/components/third-party-picker.tsx b/apps/dashboard/src/components/forms/components/third-party-picker.tsx new file mode 100644 index 00000000..f60bdd32 --- /dev/null +++ b/apps/dashboard/src/components/forms/components/third-party-picker.tsx @@ -0,0 +1,262 @@ +"use client"; + +import { Badge } from "@openstatus/ui/components/ui/badge"; +import { Button } from "@openstatus/ui/components/ui/button"; +import { Checkbox } from "@openstatus/ui/components/ui/checkbox"; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from "@openstatus/ui/components/ui/command"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@openstatus/ui/components/ui/dialog"; +import { ScrollArea } from "@openstatus/ui/components/ui/scroll-area"; +import { cn } from "@openstatus/ui/lib/utils"; +import { skipToken, useQuery } from "@tanstack/react-query"; +import { ChevronLeft } from "lucide-react"; +import { useState } from "react"; + +import { useTRPC } from "@/lib/trpc/client"; + +export type ThirdPartyEntry = { + externalServiceId: number; + externalServiceComponentId: number | null; + name: string; +}; + +type SelectedService = { id: number; slug: string; name: string }; + +export function ThirdPartyPicker({ + open, + onOpenChange, + onAdd, + existingKeys, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + onAdd: (entries: ThirdPartyEntry[]) => void; + existingKeys: Set; +}) { + const trpc = useTRPC(); + const [service, setService] = useState(null); + const [checked, setChecked] = useState>(new Set()); + + const { data: services } = useQuery({ + ...trpc.externalService.grid.queryOptions(), + enabled: open, + }); + + const { data: componentsResult, isLoading: componentsLoading } = useQuery( + trpc.externalService.components.queryOptions( + open && service ? { slug: service.slug } : skipToken, + ), + ); + + function reset() { + setService(null); + setChecked(new Set()); + } + + function close() { + reset(); + onOpenChange(false); + } + + function toggle(key: number | "all") { + setChecked((prev) => { + if (prev.has(key)) { + const next = new Set(prev); + next.delete(key); + return next; + } + // whole-service and specific components are mutually exclusive + if (key === "all") return new Set(["all"]); + const next = new Set(prev); + next.delete("all"); + next.add(key); + return next; + }); + } + + function commit() { + if (!service) return; + const entries: ThirdPartyEntry[] = []; + const components = componentsResult?.components ?? []; + for (const key of checked) { + if (key === "all") { + entries.push({ + externalServiceId: service.id, + externalServiceComponentId: null, + name: service.name, + }); + } else { + const component = components.find((c) => c.id === key); + if (component) { + entries.push({ + externalServiceId: service.id, + externalServiceComponentId: component.id, + name: component.name, + }); + } + } + } + if (entries.length > 0) onAdd(entries); + close(); + } + + return ( + { + if (!next) reset(); + onOpenChange(next); + }} + > + + + + {service ? service.name : "Add third-party service"} + + + {service + ? "Show the whole service or pick the components you depend on." + : "Surface an upstream provider's status on your page."} + + + + {!service ? ( + + + + No providers found. + + {services?.map((s) => ( + { + setService({ id: s.id, slug: s.slug, name: s.name }); + setChecked(new Set()); + }} + > + {s.name} + + ))} + + + + ) : ( + +
+ toggle("all")} + /> + {componentsLoading ? ( +

Loading…

+ ) : null} + {(componentsResult?.components ?? []).map((c) => ( + toggle(c.id)} + /> + ))} + {!componentsLoading && + componentsResult && + !componentsResult.supported ? ( +

+ This provider only exposes an overall status. +

+ ) : null} +
+
+ )} + + + {service ? ( + + ) : ( + + )} +
+ + +
+
+
+
+ ); +} + +function ThirdPartyOption({ + label, + description, + checked, + disabled, + onToggle, +}: { + label: string; + description?: string; + checked: boolean; + disabled?: boolean; + onToggle: () => void; +}) { + return ( +
!disabled && onToggle()} + onKeyDown={(e) => { + if (disabled) return; + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onToggle(); + } + }} + className={cn( + "flex w-full cursor-pointer items-center gap-3 rounded-md border px-3 py-2 text-left", + disabled ? "opacity-50" : "hover:bg-muted", + )} + > + + + {label} + {description ? ( + {description} + ) : null} + + {disabled ? Added : null} +
+ ); +} diff --git a/apps/dashboard/src/components/forms/components/update.tsx b/apps/dashboard/src/components/forms/components/update.tsx index b5dc196f..60f26a0f 100644 --- a/apps/dashboard/src/components/forms/components/update.tsx +++ b/apps/dashboard/src/components/forms/components/update.tsx @@ -83,6 +83,8 @@ export function FormComponentsUpdate() { components: componentsInGroup.map((c) => ({ id: c.id, monitorId: c.monitorId, + externalServiceId: c.externalServiceId, + externalServiceComponentId: c.externalServiceComponentId, order: c.groupOrder ?? 0, name: c.name, description: c.description ?? "", @@ -96,6 +98,8 @@ export function FormComponentsUpdate() { components: standaloneComponents.map((c) => ({ id: c.id, monitorId: c.monitorId, + externalServiceId: c.externalServiceId, + externalServiceComponentId: c.externalServiceComponentId, order: c.order ?? 0, name: c.name, description: c.description ?? "", diff --git a/apps/server/src/routes/rpc/handlers/status-page/converters.ts b/apps/server/src/routes/rpc/handlers/status-page/converters.ts index 7d631486..432ac134 100644 --- a/apps/server/src/routes/rpc/handlers/status-page/converters.ts +++ b/apps/server/src/routes/rpc/handlers/status-page/converters.ts @@ -59,7 +59,7 @@ type DBPageComponent = { pageId: number; name: string; description: string | null; - type: "static" | "monitor"; + type: "static" | "monitor" | "external"; monitorId: number | null; order: number | null; groupId: number | null; @@ -211,7 +211,7 @@ export function dbConfigurationToProto( * Convert DB component type string to proto enum. */ export function dbComponentTypeToProto( - type: "static" | "monitor", + type: "static" | "monitor" | "external", ): PageComponentType { switch (type) { case "monitor": diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/client.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/client.tsx index 0dc50174..a0ddb182 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/client.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/client.tsx @@ -50,6 +50,7 @@ import { StatusEventTimelineReportUpdate, } from "../../../../../components/status-page/status-events"; import { StatusFeed } from "../../../../../components/status-page/status-feed"; +import { ThirdPartySection } from "../../../../../components/status-page/third-party-section"; import { useEmbed } from "../../../../../hooks/use-embed"; import { usePathnamePrefix } from "../../../../../hooks/use-pathname-prefix"; import { updatesWithImpactChanges } from "../../../../../lib/report-impacts"; @@ -117,6 +118,12 @@ export function Client() { ), ); + const { data: externalSection } = useQuery( + trpc.statusPage.getExternalSection.queryOptions( + componentsVisible && pageInitial ? { slug: domain } : skipToken, + ), + ); + // NOTE: we need to filter out the incidents as we don't want to show all of them in the banner - a single one is enough // REMINDER: we could move that to the server - but we might wanna have the info of all openEvents actually const events = useMemo(() => { @@ -136,6 +143,71 @@ export function Client() { // REMINDER: if we are using the custom configuration, we need to use the pageWithCustomConfiguration const page = pageWithCustomConfiguration ?? pageInitial; + const sectionItems: { order: number; node: React.ReactNode }[] = + page.trackers.map((tracker) => { + if (tracker.type === "component") { + const component = tracker.component; + const { data, uptime } = + uptimeData?.find((u) => u.pageComponentId === component.id) ?? {}; + return { + order: tracker.order, + node: ( + + ), + }; + } + return { + order: tracker.order, + node: ( + + {tracker.components.map((component) => { + const { data, uptime } = + uptimeData?.find((u) => u.pageComponentId === component.id) ?? + {}; + return ( + + ); + })} + + ), + }; + }); + + if ( + externalSection && + externalSection.providers.length > 0 && + externalSection.position != null + ) { + sectionItems.push({ + order: externalSection.position, + node: , + }); + } + sectionItems.sort((a, b) => a.order - b.order); + return (
@@ -274,58 +346,9 @@ export function Client() { /> )} {/* NOTE: check what gap feels right */} - {page.trackers.length > 0 ? ( + {sectionItems.length > 0 ? ( - {page.trackers.map((tracker) => { - if (tracker.type === "component") { - const component = tracker.component; - const { data, uptime } = - uptimeData?.find((u) => u.pageComponentId === component.id) ?? - {}; - - return ( - - ); - } - - return ( - - {tracker.components.map((component) => { - const { data, uptime } = - uptimeData?.find( - (u) => u.pageComponentId === component.id, - ) ?? {}; - - return ( - - ); - })} - - ); - })} + {sectionItems.map((item) => item.node)} ) : null} diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/page.tsx index 412d8594..ac13cad2 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/page.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/page.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import type { SearchParams } from "nuqs/server"; +import { HydrateClient, getQueryClient, trpc } from "@/lib/trpc/server"; + import { Client } from "./client"; import { embedSearchParamsCache } from "./search-params"; @@ -21,6 +23,19 @@ export async function generateMetadata({ // await embedSearchParamsCache.parse(searchParams); // `generateMetadata` already parses per-request, so the client tree doesn't // need it for SSR correctness (nuqs reads the URL directly on both sides). -export default function Page() { - return ; +export default async function Page({ + params, +}: { + params: Promise<{ domain: string }>; +}) { + const { domain } = await params; + const queryClient = getQueryClient(); + await queryClient.prefetchQuery( + trpc.statusPage.getExternalSection.queryOptions({ slug: domain }), + ); + return ( + + + + ); } diff --git a/apps/status-page/src/components/status-page/third-party-section.tsx b/apps/status-page/src/components/status-page/third-party-section.tsx new file mode 100644 index 00000000..2cfd1bbc --- /dev/null +++ b/apps/status-page/src/components/status-page/third-party-section.tsx @@ -0,0 +1,163 @@ +"use client"; + +import type { RouterOutputs } from "@openstatus/api"; +import { + statusColors, + systemStatusLabels, +} from "@openstatus/ui/components/blocks/status.utils"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@openstatus/ui/components/ui/collapsible"; +import { cn } from "@openstatus/ui/lib/utils"; +import { ChevronDown, ExternalLink } from "lucide-react"; + +import { Link } from "@/components/common/link"; +import { StatusBar } from "@/components/status-page/status-bar"; +import { StatusComponentGroup } from "@/components/status-page/status-component-group"; + +type Section = RouterOutputs["statusPage"]["getExternalSection"]; +type Provider = Section["providers"][number]; +type SectionComponent = Provider["components"][number]; +type SectionIncident = Section["incidents"][number]; + +function safeHttpUrl(url: string | null | undefined): string | null { + if (!url) return null; + return /^https?:\/\//i.test(url) ? url : null; +} + +export function ThirdPartySection({ section }: { section: Section }) { + if (section.providers.length === 0) return null; + + return ( +
+

+ Third-party dependencies +

+ {section.providers.map((provider) => + provider.components.length === 1 && + provider.components[0].isWholeService ? ( + + ) : ( + + {provider.components.map((component) => ( + + ))} + + ), + )} + +
+ ); +} + +function ExternalCard({ + component, + statusPageUrl, +}: { + component: SectionComponent; + statusPageUrl: string; +}) { + const status = component.stale ? "empty" : component.status; + const href = safeHttpUrl(statusPageUrl); + return ( +
+
+
+ {href ? ( + + {component.name} + + + ) : ( + + {component.name} + + )} + {component.description ? ( + + {component.description} + + ) : null} +
+ + + {systemStatusLabels[status].short} + +
+ +
+ ); +} + +function ExternalIncidents({ incidents }: { incidents: SectionIncident[] }) { + if (incidents.length === 0) return null; + + return ( + + + Third-party incidents ({incidents.length}) + + + +
    + {incidents.map((incident) => { + const detailsHref = safeHttpUrl(incident.shortlink); + return ( +
  • +
    + {incident.name} + + {incident.serviceName} + +
    +
    + + {incident.resolvedAt ? "Resolved" : incident.status} + + · + + {new Date( + incident.startedAt ?? incident.createdAt, + ).toLocaleDateString()} + + {detailsHref ? ( + + Details + + ) : null} +
    +
  • + ); + })} +
+
+
+ ); +} diff --git a/apps/web/src/content/pages/docs/reference/page-components.mdx b/apps/web/src/content/pages/docs/reference/page-components.mdx index b5acaba3..1bf8d21c 100644 --- a/apps/web/src/content/pages/docs/reference/page-components.mdx +++ b/apps/web/src/content/pages/docs/reference/page-components.mdx @@ -17,7 +17,7 @@ Page components are the individual elements displayed on your status page that s ## Component types -Page components come in two distinct types, each serving different purposes on your status page. +Page components come in three distinct types, each serving different purposes on your status page. ### Monitor components @@ -145,6 +145,23 @@ Static components do not perform any health checks or generate incidents. You ar - Scheduled maintenances. - Automatic incidents are **not** supported. +### External components + +**Type:** `external` + +External components surface the live status of a third-party provider from the openstatus provider catalog (e.g. a cloud provider, CDN, or SaaS API your service depends on). Add a whole provider as a single aggregated row, or pick the specific upstream components you rely on. + +**Characteristics:** +- Status is read from the provider's published status and refreshed automatically — you do not manage it manually. +- Rendered in a dedicated **Third-party dependencies** section, auto-grouped by provider. +- Excluded from your page's overall status and uptime, so an upstream outage never counts against your own SLA. +- Each provider links out to its own status page. +- Show an **Unknown / no data** state when the upstream data is stale or unavailable. + + + ## Component groups Component groups allow you to organize related page components into logical sections on your status page. Groups improve readability and help visitors understand your service architecture. diff --git a/packages/api/src/router/externalService.ts b/packages/api/src/router/externalService.ts index 70c39e99..6be32999 100644 --- a/packages/api/src/router/externalService.ts +++ b/packages/api/src/router/externalService.ts @@ -114,6 +114,7 @@ async function safeData( } const gridItemSchema = z.object({ + id: z.number(), slug: z.string(), name: z.string(), url: z.string(), @@ -300,6 +301,7 @@ export const externalServiceRouter = createTRPCRouter({ threshold: REPORT_THRESHOLD, }); return { + id: s.id, slug: s.slug, name: s.name, url: s.url, diff --git a/packages/api/src/router/statusPage.ts b/packages/api/src/router/statusPage.ts index 74e9963d..b9f5fb93 100644 --- a/packages/api/src/router/statusPage.ts +++ b/packages/api/src/router/statusPage.ts @@ -1,6 +1,9 @@ import { Events } from "@openstatus/analytics"; -import { and, eq, inArray, sql } from "@openstatus/db"; +import { and, asc, eq, inArray, sql } from "@openstatus/db"; import { + type ExternalStatusType, + externalIndicatorSeverity, + externalIndicatorToStatus, maintenance, page, pageComponent, @@ -15,6 +18,10 @@ import { selectWorkspaceSchema, statusReport, } from "@openstatus/db/src/schema"; +import { + type ExternalDailyRow, + getPageExternalSection, +} from "@openstatus/services/external-service"; import { getSubscriberByToken, hasPendingSubscriber, @@ -86,6 +93,52 @@ const gateFieldsSchema = selectPageSchema.pick({ contactUrl: true, }); +const EXTERNAL_LOOKBACK_DAYS = 45; + +type ExternalComponentBars = { + day: string; + bar: { status: ExternalStatusType; height: number }[]; + card: { status: ExternalStatusType; value: string }[]; + events: never[]; +}; + +function fillExternalBars( + daily: ExternalDailyRow[], + days = EXTERNAL_LOOKBACK_DAYS, +): ExternalComponentBars[] { + const byDay = new Map(); + for (const row of daily) { + const key = row.day.slice(0, 10); + const prev = byDay.get(key); + if ( + !prev || + externalIndicatorSeverity(row.worstIndicator) > + externalIndicatorSeverity(prev.worstIndicator) + ) { + byDay.set(key, row); + } + } + + const out: ExternalComponentBars[] = []; + const now = new Date(); + for (let i = days - 1; i >= 0; i--) { + const date = new Date(now); + date.setUTCDate(date.getUTCDate() - i); + date.setUTCHours(0, 0, 0, 0); + const row = byDay.get(date.toISOString().slice(0, 10)); + const status: ExternalStatusType = row + ? externalIndicatorToStatus(row.worstIndicator, row.hadMaintenance > 0) + : "empty"; + out.push({ + day: date.toISOString(), + bar: [{ status, height: 100 }], + card: [{ status, value: "" }], + events: [], + }); + } + return out; +} + export const statusPageRouter = createTRPCRouter({ get: publicProcedure .input( @@ -143,7 +196,8 @@ export const statusPageRouter = createTRPCRouter({ const ws = selectWorkspaceSchema.safeParse(_page.workspace); const pageComponents = selectPageComponentWithMonitorRelation .array() - .parse(_page.pageComponents); + .parse(_page.pageComponents) + .filter((c) => c.type !== "external"); const configuration = pageConfigurationSchema.safeParse( _page.configuration ?? {}, @@ -446,6 +500,87 @@ export const statusPageRouter = createTRPCRouter({ }); }), + getExternalSection: publicProcedure + .input(z.object({ slug: z.string().toLowerCase() })) + .query(async (opts) => { + if (!opts.input.slug) { + return { position: null, providers: [], incidents: [] }; + } + + const _page = await opts.ctx.db + .select({ id: page.id }) + .from(page) + .where( + sql`lower(${page.slug}) = ${opts.input.slug} OR lower(${page.customDomain}) = ${opts.input.slug}`, + ) + .get(); + + if (!_page) { + return { position: null, providers: [], incidents: [] }; + } + + const rows = await opts.ctx.db + .select({ + id: pageComponent.id, + name: pageComponent.name, + description: pageComponent.description, + order: pageComponent.order, + externalServiceId: pageComponent.externalServiceId, + externalServiceComponentId: pageComponent.externalServiceComponentId, + }) + .from(pageComponent) + .where( + and( + eq(pageComponent.pageId, _page.id), + eq(pageComponent.type, "external"), + ), + ) + .orderBy(asc(pageComponent.order), asc(pageComponent.id)) + .all(); + + const components = rows + .filter( + (r): r is typeof r & { externalServiceId: number } => + r.externalServiceId != null, + ) + .map((r) => ({ + pageComponentId: r.id, + name: r.name, + description: r.description, + order: r.order ?? 0, + externalServiceId: r.externalServiceId, + externalServiceComponentId: r.externalServiceComponentId, + })); + + const section = await getPageExternalSection({ + ctx: { db: opts.ctx.db }, + components, + days: EXTERNAL_LOOKBACK_DAYS, + }); + + return { + position: section.position, + providers: section.providers.map((provider) => ({ + externalServiceId: provider.externalServiceId, + name: provider.name, + slug: provider.slug, + statusPageUrl: provider.statusPageUrl, + status: provider.status, + order: provider.order, + components: provider.components.map((c) => ({ + pageComponentId: c.pageComponentId, + name: c.name, + description: c.description, + status: c.status, + stale: c.stale, + isWholeService: c.isWholeService, + data: fillExternalBars(c.daily), + })), + })), + incidents: section.incidents, + }; + }), + getLight: publicProcedure .input(z.object({ slug: z.string().toLowerCase() })) .query(async (opts) => { @@ -524,7 +659,9 @@ export const statusPageRouter = createTRPCRouter({ incidents, statusReports: _page.statusReports, maintenances: _page.maintenances, - pageComponents: _page.pageComponents, + pageComponents: _page.pageComponents.filter( + (c) => c.type !== "external", + ), pageComponentGroups: _page.pageComponentGroups, workspacePlan: _page.workspace.plan, whiteLabel, @@ -883,6 +1020,8 @@ export const statusPageRouter = createTRPCRouter({ name: "API Monitor", type: "monitor" as const, monitorId: 1, + externalServiceId: null, + externalServiceComponentId: null, order: 1, groupId: null, groupOrder: null, diff --git a/packages/api/src/router/statusPage.utils.test.ts b/packages/api/src/router/statusPage.utils.test.ts index a9ae1c57..4be0338e 100644 --- a/packages/api/src/router/statusPage.utils.test.ts +++ b/packages/api/src/router/statusPage.utils.test.ts @@ -1050,6 +1050,8 @@ describe("getEvents - pageComponent filtering", () => { pageId: 1, type: monitorId ? ("monitor" as const) : ("static" as const), monitorId: monitorId ?? null, + externalServiceId: null, + externalServiceComponentId: null, name: `Component ${id}`, description: null, order: 0, @@ -1350,6 +1352,8 @@ describe("componentImpacts", () => { pageId: 1, type: monitorId ? ("monitor" as const) : ("static" as const), monitorId: monitorId ?? null, + externalServiceId: null, + externalServiceComponentId: null, name: `Component ${id}`, description: null, order: 0, diff --git a/packages/db/drizzle/0081_military_polaris.sql b/packages/db/drizzle/0081_military_polaris.sql new file mode 100644 index 00000000..6ca35427 --- /dev/null +++ b/packages/db/drizzle/0081_military_polaris.sql @@ -0,0 +1,31 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_page_component` ( + `id` integer PRIMARY KEY NOT NULL, + `workspace_id` integer NOT NULL, + `page_id` integer NOT NULL, + `type` text DEFAULT 'monitor' NOT NULL, + `monitor_id` integer, + `external_service_id` integer, + `external_service_component_id` integer, + `name` text NOT NULL, + `description` text, + `order` integer DEFAULT 0, + `group_id` integer, + `group_order` integer DEFAULT 0, + `created_at` integer DEFAULT (strftime('%s', 'now')), + `updated_at` integer DEFAULT (strftime('%s', 'now')), + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`external_service_id`) REFERENCES `external_service`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`external_service_component_id`) REFERENCES `external_service_component`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`group_id`) REFERENCES `page_component_groups`(`id`) ON UPDATE no action ON DELETE set null, + CONSTRAINT "page_component_type_check" CHECK(("__new_page_component"."type" = 'monitor' AND "__new_page_component"."monitor_id" IS NOT NULL AND "__new_page_component"."external_service_id" IS NULL AND "__new_page_component"."external_service_component_id" IS NULL) OR ("__new_page_component"."type" = 'static' AND "__new_page_component"."monitor_id" IS NULL AND "__new_page_component"."external_service_id" IS NULL AND "__new_page_component"."external_service_component_id" IS NULL) OR ("__new_page_component"."type" = 'external' AND "__new_page_component"."monitor_id" IS NULL AND "__new_page_component"."external_service_id" IS NOT NULL)) +); +--> statement-breakpoint +INSERT INTO `__new_page_component`("id", "workspace_id", "page_id", "type", "monitor_id", "name", "description", "order", "group_id", "group_order", "created_at", "updated_at") SELECT "id", "workspace_id", "page_id", "type", "monitor_id", "name", "description", "order", "group_id", "group_order", "created_at", "updated_at" FROM `page_component`;--> statement-breakpoint +DROP TABLE `page_component`;--> statement-breakpoint +ALTER TABLE `__new_page_component` RENAME TO `page_component`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE INDEX `page_component_workspace_id_idx` ON `page_component` (`workspace_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `page_component_page_id_monitor_id_unique` ON `page_component` (`page_id`,`monitor_id`); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0081_snapshot.json b/packages/db/drizzle/meta/0081_snapshot.json new file mode 100644 index 00000000..79297f51 --- /dev/null +++ b/packages/db/drizzle/meta/0081_snapshot.json @@ -0,0 +1,4857 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8ee8c9c8-68ce-4945-932f-41d2d6ff203d", + "prevId": "dd6a23ac-b31e-45b7-ad9a-d4a3929c3382", + "tables": { + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stripe_id": { + "name": "stripe_id", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ends_at": { + "name": "ends_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paid_until": { + "name": "paid_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "limits": { + "name": "limits", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "dsn": { + "name": "dsn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "workspace_stripe_id_unique": { + "name": "workspace_stripe_id_unique", + "columns": [ + "stripe_id" + ], + "isUnique": true + }, + "workspace_id_dsn_unique": { + "name": "workspace_id_dsn_unique", + "columns": [ + "id", + "dsn" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "columns": [ + "provider", + "provider_account_id" + ], + "name": "account_provider_provider_account_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_tenant_id_unique": { + "name": "user_tenant_id_unique", + "columns": [ + "tenant_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_to_workspaces": { + "name": "users_to_workspaces", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "users_to_workspaces_workspace_id_idx": { + "name": "users_to_workspaces_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "users_to_workspaces_user_id_user_id_fk": { + "name": "users_to_workspaces_user_id_user_id_fk", + "tableFrom": "users_to_workspaces", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_workspaces_workspace_id_workspace_id_fk": { + "name": "users_to_workspaces_workspace_id_workspace_id_fk", + "tableFrom": "users_to_workspaces", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_workspaces_user_id_workspace_id_pk": { + "columns": [ + "user_id", + "workspace_id" + ], + "name": "users_to_workspaces_user_id_workspace_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification_token": { + "name": "verification_token", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_token_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verification_token_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_report": { + "name": "status_report", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "status_report_workspace_created_idx": { + "name": "status_report_workspace_created_idx", + "columns": [ + "workspace_id", + "created_at" + ], + "isUnique": false + }, + "status_report_page_id_idx": { + "name": "status_report_page_id_idx", + "columns": [ + "page_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "status_report_workspace_id_workspace_id_fk": { + "name": "status_report_workspace_id_workspace_id_fk", + "tableFrom": "status_report", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "status_report_page_id_page_id_fk": { + "name": "status_report_page_id_page_id_fk", + "tableFrom": "status_report", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_report_update": { + "name": "status_report_update", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_report_id": { + "name": "status_report_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "status_report_update_status_report_id_idx": { + "name": "status_report_update_status_report_id_idx", + "columns": [ + "status_report_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "status_report_update_status_report_id_status_report_id_fk": { + "name": "status_report_update_status_report_id_status_report_id_fk", + "tableFrom": "status_report_update", + "tableTo": "status_report", + "columnsFrom": [ + "status_report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential": { + "name": "credential", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_workspace_id_idx": { + "name": "integration_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "integration_workspace_id_workspace_id_fk": { + "name": "integration_workspace_id_workspace_id_fk", + "tableFrom": "integration", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "page": { + "name": "page", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "slug": { + "name": "slug", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published": { + "name": "published", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "force_theme": { + "name": "force_theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'system'" + }, + "custom_theme": { + "name": "custom_theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_protected": { + "name": "password_protected", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "access_type": { + "name": "access_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'public'" + }, + "auth_email_domains": { + "name": "auth_email_domains", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_ip_ranges": { + "name": "allowed_ip_ranges", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "homepage_url": { + "name": "homepage_url", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contact_url": { + "name": "contact_url", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_locale": { + "name": "default_locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "locales": { + "name": "locales", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "legacy_page": { + "name": "legacy_page", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allow_index": { + "name": "allow_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_monitor_values": { + "name": "show_monitor_values", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "page_slug_unique": { + "name": "page_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "page_lower_slug_idx": { + "name": "page_lower_slug_idx", + "columns": [ + "LOWER(\"slug\")" + ], + "isUnique": false + }, + "page_lower_custom_domain_idx": { + "name": "page_lower_custom_domain_idx", + "columns": [ + "LOWER(\"custom_domain\")" + ], + "isUnique": false + }, + "page_workspace_id_idx": { + "name": "page_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "page_workspace_id_workspace_id_fk": { + "name": "page_workspace_id_workspace_id_fk", + "tableFrom": "page", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor": { + "name": "monitor", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "job_type": { + "name": "job_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'http'" + }, + "periodicity": { + "name": "periodicity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'other'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "regions": { + "name": "regions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "url": { + "name": "url", + "type": "text(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "external_name": { + "name": "external_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'GET'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 45000 + }, + "degraded_after": { + "name": "degraded_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assertions": { + "name": "assertions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "otel_endpoint": { + "name": "otel_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "otel_headers": { + "name": "otel_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public": { + "name": "public", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "retry": { + "name": "retry", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 3 + }, + "follow_redirects": { + "name": "follow_redirects", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "monitor_workspace_id_active_idx": { + "name": "monitor_workspace_id_active_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false, + "where": "\"monitor\".\"deleted_at\" IS NULL" + } + }, + "foreignKeys": { + "monitor_workspace_id_workspace_id_fk": { + "name": "monitor_workspace_id_workspace_id_fk", + "tableFrom": "monitor", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "page_subscriber": { + "name": "page_subscriber", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_type": { + "name": "channel_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'email'" + }, + "webhook_url": { + "name": "webhook_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel_config": { + "name": "channel_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'self_signup'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unsubscribed_at": { + "name": "unsubscribed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "idx_page_subscriber_email_page_active": { + "name": "idx_page_subscriber_email_page_active", + "columns": [ + "LOWER(\"email\")", + "page_id" + ], + "isUnique": true, + "where": "\"page_subscriber\".\"unsubscribed_at\" IS NULL AND \"page_subscriber\".\"channel_type\" = 'email'" + }, + "idx_page_subscriber_webhook_page_active": { + "name": "idx_page_subscriber_webhook_page_active", + "columns": [ + "LOWER(\"webhook_url\")", + "page_id" + ], + "isUnique": true, + "where": "\"page_subscriber\".\"unsubscribed_at\" IS NULL AND \"page_subscriber\".\"channel_type\" = 'webhook'" + }, + "idx_page_subscriber_slack_channel_page_active": { + "name": "idx_page_subscriber_slack_channel_page_active", + "columns": [ + "slack_channel_id", + "page_id" + ], + "isUnique": true, + "where": "\"page_subscriber\".\"unsubscribed_at\" IS NULL AND \"page_subscriber\".\"channel_type\" = 'slack'" + } + }, + "foreignKeys": { + "page_subscriber_page_id_page_id_fk": { + "name": "page_subscriber_page_id_page_id_fk", + "tableFrom": "page_subscriber", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "page_subscriber_channel_check": { + "name": "page_subscriber_channel_check", + "value": "(\"page_subscriber\".\"channel_type\" = 'email' AND \"page_subscriber\".\"email\" IS NOT NULL AND \"page_subscriber\".\"webhook_url\" IS NULL) OR (\"page_subscriber\".\"channel_type\" = 'webhook' AND \"page_subscriber\".\"webhook_url\" IS NOT NULL AND \"page_subscriber\".\"email\" IS NULL) OR (\"page_subscriber\".\"channel_type\" = 'slack' AND \"page_subscriber\".\"slack_channel_id\" IS NOT NULL AND \"page_subscriber\".\"email\" IS NULL AND \"page_subscriber\".\"webhook_url\" IS NULL)" + } + } + }, + "page_subscriber_to_page_component": { + "name": "page_subscriber_to_page_component", + "columns": { + "page_subscriber_id": { + "name": "page_subscriber_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_component_id": { + "name": "page_component_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk": { + "name": "page_subscriber_to_page_component_page_subscriber_id_page_subscriber_id_fk", + "tableFrom": "page_subscriber_to_page_component", + "tableTo": "page_subscriber", + "columnsFrom": [ + "page_subscriber_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_subscriber_to_page_component_page_component_id_page_component_id_fk": { + "name": "page_subscriber_to_page_component_page_component_id_page_component_id_fk", + "tableFrom": "page_subscriber_to_page_component", + "tableTo": "page_component", + "columnsFrom": [ + "page_component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk": { + "columns": [ + "page_subscriber_id", + "page_component_id" + ], + "name": "page_subscriber_to_page_component_page_subscriber_id_page_component_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification": { + "name": "notification", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "notification_workspace_id_idx": { + "name": "notification_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notification_workspace_id_workspace_id_fk": { + "name": "notification_workspace_id_workspace_id_fk", + "tableFrom": "notification", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_trigger": { + "name": "notification_trigger", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cron_timestamp": { + "name": "cron_timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notification_id_monitor_id_crontimestampe": { + "name": "notification_id_monitor_id_crontimestampe", + "columns": [ + "notification_id", + "monitor_id", + "cron_timestamp" + ], + "isUnique": true + } + }, + "foreignKeys": { + "notification_trigger_monitor_id_monitor_id_fk": { + "name": "notification_trigger_monitor_id_monitor_id_fk", + "tableFrom": "notification_trigger", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_trigger_notification_id_notification_id_fk": { + "name": "notification_trigger_notification_id_notification_id_fk", + "tableFrom": "notification_trigger", + "tableTo": "notification", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications_to_monitors": { + "name": "notifications_to_monitors", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "notifications_to_monitors_notification_id_idx": { + "name": "notifications_to_monitors_notification_id_idx", + "columns": [ + "notification_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notifications_to_monitors_monitor_id_monitor_id_fk": { + "name": "notifications_to_monitors_monitor_id_monitor_id_fk", + "tableFrom": "notifications_to_monitors", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_to_monitors_notification_id_notification_id_fk": { + "name": "notifications_to_monitors_notification_id_notification_id_fk", + "tableFrom": "notifications_to_monitors", + "tableTo": "notification", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notifications_to_monitors_monitor_id_notification_id_pk": { + "columns": [ + "monitor_id", + "notification_id" + ], + "name": "notifications_to_monitors_monitor_id_notification_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_status": { + "name": "monitor_status", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "monitor_status_idx": { + "name": "monitor_status_idx", + "columns": [ + "monitor_id", + "region" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_status_monitor_id_monitor_id_fk": { + "name": "monitor_status_monitor_id_monitor_id_fk", + "tableFrom": "monitor_status", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "monitor_status_monitor_id_region_pk": { + "columns": [ + "monitor_id", + "region" + ], + "name": "monitor_status_monitor_id_region_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "invitation_workspace_id_idx": { + "name": "invitation_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "incident": { + "name": "incident", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'triage'" + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acknowledged_by": { + "name": "acknowledged_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "incident_screenshot_url": { + "name": "incident_screenshot_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recovery_screenshot_url": { + "name": "recovery_screenshot_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_resolved": { + "name": "auto_resolved", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "incident_workspace_id_idx": { + "name": "incident_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + }, + "incident_monitor_id_started_at_unique": { + "name": "incident_monitor_id_started_at_unique", + "columns": [ + "monitor_id", + "started_at" + ], + "isUnique": true + } + }, + "foreignKeys": { + "incident_monitor_id_monitor_id_fk": { + "name": "incident_monitor_id_monitor_id_fk", + "tableFrom": "incident", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set default", + "onUpdate": "no action" + }, + "incident_workspace_id_workspace_id_fk": { + "name": "incident_workspace_id_workspace_id_fk", + "tableFrom": "incident", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incident_acknowledged_by_user_id_fk": { + "name": "incident_acknowledged_by_user_id_fk", + "tableFrom": "incident", + "tableTo": "user", + "columnsFrom": [ + "acknowledged_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incident_resolved_by_user_id_fk": { + "name": "incident_resolved_by_user_id_fk", + "tableFrom": "incident", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_tag": { + "name": "monitor_tag", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "monitor_tag_workspace_id_idx": { + "name": "monitor_tag_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_tag_workspace_id_workspace_id_fk": { + "name": "monitor_tag_workspace_id_workspace_id_fk", + "tableFrom": "monitor_tag", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_tag_to_monitor": { + "name": "monitor_tag_to_monitor", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monitor_tag_id": { + "name": "monitor_tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "monitor_tag_to_monitor_monitor_tag_id_idx": { + "name": "monitor_tag_to_monitor_monitor_tag_id_idx", + "columns": [ + "monitor_tag_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_tag_to_monitor_monitor_id_monitor_id_fk": { + "name": "monitor_tag_to_monitor_monitor_id_monitor_id_fk", + "tableFrom": "monitor_tag_to_monitor", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk": { + "name": "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk", + "tableFrom": "monitor_tag_to_monitor", + "tableTo": "monitor_tag", + "columnsFrom": [ + "monitor_tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk": { + "columns": [ + "monitor_id", + "monitor_tag_id" + ], + "name": "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "application": { + "name": "application", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dsn": { + "name": "dsn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "application_dsn_unique": { + "name": "application_dsn_unique", + "columns": [ + "dsn" + ], + "isUnique": true + }, + "application_workspace_id_idx": { + "name": "application_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "application_workspace_id_workspace_id_fk": { + "name": "application_workspace_id_workspace_id_fk", + "tableFrom": "application", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "maintenance": { + "name": "maintenance", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from": { + "name": "from", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to": { + "name": "to", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "maintenance_page_id_idx": { + "name": "maintenance_page_id_idx", + "columns": [ + "page_id" + ], + "isUnique": false + }, + "maintenance_workspace_id_idx": { + "name": "maintenance_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "maintenance_workspace_id_workspace_id_fk": { + "name": "maintenance_workspace_id_workspace_id_fk", + "tableFrom": "maintenance", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "maintenance_page_id_page_id_fk": { + "name": "maintenance_page_id_page_id_fk", + "tableFrom": "maintenance", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "check": { + "name": "check", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "regions": { + "name": "regions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "url": { + "name": "url", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'GET'" + }, + "count_requests": { + "name": "count_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1 + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "check_workspace_id_idx": { + "name": "check_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "check_workspace_id_workspace_id_fk": { + "name": "check_workspace_id_workspace_id_fk", + "tableFrom": "check", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_run": { + "name": "monitor_run", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runned_at": { + "name": "runned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "monitor_run_workspace_id_idx": { + "name": "monitor_run_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + }, + "monitor_run_monitor_id_idx": { + "name": "monitor_run_monitor_id_idx", + "columns": [ + "monitor_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_run_workspace_id_workspace_id_fk": { + "name": "monitor_run_workspace_id_workspace_id_fk", + "tableFrom": "monitor_run", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "monitor_run_monitor_id_monitor_id_fk": { + "name": "monitor_run_monitor_id_monitor_id_fk", + "tableFrom": "monitor_run", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "private_location_monitor_status": { + "name": "private_location_monitor_status", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_location_id": { + "name": "private_location_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "cron_timestamp": { + "name": "cron_timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "private_location_monitor_status_pl_id_idx": { + "name": "private_location_monitor_status_pl_id_idx", + "columns": [ + "private_location_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "private_location_monitor_status_monitor_id_monitor_id_fk": { + "name": "private_location_monitor_status_monitor_id_monitor_id_fk", + "tableFrom": "private_location_monitor_status", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "private_location_monitor_status_private_location_id_private_location_id_fk": { + "name": "private_location_monitor_status_private_location_id_private_location_id_fk", + "tableFrom": "private_location_monitor_status", + "tableTo": "private_location", + "columnsFrom": [ + "private_location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "private_location_monitor_status_monitor_id_private_location_id_pk": { + "columns": [ + "monitor_id", + "private_location_id" + ], + "name": "private_location_monitor_status_monitor_id_private_location_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "private_location": { + "name": "private_location", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "private_location_workspace_id_idx": { + "name": "private_location_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "private_location_workspace_id_workspace_id_fk": { + "name": "private_location_workspace_id_workspace_id_fk", + "tableFrom": "private_location", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "private_location_to_monitor": { + "name": "private_location_to_monitor", + "columns": { + "private_location_id": { + "name": "private_location_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "private_location_to_monitor_private_location_id_idx": { + "name": "private_location_to_monitor_private_location_id_idx", + "columns": [ + "private_location_id" + ], + "isUnique": false + }, + "private_location_to_monitor_monitor_id_idx": { + "name": "private_location_to_monitor_monitor_id_idx", + "columns": [ + "monitor_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "private_location_to_monitor_private_location_id_private_location_id_fk": { + "name": "private_location_to_monitor_private_location_id_private_location_id_fk", + "tableFrom": "private_location_to_monitor", + "tableTo": "private_location", + "columnsFrom": [ + "private_location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "private_location_to_monitor_monitor_id_monitor_id_fk": { + "name": "private_location_to_monitor_monitor_id_monitor_id_fk", + "tableFrom": "private_location_to_monitor", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_group": { + "name": "monitor_group", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "monitor_group_workspace_id_idx": { + "name": "monitor_group_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + }, + "monitor_group_page_id_idx": { + "name": "monitor_group_page_id_idx", + "columns": [ + "page_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_group_workspace_id_workspace_id_fk": { + "name": "monitor_group_workspace_id_workspace_id_fk", + "tableFrom": "monitor_group", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "monitor_group_page_id_page_id_fk": { + "name": "monitor_group_page_id_page_id_fk", + "tableFrom": "monitor_group", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "viewer": { + "name": "viewer", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "viewer_email_unique": { + "name": "viewer_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "viewer_accounts": { + "name": "viewer_accounts", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "viewer_accounts_user_id_viewer_id_fk": { + "name": "viewer_accounts_user_id_viewer_id_fk", + "tableFrom": "viewer_accounts", + "tableTo": "viewer", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "viewer_accounts_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "viewer_accounts_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "viewer_session": { + "name": "viewer_session", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "viewer_session_user_id_viewer_id_fk": { + "name": "viewer_session_user_id_viewer_id_fk", + "tableFrom": "viewer_session", + "tableTo": "viewer", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_key": { + "name": "api_key", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hashed_token": { + "name": "hashed_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"write\"]'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "api_key_prefix_unique": { + "name": "api_key_prefix_unique", + "columns": [ + "prefix" + ], + "isUnique": true + }, + "api_key_hashed_token_unique": { + "name": "api_key_hashed_token_unique", + "columns": [ + "hashed_token" + ], + "isUnique": true + }, + "api_key_prefix_idx": { + "name": "api_key_prefix_idx", + "columns": [ + "prefix" + ], + "isUnique": false + }, + "api_key_workspace_id_idx": { + "name": "api_key_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_id_user_id_fk": { + "name": "api_key_created_by_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "maintenance_to_page_component": { + "name": "maintenance_to_page_component", + "columns": { + "maintenance_id": { + "name": "maintenance_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_component_id": { + "name": "page_component_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "maintenance_to_page_component_page_component_id_idx": { + "name": "maintenance_to_page_component_page_component_id_idx", + "columns": [ + "page_component_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "maintenance_to_page_component_maintenance_id_maintenance_id_fk": { + "name": "maintenance_to_page_component_maintenance_id_maintenance_id_fk", + "tableFrom": "maintenance_to_page_component", + "tableTo": "maintenance", + "columnsFrom": [ + "maintenance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "maintenance_to_page_component_page_component_id_page_component_id_fk": { + "name": "maintenance_to_page_component_page_component_id_page_component_id_fk", + "tableFrom": "maintenance_to_page_component", + "tableTo": "page_component", + "columnsFrom": [ + "page_component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "maintenance_to_page_component_maintenance_id_page_component_id_pk": { + "columns": [ + "maintenance_id", + "page_component_id" + ], + "name": "maintenance_to_page_component_maintenance_id_page_component_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "page_component": { + "name": "page_component", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'monitor'" + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_service_id": { + "name": "external_service_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_service_component_id": { + "name": "external_service_component_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "group_id": { + "name": "group_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_order": { + "name": "group_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "page_component_workspace_id_idx": { + "name": "page_component_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + }, + "page_component_page_id_monitor_id_unique": { + "name": "page_component_page_id_monitor_id_unique", + "columns": [ + "page_id", + "monitor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "page_component_workspace_id_workspace_id_fk": { + "name": "page_component_workspace_id_workspace_id_fk", + "tableFrom": "page_component", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_component_page_id_page_id_fk": { + "name": "page_component_page_id_page_id_fk", + "tableFrom": "page_component", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_component_monitor_id_monitor_id_fk": { + "name": "page_component_monitor_id_monitor_id_fk", + "tableFrom": "page_component", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_component_external_service_id_external_service_id_fk": { + "name": "page_component_external_service_id_external_service_id_fk", + "tableFrom": "page_component", + "tableTo": "external_service", + "columnsFrom": [ + "external_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_component_external_service_component_id_external_service_component_id_fk": { + "name": "page_component_external_service_component_id_external_service_component_id_fk", + "tableFrom": "page_component", + "tableTo": "external_service_component", + "columnsFrom": [ + "external_service_component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_component_group_id_page_component_groups_id_fk": { + "name": "page_component_group_id_page_component_groups_id_fk", + "tableFrom": "page_component", + "tableTo": "page_component_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "page_component_type_check": { + "name": "page_component_type_check", + "value": "(\"page_component\".\"type\" = 'monitor' AND \"page_component\".\"monitor_id\" IS NOT NULL AND \"page_component\".\"external_service_id\" IS NULL AND \"page_component\".\"external_service_component_id\" IS NULL) OR (\"page_component\".\"type\" = 'static' AND \"page_component\".\"monitor_id\" IS NULL AND \"page_component\".\"external_service_id\" IS NULL AND \"page_component\".\"external_service_component_id\" IS NULL) OR (\"page_component\".\"type\" = 'external' AND \"page_component\".\"monitor_id\" IS NULL AND \"page_component\".\"external_service_id\" IS NOT NULL)" + } + } + }, + "status_report_update_to_page_component": { + "name": "status_report_update_to_page_component", + "columns": { + "status_report_update_id": { + "name": "status_report_update_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_component_id": { + "name": "page_component_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impact": { + "name": "impact", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "status_report_update_to_page_component_page_component_id_idx": { + "name": "status_report_update_to_page_component_page_component_id_idx", + "columns": [ + "page_component_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "status_report_update_to_page_component_status_report_update_id_status_report_update_id_fk": { + "name": "status_report_update_to_page_component_status_report_update_id_status_report_update_id_fk", + "tableFrom": "status_report_update_to_page_component", + "tableTo": "status_report_update", + "columnsFrom": [ + "status_report_update_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_report_update_to_page_component_page_component_id_page_component_id_fk": { + "name": "status_report_update_to_page_component_page_component_id_page_component_id_fk", + "tableFrom": "status_report_update_to_page_component", + "tableTo": "page_component", + "columnsFrom": [ + "page_component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "status_report_update_to_page_component_status_report_update_id_page_component_id_pk": { + "columns": [ + "status_report_update_id", + "page_component_id" + ], + "name": "status_report_update_to_page_component_status_report_update_id_page_component_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_report_to_page_component": { + "name": "status_report_to_page_component", + "columns": { + "status_report_id": { + "name": "status_report_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_component_id": { + "name": "page_component_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "status_report_to_page_component_page_component_id_idx": { + "name": "status_report_to_page_component_page_component_id_idx", + "columns": [ + "page_component_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "status_report_to_page_component_status_report_id_status_report_id_fk": { + "name": "status_report_to_page_component_status_report_id_status_report_id_fk", + "tableFrom": "status_report_to_page_component", + "tableTo": "status_report", + "columnsFrom": [ + "status_report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_report_to_page_component_page_component_id_page_component_id_fk": { + "name": "status_report_to_page_component_page_component_id_page_component_id_fk", + "tableFrom": "status_report_to_page_component", + "tableTo": "page_component", + "columnsFrom": [ + "page_component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "status_report_to_page_component_status_report_id_page_component_id_pk": { + "columns": [ + "status_report_id", + "page_component_id" + ], + "name": "status_report_to_page_component_status_report_id_page_component_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "page_component_groups": { + "name": "page_component_groups", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "default_open": { + "name": "default_open", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "page_component_groups_page_id_idx": { + "name": "page_component_groups_page_id_idx", + "columns": [ + "page_id" + ], + "isUnique": false + }, + "page_component_groups_workspace_id_idx": { + "name": "page_component_groups_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "page_component_groups_workspace_id_workspace_id_fk": { + "name": "page_component_groups_workspace_id_workspace_id_fk", + "tableFrom": "page_component_groups", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_component_groups_page_id_page_id_fk": { + "name": "page_component_groups_page_id_page_id_fk", + "tableFrom": "page_component_groups", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feedback": { + "name": "feedback", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "blocker": { + "name": "blocker", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "feedback_workspace_id_idx": { + "name": "feedback_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "feedback_workspace_id_workspace_id_fk": { + "name": "feedback_workspace_id_workspace_id_fk", + "tableFrom": "feedback", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_user_id_user_id_fk": { + "name": "feedback_user_id_user_id_fk", + "tableFrom": "feedback", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_log": { + "name": "audit_log", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "before": { + "name": "before", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "after": { + "name": "after", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "changed_fields": { + "name": "changed_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + "workspace_id", + "created_at" + ], + "isUnique": false + }, + "audit_log_entity_idx": { + "name": "audit_log_entity_idx", + "columns": [ + "workspace_id", + "entity_type", + "entity_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_service": { + "name": "external_service", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "aliases": { + "name": "aliases", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_array())" + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_page_url": { + "name": "status_page_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "industry": { + "name": "industry", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_config": { + "name": "api_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "external_service_slug_unique": { + "name": "external_service_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "external_service_deleted_at_idx": { + "name": "external_service_deleted_at_idx", + "columns": [ + "deleted_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_service_component": { + "name": "external_service_component", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "external_service_id": { + "name": "external_service_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upstream_component_id": { + "name": "upstream_component_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "aliases": { + "name": "aliases", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(json_array())" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "indicator": { + "name": "indicator", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "external_service_component_unique_idx": { + "name": "external_service_component_unique_idx", + "columns": [ + "external_service_id", + "upstream_component_id" + ], + "isUnique": true + }, + "external_service_component_slug_unique_idx": { + "name": "external_service_component_slug_unique_idx", + "columns": [ + "external_service_id", + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_service_component_external_service_id_external_service_id_fk": { + "name": "external_service_component_external_service_id_external_service_id_fk", + "tableFrom": "external_service_component", + "tableTo": "external_service", + "columnsFrom": [ + "external_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_service_incident": { + "name": "external_service_incident", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "external_service_id": { + "name": "external_service_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_incident_id": { + "name": "provider_incident_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impact": { + "name": "impact", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shortlink": { + "name": "shortlink", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "affected_component_ids": { + "name": "affected_component_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "raw_payload": { + "name": "raw_payload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_payload_purged_at": { + "name": "raw_payload_purged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "external_service_incident_unique_idx": { + "name": "external_service_incident_unique_idx", + "columns": [ + "external_service_id", + "provider_incident_id" + ], + "isUnique": true + }, + "external_service_incident_started_at_idx": { + "name": "external_service_incident_started_at_idx", + "columns": [ + "external_service_id", + "started_at" + ], + "isUnique": false + }, + "external_service_incident_resolved_at_idx": { + "name": "external_service_incident_resolved_at_idx", + "columns": [ + "resolved_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_service_incident_external_service_id_external_service_id_fk": { + "name": "external_service_incident_external_service_id_external_service_id_fk", + "tableFrom": "external_service_incident", + "tableTo": "external_service", + "columnsFrom": [ + "external_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_service_report": { + "name": "external_service_report", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "external_service_id": { + "name": "external_service_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_service_component_id": { + "name": "external_service_component_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reporter_hash": { + "name": "reporter_hash", + "type": "text(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "country": { + "name": "country", + "type": "text(2)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "external_service_report_service_idx": { + "name": "external_service_report_service_idx", + "columns": [ + "external_service_id", + "created_at" + ], + "isUnique": false + }, + "external_service_report_component_idx": { + "name": "external_service_report_component_idx", + "columns": [ + "external_service_component_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_service_report_external_service_id_external_service_id_fk": { + "name": "external_service_report_external_service_id_external_service_id_fk", + "tableFrom": "external_service_report", + "tableTo": "external_service", + "columnsFrom": [ + "external_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "external_service_report_external_service_component_id_external_service_component_id_fk": { + "name": "external_service_report_external_service_component_id_external_service_component_id_fk", + "tableFrom": "external_service_report", + "tableTo": "external_service_component", + "columnsFrom": [ + "external_service_component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_session": { + "name": "chat_session", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_session_workspace_user_updated_idx": { + "name": "chat_session_workspace_user_updated_idx", + "columns": [ + "workspace_id", + "user_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_session_workspace_id_workspace_id_fk": { + "name": "chat_session_workspace_id_workspace_id_fk", + "tableFrom": "chat_session", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_session_user_id_user_id_fk": { + "name": "chat_session_user_id_user_id_fk", + "tableFrom": "chat_session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "frozen_monitor_uptime": { + "name": "frozen_monitor_uptime", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "month": { + "name": "month", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "days": { + "name": "days", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "frozen_monitor_uptime_workspace_id_idx": { + "name": "frozen_monitor_uptime_workspace_id_idx", + "columns": [ + "workspace_id" + ], + "isUnique": false + }, + "frozen_monitor_uptime_monitor_id_month_unique": { + "name": "frozen_monitor_uptime_monitor_id_month_unique", + "columns": [ + "monitor_id", + "month" + ], + "isUnique": true + } + }, + "foreignKeys": { + "frozen_monitor_uptime_workspace_id_workspace_id_fk": { + "name": "frozen_monitor_uptime_workspace_id_workspace_id_fk", + "tableFrom": "frozen_monitor_uptime", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "frozen_monitor_uptime_monitor_id_monitor_id_fk": { + "name": "frozen_monitor_uptime_monitor_id_monitor_id_fk", + "tableFrom": "frozen_monitor_uptime", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "page_lower_slug_idx": { + "columns": { + "LOWER(\"slug\")": { + "isExpression": true + } + } + }, + "page_lower_custom_domain_idx": { + "columns": { + "LOWER(\"custom_domain\")": { + "isExpression": true + } + } + }, + "idx_page_subscriber_email_page_active": { + "columns": { + "LOWER(\"email\")": { + "isExpression": true + } + } + }, + "idx_page_subscriber_webhook_page_active": { + "columns": { + "LOWER(\"webhook_url\")": { + "isExpression": true + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index a7653aa8..f7806420 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -568,6 +568,13 @@ "when": 1784059243113, "tag": "0080_soft_norman_osborn", "breakpoints": true + }, + { + "idx": 81, + "version": "6", + "when": 1784110358131, + "tag": "0081_military_polaris", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/external_services/constants.ts b/packages/db/src/schema/external_services/constants.ts index 95904e7e..2f7cd80d 100644 --- a/packages/db/src/schema/external_services/constants.ts +++ b/packages/db/src/schema/external_services/constants.ts @@ -47,3 +47,57 @@ export const apiConfigSchema = z.object({ }); export type ApiConfig = z.infer; + +const INDICATOR_SEVERITY: Record = { + none: 0, + minor: 1, + major: 2, + critical: 3, +}; + +export function externalIndicatorSeverity(indicator: string): number { + return INDICATOR_SEVERITY[indicator] ?? -1; +} + +export function worstExternalIndicator(indicators: Iterable): string { + let worst = ""; + for (const indicator of indicators) { + if ( + worst === "" || + externalIndicatorSeverity(indicator) > externalIndicatorSeverity(worst) + ) { + worst = indicator; + } + } + return worst; +} + +export type ExternalStatusType = + | "success" + | "degraded" + | "error" + | "info" + | "empty"; + +export function externalIndicatorToStatus( + indicator: string, + hadMaintenance = false, +): ExternalStatusType { + let base: ExternalStatusType; + switch (indicator) { + case "none": + base = "success"; + break; + case "minor": + base = "degraded"; + break; + case "major": + case "critical": + base = "error"; + break; + default: + base = "empty"; + } + if (hadMaintenance && (base === "success" || base === "empty")) return "info"; + return base; +} diff --git a/packages/db/src/schema/page_components/constants.ts b/packages/db/src/schema/page_components/constants.ts index 7f14c0de..6a344f8d 100644 --- a/packages/db/src/schema/page_components/constants.ts +++ b/packages/db/src/schema/page_components/constants.ts @@ -1,4 +1,4 @@ -export const pageComponentTypes = ["static", "monitor"] as const; +export const pageComponentTypes = ["static", "monitor", "external"] as const; export type PageComponentType = (typeof pageComponentTypes)[number]; diff --git a/packages/db/src/schema/page_components/page_components.ts b/packages/db/src/schema/page_components/page_components.ts index 693a9f3e..302c1445 100644 --- a/packages/db/src/schema/page_components/page_components.ts +++ b/packages/db/src/schema/page_components/page_components.ts @@ -9,6 +9,10 @@ import { unique, } from "drizzle-orm/sqlite-core"; +import { + externalService, + externalServiceComponent, +} from "../external_services"; import { maintenance } from "../maintenances"; import { monitor } from "../monitors"; import { pageComponentGroup } from "../page_component_groups"; @@ -33,6 +37,13 @@ export const pageComponent = sqliteTable( monitorId: integer("monitor_id").references(() => monitor.id, { onDelete: "cascade", }), + externalServiceId: integer("external_service_id").references( + () => externalService.id, + { onDelete: "cascade" }, + ), + externalServiceComponentId: integer( + "external_service_component_id", + ).references(() => externalServiceComponent.id, { onDelete: "cascade" }), name: text("name").notNull(), description: text("description"), order: integer("order").default(0), @@ -56,8 +67,7 @@ export const pageComponent = sqliteTable( index("page_component_workspace_id_idx").on(t.workspaceId), check( "page_component_type_check", - // NOTE: This check ensures that either the component is a monitor or a static component, but not both. - sql`${t.type} = 'monitor' AND ${t.monitorId} IS NOT NULL OR ${t.type} = 'static' AND ${t.monitorId} IS NULL`, + sql`(${t.type} = 'monitor' AND ${t.monitorId} IS NOT NULL AND ${t.externalServiceId} IS NULL AND ${t.externalServiceComponentId} IS NULL) OR (${t.type} = 'static' AND ${t.monitorId} IS NULL AND ${t.externalServiceId} IS NULL AND ${t.externalServiceComponentId} IS NULL) OR (${t.type} = 'external' AND ${t.monitorId} IS NULL AND ${t.externalServiceId} IS NOT NULL)`, ), ], ); @@ -77,6 +87,14 @@ export const pageComponentRelations = relations( fields: [pageComponent.monitorId], references: [monitor.id], }), + externalService: one(externalService, { + fields: [pageComponent.externalServiceId], + references: [externalService.id], + }), + externalServiceComponent: one(externalServiceComponent, { + fields: [pageComponent.externalServiceComponentId], + references: [externalServiceComponent.id], + }), group: one(pageComponentGroup, { fields: [pageComponent.groupId], references: [pageComponentGroup.id], diff --git a/packages/db/src/schema/page_components/validation.ts b/packages/db/src/schema/page_components/validation.ts index fae44fbd..09e04473 100644 --- a/packages/db/src/schema/page_components/validation.ts +++ b/packages/db/src/schema/page_components/validation.ts @@ -13,19 +13,28 @@ export const insertPageComponentSchema = createInsertSchema(pageComponent, { name: z.string().min(1), }).refine( (data) => { - // monitorId must be set when type='monitor' - if (data.type === "monitor" && !data.monitorId) { - return false; + switch (data.type) { + case "monitor": + return ( + !!data.monitorId && + !data.externalServiceId && + !data.externalServiceComponentId + ); + case "static": + return ( + !data.monitorId && + !data.externalServiceId && + !data.externalServiceComponentId + ); + case "external": + return !data.monitorId && !!data.externalServiceId; + default: + return false; } - // monitorId must be null when type='static' - if (data.type === "static" && data.monitorId) { - return false; - } - return true; }, { message: - "monitorId must be set when type is 'monitor' and must be null when type is 'static'", + "monitor requires monitorId; static requires no refs; external requires externalServiceId", }, ); diff --git a/packages/services/src/external-service/__tests__/indicator.test.ts b/packages/services/src/external-service/__tests__/indicator.test.ts new file mode 100644 index 00000000..aa0ae0a6 --- /dev/null +++ b/packages/services/src/external-service/__tests__/indicator.test.ts @@ -0,0 +1,42 @@ +import { + externalIndicatorToStatus, + worstExternalIndicator, +} from "@openstatus/db/src/schema"; +import { expect } from "@std/expect"; +import { describe, test } from "@std/testing/bdd"; + +describe("externalIndicatorToStatus", () => { + test("maps indicators onto the status-page palette", () => { + expect(externalIndicatorToStatus("none")).toBe("success"); + expect(externalIndicatorToStatus("minor")).toBe("degraded"); + expect(externalIndicatorToStatus("major")).toBe("error"); + expect(externalIndicatorToStatus("critical")).toBe("error"); + }); + + test("maintenance shows on operational/no-data, never masks an outage", () => { + expect(externalIndicatorToStatus("none", true)).toBe("info"); + expect(externalIndicatorToStatus("", true)).toBe("info"); + expect(externalIndicatorToStatus("minor", true)).toBe("degraded"); + expect(externalIndicatorToStatus("major", true)).toBe("error"); + expect(externalIndicatorToStatus("critical", true)).toBe("error"); + }); + + test("unknown indicator is empty (no data)", () => { + expect(externalIndicatorToStatus("")).toBe("empty"); + expect(externalIndicatorToStatus("bogus")).toBe("empty"); + }); +}); + +describe("worstExternalIndicator", () => { + test("returns the worst by severity", () => { + expect(worstExternalIndicator(["none", "minor", "major"])).toBe("major"); + expect(worstExternalIndicator(["minor", "critical", "none"])).toBe( + "critical", + ); + expect(worstExternalIndicator(["none", "none"])).toBe("none"); + }); + + test("empty set yields no indicator", () => { + expect(worstExternalIndicator([])).toBe(""); + }); +}); diff --git a/packages/services/src/external-service/index.ts b/packages/services/src/external-service/index.ts index 0f3148f6..c36917d4 100644 --- a/packages/services/src/external-service/index.ts +++ b/packages/services/src/external-service/index.ts @@ -10,3 +10,12 @@ export { type ApplyDetectedProviderInput, applyDetectedProvider, } from "./update-provider"; +export { + type ExternalDailyRow, + type ExternalPageComponentInput, + type ExternalSectionComponent, + type ExternalSectionIncident, + type ExternalSectionProvider, + type PageExternalSection, + getPageExternalSection, +} from "./status-page"; diff --git a/packages/services/src/external-service/status-page.ts b/packages/services/src/external-service/status-page.ts new file mode 100644 index 00000000..19ee4159 --- /dev/null +++ b/packages/services/src/external-service/status-page.ts @@ -0,0 +1,306 @@ +import { and, db as defaultDb, inArray, isNull } from "@openstatus/db"; +import { + type ExternalStatusType, + externalIndicatorToStatus, + externalService, + worstExternalIndicator, +} from "@openstatus/db/src/schema"; + +import { defaultTb } from "../context"; +import { + type ExternalComponentListItem, + listExternalComponentsByServiceId, +} from "../external-service-component"; +import { + type ExternalIncidentListItem, + listExternalIncidentsByServiceId, +} from "../external-service-incident"; +import { retryRead } from "../retry"; +import type { GlobalReadContext } from "./internal"; + +export type ExternalPageComponentInput = { + pageComponentId: number; + name: string; + description: string | null; + order: number; + externalServiceId: number; + externalServiceComponentId: number | null; +}; + +export type ExternalDailyRow = { + day: string; + worstIndicator: string; + hadMaintenance: number; +}; + +export type ExternalSectionComponent = { + pageComponentId: number; + name: string; + description: string | null; + status: ExternalStatusType; + stale: boolean; + isWholeService: boolean; + daily: ExternalDailyRow[]; +}; + +export type ExternalSectionProvider = { + externalServiceId: number; + name: string; + slug: string; + statusPageUrl: string; + status: ExternalStatusType; + order: number; + components: ExternalSectionComponent[]; +}; + +export type ExternalSectionIncident = ExternalIncidentListItem & { + serviceName: string; + serviceSlug: string; +}; + +export type PageExternalSection = { + position: number | null; + providers: ExternalSectionProvider[]; + incidents: ExternalSectionIncident[]; +}; + +const DEFAULT_DAYS = 45; +const INCIDENTS_LIMIT = 10; + +async function safeData(promise: Promise<{ data: T[] }>): Promise { + try { + return (await promise).data; + } catch (err) { + console.warn("[external-section] tinybird history failed:", err); + return []; + } +} + +function worstStatus(statuses: ExternalStatusType[]): ExternalStatusType { + const order: ExternalStatusType[] = ["success", "info", "degraded", "error"]; + let worst: ExternalStatusType = "empty"; + for (const s of statuses) { + if (s === "empty") continue; + if (worst === "empty" || order.indexOf(s) > order.indexOf(worst)) { + worst = s; + } + } + return worst; +} + +export async function getPageExternalSection(args: { + ctx?: GlobalReadContext; + components: ExternalPageComponentInput[]; + days?: number; + now?: Date; +}): Promise { + const { ctx, components } = args; + const days = args.days ?? DEFAULT_DAYS; + if (components.length === 0) { + return { position: null, providers: [], incidents: [] }; + } + + const db = ctx?.db ?? defaultDb; + const staleCutoff = (args.now ?? new Date()).getTime() - 24 * 60 * 60 * 1000; + const serviceIds = Array.from( + new Set(components.map((c) => c.externalServiceId)), + ); + + const serviceRows = await retryRead(() => + db + .select({ + id: externalService.id, + slug: externalService.slug, + name: externalService.name, + statusPageUrl: externalService.statusPageUrl, + }) + .from(externalService) + .where( + and( + inArray(externalService.id, serviceIds), + isNull(externalService.deletedAt), + ), + ) + .all(), + ); + const serviceById = new Map(serviceRows.map((s) => [s.id, s])); + + const liveByService = new Map< + number, + Map + >(); + await Promise.all( + serviceIds.map(async (id) => { + const live = await listExternalComponentsByServiceId({ + ctx, + externalServiceId: id, + now: args.now, + }); + liveByService.set(id, new Map(live.map((c) => [c.id, c]))); + }), + ); + + const componentHistIds = components + .filter((c) => c.externalServiceComponentId != null) + .map((c) => String(c.externalServiceComponentId)); + const serviceHistSlugs = Array.from( + new Set( + components + .filter((c) => c.externalServiceComponentId == null) + .map((c) => serviceById.get(c.externalServiceId)?.slug) + .filter((slug): slug is string => !!slug), + ), + ); + + const hasTb = !!process.env.TINY_BIRD_API_KEY; + const [componentHistory, serviceHistory, serviceLatest] = await Promise.all([ + hasTb && componentHistIds.length > 0 + ? safeData( + defaultTb.externalStatusComponentHistory({ + component_ids: componentHistIds, + days, + }), + ) + : Promise.resolve([]), + hasTb && serviceHistSlugs.length > 0 + ? safeData( + defaultTb.externalStatusHistory({ ids: serviceHistSlugs, days }), + ) + : Promise.resolve([]), + hasTb && serviceHistSlugs.length > 0 + ? safeData(defaultTb.externalStatusLatest({ ids: serviceHistSlugs })) + : Promise.resolve([]), + ]); + + const serviceLatestBySlug = new Map(serviceLatest.map((r) => [r.id, r])); + + const componentDaily = new Map(); + for (const row of componentHistory) { + const arr = componentDaily.get(row.component_id) ?? []; + arr.push({ + day: row.day, + worstIndicator: row.worst_indicator, + hadMaintenance: row.had_maintenance, + }); + componentDaily.set(row.component_id, arr); + } + const serviceDaily = new Map(); + for (const row of serviceHistory) { + const arr = serviceDaily.get(row.id) ?? []; + arr.push({ + day: row.day, + worstIndicator: row.worst_indicator, + hadMaintenance: row.had_maintenance, + }); + serviceDaily.set(row.id, arr); + } + + const providers = new Map(); + for (const c of components) { + const service = serviceById.get(c.externalServiceId); + if (!service) continue; + const live = liveByService.get(c.externalServiceId) ?? new Map(); + + let status: ExternalStatusType; + let stale: boolean; + let daily: ExternalDailyRow[]; + if (c.externalServiceComponentId == null) { + const liveItems = Array.from(live.values()); + const indicators = liveItems.map((x) => x.indicator); + if (indicators.length > 0) { + const maintenance = liveItems.some( + (x) => x.status === "under_maintenance", + ); + status = externalIndicatorToStatus( + worstExternalIndicator(indicators), + maintenance, + ); + stale = false; + } else { + const latest = serviceLatestBySlug.get(service.slug); + const fresh = latest != null && latest.last_fetched_at >= staleCutoff; + status = fresh + ? externalIndicatorToStatus( + latest.indicator, + latest.status === "under_maintenance", + ) + : "empty"; + stale = !fresh; + } + daily = serviceDaily.get(service.slug) ?? []; + } else { + const item = live.get(c.externalServiceComponentId); + status = item + ? externalIndicatorToStatus( + item.indicator, + item.status === "under_maintenance", + ) + : "empty"; + stale = !item; + daily = componentDaily.get(String(c.externalServiceComponentId)) ?? []; + } + + const sectionComponent: ExternalSectionComponent = { + pageComponentId: c.pageComponentId, + name: c.name, + description: c.description, + status, + stale, + isWholeService: c.externalServiceComponentId == null, + daily, + }; + + const existing = providers.get(c.externalServiceId); + if (existing) { + existing.components.push(sectionComponent); + existing.order = Math.min(existing.order, c.order); + } else { + providers.set(c.externalServiceId, { + externalServiceId: c.externalServiceId, + name: service.name, + slug: service.slug, + statusPageUrl: service.statusPageUrl, + status: "empty", + order: c.order, + components: [sectionComponent], + }); + } + } + + for (const provider of providers.values()) { + provider.status = worstStatus(provider.components.map((x) => x.status)); + } + + const incidentLists = await Promise.all( + serviceIds.map(async (id) => { + const service = serviceById.get(id); + if (!service) return []; + const incidents = await listExternalIncidentsByServiceId({ + ctx, + externalServiceId: id, + limit: INCIDENTS_LIMIT, + }); + return incidents.map((i) => ({ + ...i, + serviceName: service.name, + serviceSlug: service.slug, + })); + }), + ); + const incidents = incidentLists + .flat() + .sort((a, b) => { + const at = (a.startedAt ?? a.createdAt).getTime(); + const bt = (b.startedAt ?? b.createdAt).getTime(); + return bt - at; + }) + .slice(0, INCIDENTS_LIMIT); + + const position = Math.min(...components.map((c) => c.order)); + + return { + position, + providers: Array.from(providers.values()).sort((a, b) => a.order - b.order), + incidents, + }; +} diff --git a/packages/services/src/page-component/__tests__/external-component.test.ts b/packages/services/src/page-component/__tests__/external-component.test.ts new file mode 100644 index 00000000..ccb7bbbd --- /dev/null +++ b/packages/services/src/page-component/__tests__/external-component.test.ts @@ -0,0 +1,353 @@ +import { and, eq } from "@openstatus/db"; +import { + externalService, + externalServiceComponent, + page, + pageComponent, +} from "@openstatus/db/src/schema"; +import { expect } from "@std/expect"; +import { beforeAll, describe, test } from "@std/testing/bdd"; + +import { SEEDED_WORKSPACE_TEAM_ID } from "../../../test/fixtures"; +import { + expectAuditRow, + loadSeededWorkspace, + makeApiKeyCtx, + makeUserCtx, + withTestTransaction, +} from "../../../test/helpers"; +import type { DrizzleTx } from "../../context"; +import type { ServiceContext } from "../../context"; +import { ForbiddenError, ValidationError } from "../../errors"; +import { updatePageComponentOrder } from "../update-order"; + +const TEST_PREFIX = "svc-external-cmp-test"; + +let teamCtx: ServiceContext; + +beforeAll(async () => { + const team = await loadSeededWorkspace(SEEDED_WORKSPACE_TEAM_ID); + teamCtx = makeUserCtx(team, { userId: 1 }); +}); + +async function setup(tx: DrizzleTx) { + const suffix = crypto.randomUUID().slice(0, 8); + const pageRow = await tx + .insert(page) + .values({ + workspaceId: teamCtx.workspace.id, + title: `${TEST_PREFIX}-page`, + description: "test", + slug: `${TEST_PREFIX}-${suffix}`, + customDomain: "", + }) + .returning() + .get(); + + const svc = await tx + .insert(externalService) + .values({ + slug: `${TEST_PREFIX}-svc-${suffix}`, + name: "Test Provider", + url: "https://example.com", + statusPageUrl: "https://example.com/status", + provider: "atlassian-statuspage", + industry: ["saas"], + }) + .returning() + .get(); + + const cmp = await tx + .insert(externalServiceComponent) + .values({ + externalServiceId: svc.id, + upstreamComponentId: `u-${suffix}`, + slug: `comp-${suffix}`, + name: "Edge Network", + indicator: "none", + status: "operational", + }) + .returning() + .get(); + + return { pageId: pageRow.id, serviceId: svc.id, componentId: cmp.id }; +} + +describe("updatePageComponentOrder — external components", () => { + test("creates whole-service and component-level external rows", async () => { + await withTestTransaction(async (tx) => { + const ctx = { ...teamCtx, db: tx }; + const { pageId, serviceId, componentId } = await setup(tx); + + await updatePageComponentOrder({ + ctx, + input: { + pageId, + components: [ + { + order: 0, + name: "Test Provider", + type: "external", + externalServiceId: serviceId, + }, + { + order: 1, + name: "Edge Network", + type: "external", + externalServiceId: serviceId, + externalServiceComponentId: componentId, + }, + ], + groups: [], + }, + }); + + const rows = await tx + .select() + .from(pageComponent) + .where( + and( + eq(pageComponent.pageId, pageId), + eq(pageComponent.type, "external"), + ), + ) + .all(); + + expect(rows).toHaveLength(2); + const whole = rows.find((r) => r.externalServiceComponentId == null); + const specific = rows.find((r) => r.externalServiceComponentId != null); + expect(whole?.externalServiceId).toBe(serviceId); + expect(whole?.monitorId).toBeNull(); + expect(specific?.externalServiceComponentId).toBe(componentId); + + if (!whole) throw new Error("unreachable"); + await expectAuditRow({ + workspaceId: teamCtx.workspace.id, + action: "page_component.create", + entityType: "page_component", + entityId: whole.id, + db: tx, + }); + }); + }); + + test("rejects an unknown external service id", async () => { + await withTestTransaction(async (tx) => { + const { pageId } = await setup(tx); + await expect( + updatePageComponentOrder({ + ctx: { ...teamCtx, db: tx }, + input: { + pageId, + components: [ + { + order: 0, + name: "Ghost", + type: "external", + externalServiceId: 999_999_999, + }, + ], + groups: [], + }, + }), + ).rejects.toBeInstanceOf(ForbiddenError); + }); + }); + + test("rejects a component that belongs to another service", async () => { + await withTestTransaction(async (tx) => { + const { pageId, componentId } = await setup(tx); + const other = await tx + .insert(externalService) + .values({ + slug: `${TEST_PREFIX}-other-${crypto.randomUUID().slice(0, 8)}`, + name: "Other", + url: "https://other.example.com", + statusPageUrl: "https://other.example.com/status", + provider: "atlassian-statuspage", + industry: ["saas"], + }) + .returning() + .get(); + + await expect( + updatePageComponentOrder({ + ctx: { ...teamCtx, db: tx }, + input: { + pageId, + components: [ + { + order: 0, + name: "Mismatched", + type: "external", + externalServiceId: other.id, + externalServiceComponentId: componentId, + }, + ], + groups: [], + }, + }), + ).rejects.toBeInstanceOf(ForbiddenError); + }); + }); + + test("rejects a static component carrying an external component id", async () => { + await withTestTransaction(async (tx) => { + const { pageId } = await setup(tx); + await expect( + updatePageComponentOrder({ + ctx: { ...teamCtx, db: tx }, + input: { + pageId, + components: [ + { + order: 0, + name: "Mixed", + type: "static", + externalServiceComponentId: 1, + }, + ], + groups: [], + }, + }), + ).rejects.toThrow(); + }); + }); + + test("rejects adding a new ref to a soft-deleted service", async () => { + await withTestTransaction(async (tx) => { + const { pageId } = await setup(tx); + const deleted = await tx + .insert(externalService) + .values({ + slug: `${TEST_PREFIX}-deleted-${crypto.randomUUID().slice(0, 8)}`, + name: "Deleted", + url: "https://deleted.example.com", + statusPageUrl: "https://deleted.example.com/status", + provider: "atlassian-statuspage", + industry: ["saas"], + deletedAt: new Date(), + }) + .returning() + .get(); + + await expect( + updatePageComponentOrder({ + ctx: { ...teamCtx, db: tx }, + input: { + pageId, + components: [ + { + order: 0, + name: "Dead", + type: "external", + externalServiceId: deleted.id, + }, + ], + groups: [], + }, + }), + ).rejects.toBeInstanceOf(ForbiddenError); + }); + }); + + test("allows re-saving an existing ref whose service was soft-deleted", async () => { + await withTestTransaction(async (tx) => { + const { pageId, serviceId } = await setup(tx); + const existing = await tx + .insert(pageComponent) + .values({ + workspaceId: teamCtx.workspace.id, + pageId, + type: "external", + name: "Provider", + order: 0, + externalServiceId: serviceId, + }) + .returning() + .get(); + + await tx + .update(externalService) + .set({ deletedAt: new Date() }) + .where(eq(externalService.id, serviceId)); + + await updatePageComponentOrder({ + ctx: { ...teamCtx, db: tx }, + input: { + pageId, + components: [ + { + id: existing.id, + order: 0, + name: "Provider", + type: "external", + externalServiceId: serviceId, + }, + ], + groups: [], + }, + }); + + const rows = await tx + .select() + .from(pageComponent) + .where( + and( + eq(pageComponent.pageId, pageId), + eq(pageComponent.type, "external"), + ), + ) + .all(); + expect(rows).toHaveLength(1); + }); + }); + + test("rejects duplicate external refs in one payload", async () => { + await withTestTransaction(async (tx) => { + const { pageId, serviceId } = await setup(tx); + await expect( + updatePageComponentOrder({ + ctx: { ...teamCtx, db: tx }, + input: { + pageId, + components: [ + { + order: 0, + name: "A", + type: "external", + externalServiceId: serviceId, + }, + { + order: 1, + name: "B", + type: "external", + externalServiceId: serviceId, + }, + ], + groups: [], + }, + }), + ).rejects.toBeInstanceOf(ValidationError); + }); + }); + + test("rejects read-only actor", async () => { + await withTestTransaction(async (tx) => { + const readOnlyCtx = { + ...makeApiKeyCtx(teamCtx.workspace, { + keyId: "k-read", + userId: 1, + scopes: ["read"], + }), + db: tx, + }; + await expect( + updatePageComponentOrder({ + ctx: readOnlyCtx, + input: { pageId: 1, components: [], groups: [] }, + }), + ).rejects.toBeInstanceOf(ForbiddenError); + }); + }); +}); diff --git a/packages/services/src/page-component/internal.ts b/packages/services/src/page-component/internal.ts index fa05e798..5aceb07b 100644 --- a/packages/services/src/page-component/internal.ts +++ b/packages/services/src/page-component/internal.ts @@ -1,5 +1,10 @@ import { and, eq, inArray, isNull } from "@openstatus/db"; -import { monitor, page } from "@openstatus/db/src/schema"; +import { + externalService, + externalServiceComponent, + monitor, + page, +} from "@openstatus/db/src/schema"; import type { DB } from "../context"; import { ForbiddenError } from "../errors"; @@ -51,3 +56,66 @@ export async function validateMonitorIds(args: { throw new ForbiddenError("Invalid monitor IDs."); } } + +export async function validateExternalRefs(args: { + tx: DB; + serviceIds: ReadonlyArray; + componentRefs: ReadonlyArray<{ serviceId: number; componentId: number }>; + // Newly-added service refs additionally must not be soft-deleted, so a fresh + // pick can't reference a removed provider. Existing refs only need to exist, + // so a provider soft-deleted after being added never blocks saving the page. + requireLiveServiceIds?: ReadonlyArray; +}): Promise { + const { tx, serviceIds, componentRefs, requireLiveServiceIds } = args; + + const ids = Array.from(new Set(serviceIds)); + if (ids.length > 0) { + const rows = await tx + .select({ id: externalService.id }) + .from(externalService) + .where(inArray(externalService.id, ids)) + .all(); + if (rows.length !== ids.length) { + throw new ForbiddenError("Invalid external service IDs."); + } + } + + const liveIds = Array.from(new Set(requireLiveServiceIds ?? [])); + if (liveIds.length > 0) { + const rows = await tx + .select({ id: externalService.id }) + .from(externalService) + .where( + and( + inArray(externalService.id, liveIds), + isNull(externalService.deletedAt), + ), + ) + .all(); + if (rows.length !== liveIds.length) { + throw new ForbiddenError("Invalid external service IDs."); + } + } + + const componentIds = Array.from( + new Set(componentRefs.map((r) => r.componentId)), + ); + if (componentIds.length > 0) { + const rows = await tx + .select({ + id: externalServiceComponent.id, + externalServiceId: externalServiceComponent.externalServiceId, + }) + .from(externalServiceComponent) + .where(inArray(externalServiceComponent.id, componentIds)) + .all(); + const serviceByComponent = new Map( + rows.map((r) => [r.id, r.externalServiceId]), + ); + for (const ref of componentRefs) { + if (serviceByComponent.get(ref.componentId) !== ref.serviceId) { + throw new ForbiddenError("Invalid external component IDs."); + } + } + } +} diff --git a/packages/services/src/page-component/schemas.ts b/packages/services/src/page-component/schemas.ts index 3ec13e47..185d6fc7 100644 --- a/packages/services/src/page-component/schemas.ts +++ b/packages/services/src/page-component/schemas.ts @@ -16,17 +16,38 @@ const componentInput = z .object({ id: z.number().int().optional(), monitorId: z.number().int().nullish(), + externalServiceId: z.number().int().nullish(), + externalServiceComponentId: z.number().int().nullish(), order: z.number().int(), name: z.string(), description: z.string().nullish(), - type: z.enum(["monitor", "static"]), + type: z.enum(["monitor", "static", "external"]), }) .refine( - (c) => (c.type === "monitor" ? c.monitorId != null : c.monitorId == null), + (c) => { + switch (c.type) { + case "monitor": + return ( + c.monitorId != null && + c.externalServiceId == null && + c.externalServiceComponentId == null + ); + case "static": + return ( + c.monitorId == null && + c.externalServiceId == null && + c.externalServiceComponentId == null + ); + case "external": + return c.monitorId == null && c.externalServiceId != null; + default: + return false; + } + }, { - path: ["monitorId"], + path: ["type"], message: - "Monitor components require a monitorId; static components must not set one.", + "monitor requires monitorId; static requires no refs; external requires externalServiceId.", }, ); diff --git a/packages/services/src/page-component/update-order.ts b/packages/services/src/page-component/update-order.ts index 7e5c5493..ba28f4f7 100644 --- a/packages/services/src/page-component/update-order.ts +++ b/packages/services/src/page-component/update-order.ts @@ -5,9 +5,15 @@ import { emitAudit } from "../audit"; import { requireScope } from "../auth"; import { type ServiceContext, withTransaction } from "../context"; import { LimitExceededError, ValidationError } from "../errors"; -import { assertPageInWorkspace, validateMonitorIds } from "./internal"; +import { + assertPageInWorkspace, + validateExternalRefs, + validateMonitorIds, +} from "./internal"; import { UpdatePageComponentOrderInput } from "./schemas"; +const isIdBased = (type: string) => type === "static" || type === "external"; + /** * Replace the full order/layout of a page's components and groups in one * transaction. Mirrors the pre-migration tRPC behaviour exactly — the @@ -16,7 +22,7 @@ import { UpdatePageComponentOrderInput } from "./schemas"; * 1. Validate the page is in the workspace. * 2. Enforce the `page-components` plan limit across the workspace. * 3. Validate every monitor id on the input set belongs to the workspace. - * 4. Delete removed monitor and static components. + * 4. Delete removed monitor, static, and external components. * 5. Clear `groupId` on all components (prevents FK errors before the * next step), then delete existing groups and recreate them. * 6. Upsert monitor components via the `(pageId, monitorId)` unique @@ -120,16 +126,49 @@ export async function updatePageComponentOrder(args: { monitorIds: inputMonitorIds, }); - const inputStaticComponentIds = [ - ...input.components - .filter((c) => c.type === "static" && c.id) - .map((c) => c.id), - ...input.groups.flatMap((g) => - g.components - .filter((c) => c.type === "static" && c.id) - .map((c) => c.id), - ), - ] as number[]; + const allInputComponents = [ + ...input.components, + ...input.groups.flatMap((g) => g.components), + ]; + const externalComponents = allInputComponents.filter( + (c): c is typeof c & { externalServiceId: number } => + c.type === "external" && c.externalServiceId != null, + ); + const externalKey = (c: { + externalServiceId?: number | null; + externalServiceComponentId?: number | null; + }) => `${c.externalServiceId}:${c.externalServiceComponentId ?? "all"}`; + + // Reject duplicate external refs in one payload — a crafted request could + // bypass the client guard and create two rows for the same provider/component + // (NULLs make a partial unique index unable to catch the whole-service case). + const inputExternalKeys = externalComponents.map(externalKey); + if (new Set(inputExternalKeys).size !== inputExternalKeys.length) { + throw new ValidationError("Duplicate external component in input."); + } + + const existingExternalKeys = new Set( + existingComponents + .filter((c) => c.type === "external") + .map((c) => externalKey(c)), + ); + await validateExternalRefs({ + tx, + serviceIds: externalComponents.map((c) => c.externalServiceId), + componentRefs: externalComponents + .filter((c) => c.externalServiceComponentId != null) + .map((c) => ({ + serviceId: c.externalServiceId, + componentId: c.externalServiceComponentId as number, + })), + requireLiveServiceIds: externalComponents + .filter((c) => !existingExternalKeys.has(externalKey(c))) + .map((c) => c.externalServiceId), + }); + + const inputIdBasedComponentIds = allInputComponents + .filter((c) => isIdBased(c.type) && c.id) + .map((c) => c.id) as number[]; // Guardrail against mass-delete via id-loss. If the input has any // static components *and* none of them carry ids, the diff below @@ -141,19 +180,19 @@ export async function updatePageComponentOrder(args: { // static entries, which still runs through the normal diff — this // guard only catches the ambiguous "new statics alongside existing // ones, but the client forgot to round-trip the existing ids" case. - const inputHasAnyStatic = - input.components.some((c) => c.type === "static") || - input.groups.some((g) => g.components.some((c) => c.type === "static")); - const hasExistingStatics = existingComponents.some( - (c) => c.type === "static", + const inputHasAnyIdBased = allInputComponents.some((c) => + isIdBased(c.type), + ); + const hasExistingIdBased = existingComponents.some((c) => + isIdBased(c.type), ); if ( - inputHasAnyStatic && - inputStaticComponentIds.length === 0 && - hasExistingStatics + inputHasAnyIdBased && + inputIdBasedComponentIds.length === 0 && + hasExistingIdBased ) { throw new ValidationError( - "Existing static components must round-trip their ids.", + "Existing static/external components must round-trip their ids.", ); } @@ -175,18 +214,18 @@ export async function updatePageComponentOrder(args: { // with `ON DELETE CASCADE`, so recreate-on-each-save would wipe every // subscriber scope / active maintenance / status-report association // the moment a static came in without its id. - const removedStaticComponents = existingComponents.filter( - (c) => c.type === "static" && !inputStaticComponentIds.includes(c.id), + const removedIdBasedComponents = existingComponents.filter( + (c) => isIdBased(c.type) && !inputIdBasedComponentIds.includes(c.id), ); const removedComponentIds = [ ...removedMonitorComponents.map((c) => c.id), - ...removedStaticComponents.map((c) => c.id), + ...removedIdBasedComponents.map((c) => c.id), ]; const removedComponents = [ ...removedMonitorComponents, - ...removedStaticComponents, + ...removedIdBasedComponents, ]; if (removedComponentIds.length > 0) { await tx @@ -330,6 +369,8 @@ export async function updatePageComponentOrder(args: { description: c.description, type: c.type, monitorId: c.monitorId, + externalServiceId: c.externalServiceId ?? null, + externalServiceComponentId: c.externalServiceComponentId ?? null, order: g.order, groupId: newGroups[i]?.id, groupOrder: c.order, @@ -344,6 +385,8 @@ export async function updatePageComponentOrder(args: { description: c.description, type: c.type, monitorId: c.monitorId, + externalServiceId: c.externalServiceId ?? null, + externalServiceComponentId: c.externalServiceComponentId ?? null, order: c.order, groupId: null as number | null, groupOrder: null as number | null, @@ -357,8 +400,8 @@ export async function updatePageComponentOrder(args: { const monitorComponents = allComponentValues.filter( (c) => c.type === "monitor" && c.monitorId, ); - const staticComponents = allComponentValues.filter( - (c) => c.type === "static", + const idBasedComponents = allComponentValues.filter((c) => + isIdBased(c.type), ); // Use the `(pageId, monitorId)` unique constraint to preserve ids. @@ -429,14 +472,14 @@ export async function updatePageComponentOrder(args: { // on the stale pre-delete snapshot, take the UPDATE branch, and // silently no-op — the new static never gets inserted. const removedIdSet = new Set(removedComponentIds); - const existingStaticById = new Map( + const existingIdBasedById = new Map( existingComponents - .filter((c) => c.type === "static" && !removedIdSet.has(c.id)) + .filter((c) => isIdBased(c.type) && !removedIdSet.has(c.id)) .map((c) => [c.id, c]), ); - for (const c of staticComponents) { - const before = c.id ? existingStaticById.get(c.id) : undefined; + for (const c of idBasedComponents) { + const before = c.id ? existingIdBasedById.get(c.id) : undefined; if (before) { const [after] = await tx .update(pageComponent) @@ -445,6 +488,8 @@ export async function updatePageComponentOrder(args: { description: c.description, type: c.type, monitorId: c.monitorId, + externalServiceId: c.externalServiceId, + externalServiceComponentId: c.externalServiceComponentId, order: c.order, groupId: c.groupId, groupOrder: c.groupOrder, @@ -459,7 +504,7 @@ export async function updatePageComponentOrder(args: { ) .returning(); if (!after) { - throw new Error("Failed to update static component"); + throw new Error("Failed to update static/external component"); } await emitAudit(tx, ctx, { @@ -479,13 +524,15 @@ export async function updatePageComponentOrder(args: { description: c.description, type: c.type, monitorId: c.monitorId, + externalServiceId: c.externalServiceId, + externalServiceComponentId: c.externalServiceComponentId, order: c.order, groupId: c.groupId, groupOrder: c.groupOrder, }) .returning(); if (!created) { - throw new Error("Failed to insert static component"); + throw new Error("Failed to insert static/external component"); } await emitAudit(tx, ctx, {