diff --git a/apps/api/src/app/api/electric/[...path]/utils.ts b/apps/api/src/app/api/electric/[...path]/utils.ts index 81d5dabcb7f..7206700204a 100644 --- a/apps/api/src/app/api/electric/[...path]/utils.ts +++ b/apps/api/src/app/api/electric/[...path]/utils.ts @@ -14,10 +14,10 @@ import { subscriptions, taskStatuses, tasks, - v2DevicePresence, - v2Devices, + v2Clients, + v2Hosts, v2Projects, - v2UsersDevices, + v2UsersHosts, v2Workspaces, workspaces, } from "@superset/db/schema"; @@ -29,10 +29,10 @@ export type AllowedTable = | "tasks" | "task_statuses" | "projects" - | "v2_devices" - | "v2_device_presence" + | "v2_hosts" + | "v2_clients" | "v2_projects" - | "v2_users_devices" + | "v2_users_hosts" | "v2_workspaces" | "auth.members" | "auth.organizations" @@ -84,22 +84,14 @@ export async function buildWhereClause( case "v2_projects": return build(v2Projects, v2Projects.organizationId, organizationId); - case "v2_devices": - return build(v2Devices, v2Devices.organizationId, organizationId); + case "v2_hosts": + return build(v2Hosts, v2Hosts.organizationId, organizationId); - case "v2_device_presence": - return build( - v2DevicePresence, - v2DevicePresence.organizationId, - organizationId, - ); + case "v2_clients": + return build(v2Clients, v2Clients.organizationId, organizationId); - case "v2_users_devices": - return build( - v2UsersDevices, - v2UsersDevices.organizationId, - organizationId, - ); + case "v2_users_hosts": + return build(v2UsersHosts, v2UsersHosts.organizationId, organizationId); case "v2_workspaces": return build(v2Workspaces, v2Workspaces.organizationId, organizationId); diff --git a/apps/desktop/src/renderer/hooks/host-service/useDiffStats/index.ts b/apps/desktop/src/renderer/hooks/host-service/useDiffStats/index.ts new file mode 100644 index 00000000000..5dc048a8ea2 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/host-service/useDiffStats/index.ts @@ -0,0 +1 @@ +export { type DiffStats, useDiffStats } from "./useDiffStats"; diff --git a/apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts b/apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts new file mode 100644 index 00000000000..13f27f2e853 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts @@ -0,0 +1,62 @@ +import { useCallback, useEffect, useState } from "react"; +import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; +import { useWorkspaceEvent } from "../useWorkspaceEvent"; +import { useWorkspaceHostUrl } from "../useWorkspaceHostUrl"; + +export interface DiffStats { + additions: number; + deletions: number; +} + +/** + * Fetches diff stats for a single workspace, auto-updates on git changes. + * Just pass the workspaceId — host resolution is handled internally. + */ +export function useDiffStats(workspaceId: string): DiffStats | null { + const [stats, setStats] = useState(null); + const hostUrl = useWorkspaceHostUrl(workspaceId); + + const fetchStats = useCallback(async () => { + if (!hostUrl) return; + try { + const client = getHostServiceClientByUrl(hostUrl); + const status = await client.git.getStatus.query({ workspaceId }); + + // Deduplicate by path — a file can appear in multiple categories + const byPath = new Map< + string, + { additions: number; deletions: number } + >(); + for (const file of status.againstBase) { + byPath.set(file.path, file); + } + for (const file of status.staged) { + byPath.set(file.path, file); + } + for (const file of status.unstaged) { + byPath.set(file.path, file); + } + + let additions = 0; + let deletions = 0; + for (const file of byPath.values()) { + additions += file.additions; + deletions += file.deletions; + } + + setStats({ additions, deletions }); + } catch { + // Host unavailable or workspace deleted + } + }, [hostUrl, workspaceId]); + + useEffect(() => { + void fetchStats(); + }, [fetchStats]); + + useWorkspaceEvent("git:changed", workspaceId, () => { + void fetchStats(); + }); + + return stats; +} diff --git a/packages/workspace-client/src/hooks/useFileDocument/index.ts b/apps/desktop/src/renderer/hooks/host-service/useFileDocument/index.ts similarity index 100% rename from packages/workspace-client/src/hooks/useFileDocument/index.ts rename to apps/desktop/src/renderer/hooks/host-service/useFileDocument/index.ts diff --git a/packages/workspace-client/src/hooks/useFileDocument/useFileDocument.ts b/apps/desktop/src/renderer/hooks/host-service/useFileDocument/useFileDocument.ts similarity index 97% rename from packages/workspace-client/src/hooks/useFileDocument/useFileDocument.ts rename to apps/desktop/src/renderer/hooks/host-service/useFileDocument/useFileDocument.ts index cd33c82e96e..cf9de5de59f 100644 --- a/packages/workspace-client/src/hooks/useFileDocument/useFileDocument.ts +++ b/apps/desktop/src/renderer/hooks/host-service/useFileDocument/useFileDocument.ts @@ -1,6 +1,6 @@ +import { workspaceTrpc } from "@superset/workspace-client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { workspaceTrpc } from "../../workspace-trpc"; -import { useWorkspaceFsEvents } from "../useWorkspaceFsEvents"; +import { useWorkspaceEvent } from "../useWorkspaceEvent"; const DEFAULT_MAX_BYTES = 2 * 1024 * 1024; const BINARY_CHECK_SIZE = 8192; @@ -162,7 +162,8 @@ export function useFileDocument({ setConflict({ diskContent }); }, [fetchCurrentDiskContent, mode]); - useWorkspaceFsEvents( + useWorkspaceEvent( + "fs:events", workspaceId, (event) => { const path = currentPathRef.current; diff --git a/packages/workspace-client/src/hooks/useFileTree/index.ts b/apps/desktop/src/renderer/hooks/host-service/useFileTree/index.ts similarity index 100% rename from packages/workspace-client/src/hooks/useFileTree/index.ts rename to apps/desktop/src/renderer/hooks/host-service/useFileTree/index.ts diff --git a/packages/workspace-client/src/hooks/useFileTree/useFileTree.ts b/apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts similarity index 98% rename from packages/workspace-client/src/hooks/useFileTree/useFileTree.ts rename to apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts index 28272b73d29..5542ca1117f 100644 --- a/packages/workspace-client/src/hooks/useFileTree/useFileTree.ts +++ b/apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts @@ -1,7 +1,7 @@ -import type { FsEntry, FsEntryKind } from "@superset/workspace-fs/host"; +import { workspaceTrpc } from "@superset/workspace-client"; +import type { FsEntry, FsEntryKind } from "@superset/workspace-fs/client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { workspaceTrpc } from "../../workspace-trpc"; -import { useWorkspaceFsEvents } from "../useWorkspaceFsEvents"; +import { useWorkspaceEvent } from "../useWorkspaceEvent"; export interface FileTreeNode { absolutePath: string; @@ -354,7 +354,8 @@ export function useFileTree({ void loadDirectory(rootPath, true); }, [loadDirectory, rootPath, updateState]); - useWorkspaceFsEvents( + useWorkspaceEvent( + "fs:events", workspaceId, (event) => { if (!rootPath) { diff --git a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/index.ts b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/index.ts new file mode 100644 index 00000000000..35bb950c735 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/index.ts @@ -0,0 +1 @@ +export { useWorkspaceEvent } from "./useWorkspaceEvent"; diff --git a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/useWorkspaceEvent.ts b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/useWorkspaceEvent.ts new file mode 100644 index 00000000000..3c647b4b778 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceEvent/useWorkspaceEvent.ts @@ -0,0 +1,65 @@ +import { getEventBus } from "@superset/workspace-client"; +import type { FsWatchEvent } from "@superset/workspace-fs/client"; +import { useEffect, useEffectEvent } from "react"; +import { getHostServiceWsToken } from "renderer/lib/host-service-auth"; +import { useWorkspaceHostUrl } from "../useWorkspaceHostUrl"; + +/** + * Subscribe to an event bus event for a workspace. + * Resolves the workspace's host and connects to the correct event bus automatically. + */ +export function useWorkspaceEvent( + type: "git:changed", + workspaceId: string, + callback: () => void, + enabled?: boolean, +): void; +export function useWorkspaceEvent( + type: "fs:events", + workspaceId: string, + callback: (event: FsWatchEvent) => void, + enabled?: boolean, +): void; +export function useWorkspaceEvent( + type: "git:changed" | "fs:events", + workspaceId: string, + callback: ((event: FsWatchEvent) => void) | (() => void), + enabled = true, +): void { + const hostUrl = useWorkspaceHostUrl(workspaceId); + const handler = useEffectEvent(callback); + + useEffect(() => { + if (!enabled || !hostUrl) return; + + const bus = getEventBus(hostUrl, () => getHostServiceWsToken(hostUrl)); + const cleanups: Array<() => void> = []; + + if (type === "fs:events") { + bus.watchFs(workspaceId); + const removeListener = bus.on( + "fs:events", + workspaceId, + (_wid, payload) => { + for (const event of payload.events) { + (handler as (event: FsWatchEvent) => void)(event); + } + }, + ); + cleanups.push(removeListener, () => bus.unwatchFs(workspaceId)); + } else { + const removeListener = bus.on("git:changed", workspaceId, () => { + (handler as () => void)(); + }); + cleanups.push(removeListener); + } + + cleanups.push(bus.retain()); + + return () => { + for (const cleanup of cleanups) { + cleanup(); + } + }; + }, [enabled, hostUrl, type, workspaceId]); +} diff --git a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/index.ts b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/index.ts new file mode 100644 index 00000000000..4b07976d824 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/index.ts @@ -0,0 +1 @@ +export { useWorkspaceHostUrl } from "./useWorkspaceHostUrl"; diff --git a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts new file mode 100644 index 00000000000..cddbe90c9ef --- /dev/null +++ b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts @@ -0,0 +1,39 @@ +import { eq } from "@tanstack/db"; +import { useLiveQuery } from "@tanstack/react-db"; +import { useMemo } from "react"; +import { getRemoteHostUrl } from "renderer/lib/v2-workspace-host"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { useHostService } from "renderer/routes/_authenticated/providers/HostServiceProvider"; + +/** + * Resolves a workspace ID to its host-service URL. + * Local host → localhost port. Remote host → relay proxy URL. + */ +export function useWorkspaceHostUrl(workspaceId: string): string | null { + const collections = useCollections(); + const { services } = useHostService(); + + const { data: workspaceWithHost = [] } = useLiveQuery( + (q) => + q + .from({ workspaces: collections.v2Workspaces }) + .innerJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => + eq(workspaces.hostId, hosts.id), + ) + .where(({ workspaces }) => eq(workspaces.id, workspaceId)) + .select(({ workspaces, hosts }) => ({ + hostId: workspaces.hostId, + hostOrgId: hosts.organizationId, + })), + [collections, workspaceId], + ); + + const match = workspaceWithHost[0] ?? null; + + return useMemo(() => { + if (!match) return null; + const localService = services.get(match.hostOrgId); + if (localService) return localService.url; + return getRemoteHostUrl(match.hostId); + }, [match, services]); +} diff --git a/apps/desktop/src/renderer/lib/v2-workspace-host.ts b/apps/desktop/src/renderer/lib/v2-workspace-host.ts index 044e70a291f..058ffc3cc18 100644 --- a/apps/desktop/src/renderer/lib/v2-workspace-host.ts +++ b/apps/desktop/src/renderer/lib/v2-workspace-host.ts @@ -3,18 +3,14 @@ import { env } from "renderer/env.renderer"; export type WorkspaceHostTarget = | { kind: "local" } | { kind: "cloud" } - | { kind: "device"; deviceId: string }; + | { kind: "host"; hostId: string }; export function getCloudWorkspaceHostUrl(): string { - return `${env.NEXT_PUBLIC_API_URL}/api/v2-workspaces/cloud/host`; + return `${env.NEXT_PUBLIC_API_URL}/api/v2-hosts/cloud/trpc`; } -export function getWorkspaceHostUrlForDevice(deviceId: string): string { - return `${env.NEXT_PUBLIC_API_URL}/api/v2-devices/${deviceId}/host`; -} - -export function getWorkspaceHostUrlForWorkspace(workspaceId: string): string { - return `${env.NEXT_PUBLIC_API_URL}/api/v2-workspaces/${workspaceId}/host`; +export function getRemoteHostUrl(hostId: string): string { + return `${env.NEXT_PUBLIC_API_URL}/api/v2-hosts/${hostId}/trpc`; } export function resolveCreateWorkspaceHostUrl( @@ -26,7 +22,7 @@ export function resolveCreateWorkspaceHostUrl( return localHostUrl; case "cloud": return getCloudWorkspaceHostUrl(); - case "device": - return getWorkspaceHostUrlForDevice(target.deviceId); + case "host": + return getRemoteHostUrl(target.hostId); } } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index f26008f4ad6..1fc60f6f72f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -1,3 +1,4 @@ +import { useDiffStats } from "renderer/hooks/host-service/useDiffStats"; import type { DashboardSidebarWorkspace } from "../../types"; import { DashboardSidebarDeleteDialog } from "../DashboardSidebarDeleteDialog"; import { DashboardSidebarCollapsedWorkspaceButton } from "./components/DashboardSidebarCollapsedWorkspaceButton"; @@ -5,7 +6,6 @@ import { DashboardSidebarExpandedWorkspaceRow } from "./components/DashboardSide import { DashboardSidebarWorkspaceContextMenu } from "./components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu"; import { DashboardSidebarWorkspaceHoverCardContent } from "./components/DashboardSidebarWorkspaceHoverCardContent"; import { useDashboardSidebarWorkspaceItemActions } from "./hooks/useDashboardSidebarWorkspaceItemActions"; -import { getWorkspaceRowMocks } from "./utils"; interface DashboardSidebarWorkspaceItemProps { workspace: DashboardSidebarWorkspace; @@ -31,7 +31,7 @@ export function DashboardSidebarWorkspaceItem({ branch, creationStatus, } = workspace; - const mockData = getWorkspaceRowMocks(id); + const diffStats = useDiffStats(id); const { cancelRename, handleClick, @@ -73,7 +73,6 @@ export function DashboardSidebarWorkspaceItem({ hostType={hostType} isActive={isActive} onClick={isCreating ? undefined : handleClick} - workspaceStatus={isCreating ? null : mockData.workspaceStatus} creationStatus={creationStatus} disabled={isCreating} aria-label={ @@ -97,7 +96,7 @@ export function DashboardSidebarWorkspaceItem({ hoverCardContent={ } onCreateSection={handleCreateSection} @@ -135,7 +134,7 @@ export function DashboardSidebarWorkspaceItem({ isRenaming={isRenaming} renameValue={renameValue} shortcutLabel={shortcutLabel} - mockData={isCreating ? { ...mockData, workspaceStatus: null } : mockData} + diffStats={isCreating ? null : diffStats} onClick={isCreating ? undefined : handleClick} onDoubleClick={isCreating ? undefined : startRename} onDeleteClick={() => setIsDeleteDialogOpen(true)} @@ -159,7 +158,7 @@ export function DashboardSidebarWorkspaceItem({ hoverCardContent={ } onCreateSection={handleCreateSection} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx index eb92eff513d..52431995bd4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx @@ -8,10 +8,10 @@ import { useRef, } from "react"; import { HiMiniXMark } from "react-icons/hi2"; +import type { DiffStats } from "renderer/hooks/host-service/useDiffStats"; import { HotkeyLabel } from "renderer/hotkeys"; import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; import type { DashboardSidebarWorkspace } from "../../../../types"; -import type { WorkspaceRowMockData } from "../../utils"; import { getCreationStatusText } from "../../utils/getCreationStatusText"; import { DashboardSidebarWorkspaceDiffStats } from "../DashboardSidebarWorkspaceDiffStats"; import { DashboardSidebarWorkspaceIcon } from "../DashboardSidebarWorkspaceIcon"; @@ -24,7 +24,7 @@ interface DashboardSidebarExpandedWorkspaceRowProps isRenaming: boolean; renameValue: string; shortcutLabel?: string; - mockData: WorkspaceRowMockData; + diffStats: DiffStats | null; onClick?: () => void; onDoubleClick?: () => void; onDeleteClick: () => void; @@ -44,7 +44,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< isRenaming, renameValue, shortcutLabel, - mockData, + diffStats, onClick, onDoubleClick, onDeleteClick, @@ -124,7 +124,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< hostType={hostType} isActive={isActive} variant="expanded" - workspaceStatus={mockData.workspaceStatus} + workspaceStatus={null} creationStatus={creationStatus} /> @@ -167,11 +167,13 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< ) : ( <> - + {diffStats && ( + + )}
{shortcutLabel && ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/DashboardSidebarWorkspaceDiffStats.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/DashboardSidebarWorkspaceDiffStats.tsx index 05875fe4393..06d431aa2c9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/DashboardSidebarWorkspaceDiffStats.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/DashboardSidebarWorkspaceDiffStats.tsx @@ -14,7 +14,7 @@ export function DashboardSidebarWorkspaceDiffStats({ return (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx index 2434b65d0fe..667b816e84e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx @@ -3,9 +3,9 @@ import { Kbd, KbdGroup } from "@superset/ui/kbd"; import { formatDistanceToNow } from "date-fns"; import { FaGithub } from "react-icons/fa"; import { LuExternalLink, LuGlobe, LuTriangleAlert } from "react-icons/lu"; +import type { DiffStats } from "renderer/hooks/host-service/useDiffStats"; import { useHotkeyDisplay } from "renderer/hotkeys"; import type { DashboardSidebarWorkspace } from "../../../../types"; -import type { WorkspaceRowMockData } from "../../utils"; import { ChecksList } from "./components/ChecksList"; import { ChecksSummary } from "./components/ChecksSummary"; import { PullRequestStatusBadge } from "./components/PullRequestStatusBadge"; @@ -13,12 +13,12 @@ import { ReviewStatus } from "./components/ReviewStatus"; interface DashboardSidebarWorkspaceHoverCardContentProps { workspace: DashboardSidebarWorkspace; - mockData: WorkspaceRowMockData; + diffStats: DiffStats | null; } export function DashboardSidebarWorkspaceHoverCardContent({ workspace, - mockData, + diffStats, }: DashboardSidebarWorkspaceHoverCardContentProps) { const { name, @@ -107,14 +107,14 @@ export function DashboardSidebarWorkspaceHoverCardContent({ /> )}
-
- - +{mockData.diffStats.additions} - - - -{mockData.diffStats.deletions} - -
+ {diffStats && ( +
+ +{diffStats.additions} + + -{diffStats.deletions} + +
+ )}

diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx index 56276b432f2..c87f5536e6e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx @@ -1,5 +1,5 @@ import { cn } from "@superset/ui/utils"; -import { LuCloud, LuGitMerge, LuLaptop } from "react-icons/lu"; +import { LuCloud, LuFolderGit2, LuLaptop } from "react-icons/lu"; import { AsciiSpinner } from "renderer/screens/main/components/AsciiSpinner"; import { StatusIndicator } from "renderer/screens/main/components/StatusIndicator"; import type { ActivePaneStatus } from "shared/tabs-types"; @@ -50,7 +50,7 @@ export function DashboardSidebarWorkspaceIcon({ strokeWidth={1.75} /> ) : ( - seed + character.charCodeAt(0) * (index + 1), - 0, - ); -} - -export function getWorkspaceRowMocks( - workspaceId: string, -): WorkspaceRowMockData { - const seed = getSeed(workspaceId); - const paneStatuses: ActivePaneStatus[] = ["permission", "working", "review"]; - const status = - seed % 6 === 0 ? paneStatuses[seed % paneStatuses.length] : null; - - return { - diffStats: { - additions: (seed % 24) + 3, - deletions: (seed % 9) + 1, - }, - workspaceStatus: status, - }; -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts index d6d66d3e0c0..5298c6aa549 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts @@ -1,3 +1 @@ export { getCreationStatusText } from "./getCreationStatusText"; -export type { WorkspaceRowMockData } from "./getWorkspaceRowMocks"; -export { getWorkspaceRowMocks } from "./getWorkspaceRowMocks"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts index f17287a786e..445d21cf687 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts @@ -99,20 +99,18 @@ export function useDashboardSidebarData() { ({ sidebarWorkspaces, workspaces }) => eq(sidebarWorkspaces.workspaceId, workspaces.id), ) - .leftJoin( - { devices: collections.v2Devices }, - ({ workspaces, devices }) => eq(workspaces.deviceId, devices.id), + .leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => + eq(workspaces.hostId, hosts.id), ) .orderBy( ({ sidebarWorkspaces }) => sidebarWorkspaces.sidebarState.tabOrder, "asc", ) - .select(({ sidebarWorkspaces, workspaces, devices }) => ({ + .select(({ sidebarWorkspaces, workspaces, hosts }) => ({ id: workspaces.id, projectId: sidebarWorkspaces.sidebarState.projectId, - deviceId: workspaces.deviceId, - deviceType: devices?.type ?? null, - deviceClientId: devices?.clientId ?? null, + hostId: workspaces.hostId, + hostMachineId: hosts?.machineId ?? null, name: workspaces.name, branch: workspaces.branch, createdAt: workspaces.createdAt, @@ -128,8 +126,8 @@ export function useDashboardSidebarData() { sidebarWorkspaces .filter( (workspace) => - workspace.deviceType !== "cloud" && - workspace.deviceClientId === deviceInfo?.deviceId, + workspace.hostMachineId != null && + workspace.hostMachineId === deviceInfo?.deviceId, ) .map((workspace) => workspace.id) .sort(), @@ -144,7 +142,7 @@ export function useDashboardSidebarData() { localWorkspaceIds, ], enabled: activeHostService !== null && localWorkspaceIds.length > 0, - refetchInterval: 15_000, + refetchInterval: 10_000, queryFn: () => activeHostService?.client.pullRequests.getByWorkspaces.query({ workspaceIds: localWorkspaceIds, @@ -221,16 +219,16 @@ export function useDashboardSidebarData() { if (!project) continue; const hostType: DashboardSidebarWorkspace["hostType"] = - workspace.deviceType === "cloud" + workspace.hostMachineId == null ? "cloud" - : workspace.deviceClientId === deviceInfo?.deviceId + : workspace.hostMachineId === deviceInfo?.deviceId ? "local-device" : "remote-device"; const sidebarWorkspace: DashboardSidebarWorkspace = { id: workspace.id, projectId: workspace.projectId, - deviceId: workspace.deviceId, + hostId: workspace.hostId, hostType, accentColor: null, name: workspace.name, @@ -284,7 +282,7 @@ export function useDashboardSidebarData() { const pendingItem: DashboardSidebarWorkspace = { id: pendingWorkspace.id, projectId: pendingWorkspace.projectId, - deviceId: deviceInfo.deviceId, + hostId: "", hostType: "local-device", accentColor: null, name: pendingWorkspace.name, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts index bd8b377c4b2..a8a52331141 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts @@ -23,7 +23,7 @@ export interface DashboardSidebarWorkspacePullRequest { export interface DashboardSidebarWorkspace { id: string; projectId: string; - deviceId: string; + hostId: string; hostType: DashboardSidebarWorkspaceHostType; accentColor: string | null; name: string; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx index ccfb65d3a1f..1b7fb51f36d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx @@ -25,24 +25,24 @@ export function V2WorkspaceOpenInButton({ [collections, workspaceId], ); const workspace = workspaces[0] ?? null; - const { data: currentDevices = [] } = useLiveQuery( + const { data: localHosts = [] } = useLiveQuery( (q) => q - .from({ devices: collections.v2Devices }) - .where(({ devices }) => + .from({ hosts: collections.v2Hosts }) + .where(({ hosts }) => and( - eq(devices.clientId, deviceInfo?.deviceId ?? ""), - eq(devices.organizationId, workspace?.organizationId ?? ""), + eq(hosts.machineId, deviceInfo?.deviceId ?? ""), + eq(hosts.organizationId, workspace?.organizationId ?? ""), ), ), [collections, deviceInfo?.deviceId, workspace?.organizationId], ); - const currentDevice = currentDevices[0] ?? null; + const localHost = localHosts[0] ?? null; const hostUrl = workspace ? (services.get(workspace.organizationId)?.url ?? null) : null; const isLocalWorkspace = - Boolean(workspace) && workspace.deviceId === currentDevice?.id; + Boolean(workspace) && workspace.hostId === localHost?.id; const workspaceQuery = useQuery({ queryKey: ["v2-open-in-workspace", hostUrl, workspaceId], diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx index 9928aec55b2..c36c4056b63 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx @@ -2,15 +2,14 @@ import { alert } from "@superset/ui/atoms/Alert"; import { Button } from "@superset/ui/button"; import { toast } from "@superset/ui/sonner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { workspaceTrpc } from "@superset/workspace-client"; +import { FilePlus, FolderPlus, FoldVertical, RefreshCw } from "lucide-react"; +import { Fragment, useCallback, useEffect, useRef, useState } from "react"; import { type FileTreeNode, useFileTree, - useWorkspaceFsEventBridge, - useWorkspaceFsEvents, - workspaceTrpc, -} from "@superset/workspace-client"; -import { FilePlus, FolderPlus, FoldVertical, RefreshCw } from "lucide-react"; -import { Fragment, useCallback, useEffect, useRef, useState } from "react"; +} from "renderer/hooks/host-service/useFileTree"; +import { useWorkspaceEvent } from "renderer/hooks/host-service/useWorkspaceEvent"; import { ROW_HEIGHT, TREE_INDENT, @@ -173,14 +172,10 @@ export function FilesTab({ workspaceTrpc.filesystem.createDirectory.useMutation(); const movePath = workspaceTrpc.filesystem.movePath.useMutation(); - useWorkspaceFsEventBridge( - workspaceId, - Boolean(workspaceId && workspaceQuery.data?.worktreePath), - ); - const fileTree = useFileTree({ workspaceId, rootPath }); - useWorkspaceFsEvents( + useWorkspaceEvent( + "fs:events", workspaceId, () => void utils.filesystem.searchFiles.invalidate(), Boolean(workspaceId), diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx index f4d85c967df..848c37e887f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx @@ -1,7 +1,7 @@ import { ContextMenu, ContextMenuTrigger } from "@superset/ui/context-menu"; import { cn } from "@superset/ui/utils"; -import type { FileTreeNode } from "@superset/workspace-client"; import { LuChevronDown, LuChevronRight } from "react-icons/lu"; +import type { FileTreeNode } from "renderer/hooks/host-service/useFileTree"; import { FileIcon } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/utils"; import { FileContextMenu } from "./components/FileContextMenu"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index e3706d681b0..9dc4f1418a1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -4,6 +4,7 @@ import { workspaceTrpc } from "@superset/workspace-client"; import type { inferRouterOutputs } from "@trpc/server"; import { GitBranch, Pencil } from "lucide-react"; import { useCallback, useMemo, useRef, useState } from "react"; +import { useWorkspaceEvent } from "renderer/hooks/host-service/useWorkspaceEvent"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { ChangesFilter } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema"; import type { SidebarTabDefinition } from "../../types"; @@ -214,14 +215,16 @@ export function useChangesTab({ [collections, workspaceId], ); + const statusUtils = workspaceTrpc.useUtils(); + const status = workspaceTrpc.git.getStatus.useQuery( { workspaceId, baseBranch: baseBranch ?? undefined }, - { refetchInterval: 3_000, refetchOnWindowFocus: true }, + { refetchOnWindowFocus: true }, ); const commits = workspaceTrpc.git.listCommits.useQuery( { workspaceId, baseBranch: baseBranch ?? undefined }, - { refetchInterval: 3_000, refetchOnWindowFocus: true }, + { refetchOnWindowFocus: true }, ); const branches = workspaceTrpc.git.listBranches.useQuery( @@ -229,6 +232,11 @@ export function useChangesTab({ { refetchInterval: 30_000, refetchOnWindowFocus: true }, ); + useWorkspaceEvent("git:changed", workspaceId, () => { + void statusUtils.git.getStatus.invalidate({ workspaceId }); + void statusUtils.git.listCommits.invalidate({ workspaceId }); + }); + const renameBranchMutation = workspaceTrpc.git.renameBranch.useMutation(); const handleRenameBranch = useCallback( @@ -267,27 +275,25 @@ export function useChangesTab({ { enabled: filter.kind === "commit" || filter.kind === "range" }, ); - const totalChanges = status.data - ? status.data.againstBase.length + - status.data.staged.length + - status.data.unstaged.length - : 0; - - const totalAdditions = status.data - ? [ - ...status.data.againstBase, - ...status.data.staged, - ...status.data.unstaged, - ].reduce((sum, f) => sum + f.additions, 0) - : 0; - - const totalDeletions = status.data - ? [ - ...status.data.againstBase, - ...status.data.staged, - ...status.data.unstaged, - ].reduce((sum, f) => sum + f.deletions, 0) - : 0; + const filteredFiles = useMemo(() => { + if (!status.data) return []; + if (filter.kind === "uncommitted") { + return [...status.data.staged, ...status.data.unstaged]; + } + if (filter.kind === "commit" || filter.kind === "range") { + return commitFiles.data?.files ?? []; + } + // "all" — deduplicate by path + const map = new Map(); + for (const f of status.data.againstBase) map.set(f.path, f); + for (const f of status.data.staged) map.set(f.path, f); + for (const f of status.data.unstaged) map.set(f.path, f); + return Array.from(map.values()); + }, [status.data, filter.kind, commitFiles.data?.files]); + + const totalChanges = filteredFiles.length; + const totalAdditions = filteredFiles.reduce((sum, f) => sum + f.additions, 0); + const totalDeletions = filteredFiles.reduce((sum, f) => sum + f.deletions, 0); const content = useMemo(() => { if (status.isLoading) { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx index 587a32d0032..b99017294e6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx @@ -1,6 +1,6 @@ import type { RendererContext } from "@superset/panes"; -import { useFileDocument } from "@superset/workspace-client"; import { useCallback } from "react"; +import { useFileDocument } from "renderer/hooks/host-service/useFileDocument"; import { isImageFile, isMarkdownFile } from "shared/file-types"; import type { FilePaneData, PaneViewerData } from "../../../../types"; import { CodeRenderer } from "./renderers/CodeRenderer"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx index 0f896fdf4e0..662386d2d35 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx @@ -43,9 +43,9 @@ export function CodeRenderer({ }, [onSave]); return ( -

+
{hasExternalChange && } -
+
q - .from({ v2Devices: collections.v2Devices }) - .where(({ v2Devices }) => + .from({ v2Hosts: collections.v2Hosts }) + .where(({ v2Hosts }) => and( - eq(v2Devices.clientId, deviceInfo?.deviceId ?? ""), - eq(v2Devices.organizationId, workspace?.organizationId ?? ""), + eq(v2Hosts.machineId, deviceInfo?.deviceId ?? ""), + eq(v2Hosts.organizationId, workspace?.organizationId ?? ""), ), ), [collections, deviceInfo?.deviceId, workspace?.organizationId], ); - const currentDevice = currentDevices[0] ?? null; + const localHost = hosts[0] ?? null; const localHostUrl = workspace ? (services.get(workspace.organizationId)?.url ?? null) : null; const shouldWaitForDeviceInfo = workspace !== null && isDeviceInfoPending; - const isLocal = workspace?.deviceId === currentDevice?.id; + const isLocal = workspace?.hostId === localHost?.id; const hostUrl = !workspace || shouldWaitForDeviceInfo ? null : isLocal ? localHostUrl - : getWorkspaceHostUrlForWorkspace(workspace.id); + : getRemoteHostUrl(workspace.hostId); const lastEnsuredWorkspaceIdRef = useRef(null); diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/DevicePicker.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/DevicePicker.tsx index a585eebbcb2..89e9f9c87c3 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/DevicePicker.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/DevicePicker.tsx @@ -14,13 +14,12 @@ import { HiChevronUpDown, HiOutlineCloud, HiOutlineComputerDesktop, - HiOutlineGlobeAlt, HiOutlineServer, } from "react-icons/hi2"; import type { WorkspaceHostTarget } from "renderer/lib/v2-workspace-host"; import { useWorkspaceHostOptions, - type WorkspaceHostDeviceOption, + type WorkspaceHostOption, } from "./hooks/useWorkspaceHostOptions"; interface DevicePickerProps { @@ -28,21 +27,14 @@ interface DevicePickerProps { onSelectHostTarget: (target: WorkspaceHostTarget) => void; } -function getDeviceIcon(type: WorkspaceHostDeviceOption["type"]) { - switch (type) { - case "cloud": - return HiOutlineCloud; - case "viewer": - return HiOutlineGlobeAlt; - default: - return HiOutlineComputerDesktop; - } +function getHostIcon(host: WorkspaceHostOption) { + return host.isCloud ? HiOutlineCloud : HiOutlineComputerDesktop; } function getSelectedLabel( hostTarget: WorkspaceHostTarget, currentDeviceName: string | null, - otherDevices: WorkspaceHostDeviceOption[], + otherHosts: WorkspaceHostOption[], ) { if (hostTarget.kind === "local") { return currentDeviceName ?? "Local Device"; @@ -53,8 +45,8 @@ function getSelectedLabel( } return ( - otherDevices.find((device) => device.id === hostTarget.deviceId)?.name ?? - "Unknown Device" + otherHosts.find((host) => host.id === hostTarget.hostId)?.name ?? + "Unknown Host" ); } @@ -74,11 +66,11 @@ export function DevicePicker({ hostTarget, onSelectHostTarget, }: DevicePickerProps) { - const { currentDeviceName, otherDevices } = useWorkspaceHostOptions(); + const { currentDeviceName, otherHosts } = useWorkspaceHostOptions(); const selectedLabel = getSelectedLabel( hostTarget, currentDeviceName, - otherDevices, + otherHosts, ); return ( @@ -111,34 +103,30 @@ export function DevicePicker({ - Other Devices + Other Hosts - {otherDevices.length === 0 ? ( - No devices found + {otherHosts.length === 0 ? ( + No hosts found ) : ( - otherDevices.map((device) => { - const DeviceIcon = getDeviceIcon(device.type); + otherHosts.map((host) => { + const HostIcon = getHostIcon(host); const isSelected = - hostTarget.kind === "device" && - hostTarget.deviceId === device.id; + hostTarget.kind === "host" && hostTarget.hostId === host.id; return ( onSelectHostTarget({ - kind: "device", - deviceId: device.id, + kind: "host", + hostId: host.id, }) } > - +
-
{device.name}
-
- {device.type} -
+
{host.name}
{isSelected && }
diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/index.ts index 06c290fd571..c029dd2553e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/index.ts @@ -1,2 +1,2 @@ -export type { WorkspaceHostDeviceOption } from "./useWorkspaceHostOptions"; +export type { WorkspaceHostOption } from "./useWorkspaceHostOptions"; export { useWorkspaceHostOptions } from "./useWorkspaceHostOptions"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts index 43b33093306..de8ae1ba296 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts @@ -11,16 +11,16 @@ import { } from "renderer/routes/_authenticated/providers/HostServiceProvider"; import { MOCK_ORG_ID } from "shared/constants"; -export interface WorkspaceHostDeviceOption { +export interface WorkspaceHostOption { id: string; name: string; - type: "host" | "cloud" | "viewer"; + isCloud: boolean; } interface UseWorkspaceHostOptionsResult { currentDeviceName: string | null; localHostService: OrgService | null; - otherDevices: WorkspaceHostDeviceOption[]; + otherHosts: WorkspaceHostOption[]; } export function useWorkspaceHostOptions(): UseWorkspaceHostOptionsResult { @@ -39,45 +39,43 @@ export function useWorkspaceHostOptions(): UseWorkspaceHostOptionsResult { ? (services.get(activeOrganizationId) ?? null) : null; - const { data: accessibleDevices = [] } = useLiveQuery( + const { data: accessibleHosts = [] } = useLiveQuery( (q) => q - .from({ userDevices: collections.v2UsersDevices }) - .innerJoin( - { devices: collections.v2Devices }, - ({ userDevices, devices }) => eq(userDevices.deviceId, devices.id), + .from({ userHosts: collections.v2UsersHosts }) + .innerJoin({ hosts: collections.v2Hosts }, ({ userHosts, hosts }) => + eq(userHosts.hostId, hosts.id), ) - .where(({ userDevices, devices }) => + .where(({ userHosts, hosts }) => and( - eq(userDevices.userId, currentUserId ?? ""), - eq(devices.organizationId, activeOrganizationId ?? ""), + eq(userHosts.userId, currentUserId ?? ""), + eq(hosts.organizationId, activeOrganizationId ?? ""), ), ) - .select(({ devices }) => ({ - id: devices.id, - clientId: devices.clientId, - name: devices.name, - type: devices.type, + .select(({ hosts }) => ({ + id: hosts.id, + machineId: hosts.machineId, + name: hosts.name, })), [activeOrganizationId, collections, currentUserId], ); - const otherDevices = useMemo( + const otherHosts = useMemo( () => - accessibleDevices - .filter((device) => device.clientId !== deviceInfo?.deviceId) - .map((device) => ({ - id: device.id, - name: device.name, - type: device.type, + accessibleHosts + .filter((host) => host.machineId !== deviceInfo?.deviceId) + .map((host) => ({ + id: host.id, + name: host.name, + isCloud: host.machineId == null, })) .sort((a, b) => a.name.localeCompare(b.name)), - [accessibleDevices, deviceInfo?.deviceId], + [accessibleHosts, deviceInfo?.deviceId], ); return { currentDeviceName: deviceInfo?.deviceName ?? null, localHostService, - otherDevices, + otherHosts, }; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts index 76cca3d0f6d..7e63d20e21c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts @@ -14,9 +14,10 @@ import type { SelectTask, SelectTaskStatus, SelectUser, - SelectV2Device, + SelectV2Client, + SelectV2Host, SelectV2Project, - SelectV2UsersDevices, + SelectV2UsersHosts, SelectV2Workspace, SelectWorkspace, } from "@superset/db/schema"; @@ -67,9 +68,10 @@ export interface OrgCollections { tasks: Collection; taskStatuses: Collection; projects: Collection; - v2Devices: Collection; + v2Hosts: Collection; + v2Clients: Collection; + v2UsersHosts: Collection; v2Projects: Collection; - v2UsersDevices: Collection; v2Workspaces: Collection; workspaces: Collection; members: Collection; @@ -230,13 +232,13 @@ function createOrgCollections(organizationId: string): OrgCollections { }), ); - const v2Devices = createCollection( - electricCollectionOptions({ - id: `v2_devices-${organizationId}`, + const v2Hosts = createCollection( + electricCollectionOptions({ + id: `v2_hosts-${organizationId}`, shapeOptions: { url: electricUrl, params: { - table: "v2_devices", + table: "v2_hosts", organizationId, }, headers: electricHeaders, @@ -246,13 +248,29 @@ function createOrgCollections(organizationId: string): OrgCollections { }), ); - const v2UsersDevices = createCollection( - electricCollectionOptions({ - id: `v2_users_devices-${organizationId}`, + const v2Clients = createCollection( + electricCollectionOptions({ + id: `v2_clients-${organizationId}`, shapeOptions: { url: electricUrl, params: { - table: "v2_users_devices", + table: "v2_clients", + organizationId, + }, + headers: electricHeaders, + columnMapper, + }, + getKey: (item) => item.id, + }), + ); + + const v2UsersHosts = createCollection( + electricCollectionOptions({ + id: `v2_users_hosts-${organizationId}`, + shapeOptions: { + url: electricUrl, + params: { + table: "v2_users_hosts", organizationId, }, headers: electricHeaders, @@ -509,9 +527,10 @@ function createOrgCollections(organizationId: string): OrgCollections { tasks, taskStatuses, projects, - v2Devices, + v2Hosts, + v2Clients, + v2UsersHosts, v2Projects, - v2UsersDevices, v2Workspaces, workspaces, members, diff --git a/apps/electric-proxy/src/where.ts b/apps/electric-proxy/src/where.ts index b7ce63acc3c..43efdb7e006 100644 --- a/apps/electric-proxy/src/where.ts +++ b/apps/electric-proxy/src/where.ts @@ -13,10 +13,10 @@ import { subscriptions, taskStatuses, tasks, - v2DevicePresence, - v2Devices, + v2Clients, + v2Hosts, v2Projects, - v2UsersDevices, + v2UsersHosts, v2Workspaces, workspaces, } from "@superset/db/schema"; @@ -55,22 +55,14 @@ export function buildWhereClause( case "v2_projects": return build(v2Projects, v2Projects.organizationId, organizationId); - case "v2_devices": - return build(v2Devices, v2Devices.organizationId, organizationId); + case "v2_hosts": + return build(v2Hosts, v2Hosts.organizationId, organizationId); - case "v2_device_presence": - return build( - v2DevicePresence, - v2DevicePresence.organizationId, - organizationId, - ); + case "v2_clients": + return build(v2Clients, v2Clients.organizationId, organizationId); - case "v2_users_devices": - return build( - v2UsersDevices, - v2UsersDevices.organizationId, - organizationId, - ); + case "v2_users_hosts": + return build(v2UsersHosts, v2UsersHosts.organizationId, organizationId); case "v2_workspaces": return build(v2Workspaces, v2Workspaces.organizationId, organizationId); diff --git a/packages/db/drizzle/0031_v2_hosts_and_clients.sql b/packages/db/drizzle/0031_v2_hosts_and_clients.sql new file mode 100644 index 00000000000..e6857044734 --- /dev/null +++ b/packages/db/drizzle/0031_v2_hosts_and_clients.sql @@ -0,0 +1,63 @@ +DELETE FROM "v2_workspaces";--> statement-breakpoint +CREATE TYPE "public"."v2_client_type" AS ENUM('desktop', 'mobile', 'web');--> statement-breakpoint +CREATE TYPE "public"."v2_users_host_role" AS ENUM('owner', 'member');--> statement-breakpoint +CREATE TABLE "v2_clients" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "machine_id" text NOT NULL, + "type" "v2_client_type" NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "v2_clients_org_user_machine_unique" UNIQUE("organization_id","user_id","machine_id") +); +--> statement-breakpoint +CREATE TABLE "v2_hosts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "machine_id" text NOT NULL, + "name" text NOT NULL, + "last_seen_at" timestamp with time zone, + "created_by_user_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "v2_hosts_org_machine_id_unique" UNIQUE("organization_id","machine_id") +); +--> statement-breakpoint +CREATE TABLE "v2_users_hosts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "organization_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "host_id" uuid NOT NULL, + "role" "v2_users_host_role" DEFAULT 'member' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "v2_users_hosts_org_user_host_unique" UNIQUE("organization_id","user_id","host_id") +); +--> statement-breakpoint +ALTER TABLE "v2_device_presence" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint +ALTER TABLE "v2_devices" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint +ALTER TABLE "v2_users_devices" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint +DROP TABLE "v2_device_presence" CASCADE;--> statement-breakpoint +DROP TABLE "v2_devices" CASCADE;--> statement-breakpoint +DROP TABLE "v2_users_devices" CASCADE;--> statement-breakpoint +DROP INDEX IF EXISTS "v2_workspaces_device_id_idx";--> statement-breakpoint +ALTER TABLE "v2_workspaces" ADD COLUMN "host_id" uuid NOT NULL;--> statement-breakpoint +ALTER TABLE "v2_clients" ADD CONSTRAINT "v2_clients_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "auth"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "v2_clients" ADD CONSTRAINT "v2_clients_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "v2_hosts" ADD CONSTRAINT "v2_hosts_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "auth"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "v2_hosts" ADD CONSTRAINT "v2_hosts_created_by_user_id_users_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "auth"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "v2_users_hosts" ADD CONSTRAINT "v2_users_hosts_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "auth"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "v2_users_hosts" ADD CONSTRAINT "v2_users_hosts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "v2_users_hosts" ADD CONSTRAINT "v2_users_hosts_host_id_v2_hosts_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."v2_hosts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "v2_clients_organization_id_idx" ON "v2_clients" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "v2_clients_user_id_idx" ON "v2_clients" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "v2_hosts_organization_id_idx" ON "v2_hosts" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "v2_users_hosts_organization_id_idx" ON "v2_users_hosts" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "v2_users_hosts_user_id_idx" ON "v2_users_hosts" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "v2_users_hosts_host_id_idx" ON "v2_users_hosts" USING btree ("host_id");--> statement-breakpoint +ALTER TABLE "v2_workspaces" ADD CONSTRAINT "v2_workspaces_host_id_v2_hosts_id_fk" FOREIGN KEY ("host_id") REFERENCES "public"."v2_hosts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "v2_workspaces_host_id_idx" ON "v2_workspaces" USING btree ("host_id");--> statement-breakpoint +ALTER TABLE "v2_workspaces" DROP COLUMN "device_id";--> statement-breakpoint +DROP TYPE "public"."v2_device_type";--> statement-breakpoint +DROP TYPE "public"."v2_users_device_role"; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0031_snapshot.json b/packages/db/drizzle/meta/0031_snapshot.json new file mode 100644 index 00000000000..b7095caeb47 --- /dev/null +++ b/packages/db/drizzle/meta/0031_snapshot.json @@ -0,0 +1,5257 @@ +{ + "id": "117b93c3-ed88-4d67-8d8e-71b79af2f3af", + "prevId": "b4f3b2ee-8405-448f-b162-948895af940c", + "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 + } + }, + "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": {} + } + }, + "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 + } + }, + "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" + } + }, + "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 + }, + "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 + }, + "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 + } + }, + "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.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": "'{}'" + }, + "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": {}, + "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 + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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.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.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" + }, + "chat_sessions_v2_workspace_id_v2_workspaces_id_fk": { + "name": "chat_sessions_v2_workspace_id_v2_workspaces_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "v2_workspaces", + "columnsFrom": [ + "v2_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_presence": { + "name": "device_presence", + "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 + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_name": { + "name": "device_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "device_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_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()" + } + }, + "indexes": { + "device_presence_user_org_idx": { + "name": "device_presence_user_org_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_presence_user_device_idx": { + "name": "device_presence_user_device_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_presence_last_seen_idx": { + "name": "device_presence_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_presence_user_id_users_id_fk": { + "name": "device_presence_user_id_users_id_fk", + "tableFrom": "device_presence", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "device_presence_organization_id_organizations_id_fk": { + "name": "device_presence_organization_id_organizations_id_fk", + "tableFrom": "device_presence", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "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_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": { + "integration_connections_unique": { + "name": "integration_connections_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "provider" + ] + } + }, + "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.sandbox_images": { + "name": "sandbox_images", + "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 + }, + "setup_commands": { + "name": "setup_commands", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "base_image": { + "name": "base_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "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": { + "sandbox_images_organization_id_idx": { + "name": "sandbox_images_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sandbox_images_organization_id_organizations_id_fk": { + "name": "sandbox_images_organization_id_organizations_id_fk", + "tableFrom": "sandbox_images", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sandbox_images_project_id_projects_id_fk": { + "name": "sandbox_images_project_id_projects_id_fk", + "tableFrom": "sandbox_images", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sandbox_images_project_unique": { + "name": "sandbox_images_project_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secrets": { + "name": "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 + }, + "project_id": { + "name": "project_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, + "default": false + }, + "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": { + "secrets_project_id_idx": { + "name": "secrets_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secrets_organization_id_idx": { + "name": "secrets_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secrets_organization_id_organizations_id_fk": { + "name": "secrets_organization_id_organizations_id_fk", + "tableFrom": "secrets", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "secrets_project_id_projects_id_fk": { + "name": "secrets_project_id_projects_id_fk", + "tableFrom": "secrets", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "secrets_created_by_user_id_users_id_fk": { + "name": "secrets_created_by_user_id_users_id_fk", + "tableFrom": "secrets", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "secrets_project_key_unique": { + "name": "secrets_project_key_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_hosts": { + "name": "session_hosts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_hosts_session_id_idx": { + "name": "session_hosts_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_hosts_org_idx": { + "name": "session_hosts_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_hosts_device_id_idx": { + "name": "session_hosts_device_id_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_hosts_session_id_chat_sessions_id_fk": { + "name": "session_hosts_session_id_chat_sessions_id_fk", + "tableFrom": "session_hosts", + "tableTo": "chat_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_hosts_organization_id_organizations_id_fk": { + "name": "session_hosts_organization_id_organizations_id_fk", + "tableFrom": "session_hosts", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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 + }, + "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 + }, + "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_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.users__slack_users": { + "name": "users__slack_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "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 + }, + "model_preference": { + "name": "model_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users__slack_users_user_idx": { + "name": "users__slack_users_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users__slack_users_org_idx": { + "name": "users__slack_users_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users__slack_users_user_id_users_id_fk": { + "name": "users__slack_users_user_id_users_id_fk", + "tableFrom": "users__slack_users", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users__slack_users_organization_id_organizations_id_fk": { + "name": "users__slack_users_organization_id_organizations_id_fk", + "tableFrom": "users__slack_users", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users__slack_users_unique": { + "name": "users__slack_users_unique", + "nullsNotDistinct": false, + "columns": [ + "slack_user_id", + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_clients": { + "name": "v2_clients", + "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 + }, + "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": {}, + "uniqueConstraints": { + "v2_clients_org_user_machine_unique": { + "name": "v2_clients_org_user_machine_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "user_id", + "machine_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_hosts": { + "name": "v2_hosts", + "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 + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_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": { + "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": {}, + "uniqueConstraints": { + "v2_hosts_org_machine_id_unique": { + "name": "v2_hosts_org_machine_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "machine_id" + ] + } + }, + "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 + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "uuid", + "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_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": "restrict", + "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": { + "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 + }, + "host_id": { + "name": "host_id", + "type": "uuid", + "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_id_v2_hosts_id_fk": { + "name": "v2_users_hosts_host_id_v2_hosts_id_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "v2_hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "v2_users_hosts_org_user_host_unique": { + "name": "v2_users_hosts_org_user_host_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "user_id", + "host_id" + ] + } + }, + "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": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "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": { + "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": {} + } + }, + "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_project_id_v2_projects_id_fk": { + "name": "v2_workspaces_project_id_v2_projects_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "v2_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_workspaces_host_id_v2_hosts_id_fk": { + "name": "v2_workspaces_host_id_v2_hosts_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "v2_hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "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" + } + }, + "compositePrimaryKeys": {}, + "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 + } + }, + "enums": { + "public.command_status": { + "name": "command_status", + "schema": "public", + "values": [ + "pending", + "completed", + "failed", + "timeout" + ] + }, + "public.device_type": { + "name": "device_type", + "schema": "public", + "values": [ + "desktop", + "mobile", + "web" + ] + }, + "public.integration_provider": { + "name": "integration_provider", + "schema": "public", + "values": [ + "linear", + "github", + "slack" + ] + }, + "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.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 e5ff54de0c5..275965b18a1 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -218,6 +218,13 @@ "when": 1775419931545, "tag": "0030_better_auth_1_5_upgrade", "breakpoints": true + }, + { + "idx": 31, + "version": "7", + "when": 1775535603220, + "tag": "0031_v2_hosts_and_clients", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/enums.ts b/packages/db/src/schema/enums.ts index 368efaa5913..ca37b8ecf8a 100644 --- a/packages/db/src/schema/enums.ts +++ b/packages/db/src/schema/enums.ts @@ -31,13 +31,13 @@ export const deviceTypeValues = ["desktop", "mobile", "web"] as const; export const deviceTypeEnum = z.enum(deviceTypeValues); export type DeviceType = z.infer; -export const v2DeviceTypeValues = ["host", "cloud", "viewer"] as const; -export const v2DeviceTypeEnum = z.enum(v2DeviceTypeValues); -export type V2DeviceType = z.infer; +export const v2ClientTypeValues = ["desktop", "mobile", "web"] as const; +export const v2ClientTypeEnum = z.enum(v2ClientTypeValues); +export type V2ClientType = z.infer; -export const v2UsersDeviceRoleValues = ["owner", "member", "viewer"] as const; -export const v2UsersDeviceRoleEnum = z.enum(v2UsersDeviceRoleValues); -export type V2UsersDeviceRole = z.infer; +export const v2UsersHostRoleValues = ["owner", "member"] as const; +export const v2UsersHostRoleEnum = z.enum(v2UsersHostRoleValues); +export type V2UsersHostRole = z.infer; export const commandStatusValues = [ "pending", diff --git a/packages/db/src/schema/relations.ts b/packages/db/src/schema/relations.ts index 58c5ad708d7..fa9400cf189 100644 --- a/packages/db/src/schema/relations.ts +++ b/packages/db/src/schema/relations.ts @@ -26,10 +26,10 @@ import { taskStatuses, tasks, usersSlackUsers, - v2DevicePresence, - v2Devices, + v2Clients, + v2Hosts, v2Projects, - v2UsersDevices, + v2UsersHosts, v2Workspaces, workspaces, } from "./schema"; @@ -44,8 +44,9 @@ export const usersRelations = relations(users, ({ many }) => ({ connectedIntegrations: many(integrationConnections), githubInstallations: many(githubInstallations), devicePresence: many(devicePresence), - v2Devices: many(v2Devices), - v2UsersDevices: many(v2UsersDevices), + v2Hosts: many(v2Hosts), + v2Clients: many(v2Clients), + v2UsersHosts: many(v2UsersHosts), v2Workspaces: many(v2Workspaces), agentCommands: many(agentCommands), chatSessions: many(chatSessions), @@ -70,10 +71,10 @@ export const organizationsRelations = relations(organizations, ({ many }) => ({ invitations: many(invitations), subscriptions: many(subscriptions), projects: many(projects), - v2Devices: many(v2Devices), - v2DevicePresence: many(v2DevicePresence), + v2Hosts: many(v2Hosts), + v2Clients: many(v2Clients), + v2UsersHosts: many(v2UsersHosts), v2Projects: many(v2Projects), - v2UsersDevices: many(v2UsersDevices), v2Workspaces: many(v2Workspaces), secrets: many(secrets), sandboxImages: many(sandboxImages), @@ -279,51 +280,44 @@ export const v2ProjectsRelations = relations(v2Projects, ({ one, many }) => ({ workspaces: many(v2Workspaces), })); -export const v2DevicesRelations = relations(v2Devices, ({ one, many }) => ({ +export const v2HostsRelations = relations(v2Hosts, ({ one, many }) => ({ organization: one(organizations, { - fields: [v2Devices.organizationId], + fields: [v2Hosts.organizationId], references: [organizations.id], }), createdBy: one(users, { - fields: [v2Devices.createdByUserId], + fields: [v2Hosts.createdByUserId], references: [users.id], }), - presence: one(v2DevicePresence, { - fields: [v2Devices.id], - references: [v2DevicePresence.deviceId], - }), - usersDevices: many(v2UsersDevices), + usersHosts: many(v2UsersHosts), workspaces: many(v2Workspaces), })); -export const v2UsersDevicesRelations = relations(v2UsersDevices, ({ one }) => ({ +export const v2ClientsRelations = relations(v2Clients, ({ one }) => ({ organization: one(organizations, { - fields: [v2UsersDevices.organizationId], + fields: [v2Clients.organizationId], references: [organizations.id], }), user: one(users, { - fields: [v2UsersDevices.userId], + fields: [v2Clients.userId], references: [users.id], }), - device: one(v2Devices, { - fields: [v2UsersDevices.deviceId], - references: [v2Devices.id], - }), })); -export const v2DevicePresenceRelations = relations( - v2DevicePresence, - ({ one }) => ({ - organization: one(organizations, { - fields: [v2DevicePresence.organizationId], - references: [organizations.id], - }), - device: one(v2Devices, { - fields: [v2DevicePresence.deviceId], - references: [v2Devices.id], - }), +export const v2UsersHostsRelations = relations(v2UsersHosts, ({ one }) => ({ + organization: one(organizations, { + fields: [v2UsersHosts.organizationId], + references: [organizations.id], }), -); + user: one(users, { + fields: [v2UsersHosts.userId], + references: [users.id], + }), + host: one(v2Hosts, { + fields: [v2UsersHosts.hostId], + references: [v2Hosts.id], + }), +})); export const v2WorkspacesRelations = relations( v2Workspaces, @@ -336,9 +330,9 @@ export const v2WorkspacesRelations = relations( fields: [v2Workspaces.projectId], references: [v2Projects.id], }), - device: one(v2Devices, { - fields: [v2Workspaces.deviceId], - references: [v2Devices.id], + host: one(v2Hosts, { + fields: [v2Workspaces.hostId], + references: [v2Hosts.id], }), createdBy: one(users, { fields: [v2Workspaces.createdByUserId], diff --git a/packages/db/src/schema/schema.ts b/packages/db/src/schema/schema.ts index 6d5d6c3dfcb..9c7402e9f77 100644 --- a/packages/db/src/schema/schema.ts +++ b/packages/db/src/schema/schema.ts @@ -20,8 +20,8 @@ import { integrationProviderValues, taskPriorityValues, taskStatusEnumValues, - v2DeviceTypeValues, - v2UsersDeviceRoleValues, + v2ClientTypeValues, + v2UsersHostRoleValues, workspaceTypeValues, } from "./enums"; import { githubRepositories } from "./github"; @@ -35,12 +35,12 @@ export const integrationProvider = pgEnum( integrationProviderValues, ); export const deviceType = pgEnum("device_type", deviceTypeValues); -export const v2DeviceType = pgEnum("v2_device_type", v2DeviceTypeValues); -export const v2UsersDeviceRole = pgEnum( - "v2_users_device_role", - v2UsersDeviceRoleValues, -); export const commandStatus = pgEnum("command_status", commandStatusValues); +export const v2ClientType = pgEnum("v2_client_type", v2ClientTypeValues); +export const v2UsersHostRole = pgEnum( + "v2_users_host_role", + v2UsersHostRoleValues, +); export const taskStatuses = pgTable( "task_statuses", @@ -406,16 +406,16 @@ export const v2Projects = pgTable( export type InsertV2Project = typeof v2Projects.$inferInsert; export type SelectV2Project = typeof v2Projects.$inferSelect; -export const v2Devices = pgTable( - "v2_devices", +export const v2Hosts = pgTable( + "v2_hosts", { id: uuid().primaryKey().defaultRandom(), organizationId: uuid("organization_id") .notNull() .references(() => organizations.id, { onDelete: "cascade" }), - clientId: text("client_id"), + machineId: text("machine_id").notNull(), name: text().notNull(), - type: v2DeviceType().notNull(), + lastSeenAt: timestamp("last_seen_at", { withTimezone: true }), createdByUserId: uuid("created_by_user_id").references(() => users.id, { onDelete: "set null", }), @@ -428,19 +428,19 @@ export const v2Devices = pgTable( .$onUpdate(() => new Date()), }, (table) => [ - index("v2_devices_organization_id_idx").on(table.organizationId), - unique("v2_devices_org_client_id_unique").on( + index("v2_hosts_organization_id_idx").on(table.organizationId), + unique("v2_hosts_org_machine_id_unique").on( table.organizationId, - table.clientId, + table.machineId, ), ], ); -export type InsertV2Device = typeof v2Devices.$inferInsert; -export type SelectV2Device = typeof v2Devices.$inferSelect; +export type InsertV2Host = typeof v2Hosts.$inferInsert; +export type SelectV2Host = typeof v2Hosts.$inferSelect; -export const v2UsersDevices = pgTable( - "v2_users_devices", +export const v2Clients = pgTable( + "v2_clients", { id: uuid().primaryKey().defaultRandom(), organizationId: uuid("organization_id") @@ -449,10 +449,8 @@ export const v2UsersDevices = pgTable( userId: uuid("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), - deviceId: uuid("device_id") - .notNull() - .references(() => v2Devices.id, { onDelete: "cascade" }), - role: v2UsersDeviceRole().notNull().default("member"), + machineId: text("machine_id").notNull(), + type: v2ClientType().notNull(), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), @@ -462,31 +460,33 @@ export const v2UsersDevices = pgTable( .$onUpdate(() => new Date()), }, (table) => [ - index("v2_users_devices_organization_id_idx").on(table.organizationId), - index("v2_users_devices_user_id_idx").on(table.userId), - index("v2_users_devices_device_id_idx").on(table.deviceId), - unique("v2_users_devices_user_device_unique").on( + index("v2_clients_organization_id_idx").on(table.organizationId), + index("v2_clients_user_id_idx").on(table.userId), + unique("v2_clients_org_user_machine_unique").on( + table.organizationId, table.userId, - table.deviceId, + table.machineId, ), ], ); -export type InsertV2UsersDevices = typeof v2UsersDevices.$inferInsert; -export type SelectV2UsersDevices = typeof v2UsersDevices.$inferSelect; +export type InsertV2Client = typeof v2Clients.$inferInsert; +export type SelectV2Client = typeof v2Clients.$inferSelect; -export const v2DevicePresence = pgTable( - "v2_device_presence", +export const v2UsersHosts = pgTable( + "v2_users_hosts", { - deviceId: uuid("device_id") - .primaryKey() - .references(() => v2Devices.id, { onDelete: "cascade" }), + id: uuid().primaryKey().defaultRandom(), organizationId: uuid("organization_id") .notNull() .references(() => organizations.id, { onDelete: "cascade" }), - lastSeenAt: timestamp("last_seen_at", { withTimezone: true }) + userId: uuid("user_id") .notNull() - .defaultNow(), + .references(() => users.id, { onDelete: "cascade" }), + hostId: uuid("host_id") + .notNull() + .references(() => v2Hosts.id, { onDelete: "cascade" }), + role: v2UsersHostRole().notNull().default("member"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), @@ -496,13 +496,19 @@ export const v2DevicePresence = pgTable( .$onUpdate(() => new Date()), }, (table) => [ - index("v2_device_presence_organization_id_idx").on(table.organizationId), - index("v2_device_presence_last_seen_idx").on(table.lastSeenAt), + index("v2_users_hosts_organization_id_idx").on(table.organizationId), + index("v2_users_hosts_user_id_idx").on(table.userId), + index("v2_users_hosts_host_id_idx").on(table.hostId), + unique("v2_users_hosts_org_user_host_unique").on( + table.organizationId, + table.userId, + table.hostId, + ), ], ); -export type InsertV2DevicePresence = typeof v2DevicePresence.$inferInsert; -export type SelectV2DevicePresence = typeof v2DevicePresence.$inferSelect; +export type InsertV2UsersHosts = typeof v2UsersHosts.$inferInsert; +export type SelectV2UsersHosts = typeof v2UsersHosts.$inferSelect; export const v2Workspaces = pgTable( "v2_workspaces", @@ -514,9 +520,9 @@ export const v2Workspaces = pgTable( projectId: uuid("project_id") .notNull() .references(() => v2Projects.id, { onDelete: "cascade" }), - deviceId: uuid("device_id") + hostId: uuid("host_id") .notNull() - .references(() => v2Devices.id), + .references(() => v2Hosts.id), name: text().notNull(), branch: text().notNull(), createdByUserId: uuid("created_by_user_id").references(() => users.id, { @@ -533,7 +539,7 @@ export const v2Workspaces = pgTable( (table) => [ index("v2_workspaces_project_id_idx").on(table.projectId), index("v2_workspaces_organization_id_idx").on(table.organizationId), - index("v2_workspaces_device_id_idx").on(table.deviceId), + index("v2_workspaces_host_id_idx").on(table.hostId), ], ); diff --git a/packages/host-service/package.json b/packages/host-service/package.json index 3d26d7ef096..2875d4d984d 100644 --- a/packages/host-service/package.json +++ b/packages/host-service/package.json @@ -12,9 +12,9 @@ "types": "./src/db/index.ts", "default": "./src/db/index.ts" }, - "./filesystem": { - "types": "./src/filesystem/index.ts", - "default": "./src/filesystem/index.ts" + "./events": { + "types": "./src/events/index.ts", + "default": "./src/events/index.ts" }, "./git": { "types": "./src/runtime/git/index.ts", diff --git a/packages/host-service/src/app.ts b/packages/host-service/src/app.ts index 05aaedf05a1..22ac1c21131 100644 --- a/packages/host-service/src/app.ts +++ b/packages/host-service/src/app.ts @@ -8,7 +8,7 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; import { createApiClient } from "./api"; import { createDb } from "./db"; -import { registerWorkspaceFilesystemEventsRoute } from "./filesystem"; +import { EventBus, registerEventBusRoute } from "./events"; import type { ApiAuthProvider } from "./providers/auth"; import { LocalGitCredentialProvider } from "./providers/git"; import type { HostAuthProvider } from "./providers/host-auth"; @@ -93,6 +93,9 @@ export function createApp(options?: CreateAppOptions): CreateAppResult { }), ); + const eventBus = new EventBus({ db, filesystem }); + eventBus.start(); + if (options?.hostAuth) { const { hostAuth } = options; const wsAuth: MiddlewareHandler = async (c, next) => { @@ -104,14 +107,10 @@ export function createApp(options?: CreateAppOptions): CreateAppResult { return next(); }; app.use("/terminal/*", wsAuth); - app.use("/workspace-filesystem/*", wsAuth); + app.use("/events", wsAuth); } - registerWorkspaceFilesystemEventsRoute({ - app, - filesystem, - upgradeWebSocket, - }); + registerEventBusRoute({ app, eventBus, upgradeWebSocket }); registerWorkspaceTerminalRoute({ app, db, diff --git a/packages/host-service/src/events/event-bus.ts b/packages/host-service/src/events/event-bus.ts new file mode 100644 index 00000000000..bf9d8f3d25c --- /dev/null +++ b/packages/host-service/src/events/event-bus.ts @@ -0,0 +1,252 @@ +import type { NodeWebSocket } from "@hono/node-ws"; +import type { FsWatchEvent } from "@superset/workspace-fs/host"; +import type { Hono } from "hono"; +import type { HostDb } from "../db"; +import type { WorkspaceFilesystemManager } from "../runtime/filesystem"; +import { GitWatcher } from "./git-watcher"; +import type { ClientMessage, ServerMessage } from "./types"; + +type WsSocket = { + send: (data: string) => void; + readyState: number; + close: (code?: number, reason?: string) => void; +}; + +interface FsSubscription { + workspaceId: string; + dispose: () => void; +} + +interface ClientState { + fsSubscriptions: Map; +} + +function sendMessage(socket: WsSocket, message: ServerMessage): void { + if (socket.readyState !== 1) return; + socket.send(JSON.stringify(message)); +} + +function parseClientMessage(data: unknown): ClientMessage | null { + try { + const raw = typeof data === "string" ? data : String(data); + const parsed = JSON.parse(raw); + if ( + parsed && + typeof parsed === "object" && + typeof parsed.type === "string" && + typeof parsed.workspaceId === "string" + ) { + if (parsed.type === "fs:watch" || parsed.type === "fs:unwatch") { + return parsed as ClientMessage; + } + } + } catch { + // Malformed message — ignore + } + return null; +} + +export interface EventBusOptions { + db: HostDb; + filesystem: WorkspaceFilesystemManager; +} + +/** + * Unified WebSocket event bus for the host-service. + * + * One connection per client. Carries: + * - `git:changed` events (auto-pushed for all workspaces) + * - `fs:events` (on-demand per client request) + */ +export class EventBus { + private readonly clients = new Map(); + private readonly gitWatcher: GitWatcher; + private readonly filesystem: WorkspaceFilesystemManager; + private removeGitListener: (() => void) | null = null; + + constructor(options: EventBusOptions) { + this.filesystem = options.filesystem; + this.gitWatcher = new GitWatcher(options.db); + } + + start(): void { + this.gitWatcher.start(); + this.removeGitListener = this.gitWatcher.onChanged((workspaceId) => { + this.broadcast({ type: "git:changed", workspaceId }); + }); + } + + close(): void { + this.removeGitListener?.(); + this.removeGitListener = null; + this.gitWatcher.close(); + for (const [socket, state] of this.clients) { + this.cleanupClient(socket, state); + } + this.clients.clear(); + } + + handleOpen(socket: WsSocket): void { + this.clients.set(socket, { fsSubscriptions: new Map() }); + } + + handleMessage(socket: WsSocket, data: unknown): void { + const state = this.clients.get(socket); + if (!state) return; + + const message = parseClientMessage(data); + if (!message) return; + + if (message.type === "fs:watch") { + this.startFsWatch(socket, state, message.workspaceId); + } else if (message.type === "fs:unwatch") { + this.stopFsWatch(state, message.workspaceId); + } + } + + handleClose(socket: WsSocket): void { + const state = this.clients.get(socket); + if (state) { + this.cleanupClient(socket, state); + this.clients.delete(socket); + } + } + + private broadcast(message: ServerMessage): void { + for (const socket of this.clients.keys()) { + sendMessage(socket, message); + } + } + + private startFsWatch( + socket: WsSocket, + state: ClientState, + workspaceId: string, + ): void { + // Already watching this workspace for this client + if (state.fsSubscriptions.has(workspaceId)) return; + + let rootPath: string; + try { + rootPath = this.filesystem.resolveWorkspaceRoot(workspaceId); + } catch { + sendMessage(socket, { + type: "error", + message: `Workspace not found: ${workspaceId}`, + }); + return; + } + + let disposed = false; + let iterator: AsyncIterator<{ events: FsWatchEvent[] }> | null = null; + + try { + const service = this.filesystem.getServiceForWorkspace(workspaceId); + const stream = service.watchPath({ + absolutePath: rootPath, + recursive: true, + }); + iterator = stream[Symbol.asyncIterator](); + } catch (error) { + sendMessage(socket, { + type: "error", + message: + error instanceof Error + ? error.message + : "Failed to start filesystem watcher", + }); + return; + } + + const dispose = () => { + disposed = true; + void iterator?.return?.().catch((error: unknown) => { + console.error("[event-bus] fs watcher cleanup failed:", { + workspaceId, + error, + }); + }); + iterator = null; + }; + + state.fsSubscriptions.set(workspaceId, { workspaceId, dispose }); + + // Start streaming events to this client + void (async () => { + try { + while (!disposed && iterator) { + const next = await iterator.next(); + if (disposed || next.done) return; + + sendMessage(socket, { + type: "fs:events", + workspaceId, + events: next.value.events, + }); + } + } catch (error) { + if (disposed) return; + console.error("[event-bus] fs stream failed:", { + workspaceId, + error, + }); + sendMessage(socket, { + type: "error", + message: + error instanceof Error + ? error.message + : "Filesystem event stream failed", + }); + } + })(); + } + + private stopFsWatch(state: ClientState, workspaceId: string): void { + const sub = state.fsSubscriptions.get(workspaceId); + if (sub) { + sub.dispose(); + state.fsSubscriptions.delete(workspaceId); + } + } + + private cleanupClient(_socket: WsSocket, state: ClientState): void { + for (const sub of state.fsSubscriptions.values()) { + sub.dispose(); + } + state.fsSubscriptions.clear(); + } +} + +// ── Route Registration ───────────────────────────────────────────── + +export interface RegisterEventBusRouteOptions { + app: Hono; + eventBus: EventBus; + upgradeWebSocket: NodeWebSocket["upgradeWebSocket"]; +} + +export function registerEventBusRoute({ + app, + eventBus, + upgradeWebSocket, +}: RegisterEventBusRouteOptions) { + app.get( + "/events", + upgradeWebSocket(() => { + return { + onOpen: (_event, ws) => { + eventBus.handleOpen(ws); + }, + onMessage: (event, ws) => { + eventBus.handleMessage(ws, event.data); + }, + onClose: (_event, ws) => { + eventBus.handleClose(ws); + }, + onError: (_event, ws) => { + eventBus.handleClose(ws); + }, + }; + }), + ); +} diff --git a/packages/host-service/src/events/git-watcher.ts b/packages/host-service/src/events/git-watcher.ts new file mode 100644 index 00000000000..ede0175ebe3 --- /dev/null +++ b/packages/host-service/src/events/git-watcher.ts @@ -0,0 +1,165 @@ +import { execFile } from "node:child_process"; +import { type FSWatcher, watch } from "node:fs"; +import { promisify } from "node:util"; +import type { HostDb } from "../db"; +import { workspaces } from "../db/schema"; + +const execFileAsync = promisify(execFile); + +const RESCAN_INTERVAL_MS = 30_000; +const DEBOUNCE_MS = 300; + +export type GitChangedListener = (workspaceId: string) => void; + +interface WatchedWorkspace { + workspaceId: string; + worktreePath: string; + gitDir: string; + watcher: FSWatcher; +} + +/** + * Watches `.git` directories for all workspaces in the host-service DB. + * Emits workspace IDs when git state changes (commits, staging, branch switches, etc). + * Auto-discovers new workspaces and stops watching removed ones every 30s. + */ +export class GitWatcher { + private readonly db: HostDb; + private readonly listeners = new Set(); + private readonly watched = new Map(); + private readonly debounceTimers = new Map< + string, + ReturnType + >(); + private rescanTimer: ReturnType | null = null; + private closed = false; + + constructor(db: HostDb) { + this.db = db; + } + + start(): void { + void this.rescan(); + this.rescanTimer = setInterval( + () => void this.rescan(), + RESCAN_INTERVAL_MS, + ); + } + + onChanged(listener: GitChangedListener): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + close(): void { + this.closed = true; + if (this.rescanTimer) { + clearInterval(this.rescanTimer); + this.rescanTimer = null; + } + for (const timer of this.debounceTimers.values()) { + clearTimeout(timer); + } + this.debounceTimers.clear(); + for (const entry of this.watched.values()) { + entry.watcher.close(); + } + this.watched.clear(); + } + + private debouncedEmit(workspaceId: string): void { + const existing = this.debounceTimers.get(workspaceId); + if (existing) clearTimeout(existing); + this.debounceTimers.set( + workspaceId, + setTimeout(() => { + this.debounceTimers.delete(workspaceId); + for (const listener of this.listeners) { + listener(workspaceId); + } + }, DEBOUNCE_MS), + ); + } + + private async rescan(): Promise { + if (this.closed) return; + + let rows: Array<{ id: string; worktreePath: string }>; + try { + rows = this.db + .select({ + id: workspaces.id, + worktreePath: workspaces.worktreePath, + }) + .from(workspaces) + .all(); + } catch { + return; + } + + const currentIds = new Set(rows.map((r) => r.id)); + + // Remove watchers for workspaces that no longer exist + for (const [id, entry] of this.watched) { + if (!currentIds.has(id)) { + entry.watcher.close(); + this.watched.delete(id); + } + } + + // Add watchers for new workspaces + for (const row of rows) { + if (this.watched.has(row.id)) continue; + await this.watchWorkspace(row.id, row.worktreePath); + } + } + + private async watchWorkspace( + workspaceId: string, + worktreePath: string, + ): Promise { + if (this.closed) return; + + let gitDir: string; + try { + const { stdout } = await execFileAsync( + "git", + ["rev-parse", "--git-dir"], + { cwd: worktreePath }, + ); + gitDir = stdout.trim(); + // If relative, resolve against worktree path + if (!gitDir.startsWith("/")) { + gitDir = `${worktreePath}/${gitDir}`; + } + } catch { + // Not a git repo or path doesn't exist — skip + return; + } + + if (this.closed || this.watched.has(workspaceId)) return; + + try { + const watcher = watch(gitDir, { recursive: true }, () => { + this.debouncedEmit(workspaceId); + }); + + watcher.on("error", () => { + // Watcher died — remove it so rescan can re-add + this.watched.delete(workspaceId); + watcher.close(); + }); + + this.watched.set(workspaceId, { + workspaceId, + worktreePath, + gitDir, + watcher, + }); + } catch { + // fs.watch failed (e.g. directory doesn't exist) + } + } +} diff --git a/packages/host-service/src/events/index.ts b/packages/host-service/src/events/index.ts new file mode 100644 index 00000000000..e64d91df2bd --- /dev/null +++ b/packages/host-service/src/events/index.ts @@ -0,0 +1,10 @@ +export { EventBus, registerEventBusRoute } from "./event-bus"; +export type { + ClientMessage, + EventBusErrorMessage, + FsEventsMessage, + FsUnwatchCommand, + FsWatchCommand, + GitChangedMessage, + ServerMessage, +} from "./types"; diff --git a/packages/host-service/src/events/types.ts b/packages/host-service/src/events/types.ts new file mode 100644 index 00000000000..7915f296975 --- /dev/null +++ b/packages/host-service/src/events/types.ts @@ -0,0 +1,38 @@ +import type { FsWatchEvent } from "@superset/workspace-fs/host"; + +// ── Server → Client ──────────────────────────────────────────────── + +export interface FsEventsMessage { + type: "fs:events"; + workspaceId: string; + events: FsWatchEvent[]; +} + +export interface GitChangedMessage { + type: "git:changed"; + workspaceId: string; +} + +export interface EventBusErrorMessage { + type: "error"; + message: string; +} + +export type ServerMessage = + | FsEventsMessage + | GitChangedMessage + | EventBusErrorMessage; + +// ── Client → Server ──────────────────────────────────────────────── + +export interface FsWatchCommand { + type: "fs:watch"; + workspaceId: string; +} + +export interface FsUnwatchCommand { + type: "fs:unwatch"; + workspaceId: string; +} + +export type ClientMessage = FsWatchCommand | FsUnwatchCommand; diff --git a/packages/host-service/src/filesystem/events.ts b/packages/host-service/src/filesystem/events.ts deleted file mode 100644 index 2c54e90e397..00000000000 --- a/packages/host-service/src/filesystem/events.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { NodeWebSocket } from "@hono/node-ws"; -import type { FsWatchEvent } from "@superset/workspace-fs/host"; -import type { Hono } from "hono"; -import type { WorkspaceFilesystemManager } from "../runtime/filesystem"; - -export interface WorkspaceFilesystemEventsMessage { - type: "events"; - events: FsWatchEvent[]; -} - -export interface WorkspaceFilesystemErrorMessage { - type: "error"; - message: string; -} - -export type WorkspaceFilesystemServerMessage = - | WorkspaceFilesystemEventsMessage - | WorkspaceFilesystemErrorMessage; - -export function buildWorkspaceFilesystemEventsPath( - workspaceId: string, -): string { - return `/workspace-filesystem/${encodeURIComponent(workspaceId)}/events`; -} - -interface RegisterWorkspaceFilesystemEventsRouteOptions { - app: Hono; - filesystem: WorkspaceFilesystemManager; - upgradeWebSocket: NodeWebSocket["upgradeWebSocket"]; -} - -function sendMessage( - socket: { - send: (data: string) => void; - readyState: number; - close: (code?: number, reason?: string) => void; - }, - message: WorkspaceFilesystemServerMessage, -): void { - if (socket.readyState !== 1) { - return; - } - - socket.send(JSON.stringify(message)); -} - -export function registerWorkspaceFilesystemEventsRoute({ - app, - filesystem, - upgradeWebSocket, -}: RegisterWorkspaceFilesystemEventsRouteOptions) { - app.get( - "/workspace-filesystem/:workspaceId/events", - upgradeWebSocket((c) => { - const workspaceId = c.req.param("workspaceId"); - let disposed = false; - let iterator: AsyncIterator<{ events: FsWatchEvent[] }> | null = null; - - const disposeIterator = () => { - if (disposed) { - return; - } - - disposed = true; - const currentIterator = iterator; - iterator = null; - void currentIterator?.return?.().catch((error: unknown) => { - console.error( - "[host-service/workspace-filesystem-events] Cleanup failed:", - { - workspaceId, - error, - }, - ); - }); - }; - - return { - onOpen: (_event, ws) => { - if (!workspaceId) { - sendMessage(ws, { - type: "error", - message: "Workspace not found", - }); - ws.close(1008, "Workspace not found"); - return; - } - - let rootPath: string; - try { - rootPath = filesystem.resolveWorkspaceRoot(workspaceId); - } catch (error) { - sendMessage(ws, { - type: "error", - message: - error instanceof Error ? error.message : "Workspace not found", - }); - ws.close(1011, "Workspace not found"); - return; - } - - try { - const service = filesystem.getServiceForWorkspace(workspaceId); - iterator = service - .watchPath({ - absolutePath: rootPath, - recursive: true, - }) - [Symbol.asyncIterator](); - } catch (error) { - sendMessage(ws, { - type: "error", - message: - error instanceof Error - ? error.message - : "Failed to start filesystem watcher", - }); - ws.close(1011, "Failed to start filesystem watcher"); - return; - } - - void (async () => { - try { - while (!disposed && iterator) { - const next = await iterator.next(); - if (disposed || next.done) { - return; - } - - sendMessage(ws, { - type: "events", - events: next.value.events, - }); - } - } catch (error) { - console.error( - "[host-service/workspace-filesystem-events] Stream failed:", - { - workspaceId, - error, - }, - ); - - sendMessage(ws, { - type: "error", - message: - error instanceof Error - ? error.message - : "Filesystem event stream failed", - }); - ws.close(1011, "Filesystem event stream failed"); - } - })(); - }, - onClose: () => { - disposeIterator(); - }, - onError: () => { - disposeIterator(); - }, - }; - }), - ); -} diff --git a/packages/host-service/src/filesystem/index.ts b/packages/host-service/src/filesystem/index.ts deleted file mode 100644 index f9ed02cc0d5..00000000000 --- a/packages/host-service/src/filesystem/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - buildWorkspaceFilesystemEventsPath, - registerWorkspaceFilesystemEventsRoute, - type WorkspaceFilesystemServerMessage, -} from "./events"; diff --git a/packages/host-service/src/index.ts b/packages/host-service/src/index.ts index 8414141622b..e90980eb042 100644 --- a/packages/host-service/src/index.ts +++ b/packages/host-service/src/index.ts @@ -1,10 +1,10 @@ export { createApiClient } from "./api"; export { type CreateAppOptions, createApp } from "./app"; export type { HostDb } from "./db"; -export { - buildWorkspaceFilesystemEventsPath, - type WorkspaceFilesystemServerMessage, -} from "./filesystem"; +export type { + ClientMessage as EventBusClientMessage, + ServerMessage as EventBusServerMessage, +} from "./events"; export type { ApiAuthProvider } from "./providers/auth"; export { DeviceKeyApiAuthProvider, JwtApiAuthProvider } from "./providers/auth"; export { diff --git a/packages/host-service/src/runtime/git/git.ts b/packages/host-service/src/runtime/git/git.ts index 466a7cae20c..40009cab3cd 100644 --- a/packages/host-service/src/runtime/git/git.ts +++ b/packages/host-service/src/runtime/git/git.ts @@ -13,6 +13,7 @@ export function createGitFactory(provider: GitCredentialProvider): GitFactory { return git.env({ ...initialCredentials.env, ...credentials.env, + GIT_OPTIONAL_LOCKS: "0", }); }; } diff --git a/packages/host-service/src/runtime/pull-requests/pull-requests.ts b/packages/host-service/src/runtime/pull-requests/pull-requests.ts index 55e4a537ba5..a64d9f55871 100644 --- a/packages/host-service/src/runtime/pull-requests/pull-requests.ts +++ b/packages/host-service/src/runtime/pull-requests/pull-requests.ts @@ -22,7 +22,7 @@ import { } from "./utils/pull-request-mappers"; const BRANCH_SYNC_INTERVAL_MS = 30_000; -const PROJECT_REFRESH_INTERVAL_MS = 30_000; +const PROJECT_REFRESH_INTERVAL_MS = 10_000; const UNBORN_HEAD_ERROR_PATTERNS = [ "ambiguous argument 'head'", "unknown revision or path not in the working tree", diff --git a/packages/host-service/src/trpc/router/workspace/workspace.ts b/packages/host-service/src/trpc/router/workspace/workspace.ts index 37aa1da2010..b2de548d5dc 100644 --- a/packages/host-service/src/trpc/router/workspace/workspace.ts +++ b/packages/host-service/src/trpc/router/workspace/workspace.ts @@ -93,8 +93,8 @@ export const workspaceRouter = router({ await git.raw(["worktree", "add", "-b", input.branch, worktreePath]); } - const device = await ctx.api.device.ensureV2Host.mutate({ - clientId: ctx.deviceClientId, + const host = await ctx.api.device.ensureV2Host.mutate({ + machineId: ctx.deviceClientId, name: ctx.deviceName, }); @@ -103,7 +103,7 @@ export const workspaceRouter = router({ projectId: input.projectId, name: input.name, branch: input.branch, - deviceId: device.id, + hostId: host.id, }) .catch(async (err) => { try { diff --git a/packages/trpc/src/router/device/device.ts b/packages/trpc/src/router/device/device.ts index 6ae9b2b01ac..17a5da464ab 100644 --- a/packages/trpc/src/router/device/device.ts +++ b/packages/trpc/src/router/device/device.ts @@ -2,9 +2,10 @@ import { db, dbWs } from "@superset/db/client"; import { devicePresence, deviceTypeValues, - v2DevicePresence, - v2Devices, - v2UsersDevices, + v2Clients, + v2ClientTypeValues, + v2Hosts, + v2UsersHosts, } from "@superset/db/schema"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; import { z } from "zod"; @@ -14,7 +15,7 @@ export const deviceRouter = { ensureV2Host: protectedProcedure .input( z.object({ - clientId: z.string().min(1), + machineId: z.string().min(1), name: z.string().min(1), }), ) @@ -30,59 +31,96 @@ export const deviceRouter = { const userId = ctx.session.user.id; const now = new Date(); - const [device] = await dbWs - .insert(v2Devices) + const [host] = await dbWs + .insert(v2Hosts) .values({ organizationId, - clientId: input.clientId, + machineId: input.machineId, name: input.name, - type: "host", + lastSeenAt: now, createdByUserId: userId, }) .onConflictDoUpdate({ - target: [v2Devices.organizationId, v2Devices.clientId], + target: [v2Hosts.organizationId, v2Hosts.machineId], set: { name: input.name, - type: "host", + lastSeenAt: now, }, }) .returning(); - if (!device) { + if (!host) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: "Failed to ensure device", + message: "Failed to ensure host", }); } await dbWs - .insert(v2UsersDevices) + .insert(v2UsersHosts) .values({ organizationId, userId, - deviceId: device.id, + hostId: host.id, role: "owner", }) .onConflictDoNothing({ - target: [v2UsersDevices.userId, v2UsersDevices.deviceId], + target: [ + v2UsersHosts.organizationId, + v2UsersHosts.userId, + v2UsersHosts.hostId, + ], }); - await dbWs - .insert(v2DevicePresence) + return host; + }), + + ensureV2Client: protectedProcedure + .input( + z.object({ + machineId: z.string().min(1), + type: z.enum(v2ClientTypeValues), + }), + ) + .mutation(async ({ ctx, input }) => { + const organizationId = ctx.session.session.activeOrganizationId; + if (!organizationId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "No active organization selected", + }); + } + + const userId = ctx.session.user.id; + + const [client] = await dbWs + .insert(v2Clients) .values({ - deviceId: device.id, organizationId, - lastSeenAt: now, + userId, + machineId: input.machineId, + type: input.type, }) .onConflictDoUpdate({ - target: [v2DevicePresence.deviceId], + target: [ + v2Clients.organizationId, + v2Clients.userId, + v2Clients.machineId, + ], set: { - organizationId, - lastSeenAt: now, + type: input.type, }, + }) + .returning(); + + if (!client) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to ensure client", }); + } - return device; + return client; }), /** diff --git a/packages/trpc/src/router/v2-workspace/v2-workspace.ts b/packages/trpc/src/router/v2-workspace/v2-workspace.ts index 00150bfaa3e..251e94e6052 100644 --- a/packages/trpc/src/router/v2-workspace/v2-workspace.ts +++ b/packages/trpc/src/router/v2-workspace/v2-workspace.ts @@ -1,5 +1,5 @@ import { dbWs } from "@superset/db/client"; -import { v2Devices, v2Projects, v2Workspaces } from "@superset/db/schema"; +import { v2Hosts, v2Projects, v2Workspaces } from "@superset/db/schema"; import type { TRPCRouterRecord } from "@trpc/server"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; @@ -32,19 +32,19 @@ async function getScopedProject(organizationId: string, projectId: string) { ); } -async function getScopedDevice(organizationId: string, deviceId: string) { +async function getScopedHost(organizationId: string, hostId: string) { return requireOrgScopedResource( () => - dbWs.query.v2Devices.findFirst({ + dbWs.query.v2Hosts.findFirst({ columns: { id: true, organizationId: true, }, - where: eq(v2Devices.id, deviceId), + where: eq(v2Hosts.id, hostId), }), { code: "BAD_REQUEST", - message: "Device not found in this organization", + message: "Host not found in this organization", organizationId, }, ); @@ -100,7 +100,7 @@ export const v2WorkspaceRouter = { projectId: z.string().uuid(), name: z.string().min(1), branch: z.string().min(1), - deviceId: z.string().uuid(), + hostId: z.string().uuid(), }), ) .mutation(async ({ ctx, input }) => { @@ -110,7 +110,7 @@ export const v2WorkspaceRouter = { ); const project = await getScopedProject(organizationId, input.projectId); - const device = await getScopedDevice(organizationId, input.deviceId); + const host = await getScopedHost(organizationId, input.hostId); const [workspace] = await dbWs .insert(v2Workspaces) @@ -119,7 +119,7 @@ export const v2WorkspaceRouter = { projectId: project.id, name: input.name, branch: input.branch, - deviceId: device.id, + hostId: host.id, createdByUserId: ctx.session.user.id, }) .returning(); @@ -132,7 +132,7 @@ export const v2WorkspaceRouter = { id: z.string().uuid(), name: z.string().min(1).optional(), branch: z.string().min(1).optional(), - deviceId: z.string().uuid().optional(), + hostId: z.string().uuid().optional(), }), ) .mutation(async ({ ctx, input }) => { @@ -148,13 +148,13 @@ export const v2WorkspaceRouter = { }, ); - if (input.deviceId !== undefined) { - await getScopedDevice(workspace.organizationId, input.deviceId); + if (input.hostId !== undefined) { + await getScopedHost(workspace.organizationId, input.hostId); } const data = { branch: input.branch, - deviceId: input.deviceId, + hostId: input.hostId, name: input.name, }; if ( diff --git a/packages/workspace-client/src/hooks/useEventBus/index.ts b/packages/workspace-client/src/hooks/useEventBus/index.ts new file mode 100644 index 00000000000..c928ae72ecf --- /dev/null +++ b/packages/workspace-client/src/hooks/useEventBus/index.ts @@ -0,0 +1 @@ +export { useEventBus } from "./useEventBus"; diff --git a/packages/workspace-client/src/hooks/useEventBus/useEventBus.ts b/packages/workspace-client/src/hooks/useEventBus/useEventBus.ts new file mode 100644 index 00000000000..360927bf20b --- /dev/null +++ b/packages/workspace-client/src/hooks/useEventBus/useEventBus.ts @@ -0,0 +1,12 @@ +import { useMemo } from "react"; +import { type EventBusHandle, getEventBus } from "../../lib/eventBus"; +import { useWorkspaceClient } from "../../providers/WorkspaceClientProvider"; + +/** + * Returns an EventBusHandle for the current host. + * One WS connection is shared across all components using the same host. + */ +export function useEventBus(): EventBusHandle { + const { hostUrl, getWsToken } = useWorkspaceClient(); + return useMemo(() => getEventBus(hostUrl, getWsToken), [hostUrl, getWsToken]); +} diff --git a/packages/workspace-client/src/hooks/useGitChangeEvents/index.ts b/packages/workspace-client/src/hooks/useGitChangeEvents/index.ts new file mode 100644 index 00000000000..88ae03ff035 --- /dev/null +++ b/packages/workspace-client/src/hooks/useGitChangeEvents/index.ts @@ -0,0 +1 @@ +export { useGitChangeEvents } from "./useGitChangeEvents"; diff --git a/packages/workspace-client/src/hooks/useGitChangeEvents/useGitChangeEvents.ts b/packages/workspace-client/src/hooks/useGitChangeEvents/useGitChangeEvents.ts new file mode 100644 index 00000000000..015a894d40c --- /dev/null +++ b/packages/workspace-client/src/hooks/useGitChangeEvents/useGitChangeEvents.ts @@ -0,0 +1,25 @@ +import { useEffect, useEffectEvent } from "react"; +import { getEventBus } from "../../lib/eventBus"; +import { useWorkspaceClient } from "../../providers/WorkspaceClientProvider"; + +/** + * Subscribe to `git:changed` events for a specific workspace (or all workspaces with "*"). + * Calls `onChanged` with the workspace ID whenever git state changes. + */ +export function useGitChangeEvents( + workspaceId: string | "*", + onChanged: (workspaceId: string) => void, + enabled = true, +): void { + const { hostUrl, getWsToken } = useWorkspaceClient(); + const handler = useEffectEvent(onChanged); + + useEffect(() => { + if (!enabled) return; + + const bus = getEventBus(hostUrl, getWsToken); + return bus.on("git:changed", workspaceId, (id) => { + handler(id); + }); + }, [hostUrl, getWsToken, workspaceId, enabled]); +} diff --git a/packages/workspace-client/src/hooks/useWorkspaceFsEventBridge/index.ts b/packages/workspace-client/src/hooks/useWorkspaceFsEventBridge/index.ts deleted file mode 100644 index 790ffd1a9c5..00000000000 --- a/packages/workspace-client/src/hooks/useWorkspaceFsEventBridge/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useWorkspaceFsEventBridge } from "./useWorkspaceFsEventBridge"; diff --git a/packages/workspace-client/src/hooks/useWorkspaceFsEventBridge/useWorkspaceFsEventBridge.ts b/packages/workspace-client/src/hooks/useWorkspaceFsEventBridge/useWorkspaceFsEventBridge.ts deleted file mode 100644 index a7133339d23..00000000000 --- a/packages/workspace-client/src/hooks/useWorkspaceFsEventBridge/useWorkspaceFsEventBridge.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect } from "react"; -import { retainWorkspaceFsBridge } from "../../lib/workspaceFsEventRegistry"; -import { useWorkspaceClient } from "../../providers/WorkspaceClientProvider"; - -export function useWorkspaceFsEventBridge( - workspaceId: string, - enabled = true, -): void { - const client = useWorkspaceClient(); - - useEffect(() => { - if (!enabled || !workspaceId) { - return; - } - - return retainWorkspaceFsBridge(client, workspaceId); - }, [client, enabled, workspaceId]); -} diff --git a/packages/workspace-client/src/hooks/useWorkspaceFsEvents/index.ts b/packages/workspace-client/src/hooks/useWorkspaceFsEvents/index.ts deleted file mode 100644 index 516d396e403..00000000000 --- a/packages/workspace-client/src/hooks/useWorkspaceFsEvents/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useWorkspaceFsEvents } from "./useWorkspaceFsEvents"; diff --git a/packages/workspace-client/src/hooks/useWorkspaceFsEvents/useWorkspaceFsEvents.ts b/packages/workspace-client/src/hooks/useWorkspaceFsEvents/useWorkspaceFsEvents.ts deleted file mode 100644 index 90b195b4284..00000000000 --- a/packages/workspace-client/src/hooks/useWorkspaceFsEvents/useWorkspaceFsEvents.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect, useEffectEvent } from "react"; -import type { WorkspaceFsEventListener } from "../../lib/workspaceFsEventRegistry"; -import { subscribeToWorkspaceFsEvents } from "../../lib/workspaceFsEventRegistry"; -import { useWorkspaceClient } from "../../providers/WorkspaceClientProvider"; - -export function useWorkspaceFsEvents( - workspaceId: string, - listener: WorkspaceFsEventListener, - enabled = true, -): void { - const client = useWorkspaceClient(); - const onEvent = useEffectEvent(listener); - - useEffect(() => { - if (!enabled || !workspaceId) { - return; - } - - return subscribeToWorkspaceFsEvents(client, workspaceId, (event) => { - onEvent(event); - }); - }, [client, enabled, workspaceId]); -} diff --git a/packages/workspace-client/src/index.ts b/packages/workspace-client/src/index.ts index dc68deaf589..7ba699a2e34 100644 --- a/packages/workspace-client/src/index.ts +++ b/packages/workspace-client/src/index.ts @@ -1,22 +1,11 @@ -export { - type UseFileDocumentParams, - type UseFileDocumentResult, - useFileDocument, -} from "./hooks/useFileDocument"; -export { - type FileTreeNode, - type UseFileTreeParams, - type UseFileTreeResult, - useFileTree, -} from "./hooks/useFileTree"; -export { useWorkspaceFsEventBridge } from "./hooks/useWorkspaceFsEventBridge"; -export { useWorkspaceFsEvents } from "./hooks/useWorkspaceFsEvents"; +export { useEventBus } from "./hooks/useEventBus"; +export { useGitChangeEvents } from "./hooks/useGitChangeEvents"; +export { type EventBusHandle, getEventBus } from "./lib/eventBus"; export { useWorkspaceClient, useWorkspaceHostUrl, useWorkspaceWsUrl, type WorkspaceClientContextValue, WorkspaceClientProvider, - type WorkspaceFsSubscriptionInput, } from "./providers/WorkspaceClientProvider"; export { workspaceTrpc } from "./workspace-trpc"; diff --git a/packages/workspace-client/src/lib/eventBus.ts b/packages/workspace-client/src/lib/eventBus.ts new file mode 100644 index 00000000000..69ec65ffab3 --- /dev/null +++ b/packages/workspace-client/src/lib/eventBus.ts @@ -0,0 +1,265 @@ +import type { + ClientMessage, + ServerMessage, +} from "@superset/host-service/events"; +import type { FsWatchEvent } from "@superset/workspace-fs/host"; + +type EventType = "fs:events" | "git:changed"; + +interface FsEventsPayload { + events: FsWatchEvent[]; +} + +type EventListener = T extends "fs:events" + ? (workspaceId: string, payload: FsEventsPayload) => void + : T extends "git:changed" + ? (workspaceId: string) => void + : never; + +interface ListenerEntry { + type: EventType; + workspaceId: string | "*"; + callback: (...args: unknown[]) => void; +} + +const RECONNECT_BASE_MS = 1_000; +const RECONNECT_MAX_MS = 30_000; + +interface ConnectionState { + socket: WebSocket | null; + refCount: number; + listeners: Set; + fsWatchedWorkspaces: Map; + reconnectAttempts: number; + reconnectTimer: ReturnType | null; + disposed: boolean; +} + +const connections = new Map(); + +function buildEventBusUrl(hostUrl: string, wsToken: string | null): string { + const url = new URL("/events", hostUrl); + url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; + if (wsToken) { + url.searchParams.set("token", wsToken); + } + return url.toString(); +} + +function sendCommand(state: ConnectionState, message: ClientMessage): void { + if (state.socket?.readyState === WebSocket.OPEN) { + state.socket.send(JSON.stringify(message)); + } +} + +function handleMessage(state: ConnectionState, data: unknown): void { + let message: ServerMessage; + try { + message = JSON.parse(String(data)) as ServerMessage; + } catch { + return; + } + + if (message.type === "error") { + console.error("[event-bus-client]", message.message); + return; + } + + for (const entry of state.listeners) { + if (entry.type !== message.type) continue; + + const workspaceId = + message.type === "fs:events" || message.type === "git:changed" + ? message.workspaceId + : null; + + if ( + workspaceId && + entry.workspaceId !== "*" && + entry.workspaceId !== workspaceId + ) { + continue; + } + + if (message.type === "fs:events") { + (entry.callback as EventListener<"fs:events">)(message.workspaceId, { + events: message.events, + }); + } else if (message.type === "git:changed") { + (entry.callback as EventListener<"git:changed">)(message.workspaceId); + } + } +} + +function connect( + state: ConnectionState, + hostUrl: string, + getWsToken: () => string | null, +): void { + if (state.disposed) return; + + const wsUrl = buildEventBusUrl(hostUrl, getWsToken()); + const socket = new WebSocket(wsUrl); + state.socket = socket; + + socket.onopen = () => { + state.reconnectAttempts = 0; + + // Re-send all active fs:watch commands + for (const workspaceId of state.fsWatchedWorkspaces.keys()) { + sendCommand(state, { type: "fs:watch", workspaceId }); + } + }; + + socket.onmessage = (event) => { + handleMessage(state, event.data); + }; + + socket.onclose = () => { + if (state.disposed) return; + state.socket = null; + scheduleReconnect(state, hostUrl, getWsToken); + }; + + socket.onerror = () => { + // onclose will fire after onerror + }; +} + +function scheduleReconnect( + state: ConnectionState, + hostUrl: string, + getWsToken: () => string | null, +): void { + if (state.disposed || state.reconnectTimer) return; + + const delay = Math.min( + RECONNECT_BASE_MS * 2 ** state.reconnectAttempts, + RECONNECT_MAX_MS, + ); + state.reconnectAttempts++; + + state.reconnectTimer = setTimeout(() => { + state.reconnectTimer = null; + if (!state.disposed) { + connect(state, hostUrl, getWsToken); + } + }, delay); +} + +function getOrCreateConnection( + hostUrl: string, + getWsToken: () => string | null, +): ConnectionState { + const key = hostUrl; + const existing = connections.get(key); + if (existing) return existing; + + const state: ConnectionState = { + socket: null, + refCount: 0, + listeners: new Set(), + fsWatchedWorkspaces: new Map(), + reconnectAttempts: 0, + reconnectTimer: null, + disposed: false, + }; + connections.set(key, state); + connect(state, hostUrl, getWsToken); + return state; +} + +function maybeCleanupConnection(hostUrl: string): void { + const key = hostUrl; + const state = connections.get(key); + if (!state) return; + + if (state.refCount > 0 || state.listeners.size > 0) return; + + state.disposed = true; + if (state.reconnectTimer) { + clearTimeout(state.reconnectTimer); + state.reconnectTimer = null; + } + if ( + state.socket?.readyState === WebSocket.CONNECTING || + state.socket?.readyState === WebSocket.OPEN + ) { + state.socket.close(1000, "No more subscribers"); + } + connections.delete(key); +} + +// ── Public API ───────────────────────────────────────────────────── + +export interface EventBusHandle { + on( + type: T, + workspaceId: string | "*", + listener: EventListener, + ): () => void; + watchFs(workspaceId: string): void; + unwatchFs(workspaceId: string): void; + retain(): () => void; +} + +/** + * Get a handle to the event bus for a given host. + * One WS connection is shared across all handles for the same hostUrl. + */ +export function getEventBus( + hostUrl: string, + getWsToken: () => string | null, +): EventBusHandle { + const state = getOrCreateConnection(hostUrl, getWsToken); + + return { + on( + type: T, + workspaceId: string | "*", + listener: EventListener, + ): () => void { + const entry: ListenerEntry = { + type, + workspaceId, + callback: listener as (...args: unknown[]) => void, + }; + state.listeners.add(entry); + + return () => { + state.listeners.delete(entry); + maybeCleanupConnection(hostUrl); + }; + }, + + watchFs(workspaceId: string): void { + const count = state.fsWatchedWorkspaces.get(workspaceId) ?? 0; + state.fsWatchedWorkspaces.set(workspaceId, count + 1); + if (count === 0) { + sendCommand(state, { type: "fs:watch", workspaceId }); + } + }, + + unwatchFs(workspaceId: string): void { + const count = state.fsWatchedWorkspaces.get(workspaceId) ?? 0; + if (count <= 1) { + state.fsWatchedWorkspaces.delete(workspaceId); + sendCommand(state, { type: "fs:unwatch", workspaceId }); + } else { + state.fsWatchedWorkspaces.set(workspaceId, count - 1); + } + }, + + /** + * Increment ref count to keep the connection alive even without listeners. + * Returns a release function. + */ + retain(): () => void { + state.refCount++; + return () => { + state.refCount = Math.max(0, state.refCount - 1); + maybeCleanupConnection(hostUrl); + }; + }, + }; +} diff --git a/packages/workspace-client/src/lib/workspaceFsEventRegistry.ts b/packages/workspace-client/src/lib/workspaceFsEventRegistry.ts deleted file mode 100644 index 2ba2a620d71..00000000000 --- a/packages/workspace-client/src/lib/workspaceFsEventRegistry.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { FsWatchEvent } from "@superset/workspace-fs/host"; -import type { - WorkspaceClientContextValue, - WorkspaceFsSubscriptionInput, -} from "../providers/WorkspaceClientProvider"; - -export type WorkspaceFsEventListener = (event: FsWatchEvent) => void; - -interface WorkspaceFsSubscriptionState { - bridgeCount: number; - client: WorkspaceClientContextValue; - listeners: Set; - unsubscribeTransport: (() => void) | null; - workspaceId: string; -} - -const subscriptions = new Map(); - -function getSubscriptionKey( - client: WorkspaceClientContextValue, - workspaceId: string, -): string { - return `${client.hostUrl}:${workspaceId}`; -} - -function getOrCreateSubscription( - client: WorkspaceClientContextValue, - workspaceId: string, -): WorkspaceFsSubscriptionState { - const key = getSubscriptionKey(client, workspaceId); - const existing = subscriptions.get(key); - if (existing) { - return existing; - } - - const nextState: WorkspaceFsSubscriptionState = { - bridgeCount: 0, - client, - listeners: new Set(), - unsubscribeTransport: null, - workspaceId, - }; - subscriptions.set(key, nextState); - return nextState; -} - -function removeSubscriptionIfInactive( - state: WorkspaceFsSubscriptionState, -): void { - if (state.bridgeCount > 0 || state.listeners.size > 0) { - return; - } - - state.unsubscribeTransport?.(); - state.unsubscribeTransport = null; - subscriptions.delete(getSubscriptionKey(state.client, state.workspaceId)); -} - -function ensureTransport(state: WorkspaceFsSubscriptionState): void { - if (state.unsubscribeTransport) { - return; - } - - if (state.bridgeCount === 0 && state.listeners.size === 0) { - return; - } - - const input: WorkspaceFsSubscriptionInput = { - workspaceId: state.workspaceId, - onEvent: (event) => { - for (const listener of state.listeners) { - listener(event); - } - }, - onError: (error) => { - console.error("[workspace-client/fs-events] Stream failed:", { - hostUrl: state.client.hostUrl, - workspaceId: state.workspaceId, - error, - }); - }, - }; - - state.unsubscribeTransport = state.client.subscribeToWorkspaceFsEvents(input); -} - -export function retainWorkspaceFsBridge( - client: WorkspaceClientContextValue, - workspaceId: string, -): () => void { - const state = getOrCreateSubscription(client, workspaceId); - state.bridgeCount += 1; - ensureTransport(state); - - return () => { - state.bridgeCount = Math.max(0, state.bridgeCount - 1); - removeSubscriptionIfInactive(state); - }; -} - -export function subscribeToWorkspaceFsEvents( - client: WorkspaceClientContextValue, - workspaceId: string, - listener: WorkspaceFsEventListener, -): () => void { - const state = getOrCreateSubscription(client, workspaceId); - state.listeners.add(listener); - ensureTransport(state); - - return () => { - state.listeners.delete(listener); - removeSubscriptionIfInactive(state); - }; -} diff --git a/packages/workspace-client/src/providers/WorkspaceClientProvider/WorkspaceClientProvider.tsx b/packages/workspace-client/src/providers/WorkspaceClientProvider/WorkspaceClientProvider.tsx index 2bf551423fa..f87c1467c44 100644 --- a/packages/workspace-client/src/providers/WorkspaceClientProvider/WorkspaceClientProvider.tsx +++ b/packages/workspace-client/src/providers/WorkspaceClientProvider/WorkspaceClientProvider.tsx @@ -1,6 +1,3 @@ -import type { WorkspaceFilesystemServerMessage } from "@superset/host-service/filesystem"; -import { buildWorkspaceFilesystemEventsPath } from "@superset/host-service/filesystem"; -import type { FsWatchEvent } from "@superset/workspace-fs/host"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { httpBatchLink } from "@trpc/client"; import { createContext, type ReactNode, useContext } from "react"; @@ -10,18 +7,9 @@ import { workspaceTrpc } from "../../workspace-trpc"; const STALE_TIME_MS = 5_000; const GC_TIME_MS = 30 * 60 * 1_000; -export interface WorkspaceFsSubscriptionInput { - workspaceId: string; - onEvent: (event: FsWatchEvent) => void; - onError?: (error: unknown) => void; -} - export interface WorkspaceClientContextValue { hostUrl: string; queryClient: QueryClient; - subscribeToWorkspaceFsEvents: ( - input: WorkspaceFsSubscriptionInput, - ) => () => void; getWsToken: () => string | null; } @@ -37,9 +25,6 @@ interface WorkspaceClients { hostUrl: string; queryClient: QueryClient; trpcClient: ReturnType; - subscribeToWorkspaceFsEvents: ( - input: WorkspaceFsSubscriptionInput, - ) => () => void; getWsToken: () => string | null; } @@ -47,94 +32,6 @@ const workspaceClientsCache = new Map(); const WorkspaceClientContext = createContext(null); -function toWorkspaceFilesystemEventsUrl( - hostUrl: string, - workspaceId: string, - getWsToken?: () => string | null, -): string { - const url = new URL(buildWorkspaceFilesystemEventsPath(workspaceId), hostUrl); - url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; - const token = getWsToken?.(); - if (token) { - url.searchParams.set("token", token); - } - return url.toString(); -} - -function toSubscriptionError(message: string, event?: CloseEvent): Error { - return new Error(event ? `${message} (code ${event.code})` : message); -} - -function createWorkspaceFsSubscription( - hostUrl: string, - input: WorkspaceFsSubscriptionInput, - getWsToken?: () => string | null, -): () => void { - const socket = new WebSocket( - toWorkspaceFilesystemEventsUrl(hostUrl, input.workspaceId, getWsToken), - ); - let disposed = false; - let opened = false; - - socket.onopen = () => { - opened = true; - }; - - socket.onmessage = (messageEvent) => { - let message: WorkspaceFilesystemServerMessage; - try { - message = JSON.parse( - String(messageEvent.data), - ) as WorkspaceFilesystemServerMessage; - } catch (error) { - input.onError?.(error); - return; - } - - if (message.type === "error") { - input.onError?.(new Error(message.message)); - return; - } - - for (const event of message.events) { - input.onEvent(event); - } - }; - - socket.onerror = () => { - input.onError?.( - toSubscriptionError( - "Workspace filesystem event stream encountered an error", - ), - ); - }; - - socket.onclose = (event) => { - if (disposed) { - return; - } - - if (!opened || !event.wasClean) { - input.onError?.( - toSubscriptionError( - "Workspace filesystem event stream closed unexpectedly", - event, - ), - ); - } - }; - - return () => { - disposed = true; - if ( - socket.readyState === WebSocket.CONNECTING || - socket.readyState === WebSocket.OPEN - ) { - socket.close(1000, "Client unsubscribed"); - } - }; -} - function getWorkspaceClients( cacheKey: string, hostUrl: string, @@ -174,9 +71,6 @@ function getWorkspaceClients( queryClient, trpcClient, getWsToken, - subscribeToWorkspaceFsEvents(input) { - return createWorkspaceFsSubscription(hostUrl, input, getWsToken); - }, }; workspaceClientsCache.set(clientKey, clients); return clients; @@ -193,7 +87,6 @@ export function WorkspaceClientProvider({ const contextValue: WorkspaceClientContextValue = { hostUrl: clients.hostUrl, queryClient: clients.queryClient, - subscribeToWorkspaceFsEvents: clients.subscribeToWorkspaceFsEvents, getWsToken: clients.getWsToken, }; diff --git a/packages/workspace-client/src/providers/WorkspaceClientProvider/index.ts b/packages/workspace-client/src/providers/WorkspaceClientProvider/index.ts index 71e8bd48df8..acb561c2dc3 100644 --- a/packages/workspace-client/src/providers/WorkspaceClientProvider/index.ts +++ b/packages/workspace-client/src/providers/WorkspaceClientProvider/index.ts @@ -4,5 +4,4 @@ export { useWorkspaceWsUrl, type WorkspaceClientContextValue, WorkspaceClientProvider, - type WorkspaceFsSubscriptionInput, } from "./WorkspaceClientProvider"; diff --git a/packages/workspace-fs/src/client/index.ts b/packages/workspace-fs/src/client/index.ts index 4305f130647..1afc1870bfc 100644 --- a/packages/workspace-fs/src/client/index.ts +++ b/packages/workspace-fs/src/client/index.ts @@ -3,6 +3,7 @@ export type { FsService, FsSubscriptionMap, } from "../core/service"; +export type { FsEntry, FsEntryKind, FsWatchEvent } from "../types"; import type { FsRequestMap,