diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/LoadMoreSentinel/LoadMoreSentinel.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/LoadMoreSentinel/LoadMoreSentinel.tsx index cfda207cea3..a11daad01f1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/LoadMoreSentinel/LoadMoreSentinel.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/LoadMoreSentinel/LoadMoreSentinel.tsx @@ -1,6 +1,5 @@ -import { Trans } from "@lingui/react/macro"; import type { RefObject } from "react"; -import { LuRefreshCw } from "react-icons/lu"; +import { LuLoader } from "react-icons/lu"; interface LoadMoreSentinelProps { sentinelRef: RefObject; @@ -24,12 +23,7 @@ export function LoadMoreSentinel({ className="flex items-center justify-center gap-2 py-3 text-xs text-muted-foreground" > {isFetchingNextPage && ( - <> - - - Loading more… - - + )} ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/hooks/usePagesList/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/hooks/usePagesList/index.ts new file mode 100644 index 00000000000..5c7c2b172a4 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/hooks/usePagesList/index.ts @@ -0,0 +1 @@ +export { usePagesList } from "./usePagesList"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/hooks/usePagesList/usePagesList.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/hooks/usePagesList/usePagesList.ts new file mode 100644 index 00000000000..37caeb84f89 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/hooks/usePagesList/usePagesList.ts @@ -0,0 +1,63 @@ +import { useEffect, useMemo, useRef } from "react"; +import { cloudTrpc } from "renderer/lib/cloud-trpc"; +import { + type PagesListFilter, + pagesListInput, +} from "renderer/routes/_authenticated/_dashboard/utils/pagesListInput"; + +/** + * One batch per scroll. Attach `scrollRef` to the scroll container and + * `sentinelRef` to an element after the last row; nothing fetches ahead of + * what the viewer has reached. + */ +export function usePagesList( + filter: PagesListFilter = {}, + options: { enabled?: boolean; staleTime?: number } = {}, +) { + const query = cloudTrpc.page.list.useInfiniteQuery(pagesListInput(filter), { + getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, + ...options, + }); + + const { + hasNextPage, + isFetchingNextPage, + isFetchNextPageError, + fetchNextPage, + } = query; + + const scrollRef = useRef(null); + const sentinelRef = useRef(null); + + useEffect(() => { + const sentinel = sentinelRef.current; + const root = scrollRef.current; + // React Query derives `hasNextPage` from the last successful fetch, so a + // failed batch leaves it true — observing again would spin. The caller + // offers a retry instead. + if ( + !sentinel || + !root || + !hasNextPage || + isFetchingNextPage || + isFetchNextPageError + ) { + return; + } + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) void fetchNextPage(); + }, + { root, rootMargin: "200px" }, + ); + observer.observe(sentinel); + return () => observer.disconnect(); + }, [hasNextPage, isFetchingNextPage, isFetchNextPageError, fetchNextPage]); + + const items = useMemo( + () => query.data?.pages.flatMap((page) => page.items) ?? [], + [query.data], + ); + + return { ...query, items, scrollRef, sentinelRef }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesGrid/PagesGrid.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesGrid/PagesGrid.tsx index d937943b152..2bc0c6a62e1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesGrid/PagesGrid.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesGrid/PagesGrid.tsx @@ -76,8 +76,8 @@ export function PagesGrid({ if (pages.length === 0) { return ( - - + + {hasFilters ? ( @@ -103,7 +103,12 @@ export function PagesGrid({ {!hasFilters && ( - diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/PagesView.test.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/PagesView.test.tsx new file mode 100644 index 00000000000..c39578402cf --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/PagesView.test.tsx @@ -0,0 +1,403 @@ +import { + afterAll, + afterEach, + beforeEach, + describe, + expect, + mock, + test, +} from "bun:test"; +import { GlobalRegistrator } from "@happy-dom/global-registrator"; + +// happy-dom is process-wide; unregister in afterAll so the shared mock +// document is restored for the other renderer suites. +const alreadyRegistered = GlobalRegistrator.isRegistered; +if (!alreadyRegistered) GlobalRegistrator.register(); +( + globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } +).IS_REACT_ACT_ENVIRONMENT = true; + +interface ListPage { + items: Array<{ + id: string; + title: string; + slug: string; + visibility: string; + }>; + nextCursor: string | null; +} + +interface InfiniteResult { + data?: { pages: ListPage[] }; + error?: { message: string } | null; + isPending: boolean; + hasNextPage: boolean; + isFetchingNextPage: boolean; + isFetchNextPageError: boolean; + fetchNextPage: () => void; +} + +const fetchNextPage = mock(() => {}); + +let listResult: InfiniteResult; +let listInput: Record | undefined; +let countsInput: Record | undefined; +let countsData: { + all: number; + team: number; + mine: number; + pinned: number; + workspaces: Array<{ workspaceId: string; count: number }>; + authors: Array<{ + userId: string | null; + name: string | null; + image: string | null; + count: number; + }>; +}; + +/** Lets a test fire the sentinel without a real viewport. */ +const observed: Array<() => void> = []; +class TestIntersectionObserver { + constructor( + private callback: (entries: Array<{ isIntersecting: boolean }>) => void, + ) {} + observe() { + observed.push(() => this.callback([{ isIntersecting: true }])); + } + disconnect() {} +} +(globalThis as { IntersectionObserver?: unknown }).IntersectionObserver = + TestIntersectionObserver; + +function page(id: string, visibility = "org") { + return { id, title: `Page ${id}`, slug: `page-${id}`, visibility }; +} + +mock.module("renderer/lib/cloud-trpc", () => ({ + cloudTrpc: { + page: { + list: { + useInfiniteQuery: (input: Record) => { + listInput = input; + return listResult; + }, + }, + counts: { + useQuery: (input: Record) => { + countsInput = input; + return { data: countsData }; + }, + }, + delete: { useMutation: () => ({ mutateAsync: mock() }) }, + }, + useUtils: () => ({ + page: { + list: { + cancel: mock(async () => {}), + getInfiniteData: mock(() => undefined), + setInfiniteData: mock(), + invalidate: mock(), + }, + counts: { invalidate: mock() }, + }, + }), + }, +})); + +mock.module("renderer/lib/auth-client", () => ({ + authClient: { useSession: () => ({ data: null }) }, +})); + +// The grid pulls in page cards, thumbnails and the whole UI kit. This suite is +// about what PagesView hands it, so record the props instead of rendering them. +let gridProps: { pages: unknown[]; error?: string; isPending: boolean }; +mock.module("../PagesGrid", () => ({ + PagesGrid: (props: typeof gridProps) => { + gridProps = props; + return null; + }, +})); + +const onScopeChange = mock((_scope: string) => {}); +const onSearchChange = mock((_search: string) => {}); + +mock.module("./hooks/useCreatePageWithAgent", () => ({ + useCreatePageWithAgent: () => ({ + creatingWithAgent: false, + handleCreateWithAgent: mock(), + }), +})); + +const PINS = ["pin-1", "pin-2"]; +mock.module( + "renderer/routes/_authenticated/_dashboard/hooks/usePageFavorites", + () => ({ + usePageFavorites: () => ({ + favoritePageIds: PINS, + favoritePageIdSet: new Set(PINS), + toggleFavorite: mock(), + }), + }), +); + +mock.module( + "renderer/routes/_authenticated/_dashboard/hooks/useOpenPage", + () => ({ + useOpenPage: () => mock(), + isPaneModifier: () => false, + }), +); + +mock.module("../../hooks/usePageWorkspaceNames", () => ({ + usePageWorkspaceNames: () => new Map([["ws-1", "Workspace One"]]), +})); + +const { act, cleanup, fireEvent, render } = await import( + "@testing-library/react" +); +const { PagesView } = await import("./PagesView"); + +function renderView({ + scope = "all" as "all" | "pinned" | "team" | "mine", + search = "", + authorId = null as string | null, + workspaceId = null as string | null, +} = {}) { + return render( + , + ); +} + +beforeEach(() => { + fetchNextPage.mockClear(); + onScopeChange.mockClear(); + onSearchChange.mockClear(); + observed.length = 0; + listInput = undefined; + countsInput = undefined; + countsData = { + all: 1, + team: 1, + mine: 0, + pinned: 2, + workspaces: [{ workspaceId: "ws-1", count: 3 }], + authors: [ + { userId: "user-1", name: "Ada", image: null, count: 2 }, + { userId: "user-2", name: "Grace", image: null, count: 1 }, + ], + }; + listResult = { + data: { pages: [{ items: [page("a")], nextCursor: null }] }, + error: null, + isPending: false, + hasNextPage: false, + isFetchingNextPage: false, + isFetchNextPageError: false, + fetchNextPage, + }; +}); + +afterEach(cleanup); +afterAll(async () => { + if (!alreadyRegistered) await GlobalRegistrator.unregister(); +}); + +describe("PagesView server-side filtering", () => { + test("sends the search to the server instead of filtering what it loaded", async () => { + await act(async () => { + renderView({ search: "report" }); + }); + expect(listInput).toMatchObject({ search: "report" }); + }); + + test("sends the scope, the author and the workspace too", async () => { + await act(async () => { + renderView({ scope: "team", authorId: "user-1", workspaceId: "ws-1" }); + }); + expect(listInput).toMatchObject({ + scope: "team", + authorId: "user-1", + workspaceId: "ws-1", + }); + }); + + test("asks for the pinned tab by id, because pins are not a server column", async () => { + await act(async () => { + renderView({ scope: "pinned" }); + }); + expect(listInput).toMatchObject({ scope: "all", ids: PINS }); + }); + + test("asks for one batch, not the whole organization", async () => { + await act(async () => { + renderView(); + }); + expect(listInput?.limit).toBe(48); + }); +}); + +describe("PagesView loads on scroll", () => { + test("does not sweep the organization on mount", async () => { + listResult.hasNextPage = true; + await act(async () => { + renderView(); + }); + expect(fetchNextPage).not.toHaveBeenCalled(); + }); + + test("fetches the next batch when the sentinel comes into view", async () => { + listResult.hasNextPage = true; + await act(async () => { + renderView(); + }); + expect(observed.length).toBeGreaterThan(0); + await act(async () => { + for (const fire of observed) fire(); + }); + expect(fetchNextPage).toHaveBeenCalled(); + }); + + test("does not observe while a batch is already in flight", async () => { + listResult.hasNextPage = true; + listResult.isFetchingNextPage = true; + await act(async () => { + renderView(); + }); + expect(observed).toHaveLength(0); + }); + + test("does not re-observe after a failed batch, which would spin", async () => { + listResult.hasNextPage = true; + listResult.isFetchNextPageError = true; + await act(async () => { + renderView(); + }); + expect(observed).toHaveLength(0); + }); +}); + +describe("PagesView counts", () => { + test("takes the tab counts from the server, not from what it loaded", async () => { + countsData.all = 42; + const view = await act(async () => renderView()); + const allTab = view.getByRole("tab", { name: /All/ }); + // One loaded page, 42 in the organization: the count is the server's. + expect(allTab.textContent).toContain("42"); + expect(gridProps.pages).toHaveLength(1); + expect(countsInput).toMatchObject({ pinnedIds: PINS }); + }); + + test("offers the workspace filter off the server breakdown", async () => { + const view = await act(async () => renderView()); + expect( + view.queryByRole("button", { name: /Filter by workspace/ }), + ).toBeTruthy(); + }); + + test("hides the workspace filter when no workspace has pages", async () => { + countsData.workspaces = []; + const view = await act(async () => renderView()); + expect( + view.queryByRole("button", { name: /Filter by workspace/ }), + ).toBeNull(); + }); + + test("does not narrow the counts by the scope being counted", async () => { + await act(async () => { + renderView({ scope: "mine" }); + }); + expect(countsInput).not.toHaveProperty("scope"); + }); +}); + +describe("PagesView search", () => { + test("does not strand typed text in the box when another filter changes", async () => { + const view = await act(async () => renderView()); + const box = view.getByPlaceholderText("Search pages") as HTMLInputElement; + + await act(async () => { + fireEvent.change(box, { target: { value: "quarterly" } }); + }); + expect(box.value).toBe("quarterly"); + + // Switching tabs inside the debounce window must not drop the pending + // search: the box would keep showing text the grid was not filtered by. + await act(async () => { + const tab = view.getByRole("tab", { name: /Team/ }); + fireEvent.pointerDown(tab, { button: 0, ctrlKey: false }); + fireEvent.mouseDown(tab, { button: 0, ctrlKey: false }); + fireEvent.click(tab); + }); + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 400)); + }); + + expect(onScopeChange).toHaveBeenCalledWith("team"); + expect(onSearchChange).toHaveBeenCalledWith("quarterly"); + }); +}); + +describe("PagesView empty state", () => { + test("does not blank a loaded grid while the counts are still in flight", async () => { + countsData = undefined as never; + const view = await act(async () => renderView()); + expect(gridProps.pages).toHaveLength(1); + expect(view.queryByRole("tab", { name: /All/ })).toBeTruthy(); + }); + + test("keeps the tabs but drops their counts when the organization has no pages", async () => { + listResult.data = { pages: [{ items: [], nextCursor: null }] }; + countsData.all = 0; + const view = await act(async () => renderView()); + expect(view.getByRole("tab", { name: /All/ }).textContent).toBe("All"); + }); +}); + +describe("PagesView error surfacing", () => { + test("keeps the loaded pages on screen when a later batch fails", async () => { + listResult.hasNextPage = true; + listResult.isFetchNextPageError = true; + listResult.error = { message: "network down" }; + await act(async () => { + renderView(); + }); + expect(gridProps.error).toBeUndefined(); + expect(gridProps.pages).toHaveLength(1); + }); + + test("shows the error when the first batch failed and nothing loaded", async () => { + listResult.data = { pages: [] }; + listResult.error = { message: "network down" }; + await act(async () => { + renderView(); + }); + expect(gridProps.error).toBe("network down"); + }); +}); + +describe("PagesView pinned tab", () => { + test("stays on the pinned tab — the server already answered how many", async () => { + await act(async () => { + renderView({ scope: "pinned" }); + }); + expect(onScopeChange).not.toHaveBeenCalled(); + }); + + test("keeps the tab visible when the viewer is sitting on it with nothing pinned", async () => { + countsData.pinned = 0; + const view = await act(async () => renderView({ scope: "pinned" })); + expect(view.queryByText("Pinned")).toBeTruthy(); + expect(onScopeChange).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/PagesView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/PagesView.tsx index d56116d1ae3..9518f30ee3a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/PagesView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/PagesView.tsx @@ -3,24 +3,28 @@ import { COMPANY } from "@superset/shared/constants"; import { Input } from "@superset/ui/input"; import { toast } from "@superset/ui/sonner"; import { Tabs, TabsList, TabsTrigger } from "@superset/ui/tabs"; -import { useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { LuSearch } from "react-icons/lu"; import { authClient } from "renderer/lib/auth-client"; import { cloudTrpc } from "renderer/lib/cloud-trpc"; import { FeatureHeader } from "renderer/routes/_authenticated/_dashboard/components/FeatureHeader"; +import { LoadMoreSentinel } from "renderer/routes/_authenticated/_dashboard/components/LoadMoreSentinel"; +import { useDebouncedSearchNavigation } from "renderer/routes/_authenticated/_dashboard/hooks/useDebouncedSearchNavigation"; import { isPaneModifier, useOpenPage, } from "renderer/routes/_authenticated/_dashboard/hooks/useOpenPage"; import { usePageFavorites } from "renderer/routes/_authenticated/_dashboard/hooks/usePageFavorites"; -import { - filterPages, - matchesScope, - type PageScope, - sortPinnedFirst, -} from "../../utils/filterPages"; +import { usePagesList } from "renderer/routes/_authenticated/_dashboard/hooks/usePagesList"; +import { pagesListInput } from "renderer/routes/_authenticated/_dashboard/utils/pagesListInput"; +import { usePageWorkspaceNames } from "../../hooks/usePageWorkspaceNames"; +import { type PageScope, serverScope } from "../../utils/pageScope"; import { PagesGrid } from "../PagesGrid"; import { AuthorFilter, type PageAuthorOption } from "./components/AuthorFilter"; +import { + type PageWorkspaceOption, + WorkspaceFilter, +} from "./components/WorkspaceFilter"; import { useCreatePageWithAgent } from "./hooks/useCreatePageWithAgent"; const TABS: Array<{ value: PageScope }> = [ @@ -34,42 +38,109 @@ interface PagesViewProps { search: string; scope: PageScope; authorId: string | null; + workspaceId: string | null; onSearchChange: (search: string) => void; onScopeChange: (scope: PageScope) => void; onAuthorChange: (authorId: string | null) => void; + onWorkspaceChange: (workspaceId: string | null) => void; } export function PagesView({ search, scope, authorId, + workspaceId, onSearchChange, onScopeChange, onAuthorChange, + onWorkspaceChange, }: PagesViewProps) { const { t } = useLingui(); const { creatingWithAgent, handleCreateWithAgent } = useCreatePageWithAgent(); const { data: session } = authClient.useSession(); const utils = cloudTrpc.useUtils(); - const pages = cloudTrpc.page.list.useQuery({}); + const { favoritePageIds, favoritePageIdSet, toggleFavorite } = + usePageFavorites(); + const openPage = useOpenPage(); + + // The input answers the keystroke; the URL — and so the query — settles. + // + // A pending search is deliberately never cancelled when another filter + // changes: every handler here updates the URL through a functional updater, + // so a search landing after a scope change composes with it instead of + // clobbering it. Cancelling would strand the typed text in the box while the + // grid stayed unfiltered. + const [searchInput, setSearchInput] = useState(search); + useEffect(() => setSearchInput(search), [search]); + const { scheduleSearchNavigation } = + useDebouncedSearchNavigation(onSearchChange); + const handleSearchChange = useCallback( + (value: string) => { + setSearchInput(value); + scheduleSearchNavigation(value); + }, + [scheduleSearchNavigation], + ); + + const filter = useMemo( + () => ({ + ...(search ? { search } : {}), + scope: serverScope(scope), + ...(authorId ? { authorId } : {}), + ...(workspaceId ? { workspaceId } : {}), + ...(scope === "pinned" ? { ids: favoritePageIds } : {}), + }), + [search, scope, authorId, workspaceId, favoritePageIds], + ); + + const pages = usePagesList(filter); + const { items, hasNextPage, isFetchingNextPage, scrollRef, sentinelRef } = + pages; + + const countsQuery = cloudTrpc.page.counts.useQuery({ + ...(search ? { search } : {}), + ...(authorId ? { authorId } : {}), + ...(workspaceId ? { workspaceId } : {}), + pinnedIds: favoritePageIds, + }); + const counts = useMemo( + () => ({ + all: countsQuery.data?.all ?? 0, + pinned: countsQuery.data?.pinned ?? 0, + team: countsQuery.data?.team ?? 0, + mine: countsQuery.data?.mine ?? 0, + }), + [countsQuery.data], + ); + + const listInput = useMemo(() => pagesListInput(filter), [filter]); const deletePage = cloudTrpc.page.delete.useMutation({ onMutate: async ({ id }) => { - await utils.page.list.cancel({}); - const previous = utils.page.list.getData({}); - utils.page.list.setData({}, (old) => - old?.filter((entry) => entry.id !== id), + await utils.page.list.cancel(listInput); + const previous = utils.page.list.getInfiniteData(listInput); + utils.page.list.setInfiniteData(listInput, (old) => + old + ? { + ...old, + pages: old.pages.map((page) => ({ + ...page, + items: page.items.filter((entry) => entry.id !== id), + })), + } + : old, ); return { previous }; }, onError: (_error, _variables, context) => { - if (context?.previous) utils.page.list.setData({}, context.previous); + if (context?.previous) { + utils.page.list.setInfiniteData(listInput, context.previous); + } }, onSettled: () => { - void utils.page.list.invalidate({}); + void utils.page.list.invalidate(listInput); + void utils.page.counts.invalidate(); }, }); - const { favoritePageIdSet, toggleFavorite } = usePageFavorites(); - const openPage = useOpenPage(); const tabLabels: Record = { all: t({ message: "All" }), @@ -78,145 +149,136 @@ export function PagesView({ mine: t({ message: "Just me" }), }; - const all = useMemo(() => pages.data ?? [], [pages.data]); - const currentUserId = session?.user.id; const authorOptions = useMemo(() => { - const byAuthor = new Map(); - for (const page of all) { - if (!page.createdByUserId || byAuthor.has(page.createdByUserId)) { - continue; - } - byAuthor.set(page.createdByUserId, { - userId: page.createdByUserId, - name: - page.ownerName || - t({ - message: "Unknown", - }), - image: page.ownerImage, - isCurrentUser: page.createdByUserId === currentUserId, + const rows = countsQuery.data?.authors ?? []; + return rows + .flatMap((row) => + row.userId + ? [ + { + userId: row.userId, + name: row.name || t({ message: "Unknown" }), + image: row.image, + isCurrentUser: row.userId === currentUserId, + }, + ] + : [], + ) + .sort((a, b) => { + if (a.isCurrentUser !== b.isCurrentUser) + return a.isCurrentUser ? -1 : 1; + return a.name.localeCompare(b.name); }); - } - return Array.from(byAuthor.values()).sort((a, b) => { - if (a.isCurrentUser !== b.isCurrentUser) return a.isCurrentUser ? -1 : 1; - return a.name.localeCompare(b.name); - }); - }, [all, currentUserId, t]); + }, [countsQuery.data, currentUserId, t]); - const counts = useMemo( - () => ({ - all: all.length, - pinned: all.filter((page) => favoritePageIdSet.has(page.id)).length, - team: all.filter((page) => matchesScope(page, "team", favoritePageIdSet)) - .length, - mine: all.filter((page) => matchesScope(page, "mine", favoritePageIdSet)) - .length, - }), - [all, favoritePageIdSet], + const workspaceNames = usePageWorkspaceNames(); + const workspaceOptions = useMemo( + () => + (countsQuery.data?.workspaces ?? []) + .filter((row) => workspaceNames.has(row.workspaceId)) + .map((row) => ({ + workspaceId: row.workspaceId, + name: workspaceNames.get(row.workspaceId) ?? row.workspaceId, + count: row.count, + })) + .sort((a, b) => a.name.localeCompare(b.name)), + [countsQuery.data, workspaceNames], ); const tabs = useMemo( - () => TABS.filter((tab) => tab.value !== "pinned" || counts.pinned > 0), - [counts.pinned], - ); - - const pinnedEmpty = scope === "pinned" && counts.pinned === 0; - const activeScope = pinnedEmpty ? "all" : scope; - - useEffect(() => { - if (pinnedEmpty) onScopeChange("all"); - }, [pinnedEmpty, onScopeChange]); - - const visible = useMemo( () => - sortPinnedFirst( - filterPages(all, { - search, - scope: activeScope, - pinnedPageIds: favoritePageIdSet, - authorId, - }), - favoritePageIdSet, + TABS.filter( + (tab) => + tab.value !== "pinned" || counts.pinned > 0 || scope === "pinned", ), - [all, search, activeScope, favoritePageIdSet, authorId], + [counts.pinned, scope], ); - const orgEmpty = !pages.isPending && !pages.error && all.length === 0; + const hasFilters = + Boolean(search.trim()) || + scope !== "all" || + authorId !== null || + workspaceId !== null; + // From the list, not the counts: those arrive on their own query, and a + // zero default while they are in flight would blank a loaded grid. + const orgEmpty = + !hasFilters && !pages.isPending && !pages.error && items.length === 0; return (
-
+
Pages} docsUrl={`${COMPANY.DOCS_URL}/pages`} onCreate={handleCreateWithAgent} isCreating={creatingWithAgent} - showCreate={!orgEmpty} /> - {!orgEmpty && ( -
- onScopeChange(value as PageScope)} - > - - {tabs.map((tab) => ( - - {tabLabels[tab.value]} +
+ onScopeChange(value as PageScope)} + > + + {tabs.map((tab) => ( + + {tabLabels[tab.value]} + {!orgEmpty && ( {counts[tab.value]} - - ))} - - - -
- {(authorOptions.length > 1 || authorId !== null) && ( - - )} -
- - onSearchChange(event.target.value)} - placeholder={t({ - message: "Search pages", - })} - className="h-8 pl-7 text-sm" - /> -
+ )} + + ))} + + + +
+ {(workspaceOptions.length > 0 || workspaceId !== null) && ( + + )} + {(authorOptions.length > 1 || authorId !== null) && ( + + )} +
+ + handleSearchChange(event.target.value)} + placeholder={t({ + message: "Search pages", + })} + className="h-8 pl-7 text-sm" + />
- )} +
openPage( page, @@ -233,6 +295,12 @@ export function PagesView({ ); }} /> + +
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/components/WorkspaceFilter/WorkspaceFilter.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/components/WorkspaceFilter/WorkspaceFilter.tsx new file mode 100644 index 00000000000..3f794a4e849 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/components/WorkspaceFilter/WorkspaceFilter.tsx @@ -0,0 +1,110 @@ +import { Trans, useLingui } from "@lingui/react/macro"; +import { Button } from "@superset/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@superset/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover"; +import { cn } from "@superset/ui/utils"; +import { Check, ChevronDown, LayoutGrid } from "lucide-react"; +import { useState } from "react"; + +export interface PageWorkspaceOption { + workspaceId: string; + name: string; + count: number; +} + +interface WorkspaceFilterProps { + value: string | null; + options: PageWorkspaceOption[]; + onChange: (value: string | null) => void; +} + +export function WorkspaceFilter({ + value, + options, + onChange, +}: WorkspaceFilterProps) { + const { t } = useLingui(); + const [open, setOpen] = useState(false); + + const selected = + options.find((option) => option.workspaceId === value) ?? null; + const label = selected + ? selected.name + : value + ? t({ message: "Unknown workspace" }) + : t({ message: "All workspaces" }); + + const select = (next: string | null) => { + onChange(next); + setOpen(false); + }; + + return ( + + + + + + + + + + No workspaces found. + + + select(null)} + > + + + All workspaces + + {value === null && ( + + )} + + {options.map((option) => ( + select(option.workspaceId)} + > + + {option.name} + + {option.count} + + {value === option.workspaceId && ( + + )} + + ))} + + + + + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/components/WorkspaceFilter/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/components/WorkspaceFilter/index.ts new file mode 100644 index 00000000000..d4447f614a8 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/components/PagesView/components/WorkspaceFilter/index.ts @@ -0,0 +1 @@ +export { type PageWorkspaceOption, WorkspaceFilter } from "./WorkspaceFilter"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/hooks/usePageWorkspaceNames/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/hooks/usePageWorkspaceNames/index.ts new file mode 100644 index 00000000000..1da5ecf470b --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/hooks/usePageWorkspaceNames/index.ts @@ -0,0 +1 @@ +export { usePageWorkspaceNames } from "./usePageWorkspaceNames"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/hooks/usePageWorkspaceNames/usePageWorkspaceNames.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/hooks/usePageWorkspaceNames/usePageWorkspaceNames.ts new file mode 100644 index 00000000000..55d1901c550 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/hooks/usePageWorkspaceNames/usePageWorkspaceNames.ts @@ -0,0 +1,29 @@ +import { useMemo } from "react"; +import { useCloudWorkspaces } from "renderer/hooks/useCloudWorkspaces"; +import { useHostWorkspaces } from "renderer/routes/_authenticated/providers/HostWorkspacesProvider"; + +/** + * Workspace names for the pages workspace filter, and nothing else. + * + * `page.list` returns workspace ids: names live in each host's SQLite and in + * `cloud_workspaces`, never anywhere the server could join them. Both sources + * here are already shared — the host fan-out runs once in its provider, the + * cloud list is one query — so this adds no fetching of its own. + */ +export function usePageWorkspaceNames(): Map { + const { workspaces: hostWorkspaces } = useHostWorkspaces(); + const { workspaces: cloudWorkspaces } = useCloudWorkspaces(); + + return useMemo(() => { + const names = new Map(); + for (const workspace of hostWorkspaces) { + names.set(workspace.id, workspace.name); + } + // Second, so a sandbox rename that landed in `cloud_workspaces` wins over + // a stale row a host is still serving. + for (const workspace of cloudWorkspaces ?? []) { + names.set(workspace.id, workspace.name); + } + return names; + }, [hostWorkspaces, cloudWorkspaces]); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/page.tsx index 8b524d68cc4..141fda910c5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/page.tsx @@ -3,9 +3,14 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useFeatureFlagEnabled } from "posthog-js/react"; import { Redirect } from "renderer/components/Redirect"; import { PagesView } from "./components/PagesView"; -import { isPageScope, type PageScope } from "./utils/filterPages"; +import { isPageScope, type PageScope } from "./utils/pageScope"; -export type PagesSearch = { q?: string; scope?: PageScope; author?: string }; +export type PagesSearch = { + q?: string; + scope?: PageScope; + author?: string; + workspace?: string; +}; export const Route = createFileRoute("/_authenticated/_dashboard/pages/")({ component: PagesPage, @@ -16,11 +21,15 @@ export const Route = createFileRoute("/_authenticated/_dashboard/pages/")({ typeof search.author === "string" && search.author ? search.author : undefined, + workspace: + typeof search.workspace === "string" && search.workspace + ? search.workspace + : undefined, }), }); function PagesPage() { - const { q, scope, author } = Route.useSearch(); + const { q, scope, author, workspace } = Route.useSearch(); const navigate = useNavigate({ from: Route.fullPath }); const isEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.PAGES); @@ -32,6 +41,7 @@ function PagesPage() { search={q ?? ""} scope={scope ?? "all"} authorId={author ?? null} + workspaceId={workspace ?? null} onSearchChange={(value) => navigate({ search: (prev) => ({ ...prev, q: value || undefined }), @@ -53,6 +63,12 @@ function PagesPage() { replace: true, }) } + onWorkspaceChange={(value) => + navigate({ + search: (prev) => ({ ...prev, workspace: value ?? undefined }), + replace: true, + }) + } /> ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/filterPages/filterPages.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/filterPages/filterPages.test.ts deleted file mode 100644 index 361a083c00c..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/filterPages/filterPages.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { - filterPages, - isPageScope, - matchesAuthor, - matchesScope, - matchesSearch, - sortPinnedFirst, -} from "./filterPages"; - -const page = ( - id: string, - title: string, - visibility: string, - description: string | null = null, - createdByUserId: string | null = null, -) => ({ - id, - title, - slug: title.toLowerCase().replace(/ /g, "-"), - visibility, - description, - createdByUserId, -}); - -const team = page("1", "Q3 Metrics", "org", null, "user-a"); -const mine = page("2", "Ingest Runbook", "just_me", "hookdeck notes", "user-b"); -const other = page("3", "Warning Tokens", "org"); -const published = page("4", "Launch Notes", "everyone", null, "user-a"); - -describe("isPageScope", () => { - it("accepts known scopes and rejects anything else", () => { - expect(isPageScope("all")).toBe(true); - expect(isPageScope("pinned")).toBe(true); - expect(isPageScope("nope")).toBe(false); - expect(isPageScope(undefined)).toBe(false); - }); -}); - -describe("matchesSearch", () => { - it("matches on title, slug, and description, case-insensitively", () => { - expect(matchesSearch(team, "q3")).toBe(true); - expect(matchesSearch(team, "q3-metrics")).toBe(true); - expect(matchesSearch(mine, "HOOKDECK")).toBe(true); - expect(matchesSearch(team, "hookdeck")).toBe(false); - }); - - it("treats blank and whitespace-only queries as no filter", () => { - expect(matchesSearch(team, "")).toBe(true); - expect(matchesSearch(team, " ")).toBe(true); - }); -}); - -describe("matchesScope", () => { - const pinned = new Set(["2"]); - - it("splits team and just-me by visibility", () => { - expect(matchesScope(team, "team", pinned)).toBe(true); - expect(matchesScope(mine, "team", pinned)).toBe(false); - expect(matchesScope(mine, "mine", pinned)).toBe(true); - expect(matchesScope(team, "mine", pinned)).toBe(false); - }); - - it("keeps a page shared with everyone in the team scope", () => { - expect(matchesScope(published, "team", pinned)).toBe(true); - expect(matchesScope(published, "mine", pinned)).toBe(false); - }); - - it("uses the pinned set rather than visibility for the pinned scope", () => { - expect(matchesScope(mine, "pinned", pinned)).toBe(true); - expect(matchesScope(team, "pinned", pinned)).toBe(false); - }); - - it("passes everything through for all", () => { - expect(matchesScope(team, "all", pinned)).toBe(true); - expect(matchesScope(mine, "all", new Set())).toBe(true); - }); -}); - -describe("matchesAuthor", () => { - it("passes everything through when no author is selected", () => { - expect(matchesAuthor(team, null)).toBe(true); - expect(matchesAuthor(other, null)).toBe(true); - }); - - it("matches only the selected author's pages", () => { - expect(matchesAuthor(team, "user-a")).toBe(true); - expect(matchesAuthor(mine, "user-a")).toBe(false); - }); - - it("excludes pages without a creator when an author is selected", () => { - expect(matchesAuthor(other, "user-a")).toBe(false); - }); -}); - -describe("filterPages", () => { - it("applies search and scope together", () => { - const result = filterPages([team, mine, other], { - search: "e", - scope: "team", - pinnedPageIds: new Set(), - }); - expect(result.map((p) => p.id)).toEqual(["1", "3"]); - }); - - it("returns nothing when the scope excludes every search hit", () => { - expect( - filterPages([team, mine], { - search: "hookdeck", - scope: "team", - pinnedPageIds: new Set(), - }), - ).toEqual([]); - }); - - it("applies the author filter alongside search and scope", () => { - const result = filterPages([team, mine, other], { - search: "", - scope: "all", - pinnedPageIds: new Set(), - authorId: "user-a", - }); - expect(result.map((p) => p.id)).toEqual(["1"]); - }); -}); - -describe("sortPinnedFirst", () => { - it("floats pinned pages while preserving relative order", () => { - const sorted = sortPinnedFirst([team, mine, other], new Set(["3"])); - expect(sorted.map((p) => p.id)).toEqual(["3", "1", "2"]); - }); - - it("does not mutate the input array", () => { - const input = [team, mine]; - sortPinnedFirst(input, new Set(["2"])); - expect(input.map((p) => p.id)).toEqual(["1", "2"]); - }); -}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/filterPages/filterPages.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/filterPages/filterPages.ts deleted file mode 100644 index ea73ff08672..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/filterPages/filterPages.ts +++ /dev/null @@ -1,82 +0,0 @@ -export const PAGE_SCOPES = ["all", "pinned", "team", "mine"] as const; - -export type PageScope = (typeof PAGE_SCOPES)[number]; - -export function isPageScope(value: unknown): value is PageScope { - return (PAGE_SCOPES as readonly unknown[]).includes(value); -} - -export interface FilterablePage { - id: string; - title: string; - slug: string; - visibility: string; - description?: string | null; - createdByUserId?: string | null; -} - -export function matchesSearch(page: FilterablePage, query: string): boolean { - const needle = query.trim().toLowerCase(); - if (!needle) return true; - if (page.title.toLowerCase().includes(needle)) return true; - if (page.slug.toLowerCase().includes(needle)) return true; - return Boolean(page.description?.toLowerCase().includes(needle)); -} - -export function matchesScope( - page: FilterablePage, - scope: PageScope, - pinnedPageIds: ReadonlySet, -): boolean { - switch (scope) { - case "pinned": - return pinnedPageIds.has(page.id); - case "team": - return page.visibility !== "just_me"; - case "mine": - return page.visibility === "just_me"; - default: - return true; - } -} - -export function matchesAuthor( - page: FilterablePage, - authorId: string | null, -): boolean { - if (!authorId) return true; - return page.createdByUserId === authorId; -} - -export function filterPages( - pages: T[], - { - search, - scope, - pinnedPageIds, - authorId = null, - }: { - search: string; - scope: PageScope; - pinnedPageIds: ReadonlySet; - authorId?: string | null; - }, -): T[] { - return pages.filter( - (page) => - matchesSearch(page, search) && - matchesScope(page, scope, pinnedPageIds) && - matchesAuthor(page, authorId), - ); -} - -export function sortPinnedFirst( - pages: T[], - pinnedPageIds: ReadonlySet, -): T[] { - return [...pages].sort((a, b) => { - const aPinned = pinnedPageIds.has(a.id) ? 0 : 1; - const bPinned = pinnedPageIds.has(b.id) ? 0 : 1; - return aPinned - bPinned; - }); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/filterPages/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/filterPages/index.ts deleted file mode 100644 index be55a469226..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/filterPages/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - type FilterablePage, - filterPages, - isPageScope, - matchesAuthor, - matchesScope, - matchesSearch, - PAGE_SCOPES, - type PageScope, - sortPinnedFirst, -} from "./filterPages"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/pageScope/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/pageScope/index.ts new file mode 100644 index 00000000000..bafc59a0537 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/pageScope/index.ts @@ -0,0 +1,6 @@ +export { + isPageScope, + PAGE_SCOPES, + type PageScope, + serverScope, +} from "./pageScope"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/pageScope/pageScope.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/pageScope/pageScope.test.ts new file mode 100644 index 00000000000..570bc1edd63 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/pageScope/pageScope.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test"; +import { isPageScope, PAGE_SCOPES, serverScope } from "./pageScope"; + +describe("isPageScope", () => { + test("accepts every tab", () => { + for (const scope of PAGE_SCOPES) expect(isPageScope(scope)).toBe(true); + }); + + test("rejects anything else", () => { + expect(isPageScope("archived")).toBe(false); + expect(isPageScope("")).toBe(false); + expect(isPageScope(undefined)).toBe(false); + expect(isPageScope(null)).toBe(false); + expect(isPageScope(1)).toBe(false); + }); +}); + +describe("serverScope", () => { + test("passes the scopes the server filters on straight through", () => { + expect(serverScope("all")).toBe("all"); + expect(serverScope("team")).toBe("team"); + expect(serverScope("mine")).toBe("mine"); + }); + + test("pinned is not a server scope — it narrows by id instead", () => { + expect(serverScope("pinned")).toBe("all"); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/pageScope/pageScope.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/pageScope/pageScope.ts new file mode 100644 index 00000000000..55b053c1d27 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pages/utils/pageScope/pageScope.ts @@ -0,0 +1,17 @@ +import type { PageListScope } from "@superset/trpc/page-schema"; + +/** + * The tabs. `pinned` is not a server scope: pins live in renderer storage, so + * the list asks for them by id instead of by a column. + */ +export const PAGE_SCOPES = ["all", "pinned", "team", "mine"] as const; + +export type PageScope = (typeof PAGE_SCOPES)[number]; + +export function isPageScope(value: unknown): value is PageScope { + return (PAGE_SCOPES as readonly unknown[]).includes(value); +} + +export function serverScope(scope: PageScope): PageListScope { + return scope === "pinned" ? "all" : scope; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/pagesListInput/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/pagesListInput/index.ts new file mode 100644 index 00000000000..5a001db5f20 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/pagesListInput/index.ts @@ -0,0 +1 @@ +export { type PagesListFilter, pagesListInput } from "./pagesListInput"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/pagesListInput/pagesListInput.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/pagesListInput/pagesListInput.ts new file mode 100644 index 00000000000..bcf3597b40e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/pagesListInput/pagesListInput.ts @@ -0,0 +1,19 @@ +import type { PageListScope } from "@superset/trpc/page-schema"; + +export const PAGES_PER_BATCH = 48; + +export interface PagesListFilter { + workspaceId?: string; + search?: string; + scope?: PageListScope; + authorId?: string; + ids?: string[]; + limit?: number; +} + +export function pagesListInput({ + limit = PAGES_PER_BATCH, + ...filter +}: PagesListFilter = {}) { + return { limit, ...filter }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspacePagesMenu/WorkspacePagesMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspacePagesMenu/WorkspacePagesMenu.tsx index 783dc98b19c..9323f63c816 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspacePagesMenu/WorkspacePagesMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspacePagesMenu/WorkspacePagesMenu.tsx @@ -12,6 +12,8 @@ import { useOpenPage, } from "renderer/routes/_authenticated/_dashboard/hooks/useOpenPage"; import { usePageFavorites } from "renderer/routes/_authenticated/_dashboard/hooks/usePageFavorites"; +import { usePagesList } from "renderer/routes/_authenticated/_dashboard/hooks/usePagesList"; +import { pagesListInput } from "renderer/routes/_authenticated/_dashboard/utils/pagesListInput"; import type { CreateNewAgentSession } from "../../hooks/useAgentSessionLauncher"; import { NewPageComposer } from "./components/NewPageComposer"; import { PagesMenuRow } from "./components/PagesMenuRow"; @@ -21,6 +23,8 @@ import { } from "./stores/pagesMenuSeenStore"; import { type MenuPage, selectMenuPages } from "./utils/selectMenuPages"; +const MENU_PAGE_LIMIT = 200; + interface WorkspacePagesMenuProps { workspaceId: string; onCreateNewAgentSession: CreateNewAgentSession; @@ -43,12 +47,24 @@ export function WorkspacePagesMenu({ const [open, setOpen] = useState(false); const [composing, setComposing] = useState(false); - const workspacePagesQuery = cloudTrpc.page.list.useQuery( - { workspaceId }, - { staleTime: 60_000 }, + // This menu orders by publish time, not creation time, so it takes one + // large batch rather than the grid's scroll-sized one. Built once because + // `invalidate` matches a cached query by its input — a different `limit` + // here than below and neither a publish nor opening the menu would refresh. + const workspaceFilter = useMemo( + () => ({ workspaceId, limit: MENU_PAGE_LIMIT }), + [workspaceId], + ); + const workspaceListInput = useMemo( + () => pagesListInput(workspaceFilter), + [workspaceFilter], ); - const orgPagesQuery = cloudTrpc.page.list.useQuery( - {}, + const workspacePagesQuery = usePagesList(workspaceFilter, { + staleTime: 60_000, + }); + // Only the pins, by id — this menu never needed the rest of the org. + const pinnedPagesQuery = usePagesList( + { ids: favoritePageIds, limit: MENU_PAGE_LIMIT }, { enabled: open && favoritePageIds.length > 0, staleTime: 60_000 }, ); @@ -57,25 +73,30 @@ export function WorkspacePagesMenu({ "page-watch:changed", workspaceId, useCallback(() => { - void utils.page.list.invalidate({ workspaceId }); - }, [utils, workspaceId]), + void utils.page.list.invalidate(workspaceListInput); + }, [utils, workspaceListInput]), ); const { workspace, pinned, hasNew } = useMemo( () => selectMenuPages({ - workspacePages: workspacePagesQuery.data ?? [], - orgPages: orgPagesQuery.data ?? [], + workspacePages: workspacePagesQuery.items, + orgPages: pinnedPagesQuery.items, favoritePageIds, seenAt, }), - [workspacePagesQuery.data, orgPagesQuery.data, favoritePageIds, seenAt], + [ + workspacePagesQuery.items, + pinnedPagesQuery.items, + favoritePageIds, + seenAt, + ], ); const handleOpenChange = (next: boolean) => { setOpen(next); if (next) { - void utils.page.list.invalidate({ workspaceId }); + void utils.page.list.invalidate(workspaceListInput); return; } setComposing(false); diff --git a/apps/mobile/screens/(authenticated)/pages/PagesScreen.tsx b/apps/mobile/screens/(authenticated)/pages/PagesScreen.tsx index 7b2796c9e73..e6a3622fbfd 100644 --- a/apps/mobile/screens/(authenticated)/pages/PagesScreen.tsx +++ b/apps/mobile/screens/(authenticated)/pages/PagesScreen.tsx @@ -2,27 +2,23 @@ import { LegendList } from "@legendapp/list/react-native"; import { useLingui } from "@lingui/react/macro"; import * as Haptics from "expo-haptics"; import { Stack, useRouter } from "expo-router"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { RefreshControl, View } from "react-native"; import { Spinner } from "@/components/ui/spinner"; import { Text } from "@/components/ui/text"; import { PageRow } from "./components/PageRow"; -import { NO_PAGES, type OrgPage, usePagesQuery } from "./hooks/usePages"; -import { matchesScope, usePagesFilterStore } from "./stores/pagesFilterStore"; +import { type OrgPage, usePagesQuery } from "./hooks/usePages"; +import { usePagesFilterStore } from "./stores/pagesFilterStore"; export function PagesScreen() { const { t } = useLingui(); const router = useRouter(); const [refreshing, setRefreshing] = useState(false); - const pages = usePagesQuery(); const scope = usePagesFilterStore((state) => state.scope); const hasHydrated = usePagesFilterStore((state) => state.hasHydrated); + const pages = usePagesQuery(scope); const offline = pages.status === "pending" && pages.fetchStatus === "paused"; - - const visible = useMemo( - () => (pages.data ?? NO_PAGES).filter((page) => matchesScope(page, scope)), - [pages.data, scope], - ); + const visible = pages.items; const onRefresh = useCallback(async () => { setRefreshing(true); @@ -68,6 +64,12 @@ export function PagesScreen() { extraData={renderItem} keyExtractor={(page) => page.id} renderItem={renderItem} + onEndReachedThreshold={0.5} + onEndReached={() => { + if (pages.hasNextPage && !pages.isFetchingNextPage) { + void pages.fetchNextPage(); + } + }} refreshControl={ } diff --git a/apps/mobile/screens/(authenticated)/pages/hooks/usePages/usePages.ts b/apps/mobile/screens/(authenticated)/pages/hooks/usePages/usePages.ts index f94ee75282f..606a8fba314 100644 --- a/apps/mobile/screens/(authenticated)/pages/hooks/usePages/usePages.ts +++ b/apps/mobile/screens/(authenticated)/pages/hooks/usePages/usePages.ts @@ -1,38 +1,85 @@ import type { RouterOutputs } from "@superset/trpc"; -import { type UseQueryResult, useQuery } from "@tanstack/react-query"; +import type { PageListScope } from "@superset/trpc/page-schema"; +import { + type UseQueryResult, + useInfiniteQuery, + useQuery, +} from "@tanstack/react-query"; +import { useMemo } from "react"; import { useSession } from "@/lib/auth/client"; import { apiClient } from "@/lib/trpc/client"; -export type OrgPage = RouterOutputs["page"]["list"][number]; +export type OrgPage = RouterOutputs["page"]["list"]["items"][number]; export type PulledPage = RouterOutputs["page"]["pull"]; export const NO_PAGES: OrgPage[] = []; -export function usePagesQuery(): UseQueryResult { +const PAGES_PER_REQUEST = 50; + +/** One batch per `onEndReached`, rather than every page up front. */ +export function usePagesQuery(scope: PageListScope = "all") { const { data: session } = useSession(); const organizationId = session?.session?.activeOrganizationId ?? null; - return useQuery({ - queryKey: ["cloud", "page", "list", organizationId], + const query = useInfiniteQuery({ + queryKey: ["cloud", "page", "list", organizationId, scope], enabled: organizationId !== null, - queryFn: () => apiClient.page.list.query({}), + initialPageParam: undefined as string | undefined, + queryFn: ({ pageParam, signal }) => + apiClient.page.list.query( + { + limit: PAGES_PER_REQUEST, + scope, + ...(pageParam ? { cursor: pageParam } : {}), + }, + { signal }, + ), + getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, staleTime: 30_000, }); + + const items = useMemo( + () => query.data?.pages.flatMap((page) => page.items) ?? NO_PAGES, + [query.data], + ); + + return { ...query, items }; } -export function useWorkspacePagesQuery( - workspaceId: string | null, -): UseQueryResult { +export function useWorkspacePagesQuery(workspaceId: string | null) { const { data: session } = useSession(); const organizationId = session?.session?.activeOrganizationId ?? null; - return useQuery({ - queryKey: ["cloud", "page", "list", organizationId, workspaceId], + const query = useInfiniteQuery({ + queryKey: [ + "cloud", + "page", + "list", + organizationId, + "workspace", + workspaceId, + ], enabled: organizationId !== null && workspaceId !== null, - queryFn: () => - apiClient.page.list.query({ workspaceId: workspaceId ?? "" }), + initialPageParam: undefined as string | undefined, + queryFn: ({ pageParam, signal }) => + apiClient.page.list.query( + { + limit: PAGES_PER_REQUEST, + workspaceId: workspaceId ?? "", + ...(pageParam ? { cursor: pageParam } : {}), + }, + { signal }, + ), + getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, staleTime: 30_000, }); + + const items = useMemo( + () => query.data?.pages.flatMap((page) => page.items) ?? NO_PAGES, + [query.data], + ); + + return { ...query, items }; } const PULLED_PAGE_STALE_MS = 5 * 60_000; diff --git a/apps/mobile/screens/(authenticated)/pages/stores/pagesFilterStore/index.ts b/apps/mobile/screens/(authenticated)/pages/stores/pagesFilterStore/index.ts index 87ba74095f2..642b19abecc 100644 --- a/apps/mobile/screens/(authenticated)/pages/stores/pagesFilterStore/index.ts +++ b/apps/mobile/screens/(authenticated)/pages/stores/pagesFilterStore/index.ts @@ -1,5 +1,4 @@ export { - matchesScope, PAGE_SCOPES, type PageScope, usePagesFilterStore, diff --git a/apps/mobile/screens/(authenticated)/pages/stores/pagesFilterStore/pagesFilterStore.ts b/apps/mobile/screens/(authenticated)/pages/stores/pagesFilterStore/pagesFilterStore.ts index c3e57057b1f..03a4ce519d6 100644 --- a/apps/mobile/screens/(authenticated)/pages/stores/pagesFilterStore/pagesFilterStore.ts +++ b/apps/mobile/screens/(authenticated)/pages/stores/pagesFilterStore/pagesFilterStore.ts @@ -27,17 +27,3 @@ export const usePagesFilterStore = create()( }, ), ); - -export function matchesScope( - page: { visibility: string }, - scope: PageScope, -): boolean { - switch (scope) { - case "team": - return page.visibility !== "just_me"; - case "mine": - return page.visibility === "just_me"; - default: - return true; - } -} diff --git a/apps/mobile/screens/(authenticated)/workspace/[id]/actions/components/WorkspacePages/WorkspacePages.tsx b/apps/mobile/screens/(authenticated)/workspace/[id]/actions/components/WorkspacePages/WorkspacePages.tsx index 474e5c3be26..b2946213eb5 100644 --- a/apps/mobile/screens/(authenticated)/workspace/[id]/actions/components/WorkspacePages/WorkspacePages.tsx +++ b/apps/mobile/screens/(authenticated)/workspace/[id]/actions/components/WorkspacePages/WorkspacePages.tsx @@ -6,7 +6,6 @@ import { Pressable, View } from "react-native"; import { Icon } from "@/components/ui/icon"; import { Text } from "@/components/ui/text"; import { - NO_PAGES, type OrgPage, useWorkspacePagesQuery, } from "@/screens/(authenticated)/pages/hooks/usePages"; @@ -55,7 +54,7 @@ export function WorkspacePages({ workspaceId: string | null; }) { const pages = useWorkspacePagesQuery(workspaceId); - const items = pages.data ?? NO_PAGES; + const items = pages.items; if (items.length === 0) return null; diff --git a/packages/cli-framework/src/output.test.ts b/packages/cli-framework/src/output.test.ts index b0c676bfbfc..1ff6a05a5f6 100644 --- a/packages/cli-framework/src/output.test.ts +++ b/packages/cli-framework/src/output.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { table } from "./output"; +import { formatOutput, table } from "./output"; const URL = "http://localhost:3000/page/schema-history-superset-7njjhq"; const ROWS = [{ title: "Schema history", url: URL }]; @@ -93,3 +93,44 @@ describe("table", () => { expect(out).not.toContain("\x1b]8;;"); }); }); + +describe("formatOutput --quiet", () => { + test("lists the ids inside a paginated envelope", () => { + const out = formatOutput( + { data: { items: [{ id: "a" }, { id: "b" }], nextCursor: "tok" } }, + undefined, + { json: false, quiet: true }, + ); + expect(out).toBe("a\nb"); + }); + + test("still lists the ids of a bare array", () => { + const out = formatOutput([{ id: "a" }, { id: "b" }], undefined, { + json: false, + quiet: true, + }); + expect(out).toBe("a\nb"); + }); + + test("leaves an object that merely has items alone", () => { + const out = formatOutput({ items: [{ id: "a" }] }, undefined, { + json: false, + quiet: true, + }); + expect(out).toBe(JSON.stringify({ items: [{ id: "a" }] })); + }); +}); + +describe("formatOutput --json", () => { + test("hands the cursor to the caller alongside the rows", () => { + const out = formatOutput( + { data: { items: [{ id: "a" }], nextCursor: "tok" } }, + undefined, + { json: true, quiet: false }, + ); + expect(JSON.parse(out)).toEqual({ + items: [{ id: "a" }], + nextCursor: "tok", + }); + }); +}); diff --git a/packages/cli-framework/src/output.ts b/packages/cli-framework/src/output.ts index 24bc19c291c..37d4e0a5734 100644 --- a/packages/cli-framework/src/output.ts +++ b/packages/cli-framework/src/output.ts @@ -45,6 +45,8 @@ function isResultWithMessage(result: unknown): result is { message: string } { } function extractIds(data: unknown): string { + if (isPaginatedEnvelope(data)) return extractIds(data.items); + if (Array.isArray(data)) { return data .map((item) => { @@ -63,6 +65,16 @@ function extractIds(data: unknown): string { return JSON.stringify(data); } +function isPaginatedEnvelope(data: unknown): data is { items: unknown[] } { + return ( + typeof data === "object" && + data !== null && + "items" in data && + "nextCursor" in data && + Array.isArray((data as { items: unknown }).items) + ); +} + const URL_CELL = /^https?:\/\/\S+$/; const supportsHyperlinks = () => diff --git a/packages/cli/src/commands/pages/comments/list/command.ts b/packages/cli/src/commands/pages/comments/list/command.ts index 97d3c0e173a..7213b56368f 100644 --- a/packages/cli/src/commands/pages/comments/list/command.ts +++ b/packages/cli/src/commands/pages/comments/list/command.ts @@ -46,23 +46,13 @@ export default command({ ...activatedOnly, })) as unknown as Thread[]; } else { - const pages = await ctx.api.page.list.query( - options.workspace ? { workspaceId: options.workspace } : undefined, - ); - const perPage = await mapWithConcurrency(pages, 8, async (page) => { - const rows = (await ctx.api.pageComment.list.query({ - pageId: page.id, - ...activatedOnly, - })) as unknown as Thread[]; - return rows.map((row) => ({ - ...row, - pageTitle: page.title, - pageSlug: page.slug, - })); - }); - threads = perPage - .flat() - .sort((a, b) => stamp(a.createdAt) - stamp(b.createdAt)); + // One org-scoped query. Asking page by page was a call per page in the + // organization, and it grew with the org rather than with the comments. + threads = (await ctx.api.pageComment.listForOrganization.query({ + ...(options.workspace ? { workspaceId: options.workspace } : {}), + ...activatedOnly, + ...(options.unresolved ? { unresolvedOnly: true } : {}), + })) as unknown as Thread[]; } if (options.unresolved) { @@ -111,30 +101,6 @@ export default command({ }, }); -function stamp(value: string | Date): number { - return value instanceof Date ? value.getTime() : Date.parse(value); -} - -async function mapWithConcurrency( - items: readonly T[], - limit: number, - work: (item: T) => Promise, -): Promise { - const results = new Array(items.length); - let next = 0; - const workers = Array.from( - { length: Math.min(limit, items.length) }, - async () => { - while (next < items.length) { - const index = next++; - results[index] = await work(items[index] as T); - } - }, - ); - await Promise.all(workers); - return results; -} - function indent(text: string, prefix: string): string { return text .split("\n") diff --git a/packages/cli/src/commands/pages/list/command.test.ts b/packages/cli/src/commands/pages/list/command.test.ts new file mode 100644 index 00000000000..9e5be7eeb73 --- /dev/null +++ b/packages/cli/src/commands/pages/list/command.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, test } from "bun:test"; +import { nameLinks, workspaceCell } from "./command"; + +const WORKSPACE_A = "11111111-1111-4111-8111-111111111111"; +const WORKSPACE_B = "22222222-2222-4222-8222-222222222222"; + +type Link = { workspaceId: string; entryPath: string }; + +const row = (links?: Link[]) => ({ + id: "page-1", + title: "Q3 pipeline", + workspaceLinks: links, +}); + +function named(links: Link[], names: Map) { + const [first] = nameLinks([row(links)], names); + if (!first) throw new Error("nameLinks dropped the row"); + return first; +} + +describe("workspaceCell", () => { + test("prefers the resolved name over the id", () => { + expect( + workspaceCell( + named( + [{ workspaceId: WORKSPACE_A, entryPath: "report.html" }], + new Map([[WORKSPACE_A, "pipeline-review"]]), + ), + ), + ).toBe("pipeline-review"); + }); + + test("falls back to a short id when the workspace is on another machine", () => { + expect( + workspaceCell(row([{ workspaceId: WORKSPACE_A, entryPath: "a.html" }])), + ).toBe("11111111"); + }); + + test("counts the rest when a page was published from several workspaces", () => { + expect( + workspaceCell( + named( + [ + { workspaceId: WORKSPACE_A, entryPath: "a.html" }, + { workspaceId: WORKSPACE_B, entryPath: "b.html" }, + ], + new Map([[WORKSPACE_A, "pipeline-review"]]), + ), + ), + ).toBe("pipeline-review +1"); + }); + + test("shows a name from a later link when the first is unresolved", () => { + expect( + workspaceCell( + named( + [ + { workspaceId: WORKSPACE_B, entryPath: "b.html" }, + { workspaceId: WORKSPACE_A, entryPath: "a.html" }, + ], + new Map([[WORKSPACE_A, "pipeline-review"]]), + ), + ), + ).toBe("pipeline-review +1"); + }); + + test("marks a page that belongs to no workspace", () => { + expect(workspaceCell(row([]))).toBe("—"); + expect(workspaceCell(row())).toBe("—"); + }); +}); + +describe("nameLinks", () => { + test("leaves rows untouched when no workspace name is known", () => { + const rows = [row([{ workspaceId: WORKSPACE_A, entryPath: "a.html" }])]; + expect(nameLinks(rows, new Map())).toBe(rows); + }); + + test("names only the links it can resolve", () => { + expect( + named( + [ + { workspaceId: WORKSPACE_A, entryPath: "a.html" }, + { workspaceId: WORKSPACE_B, entryPath: "b.html" }, + ], + new Map([[WORKSPACE_A, "pipeline-review"]]), + ).workspaceLinks, + ).toEqual([ + { + workspaceId: WORKSPACE_A, + entryPath: "a.html", + name: "pipeline-review", + }, + { workspaceId: WORKSPACE_B, entryPath: "b.html" }, + ]); + }); +}); diff --git a/packages/cli/src/commands/pages/list/command.ts b/packages/cli/src/commands/pages/list/command.ts index 5c777e50038..a49aae5d55e 100644 --- a/packages/cli/src/commands/pages/list/command.ts +++ b/packages/cli/src/commands/pages/list/command.ts @@ -1,16 +1,107 @@ -import { string, table } from "@superset/cli-framework"; -import { command } from "../../../lib/command"; +import { number, string, table } from "@superset/cli-framework"; +import { PAGE_LIST_MAX_LIMIT } from "@superset/trpc/page-schema"; +import { type CliContext, command } from "../../../lib/command"; +import { listWorkspacesOnHost } from "../../../lib/host-workspaces"; +import { fetchAllPages, fetchPageList } from "../pageList"; import { resolveWorkspaceId } from "../workspaceRef"; +interface WorkspaceLink { + workspaceId: string; + entryPath: string; + name?: string; +} + +type PageRow = Record & { workspaceLinks?: WorkspaceLink[] }; + +async function cloudWorkspaceNames( + ctx: CliContext, + organizationId: string, +): Promise<[string, string][]> { + try { + const workspaces = await ctx.api.cloudWorkspace.list.query({ + organizationId, + }); + return workspaces.map((workspace) => [workspace.id, workspace.name]); + } catch { + return []; + } +} + +async function hostWorkspaceNames( + ctx: CliContext, + organizationId: string, +): Promise<[string, string][]> { + try { + const { workspaces } = await listWorkspacesOnHost({ + organizationId, + userJwt: ctx.bearer, + api: ctx.api, + }); + return workspaces.map((workspace) => [workspace.id, workspace.name]); + } catch { + return []; + } +} + +async function workspaceNames(ctx: CliContext): Promise> { + const organizationId = ctx.config.organizationId; + if (!organizationId) return new Map(); + const [cloud, host] = await Promise.all([ + cloudWorkspaceNames(ctx, organizationId), + hostWorkspaceNames(ctx, organizationId), + ]); + return new Map([...host, ...cloud]); +} + +export function nameLinks( + rows: PageRow[], + names: Map, +): PageRow[] { + if (names.size === 0) return rows; + return rows.map((row) => + row.workspaceLinks + ? { + ...row, + workspaceLinks: row.workspaceLinks.map((link) => { + const name = names.get(link.workspaceId); + return name ? { ...link, name } : link; + }), + } + : row, + ); +} + +export function workspaceCell(row: PageRow): string { + const links = row.workspaceLinks ?? []; + const shown = links.find((link) => link.name) ?? links[0]; + if (!shown) return "—"; + const label = shown.name ?? shown.workspaceId.slice(0, 8); + return links.length > 1 ? `${label} +${links.length - 1}` : label; +} + export default command({ description: "List pages in the organization", options: { workspace: string().desc( - "Only pages published from this workspace, by name or id (defaults to $SUPERSET_WORKSPACE_ID)", + "Only pages published from this workspace, by name or id (defaults to $SUPERSET_WORKSPACE_ID; pass '' for the whole org)", ), + search: string() + .alias("q") + .desc("Only pages whose title, slug or description contains this text"), + limit: number() + .int() + .min(1) + .max(PAGE_LIST_MAX_LIMIT) + .desc( + `Return one batch of this many pages (1-${PAGE_LIST_MAX_LIMIT}) plus a cursor, instead of every page`, + ), + cursor: string().desc("Continue from a previous run's nextCursor"), }, run: async ({ ctx, options }) => { - const workspace = options.workspace ?? process.env.SUPERSET_WORKSPACE_ID; + const asked = options.workspace !== undefined; + const workspace = asked + ? options.workspace + : process.env.SUPERSET_WORKSPACE_ID; const workspaceId = workspace ? await resolveWorkspaceId({ value: workspace, @@ -19,21 +110,67 @@ export default command({ api: ctx.api, }) : undefined; - return await ctx.api.page.list.query( - workspaceId ? { workspaceId } : undefined, - ); + + const query = { + ...(workspaceId ? { workspaceId } : {}), + ...(options.search ? { search: options.search } : {}), + ...(options.limit !== undefined ? { limit: options.limit } : {}), + }; + + const drains = options.limit === undefined && options.cursor === undefined; + const result = drains + ? { items: await fetchAllPages(ctx, query), nextCursor: null } + : await fetchPageList(ctx, query, options.cursor); + + // Naming a workspace costs a cloud call and a host call, and the host one + // waits on a machine that may be asleep. Under `--quiet` only ids are + // printed, and with no linked page there is nothing to name. + const quiet = (options as Record).quiet === true; + const wanted = + !quiet && + (Boolean(workspaceId && !asked) || + result.items.some((row) => row.workspaceLinks?.length)); + const names = wanted + ? await workspaceNames(ctx) + : new Map(); + + if (workspaceId && !asked) { + const label = names.get(workspaceId) ?? workspaceId; + process.stderr.write( + `Showing pages from the current workspace (${label}). Pass --workspace '' for the whole organization.\n`, + ); + } + + if (drains) return { data: nameLinks(result.items, names) }; + return { + data: { + items: nameLinks(result.items, names), + nextCursor: result.nextCursor, + }, + }; }, - display: (data) => - table( - (data as Record[]).map((row) => ({ + display: (data) => { + const items = Array.isArray(data) + ? (data as PageRow[]) + : ((data as { items: PageRow[] }).items ?? []); + const nextCursor = Array.isArray(data) + ? null + : ((data as { nextCursor: string | null }).nextCursor ?? null); + + const rendered = table( + items.map((row) => ({ title: row.title, version: row.latestVersion, visibility: row.visibility, + workspace: workspaceCell(row), url: row.url, id: row.id, })), - ["title", "version", "visibility", "url", "id"], - ["TITLE", "V", "VISIBILITY", "URL", "ID"], - [30, 4, 10, 50, 36], - ), + ["title", "version", "visibility", "workspace", "url", "id"], + ["TITLE", "V", "VISIBILITY", "WORKSPACE", "URL", "ID"], + [30, 4, 10, 26, 50, 36], + ); + if (!nextCursor) return rendered; + return `${rendered}\n\nMore pages available — re-run with --cursor ${nextCursor}, or drop --limit to fetch every page.`; + }, }); diff --git a/packages/cli/src/commands/pages/pageList.ts b/packages/cli/src/commands/pages/pageList.ts new file mode 100644 index 00000000000..ecdd0812b18 --- /dev/null +++ b/packages/cli/src/commands/pages/pageList.ts @@ -0,0 +1,43 @@ +import type { RouterOutputs } from "@superset/trpc"; +import { PAGE_LIST_MAX_LIMIT } from "@superset/trpc/page-schema"; +import type { CliContext } from "../../lib/command"; + +type PageList = RouterOutputs["page"]["list"]; + +export interface PageListQuery { + workspaceId?: string; + search?: string; + limit?: number; +} + +export interface PageListPage { + items: TPage[]; + nextCursor: PageList["nextCursor"]; +} + +export async function fetchPageList( + ctx: CliContext, + query: PageListQuery, + cursor?: string, +): Promise> { + const result = await ctx.api.page.list.query({ + ...query, + ...(cursor ? { cursor } : {}), + }); + return result as PageListPage; +} + +export async function fetchAllPages( + ctx: CliContext, + query: PageListQuery = {}, +): Promise { + const batched = { limit: PAGE_LIST_MAX_LIMIT, ...query }; + const items: TPage[] = []; + let cursor: string | undefined; + do { + const result = await fetchPageList(ctx, batched, cursor); + items.push(...result.items); + cursor = result.nextCursor ?? undefined; + } while (cursor); + return items; +} diff --git a/packages/db/drizzle/0120_pages_created_at_keyset_index.sql b/packages/db/drizzle/0120_pages_created_at_keyset_index.sql new file mode 100644 index 00000000000..f59db0530b0 --- /dev/null +++ b/packages/db/drizzle/0120_pages_created_at_keyset_index.sql @@ -0,0 +1,2 @@ +DROP INDEX "pages_organization_id_updated_at_idx";--> statement-breakpoint +CREATE INDEX "pages_organization_id_created_at_id_idx" ON "pages" USING btree ("organization_id","created_at" desc,"id" desc); \ No newline at end of file diff --git a/packages/db/drizzle/meta/0120_snapshot.json b/packages/db/drizzle/meta/0120_snapshot.json new file mode 100644 index 00000000000..a36add2994f --- /dev/null +++ b/packages/db/drizzle/meta/0120_snapshot.json @@ -0,0 +1,11053 @@ +{ + "id": "fa374306-8070-4c3f-aec6-d4431f1bd163", + "prevId": "e2dbc465-8d48-4ac6-957e-cf51815f7afe", + "version": "7", + "dialect": "postgresql", + "tables": { + "auth.accounts": { + "name": "accounts", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.apikeys": { + "name": "apikeys", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n\t\t\t\tWHEN metadata IS NULL OR metadata = '' THEN NULL\n\t\t\t\tWHEN NOT (metadata IS JSON OBJECT) THEN NULL\n\t\t\t\tWHEN (metadata::jsonb->>'organizationId') ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'\n\t\t\t\t\tTHEN (metadata::jsonb->>'organizationId')::uuid\n\t\t\t\tELSE NULL\n\t\t\tEND", + "type": "stored" + } + } + }, + "indexes": { + "apikeys_configId_idx": { + "name": "apikeys_configId_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikeys_referenceId_idx": { + "name": "apikeys_referenceId_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikeys_key_idx": { + "name": "apikeys_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikeys_organization_id_idx": { + "name": "apikeys_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikeys_metadata_trgm_idx": { + "name": "apikeys_metadata_trgm_idx", + "columns": [ + { + "expression": "\"metadata\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.device_codes": { + "name": "device_codes", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.invitations": { + "name": "invitations", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invitations_organization_id_idx": { + "name": "invitations_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitations_email_idx": { + "name": "invitations_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitations_organization_id_organizations_id_fk": { + "name": "invitations_organization_id_organizations_id_fk", + "tableFrom": "invitations", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_inviter_id_users_id_fk": { + "name": "invitations_inviter_id_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_team_id_teams_id_fk": { + "name": "invitations_team_id_teams_id_fk", + "tableFrom": "invitations", + "tableTo": "teams", + "schemaTo": "auth", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.jwkss": { + "name": "jwkss", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.members": { + "name": "members", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "members_organization_id_idx": { + "name": "members_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "members_user_id_idx": { + "name": "members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.oauth_access_tokens": { + "name": "oauth_access_tokens", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_access_tokens_client_id_oauth_clients_client_id_fk": { + "name": "oauth_access_tokens_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "oauth_clients", + "schemaTo": "auth", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_tokens_session_id_sessions_id_fk": { + "name": "oauth_access_tokens_session_id_sessions_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "sessions", + "schemaTo": "auth", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_tokens_user_id_users_id_fk": { + "name": "oauth_access_tokens_user_id_users_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_tokens_refresh_id_oauth_refresh_tokens_id_fk": { + "name": "oauth_access_tokens_refresh_id_oauth_refresh_tokens_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "oauth_refresh_tokens", + "schemaTo": "auth", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_tokens_token_unique": { + "name": "oauth_access_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.oauth_clients": { + "name": "oauth_clients", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_clients_user_id_users_id_fk": { + "name": "oauth_clients_user_id_users_id_fk", + "tableFrom": "oauth_clients", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_clients_client_id_unique": { + "name": "oauth_clients_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.oauth_consents": { + "name": "oauth_consents", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_consents_client_id_oauth_clients_client_id_fk": { + "name": "oauth_consents_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_consents", + "tableTo": "oauth_clients", + "schemaTo": "auth", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consents_user_id_users_id_fk": { + "name": "oauth_consents_user_id_users_id_fk", + "tableFrom": "oauth_consents", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.oauth_refresh_tokens": { + "name": "oauth_refresh_tokens", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk": { + "name": "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "oauth_clients", + "schemaTo": "auth", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_session_id_sessions_id_fk": { + "name": "oauth_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "sessions", + "schemaTo": "auth", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_user_id_users_id_fk": { + "name": "oauth_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.organizations": { + "name": "organizations", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_domains": { + "name": "allowed_domains", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizations_allowed_domains_idx": { + "name": "organizations_allowed_domains_idx", + "columns": [ + { + "expression": "allowed_domains", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_team_id": { + "name": "active_team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.team_members": { + "name": "team_members", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "team_members_team_id_idx": { + "name": "team_members_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_members_user_id_idx": { + "name": "team_members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_members_organization_id_idx": { + "name": "team_members_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_members_team_user_unique": { + "name": "team_members_team_user_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "schemaTo": "auth", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_organization_id_organizations_id_fk": { + "name": "team_members_organization_id_organizations_id_fk", + "tableFrom": "team_members", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.teams": { + "name": "teams", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "teams_organization_id_idx": { + "name": "teams_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_org_slug_unique": { + "name": "teams_org_slug_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_organization_id_organizations_id_fk": { + "name": "teams_organization_id_organizations_id_fk", + "tableFrom": "teams", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.users": { + "name": "users", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_ids": { + "name": "organization_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "last_active_organization_id": { + "name": "last_active_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "onboarded_at": { + "name": "onboarded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deletion_requested_at": { + "name": "deletion_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_organization_ids_idx": { + "name": "users_organization_ids_idx", + "columns": [ + { + "expression": "organization_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verifications": { + "name": "verifications", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verifications_identifier_idx": { + "name": "verifications_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_installations": { + "name": "github_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suspended": { + "name": "suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_installations_installation_id_idx": { + "name": "github_installations_installation_id_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_installations_organization_id_organizations_id_fk": { + "name": "github_installations_organization_id_organizations_id_fk", + "tableFrom": "github_installations", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_installations_connected_by_user_id_users_id_fk": { + "name": "github_installations_connected_by_user_id_users_id_fk", + "tableFrom": "github_installations", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "connected_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_installations_installation_id_unique": { + "name": "github_installations_installation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "installation_id" + ] + }, + "github_installations_org_unique": { + "name": "github_installations_org_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_pull_requests": { + "name": "github_pull_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_login": { + "name": "author_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_avatar_url": { + "name": "author_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_draft": { + "name": "is_draft", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "review_decision": { + "name": "review_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checks_status": { + "name": "checks_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "checks": { + "name": "checks", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_pull_requests_repository_id_idx": { + "name": "github_pull_requests_repository_id_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_state_idx": { + "name": "github_pull_requests_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_head_branch_idx": { + "name": "github_pull_requests_head_branch_idx", + "columns": [ + { + "expression": "head_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_org_id_idx": { + "name": "github_pull_requests_org_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_pull_requests_repository_id_github_repositories_id_fk": { + "name": "github_pull_requests_repository_id_github_repositories_id_fk", + "tableFrom": "github_pull_requests", + "tableTo": "github_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_pull_requests_organization_id_organizations_id_fk": { + "name": "github_pull_requests_organization_id_organizations_id_fk", + "tableFrom": "github_pull_requests", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_pull_requests_repo_pr_unique": { + "name": "github_pull_requests_repo_pr_unique", + "nullsNotDistinct": false, + "columns": [ + "repository_id", + "pr_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_repositories": { + "name": "github_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "installation_id": { + "name": "installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_id": { + "name": "repo_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_repositories_installation_id_idx": { + "name": "github_repositories_installation_id_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_repositories_full_name_idx": { + "name": "github_repositories_full_name_idx", + "columns": [ + { + "expression": "full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_repositories_org_id_idx": { + "name": "github_repositories_org_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_repositories_installation_id_github_installations_id_fk": { + "name": "github_repositories_installation_id_github_installations_id_fk", + "tableFrom": "github_repositories", + "tableTo": "github_installations", + "columnsFrom": [ + "installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_repositories_organization_id_organizations_id_fk": { + "name": "github_repositories_organization_id_organizations_id_fk", + "tableFrom": "github_repositories", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_repositories_repo_id_unique": { + "name": "github_repositories_repo_id_unique", + "nullsNotDistinct": false, + "columns": [ + "repo_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "ingest.webhook_events": { + "name": "webhook_events", + "schema": "ingest", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "integration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "received_at": { + "name": "received_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_events_provider_status_idx": { + "name": "webhook_events_provider_status_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_events_provider_event_id_idx": { + "name": "webhook_events_provider_event_id_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_events_received_at_idx": { + "name": "webhook_events_received_at_idx", + "columns": [ + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leaderboard_daily": { + "name": "leaderboard_daily", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uncached_input": { + "name": "uncached_input", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input": { + "name": "cached_input", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_write_5m": { + "name": "cache_write_5m", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_write_1h": { + "name": "cache_write_1h", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output": { + "name": "output", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_output": { + "name": "reasoning_output", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tokens": { + "name": "tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usd_estimate": { + "name": "usd_estimate", + "type": "numeric(14, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "approximate": { + "name": "approximate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sessions": { + "name": "sessions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "leaderboard_daily_day_idx": { + "name": "leaderboard_daily_day_idx", + "columns": [ + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leaderboard_daily_user_day_idx": { + "name": "leaderboard_daily_user_day_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leaderboard_daily_model_day_idx": { + "name": "leaderboard_daily_model_day_idx", + "columns": [ + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "leaderboard_daily_user_id_public_profiles_user_id_fk": { + "name": "leaderboard_daily_user_id_public_profiles_user_id_fk", + "tableFrom": "leaderboard_daily", + "tableTo": "public_profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "leaderboard_daily_identity_key": { + "name": "leaderboard_daily_identity_key", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "day", + "provider", + "model", + "host_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leaderboard_daily_factory": { + "name": "leaderboard_daily_factory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sessions": { + "name": "sessions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "parallel_sessions": { + "name": "parallel_sessions", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "agent_prs_merged": { + "name": "agent_prs_merged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "leaderboard_daily_factory_day_idx": { + "name": "leaderboard_daily_factory_day_idx", + "columns": [ + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leaderboard_daily_factory_user_day_idx": { + "name": "leaderboard_daily_factory_user_day_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "leaderboard_daily_factory_user_id_public_profiles_user_id_fk": { + "name": "leaderboard_daily_factory_user_id_public_profiles_user_id_fk", + "tableFrom": "leaderboard_daily_factory", + "tableTo": "public_profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "leaderboard_daily_factory_identity_key": { + "name": "leaderboard_daily_factory_identity_key", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "day", + "host_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leaderboard_participants": { + "name": "leaderboard_participants", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "leaderboard_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "opted_in_at": { + "name": "opted_in_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "flagged_at": { + "name": "flagged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_published_at": { + "name": "last_published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payload_version": { + "name": "payload_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "tokens": { + "name": "tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usd": { + "name": "usd", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sessions": { + "name": "sessions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "uncached_input": { + "name": "uncached_input", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input": { + "name": "cached_input", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_write_5m": { + "name": "cache_write_5m", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_write_1h": { + "name": "cache_write_1h", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output": { + "name": "output", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_output": { + "name": "reasoning_output", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "approximate": { + "name": "approximate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "day_range_start": { + "name": "day_range_start", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "day_range_end": { + "name": "day_range_end", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tier_computed_at": { + "name": "tier_computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active_days": { + "name": "active_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "axis_width": { + "name": "axis_width", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "axis_depth": { + "name": "axis_depth", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "axis_output": { + "name": "axis_output", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "axis_cost": { + "name": "axis_cost", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "leaderboard_participants_tokens_idx": { + "name": "leaderboard_participants_tokens_idx", + "columns": [ + { + "expression": "tokens", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leaderboard_participants_org_idx": { + "name": "leaderboard_participants_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leaderboard_participants_usd_idx": { + "name": "leaderboard_participants_usd_idx", + "columns": [ + { + "expression": "usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leaderboard_participants_tier_idx": { + "name": "leaderboard_participants_tier_idx", + "columns": [ + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "leaderboard_participants_user_id_users_id_fk": { + "name": "leaderboard_participants_user_id_users_id_fk", + "tableFrom": "leaderboard_participants", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "leaderboard_participants_organization_id_organizations_id_fk": { + "name": "leaderboard_participants_organization_id_organizations_id_fk", + "tableFrom": "leaderboard_participants", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "leaderboard_participants_handle_unique": { + "name": "leaderboard_participants_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_installs": { + "name": "plugin_installs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "marketplace": { + "name": "marketplace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_name": { + "name": "plugin_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_installs_user_plugin_unique": { + "name": "plugin_installs_user_plugin_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "marketplace", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_installs_user_idx": { + "name": "plugin_installs_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_installs_organization_id_organizations_id_fk": { + "name": "plugin_installs_organization_id_organizations_id_fk", + "tableFrom": "plugin_installs", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_installs_user_id_users_id_fk": { + "name": "plugin_installs_user_id_users_id_fk", + "tableFrom": "plugin_installs", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_marketplaces": { + "name": "plugin_marketplaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_at": { + "name": "added_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_marketplaces_user_name_unique": { + "name": "plugin_marketplaces_user_name_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_marketplaces_user_idx": { + "name": "plugin_marketplaces_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_marketplaces_organization_id_organizations_id_fk": { + "name": "plugin_marketplaces_organization_id_organizations_id_fk", + "tableFrom": "plugin_marketplaces", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_marketplaces_user_id_users_id_fk": { + "name": "plugin_marketplaces_user_id_users_id_fk", + "tableFrom": "plugin_marketplaces", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_oauth_clients": { + "name": "plugin_oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret_expires_at": { + "name": "client_secret_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "registration_access_token": { + "name": "registration_access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_client_uri": { + "name": "registration_client_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_oauth_clients_issuer_redirect_unique": { + "name": "plugin_oauth_clients_issuer_redirect_unique", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "redirect_uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.handles": { + "name": "handles", + "schema": "", + "columns": { + "handle": { + "name": "handle", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_type": { + "name": "owner_type", + "type": "handle_owner_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "handles_owner_type_idx": { + "name": "handles_owner_type_idx", + "columns": [ + { + "expression": "owner_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "handles_user_id_users_id_fk": { + "name": "handles_user_id_users_id_fk", + "tableFrom": "handles", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "handles_organization_id_organizations_id_fk": { + "name": "handles_organization_id_organizations_id_fk", + "tableFrom": "handles", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "handles_user_key": { + "name": "handles_user_key", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "handles_organization_key": { + "name": "handles_organization_key", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + }, + "handles_user_owner_key": { + "name": "handles_user_owner_key", + "nullsNotDistinct": false, + "columns": [ + "handle", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "handles_owner_matches_type": { + "name": "handles_owner_matches_type", + "value": "(\n\t\t\t\t(\"handles\".\"owner_type\" = 'user' and \"handles\".\"user_id\" is not null and \"handles\".\"organization_id\" is null)\n\t\t\t\tor (\"handles\".\"owner_type\" = 'organization' and \"handles\".\"organization_id\" is not null and \"handles\".\"user_id\" is null)\n\t\t\t\tor (\"handles\".\"owner_type\" = 'reserved' and \"handles\".\"user_id\" is null and \"handles\".\"organization_id\" is null)\n\t\t\t)" + }, + "handles_shape": { + "name": "handles_shape", + "value": "length(\"handles\".\"handle\") between 2 and 39 and \"handles\".\"handle\" ~ '^[a-z0-9]+(-[a-z0-9]+)*$'" + } + }, + "isRLSEnabled": false + }, + "public.profile_awards": { + "name": "profile_awards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "value": { + "name": "value", + "type": "numeric(20, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "awarded_on": { + "name": "awarded_on", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "awarded_at": { + "name": "awarded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "profile_awards_user_idx": { + "name": "profile_awards_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profile_awards_slug_idx": { + "name": "profile_awards_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "awarded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "profile_awards_user_id_public_profiles_user_id_fk": { + "name": "profile_awards_user_id_public_profiles_user_id_fk", + "tableFrom": "profile_awards", + "tableTo": "public_profiles", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_awards_identity_key": { + "name": "profile_awards_identity_key", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "slug", + "tier" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_profiles": { + "name": "public_profiles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "leaderboard_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_handle": { + "name": "github_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "x_handle": { + "name": "x_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "opted_in_at": { + "name": "opted_in_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "flagged_at": { + "name": "flagged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_published_at": { + "name": "last_published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payload_version": { + "name": "payload_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "awards_catalog_version": { + "name": "awards_catalog_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tokens": { + "name": "tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "usd": { + "name": "usd", + "type": "numeric(20, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sessions": { + "name": "sessions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "uncached_input": { + "name": "uncached_input", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input": { + "name": "cached_input", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_write_5m": { + "name": "cache_write_5m", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_write_1h": { + "name": "cache_write_1h", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output": { + "name": "output", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_output": { + "name": "reasoning_output", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "approximate": { + "name": "approximate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "day_range_start": { + "name": "day_range_start", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "day_range_end": { + "name": "day_range_end", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tier_computed_at": { + "name": "tier_computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active_days": { + "name": "active_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "axis_width": { + "name": "axis_width", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "axis_depth": { + "name": "axis_depth", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "axis_output": { + "name": "axis_output", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "axis_cost": { + "name": "axis_cost", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_profiles_tokens_idx": { + "name": "public_profiles_tokens_idx", + "columns": [ + { + "expression": "tokens", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_profiles_org_idx": { + "name": "public_profiles_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_profiles_usd_idx": { + "name": "public_profiles_usd_idx", + "columns": [ + { + "expression": "usd", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_profiles_tier_idx": { + "name": "public_profiles_tier_idx", + "columns": [ + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_profiles_user_id_users_id_fk": { + "name": "public_profiles_user_id_users_id_fk", + "tableFrom": "public_profiles", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_profiles_organization_id_organizations_id_fk": { + "name": "public_profiles_organization_id_organizations_id_fk", + "tableFrom": "public_profiles", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "public_profiles_handle_owner_fk": { + "name": "public_profiles_handle_owner_fk", + "tableFrom": "public_profiles", + "tableTo": "handles", + "columnsFrom": [ + "handle", + "user_id" + ], + "columnsTo": [ + "handle", + "user_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "public_profiles_handle_unique": { + "name": "public_profiles_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": { + "public_profiles_bio_length": { + "name": "public_profiles_bio_length", + "value": "length(\"public_profiles\".\"bio\") <= 160" + }, + "public_profiles_website_scheme": { + "name": "public_profiles_website_scheme", + "value": "\"public_profiles\".\"website_url\" is null or \"public_profiles\".\"website_url\" ~ '^https://'" + } + }, + "isRLSEnabled": false + }, + "public.agent_commands": { + "name": "agent_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_device_id": { + "name": "target_device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_device_type": { + "name": "target_device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool": { + "name": "tool", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parent_command_id": { + "name": "parent_command_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "command_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "timeout_at": { + "name": "timeout_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_commands_user_status_idx": { + "name": "agent_commands_user_status_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_commands_target_device_status_idx": { + "name": "agent_commands_target_device_status_idx", + "columns": [ + { + "expression": "target_device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_commands_org_created_idx": { + "name": "agent_commands_org_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_commands_user_id_users_id_fk": { + "name": "agent_commands_user_id_users_id_fk", + "tableFrom": "agent_commands", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_commands_organization_id_organizations_id_fk": { + "name": "agent_commands_organization_id_organizations_id_fk", + "tableFrom": "agent_commands", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_credentials": { + "name": "agent_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "agent_credential_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_label": { + "name": "account_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_validated_at": { + "name": "last_validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_credentials_user_id_idx": { + "name": "agent_credentials_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_credentials_user_id_users_id_fk": { + "name": "agent_credentials_user_id_users_id_fk", + "tableFrom": "agent_credentials", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agent_credentials_user_id_agent_unique": { + "name": "agent_credentials_user_id_agent_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "agent" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.attachments": { + "name": "attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "file_id": { + "name": "file_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_kind": { + "name": "parent_kind", + "type": "attachment_parent_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "attachments_file_id_idx": { + "name": "attachments_file_id_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_parent_kind_parent_id_idx": { + "name": "attachments_parent_kind_parent_id_idx", + "columns": [ + { + "expression": "parent_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "attachments_parent_path_unique": { + "name": "attachments_parent_path_unique", + "columns": [ + { + "expression": "parent_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"attachments\".\"path\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "attachments_file_id_files_id_fk": { + "name": "attachments_file_id_files_id_fk", + "tableFrom": "attachments", + "tableTo": "files", + "columnsFrom": [ + "file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_events": { + "name": "automation_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_connection_id": { + "name": "integration_connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_event_id": { + "name": "external_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_login": { + "name": "actor_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_is_external": { + "name": "actor_is_external", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "webhook_event_id": { + "name": "webhook_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "dispatch_input": { + "name": "dispatch_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_events_undispatched_idx": { + "name": "automation_events_undispatched_idx", + "columns": [ + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"automation_events\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_events_org_received_idx": { + "name": "automation_events_org_received_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_events_resource_idx": { + "name": "automation_events_resource_idx", + "columns": [ + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_events_prunable_idx": { + "name": "automation_events_prunable_idx", + "columns": [ + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"automation_events\".\"payload\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_events_organization_id_organizations_id_fk": { + "name": "automation_events_organization_id_organizations_id_fk", + "tableFrom": "automation_events", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "automation_events_dedup_unique": { + "name": "automation_events_dedup_unique", + "nullsNotDistinct": true, + "columns": [ + "integration_connection_id", + "provider", + "external_event_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_prompt_versions": { + "name": "automation_prompt_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "automation_id": { + "name": "automation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_bucket": { + "name": "window_bucket", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "automation_prompt_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "restored_from_version_id": { + "name": "restored_from_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_prompt_versions_bucket_uniq": { + "name": "automation_prompt_versions_bucket_uniq", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_bucket", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"automation_prompt_versions\".\"source\" <> 'restore'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_prompt_versions_automation_idx": { + "name": "automation_prompt_versions_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_prompt_versions_automation_id_automations_id_fk": { + "name": "automation_prompt_versions_automation_id_automations_id_fk", + "tableFrom": "automation_prompt_versions", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_prompt_versions_author_user_id_users_id_fk": { + "name": "automation_prompt_versions_author_user_id_users_id_fk", + "tableFrom": "automation_prompt_versions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_prompt_versions_restored_from_version_id_fk": { + "name": "automation_prompt_versions_restored_from_version_id_fk", + "tableFrom": "automation_prompt_versions", + "tableTo": "automation_prompt_versions", + "columnsFrom": [ + "restored_from_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_runs": { + "name": "automation_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "automation_id": { + "name": "automation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "v2_workspace_id": { + "name": "v2_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_kind": { + "name": "session_kind", + "type": "automation_session_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "chat_session_id": { + "name": "chat_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "terminal_session_id": { + "name": "terminal_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "automation_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "automation_run_error_code", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_runs_schedule_dedup_idx": { + "name": "automation_runs_schedule_dedup_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_event_dedup_idx": { + "name": "automation_runs_event_dedup_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "event_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_inflight_resource_idx": { + "name": "automation_runs_inflight_resource_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status IN ('dispatching', 'dispatched')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_history_idx": { + "name": "automation_runs_history_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_status_idx": { + "name": "automation_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_workspace_idx": { + "name": "automation_runs_workspace_idx", + "columns": [ + { + "expression": "v2_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_runs_organization_id_organizations_id_fk": { + "name": "automation_runs_organization_id_organizations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_trigger_id_automation_triggers_id_fk": { + "name": "automation_runs_trigger_id_automation_triggers_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automation_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "automation_runs_event_id_automation_events_id_fk": { + "name": "automation_runs_event_id_automation_events_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automation_events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "automation_runs_chat_session_id_chat_sessions_id_fk": { + "name": "automation_runs_chat_session_id_chat_sessions_id_fk", + "tableFrom": "automation_runs", + "tableTo": "chat_sessions", + "columnsFrom": [ + "chat_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "automation_runs_automation_org_fk": { + "name": "automation_runs_automation_org_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_triggers": { + "name": "automation_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "automation_id": { + "name": "automation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "automation_trigger_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_prefix": { + "name": "secret_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_rotated_at": { + "name": "secret_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_triggers_dispatcher_idx": { + "name": "automation_triggers_dispatcher_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "kind = 'schedule'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_triggers_matcher_idx": { + "name": "automation_triggers_matcher_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_triggers_automation_idx": { + "name": "automation_triggers_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_triggers_schedule_idx": { + "name": "automation_triggers_schedule_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "kind = 'schedule'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_triggers_organization_id_organizations_id_fk": { + "name": "automation_triggers_organization_id_organizations_id_fk", + "tableFrom": "automation_triggers", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_triggers_automation_org_fk": { + "name": "automation_triggers_automation_org_fk", + "tableFrom": "automation_triggers", + "tableTo": "automations", + "columnsFrom": [ + "automation_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "automation_triggers_kind_matches_config": { + "name": "automation_triggers_kind_matches_config", + "value": "config->>'kind' = kind::text" + } + }, + "isRLSEnabled": false + }, + "public.automations": { + "name": "automations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_host_id": { + "name": "target_host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "v2_project_id": { + "name": "v2_project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "v2_workspace_id": { + "name": "v2_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"automation\"]'::jsonb" + }, + "continue_agent_session": { + "name": "continue_agent_session", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automations_owner_idx": { + "name": "automations_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automations_organization_idx": { + "name": "automations_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automations_organization_id_organizations_id_fk": { + "name": "automations_organization_id_organizations_id_fk", + "tableFrom": "automations", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automations_owner_user_id_users_id_fk": { + "name": "automations_owner_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "automations_id_org_unique": { + "name": "automations_id_org_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_sessions": { + "name": "chat_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "v2_workspace_id": { + "name": "v2_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_sessions_org_idx": { + "name": "chat_sessions_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_sessions_created_by_idx": { + "name": "chat_sessions_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_sessions_last_active_idx": { + "name": "chat_sessions_last_active_idx", + "columns": [ + { + "expression": "last_active_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_sessions_organization_id_organizations_id_fk": { + "name": "chat_sessions_organization_id_organizations_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_sessions_created_by_users_id_fk": { + "name": "chat_sessions_created_by_users_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_sessions_workspace_id_workspaces_id_fk": { + "name": "chat_sessions_workspace_id_workspaces_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_workspace_repositories": { + "name": "cloud_workspace_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cloud_workspace_id": { + "name": "cloud_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "cloud_workspace_repositories_cloud_workspace_id_idx": { + "name": "cloud_workspace_repositories_cloud_workspace_id_idx", + "columns": [ + { + "expression": "cloud_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_workspace_repositories_cloud_workspace_id_cloud_workspaces_id_fk": { + "name": "cloud_workspace_repositories_cloud_workspace_id_cloud_workspaces_id_fk", + "tableFrom": "cloud_workspace_repositories", + "tableTo": "cloud_workspaces", + "columnsFrom": [ + "cloud_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_workspace_repositories_repository_id_github_repositories_id_fk": { + "name": "cloud_workspace_repositories_repository_id_github_repositories_id_fk", + "tableFrom": "cloud_workspace_repositories", + "tableTo": "github_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cloud_workspace_repositories_workspace_repository_unique": { + "name": "cloud_workspace_repositories_workspace_repository_unique", + "nullsNotDistinct": false, + "columns": [ + "cloud_workspace_id", + "repository_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_workspaces": { + "name": "cloud_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'vercel'" + }, + "provider_sandbox_id": { + "name": "provider_sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_url": { + "name": "sandbox_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "cloud_workspace_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'provisioning'" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "host_version": { + "name": "host_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cloud_workspaces_organization_id_idx": { + "name": "cloud_workspaces_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_workspaces_organization_id_organizations_id_fk": { + "name": "cloud_workspaces_organization_id_organizations_id_fk", + "tableFrom": "cloud_workspaces", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_workspaces_environment_id_environments_id_fk": { + "name": "cloud_workspaces_environment_id_environments_id_fk", + "tableFrom": "cloud_workspaces", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cloud_workspaces_created_by_user_id_users_id_fk": { + "name": "cloud_workspaces_created_by_user_id_users_id_fk", + "tableFrom": "cloud_workspaces", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cloud_workspaces_provider_sandbox_id_unique": { + "name": "cloud_workspaces_provider_sandbox_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "provider_sandbox_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connections": { + "name": "connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connector": { + "name": "connector", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_kind": { + "name": "owner_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_method": { + "name": "auth_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_expires_at": { + "name": "token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_account_id": { + "name": "external_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_account_label": { + "name": "external_account_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_user_id": { + "name": "external_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_user_label": { + "name": "external_user_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "disconnect_reason": { + "name": "disconnect_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "connections_org_connector_unique": { + "name": "connections_org_connector_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connections\".\"owner_kind\" = 'org'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "connections_user_connector_unique": { + "name": "connections_user_connector_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connector", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connected_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connections\".\"owner_kind\" = 'user'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "connections_org_idx": { + "name": "connections_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connections_user_connector_idx": { + "name": "connections_user_connector_idx", + "columns": [ + { + "expression": "connected_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"connections\".\"disconnected_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "connections_external_account_idx": { + "name": "connections_external_account_idx", + "columns": [ + { + "expression": "connector", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connections_organization_id_organizations_id_fk": { + "name": "connections_organization_id_organizations_id_fk", + "tableFrom": "connections", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connections_connected_by_user_id_users_id_fk": { + "name": "connections_connected_by_user_id_users_id_fk", + "tableFrom": "connections", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "connected_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "connections_user_identity_present": { + "name": "connections_user_identity_present", + "value": "owner_kind <> 'user' OR external_user_id IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.deal_redemptions": { + "name": "deal_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_redemption_id": { + "name": "external_redemption_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deal_id": { + "name": "deal_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_batch": { + "name": "company_batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promotion_code": { + "name": "promotion_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deal_redemptions_source_external_id_unique": { + "name": "deal_redemptions_source_external_id_unique", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_redemption_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deal_redemptions_email_idx": { + "name": "deal_redemptions_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deal_redemptions_organization_id_organizations_id_fk": { + "name": "deal_redemptions_organization_id_organizations_id_fk", + "tableFrom": "deal_redemptions", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.desktop_notices": { + "name": "desktop_notices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "severity": { + "name": "severity", + "type": "desktop_notice_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "desktop_notice_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'immediate'" + }, + "min_version": { + "name": "min_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_version": { + "name": "max_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platforms": { + "name": "platforms", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "channels": { + "name": "channels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta_label": { + "name": "cta_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cta_action": { + "name": "cta_action", + "type": "desktop_notice_cta_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cta_url": { + "name": "cta_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dismissible": { + "name": "dismissible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "desktop_notices_active_idx": { + "name": "desktop_notices_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_repositories": { + "name": "environment_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "environment_repositories_environment_id_idx": { + "name": "environment_repositories_environment_id_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_repositories_environment_id_environments_id_fk": { + "name": "environment_repositories_environment_id_environments_id_fk", + "tableFrom": "environment_repositories", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_repositories_repository_id_github_repositories_id_fk": { + "name": "environment_repositories_repository_id_github_repositories_id_fk", + "tableFrom": "environment_repositories", + "tableTo": "github_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_repositories_environment_id_repository_id_unique": { + "name": "environment_repositories_environment_id_repository_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "repository_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_secrets": { + "name": "environment_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_secrets_environment_id_idx": { + "name": "environment_secrets_environment_id_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_secrets_organization_id_idx": { + "name": "environment_secrets_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_secrets_organization_id_organizations_id_fk": { + "name": "environment_secrets_organization_id_organizations_id_fk", + "tableFrom": "environment_secrets", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_secrets_environment_id_environments_id_fk": { + "name": "environment_secrets_environment_id_environments_id_fk", + "tableFrom": "environment_secrets", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_secrets_created_by_user_id_users_id_fk": { + "name": "environment_secrets_created_by_user_id_users_id_fk", + "tableFrom": "environment_secrets", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_secrets_environment_id_organization_id_key_unique": { + "name": "environment_secrets_environment_id_organization_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "organization_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'vercel'" + }, + "source_kind": { + "name": "source_kind", + "type": "environment_source_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bundle_sha": { + "name": "bundle_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hooks_repository_id": { + "name": "hooks_repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "environment_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environments_organization_id_idx": { + "name": "environments_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environments_organization_id_organizations_id_fk": { + "name": "environments_organization_id_organizations_id_fk", + "tableFrom": "environments", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environments_hooks_repository_id_github_repositories_id_fk": { + "name": "environments_hooks_repository_id_github_repositories_id_fk", + "tableFrom": "environments", + "tableTo": "github_repositories", + "columnsFrom": [ + "hooks_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environments_created_by_user_id_users_id_fk": { + "name": "environments_created_by_user_id_users_id_fk", + "tableFrom": "environments", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_organization_id_name_unique": { + "name": "environments_organization_id_name_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "file_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "files_organization_id_created_at_idx": { + "name": "files_organization_id_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "files_status_created_at_idx": { + "name": "files_status_created_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "files_organization_id_organizations_id_fk": { + "name": "files_organization_id_organizations_id_fk", + "tableFrom": "files", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "files_created_by_user_id_users_id_fk": { + "name": "files_created_by_user_id_users_id_fk", + "tableFrom": "files", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_user_connections": { + "name": "github_user_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_access_token": { + "name": "encrypted_access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "encrypted_refresh_token": { + "name": "encrypted_refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "github_user_connections_user_id_users_id_fk": { + "name": "github_user_connections_user_id_users_id_fk", + "tableFrom": "github_user_connections", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_user_connections_user_id_unique": { + "name": "github_user_connections_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connections": { + "name": "integration_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "integration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_expires_at": { + "name": "token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "disconnect_reason": { + "name": "disconnect_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_org_id": { + "name": "external_org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_org_name": { + "name": "external_org_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_connections_org_provider_unique": { + "name": "integration_connections_org_provider_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"integration_connections\".\"provider\" <> 'google'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connections_google_user_unique": { + "name": "integration_connections_google_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connected_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"integration_connections\".\"provider\" = 'google'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connections_slack_external_org_active_unique": { + "name": "integration_connections_slack_external_org_active_unique", + "columns": [ + { + "expression": "external_org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"integration_connections\".\"provider\" = 'slack' AND \"integration_connections\".\"disconnected_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connections_org_idx": { + "name": "integration_connections_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_connections_organization_id_organizations_id_fk": { + "name": "integration_connections_organization_id_organizations_id_fk", + "tableFrom": "integration_connections", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_connections_connected_by_user_id_users_id_fk": { + "name": "integration_connections_connected_by_user_id_users_id_fk", + "tableFrom": "integration_connections", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "connected_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_comment_threads": { + "name": "page_comment_threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "page_id": { + "name": "page_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "page_version_id": { + "name": "page_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "anchor_kind": { + "name": "anchor_kind", + "type": "page_comment_anchor_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "intent": { + "name": "intent", + "type": "page_comment_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "anchor": { + "name": "anchor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "anchor_text": { + "name": "anchor_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_activated_at": { + "name": "agent_activated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "agent_activated_by_user_id": { + "name": "agent_activated_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "page_comment_threads_page_id_idx": { + "name": "page_comment_threads_page_id_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_comment_threads_page_version_id_idx": { + "name": "page_comment_threads_page_version_id_idx", + "columns": [ + { + "expression": "page_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_comment_threads_open_idx": { + "name": "page_comment_threads_open_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "resolved_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_comment_threads_page_id_pages_id_fk": { + "name": "page_comment_threads_page_id_pages_id_fk", + "tableFrom": "page_comment_threads", + "tableTo": "pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_comment_threads_page_version_id_page_versions_id_fk": { + "name": "page_comment_threads_page_version_id_page_versions_id_fk", + "tableFrom": "page_comment_threads", + "tableTo": "page_versions", + "columnsFrom": [ + "page_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_comment_threads_created_by_user_id_users_id_fk": { + "name": "page_comment_threads_created_by_user_id_users_id_fk", + "tableFrom": "page_comment_threads", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "page_comment_threads_agent_activated_by_user_id_users_id_fk": { + "name": "page_comment_threads_agent_activated_by_user_id_users_id_fk", + "tableFrom": "page_comment_threads", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "agent_activated_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "page_comment_threads_resolved_by_user_id_users_id_fk": { + "name": "page_comment_threads_resolved_by_user_id_users_id_fk", + "tableFrom": "page_comment_threads", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "resolved_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "page_comment_threads_anchor_matches_kind": { + "name": "page_comment_threads_anchor_matches_kind", + "value": "(anchor_kind = 'page') = (anchor IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.page_comments": { + "name": "page_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_kind": { + "name": "author_kind", + "type": "page_comment_author_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'human'" + }, + "author_user_id": { + "name": "author_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_session_id": { + "name": "agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "page_comments_thread_id_created_at_idx": { + "name": "page_comments_thread_id_created_at_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_comments_thread_id_page_comment_threads_id_fk": { + "name": "page_comments_thread_id_page_comment_threads_id_fk", + "tableFrom": "page_comments", + "tableTo": "page_comment_threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_comments_author_user_id_users_id_fk": { + "name": "page_comments_author_user_id_users_id_fk", + "tableFrom": "page_comments", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "page_comments_agent_has_session": { + "name": "page_comments_agent_has_session", + "value": "author_kind <> 'agent' OR agent_session_id IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.page_versions": { + "name": "page_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "page_id": { + "name": "page_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "page_versions_page_id_idx": { + "name": "page_versions_page_id_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_versions_page_id_pages_id_fk": { + "name": "page_versions_page_id_pages_id_fk", + "tableFrom": "page_versions", + "tableTo": "pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_versions_created_by_user_id_users_id_fk": { + "name": "page_versions_created_by_user_id_users_id_fk", + "tableFrom": "page_versions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "page_versions_page_id_version_unique": { + "name": "page_versions_page_id_version_unique", + "nullsNotDistinct": false, + "columns": [ + "page_id", + "version" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pages": { + "name": "pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "page_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'just_me'" + }, + "shared_version": { + "name": "shared_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "watched_by_agent": { + "name": "watched_by_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watch_heartbeat_at": { + "name": "watch_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pages_slug_unique": { + "name": "pages_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pages_organization_id_created_at_id_idx": { + "name": "pages_organization_id_created_at_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" desc", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pages_created_by_user_id_idx": { + "name": "pages_created_by_user_id_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pages_organization_id_organizations_id_fk": { + "name": "pages_organization_id_organizations_id_fk", + "tableFrom": "pages", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pages_created_by_user_id_users_id_fk": { + "name": "pages_created_by_user_id_users_id_fk", + "tableFrom": "pages", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_organization_id_idx": { + "name": "projects_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_organization_id_organizations_id_fk": { + "name": "projects_organization_id_organizations_id_fk", + "tableFrom": "projects", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "projects_github_repository_id_github_repositories_id_fk": { + "name": "projects_github_repository_id_github_repositories_id_fk", + "tableFrom": "projects", + "tableTo": "github_repositories", + "columnsFrom": [ + "github_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_org_slug_unique": { + "name": "projects_org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submitted_prompts": { + "name": "submitted_prompts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "prompt_text": { + "name": "prompt_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submitter_name": { + "name": "submitter_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "submitted_prompts_user_id_idx": { + "name": "submitted_prompts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submitted_prompts_organization_id_idx": { + "name": "submitted_prompts_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submitted_prompts_created_at_idx": { + "name": "submitted_prompts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "submitted_prompts_user_id_users_id_fk": { + "name": "submitted_prompts_user_id_users_id_fk", + "tableFrom": "submitted_prompts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submitted_prompts_organization_id_organizations_id_fk": { + "name": "submitted_prompts_organization_id_organizations_id_fk", + "tableFrom": "submitted_prompts", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'incomplete'" + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subscriptions_reference_id_idx": { + "name": "subscriptions_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscriptions_stripe_customer_id_idx": { + "name": "subscriptions_stripe_customer_id_idx", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscriptions_status_idx": { + "name": "subscriptions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscriptions_reference_id_organizations_id_fk": { + "name": "subscriptions_reference_id_organizations_id_fk", + "tableFrom": "subscriptions", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_statuses": { + "name": "task_statuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "progress_percent": { + "name": "progress_percent", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "external_provider": { + "name": "external_provider", + "type": "integration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_statuses_organization_id_idx": { + "name": "task_statuses_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_statuses_type_idx": { + "name": "task_statuses_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_statuses_organization_id_organizations_id_fk": { + "name": "task_statuses_organization_id_organizations_id_fk", + "tableFrom": "task_statuses", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "task_statuses_org_external_unique": { + "name": "task_statuses_org_external_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "external_provider", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_id": { + "name": "status_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assignee_id": { + "name": "assignee_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "estimate": { + "name": "estimate", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_provider": { + "name": "external_provider", + "type": "integration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_updated_at": { + "name": "external_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_project_name": { + "name": "external_project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_cycle_id": { + "name": "external_cycle_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_cycle_name": { + "name": "external_cycle_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_external_id": { + "name": "assignee_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_display_name": { + "name": "assignee_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_avatar_url": { + "name": "assignee_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_slug_idx": { + "name": "tasks_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_organization_id_idx": { + "name": "tasks_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_assignee_id_idx": { + "name": "tasks_assignee_id_idx", + "columns": [ + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_creator_id_idx": { + "name": "tasks_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_id_idx": { + "name": "tasks_status_id_idx", + "columns": [ + { + "expression": "status_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_created_at_idx": { + "name": "tasks_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_external_provider_idx": { + "name": "tasks_external_provider_idx", + "columns": [ + { + "expression": "external_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_external_project_id_idx": { + "name": "tasks_external_project_id_idx", + "columns": [ + { + "expression": "external_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_external_project_name_idx": { + "name": "tasks_external_project_name_idx", + "columns": [ + { + "expression": "external_project_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_external_cycle_id_idx": { + "name": "tasks_external_cycle_id_idx", + "columns": [ + { + "expression": "external_cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_assignee_external_id_idx": { + "name": "tasks_assignee_external_id_idx", + "columns": [ + { + "expression": "assignee_external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_status_id_task_statuses_id_fk": { + "name": "tasks_status_id_task_statuses_id_fk", + "tableFrom": "tasks", + "tableTo": "task_statuses", + "columnsFrom": [ + "status_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_organization_id_organizations_id_fk": { + "name": "tasks_organization_id_organizations_id_fk", + "tableFrom": "tasks", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_assignee_id_users_id_fk": { + "name": "tasks_assignee_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_creator_id_users_id_fk": { + "name": "tasks_creator_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tasks_external_unique": { + "name": "tasks_external_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "external_provider", + "external_id" + ] + }, + "tasks_org_slug_unique": { + "name": "tasks_org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_identities": { + "name": "user_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_scope_id": { + "name": "external_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_identities_user_idx": { + "name": "user_identities_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_identities_org_provider_idx": { + "name": "user_identities_org_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_identities_user_id_users_id_fk": { + "name": "user_identities_user_id_users_id_fk", + "tableFrom": "user_identities", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_identities_organization_id_organizations_id_fk": { + "name": "user_identities_organization_id_organizations_id_fk", + "tableFrom": "user_identities", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_identities_account_unique": { + "name": "user_identities_account_unique", + "nullsNotDistinct": true, + "columns": [ + "organization_id", + "provider", + "external_scope_id", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_clients": { + "name": "v2_clients", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "v2_client_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_clients_organization_id_idx": { + "name": "v2_clients_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_clients_user_id_idx": { + "name": "v2_clients_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_clients_organization_id_organizations_id_fk": { + "name": "v2_clients_organization_id_organizations_id_fk", + "tableFrom": "v2_clients", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_clients_user_id_users_id_fk": { + "name": "v2_clients_user_id_users_id_fk", + "tableFrom": "v2_clients", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "v2_clients_organization_id_user_id_machine_id_pk": { + "name": "v2_clients_organization_id_user_id_machine_id_pk", + "columns": [ + "organization_id", + "user_id", + "machine_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_hosts": { + "name": "v2_hosts", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wake_command": { + "name": "wake_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "install_source": { + "name": "install_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_hosts_organization_id_idx": { + "name": "v2_hosts_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_hosts_organization_id_organizations_id_fk": { + "name": "v2_hosts_organization_id_organizations_id_fk", + "tableFrom": "v2_hosts", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_hosts_created_by_user_id_users_id_fk": { + "name": "v2_hosts_created_by_user_id_users_id_fk", + "tableFrom": "v2_hosts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "v2_hosts_organization_id_machine_id_pk": { + "name": "v2_hosts_organization_id_machine_id_pk", + "columns": [ + "organization_id", + "machine_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_projects": { + "name": "v2_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_clone_url": { + "name": "repo_clone_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_projects_organization_id_idx": { + "name": "v2_projects_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_projects_organization_id_organizations_id_fk": { + "name": "v2_projects_organization_id_organizations_id_fk", + "tableFrom": "v2_projects", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_projects_github_repository_id_github_repositories_id_fk": { + "name": "v2_projects_github_repository_id_github_repositories_id_fk", + "tableFrom": "v2_projects", + "tableTo": "github_repositories", + "columnsFrom": [ + "github_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "v2_projects_org_slug_unique": { + "name": "v2_projects_org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_users_hosts": { + "name": "v2_users_hosts", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "v2_users_host_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_users_hosts_organization_id_idx": { + "name": "v2_users_hosts_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_users_hosts_user_id_idx": { + "name": "v2_users_hosts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_users_hosts_host_id_idx": { + "name": "v2_users_hosts_host_id_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_users_hosts_organization_id_organizations_id_fk": { + "name": "v2_users_hosts_organization_id_organizations_id_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_users_hosts_user_id_users_id_fk": { + "name": "v2_users_hosts_user_id_users_id_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_users_hosts_host_fk": { + "name": "v2_users_hosts_host_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "v2_hosts", + "columnsFrom": [ + "organization_id", + "host_id" + ], + "columnsTo": [ + "organization_id", + "machine_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "v2_users_hosts_organization_id_user_id_host_id_pk": { + "name": "v2_users_hosts_organization_id_user_id_host_id_pk", + "columns": [ + "organization_id", + "user_id", + "host_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_workspaces": { + "name": "v2_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "v2_workspace_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'worktree'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_workspaces_project_id_idx": { + "name": "v2_workspaces_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_organization_id_idx": { + "name": "v2_workspaces_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_host_id_idx": { + "name": "v2_workspaces_host_id_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_task_id_idx": { + "name": "v2_workspaces_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_one_main_per_host": { + "name": "v2_workspaces_one_main_per_host", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"v2_workspaces\".\"type\" = 'main'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_workspaces_organization_id_organizations_id_fk": { + "name": "v2_workspaces_organization_id_organizations_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_workspaces_created_by_user_id_users_id_fk": { + "name": "v2_workspaces_created_by_user_id_users_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "v2_workspaces_task_id_tasks_id_fk": { + "name": "v2_workspaces_task_id_tasks_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "v2_workspaces_host_fk": { + "name": "v2_workspaces_host_fk", + "tableFrom": "v2_workspaces", + "tableTo": "v2_hosts", + "columnsFrom": [ + "organization_id", + "host_id" + ], + "columnsTo": [ + "organization_id", + "machine_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_pages": { + "name": "workspace_pages", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entry_path": { + "name": "entry_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_pages_workspace_id_entry_path_unique": { + "name": "workspace_pages_workspace_id_entry_path_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entry_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_pages_page_id_idx": { + "name": "workspace_pages_page_id_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_pages_page_id_pages_id_fk": { + "name": "workspace_pages_page_id_pages_id_fk", + "tableFrom": "workspace_pages", + "tableTo": "pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_pages_workspace_id_page_id_pk": { + "name": "workspace_pages_workspace_id_page_id_pk", + "columns": [ + "workspace_id", + "page_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "workspace_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspaces_project_id_idx": { + "name": "workspaces_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_organization_id_idx": { + "name": "workspaces_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_type_idx": { + "name": "workspaces_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspaces_organization_id_organizations_id_fk": { + "name": "workspaces_organization_id_organizations_id_fk", + "tableFrom": "workspaces", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspaces_project_id_projects_id_fk": { + "name": "workspaces_project_id_projects_id_fk", + "tableFrom": "workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspaces_created_by_user_id_users_id_fk": { + "name": "workspaces_created_by_user_id_users_id_fk", + "tableFrom": "workspaces", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_thread_sessions": { + "name": "slack_thread_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_ts": { + "name": "thread_ts", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "quiet": { + "name": "quiet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stop_requested_at": { + "name": "stop_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_context_ts": { + "name": "last_context_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_log": { + "name": "entity_log", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "queued_events": { + "name": "queued_events", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "slack_thread_sessions_organization_id_organizations_id_fk": { + "name": "slack_thread_sessions_organization_id_organizations_id_fk", + "tableFrom": "slack_thread_sessions", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_thread_sessions_started_by_user_id_users_id_fk": { + "name": "slack_thread_sessions_started_by_user_id_users_id_fk", + "tableFrom": "slack_thread_sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "started_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "slack_thread_sessions_thread_unique": { + "name": "slack_thread_sessions_thread_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "team_id", + "channel_id", + "thread_ts" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.handle_owner_type": { + "name": "handle_owner_type", + "schema": "public", + "values": [ + "user", + "organization", + "reserved" + ] + }, + "public.leaderboard_visibility": { + "name": "leaderboard_visibility", + "schema": "public", + "values": [ + "public", + "hidden" + ] + }, + "public.agent_credential_kind": { + "name": "agent_credential_kind", + "schema": "public", + "values": [ + "subscription", + "api_key" + ] + }, + "public.attachment_parent_kind": { + "name": "attachment_parent_kind", + "schema": "public", + "values": [ + "page_version", + "issue", + "doc", + "chat_session", + "comment", + "page" + ] + }, + "public.automation_prompt_source": { + "name": "automation_prompt_source", + "schema": "public", + "values": [ + "human", + "agent", + "restore" + ] + }, + "public.automation_run_error_code": { + "name": "automation_run_error_code", + "schema": "public", + "values": [ + "no_instructions", + "host_offline", + "agent_not_found", + "workspace_not_found" + ] + }, + "public.automation_run_status": { + "name": "automation_run_status", + "schema": "public", + "values": [ + "dispatching", + "dispatched", + "skipped_offline", + "dispatch_failed", + "debounced", + "rejected" + ] + }, + "public.automation_session_kind": { + "name": "automation_session_kind", + "schema": "public", + "values": [ + "chat", + "terminal" + ] + }, + "public.automation_trigger_kind": { + "name": "automation_trigger_kind", + "schema": "public", + "values": [ + "schedule", + "webhook", + "github", + "slack", + "linear", + "sentry", + "microsoft_teams", + "google_calendar", + "gmail", + "notion" + ] + }, + "public.cloud_workspace_status": { + "name": "cloud_workspace_status", + "schema": "public", + "values": [ + "provisioning", + "ready", + "failed", + "deleted" + ] + }, + "public.command_status": { + "name": "command_status", + "schema": "public", + "values": [ + "pending", + "completed", + "failed", + "timeout" + ] + }, + "public.desktop_notice_cta_action": { + "name": "desktop_notice_cta_action", + "schema": "public", + "values": [ + "install-update", + "open-url" + ] + }, + "public.desktop_notice_severity": { + "name": "desktop_notice_severity", + "schema": "public", + "values": [ + "info", + "warning", + "blocking" + ] + }, + "public.desktop_notice_trigger": { + "name": "desktop_notice_trigger", + "schema": "public", + "values": [ + "immediate", + "pre-update", + "post-update" + ] + }, + "public.environment_scope": { + "name": "environment_scope", + "schema": "public", + "values": [ + "organization", + "personal" + ] + }, + "public.environment_source_kind": { + "name": "environment_source_kind", + "schema": "public", + "values": [ + "image", + "fork" + ] + }, + "public.file_status": { + "name": "file_status", + "schema": "public", + "values": [ + "pending", + "ready" + ] + }, + "public.integration_provider": { + "name": "integration_provider", + "schema": "public", + "values": [ + "linear", + "github", + "slack", + "sentry", + "microsoft_teams", + "google", + "notion" + ] + }, + "public.page_comment_anchor_kind": { + "name": "page_comment_anchor_kind", + "schema": "public", + "values": [ + "element", + "text", + "page" + ] + }, + "public.page_comment_author_kind": { + "name": "page_comment_author_kind", + "schema": "public", + "values": [ + "human", + "agent" + ] + }, + "public.page_comment_intent": { + "name": "page_comment_intent", + "schema": "public", + "values": [ + "delete", + "approve" + ] + }, + "public.page_visibility": { + "name": "page_visibility", + "schema": "public", + "values": [ + "just_me", + "org", + "everyone" + ] + }, + "public.task_priority": { + "name": "task_priority", + "schema": "public", + "values": [ + "urgent", + "high", + "medium", + "low", + "none" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "backlog", + "todo", + "planning", + "working", + "needs-feedback", + "ready-to-merge", + "completed", + "canceled" + ] + }, + "public.v2_client_type": { + "name": "v2_client_type", + "schema": "public", + "values": [ + "desktop", + "mobile", + "web" + ] + }, + "public.v2_users_host_role": { + "name": "v2_users_host_role", + "schema": "public", + "values": [ + "owner", + "member" + ] + }, + "public.v2_workspace_type": { + "name": "v2_workspace_type", + "schema": "public", + "values": [ + "main", + "worktree" + ] + }, + "public.workspace_type": { + "name": "workspace_type", + "schema": "public", + "values": [ + "local", + "cloud" + ] + } + }, + "schemas": { + "auth": "auth", + "ingest": "ingest" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index b1dc74be227..b450cd62036 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -841,6 +841,13 @@ "when": 1789625882184, "tag": "0119_plugin_oauth_clients_and_connectors", "breakpoints": true + }, + { + "idx": 120, + "version": "7", + "when": 1790142341751, + "tag": "0120_pages_created_at_keyset_index", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/schema.ts b/packages/db/src/schema/schema.ts index a595483be72..179121115c3 100644 --- a/packages/db/src/schema/schema.ts +++ b/packages/db/src/schema/schema.ts @@ -1465,9 +1465,10 @@ export const pages = pgTable( }, (table) => [ uniqueIndex("pages_slug_unique").on(table.slug), - index("pages_organization_id_updated_at_idx").on( + index("pages_organization_id_created_at_id_idx").on( table.organizationId, - desc(table.updatedAt), + desc(table.createdAt), + desc(table.id), ), index("pages_created_by_user_id_idx").on(table.createdByUserId), ], diff --git a/packages/db/src/task-list-query.ts b/packages/db/src/task-list-query.ts index ad705bc0d71..519a0947475 100644 --- a/packages/db/src/task-list-query.ts +++ b/packages/db/src/task-list-query.ts @@ -15,6 +15,7 @@ import { import { QueryBuilder } from "drizzle-orm/pg-core"; import { taskStatuses, tasks } from "./schema"; import { type TaskPriority, taskPriorityValues } from "./schema/enums"; +import { escapeLikePattern } from "./utils/like"; export const taskStatusTypeValues = [ "backlog", @@ -53,10 +54,6 @@ export interface TaskListFilters { dueDateTo?: Date; } -function escapeLikePattern(value: string): string { - return value.replace(/[\\%_]/g, (match) => `\\${match}`); -} - export function buildTaskListConditions( filters: TaskListFilters, ): SQL[] { diff --git a/packages/db/src/utils/index.ts b/packages/db/src/utils/index.ts index 60c8874cf7e..4d012e55063 100644 --- a/packages/db/src/utils/index.ts +++ b/packages/db/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from "./like"; export * from "./membership"; export * from "./provider-identity"; export * from "./sql"; diff --git a/packages/db/src/utils/like.ts b/packages/db/src/utils/like.ts new file mode 100644 index 00000000000..7258e3051a1 --- /dev/null +++ b/packages/db/src/utils/like.ts @@ -0,0 +1,7 @@ +/** + * Escapes the wildcards in a user-supplied LIKE/ILIKE term, so searching for + * "100%" matches that text instead of every row. + */ +export function escapeLikePattern(value: string): string { + return value.replace(/[\\%_]/g, (match) => `\\${match}`); +} diff --git a/packages/i18n/locales/cs/messages.po b/packages/i18n/locales/cs/messages.po index 9f0d579ce96..bad3b52d975 100644 --- a/packages/i18n/locales/cs/messages.po +++ b/packages/i18n/locales/cs/messages.po @@ -6610,6 +6610,9 @@ msgstr "Filtrovat podle revizí" msgid "Filter by state" msgstr "Filtrovat podle stavu" +msgid "Filter by workspace: {label}" +msgstr "Filtrovat podle pracovního prostoru: {label}" + msgid "Filter hosts" msgstr "Filtrovat hosty" @@ -15999,6 +16002,9 @@ msgstr "Neznámé proměnné: {unknownVariables}" msgid "Unknown Welder" msgstr "Neznámý svářeč" +msgid "Unknown workspace" +msgstr "Neznámý pracovní prostor" + msgid "Unlimited" msgstr "Neomezeně" diff --git a/packages/i18n/locales/de/messages.po b/packages/i18n/locales/de/messages.po index 77be911fa3e..2318e6020eb 100644 --- a/packages/i18n/locales/de/messages.po +++ b/packages/i18n/locales/de/messages.po @@ -6610,6 +6610,9 @@ msgstr "Nach Reviews filtern" msgid "Filter by state" msgstr "Nach Status filtern" +msgid "Filter by workspace: {label}" +msgstr "Nach Arbeitsbereich filtern: {label}" + msgid "Filter hosts" msgstr "Hosts filtern" @@ -15999,6 +16002,9 @@ msgstr "Unbekannte Variablen: {unknownVariables}" msgid "Unknown Welder" msgstr "Unbekannter Schweißer" +msgid "Unknown workspace" +msgstr "Unbekannter Arbeitsbereich" + msgid "Unlimited" msgstr "Unbegrenzt" diff --git a/packages/i18n/locales/en/messages.po b/packages/i18n/locales/en/messages.po index b5d457d4c18..e49882d939a 100644 --- a/packages/i18n/locales/en/messages.po +++ b/packages/i18n/locales/en/messages.po @@ -6610,6 +6610,9 @@ msgstr "Filter by reviews" msgid "Filter by state" msgstr "Filter by state" +msgid "Filter by workspace: {label}" +msgstr "Filter by workspace: {label}" + msgid "Filter hosts" msgstr "Filter hosts" @@ -15999,6 +16002,9 @@ msgstr "Unknown variables: {unknownVariables}" msgid "Unknown Welder" msgstr "Unknown Welder" +msgid "Unknown workspace" +msgstr "Unknown workspace" + msgid "Unlimited" msgstr "Unlimited" diff --git a/packages/i18n/locales/es/messages.po b/packages/i18n/locales/es/messages.po index 70f2b97405c..da33b601014 100644 --- a/packages/i18n/locales/es/messages.po +++ b/packages/i18n/locales/es/messages.po @@ -6610,6 +6610,9 @@ msgstr "Filtrar por revisiones" msgid "Filter by state" msgstr "Filtrar por estado" +msgid "Filter by workspace: {label}" +msgstr "Filtrar por espacio de trabajo: {label}" + msgid "Filter hosts" msgstr "Filtrar hosts" @@ -15999,6 +16002,9 @@ msgstr "Variables desconocidas: {unknownVariables}" msgid "Unknown Welder" msgstr "Soldador desconocido" +msgid "Unknown workspace" +msgstr "Espacio de trabajo desconocido" + msgid "Unlimited" msgstr "Ilimitado" diff --git a/packages/i18n/locales/fr/messages.po b/packages/i18n/locales/fr/messages.po index ed78b1bdabc..5a281b8c04e 100644 --- a/packages/i18n/locales/fr/messages.po +++ b/packages/i18n/locales/fr/messages.po @@ -6610,6 +6610,9 @@ msgstr "Filtrer par revues" msgid "Filter by state" msgstr "Filtrer par état" +msgid "Filter by workspace: {label}" +msgstr "Filtrer par espace de travail : {label}" + msgid "Filter hosts" msgstr "Filtrer les hôtes" @@ -15999,6 +16002,9 @@ msgstr "Variables inconnues : {unknownVariables}" msgid "Unknown Welder" msgstr "Soudeur inconnu" +msgid "Unknown workspace" +msgstr "Espace de travail inconnu" + msgid "Unlimited" msgstr "Illimité" diff --git a/packages/i18n/locales/id/messages.po b/packages/i18n/locales/id/messages.po index 42e0b637771..b0b12a270fd 100644 --- a/packages/i18n/locales/id/messages.po +++ b/packages/i18n/locales/id/messages.po @@ -6610,6 +6610,9 @@ msgstr "Filter menurut review" msgid "Filter by state" msgstr "Filter menurut status" +msgid "Filter by workspace: {label}" +msgstr "Filter menurut workspace: {label}" + msgid "Filter hosts" msgstr "Filter host" @@ -15999,6 +16002,9 @@ msgstr "Variabel tidak dikenal: {unknownVariables}" msgid "Unknown Welder" msgstr "Tukang Las Tak Dikenal" +msgid "Unknown workspace" +msgstr "Workspace tidak dikenal" + msgid "Unlimited" msgstr "Tanpa batas" diff --git a/packages/i18n/locales/it/messages.po b/packages/i18n/locales/it/messages.po index ca08d4b2e04..25f9de0663f 100644 --- a/packages/i18n/locales/it/messages.po +++ b/packages/i18n/locales/it/messages.po @@ -6610,6 +6610,9 @@ msgstr "Filtra per revisioni" msgid "Filter by state" msgstr "Filtra per stato" +msgid "Filter by workspace: {label}" +msgstr "Filtra per workspace: {label}" + msgid "Filter hosts" msgstr "Filtra gli host" @@ -15999,6 +16002,9 @@ msgstr "Variabili sconosciute: {unknownVariables}" msgid "Unknown Welder" msgstr "Saldatore Sconosciuto" +msgid "Unknown workspace" +msgstr "Workspace sconosciuto" + msgid "Unlimited" msgstr "Illimitati" diff --git a/packages/i18n/locales/ja/messages.po b/packages/i18n/locales/ja/messages.po index 8eb06b1b01d..524ec1c732c 100644 --- a/packages/i18n/locales/ja/messages.po +++ b/packages/i18n/locales/ja/messages.po @@ -6610,6 +6610,9 @@ msgstr "レビューで絞り込む" msgid "Filter by state" msgstr "状態で絞り込む" +msgid "Filter by workspace: {label}" +msgstr "ワークスペースで絞り込み: {label}" + msgid "Filter hosts" msgstr "ホストを絞り込み" @@ -15999,6 +16002,9 @@ msgstr "不明な変数: {unknownVariables}" msgid "Unknown Welder" msgstr "正体不明の溶接工" +msgid "Unknown workspace" +msgstr "不明なワークスペース" + msgid "Unlimited" msgstr "無制限" diff --git a/packages/i18n/locales/ko/messages.po b/packages/i18n/locales/ko/messages.po index 6d4b732c758..685a76ec488 100644 --- a/packages/i18n/locales/ko/messages.po +++ b/packages/i18n/locales/ko/messages.po @@ -6610,6 +6610,9 @@ msgstr "리뷰로 필터링" msgid "Filter by state" msgstr "상태로 필터링" +msgid "Filter by workspace: {label}" +msgstr "워크스페이스별 필터: {label}" + msgid "Filter hosts" msgstr "호스트 필터" @@ -15999,6 +16002,9 @@ msgstr "알 수 없는 변수: {unknownVariables}" msgid "Unknown Welder" msgstr "미지의 용접공" +msgid "Unknown workspace" +msgstr "알 수 없는 워크스페이스" + msgid "Unlimited" msgstr "무제한" diff --git a/packages/i18n/locales/nl/messages.po b/packages/i18n/locales/nl/messages.po index 6b4b1970f30..5f24f7aefac 100644 --- a/packages/i18n/locales/nl/messages.po +++ b/packages/i18n/locales/nl/messages.po @@ -6610,6 +6610,9 @@ msgstr "Filteren op reviews" msgid "Filter by state" msgstr "Filteren op status" +msgid "Filter by workspace: {label}" +msgstr "Filteren op werkruimte: {label}" + msgid "Filter hosts" msgstr "Hosts filteren" @@ -15999,6 +16002,9 @@ msgstr "Onbekende variabelen: {unknownVariables}" msgid "Unknown Welder" msgstr "Onbekende Lasser" +msgid "Unknown workspace" +msgstr "Onbekende werkruimte" + msgid "Unlimited" msgstr "Onbeperkt" diff --git a/packages/i18n/locales/pl/messages.po b/packages/i18n/locales/pl/messages.po index 7d50ca2474e..77f8488dede 100644 --- a/packages/i18n/locales/pl/messages.po +++ b/packages/i18n/locales/pl/messages.po @@ -6610,6 +6610,9 @@ msgstr "Filtruj wg przeglądów" msgid "Filter by state" msgstr "Filtruj wg stanu" +msgid "Filter by workspace: {label}" +msgstr "Filtruj według obszaru roboczego: {label}" + msgid "Filter hosts" msgstr "Filtruj hosty" @@ -15999,6 +16002,9 @@ msgstr "Nieznane zmienne: {unknownVariables}" msgid "Unknown Welder" msgstr "Nieznany Spawacz" +msgid "Unknown workspace" +msgstr "Nieznany obszar roboczy" + msgid "Unlimited" msgstr "Bez limitu" diff --git a/packages/i18n/locales/pt-BR/messages.po b/packages/i18n/locales/pt-BR/messages.po index 3ed462382cc..b132f9253a2 100644 --- a/packages/i18n/locales/pt-BR/messages.po +++ b/packages/i18n/locales/pt-BR/messages.po @@ -6610,6 +6610,9 @@ msgstr "Filtrar por revisões" msgid "Filter by state" msgstr "Filtrar por estado" +msgid "Filter by workspace: {label}" +msgstr "Filtrar por área de trabalho: {label}" + msgid "Filter hosts" msgstr "Filtrar hosts" @@ -15999,6 +16002,9 @@ msgstr "Variáveis desconhecidas: {unknownVariables}" msgid "Unknown Welder" msgstr "Soldador Desconhecido" +msgid "Unknown workspace" +msgstr "Área de trabalho desconhecida" + msgid "Unlimited" msgstr "Ilimitado" diff --git a/packages/i18n/locales/ru/messages.po b/packages/i18n/locales/ru/messages.po index 947e329b152..740259b6445 100644 --- a/packages/i18n/locales/ru/messages.po +++ b/packages/i18n/locales/ru/messages.po @@ -6610,6 +6610,9 @@ msgstr "Фильтр по ревью" msgid "Filter by state" msgstr "Фильтр по состоянию" +msgid "Filter by workspace: {label}" +msgstr "Фильтр по рабочей области: {label}" + msgid "Filter hosts" msgstr "Фильтр хостов" @@ -15999,6 +16002,9 @@ msgstr "Неизвестные переменные: {unknownVariables}" msgid "Unknown Welder" msgstr "Неизвестный сварщик" +msgid "Unknown workspace" +msgstr "Неизвестная рабочая область" + msgid "Unlimited" msgstr "Без ограничений" diff --git a/packages/i18n/locales/tr/messages.po b/packages/i18n/locales/tr/messages.po index d95cd903ec7..dcc795304ba 100644 --- a/packages/i18n/locales/tr/messages.po +++ b/packages/i18n/locales/tr/messages.po @@ -6610,6 +6610,9 @@ msgstr "İncelemeye göre filtrele" msgid "Filter by state" msgstr "Duruma göre filtrele" +msgid "Filter by workspace: {label}" +msgstr "Çalışma alanına göre filtrele: {label}" + msgid "Filter hosts" msgstr "Host'ları filtrele" @@ -15999,6 +16002,9 @@ msgstr "Bilinmeyen değişkenler: {unknownVariables}" msgid "Unknown Welder" msgstr "Bilinmeyen Kaynakçı" +msgid "Unknown workspace" +msgstr "Bilinmeyen çalışma alanı" + msgid "Unlimited" msgstr "Sınırsız" diff --git a/packages/i18n/locales/vi/messages.po b/packages/i18n/locales/vi/messages.po index f0b30f461f9..13d1ed626c3 100644 --- a/packages/i18n/locales/vi/messages.po +++ b/packages/i18n/locales/vi/messages.po @@ -6610,6 +6610,9 @@ msgstr "Lọc theo review" msgid "Filter by state" msgstr "Lọc theo trạng thái" +msgid "Filter by workspace: {label}" +msgstr "Lọc theo workspace: {label}" + msgid "Filter hosts" msgstr "Lọc host" @@ -15999,6 +16002,9 @@ msgstr "Biến không xác định: {unknownVariables}" msgid "Unknown Welder" msgstr "Thợ hàn vô danh" +msgid "Unknown workspace" +msgstr "Workspace không xác định" + msgid "Unlimited" msgstr "Không giới hạn" diff --git a/packages/i18n/locales/zh-CN/messages.po b/packages/i18n/locales/zh-CN/messages.po index 54ff4a3b219..977ee70e8d2 100644 --- a/packages/i18n/locales/zh-CN/messages.po +++ b/packages/i18n/locales/zh-CN/messages.po @@ -6610,6 +6610,9 @@ msgstr "按审查筛选" msgid "Filter by state" msgstr "按状态筛选" +msgid "Filter by workspace: {label}" +msgstr "按工作区筛选:{label}" + msgid "Filter hosts" msgstr "筛选主机" @@ -15999,6 +16002,9 @@ msgstr "未知变量:{unknownVariables}" msgid "Unknown Welder" msgstr "未知焊工" +msgid "Unknown workspace" +msgstr "未知工作区" + msgid "Unlimited" msgstr "无限" diff --git a/packages/i18n/locales/zh-TW/messages.po b/packages/i18n/locales/zh-TW/messages.po index c906727146e..c407902b534 100644 --- a/packages/i18n/locales/zh-TW/messages.po +++ b/packages/i18n/locales/zh-TW/messages.po @@ -6610,6 +6610,9 @@ msgstr "按審查篩選" msgid "Filter by state" msgstr "按狀態篩選" +msgid "Filter by workspace: {label}" +msgstr "依工作區篩選:{label}" + msgid "Filter hosts" msgstr "篩選主機" @@ -15999,6 +16002,9 @@ msgstr "未知變數:{unknownVariables}" msgid "Unknown Welder" msgstr "未知焊接工" +msgid "Unknown workspace" +msgstr "未知工作區" + msgid "Unlimited" msgstr "無限" diff --git a/packages/mcp/src/tools/pages/list.ts b/packages/mcp/src/tools/pages/list.ts index 46782efd82c..149e6b0deec 100644 --- a/packages/mcp/src/tools/pages/list.ts +++ b/packages/mcp/src/tools/pages/list.ts @@ -1,4 +1,9 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + PAGE_LIST_DEFAULT_LIMIT, + PAGE_LIST_MAX_LIMIT, + PAGE_LIST_SCOPES, +} from "@superset/trpc/page-schema"; import { z } from "zod"; import { createMcpCaller } from "../../caller"; import { defineTool } from "../../define-tool"; @@ -8,7 +13,7 @@ export function register(server: McpServer): void { name: "pages_list", annotations: { readOnlyHint: true }, description: - "List published pages in the active organization, newest first. A page is one self-contained HTML file published to a shareable URL. Returns only pages the caller may read: everything shared with the organization, plus the caller's own private ones. Use this to find a page's id or slug before calling any other pages_* tool.", + "List published pages in the active organization, newest-created first. A page is one self-contained HTML file published to a shareable URL. Returns only pages the caller may read: everything shared with the organization, plus the caller's own private ones. Use this to find a page's id or slug before calling any other pages_* tool. Ordering is by creation, so republishing a page does not move it. Results are paginated: pass `search` to narrow by title, slug or description, and when `nextCursor` comes back non-null pass it as `cursor` to get the next batch.", inputSchema: { workspaceId: z .string() @@ -17,12 +22,46 @@ export function register(server: McpServer): void { .describe( "Only pages published from this workspace. Omit for every page in the organization.", ), + search: z + .string() + .min(1) + .max(200) + .nullish() + .describe( + "Only pages whose title, slug or description contains this text, case-insensitive.", + ), + limit: z + .number() + .int() + .min(1) + .max(PAGE_LIST_MAX_LIMIT) + .nullish() + .describe( + `How many pages to return, 1-${PAGE_LIST_MAX_LIMIT} (default ${PAGE_LIST_DEFAULT_LIMIT}).`, + ), + scope: z + .enum(PAGE_LIST_SCOPES) + .nullish() + .describe( + "`team` for pages shared with the organization, `mine` for the caller's private ones, `all` for both (default).", + ), + cursor: z + .string() + .max(256) + .nullish() + .describe( + "The `nextCursor` string from a previous call, passed back unchanged, to fetch the next batch. Omit for the first batch. Treat it as opaque.", + ), }, handler: async (input, ctx) => { const caller = createMcpCaller(ctx); - return caller.page.list( - input?.workspaceId ? { workspaceId: input.workspaceId } : undefined, - ); + return caller.page.list({ + ...(input?.workspaceId ? { workspaceId: input.workspaceId } : {}), + ...(input?.search ? { search: input.search } : {}), + ...(input?.scope ? { scope: input.scope } : {}), + ...(input?.limit ? { limit: input.limit } : {}), + ...(input?.cursor ? { cursor: input.cursor } : {}), + }); }, }); } diff --git a/packages/trpc/src/router/automation/automation.ts b/packages/trpc/src/router/automation/automation.ts index da60f91f2b0..ee104165e2a 100644 --- a/packages/trpc/src/router/automation/automation.ts +++ b/packages/trpc/src/router/automation/automation.ts @@ -7,6 +7,7 @@ import { v2UsersHosts, v2Workspaces, } from "@superset/db/schema"; +import { escapeLikePattern } from "@superset/db/utils"; import type { DraftTrigger } from "@superset/shared/automation-triggers"; import { AUTOMATIONS_REQUIRED_PLAN, @@ -74,10 +75,6 @@ async function requireAutomationsPlan( return organizationId; } -function escapeLikePattern(value: string): string { - return value.replace(/[\\%_]/g, (match) => `\\${match}`); -} - async function verifyHostAccess( userId: string, organizationId: string, diff --git a/packages/trpc/src/router/leaderboard/queries.ts b/packages/trpc/src/router/leaderboard/queries.ts index 4776148cde1..ce822fb3dd7 100644 --- a/packages/trpc/src/router/leaderboard/queries.ts +++ b/packages/trpc/src/router/leaderboard/queries.ts @@ -5,6 +5,7 @@ import { publicProfiles, users, } from "@superset/db/schema"; +import { escapeLikePattern } from "@superset/db/utils"; import { and, desc, eq, gt, gte, isNull, lte, sql } from "drizzle-orm"; import { type LeaderboardPeriod, resolveWindow } from "./periods"; import { type Tier, tierProgress } from "./tier"; @@ -377,9 +378,6 @@ export async function listPublicHandles(): Promise< export const SEARCH_MIN_LENGTH = 2; -const escapeLike = (value: string) => - value.replace(/[\\%_]/g, (char) => `\\${char}`); - interface SearchRow extends Record { handle: string; name: string | null; @@ -406,8 +404,8 @@ export async function searchParticipants( const range = resolveWindow(opts); const byCost = opts.metric === "cost"; - const prefix = `${escapeLike(query)}%`; - const contains = `%${escapeLike(query)}%`; + const prefix = `${escapeLikePattern(query)}%`; + const contains = `%${escapeLikePattern(query)}%`; const take = Math.min(limit, SEARCH_LIMIT); const ranked = range diff --git a/packages/trpc/src/router/page-comment/list-for-organization.integration.ts b/packages/trpc/src/router/page-comment/list-for-organization.integration.ts new file mode 100644 index 00000000000..a236a5e6441 --- /dev/null +++ b/packages/trpc/src/router/page-comment/list-for-organization.integration.ts @@ -0,0 +1,261 @@ +import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test"; +import { createHash } from "node:crypto"; + +mock.module("../../lib/r2", () => ({ + presignedGetUrl: async (key: string) => `https://storage.test/${key}`, + presignedPutUrl: async ({ key }: { key: string }) => ({ + url: `https://storage.test/${key}`, + headers: {}, + }), + putObject: async () => {}, + getObject: async () => null, + headObject: async () => null, + copyObject: async () => {}, + deleteObjects: async () => {}, + objectExists: async () => true, +})); + +const { db, dbWs } = await import("@superset/db/client"); +const { + members, + organizations, + pageComments, + pageCommentThreads, + pages, + pageVersions, + users, + workspacePages, +} = await import("@superset/db/schema"); +const { eq } = await import("drizzle-orm"); +const { pageCommentRouter } = await import("./page-comment"); +const { createCallerFactory, createTRPCContext, createTRPCRouter } = + await import("../../trpc"); + +const ORG = crypto.randomUUID(); +const USER = crypto.randomUUID(); +const OTHER_USER = crypto.randomUUID(); +const WORKSPACE = crypto.randomUUID(); +const suffix = Date.now(); + +const callerFor = (userId: string) => + createCallerFactory(createTRPCRouter({ pageComment: pageCommentRouter }))( + createTRPCContext({ + session: { + user: { id: userId, email: `u-${userId}@example.com` }, + session: { activeOrganizationId: ORG }, + } as never, + auth: {} as never, + headers: new Headers(), + }), + ); + +const caller = callerFor(USER); + +/** id of each page, keyed by the label the test refers to it by. */ +const pageIds: Record = {}; +const threadIds: Record = {}; + +async function seedPage( + label: string, + { + visibility, + owner, + inWorkspace = false, + }: { + visibility: "org" | "just_me"; + owner: string; + inWorkspace?: boolean; + }, +) { + const [page] = await db + .insert(pages) + .values({ + slug: `comment-sweep-${suffix}-${label}`, + organizationId: ORG, + createdByUserId: owner, + title: `Page ${label}`, + visibility, + }) + .returning({ id: pages.id }); + if (!page) throw new Error("failed to insert page"); + pageIds[label] = page.id; + + const [version] = await db + .insert(pageVersions) + .values({ + pageId: page.id, + version: 1, + storageKey: `pages/${page.id}/1/index.html`, + contentType: "text/html", + sizeBytes: 10, + sha256: createHash("sha256").update(label).digest("hex"), + createdByUserId: owner, + }) + .returning({ id: pageVersions.id }); + if (!version) throw new Error("failed to insert version"); + + if (inWorkspace) { + await db.insert(workspacePages).values({ + pageId: page.id, + workspaceId: WORKSPACE, + entryPath: `/${label}.html`, + }); + } + + const [thread] = await db + .insert(pageCommentThreads) + .values({ + pageId: page.id, + pageVersionId: version.id, + anchorKind: "page", + anchor: null, + createdByUserId: owner, + ...(label === "resolved" ? { resolvedAt: new Date() } : {}), + ...(label === "activated" ? { agentActivatedAt: new Date() } : {}), + }) + .returning({ id: pageCommentThreads.id }); + if (!thread) throw new Error("failed to insert thread"); + threadIds[label] = thread.id; + + await db.insert(pageComments).values({ + threadId: thread.id, + body: `comment on ${label}`, + authorKind: "human", + authorUserId: owner, + }); +} + +beforeAll(async () => { + await db.insert(organizations).values({ + id: ORG, + name: "Sweep Org", + slug: `sweep-org-${suffix}`, + }); + await db.insert(users).values([ + { + id: USER, + name: "Sweep User", + email: `sweep-${suffix}@example.com`, + organizationIds: [ORG], + }, + { + id: OTHER_USER, + name: "Other User", + email: `sweep-other-${suffix}@example.com`, + organizationIds: [ORG], + }, + ]); + await db.insert(members).values([ + { + id: crypto.randomUUID(), + organizationId: ORG, + userId: USER, + role: "owner", + createdAt: new Date(), + }, + { + id: crypto.randomUUID(), + organizationId: ORG, + userId: OTHER_USER, + role: "member", + createdAt: new Date(), + }, + ]); + + await seedPage("shared", { visibility: "org", owner: OTHER_USER }); + await seedPage("mine", { visibility: "just_me", owner: USER }); + await seedPage("theirs", { visibility: "just_me", owner: OTHER_USER }); + await seedPage("linked", { + visibility: "org", + owner: USER, + inWorkspace: true, + }); + await seedPage("resolved", { visibility: "org", owner: USER }); + await seedPage("activated", { visibility: "org", owner: USER }); +}); + +afterAll(async () => { + await db.delete(organizations).where(eq(organizations.id, ORG)); + await db.delete(users).where(eq(users.id, USER)); + await db.delete(users).where(eq(users.id, OTHER_USER)); + // Guarded: the pooled client is shared, so a sibling integration file that + // already closed it must not fail this teardown. + await dbWs.$client.end?.().catch(() => {}); +}); + +describe("pageComment.listForOrganization", () => { + test("sweeps every readable page in one call", async () => { + const threads = await caller.pageComment.listForOrganization({}); + const ids = threads.map((thread) => thread.id); + + expect(ids).toContain(threadIds.shared); + expect(ids).toContain(threadIds.mine); + expect(ids).toContain(threadIds.linked); + }); + + test("applies the same visibility rule page.list does", async () => { + const threads = await caller.pageComment.listForOrganization({}); + const ids = threads.map((thread) => thread.id); + + // A colleague's `just_me` page is unreadable, so its thread is not listed. + expect(ids).not.toContain(threadIds.theirs); + // ...and it is listed for the person who owns it. + const theirs = await callerFor(OTHER_USER).pageComment.listForOrganization( + {}, + ); + expect(theirs.map((thread) => thread.id)).toContain(threadIds.theirs); + }); + + test("names the page each thread belongs to, so a sweep can be grouped", async () => { + const threads = await caller.pageComment.listForOrganization({}); + const shared = threads.find((thread) => thread.id === threadIds.shared); + + expect(shared?.pageTitle).toBe("Page shared"); + expect(shared?.pageSlug).toBe(`comment-sweep-${suffix}-shared`); + expect(shared?.pageId).toBe(pageIds.shared); + }); + + test("carries the comments, not just the threads", async () => { + const threads = await caller.pageComment.listForOrganization({}); + const shared = threads.find((thread) => thread.id === threadIds.shared); + + expect(shared?.comments.map((comment) => comment.body)).toEqual([ + "comment on shared", + ]); + }); + + test("narrows to one workspace's pages", async () => { + const threads = await caller.pageComment.listForOrganization({ + workspaceId: WORKSPACE, + }); + + expect(threads.map((thread) => thread.id)).toEqual([threadIds.linked]); + }); + + test("narrows to unresolved threads", async () => { + const threads = await caller.pageComment.listForOrganization({ + unresolvedOnly: true, + }); + const ids = threads.map((thread) => thread.id); + + expect(ids).not.toContain(threadIds.resolved); + expect(ids).toContain(threadIds.shared); + }); + + test("narrows to the threads an agent was activated on", async () => { + const threads = await caller.pageComment.listForOrganization({ + activatedOnly: true, + }); + + expect(threads.map((thread) => thread.id)).toEqual([threadIds.activated]); + }); + + test("orders oldest first, the way the per-page list did", async () => { + const threads = await caller.pageComment.listForOrganization({}); + const stamps = threads.map((thread) => + new Date(thread.createdAt).getTime(), + ); + + expect([...stamps].sort((a, b) => a - b)).toEqual(stamps); + }); +}); diff --git a/packages/trpc/src/router/page-comment/page-comment.ts b/packages/trpc/src/router/page-comment/page-comment.ts index a385188496a..ea442e3715e 100644 --- a/packages/trpc/src/router/page-comment/page-comment.ts +++ b/packages/trpc/src/router/page-comment/page-comment.ts @@ -6,9 +6,10 @@ import { pageVersions, type SelectPage, users, + workspacePages, } from "@superset/db/schema"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; -import { and, asc, eq, isNotNull, isNull } from "drizzle-orm"; +import { and, asc, eq, isNotNull, isNull, or } from "drizzle-orm"; import { protectedProcedure, userError } from "../../trpc"; import { assertPageReadable } from "../page/access"; import { requireActiveOrgMembership } from "../utils/active-org"; @@ -21,6 +22,7 @@ import { createPageCommentThreadSchema, deletePageCommentThreadSchema, editPageCommentSchema, + listOrganizationPageCommentsSchema, listPageCommentsSchema, replyPageCommentSchema, resolvePageCommentThreadSchema, @@ -157,6 +159,126 @@ export const pageCommentRouter = { ); }), + /** + * One query for the whole organization, so a sweep does not call `list` + * once per page. Visibility is the same rule `page.list` applies, enforced + * in the join rather than by loading each page. + */ + listForOrganization: protectedProcedure + .input(listOrganizationPageCommentsSchema) + .query(async ({ ctx, input }) => { + const organizationId = await requireActiveOrgMembership(ctx); + const userId = ctx.session.user.id; + + const activatedOnly = ctx.agentCaller + ? true + : (input?.activatedOnly ?? false); + + const readable = and( + eq(pages.organizationId, organizationId), + or( + eq(pages.visibility, "org"), + eq(pages.visibility, "everyone"), + and( + eq(pages.visibility, "just_me"), + eq(pages.createdByUserId, userId), + ), + ), + activatedOnly + ? isNotNull(pageCommentThreads.agentActivatedAt) + : undefined, + input?.unresolvedOnly + ? isNull(pageCommentThreads.resolvedAt) + : undefined, + ); + + let scoped = db + .select({ + thread: pageCommentThreads, + version: pageVersions.version, + pageTitle: pages.title, + pageSlug: pages.slug, + }) + .from(pageCommentThreads) + .innerJoin(pages, eq(pages.id, pageCommentThreads.pageId)) + .innerJoin( + pageVersions, + eq(pageVersions.id, pageCommentThreads.pageVersionId), + ) + .$dynamic(); + + if (input?.workspaceId) { + scoped = scoped.innerJoin( + workspacePages, + and( + eq(workspacePages.pageId, pages.id), + eq(workspacePages.workspaceId, input.workspaceId), + ), + ); + } + + const threadRows = await scoped + .where(readable) + .orderBy(asc(pageCommentThreads.createdAt)); + + if (threadRows.length === 0) return []; + + // Selected through the same join as the threads rather than by an id + // list: one bind parameter per thread would fail outright past + // Postgres' 65535-parameter ceiling, and this query is org-wide. + let commentQuery = db + .select({ + comment: pageComments, + authorName: users.name, + authorImage: users.image, + }) + .from(pageComments) + .innerJoin( + pageCommentThreads, + eq(pageCommentThreads.id, pageComments.threadId), + ) + .innerJoin(pages, eq(pages.id, pageCommentThreads.pageId)) + .leftJoin(users, eq(users.id, pageComments.authorUserId)) + .$dynamic(); + + if (input?.workspaceId) { + commentQuery = commentQuery.innerJoin( + workspacePages, + and( + eq(workspacePages.pageId, pages.id), + eq(workspacePages.workspaceId, input.workspaceId), + ), + ); + } + + const commentRows = await commentQuery + .where(and(readable, isNull(pageComments.deletedAt))) + .orderBy(asc(pageComments.createdAt)); + + const byThread = new Map(); + for (const row of commentRows) { + const existing = byThread.get(row.comment.threadId); + if (existing) existing.push(row); + else byThread.set(row.comment.threadId, [row]); + } + + return threadRows.map(({ thread, version, pageTitle, pageSlug }) => ({ + ...shapeThread( + thread, + version, + (byThread.get(thread.id) ?? []).map((row) => + shapeComment(row.comment, { + name: row.authorName, + image: row.authorImage, + }), + ), + ), + pageId: thread.pageId, + pageTitle, + pageSlug, + })); + }), + create: protectedProcedure .input(createPageCommentThreadSchema) .mutation(async ({ ctx, input }) => { diff --git a/packages/trpc/src/router/page-comment/schema.ts b/packages/trpc/src/router/page-comment/schema.ts index ee96b80507c..43cfc426fb4 100644 --- a/packages/trpc/src/router/page-comment/schema.ts +++ b/packages/trpc/src/router/page-comment/schema.ts @@ -24,6 +24,17 @@ export const listPageCommentsSchema = z.object({ activatedOnly: z.boolean().optional(), }); +/** + * Every thread in the organization, for a caller that would otherwise ask + * page by page. `workspaceId` narrows to the pages published from one + * workspace, matching `page.list`. + */ +export const listOrganizationPageCommentsSchema = z.object({ + workspaceId: z.string().uuid().optional(), + activatedOnly: z.boolean().optional(), + unresolvedOnly: z.boolean().optional(), +}); + export const createPageCommentThreadSchema = z .object({ pageId: z.string().uuid(), diff --git a/packages/trpc/src/router/page/cursor.test.ts b/packages/trpc/src/router/page/cursor.test.ts new file mode 100644 index 00000000000..e8bb1d2c455 --- /dev/null +++ b/packages/trpc/src/router/page/cursor.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, test } from "bun:test"; +import { decodePageCursor, encodePageCursor } from "./cursor"; + +const PAGE = "00000000-0000-4000-8000-000000000002"; +const CREATED_AT = "2026-09-14 10:00:00.123456+00"; + +describe("page list cursor", () => { + test("survives a round trip", () => { + const encoded = encodePageCursor({ createdAt: CREATED_AT, id: PAGE }); + expect(decodePageCursor(encoded)).toEqual({ + createdAt: CREATED_AT, + id: PAGE, + }); + }); + + test("keeps the sub-millisecond digits Postgres reported", () => { + const decoded = decodePageCursor( + encodePageCursor({ createdAt: CREATED_AT, id: PAGE }), + ); + expect(decoded?.createdAt).toContain(".123456"); + }); + + test("encodes without characters a shell or URL would mangle", () => { + const encoded = encodePageCursor({ createdAt: CREATED_AT, id: PAGE }); + expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/); + }); + + test("rejects a typo'd cursor rather than crashing on the parse", () => { + expect(decodePageCursor("not-a-cursor")).toBeNull(); + }); + + test("rejects a well-formed token missing the id", () => { + const encoded = Buffer.from( + JSON.stringify({ createdAt: CREATED_AT }), + ).toString("base64url"); + expect(decodePageCursor(encoded)).toBeNull(); + }); + + test("rejects a token whose timestamp is not a string", () => { + const encoded = Buffer.from( + JSON.stringify({ createdAt: 12345, id: PAGE }), + ).toString("base64url"); + expect(decodePageCursor(encoded)).toBeNull(); + }); + + test("rejects a timestamp that is not a Postgres timestamptz", () => { + // This value reaches the query as `::timestamptz`, so the shape is + // checked rather than trusted from whatever the caller echoed back. + const encoded = Buffer.from( + JSON.stringify({ + createdAt: "2026-09-14T10:00:00Z'; drop table", + id: PAGE, + }), + ).toString("base64url"); + expect(decodePageCursor(encoded)).toBeNull(); + }); + + test("accepts the offsets Postgres emits, whole-hour and half-hour alike", () => { + for (const createdAt of [ + "2026-09-14 10:00:00+00", + "2026-09-14 10:00:00.123456+05:30", + "2026-09-14 10:00:00.1-08", + ]) { + const encoded = Buffer.from( + JSON.stringify({ createdAt, id: PAGE }), + ).toString("base64url"); + expect(decodePageCursor(encoded)).toEqual({ createdAt, id: PAGE }); + } + }); + + test("rejects an id that is not a uuid", () => { + const encoded = Buffer.from( + JSON.stringify({ createdAt: CREATED_AT, id: "not-a-uuid" }), + ).toString("base64url"); + expect(decodePageCursor(encoded)).toBeNull(); + }); +}); diff --git a/packages/trpc/src/router/page/cursor.ts b/packages/trpc/src/router/page/cursor.ts new file mode 100644 index 00000000000..9ae1cec9cb9 --- /dev/null +++ b/packages/trpc/src/router/page/cursor.ts @@ -0,0 +1,41 @@ +/** + * The list cursor is opaque to every caller: the CLI passes it back as a + * string, and the MCP tool schema describes it as one. Keeping the keyset + * columns out of the public shape is what lets the ordering change without + * breaking a client, which the move from `updated_at` to `created_at` needed. + */ +export interface PageListKeyset { + createdAt: string; + id: string; +} + +const POSTGRES_TIMESTAMPTZ_TEXT = + /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{1,6})?[+-]\d{2}(:\d{2}(:\d{2})?)?$/; + +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export function encodePageCursor(keyset: PageListKeyset): string { + return Buffer.from( + JSON.stringify({ createdAt: keyset.createdAt, id: keyset.id }), + ).toString("base64url"); +} + +export function decodePageCursor(value: string): PageListKeyset | null { + let parsed: unknown; + try { + parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")); + } catch { + return null; + } + + const keyset = parsed as Partial | null; + if (typeof keyset?.createdAt !== "string" || typeof keyset.id !== "string") { + return null; + } + // `createdAt` is interpolated into the query as `::timestamptz`, so it is + // checked here rather than trusted from whatever the caller echoed back. + if (!POSTGRES_TIMESTAMPTZ_TEXT.test(keyset.createdAt)) return null; + if (!UUID.test(keyset.id)) return null; + + return { createdAt: keyset.createdAt, id: keyset.id }; +} diff --git a/packages/trpc/src/router/page/list.integration.ts b/packages/trpc/src/router/page/list.integration.ts new file mode 100644 index 00000000000..407c018cc91 --- /dev/null +++ b/packages/trpc/src/router/page/list.integration.ts @@ -0,0 +1,294 @@ +import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test"; + +mock.module("../../lib/r2", () => ({ + presignedGetUrl: async (key: string) => `https://storage.test/${key}`, + presignedPutUrl: async ({ key }: { key: string }) => ({ + url: `https://storage.test/${key}`, + headers: {}, + }), + putObject: async () => {}, + getObject: async () => null, + headObject: async () => null, + copyObject: async () => {}, + deleteObjects: async () => {}, + objectExists: async () => true, +})); + +const { db, dbWs } = await import("@superset/db/client"); +const { members, organizations, pages, users, workspacePages } = await import( + "@superset/db/schema" +); +const { and, eq, ne, or } = await import("drizzle-orm"); +const { pageRouter } = await import("./page"); +const { createCallerFactory, createTRPCContext, createTRPCRouter } = + await import("../../trpc"); + +const ORG = crypto.randomUUID(); +const USER = crypto.randomUUID(); +const OTHER_USER = crypto.randomUUID(); +const WORKSPACE = crypto.randomUUID(); +const suffix = Date.now(); + +const callerFor = (userId: string) => + createCallerFactory(createTRPCRouter({ page: pageRouter }))( + createTRPCContext({ + session: { + user: { id: userId, email: `u-${userId}@example.com` }, + session: { activeOrganizationId: ORG }, + } as never, + auth: {} as never, + headers: new Headers(), + }), + ); + +const caller = callerFor(USER); + +const TOTAL = 463; + +/** Walks the whole list through the cursor, the way every client does. */ +async function walk( + input: Parameters[0] = {}, + onBatch?: (batch: number) => Promise, + limit = 200, +): Promise { + const seen: string[] = []; + let cursor: string | undefined; + let batch = 0; + do { + const result = await caller.page.list({ + ...input, + limit, + ...(cursor ? { cursor } : {}), + }); + seen.push(...result.items.map((item) => item.id)); + cursor = result.nextCursor ?? undefined; + batch += 1; + if (onBatch) await onBatch(batch); + } while (cursor); + return seen; +} + +beforeAll(async () => { + await db.insert(organizations).values({ + id: ORG, + name: "List Org", + slug: `list-org-${suffix}`, + }); + await db.insert(users).values([ + { + id: USER, + name: "List User", + email: `list-${suffix}@example.com`, + organizationIds: [ORG], + }, + { + id: OTHER_USER, + name: "Other User", + email: `list-other-${suffix}@example.com`, + organizationIds: [ORG], + }, + ]); + await db.insert(members).values([ + { + id: crypto.randomUUID(), + organizationId: ORG, + userId: USER, + role: "owner", + createdAt: new Date(), + }, + { + id: crypto.randomUUID(), + organizationId: ORG, + userId: OTHER_USER, + role: "member", + createdAt: new Date(), + }, + ]); + + // Every row shares one `created_at` second so the id half of the keyset is + // exercised rather than skipped by distinct timestamps. + const base = new Date("2026-01-01T00:00:00.000Z"); + await db.insert(pages).values( + Array.from({ length: TOTAL }, (_, i) => ({ + id: crypto.randomUUID(), + slug: `list-page-${suffix}-${i}`, + organizationId: ORG, + createdByUserId: i % 3 === 0 ? OTHER_USER : USER, + title: i % 5 === 0 ? `Report ${i}` : `Page ${i}`, + description: i % 7 === 0 ? "quarterly summary" : null, + visibility: i % 4 === 0 ? ("just_me" as const) : ("org" as const), + createdAt: new Date(base.getTime() + Math.floor(i / 20) * 1000), + })), + ); + + // Index 1 specifically: `org` visibility and created by USER, so the + // workspace-scoped assertions below have exactly one expected author. + const [linked] = await db + .select({ id: pages.id }) + .from(pages) + .where(eq(pages.slug, `list-page-${suffix}-1`)) + .limit(1); + if (!linked) throw new Error("failed to find the page to link"); + await db.insert(workspacePages).values({ + pageId: linked.id, + workspaceId: WORKSPACE, + entryPath: "/index.html", + }); +}); + +afterAll(async () => { + await db.delete(organizations).where(eq(organizations.id, ORG)); + await db.delete(users).where(eq(users.id, USER)); + await db.delete(users).where(eq(users.id, OTHER_USER)); + // Guarded: the pooled client is shared, so a sibling integration file that + // already closed it must not fail this teardown. + await dbWs.$client.end?.().catch(() => {}); +}); + +describe("page.list keyset", () => { + test("a full walk returns every visible page exactly once", async () => { + const seen = await walk({}, undefined, 50); + const visible = await db + .select({ id: pages.id }) + .from(pages) + .where( + and( + eq(pages.organizationId, ORG), + or(ne(pages.visibility, "just_me"), eq(pages.createdByUserId, USER)), + ), + ); + + expect(new Set(seen).size).toBe(seen.length); + expect(seen.length).toBe(visible.length); + expect([...seen].sort()).toEqual(visible.map((row) => row.id).sort()); + }, 30_000); + + test("a watch heartbeat mid-walk does not drop a page", async () => { + // The bug this keyset replaced: `setWatch` bumps `updated_at` every 30s, + // which moved a row across an open cursor and skipped its neighbour. + const owned = await db + .select({ id: pages.id }) + .from(pages) + .where(eq(pages.createdByUserId, USER)); + + const bumped: string[] = []; + const seen = await walk( + {}, + async (batch) => { + const row = owned[batch * 7]; + if (!row) return; + await caller.page.setWatch({ id: row.id, agentId: "claude" }); + bumped.push(row.id); + }, + 50, + ); + + expect(bumped.length).toBeGreaterThan(0); + expect(new Set(seen).size).toBe(seen.length); + for (const id of bumped) expect(seen).toContain(id); + }, 30_000); + + test("an unreadable cursor is a BAD_REQUEST, not a crash", async () => { + await expect(caller.page.list({ cursor: "not-a-cursor" })).rejects.toThrow( + /cursor/i, + ); + }); +}); + +describe("page.list filters", () => { + test("scope mine and team partition the list", async () => { + const all = await walk({ scope: "all" }); + const mine = await walk({ scope: "mine" }); + const team = await walk({ scope: "team" }); + + expect(mine.length + team.length).toBe(all.length); + expect(mine.filter((id) => team.includes(id))).toEqual([]); + }, 30_000); + + test("search matches title, slug and description", async () => { + const byTitle = await walk({ search: "Report" }); + const byDescription = await walk({ search: "quarterly" }); + + expect(byTitle.length).toBeGreaterThan(0); + expect(byDescription.length).toBeGreaterThan(0); + }, 30_000); + + test("an empty search is the unfiltered list", async () => { + const blank = await caller.page.list({ search: " ", limit: 5 }); + const none = await caller.page.list({ limit: 5 }); + + expect(blank.items.map((i) => i.id)).toEqual(none.items.map((i) => i.id)); + }); + + test("ids narrows to the pinned set, and an empty ids returns nothing", async () => { + const first = await caller.page.list({ limit: 3 }); + const ids = first.items.map((item) => item.id); + + const pinned = await caller.page.list({ ids, limit: 50 }); + expect(pinned.items.map((item) => item.id).sort()).toEqual([...ids].sort()); + + const empty = await caller.page.list({ ids: [], limit: 50 }); + expect(empty.items).toEqual([]); + }); + + test("authorId narrows to that author", async () => { + const mine = await walk({ authorId: OTHER_USER }); + expect(mine.length).toBeGreaterThan(0); + }); +}); + +describe("page.counts", () => { + test("counts agree with what the list returns", async () => { + const counts = await caller.page.counts(); + const all = await walk({ scope: "all" }); + const mine = await walk({ scope: "mine" }); + const team = await walk({ scope: "team" }); + + expect(counts.all).toBe(all.length); + expect(counts.mine).toBe(mine.length); + expect(counts.team).toBe(team.length); + }, 30_000); + + test("pinned counts only the ids it is given", async () => { + const first = await caller.page.list({ limit: 4 }); + const ids = first.items.map((item) => item.id); + const counts = await caller.page.counts({ pinnedIds: ids }); + + expect(counts.pinned).toBe(ids.length); + expect((await caller.page.counts({ pinnedIds: [] })).pinned).toBe(0); + }); + + test("the author breakdown offers every author, not just the selected one", async () => { + const all = await caller.page.counts(); + const selected = await caller.page.counts({ authorId: OTHER_USER }); + + // Narrowing the breakdown by the dimension it offers would collapse the + // picker to whatever is already chosen, so you could never switch author. + expect(all.authors.length).toBeGreaterThan(1); + expect(selected.authors.map((row) => row.userId).sort()).toEqual( + all.authors.map((row) => row.userId).sort(), + ); + }); + + test("the author breakdown respects the selected workspace", async () => { + const counts = await caller.page.counts({ workspaceId: WORKSPACE }); + + // Only the linked page is in this workspace, and USER created it — an + // author with nothing here would lead to an empty list when picked. + expect(counts.authors.map((row) => row.userId)).toEqual([USER]); + }); + + test("the workspace breakdown is not narrowed by the selected workspace", async () => { + const counts = await caller.page.counts({ workspaceId: WORKSPACE }); + expect(counts.workspaces.map((row) => row.workspaceId)).toContain( + WORKSPACE, + ); + }); + + test("a search narrows the counts too", async () => { + const counts = await caller.page.counts({ search: "Report" }); + const listed = await walk({ search: "Report" }); + + expect(counts.all).toBe(listed.length); + }, 30_000); +}); diff --git a/packages/trpc/src/router/page/page.ts b/packages/trpc/src/router/page/page.ts index 851f89a8432..9aed44b9023 100644 --- a/packages/trpc/src/router/page/page.ts +++ b/packages/trpc/src/router/page/page.ts @@ -10,6 +10,7 @@ import { users, workspacePages, } from "@superset/db/schema"; +import { escapeLikePattern } from "@superset/db/utils"; import { mintPageSlug } from "@superset/shared/page-slug"; import { fileOriginalKey, @@ -23,7 +24,10 @@ import { and, desc, eq, + ilike, inArray, + isNotNull, + lt, notExists, or, type SQL, @@ -36,6 +40,7 @@ import { protectedProcedure, publicProcedure, userError } from "../../trpc"; import { requireActiveOrgMembership } from "../utils/active-org"; import { assertPageReadable, assertPageWritable } from "./access"; import { pageAssetRouter } from "./assets"; +import { decodePageCursor, encodePageCursor } from "./cursor"; import { pageUrl } from "./page-url"; import { publishPage } from "./publish"; import { isEntryPathConflict } from "./publish-rules"; @@ -44,6 +49,9 @@ import { createPageSchema, deletePageSchema, listPagesSchema, + PAGE_LIST_DEFAULT_LIMIT, + type PageListScope, + pageCountsSchema, pageFields, pageRefSchema, publicPageSchema, @@ -72,6 +80,49 @@ function visibilityFilter(userId: string) { ); } +function scopeFilter(scope: PageListScope): SQL | undefined { + switch (scope) { + case "team": + return sql`${pages.visibility} <> 'just_me'`; + case "mine": + return eq(pages.visibility, "just_me"); + default: + return undefined; + } +} + +/** + * Shared by `list` and `counts` so a tab's count and its contents can't be + * answered by two different WHERE clauses. + */ +function pageFilters(input: { + search?: string | undefined; + scope?: PageListScope | undefined; + authorId?: string | undefined; + ids?: string[] | undefined; +}): (SQL | undefined)[] { + const filters: (SQL | undefined)[] = []; + + if (input.search) { + const term = `%${escapeLikePattern(input.search)}%`; + filters.push( + or( + ilike(pages.title, term), + ilike(pages.slug, term), + ilike(pages.description, term), + ), + ); + } + + if (input.scope) filters.push(scopeFilter(input.scope)); + if (input.authorId) filters.push(eq(pages.createdByUserId, input.authorId)); + // An empty array is "no pins", which must return nothing rather than + // degrade to the unfiltered list. + if (input.ids) filters.push(inArray(pages.id, input.ids)); + + return filters; +} + async function pageNotFound(identity: SQL, userId: string): Promise { const [elsewhere] = await db .select({ organizationName: organizations.name }) @@ -247,6 +298,7 @@ export const pageRouter = { .query(async ({ ctx, input }) => { const organizationId = await requireActiveOrgMembership(ctx); const userId = ctx.session.user.id; + const limit = input?.limit ?? PAGE_LIST_DEFAULT_LIMIT; if (input?.workspaceId) { await assertWorkspaceAccess({ @@ -279,6 +331,9 @@ export const pageRouter = { sharedVersion: pages.sharedVersion, createdAt: pages.createdAt, updatedAt: pages.updatedAt, + // `::text` keeps the microseconds a JS Date would truncate, which + // the keyset comparison needs to be exact. + createdAtCursor: sql`${pages.createdAt}::text`, createdByUserId: pages.createdByUserId, ownerName: users.name, ownerImage: users.image, @@ -291,27 +346,83 @@ export const pageRouter = { .leftJoin(users, eq(users.id, pages.createdByUserId)) .leftJoinLateral(latest, sql`true`); + const filters: (SQL | undefined)[] = [ + eq(pages.organizationId, organizationId), + visibilityFilter(userId), + ...pageFilters(input ?? {}), + ]; + + if (input?.cursor) { + const keyset = decodePageCursor(input.cursor); + if (!keyset) { + throw userError({ + code: "BAD_REQUEST", + message: + "That cursor could not be read. Drop it to start from the first page.", + i18nKey: "serverError.page.cursorCouldNotBeRead", + }); + } + const at = sql`${keyset.createdAt}::timestamptz`; + filters.push( + or( + lt(pages.createdAt, at), + and(eq(pages.createdAt, at), lt(pages.id, keyset.id)), + ), + ); + } + const scoped = input?.workspaceId ? base .innerJoin(workspacePages, eq(workspacePages.pageId, pages.id)) .where( and( - eq(pages.organizationId, organizationId), + ...filters, eq(workspacePages.workspaceId, input.workspaceId), - visibilityFilter(userId), ), ) - : base.where( - and( - eq(pages.organizationId, organizationId), - visibilityFilter(userId), - ), - ); + : base.where(and(...filters)); + + const rows = await scoped + .orderBy(desc(pages.createdAt), desc(pages.id)) + .limit(limit + 1); + + const pageRows = rows.slice(0, limit); + const last = pageRows.at(-1); + const nextCursor = + rows.length > limit && last + ? encodePageCursor({ createdAt: last.createdAtCursor, id: last.id }) + : null; + + const links = pageRows.length + ? await db + .select({ + pageId: workspacePages.pageId, + workspaceId: workspacePages.workspaceId, + entryPath: workspacePages.entryPath, + }) + .from(workspacePages) + .where( + inArray( + workspacePages.pageId, + pageRows.map((row) => row.id), + ), + ) + .orderBy(workspacePages.workspaceId) + : []; + + const linksByPage = new Map< + string, + { workspaceId: string; entryPath: string }[] + >(); + for (const link of links) { + const list = linksByPage.get(link.pageId) ?? []; + list.push({ workspaceId: link.workspaceId, entryPath: link.entryPath }); + linksByPage.set(link.pageId, list); + } - const rows = await scoped.orderBy(desc(pages.updatedAt)); const baseUrl = env.USERCONTENT_URL; - return await Promise.all( - rows.map(async (row) => { + const items = await Promise.all( + pageRows.map(async ({ createdAtCursor: _cursor, ...row }) => { const served = servedVersion(row.sharedVersion, row.latestVersion); const ticket = await mintPageTicket(row); // Version-bound, so it turns daily instead of hourly — the capture @@ -322,6 +433,7 @@ export const pageRouter = { : await mintPageTicket(row, { version: served }); return { ...row, + workspaceLinks: linksByPage.get(row.id) ?? [], url: pageUrl(row.slug), viewUrl: pageViewUrl({ baseUrl, pageId: row.id, ticket }), thumbnailUrl: @@ -338,6 +450,109 @@ export const pageRouter = { }; }), ); + + return { items, nextCursor }; + }), + + counts: protectedProcedure + .input(pageCountsSchema) + .query(async ({ ctx, input }) => { + const organizationId = await requireActiveOrgMembership(ctx); + const userId = ctx.session.user.id; + + if (input?.workspaceId) { + await assertWorkspaceAccess({ + executor: db, + workspaceId: input.workspaceId, + organizationId, + }); + } + + const filters: (SQL | undefined)[] = [ + eq(pages.organizationId, organizationId), + visibilityFilter(userId), + ...pageFilters({ + search: input?.search, + authorId: input?.authorId, + }), + ]; + + const pinned = input?.pinnedIds ?? []; + const selection = { + all: sql`count(*)::int`, + team: sql`count(*) filter (where ${pages.visibility} <> 'just_me')::int`, + mine: sql`count(*) filter (where ${pages.visibility} = 'just_me')::int`, + pinned: pinned.length + ? sql`count(*) filter (where ${inArray(pages.id, pinned)})::int` + : sql`0::int`, + }; + + const query = db.select(selection).from(pages); + const [row] = await (input?.workspaceId + ? query + .innerJoin(workspacePages, eq(workspacePages.pageId, pages.id)) + .where( + and( + ...filters, + eq(workspacePages.workspaceId, input.workspaceId), + ), + ) + : query.where(and(...filters))); + + // Each picker's own counts, so choosing an option does not depend on + // having downloaded every page to count them. A breakdown is never + // narrowed by the dimension it offers — that is the choice being made, + // and narrowing by it collapses the list to whatever is already + // selected — but it is narrowed by every other active filter, or it + // offers options that lead to an empty list. + const workspaces = await db + .select({ + workspaceId: workspacePages.workspaceId, + count: sql`count(*)::int`, + }) + .from(workspacePages) + .innerJoin(pages, eq(pages.id, workspacePages.pageId)) + .where(and(...filters)) + .groupBy(workspacePages.workspaceId); + + let authorsBase = db + .select({ + userId: pages.createdByUserId, + name: users.name, + image: users.image, + count: sql`count(*)::int`, + }) + .from(pages) + .leftJoin(users, eq(users.id, pages.createdByUserId)) + .$dynamic(); + + if (input?.workspaceId) { + authorsBase = authorsBase.innerJoin( + workspacePages, + and( + eq(workspacePages.pageId, pages.id), + eq(workspacePages.workspaceId, input.workspaceId), + ), + ); + } + + const authors = await authorsBase + .where( + and( + eq(pages.organizationId, organizationId), + visibilityFilter(userId), + // `authorId` deliberately absent; `search` still applies. + ...pageFilters({ search: input?.search }), + isNotNull(pages.createdByUserId), + ), + ) + .groupBy(pages.createdByUserId, users.name, users.image); + + return { + ...(row ?? { all: 0, team: 0, mine: 0, pinned: 0 }), + workspaces, + authors, + }; }), get: protectedProcedure.input(pageRefSchema).query(async ({ ctx, input }) => { diff --git a/packages/trpc/src/router/page/schema.test.ts b/packages/trpc/src/router/page/schema.test.ts index 3b73dc7ae02..d1d017343cf 100644 --- a/packages/trpc/src/router/page/schema.test.ts +++ b/packages/trpc/src/router/page/schema.test.ts @@ -1,5 +1,11 @@ import { describe, expect, test } from "bun:test"; -import { publishPageSchema } from "./schema"; +import { + listPagesSchema, + PAGE_LIST_DEFAULT_LIMIT, + PAGE_LIST_MAX_IDS, + PAGE_LIST_MAX_LIMIT, + publishPageSchema, +} from "./schema"; const WORKSPACE = "00000000-0000-4000-8000-000000000001"; const PAGE = "00000000-0000-4000-8000-000000000002"; @@ -77,3 +83,67 @@ describe("publishPageSchema", () => { ).toBe(true); }); }); + +describe("listPagesSchema", () => { + test("defaults the limit when an input object omits it", () => { + const result = listPagesSchema.parse({}); + expect(result?.limit).toBe(PAGE_LIST_DEFAULT_LIMIT); + }); + + test("still accepts the no-argument call the CLI and MCP tools make", () => { + expect(listPagesSchema.parse(undefined)).toBeUndefined(); + }); + + test("refuses a limit past the ceiling", () => { + expect( + listPagesSchema.safeParse({ limit: PAGE_LIST_MAX_LIMIT + 1 }).success, + ).toBe(false); + }); + + test("treats an empty or blank search as no search, not as match-everything", () => { + expect(listPagesSchema.parse({ search: "" })?.search).toBeUndefined(); + expect(listPagesSchema.parse({ search: " " })?.search).toBeUndefined(); + }); + + test("trims a search so a stray space does not change the query", () => { + expect(listPagesSchema.parse({ search: " report " })?.search).toBe( + "report", + ); + }); + + test("takes the cursor as an opaque string", () => { + const cursor = Buffer.from("whatever the server emitted").toString( + "base64url", + ); + expect(listPagesSchema.parse({ cursor })?.cursor).toBe(cursor); + }); + + test("refuses a cursor that is not a string, so the keyset stays private", () => { + expect( + listPagesSchema.safeParse({ + cursor: { updatedAt: "2026-09-14 10:00:00+00", id: PAGE }, + }).success, + ).toBe(false); + }); + + test("defaults scope to all and refuses one it does not filter on", () => { + expect(listPagesSchema.parse({})?.scope).toBe("all"); + expect(listPagesSchema.safeParse({ scope: "pinned" }).success).toBe(false); + }); + + test("accepts an ids filter up to the pin cap and refuses more", () => { + const ids = Array.from({ length: PAGE_LIST_MAX_IDS }, () => PAGE); + expect(listPagesSchema.safeParse({ ids }).success).toBe(true); + expect(listPagesSchema.safeParse({ ids: [...ids, PAGE] }).success).toBe( + false, + ); + }); + + test("accepts an empty ids filter — no pins is not no filter", () => { + expect(listPagesSchema.parse({ ids: [] })?.ids).toEqual([]); + }); + + test("refuses an authorId that is not a uuid", () => { + expect(listPagesSchema.safeParse({ authorId: "nope" }).success).toBe(false); + }); +}); diff --git a/packages/trpc/src/router/page/schema.ts b/packages/trpc/src/router/page/schema.ts index b860ad85014..c6e69bff30c 100644 --- a/packages/trpc/src/router/page/schema.ts +++ b/packages/trpc/src/router/page/schema.ts @@ -88,10 +88,70 @@ export const createPageSchema = z export type CreatePageInput = z.infer; +export const PAGE_LIST_DEFAULT_LIMIT = 50; +export const PAGE_LIST_MAX_LIMIT = 200; + +/** + * `MAX_FAVORITE_PAGE_IDS` on the desktop, which is where the only unbounded + * caller comes from: pins live in renderer storage, so the pinned tab asks for + * them by id rather than by a column the server could filter on. + */ +export const PAGE_LIST_MAX_IDS = 200; + +export const PAGE_LIST_SCOPES = ["all", "team", "mine"] as const; + +export type PageListScope = (typeof PAGE_LIST_SCOPES)[number]; + +/** + * An empty search is the cleared search box, not a request for pages whose + * title contains "". Normalising here rather than at each caller keeps a + * client from having to strip the key to get the unfiltered list back. + */ +const searchField = z + .string() + .max(200) + .transform((value) => value.trim()) + .transform((value) => (value.length === 0 ? undefined : value)) + .optional(); + +const pageListFilterFields = { + workspaceId: pageFields.workspaceId.optional(), + search: searchField, + scope: z.enum(PAGE_LIST_SCOPES).default("all"), + authorId: z.string().uuid().optional(), + ids: z.array(pageFields.id).max(PAGE_LIST_MAX_IDS).optional(), +} as const; + export const listPagesSchema = z - .object({ workspaceId: pageFields.workspaceId.optional() }) + .object({ + ...pageListFilterFields, + cursor: z.string().max(256).optional(), + limit: z + .number() + .int() + .min(1) + .max(PAGE_LIST_MAX_LIMIT) + .default(PAGE_LIST_DEFAULT_LIMIT), + }) .optional(); +/** + * The tab counts. They are a separate query because they are counts over the + * whole filtered set, which a paginated list can no longer derive from what it + * has loaded. + */ +export const pageCountsSchema = z + .object({ + workspaceId: pageListFilterFields.workspaceId, + search: pageListFilterFields.search, + authorId: pageListFilterFields.authorId, + pinnedIds: pageListFilterFields.ids, + }) + .optional(); + +export type ListPagesInput = z.infer; +export type PageCountsInput = z.infer; + const pageRefFieldsSchema = z.object({ id: pageFields.id.optional(), slug: pageFields.slug.optional(), diff --git a/plugins/superset/skills/page/SKILL.md b/plugins/superset/skills/page/SKILL.md index 300bf3b89af..7cc68e5efb3 100644 --- a/plugins/superset/skills/page/SKILL.md +++ b/plugins/superset/skills/page/SKILL.md @@ -327,6 +327,7 @@ until someone widens it. ```bash superset pages list --workspace # or omit --workspace for the whole org +superset pages list --search "Q3 close" # -q also works; matches title or slug superset pages get superset pages versions superset pages pull --version 2 > v2.html @@ -335,6 +336,17 @@ superset pages pull --version 2 > v2.html `pull` writes HTML to stdout; use it to recover a source file you no longer have, or to diff what actually shipped against what you have locally. +`list` returns every page it can see, so reach for `--search` before you reach +for a pipe into `grep`. Two flags change that: `--limit <1-200>` returns a +single batch, and `--cursor` continues from where a batch stopped. + +They also change the JSON. Under `--json`, a plain `list` is a bare array, but +passing either flag wraps it as `{ items, nextCursor }`. Feed that `nextCursor` +back as `--cursor` until it comes back `null`. Parsing the output? Either don't +pass the flags, or handle the envelope. The other two modes are unaffected: the +default table looks the same and tells you when there's more, and `--quiet` +prints ids either way. + `get` carries `workspaceLinks`: the workspace and the path each publish resolved against. When you have lost the source, pull it back to that path inside that workspace and a later publish versions the page instead of minting