diff --git a/apps/desktop/plans/20260421-v2-main-workspace-creation.md b/apps/desktop/plans/20260421-v2-main-workspace-creation.md new file mode 100644 index 00000000000..ef21df5a599 --- /dev/null +++ b/apps/desktop/plans/20260421-v2-main-workspace-creation.md @@ -0,0 +1,123 @@ +# V2 Main Workspace Creation + +## Problem + +V1 auto-creates a singleton `type='branch'` workspace per project via +`ensureMainWorkspace` (`apps/desktop/src/lib/trpc/routers/projects/projects.ts:174`), +called inline from five mutations. V2 has no equivalent — `v2_workspaces` rows only +come from `workspaceCreation`, which always produces worktrees. Users finish +`project.setup` and see an empty sidebar. + +## Goals + +- Each `(projectId, hostId)` gets one "main" workspace whose path == the host's + `repoPath`. Multiple mains per project (one per host) are allowed. +- Created automatically on project create/setup success — no type picker, no UI + step. +- Main workspaces are real v2 workspace rows but are not deletable through normal + workspace delete flows. Users can remove them from the sidebar; host/project + removal owns deleting the cloud row. + +## Design + +### Schema + +Add to `v2_workspaces` (`packages/db/src/schema/schema.ts:524`): + +```ts +type: v2WorkspaceType().notNull().default("worktree"), +``` + +Backed by a `pgEnum("v2_workspace_type", ["main", "worktree"])` for DB-level +enforcement, matching the `v2ClientType` / `v2UsersHostRole` precedent. +Partial unique index: `(projectId, hostId) WHERE type = 'main'`. + +Column name `type` over `isMain: boolean` so the workspace-creation modal's +contract is explicit — it only ever writes `'worktree'`. + +No `pinnedAt`/pinning column. Sidebar membership remains renderer-local in +`v2WorkspaceLocalState`, and main workspace auto-visibility is derived from +`type='main'`, current host, and project sidebar membership. + +### `ensureMainWorkspace` helper (host-service) + +New helper in `packages/host-service/src/trpc/router/project/`. Given +`(projectId, repoPath)`: + +1. `ensureV2Host` (reuse call from `workspace-creation.ts:372`). +2. Resolve current branch: `git symbolic-ref --short HEAD` at `repoPath`. +3. `ctx.api.v2Workspace.create.mutate({ ..., type: "main", branch, name: branch })`. + Skip if the unique index rejects — idempotent. +4. Insert local `workspaces` row (`packages/host-service/src/db/schema.ts:95`) + with `worktreePath = repoPath`. The column is named `worktreePath` but holds + any absolute checkout path; for main that's the repo root. + +Log-and-continue on failure: any cloud/local error is caught, logged, and +swallowed (the helper returns `null`). `project.setup` doesn't regress when a +transient cloud blip hits — the startup sweep backfills on the next boot. +Idempotency via the partial unique index handles duplicates on retry. + +If a main row already exists and the repo root branch changed, cloud branch +metadata is refreshed. The display name only follows the branch while it still +equals the previous branch name; user-renamed main workspaces keep their label. + +### Call sites + +1. **`project.create` success** — after cloning/importing a new cloud project and + persisting the local project row. +2. **`project.setup` success** (`packages/host-service/src/trpc/router/project/project.ts:134`) — + after `persistLocalProject` in both `clone` and `import` branches. +3. **Workspace creation/adoption/checkout preflight** — before creating or + adopting a worktree, call the helper for the local project. This keeps normal + workspace flows self-healing if setup returned before main creation completed. +4. **Host-service startup sweep** — on boot, iterate local `projects` rows and + call the helper for each. Idempotent via the unique index, so it's safe on + every boot; in practice only does work once per pre-existing project. This is + the recovery path for projects already set up before this change ships. + +### Sidebar and modal + +Workspace-creation modal continues to write worktree workspaces only. Project +setup/create responses include `mainWorkspaceId` when the helper succeeds, so the +renderer can add the main workspace to the sidebar immediately. + +Main workspaces are auto-visible when: + +- the workspace is `type='main'` +- the host is the current machine +- the project is in the sidebar +- no renderer-local hidden tombstone exists for that workspace + +Removing a main workspace from the sidebar sets +`v2WorkspaceLocalState.sidebarState.isHidden = true` instead of deleting the local +state row. This prevents auto-visibility from immediately re-adding the row and +keeps hidden-row filtering centralized through the dashboard sidebar local-state +visibility helper. + +### Delete behavior + +`v2Workspace.delete` rejects `type='main'` rows. Regular workspace cleanup/delete +flows are worktree-only. Host project removal uses the explicit +`v2Workspace.deleteMainForHost` endpoint for the repo-root main row, then removes +the local project row. Cloud project deletion still cascades through the database +like any other project-level delete. + +## Migration + +`bunx drizzle-kit generate --name="v2_workspaces_main_type"`. No SQL backfill — +the cloud doesn't know `repoPath` or current branch. Existing setups are filled +in by the startup sweep the first time the updated host-service boots. + +## Rollout + +Cloud/API before desktop (per deploy ordering). Verify: + +- Fresh `project.setup` creates exactly one main row + local row. +- Re-running `project.setup` on the same host is idempotent. +- Two hosts on one project each get their own main row. +- Startup sweep fills in a main row for a project set up pre-update, without + duplicating on subsequent boots. +- `project.remove` cleans up the main row alongside worktrees. +- Normal workspace delete and cleanup flows reject main workspaces. +- Removing a main workspace from the sidebar hides it and does not re-add it on + the next sidebar recompute. diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/NewProjectModal/NewProjectModal.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/NewProjectModal/NewProjectModal.tsx index 4cf0ced1348..feff913ccbd 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/NewProjectModal/NewProjectModal.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/components/NewProjectModal/NewProjectModal.tsx @@ -27,7 +27,11 @@ type NewProjectMode = "clone" | "empty" | "template"; interface NewProjectModalProps { open: boolean; onOpenChange: (open: boolean) => void; - onSuccess?: (result: { projectId: string; repoPath: string }) => void; + onSuccess?: (result: { + projectId: string; + repoPath: string; + mainWorkspaceId: string | null; + }) => void; onError?: (message: string) => void; } @@ -72,7 +76,8 @@ export function NewProjectModal({ onError, }: NewProjectModalProps) { const { activeHostUrl } = useLocalHostService(); - const { ensureProjectInSidebar } = useDashboardSidebarState(); + const { ensureProjectInSidebar, ensureWorkspaceInSidebar } = + useDashboardSidebarState(); const selectDirectory = electronTrpc.window.selectDirectory.useMutation(); const { data: homeDir } = electronTrpc.window.getHomeDir.useQuery(); @@ -142,7 +147,11 @@ export function NewProjectModal({ name, mode: { kind: "clone", parentDir: trimmedParent, url: trimmedUrl }, }); - ensureProjectInSidebar(result.projectId); + if (result.mainWorkspaceId) { + ensureWorkspaceInSidebar(result.mainWorkspaceId, result.projectId); + } else { + ensureProjectInSidebar(result.projectId); + } onSuccess?.(result); reset(); onOpenChange(false); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport/useFolderFirstImport.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport/useFolderFirstImport.ts index f07f12bd558..794df2538b8 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport/useFolderFirstImport.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/hooks/useFolderFirstImport/useFolderFirstImport.ts @@ -5,6 +5,12 @@ import { getBaseName } from "renderer/lib/pathBasename"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; +interface ProjectSetupResult { + projectId: string; + repoPath: string; + mainWorkspaceId: string | null; +} + export interface UseFolderFirstImportResult { start: () => Promise; } @@ -15,21 +21,26 @@ interface MatchingProject { } export function useFolderFirstImport(options?: { - onSuccess?: (result: { projectId: string; repoPath: string }) => void; + onSuccess?: (result: ProjectSetupResult) => void; onError?: (message: string) => void; onMultipleProjects?: (input: { candidates: MatchingProject[] }) => void; }): UseFolderFirstImportResult { const { activeHostUrl } = useLocalHostService(); - const { ensureProjectInSidebar } = useDashboardSidebarState(); + const { ensureProjectInSidebar, ensureWorkspaceInSidebar } = + useDashboardSidebarState(); const selectDirectory = electronTrpc.window.selectDirectory.useMutation(); const { onError, onMultipleProjects, onSuccess } = options ?? {}; const reportSuccess = useCallback( - (result: { projectId: string; repoPath: string }) => { - ensureProjectInSidebar(result.projectId); + (result: ProjectSetupResult) => { + if (result.mainWorkspaceId) { + ensureWorkspaceInSidebar(result.mainWorkspaceId, result.projectId); + } else { + ensureProjectInSidebar(result.projectId); + } onSuccess?.(result); }, - [ensureProjectInSidebar, onSuccess], + [ensureProjectInSidebar, ensureWorkspaceInSidebar, onSuccess], ); const reportError = useCallback( @@ -85,7 +96,11 @@ export function useFolderFirstImport(options?: { projectId: only.id, mode: { kind: "import", repoPath }, }); - reportSuccess({ projectId: only.id, repoPath: result.repoPath }); + reportSuccess({ + projectId: only.id, + repoPath: result.repoPath, + mainWorkspaceId: result.mainWorkspaceId, + }); } else { const result = await client.project.create.mutate({ name: getBaseName(repoPath), 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 88630271e33..0878c23cd53 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 @@ -35,6 +35,7 @@ export function DashboardSidebarWorkspaceItem({ branch, creationStatus, } = workspace; + const isMainWorkspace = workspace.type === "main"; const diffStats = useDiffStats(id); const workspaceStatus = useV2WorkspaceNotificationStatus(id); const { @@ -62,6 +63,7 @@ export function DashboardSidebarWorkspaceItem({ projectId, workspaceName: name, branch, + isMainWorkspace, }); const navigate = useNavigate(); @@ -132,7 +134,9 @@ export function DashboardSidebarWorkspaceItem({ onCopyBranchName={handleCopyBranchName} onRemoveFromSidebar={handleRemoveFromSidebar} onRename={startRename} - onDelete={() => setIsDeleteDialogOpen(true)} + onDelete={ + isMainWorkspace ? undefined : () => setIsDeleteDialogOpen(true) + } onToggleUnread={handleToggleUnread} > {content} @@ -140,7 +144,7 @@ export function DashboardSidebarWorkspaceItem({ )} - {!isPending && ( + {!isPending && !isMainWorkspace && ( setIsDeleteDialogOpen(true)} + onDeleteClick={ + isMainWorkspace + ? handleRemoveFromSidebar + : () => setIsDeleteDialogOpen(true) + } onRenameValueChange={setRenameValue} onSubmitRename={submitRename} onCancelRename={cancelRename} @@ -200,7 +208,9 @@ export function DashboardSidebarWorkspaceItem({ onCopyBranchName={handleCopyBranchName} onRemoveFromSidebar={handleRemoveFromSidebar} onRename={startRename} - onDelete={() => setIsDeleteDialogOpen(true)} + onDelete={ + isMainWorkspace ? undefined : () => setIsDeleteDialogOpen(true) + } onToggleUnread={handleToggleUnread} > {expandedContent} @@ -208,7 +218,7 @@ export function DashboardSidebarWorkspaceItem({ )} - {!isPending && ( + {!isPending && !isMainWorkspace && ( getCreationStatusText(creationStatus), [creationStatus], ); + const isMainWorkspace = workspace.type === "main"; + const workspaceKindTitle = isMainWorkspace + ? "Main workspace" + : "Worktree workspace"; + const workspaceKindDescription = isMainWorkspace + ? "Uses the repository checkout on this host" + : "Isolated copy for parallel development"; + const closeLabel = isMainWorkspace + ? "Remove from sidebar" + : "Close workspace"; return ( // biome-ignore lint/a11y/noStaticElementInteractions: Mirrors the legacy sidebar row UI, which includes nested action buttons. @@ -192,22 +202,26 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< ) : ( <>

- {hostType === "local-device" - ? "Local workspace" - : hostType === "remote-device" - ? hostIsOnline === false - ? "Remote workspace — device offline" - : "Remote workspace" - : "Cloud workspace"} + {isMainWorkspace + ? workspaceKindTitle + : hostType === "local-device" + ? "Local workspace" + : hostType === "remote-device" + ? hostIsOnline === false + ? "Remote workspace — device offline" + : "Remote workspace" + : "Cloud workspace"}

- {hostType === "local-device" - ? "Running on this device" - : hostType === "remote-device" - ? hostIsOnline === false - ? "The associated device isn't reachable right now" - : "Running on a paired device" - : "Hosted in the cloud"} + {isMainWorkspace + ? workspaceKindDescription + : hostType === "local-device" + ? "Running on this device" + : hostType === "remote-device" + ? hostIsOnline === false + ? "The associated device isn't reachable right now" + : "Running on a paired device" + : "Hosted in the cloud"}

)} @@ -273,15 +287,19 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< onDeleteClick(); }} className="flex items-center justify-center text-muted-foreground hover:text-foreground" - aria-label="Close workspace" + aria-label={closeLabel} > diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx index e1b4dcad459..8c74ff2ede4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx @@ -45,7 +45,7 @@ interface DashboardSidebarWorkspaceContextMenuProps { onCopyBranchName: () => void; onRemoveFromSidebar: () => void; onRename: () => void; - onDelete: () => void; + onDelete?: () => void; onToggleUnread: () => void; children: React.ReactNode; } @@ -168,13 +168,15 @@ export function DashboardSidebarWorkspaceContextMenu({ Remove from Sidebar - - - Delete - + {onDelete ? ( + + + Delete + + ) : null} ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index fb1d61cb578..90455b6a8a9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -19,6 +19,7 @@ interface UseDashboardSidebarWorkspaceItemActionsOptions { projectId: string; workspaceName: string; branch: string; + isMainWorkspace?: boolean; } export function useDashboardSidebarWorkspaceItemActions({ @@ -26,6 +27,7 @@ export function useDashboardSidebarWorkspaceItemActions({ projectId, workspaceName, branch, + isMainWorkspace = false, }: UseDashboardSidebarWorkspaceItemActionsOptions) { const navigate = useNavigate(); const matchRoute = useMatchRoute(); @@ -39,8 +41,12 @@ export function useDashboardSidebarWorkspaceItemActions({ ); const setManualUnread = useV2NotificationStore((s) => s.setManualUnread); const isUnread = useV2WorkspaceIsUnread(workspaceId); - const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = - useDashboardSidebarState(); + const { + createSection, + hideWorkspaceInSidebar, + moveWorkspaceToSection, + removeWorkspaceFromSidebar, + } = useDashboardSidebarState(); const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(workspaceName); @@ -84,6 +90,10 @@ export function useDashboardSidebarWorkspaceItemActions({ const handleRemoveFromSidebar = () => { navigateAway(workspaceId); + if (isMainWorkspace) { + hideWorkspaceInSidebar(workspaceId, projectId); + return; + } removeWorkspaceFromSidebar(workspaceId); }; 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 7146ad215ce..a6ad62f68ef 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 @@ -7,6 +7,7 @@ import { authClient } from "renderer/lib/auth-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { getVisibleSidebarWorkspaces } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import { MOCK_ORG_ID } from "shared/constants"; import type { @@ -19,6 +20,7 @@ import type { // Sits above every real workspace so the pending row lines up with the real one, // which is inserted via getPrependTabOrder. const PENDING_WORKSPACE_TAB_ORDER = Number.MIN_SAFE_INTEGER; +const MAIN_WORKSPACE_TAB_ORDER = Number.MIN_SAFE_INTEGER; type LocalPullRequest = DashboardSidebarWorkspace["pullRequest"]; type PullRequestWorkspaceRow = { @@ -221,7 +223,7 @@ export function useDashboardSidebarData() { [collections], ); - const { data: sidebarWorkspaces = [] } = useLiveQuery( + const { data: rawSidebarWorkspaces = [] } = useLiveQuery( (q) => q .from({ sidebarWorkspaces: collections.v2WorkspaceLocalState }) @@ -242,6 +244,7 @@ export function useDashboardSidebarData() { projectId: sidebarWorkspaces.sidebarState.projectId, hostId: workspaces.hostId, hostMachineId: hosts?.machineId ?? null, + type: workspaces.type, hostIsOnline: hosts?.isOnline ?? null, name: workspaces.name, branch: workspaces.branch, @@ -249,13 +252,70 @@ export function useDashboardSidebarData() { updatedAt: workspaces.updatedAt, tabOrder: sidebarWorkspaces.sidebarState.tabOrder, sectionId: sidebarWorkspaces.sidebarState.sectionId, + isHidden: sidebarWorkspaces.sidebarState.isHidden, })), [collections], ); + const sidebarWorkspaces = useMemo( + () => getVisibleSidebarWorkspaces(rawSidebarWorkspaces), + [rawSidebarWorkspaces], + ); + + const localStateWorkspaceIds = useMemo( + () => new Set(rawSidebarWorkspaces.map((workspace) => workspace.id)), + [rawSidebarWorkspaces], + ); + + const { data: localMainWorkspaces = [] } = useLiveQuery( + (q) => + q + .from({ workspaces: collections.v2Workspaces }) + .leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => + eq(workspaces.hostId, hosts.id), + ) + .where(({ workspaces }) => eq(workspaces.type, "main")) + .select(({ workspaces, hosts }) => ({ + id: workspaces.id, + projectId: workspaces.projectId, + hostId: workspaces.hostId, + hostMachineId: hosts?.machineId ?? null, + type: workspaces.type, + hostIsOnline: hosts?.isOnline ?? null, + name: workspaces.name, + branch: workspaces.branch, + createdAt: workspaces.createdAt, + updatedAt: workspaces.updatedAt, + tabOrder: MAIN_WORKSPACE_TAB_ORDER, + sectionId: null as string | null, + })), + [collections], + ); + + const visibleSidebarWorkspaces = useMemo(() => { + const sidebarProjectIds = new Set( + sidebarProjects.map((project) => project.id), + ); + const autoLocalMainWorkspaces = localMainWorkspaces.filter( + (workspace) => + !localStateWorkspaceIds.has(workspace.id) && + workspace.hostMachineId != null && + workspace.hostMachineId === machineId && + sidebarProjectIds.has(workspace.projectId), + ); + + return [...autoLocalMainWorkspaces, ...sidebarWorkspaces]; + }, [ + localMainWorkspaces, + localStateWorkspaceIds, + machineId, + sidebarProjects, + sidebarWorkspaces, + ]); + const computedLocalWorkspaceIds = useMemo( () => - sidebarWorkspaces + visibleSidebarWorkspaces .filter( (workspace) => workspace.hostMachineId != null && @@ -263,7 +323,7 @@ export function useDashboardSidebarData() { ) .map((workspace) => workspace.id) .sort(), - [machineId, sidebarWorkspaces], + [machineId, visibleSidebarWorkspaces], ); const localWorkspaceIds = useStableStringArray(computedLocalWorkspaceIds); @@ -339,7 +399,7 @@ export function useDashboardSidebarData() { }); } - for (const workspace of sidebarWorkspaces) { + for (const workspace of visibleSidebarWorkspaces) { const project = projectsById.get(workspace.projectId); if (!project) continue; @@ -355,6 +415,7 @@ export function useDashboardSidebarData() { projectId: workspace.projectId, hostId: workspace.hostId, hostType, + type: workspace.type, hostIsOnline: hostType === "remote-device" ? (workspace.hostIsOnline ?? null) @@ -410,6 +471,7 @@ export function useDashboardSidebarData() { projectId: pw.projectId, hostId: "", hostType: "local-device", + type: "worktree", hostIsOnline: null, accentColor: null, name: pw.name, @@ -478,7 +540,7 @@ export function useDashboardSidebarData() { pendingWorkspaces, sidebarProjects, sidebarSections, - sidebarWorkspaces, + visibleSidebarWorkspaces, ]); const groups = useStableDashboardSidebarProjects(computedGroups); 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 e2ff2406ec2..1f5f40bbded 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 @@ -3,6 +3,8 @@ export type DashboardSidebarWorkspaceHostType = | "remote-device" | "cloud"; +export type DashboardSidebarWorkspaceType = "main" | "worktree"; + export interface DashboardSidebarWorkspacePullRequestCheck { name: string; status: "success" | "failure" | "pending" | "skipped" | "cancelled"; @@ -25,6 +27,7 @@ export interface DashboardSidebarWorkspace { projectId: string; hostId: string; hostType: DashboardSidebarWorkspaceHostType; + type: DashboardSidebarWorkspaceType; hostIsOnline: boolean | null; accentColor: string | null; name: string; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts index 04a8aad2735..6bb07fc8690 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds/getFlattenedV2WorkspaceIds.ts @@ -1,4 +1,5 @@ import type { AppCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider/collections"; +import { getVisibleSidebarWorkspaces } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; type TopLevelItem = | { kind: "workspace"; tabOrder: number; workspaceId: string } @@ -17,11 +18,12 @@ export function getFlattenedV2WorkspaceIds( const allWorkspaces = Array.from( collections.v2WorkspaceLocalState.state.values(), ); + const visibleWorkspaces = getVisibleSidebarWorkspaces(allWorkspaces); const result: string[] = []; for (const project of projects) { - const projectWorkspaces = allWorkspaces.filter( + const projectWorkspaces = visibleWorkspaces.filter( (workspace) => workspace.sidebarState.projectId === project.projectId, ); const projectSections = allSections.filter( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx index 46b7d39e635..8d2ecbbcdec 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx @@ -17,6 +17,7 @@ import { } from "react-icons/hi2"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { getVisibleSidebarWorkspaces } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; import { useTabsStore } from "renderer/stores/tabs/store"; import { AppResourceSection } from "./components/AppResourceSection"; import { MetricBadge } from "./components/MetricBadge"; @@ -83,7 +84,10 @@ export function ResourceConsumption() { q .from({ ws: collections.v2WorkspaceLocalState }) .orderBy(({ ws }) => ws.sidebarState.tabOrder, "asc") - .select(({ ws }) => ({ workspaceId: ws.workspaceId })), + .select(({ ws }) => ({ + workspaceId: ws.workspaceId, + isHidden: ws.sidebarState.isHidden, + })), [collections], ); @@ -93,7 +97,10 @@ export function ResourceConsumption() { ); const sidebarWorkspaceOrder = useMemo( - () => rawSidebarWorkspaces.map((w) => w.workspaceId), + () => + getVisibleSidebarWorkspaces(rawSidebarWorkspaces).map( + (w) => w.workspaceId, + ), [rawSidebarWorkspaces], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx index 50cf05473f0..3f781f11471 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx @@ -41,8 +41,12 @@ export function V2WorkspaceRow({ isCurrentRoute, }: V2WorkspaceRowProps) { const navigate = useNavigate(); - const { ensureWorkspaceInSidebar, removeWorkspaceFromSidebar } = - useDashboardSidebarState(); + const { + ensureWorkspaceInSidebar, + hideWorkspaceInSidebar, + removeWorkspaceFromSidebar, + } = useDashboardSidebarState(); + const isMainWorkspace = workspace.type === "main"; const HostIcon = hostIconFor(workspace.hostType); @@ -70,9 +74,20 @@ export function V2WorkspaceRow({ event.preventDefault(); return; } + if (isMainWorkspace) { + hideWorkspaceInSidebar(workspace.id, workspace.projectId); + return; + } removeWorkspaceFromSidebar(workspace.id); }, - [isCurrentRoute, removeWorkspaceFromSidebar, workspace.id], + [ + hideWorkspaceInSidebar, + isCurrentRoute, + isMainWorkspace, + removeWorkspaceFromSidebar, + workspace.id, + workspace.projectId, + ], ); const creatorLabel = workspace.isCreatedByCurrentUser diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts index 764feb6241a..3dcd326b852 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts @@ -4,6 +4,7 @@ import { useMemo } from "react"; import { env } from "renderer/env.renderer"; import { authClient } from "renderer/lib/auth-client"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { isSidebarWorkspaceVisible } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import { MOCK_ORG_ID } from "shared/constants"; @@ -13,6 +14,7 @@ export interface AccessibleV2Workspace { id: string; name: string; branch: string; + type: "main" | "worktree"; createdAt: Date; createdByUserId: string | null; createdByName: string | null; @@ -100,6 +102,11 @@ export function useAccessibleV2Workspaces( ({ workspaces, sidebarState }) => eq(sidebarState.workspaceId, workspaces.id), ) + .leftJoin( + { sidebarProject: collections.v2SidebarProjects }, + ({ projects, sidebarProject }) => + eq(sidebarProject.projectId, projects.id), + ) .leftJoin({ creators: collections.users }, ({ workspaces, creators }) => eq(workspaces.createdByUserId, creators.id), ) @@ -109,22 +116,34 @@ export function useAccessibleV2Workspaces( eq(userHosts.userId, currentUserId ?? ""), ), ) - .select(({ workspaces, hosts, projects, sidebarState, creators }) => ({ - id: workspaces.id, - name: workspaces.name, - branch: workspaces.branch, - createdAt: workspaces.createdAt, - createdByUserId: workspaces.createdByUserId, - createdByName: creators?.name ?? null, - createdByImage: creators?.image ?? null, - projectId: projects.id, - projectName: projects.name, - hostId: hosts.id, - hostName: hosts.name, - hostMachineId: hosts.machineId, - hostIsOnline: hosts.isOnline, - sidebarWorkspaceId: sidebarState?.workspaceId ?? null, - })), + .select( + ({ + workspaces, + hosts, + projects, + sidebarState, + sidebarProject, + creators, + }) => ({ + id: workspaces.id, + name: workspaces.name, + branch: workspaces.branch, + type: workspaces.type, + createdAt: workspaces.createdAt, + createdByUserId: workspaces.createdByUserId, + createdByName: creators?.name ?? null, + createdByImage: creators?.image ?? null, + projectId: projects.id, + projectName: projects.name, + hostId: hosts.id, + hostName: hosts.name, + hostMachineId: hosts.machineId, + hostIsOnline: hosts.isOnline, + sidebarProjectId: sidebarProject?.projectId ?? null, + sidebarWorkspaceId: sidebarState?.workspaceId ?? null, + sidebarIsHidden: sidebarState?.sidebarState.isHidden ?? false, + }), + ), [activeOrganizationId, collections, currentUserId], ); @@ -138,10 +157,20 @@ export function useAccessibleV2Workspaces( : row.hostMachineId === machineId ? "local-device" : "remote-device"; + const isAutoVisibleMain = + row.type === "main" && + row.hostMachineId != null && + row.hostMachineId === machineId && + row.sidebarProjectId != null; + const isInSidebar = + isSidebarWorkspaceVisible({ isHidden: row.sidebarIsHidden }) && + (row.sidebarWorkspaceId != null || isAutoVisibleMain); + deduped.set(row.id, { id: row.id, name: row.name, branch: row.branch, + type: row.type, createdAt: new Date(row.createdAt), createdByUserId: row.createdByUserId, createdByName: row.createdByName ?? null, @@ -155,7 +184,7 @@ export function useAccessibleV2Workspaces( hostMachineId: row.hostMachineId, hostIsOnline: row.hostIsOnline, hostType, - isInSidebar: row.sidebarWorkspaceId != null, + isInSidebar, }); } return Array.from(deduped.values()).sort( diff --git a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts index 6460b5366af..108ea0afb1f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts @@ -8,6 +8,7 @@ import { } from "renderer/routes/_authenticated/components/utils/paneLifecycleRows"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { AppCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider/collections"; +import { isSidebarWorkspaceVisible } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; import { PROJECT_CUSTOM_COLORS } from "shared/constants/project-colors"; function getNextTabOrder(items: Array<{ tabOrder: number }>): number { @@ -58,6 +59,7 @@ function getProjectTopLevelItems( .filter( (item) => item.sidebarState.projectId === projectId && + isSidebarWorkspaceVisible(item) && item.sidebarState.sectionId === null && item.workspaceId !== options.excludeWorkspaceId, ) @@ -85,6 +87,14 @@ function getFirstSectionIndex(items: ProjectTopLevelItem[]): number { return firstSectionIndex === -1 ? items.length : firstSectionIndex; } +function createEmptyPaneLayout(): WorkspaceState { + return { + version: 1, + tabs: [], + activeTabId: null, + } satisfies WorkspaceState; +} + /** * Rewrites the flat top-level project lane. Workspace items are explicitly * ungrouped by setting sidebarState.projectId and clearing sidebarState.sectionId. @@ -102,6 +112,7 @@ function writeProjectTopLevelOrder( draft.sidebarState.projectId = projectId; draft.sidebarState.sectionId = null; draft.sidebarState.tabOrder = tabOrder; + draft.sidebarState.isHidden = false; }); return; } @@ -139,12 +150,23 @@ function ensureSidebarWorkspaceRecord( workspaceId: string, projectId: string, ): void { - if (collections.v2WorkspaceLocalState.get(workspaceId)) { + const existing = collections.v2WorkspaceLocalState.get(workspaceId); + if (existing && isSidebarWorkspaceVisible(existing)) { return; } const topLevelItems = getProjectTopLevelItems(collections, projectId); + if (existing) { + collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { + draft.sidebarState.projectId = projectId; + draft.sidebarState.tabOrder = getPrependTabOrder(topLevelItems); + draft.sidebarState.sectionId = null; + draft.sidebarState.isHidden = false; + }); + return; + } + collections.v2WorkspaceLocalState.insert({ workspaceId, createdAt: new Date(), @@ -152,12 +174,9 @@ function ensureSidebarWorkspaceRecord( projectId, tabOrder: getPrependTabOrder(topLevelItems), sectionId: null, + isHidden: false, }, - paneLayout: { - version: 1, - tabs: [], - activeTabId: null, - } satisfies WorkspaceState, + paneLayout: createEmptyPaneLayout(), }); } @@ -228,6 +247,7 @@ export function useDashboardSidebarState() { if (!collections.v2WorkspaceLocalState.get(workspaceId)) return; collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { draft.sidebarState.tabOrder = index + 1; + draft.sidebarState.isHidden = false; }); }); }, @@ -247,6 +267,7 @@ export function useDashboardSidebarState() { draft.sidebarState.tabOrder = tabOrder; draft.sidebarState.sectionId = null; draft.sidebarState.projectId = projectId; + draft.sidebarState.isHidden = false; }); } else { if (!collections.v2SidebarSections.get(item.id)) return; @@ -274,6 +295,7 @@ export function useDashboardSidebarState() { .filter( (item) => item.sidebarState.projectId === projectId && + isSidebarWorkspaceVisible(item) && item.workspaceId !== workspaceId && item.sidebarState.sectionId === sectionId, ) @@ -285,6 +307,7 @@ export function useDashboardSidebarState() { draft.sidebarState.tabOrder = i + 1; draft.sidebarState.sectionId = sectionId; draft.sidebarState.projectId = projectId; + draft.sidebarState.isHidden = false; }); }); }, @@ -376,14 +399,17 @@ export function useDashboardSidebarState() { .filter( (item) => item.sidebarState.projectId === projectId && + isSidebarWorkspaceVisible(item) && item.workspaceId !== workspaceId && item.sidebarState.sectionId === sectionId, ) .map((item) => ({ tabOrder: item.sidebarState.tabOrder })); collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { + draft.sidebarState.projectId = projectId; draft.sidebarState.sectionId = sectionId; draft.sidebarState.tabOrder = getNextTabOrder(siblingRows); + draft.sidebarState.isHidden = false; }); }, [collections], @@ -405,6 +431,7 @@ export function useDashboardSidebarState() { .filter( (item) => item.sidebarState.projectId === section.projectId && + isSidebarWorkspaceVisible(item) && item.sidebarState.sectionId === sectionId, ) .sort( @@ -439,6 +466,35 @@ export function useDashboardSidebarState() { [collections], ); + const hideWorkspaceInSidebar = useCallback( + (workspaceId: string, projectId: string) => { + const workspace = collections.v2WorkspaceLocalState.get(workspaceId); + if (!workspace) { + collections.v2WorkspaceLocalState.insert({ + workspaceId, + createdAt: new Date(), + sidebarState: { + projectId, + tabOrder: 0, + sectionId: null, + isHidden: true, + }, + paneLayout: createEmptyPaneLayout(), + }); + return; + } + + cleanupWorkspacePaneRuntimes([workspace]); + collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { + draft.sidebarState.projectId = projectId; + draft.sidebarState.sectionId = null; + draft.sidebarState.isHidden = true; + draft.paneLayout = createEmptyPaneLayout(); + }); + }, + [collections], + ); + const removeProjectFromSidebar = useCallback( (projectId: string) => { const workspaceRows = Array.from( @@ -470,6 +526,7 @@ export function useDashboardSidebarState() { deleteSection, ensureProjectInSidebar, ensureWorkspaceInSidebar, + hideWorkspaceInSidebar, moveWorkspaceToSection, moveWorkspaceToSectionAtIndex, removeProjectFromSidebar, diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/index.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/index.ts index 686fbd9e9f9..53b72976220 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/index.ts @@ -1 +1,2 @@ export * from "./schema"; +export * from "./sidebarVisibility"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts index 41a80b2e969..ef260659947 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts @@ -37,6 +37,7 @@ export const workspaceLocalStateSchema = z.object({ sectionId: z.string().uuid().nullable().default(null), changesFilter: changesFilterSchema.default({ kind: "all" }), changesSubtab: z.enum(["diffs", "review"]).default("diffs"), + isHidden: z.boolean().default(false), }), paneLayout: paneWorkspaceStateSchema, viewedFiles: z.array(z.string()).default([]), diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/sidebarVisibility.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/sidebarVisibility.ts new file mode 100644 index 00000000000..9cf2b5c7d11 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/sidebarVisibility.ts @@ -0,0 +1,24 @@ +type SidebarWorkspaceVisibilitySource = + | { isHidden?: boolean | null } + | { sidebarState: { isHidden?: boolean | null } }; + +export function getSidebarWorkspaceIsHidden( + workspace: SidebarWorkspaceVisibilitySource, +): boolean { + if ("sidebarState" in workspace) { + return workspace.sidebarState.isHidden === true; + } + return workspace.isHidden === true; +} + +export function isSidebarWorkspaceVisible( + workspace: SidebarWorkspaceVisibilitySource, +): boolean { + return !getSidebarWorkspaceIsHidden(workspace); +} + +export function getVisibleSidebarWorkspaces< + Workspace extends SidebarWorkspaceVisibilitySource, +>(workspaces: readonly Workspace[]): Workspace[] { + return workspaces.filter(isSidebarWorkspaceVisible); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/ProjectLocationSection/ProjectLocationSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/ProjectLocationSection/ProjectLocationSection.tsx index 0fa19d979e1..8f0f38e2c83 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/ProjectLocationSection/ProjectLocationSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/ProjectLocationSection/ProjectLocationSection.tsx @@ -16,6 +16,7 @@ import { useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import { ClickablePath } from "../../../../../../components/ClickablePath"; @@ -42,6 +43,8 @@ export function ProjectLocationSection({ const selectDirectory = electronTrpc.window.selectDirectory.useMutation(); const navigate = useNavigate(); const collections = useCollections(); + const { ensureProjectInSidebar, ensureWorkspaceInSidebar } = + useDashboardSidebarState(); const { data: hostRows } = useLiveQuery( (q) => @@ -76,6 +79,11 @@ export function ProjectLocationSection({ ? `Project relocated to ${result.repoPath}` : `Project set up at ${result.repoPath}`, ); + if (result.mainWorkspaceId) { + ensureWorkspaceInSidebar(result.mainWorkspaceId, projectId); + } else { + ensureProjectInSidebar(projectId); + } onChanged?.(); return true; } catch (err) { @@ -96,6 +104,11 @@ export function ProjectLocationSection({ mode: { kind: "clone", parentDir }, }); toast.success(`Cloned to ${result.repoPath}`); + if (result.mainWorkspaceId) { + ensureWorkspaceInSidebar(result.mainWorkspaceId, projectId); + } else { + ensureProjectInSidebar(projectId); + } onChanged?.(); return true; } catch (err) { diff --git a/packages/db/drizzle/0038_v2_workspaces_main_type.sql b/packages/db/drizzle/0038_v2_workspaces_main_type.sql new file mode 100644 index 00000000000..9fd2ad790f0 --- /dev/null +++ b/packages/db/drizzle/0038_v2_workspaces_main_type.sql @@ -0,0 +1,3 @@ +CREATE TYPE "public"."v2_workspace_type" AS ENUM('main', 'worktree');--> statement-breakpoint +ALTER TABLE "v2_workspaces" ADD COLUMN "type" "v2_workspace_type" DEFAULT 'worktree' NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "v2_workspaces_one_main_per_host" ON "v2_workspaces" USING btree ("project_id","host_id") WHERE "v2_workspaces"."type" = 'main'; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0038_snapshot.json b/packages/db/drizzle/meta/0038_snapshot.json new file mode 100644 index 00000000000..016ce23212a --- /dev/null +++ b/packages/db/drizzle/meta/0038_snapshot.json @@ -0,0 +1,5781 @@ +{ + "id": "ef3bd128-1ac9-4166-a066-b1aa6811772a", + "prevId": "16fdad31-9719-48da-880a-05e1c736dee7", + "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 + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk": { + "name": "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "oauth_clients", + "schemaTo": "auth", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_session_id_sessions_id_fk": { + "name": "oauth_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "sessions", + "schemaTo": "auth", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_user_id_users_id_fk": { + "name": "oauth_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.organizations": { + "name": "organizations", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_domains": { + "name": "allowed_domains", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizations_allowed_domains_idx": { + "name": "organizations_allowed_domains_idx", + "columns": [ + { + "expression": "allowed_domains", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "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.automation_runs": { + "name": "automation_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "automation_id": { + "name": "automation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "v2_workspace_id": { + "name": "v2_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_kind": { + "name": "session_kind", + "type": "automation_session_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "chat_session_id": { + "name": "chat_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "terminal_session_id": { + "name": "terminal_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "automation_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_runs_dedup_idx": { + "name": "automation_runs_dedup_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_history_idx": { + "name": "automation_runs_history_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_status_idx": { + "name": "automation_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_workspace_idx": { + "name": "automation_runs_workspace_idx", + "columns": [ + { + "expression": "v2_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_organization_id_organizations_id_fk": { + "name": "automation_runs_organization_id_organizations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_host_id_v2_hosts_id_fk": { + "name": "automation_runs_host_id_v2_hosts_id_fk", + "tableFrom": "automation_runs", + "tableTo": "v2_hosts", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "automation_runs_chat_session_id_chat_sessions_id_fk": { + "name": "automation_runs_chat_session_id_chat_sessions_id_fk", + "tableFrom": "automation_runs", + "tableTo": "chat_sessions", + "columnsFrom": [ + "chat_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automations": { + "name": "automations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_config": { + "name": "agent_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "target_host_id": { + "name": "target_host_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "v2_project_id": { + "name": "v2_project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "v2_workspace_id": { + "name": "v2_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dtstart": { + "name": "dtstart", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mcp_scope": { + "name": "mcp_scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "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": { + "automations_dispatcher_idx": { + "name": "automations_dispatcher_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automations_owner_idx": { + "name": "automations_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automations_organization_idx": { + "name": "automations_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automations_organization_id_organizations_id_fk": { + "name": "automations_organization_id_organizations_id_fk", + "tableFrom": "automations", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automations_owner_user_id_users_id_fk": { + "name": "automations_owner_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automations_target_host_id_v2_hosts_id_fk": { + "name": "automations_target_host_id_v2_hosts_id_fk", + "tableFrom": "automations", + "tableTo": "v2_hosts", + "columnsFrom": [ + "target_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "automations_v2_project_id_v2_projects_id_fk": { + "name": "automations_v2_project_id_v2_projects_id_fk", + "tableFrom": "automations", + "tableTo": "v2_projects", + "columnsFrom": [ + "v2_project_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 + }, + "is_online": { + "name": "is_online", + "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 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 + }, + "repo_clone_url": { + "name": "repo_clone_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_projects_organization_id_idx": { + "name": "v2_projects_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_projects_organization_id_organizations_id_fk": { + "name": "v2_projects_organization_id_organizations_id_fk", + "tableFrom": "v2_projects", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_projects_github_repository_id_github_repositories_id_fk": { + "name": "v2_projects_github_repository_id_github_repositories_id_fk", + "tableFrom": "v2_projects", + "tableTo": "github_repositories", + "columnsFrom": [ + "github_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "v2_projects_org_slug_unique": { + "name": "v2_projects_org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_users_hosts": { + "name": "v2_users_hosts", + "schema": "", + "columns": { + "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 + }, + "type": { + "name": "type", + "type": "v2_workspace_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'worktree'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_workspaces_project_id_idx": { + "name": "v2_workspaces_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_organization_id_idx": { + "name": "v2_workspaces_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_host_id_idx": { + "name": "v2_workspaces_host_id_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_one_main_per_host": { + "name": "v2_workspaces_one_main_per_host", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"v2_workspaces\".\"type\" = 'main'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_workspaces_organization_id_organizations_id_fk": { + "name": "v2_workspaces_organization_id_organizations_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_workspaces_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.automation_run_status": { + "name": "automation_run_status", + "schema": "public", + "values": [ + "dispatching", + "dispatched", + "skipped_offline", + "dispatch_failed" + ] + }, + "public.automation_session_kind": { + "name": "automation_session_kind", + "schema": "public", + "values": [ + "chat", + "terminal" + ] + }, + "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.v2_workspace_type": { + "name": "v2_workspace_type", + "schema": "public", + "values": [ + "main", + "worktree" + ] + }, + "public.workspace_type": { + "name": "workspace_type", + "schema": "public", + "values": [ + "local", + "cloud" + ] + } + }, + "schemas": { + "auth": "auth", + "ingest": "ingest" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index d164c18a25b..0c4ecb7875b 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -267,6 +267,13 @@ "when": 1777226062658, "tag": "0037_drop_v2_project_repo_clone_url_unique", "breakpoints": true + }, + { + "idx": 38, + "version": "7", + "when": 1777259447556, + "tag": "0038_v2_workspaces_main_type", + "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 ac04c9f356d..2b7ef0ae352 100644 --- a/packages/db/src/schema/enums.ts +++ b/packages/db/src/schema/enums.ts @@ -68,6 +68,10 @@ export const workspaceTypeValues = ["local", "cloud"] as const; export const workspaceTypeEnum = z.enum(workspaceTypeValues); export type WorkspaceType = z.infer; +export const v2WorkspaceTypeValues = ["main", "worktree"] as const; +export const v2WorkspaceTypeEnum = z.enum(v2WorkspaceTypeValues); +export type V2WorkspaceType = z.infer; + export const automationRunStatusValues = [ "dispatching", "dispatched", diff --git a/packages/db/src/schema/schema.ts b/packages/db/src/schema/schema.ts index e4521d0e0ce..4b3aa7ef70c 100644 --- a/packages/db/src/schema/schema.ts +++ b/packages/db/src/schema/schema.ts @@ -1,4 +1,5 @@ import type { ResolvedAgentConfig } from "@superset/shared/agent-settings"; +import { sql } from "drizzle-orm"; import { boolean, index, @@ -24,6 +25,7 @@ import { taskStatusEnumValues, v2ClientTypeValues, v2UsersHostRoleValues, + v2WorkspaceTypeValues, workspaceTypeValues, } from "./enums"; import { githubRepositories } from "./github"; @@ -43,6 +45,10 @@ export const v2UsersHostRole = pgEnum( "v2_users_host_role", v2UsersHostRoleValues, ); +export const v2WorkspaceType = pgEnum( + "v2_workspace_type", + v2WorkspaceTypeValues, +); export const taskStatuses = pgTable( "task_statuses", @@ -529,6 +535,7 @@ export const v2Workspaces = pgTable( .references(() => v2Hosts.id), name: text().notNull(), branch: text().notNull(), + type: v2WorkspaceType().notNull().default("worktree"), createdByUserId: uuid("created_by_user_id").references(() => users.id, { onDelete: "set null", }), @@ -544,6 +551,9 @@ export const v2Workspaces = pgTable( index("v2_workspaces_project_id_idx").on(table.projectId), index("v2_workspaces_organization_id_idx").on(table.organizationId), index("v2_workspaces_host_id_idx").on(table.hostId), + uniqueIndex("v2_workspaces_one_main_per_host") + .on(table.projectId, table.hostId) + .where(sql`${table.type} = 'main'`), ], ); diff --git a/packages/host-service/src/app.ts b/packages/host-service/src/app.ts index 26abc493a41..f374d28390e 100644 --- a/packages/host-service/src/app.ts +++ b/packages/host-service/src/app.ts @@ -15,6 +15,7 @@ import { ChatRuntimeManager } from "./runtime/chat"; import { WorkspaceFilesystemManager } from "./runtime/filesystem"; import type { GitCredentialProvider } from "./runtime/git"; import { createGitFactory } from "./runtime/git"; +import { runMainWorkspaceSweep } from "./runtime/main-workspace-sweep"; import { PullRequestRuntimeManager } from "./runtime/pull-requests"; import { registerWorkspaceTerminalRoute } from "./terminal/terminal"; import { appRouter } from "./trpc/router"; @@ -94,6 +95,18 @@ export function createApp(options: CreateAppOptions): CreateAppResult { const eventBus = new EventBus({ db, filesystem }); eventBus.start(); + // Backfill `kind='main'` v2 workspaces for projects already set up before + // this column shipped. Idempotent; runs in the background so it doesn't + // block server startup. + void runMainWorkspaceSweep({ + api, + db, + git, + organizationId: config.organizationId, + }).catch((err) => { + console.warn("[host-service] main-workspace sweep failed:", err); + }); + const wsAuth: MiddlewareHandler = async (c, next) => { const token = c.req.query("token"); const authorized = diff --git a/packages/host-service/src/runtime/main-workspace-sweep.ts b/packages/host-service/src/runtime/main-workspace-sweep.ts new file mode 100644 index 00000000000..cfc10022739 --- /dev/null +++ b/packages/host-service/src/runtime/main-workspace-sweep.ts @@ -0,0 +1,33 @@ +import { existsSync } from "node:fs"; +import { projects } from "../db/schema"; +import { + type EnsureMainWorkspaceContext, + ensureMainWorkspace, +} from "../trpc/router/project/utils/ensure-main-workspace"; + +/** + * Recovery path for projects set up before `type='main'` shipped. + * + * Iterates local `projects` and ensures each has a main v2 workspace bound to + * the current host. Idempotent via the `(projectId, hostId) WHERE type='main'` + * unique index, so it's safe on every boot — only does real work the first + * time after upgrade. + */ +export async function runMainWorkspaceSweep( + ctx: EnsureMainWorkspaceContext, +): Promise { + const rows = ctx.db + .select({ id: projects.id, repoPath: projects.repoPath }) + .from(projects) + .all(); + + for (const row of rows) { + if (!existsSync(row.repoPath)) { + console.warn( + `[main-workspace-sweep] skipping ${row.id}: repoPath ${row.repoPath} does not exist`, + ); + continue; + } + await ensureMainWorkspace(ctx, row.id, row.repoPath); + } +} diff --git a/packages/host-service/src/trpc/router/project/handlers.ts b/packages/host-service/src/trpc/router/project/handlers.ts index b070e67ee9b..d2e02aeb0c1 100644 --- a/packages/host-service/src/trpc/router/project/handlers.ts +++ b/packages/host-service/src/trpc/router/project/handlers.ts @@ -1,6 +1,7 @@ import { rmSync } from "node:fs"; import { TRPCError } from "@trpc/server"; import type { HostServiceContext } from "../../../types"; +import { ensureMainWorkspace } from "./utils/ensure-main-workspace"; import { persistLocalProject } from "./utils/persist-project"; import { cloneRepoInto, @@ -26,6 +27,7 @@ function slugifyProjectName(name: string): string { interface CreateResult { projectId: string; repoPath: string; + mainWorkspaceId: string | null; } function slugWithSuffix(baseSlug: string, attempt: number): string { @@ -112,7 +114,16 @@ export async function createFromClone( resolved, "createFromClone", ); - return { projectId: cloudProject.id, repoPath: resolved.repoPath }; + const mainWorkspace = await ensureMainWorkspace( + ctx, + cloudProject.id, + resolved.repoPath, + ); + return { + projectId: cloudProject.id, + repoPath: resolved.repoPath, + mainWorkspaceId: mainWorkspace?.id ?? null, + }; } catch (err) { // Once a cloud project exists, keep the clone in place so rerun/recovery // has a local repo path to relink instead of creating a second clone. @@ -144,5 +155,14 @@ export async function createFromImportLocal( resolved, "createFromImportLocal", ); - return { projectId: cloudProject.id, repoPath: resolved.repoPath }; + const mainWorkspace = await ensureMainWorkspace( + ctx, + cloudProject.id, + resolved.repoPath, + ); + return { + projectId: cloudProject.id, + repoPath: resolved.repoPath, + mainWorkspaceId: mainWorkspace?.id ?? null, + }; } diff --git a/packages/host-service/src/trpc/router/project/project.ts b/packages/host-service/src/trpc/router/project/project.ts index 4f121c25d55..9052501ac9f 100644 --- a/packages/host-service/src/trpc/router/project/project.ts +++ b/packages/host-service/src/trpc/router/project/project.ts @@ -7,6 +7,7 @@ import { z } from "zod"; import { projects, workspaces } from "../../../db/schema"; import { protectedProcedure, router } from "../../index"; import { createFromClone, createFromImportLocal } from "./handlers"; +import { ensureMainWorkspace } from "./utils/ensure-main-workspace"; import { persistLocalProject } from "./utils/persist-project"; import { cloneRepoInto, @@ -195,13 +196,31 @@ export const projectRouter = router({ expectedParsed.name, ); rejectIfRepoint(predictedPath); - if (existing) return { repoPath: existing.repoPath }; + if (existing) { + const mainWorkspace = await ensureMainWorkspace( + ctx, + input.projectId, + existing.repoPath, + ); + return { + repoPath: existing.repoPath, + mainWorkspaceId: mainWorkspace?.id ?? null, + }; + } const resolved = await cloneRepoInto( cloudProject.repoCloneUrl, input.mode.parentDir, ); persistLocalProject(ctx, input.projectId, resolved); - return { repoPath: resolved.repoPath }; + const mainWorkspace = await ensureMainWorkspace( + ctx, + input.projectId, + resolved.repoPath, + ); + return { + repoPath: resolved.repoPath, + mainWorkspaceId: mainWorkspace?.id ?? null, + }; } case "import": { let resolved: ResolvedRepo; @@ -223,7 +242,15 @@ export const projectRouter = router({ rejectIfRepoint(resolved.repoPath); if (existing && existing.repoPath === resolved.repoPath) { - return { repoPath: existing.repoPath }; + const mainWorkspace = await ensureMainWorkspace( + ctx, + input.projectId, + existing.repoPath, + ); + return { + repoPath: existing.repoPath, + mainWorkspaceId: mainWorkspace?.id ?? null, + }; } if (!cloudProject.repoCloneUrl && resolved.parsed) { @@ -234,7 +261,15 @@ export const projectRouter = router({ }); } persistLocalProject(ctx, input.projectId, resolved); - return { repoPath: resolved.repoPath }; + const mainWorkspace = await ensureMainWorkspace( + ctx, + input.projectId, + resolved.repoPath, + ); + return { + repoPath: resolved.repoPath, + mainWorkspaceId: mainWorkspace?.id ?? null, + }; } } }), @@ -254,6 +289,13 @@ export const projectRouter = router({ .all(); for (const ws of localWorkspaces) { + if (ws.worktreePath === localProject.repoPath) { + await ctx.api.v2Workspace.deleteMainForHost.mutate({ + id: ws.id, + projectId: input.projectId, + }); + continue; + } try { const git = await ctx.git(localProject.repoPath); await git.raw(["worktree", "remove", ws.worktreePath]); diff --git a/packages/host-service/src/trpc/router/project/utils/ensure-main-workspace.ts b/packages/host-service/src/trpc/router/project/utils/ensure-main-workspace.ts new file mode 100644 index 00000000000..86cae761634 --- /dev/null +++ b/packages/host-service/src/trpc/router/project/utils/ensure-main-workspace.ts @@ -0,0 +1,93 @@ +import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { workspaces } from "../../../../db/schema"; +import type { HostServiceContext } from "../../../../types"; + +export type EnsureMainWorkspaceContext = Pick< + HostServiceContext, + "api" | "db" | "git" | "organizationId" +>; + +async function getCurrentBranchName( + git: Awaited>, +): Promise { + try { + const branch = await git.raw(["symbolic-ref", "--short", "HEAD"]); + const trimmed = branch.trim(); + return trimmed || null; + } catch { + try { + const branch = await git.revparse(["--abbrev-ref", "HEAD"]); + const trimmed = branch.trim(); + return trimmed && trimmed !== "HEAD" ? trimmed : null; + } catch { + return null; + } + } +} + +/** + * Ensures a `type='main'` v2 workspace exists for (projectId, currentHost), + * with a matching local `workspaces` row whose `worktreePath` is the repo root. + * + * Idempotent: safe to call from `project.setup` and from the startup sweep. + * Log-and-continue: on any cloud/local failure, logs and returns null so + * callers (e.g. `project.setup`) don't regress when a transient cloud blip + * hits. The startup sweep will backfill on the next boot. + */ +export async function ensureMainWorkspace( + ctx: EnsureMainWorkspaceContext, + projectId: string, + repoPath: string, +): Promise<{ id: string } | null> { + try { + const git = await ctx.git(repoPath); + const branch = await getCurrentBranchName(git); + if (!branch) { + console.warn( + `[ensureMainWorkspace] could not resolve current branch for ${projectId} at ${repoPath}; skipping`, + ); + return null; + } + + const host = await ctx.api.device.ensureV2Host.mutate({ + organizationId: ctx.organizationId, + machineId: getHashedDeviceId(), + name: getDeviceName(), + }); + + const cloudRow = await ctx.api.v2Workspace.create.mutate({ + organizationId: ctx.organizationId, + projectId, + name: branch, + branch, + hostId: host.id, + type: "main", + }); + + ctx.db + .insert(workspaces) + .values({ + id: cloudRow.id, + projectId, + worktreePath: repoPath, + branch, + }) + .onConflictDoUpdate({ + target: workspaces.id, + set: { + projectId, + worktreePath: repoPath, + branch, + }, + }) + .run(); + + return { id: cloudRow.id }; + } catch (err) { + console.warn( + `[ensureMainWorkspace] failed for ${projectId} at ${repoPath}; will retry via startup sweep`, + err, + ); + return null; + } +} diff --git a/packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts b/packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts index d33f5e35c0e..ff7609dbc30 100644 --- a/packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts +++ b/packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts @@ -56,6 +56,27 @@ export const workspaceCleanupRouter = router({ .sync() : undefined; + if (local && project && local.worktreePath === project.repoPath) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "Main workspaces cannot be deleted. Remove them from the sidebar or remove the project from this host instead.", + }); + } + if (ctx.api) { + const cloudWorkspace = await ctx.api.v2Workspace.getFromHost.query({ + organizationId: ctx.organizationId, + id: input.workspaceId, + }); + if (cloudWorkspace?.type === "main") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "Main workspaces cannot be deleted. Remove them from the sidebar or remove the project from this host instead.", + }); + } + } + // ─── Step 0: Preflight ───────────────────────────────────────── // Block only on dirty worktree (the common "I forgot to commit" // case). Anything else the local-cleanup phase handles as warning. diff --git a/packages/host-service/src/trpc/router/workspace-creation/procedures/adopt.ts b/packages/host-service/src/trpc/router/workspace-creation/procedures/adopt.ts index 5d0a1209704..963493ba7c3 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/procedures/adopt.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/procedures/adopt.ts @@ -4,6 +4,7 @@ import { and, eq, ne, or } from "drizzle-orm"; import { workspaces } from "../../../../db/schema"; import type { HostServiceContext } from "../../../../types"; import { protectedProcedure } from "../../../index"; +import { ensureMainWorkspace } from "../../project/utils/ensure-main-workspace"; import { adoptInputSchema } from "../schemas"; import { getWorktreeBranchAtPath, @@ -118,6 +119,7 @@ export const adopt = protectedProcedure const deviceName = getDeviceName(); const localProject = requireLocalProject(ctx, input.projectId); + await ensureMainWorkspace(ctx, input.projectId, localProject.repoPath); let branch = input.branch.trim(); if (!branch) { diff --git a/packages/host-service/src/trpc/router/workspace-creation/procedures/checkout.ts b/packages/host-service/src/trpc/router/workspace-creation/procedures/checkout.ts index 1483cd3652b..8ac3a156363 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/procedures/checkout.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/procedures/checkout.ts @@ -5,6 +5,7 @@ import { and, eq } from "drizzle-orm"; import { workspaces } from "../../../../db/schema"; import { resolveRef } from "../../../../runtime/git/refs"; import { protectedProcedure } from "../../../index"; +import { ensureMainWorkspace } from "../../project/utils/ensure-main-workspace"; import { checkoutInputSchema } from "../schemas"; import { finishCheckout } from "../shared/finish-checkout"; import { enablePushAutoSetupRemote } from "../shared/git-config"; @@ -20,6 +21,7 @@ export const checkout = protectedProcedure setProgress(input.pendingId, "ensuring_repo"); const localProject = requireLocalProject(ctx, input.projectId); + await ensureMainWorkspace(ctx, input.projectId, localProject.repoPath); setProgress(input.pendingId, "creating_worktree"); diff --git a/packages/host-service/src/trpc/router/workspace-creation/procedures/create.ts b/packages/host-service/src/trpc/router/workspace-creation/procedures/create.ts index d89577aaa31..109d34a6299 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/procedures/create.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/procedures/create.ts @@ -10,6 +10,7 @@ import { resolveUpstream, } from "../../../../runtime/git/refs"; import { protectedProcedure } from "../../../index"; +import { ensureMainWorkspace } from "../../project/utils/ensure-main-workspace"; import { createInputSchema } from "../schemas"; import { enablePushAutoSetupRemote } from "../shared/git-config"; import { requireLocalProject } from "../shared/local-project"; @@ -31,6 +32,7 @@ export const create = protectedProcedure setProgress(input.pendingId, "ensuring_repo"); const localProject = requireLocalProject(ctx, input.projectId); + await ensureMainWorkspace(ctx, input.projectId, localProject.repoPath); setProgress(input.pendingId, "creating_worktree"); diff --git a/packages/host-service/src/trpc/router/workspace/workspace.ts b/packages/host-service/src/trpc/router/workspace/workspace.ts index d21112a045e..a912cc18995 100644 --- a/packages/host-service/src/trpc/router/workspace/workspace.ts +++ b/packages/host-service/src/trpc/router/workspace/workspace.ts @@ -8,6 +8,7 @@ import { z } from "zod"; import { projects, workspaces } from "../../../db/schema"; import { invalidateLabelCache } from "../../../ports/static-ports"; import { protectedProcedure, router } from "../../index"; +import { ensureMainWorkspace } from "../project/utils/ensure-main-workspace"; export const workspaceRouter = router({ get: protectedProcedure @@ -77,6 +78,8 @@ export const workspaceRouter = router({ localProject = inserted; } + await ensureMainWorkspace(ctx, input.projectId, localProject.repoPath); + const worktreePath = join( localProject.repoPath, ".worktrees", @@ -172,17 +175,42 @@ export const workspaceRouter = router({ }); } - await ctx.api.v2Workspace.delete.mutate({ id: input.id }); - const localWorkspace = ctx.db.query.workspaces .findFirst({ where: eq(workspaces.id, input.id) }) .sync(); + const localProject = localWorkspace + ? ctx.db.query.projects + .findFirst({ where: eq(projects.id, localWorkspace.projectId) }) + .sync() + : undefined; + + if ( + localWorkspace && + localProject && + localWorkspace.worktreePath === localProject.repoPath + ) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "Main workspaces cannot be deleted. Remove them from the sidebar or remove the project from this host instead.", + }); + } - if (localWorkspace) { - const localProject = ctx.db.query.projects - .findFirst({ where: eq(projects.id, localWorkspace.projectId) }) - .sync(); + const cloudWorkspace = await ctx.api.v2Workspace.getFromHost.query({ + organizationId: ctx.organizationId, + id: input.id, + }); + if (cloudWorkspace?.type === "main") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "Main workspaces cannot be deleted. Remove them from the sidebar or remove the project from this host instead.", + }); + } + await ctx.api.v2Workspace.delete.mutate({ id: input.id }); + + if (localWorkspace) { if (localProject) { try { const git = await ctx.git(localProject.repoPath); diff --git a/packages/trpc/src/router/v2-workspace/v2-workspace.ts b/packages/trpc/src/router/v2-workspace/v2-workspace.ts index d361978e8fa..af6f406f198 100644 --- a/packages/trpc/src/router/v2-workspace/v2-workspace.ts +++ b/packages/trpc/src/router/v2-workspace/v2-workspace.ts @@ -1,4 +1,5 @@ import { dbWs } from "@superset/db/client"; +import { v2WorkspaceTypeValues } from "@superset/db/enums"; import { v2Hosts, v2Projects, v2Workspaces } from "@superset/db/schema"; import { getCurrentTxid } from "@superset/db/utils"; import type { TRPCRouterRecord } from "@trpc/server"; @@ -12,6 +13,9 @@ import { requireOrgScopedResource, } from "../utils/org-resource-access"; +const MAIN_WORKSPACE_DELETE_MESSAGE = + "Main workspaces cannot be deleted through workspace delete. Remove them from the sidebar or remove the project from this host instead."; + async function getScopedProject(organizationId: string, projectId: string) { return requireOrgScopedResource( () => @@ -103,6 +107,7 @@ export const v2WorkspaceRouter = { name: z.string().min(1), branch: z.string().min(1), hostId: z.string().uuid(), + type: z.enum(v2WorkspaceTypeValues).default("worktree"), }), ) .mutation(async ({ ctx, input }) => { @@ -119,7 +124,11 @@ export const v2WorkspaceRouter = { ); const host = await getScopedHost(input.organizationId, input.hostId); - const [workspace] = await dbWs + // Relies on the partial unique index + // (project_id, host_id) WHERE type='main' for idempotency — race-safe + // even if two callers (e.g. the startup sweep and project.setup) both + // miss the existence check at the same instant. + const [inserted] = await dbWs .insert(v2Workspaces) .values({ organizationId: project.organizationId, @@ -127,10 +136,49 @@ export const v2WorkspaceRouter = { name: input.name, branch: input.branch, hostId: host.id, + type: input.type, createdByUserId: ctx.userId, }) + .onConflictDoNothing() .returning(); - return workspace; + + if (inserted) return inserted; + + if (input.type === "main") { + const existing = await dbWs.query.v2Workspaces.findFirst({ + where: and( + eq(v2Workspaces.projectId, project.id), + eq(v2Workspaces.hostId, host.id), + eq(v2Workspaces.type, "main"), + ), + }); + if (existing) { + const patch: { + branch?: string; + name?: string; + } = {}; + if (existing.branch !== input.branch) { + patch.branch = input.branch; + if (existing.name === existing.branch) { + patch.name = input.name; + } + } + if (Object.keys(patch).length > 0) { + const [updated] = await dbWs + .update(v2Workspaces) + .set(patch) + .where(eq(v2Workspaces.id, existing.id)) + .returning(); + return updated ?? existing; + } + return existing; + } + } + + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Workspace insert returned no row (type=${input.type}, projectId=${project.id}, hostId=${host.id})`, + }); }), getFromHost: jwtProcedure @@ -285,7 +333,7 @@ export const v2WorkspaceRouter = { .input(z.object({ id: z.string().uuid() })) .mutation(async ({ ctx, input }) => { const workspace = await dbWs.query.v2Workspaces.findFirst({ - columns: { id: true, organizationId: true }, + columns: { id: true, organizationId: true, type: true }, where: eq(v2Workspaces.id, input.id), }); if (!workspace) { @@ -298,6 +346,52 @@ export const v2WorkspaceRouter = { message: "Not a member of this organization", }); } + if (workspace.type === "main") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: MAIN_WORKSPACE_DELETE_MESSAGE, + }); + } + await dbWs.delete(v2Workspaces).where(eq(v2Workspaces.id, workspace.id)); + return { success: true, alreadyGone: false as const }; + }), + + // Main workspaces are not normal delete targets. This endpoint is reserved + // for host project removal, where the repo-root workspace must be detached + // from this host before the local project row disappears. + deleteMainForHost: jwtProcedure + .input(z.object({ id: z.string().uuid(), projectId: z.string().uuid() })) + .mutation(async ({ ctx, input }) => { + const workspace = await dbWs.query.v2Workspaces.findFirst({ + columns: { + id: true, + organizationId: true, + projectId: true, + type: true, + }, + where: eq(v2Workspaces.id, input.id), + }); + if (!workspace) { + return { success: true, alreadyGone: true as const }; + } + if (!ctx.organizationIds.includes(workspace.organizationId)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Not a member of this organization", + }); + } + if (workspace.projectId !== input.projectId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Workspace does not belong to this project", + }); + } + if (workspace.type !== "main") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Workspace is not a main workspace", + }); + } await dbWs.delete(v2Workspaces).where(eq(v2Workspaces.id, workspace.id)); return { success: true, alreadyGone: false as const }; }),