diff --git a/apps/desktop/src/lib/trpc/routers/auth/index.ts b/apps/desktop/src/lib/trpc/routers/auth/index.ts index 8259f638d18..d622d9425b3 100644 --- a/apps/desktop/src/lib/trpc/routers/auth/index.ts +++ b/apps/desktop/src/lib/trpc/routers/auth/index.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import { AUTH_PROVIDERS } from "@superset/shared/constants"; -import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { getHostId, getHostName } from "@superset/shared/host-info"; import { observable } from "@trpc/server/observable"; import { shell } from "electron"; import { env } from "main/env.main"; @@ -23,8 +23,8 @@ export const createAuthRouter = () => { getStoredToken: publicProcedure.query(() => loadToken()), getDeviceInfo: publicProcedure.query(() => ({ - deviceId: getHashedDeviceId(), - deviceName: getDeviceName(), + deviceId: getHostId(), + deviceName: getHostName(), })), persistToken: publicProcedure diff --git a/apps/desktop/src/lib/trpc/routers/auth/utils/crypto-storage.ts b/apps/desktop/src/lib/trpc/routers/auth/utils/crypto-storage.ts index 1bf8fc3df3b..9b2ec3ae2ad 100644 --- a/apps/desktop/src/lib/trpc/routers/auth/utils/crypto-storage.ts +++ b/apps/desktop/src/lib/trpc/routers/auth/utils/crypto-storage.ts @@ -4,7 +4,7 @@ import { randomBytes, scryptSync, } from "node:crypto"; -import { getMachineId } from "@superset/shared/device-info"; +import { getMachineId } from "@superset/shared/host-info"; const ALGORITHM = "aes-256-gcm"; const KEY_LENGTH = 32; diff --git a/apps/desktop/src/main/lib/host-service-coordinator.ts b/apps/desktop/src/main/lib/host-service-coordinator.ts index 9e28de06ca6..3f0d406eaef 100644 --- a/apps/desktop/src/main/lib/host-service-coordinator.ts +++ b/apps/desktop/src/main/lib/host-service-coordinator.ts @@ -4,7 +4,7 @@ import { EventEmitter } from "node:events"; import * as fs from "node:fs"; import path from "node:path"; import { settings } from "@superset/local-db"; -import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { getHostId, getHostName } from "@superset/shared/host-info"; import { app } from "electron"; import { env } from "main/env.main"; import semver from "semver"; @@ -35,9 +35,12 @@ import { HOOK_PROTOCOL_VERSION } from "./terminal/env"; * which is how we prevent the renderer from talking to a stale host-service * that's missing newly-added procedures/params. * + * 0.3.0: host-service registers via cloud `host.ensure` (was + * `device.ensureV2Host`); v2_hosts/v2_users_hosts/v2_workspaces use + * machineId text instead of uuid surrogates. * 0.2.0: `workspaceCreation.adopt` gained optional `worktreePath`. */ -const MIN_HOST_SERVICE_VERSION = "0.2.0"; +const MIN_HOST_SERVICE_VERSION = "0.3.0"; export type HostServiceStatus = "starting" | "running" | "stopped"; @@ -75,7 +78,7 @@ export class HostServiceCoordinator extends EventEmitter { ReturnType >(); private scriptPath = path.join(__dirname, "host-service.js"); - private machineId = getHashedDeviceId(); + private machineId = getHostId(); private devReloadWatcher: fs.FSWatcher | null = null; async start( @@ -461,8 +464,8 @@ export class HostServiceCoordinator extends EventEmitter { ...(process.env as Record), ELECTRON_RUN_AS_NODE: "1", ORGANIZATION_ID: organizationId, - DEVICE_CLIENT_ID: getHashedDeviceId(), - DEVICE_NAME: getDeviceName(), + HOST_CLIENT_ID: getHostId(), + HOST_NAME: getHostName(), HOST_SERVICE_SECRET: secret, HOST_SERVICE_PORT: String(port), HOST_MANIFEST_DIR: organizationDir, diff --git a/apps/desktop/src/renderer/hooks/host-service/useHostTargetUrl/index.ts b/apps/desktop/src/renderer/hooks/host-service/useHostTargetUrl/index.ts new file mode 100644 index 00000000000..c6e517c7d34 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/host-service/useHostTargetUrl/index.ts @@ -0,0 +1 @@ +export { useHostTargetUrl } from "./useHostTargetUrl"; diff --git a/apps/desktop/src/renderer/hooks/host-service/useHostTargetUrl/useHostTargetUrl.ts b/apps/desktop/src/renderer/hooks/host-service/useHostTargetUrl/useHostTargetUrl.ts new file mode 100644 index 00000000000..a3edd0ee1a0 --- /dev/null +++ b/apps/desktop/src/renderer/hooks/host-service/useHostTargetUrl/useHostTargetUrl.ts @@ -0,0 +1,25 @@ +import { buildHostRoutingKey } from "@superset/shared/host-routing"; +import { useMemo } from "react"; +import { env } from "renderer/env.renderer"; +import { authClient } from "renderer/lib/auth-client"; +import type { WorkspaceHostTarget } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/types"; +import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; + +export function useHostTargetUrl( + hostTarget: WorkspaceHostTarget | null | undefined, +): string | null { + const { activeHostUrl } = useLocalHostService(); + const { data: session } = authClient.useSession(); + const activeOrganizationId = session?.session?.activeOrganizationId ?? null; + + return useMemo(() => { + if (!hostTarget) return null; + if (hostTarget.kind === "local") return activeHostUrl; + if (!activeOrganizationId) return null; + const routingKey = buildHostRoutingKey( + activeOrganizationId, + hostTarget.hostId, + ); + return `${env.RELAY_URL}/hosts/${routingKey}`; + }, [hostTarget, activeOrganizationId, activeHostUrl]); +} diff --git a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts index a9539f56cba..bd957947f07 100644 --- a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts +++ b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts @@ -1,3 +1,4 @@ +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { useMemo } from "react"; @@ -18,10 +19,11 @@ export function useWorkspaceHostUrl(workspaceId: string | null): string | null { q .from({ workspaces: collections.v2Workspaces }) .leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => - eq(workspaces.hostId, hosts.id), + eq(workspaces.hostId, hosts.machineId), ) .where(({ workspaces }) => eq(workspaces.id, workspaceId ?? "")) .select(({ workspaces, hosts }) => ({ + organizationId: workspaces.organizationId, hostId: workspaces.hostId, hostMachineId: hosts?.machineId ?? null, })), @@ -33,6 +35,7 @@ export function useWorkspaceHostUrl(workspaceId: string | null): string | null { return useMemo(() => { if (!match) return null; if (match.hostMachineId === machineId) return activeHostUrl; - return `${env.RELAY_URL}/hosts/${match.hostId}`; + const routingKey = buildHostRoutingKey(match.organizationId, match.hostId); + return `${env.RELAY_URL}/hosts/${routingKey}`; }, [match, machineId, activeHostUrl]); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useProjectFileSearch/useProjectFileSearch.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useProjectFileSearch/useProjectFileSearch.ts index 0052c1df0ad..116930ee6be 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useProjectFileSearch/useProjectFileSearch.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/hooks/useProjectFileSearch/useProjectFileSearch.ts @@ -1,9 +1,8 @@ import { useCallback } from "react"; import type { FileMentionSearchFn } from "renderer/components/MarkdownEditor/components/FileMention"; -import { env } from "renderer/env.renderer"; +import { useHostTargetUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import type { WorkspaceHostTarget } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/types"; -import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; const SEARCH_LIMIT = 15; @@ -14,12 +13,7 @@ export function useProjectFileSearch({ hostTarget: WorkspaceHostTarget; projectId: string | null; }): FileMentionSearchFn | undefined { - const { activeHostUrl } = useLocalHostService(); - - const hostUrl = - hostTarget.kind === "local" - ? activeHostUrl - : `${env.RELAY_URL}/hosts/${hostTarget.hostId}`; + const hostUrl = useHostTargetUrl(hostTarget); return useCallback( async (query) => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx index 5696520e8ff..a395f53eb3d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx @@ -136,7 +136,7 @@ function AutomationsPage() { (q) => q .from({ h: collections.v2Hosts }) - .select(({ h }) => ({ id: h.id, name: h.name })), + .select(({ h }) => ({ machineId: h.machineId, name: h.name })), [collections.v2Hosts], ); @@ -167,7 +167,10 @@ function AutomationsPage() { const hostsById = useMemo( () => new Map( - (hostRows as Pick[]).map((h) => [h.id, h]), + (hostRows as Pick[]).map((h) => [ + h.machineId, + h, + ]), ), [hostRows], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts index 5c16040b339..117817de03a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts @@ -154,8 +154,16 @@ describe("deriveHostPortQueryTargets", () => { const targets = deriveHostPortQueryTargets({ activeHostUrl: "http://127.0.0.1:4567", hosts: [ - { id: "remote-host", isOnline: true, machineId: "remote-machine" }, - { id: "local-host", isOnline: true, machineId: "local-machine" }, + { + organizationId: "org-1", + machineId: "remote-machine", + isOnline: true, + }, + { + organizationId: "org-1", + machineId: "local-machine", + isOnline: true, + }, ], machineId: "local-machine", relayUrl: "https://relay.example.com", @@ -163,19 +171,19 @@ describe("deriveHostPortQueryTargets", () => { { id: "workspace-b", name: "Workspace B", - hostId: "local-host", + hostId: "local-machine", hostMachineId: "local-machine", }, { id: "workspace-a", name: "Workspace A", - hostId: "local-host", + hostId: "local-machine", hostMachineId: "local-machine", }, { id: "workspace-c", name: "Workspace C", - hostId: "remote-host", + hostId: "remote-machine", hostMachineId: "remote-machine", }, ], @@ -183,13 +191,13 @@ describe("deriveHostPortQueryTargets", () => { expect(targets).toEqual([ { - id: "remote-host", + machineId: "remote-machine", hostType: "remote-device", - hostUrl: "https://relay.example.com/hosts/remote-host", + hostUrl: "https://relay.example.com/hosts/org-1:remote-machine", workspaceIds: ["workspace-c"], }, { - id: "local-host", + machineId: "local-machine", hostType: "local-device", hostUrl: "http://127.0.0.1:4567", workspaceIds: ["workspace-a", "workspace-b"], @@ -201,8 +209,16 @@ describe("deriveHostPortQueryTargets", () => { const targets = deriveHostPortQueryTargets({ activeHostUrl: null, hosts: [ - { id: "offline-host", isOnline: false, machineId: "remote-machine" }, - { id: "local-host", isOnline: true, machineId: "local-machine" }, + { + organizationId: "org-1", + machineId: "remote-machine", + isOnline: false, + }, + { + organizationId: "org-1", + machineId: "local-machine", + isOnline: true, + }, ], machineId: "local-machine", relayUrl: "https://relay.example.com", @@ -210,13 +226,13 @@ describe("deriveHostPortQueryTargets", () => { { id: "workspace-remote", name: "Remote", - hostId: "offline-host", + hostId: "remote-machine", hostMachineId: "remote-machine", }, { id: "workspace-local", name: "Local", - hostId: "local-host", + hostId: "local-machine", hostMachineId: "local-machine", }, ], diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts index 2af64cf4899..0992c72266d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts @@ -41,9 +41,9 @@ export function useDashboardSidebarPortsData(): { const { data: hosts = [] } = useLiveQuery( (q) => q.from({ hosts: collections.v2Hosts }).select(({ hosts }) => ({ - id: hosts.id, - isOnline: hosts.isOnline, + organizationId: hosts.organizationId, machineId: hosts.machineId, + isOnline: hosts.isOnline, })), [collections], ); @@ -53,7 +53,7 @@ export function useDashboardSidebarPortsData(): { q .from({ workspaces: collections.v2Workspaces }) .leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => - eq(workspaces.hostId, hosts.id), + eq(workspaces.hostId, hosts.machineId), ) .select(({ workspaces, hosts }) => ({ id: workspaces.id, @@ -86,7 +86,7 @@ export function useDashboardSidebarPortsData(): { workspaceIds: host.workspaceIds, }); return { - hostId: host.id, + hostId: host.machineId, hostType: host.hostType, hostUrl: host.hostUrl, ports, @@ -110,7 +110,7 @@ export function useDashboardSidebarPortsData(): { getHostPortsQueryKey(host), (result) => applyPortEventsToHostPortsResult(result, events, { - hostId: host.id, + hostId: host.machineId, hostType: host.hostType, hostUrl: host.hostUrl, }), @@ -175,7 +175,7 @@ export function useDashboardSidebarPortsData(): { if (!host) return []; return [ { - hostId: host.id, + hostId: host.machineId, hostType: host.hostType, message: query.error instanceof Error diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts index 3fbf9df26d8..3bfffbc160a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts @@ -1,3 +1,4 @@ +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import type { PortChangedPayload } from "@superset/workspace-client"; import type { DetectedPort } from "shared/types"; import type { DashboardSidebarWorkspaceHostType } from "../../../../types"; @@ -38,16 +39,16 @@ type HostPortsMetadata = Pick< >; export interface HostPortsQueryTarget { - id: string; + machineId: string; hostType: DashboardSidebarWorkspaceHostType; hostUrl: string; workspaceIds: string[]; } export interface DashboardSidebarHostRow { - id: string; + organizationId: string; + machineId: string; isOnline: boolean; - machineId: string | null | undefined; } export interface DashboardSidebarWorkspaceRow { @@ -62,7 +63,7 @@ export function getHostPortsQueryKey(host: HostPortsQueryTarget) { "host-service", "ports", "getAll", - host.id, + host.machineId, host.hostUrl, host.workspaceIds, ] as const; @@ -139,18 +140,20 @@ export function deriveHostPortQueryTargets({ } return hosts.flatMap((host) => { - const workspaceIds = workspaceIdsByHostId.get(host.id); + const workspaceIds = workspaceIdsByHostId.get(host.machineId); if (!workspaceIds || workspaceIds.length === 0) return []; const isLocal = host.machineId === machineId; if (!isLocal && !host.isOnline) return []; - const hostUrl = isLocal ? activeHostUrl : `${relayUrl}/hosts/${host.id}`; + const hostUrl = isLocal + ? activeHostUrl + : `${relayUrl}/hosts/${buildHostRoutingKey(host.organizationId, host.machineId)}`; if (!hostUrl) return []; return [ { - id: host.id, + machineId: host.machineId, hostType: isLocal ? ("local-device" as const) : ("remote-device" as const), 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 a6ad62f68ef..1bbe24c88ce 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 @@ -233,7 +233,7 @@ export function useDashboardSidebarData() { eq(sidebarWorkspaces.workspaceId, workspaces.id), ) .leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => - eq(workspaces.hostId, hosts.id), + eq(workspaces.hostId, hosts.machineId), ) .orderBy( ({ sidebarWorkspaces }) => sidebarWorkspaces.sidebarState.tabOrder, @@ -272,7 +272,7 @@ export function useDashboardSidebarData() { q .from({ workspaces: collections.v2Workspaces }) .leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => - eq(workspaces.hostId, hosts.id), + eq(workspaces.hostId, hosts.machineId), ) .where(({ workspaces }) => eq(workspaces.type, "main")) .select(({ workspaces, hosts }) => ({ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx index eab12edd4d6..161451163d2 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx @@ -21,7 +21,7 @@ export function V2WorkspaceOpenInButton({ q .from({ workspaces: collections.v2Workspaces }) .leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => - eq(workspaces.hostId, hosts.id), + eq(workspaces.hostId, hosts.machineId), ) .where(({ workspaces }) => eq(workspaces.id, workspaceId)) .select(({ workspaces, hosts }) => ({ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/dispatchForkLaunch.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/dispatchForkLaunch.ts index 8fa665f02e1..fe1a91848cc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/dispatchForkLaunch.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/dispatchForkLaunch.ts @@ -1,4 +1,5 @@ import type { ResolvedAgentConfig } from "@superset/shared/agent-settings"; +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { toast } from "@superset/ui/sonner"; import { env } from "renderer/env.renderer"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; @@ -27,6 +28,7 @@ export interface DispatchForkLaunchInputs { loadedAttachments: LoadedAttachment[] | undefined; agentConfigs: ResolvedAgentConfig[]; activeHostUrl: string | null; + activeOrganizationId: string | null; /** * Pre-resolved PR content from the pr-checkout flow. Threaded into * `buildForkAgentLaunch` so the `fetchPullRequest` resolver skips a @@ -55,6 +57,7 @@ export async function dispatchForkLaunch({ loadedAttachments, agentConfigs, activeHostUrl, + activeOrganizationId, resolvedPr, onApplyToRow, }: DispatchForkLaunchInputs): Promise { @@ -65,7 +68,11 @@ export async function dispatchForkLaunch({ agentConfigCount: agentConfigs.length, }); - const hostUrl = resolveHostUrl(pending.hostTarget, activeHostUrl); + const hostUrl = resolveHostUrl( + pending.hostTarget, + activeHostUrl, + activeOrganizationId, + ); const hostClient = hostUrl ? getHostServiceClientByUrl(hostUrl) : undefined; let build: Awaited>; @@ -159,9 +166,15 @@ export async function dispatchForkLaunch({ function resolveHostUrl( hostTarget: PendingWorkspaceRow["hostTarget"], activeHostUrl: string | null, + activeOrganizationId: string | null, ): string | null { if (hostTarget.kind === "local") return activeHostUrl; - return `${env.RELAY_URL}/hosts/${hostTarget.hostId}`; + if (!activeOrganizationId) return null; + const routingKey = buildHostRoutingKey( + activeOrganizationId, + hostTarget.hostId, + ); + return `${env.RELAY_URL}/hosts/${routingKey}`; } async function writeAttachmentsToWorktree({ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx index e928d439695..af52727288d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx @@ -6,7 +6,8 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useCallback, useEffect, useRef, useState } from "react"; import { GoGitBranch } from "react-icons/go"; import { HiCheck, HiExclamationTriangle } from "react-icons/hi2"; -import { env } from "renderer/env.renderer"; +import { useHostTargetUrl } from "renderer/hooks/host-service/useHostTargetUrl"; +import { authClient } from "renderer/lib/auth-client"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { formatRelativeTime } from "renderer/lib/formatRelativeTime"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; @@ -58,6 +59,9 @@ function useFireIntent(pendingId: string, pending: PendingWorkspaceRow | null) { const adoptWorktree = useAdoptWorktree(); const trpcUtils = electronTrpc.useUtils(); const { activeHostUrl } = useLocalHostService(); + const hostUrl = useHostTargetUrl(pending?.hostTarget ?? null); + const { data: session } = authClient.useSession(); + const activeOrganizationId = session?.session?.activeOrganizationId ?? null; const fire = useCallback(async () => { if (!pending) return; @@ -112,10 +116,6 @@ function useFireIntent(pendingId: string, pending: PendingWorkspaceRow | null) { if (!pending.linkedPR) { throw new Error("pr-checkout intent requires a linkedPR"); } - const hostUrl = - pending.hostTarget.kind === "local" - ? activeHostUrl - : `${env.RELAY_URL}/hosts/${pending.hostTarget.hostId}`; if (!hostUrl) { throw new Error("Host service not available"); } @@ -164,6 +164,7 @@ function useFireIntent(pendingId: string, pending: PendingWorkspaceRow | null) { loadedAttachments, agentConfigs, activeHostUrl, + activeOrganizationId, resolvedPr, onApplyToRow: (patch) => { collections.pendingWorkspaces.update(pendingId, (draft) => { @@ -201,6 +202,8 @@ function useFireIntent(pendingId: string, pending: PendingWorkspaceRow | null) { pendingId, trpcUtils, activeHostUrl, + activeOrganizationId, + hostUrl, ]); return fire; @@ -210,7 +213,6 @@ function PendingWorkspacePage() { const { pendingId } = Route.useParams(); const navigate = useNavigate(); const collections = useCollections(); - const { activeHostUrl } = useLocalHostService(); const { ensureWorkspaceInSidebar } = useDashboardSidebarState(); const navigatedRef = useRef(false); const firedRef = useRef(false); @@ -267,11 +269,7 @@ function PendingWorkspacePage() { // adopt is fast and doesn't instrument progress). const intentHasProgress = pending?.intent === "fork" || pending?.intent === "checkout"; - const hostUrl = !pending - ? activeHostUrl - : pending.hostTarget.kind === "local" - ? activeHostUrl - : `${env.RELAY_URL}/hosts/${pending.hostTarget.hostId}`; + const hostUrl = useHostTargetUrl(pending?.hostTarget ?? null); const { data: progress } = useQuery({ queryKey: ["workspaceCreation", "getProgress", pendingId, hostUrl], diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts index eb9058a466b..215154de0d5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts @@ -21,7 +21,7 @@ export function useOpenInExternalEditor(workspaceId: string) { q .from({ workspaces: collections.v2Workspaces }) .leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => - eq(workspaces.hostId, hosts.id), + eq(workspaces.hostId, hosts.machineId), ) .where(({ workspaces }) => eq(workspaces.id, workspaceId)) .select(({ workspaces, hosts }) => ({ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx index 392d4fd56de..5cd7cbebe16 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx @@ -1,3 +1,4 @@ +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router"; @@ -35,11 +36,12 @@ function V2WorkspaceLayout() { q .from({ v2Workspaces: collections.v2Workspaces }) .leftJoin({ hosts: collections.v2Hosts }, ({ v2Workspaces, hosts }) => - eq(v2Workspaces.hostId, hosts.id), + eq(v2Workspaces.hostId, hosts.machineId), ) .where(({ v2Workspaces }) => eq(v2Workspaces.id, workspaceId ?? "")) .select(({ v2Workspaces, hosts }) => ({ id: v2Workspaces.id, + organizationId: v2Workspaces.organizationId, hostId: v2Workspaces.hostId, hostMachineId: hosts?.machineId ?? null, projectId: v2Workspaces.projectId, @@ -54,7 +56,7 @@ function V2WorkspaceLayout() { ? null : isLocal ? activeHostUrl - : `${env.RELAY_URL}/hosts/${workspace.hostId}`; + : `${env.RELAY_URL}/hosts/${buildHostRoutingKey(workspace.organizationId, workspace.hostId)}`; const lastEnsuredWorkspaceIdRef = useRef(null); 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 3dcd326b852..e34b2d481d8 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 @@ -87,11 +87,11 @@ export function useAccessibleV2Workspaces( q .from({ workspaces: collections.v2Workspaces }) .innerJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => - eq(workspaces.hostId, hosts.id), + eq(workspaces.hostId, hosts.machineId), ) .innerJoin( { userHosts: collections.v2UsersHosts }, - ({ hosts, userHosts }) => eq(userHosts.hostId, hosts.id), + ({ hosts, userHosts }) => eq(userHosts.hostId, hosts.machineId), ) .innerJoin( { projects: collections.v2Projects }, @@ -135,7 +135,7 @@ export function useAccessibleV2Workspaces( createdByImage: creators?.image ?? null, projectId: projects.id, projectName: projects.name, - hostId: hosts.id, + hostId: hosts.machineId, hostName: hosts.name, hostMachineId: hosts.machineId, hostIsOnline: hosts.isOnline, diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx index 11494111d8d..bf488e7575b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx @@ -12,10 +12,9 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useQuery } from "@tanstack/react-query"; import type { ReactNode } from "react"; import { useId, useState } from "react"; -import { env } from "renderer/env.renderer"; +import { useHostTargetUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; -import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import { IssueIcon, type IssueState, @@ -54,17 +53,12 @@ export function GitHubIssueLinkCommand({ const [showClosed, setShowClosed] = useState(false); const showClosedId = useId(); const debouncedQuery = useDebouncedValue(searchQuery, 300); - const { activeHostUrl } = useLocalHostService(); + const hostUrl = useHostTargetUrl(hostTarget); const trimmedQuery = searchQuery.trim(); const debouncedTrimmed = debouncedQuery.trim(); const isPendingDebounce = trimmedQuery !== debouncedTrimmed; - const hostUrl = - hostTarget.kind === "local" - ? activeHostUrl - : `${env.RELAY_URL}/hosts/${hostTarget.hostId}`; - const { data, isFetching } = useQuery({ queryKey: [ "workspaceCreation", diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx index 4bfabc827e1..2603272f665 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx @@ -12,10 +12,9 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useQuery } from "@tanstack/react-query"; import type { ReactNode } from "react"; import { useId, useState } from "react"; -import { env } from "renderer/env.renderer"; +import { useHostTargetUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; -import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import { PRIcon, type PRState, @@ -55,17 +54,12 @@ export function PRLinkCommand({ const [showClosed, setShowClosed] = useState(false); const showClosedId = useId(); const debouncedQuery = useDebouncedValue(searchQuery, 300); - const { activeHostUrl } = useLocalHostService(); + const hostUrl = useHostTargetUrl(hostTarget); const trimmedQuery = searchQuery.trim(); const debouncedTrimmed = debouncedQuery.trim(); const isPendingDebounce = trimmedQuery !== debouncedTrimmed; - const hostUrl = - hostTarget.kind === "local" - ? activeHostUrl - : `${env.RELAY_URL}/hosts/${hostTarget.hostId}`; - const { data, isFetching } = useQuery({ queryKey: [ "workspaceCreation", diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useBranchPickerController/useBranchPickerController.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useBranchPickerController/useBranchPickerController.ts index fb0d1dac769..b3d54371850 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useBranchPickerController/useBranchPickerController.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/hooks/useBranchPickerController/useBranchPickerController.ts @@ -87,7 +87,7 @@ export function useBranchPickerController(args: UseBranchPickerControllerArgs) { const targetHostId = useMemo(() => { if (hostTarget.kind === "host") return hostTarget.hostId; if (!machineId || !allHosts) return null; - return allHosts.find((h) => h.machineId === machineId)?.id ?? null; + return allHosts.find((h) => h.machineId === machineId)?.machineId ?? null; }, [hostTarget, allHosts, machineId]); const workspaceByBranch = useMemo(() => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts index 378a00d5765..207ea812ebf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts @@ -15,7 +15,7 @@ export interface WorkspaceHostOption { interface UseWorkspaceHostOptionsResult { currentDeviceName: string | null; - /** v2_hosts.id for the current device (the one running this desktop app). */ + /** machineId of the current device (the one running this desktop app). */ localHostId: string | null; activeHostUrl: string | null; otherHosts: WorkspaceHostOption[]; @@ -36,7 +36,7 @@ export function useWorkspaceHostOptions(): UseWorkspaceHostOptionsResult { q .from({ userHosts: collections.v2UsersHosts }) .innerJoin({ hosts: collections.v2Hosts }, ({ userHosts, hosts }) => - eq(userHosts.hostId, hosts.id), + eq(userHosts.hostId, hosts.machineId), ) .where(({ userHosts, hosts }) => and( @@ -45,7 +45,6 @@ export function useWorkspaceHostOptions(): UseWorkspaceHostOptionsResult { ), ) .select(({ hosts }) => ({ - id: hosts.id, machineId: hosts.machineId, name: hosts.name, isOnline: hosts.isOnline, @@ -63,7 +62,7 @@ export function useWorkspaceHostOptions(): UseWorkspaceHostOptionsResult { accessibleHosts .filter((host) => host.machineId !== machineId) .map((host) => ({ - id: host.id, + id: host.machineId, name: host.name, isOnline: host.isOnline ?? false, })) @@ -73,7 +72,7 @@ export function useWorkspaceHostOptions(): UseWorkspaceHostOptionsResult { return { currentDeviceName: localHost?.name ?? null, - localHostId: localHost?.id ?? null, + localHostId: localHost?.machineId ?? null, activeHostUrl, otherHosts, }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts index fb089dbc211..96c4dfa879b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts @@ -2,9 +2,8 @@ import type { AppRouter } from "@superset/host-service"; import { useInfiniteQuery } from "@tanstack/react-query"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { useMemo } from "react"; -import { env } from "renderer/env.renderer"; +import { useHostTargetUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; -import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import type { WorkspaceHostTarget } from "../../components/DevicePicker"; type SearchBranchesInput = @@ -29,11 +28,7 @@ export function useBranchContext( query: string, filter: BranchFilter = "branch", ) { - const { activeHostUrl } = useLocalHostService(); - const hostUrl = - hostTarget.kind === "local" - ? activeHostUrl - : `${env.RELAY_URL}/hosts/${hostTarget.hostId}`; + const hostUrl = useHostTargetUrl(hostTarget); const q = useInfiniteQuery({ queryKey: [ diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/hooks/useSelectedHostProjectIds/useSelectedHostProjectIds.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/hooks/useSelectedHostProjectIds/useSelectedHostProjectIds.ts index 0651c64dff1..e180421ed07 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/hooks/useSelectedHostProjectIds/useSelectedHostProjectIds.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/hooks/useSelectedHostProjectIds/useSelectedHostProjectIds.ts @@ -1,7 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { env } from "renderer/env.renderer"; +import { useHostTargetUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; -import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import type { WorkspaceHostTarget } from "../../../DashboardNewWorkspaceForm/components/DevicePicker/types"; /** @@ -11,11 +10,7 @@ import type { WorkspaceHostTarget } from "../../../DashboardNewWorkspaceForm/com export function useSelectedHostProjectIds( hostTarget: WorkspaceHostTarget, ): Set | null { - const { activeHostUrl } = useLocalHostService(); - const hostUrl = - hostTarget.kind === "local" - ? activeHostUrl - : `${env.RELAY_URL}/hosts/${hostTarget.hostId}`; + const hostUrl = useHostTargetUrl(hostTarget); const { data } = useQuery({ queryKey: ["project", "list", hostUrl], diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useAdoptWorktree/useAdoptWorktree.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useAdoptWorktree/useAdoptWorktree.ts index 9c1ff0f32d0..b3415730c9e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useAdoptWorktree/useAdoptWorktree.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useAdoptWorktree/useAdoptWorktree.ts @@ -1,5 +1,7 @@ +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { useCallback } from "react"; import { env } from "renderer/env.renderer"; +import { authClient } from "renderer/lib/auth-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import type { WorkspaceHostTarget } from "../../components/DashboardNewWorkspaceForm/components/DevicePicker"; @@ -17,13 +19,17 @@ export interface AdoptWorktreeInput { */ export function useAdoptWorktree() { const { activeHostUrl } = useLocalHostService(); + const { data: session } = authClient.useSession(); + const activeOrganizationId = session?.session?.activeOrganizationId ?? null; return useCallback( async (input: AdoptWorktreeInput) => { const hostUrl = input.hostTarget.kind === "local" ? activeHostUrl - : `${env.RELAY_URL}/hosts/${input.hostTarget.hostId}`; + : activeOrganizationId + ? `${env.RELAY_URL}/hosts/${buildHostRoutingKey(activeOrganizationId, input.hostTarget.hostId)}` + : null; if (!hostUrl) throw new Error("Host service not available"); const client = getHostServiceClientByUrl(hostUrl); return client.workspaceCreation.adopt.mutate({ @@ -32,6 +38,6 @@ export function useAdoptWorktree() { branch: input.branch, }); }, - [activeHostUrl], + [activeHostUrl, activeOrganizationId], ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCheckoutDashboardWorkspace/useCheckoutDashboardWorkspace.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCheckoutDashboardWorkspace/useCheckoutDashboardWorkspace.ts index 30a9bf145da..f0718e1f921 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCheckoutDashboardWorkspace/useCheckoutDashboardWorkspace.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCheckoutDashboardWorkspace/useCheckoutDashboardWorkspace.ts @@ -1,5 +1,7 @@ +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { useCallback } from "react"; import { env } from "renderer/env.renderer"; +import { authClient } from "renderer/lib/auth-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import type { WorkspaceHostTarget } from "../../components/DashboardNewWorkspaceForm/components/DevicePicker"; @@ -52,13 +54,17 @@ export interface CheckoutWorkspaceInput { */ export function useCheckoutDashboardWorkspace() { const { activeHostUrl } = useLocalHostService(); + const { data: session } = authClient.useSession(); + const activeOrganizationId = session?.session?.activeOrganizationId ?? null; return useCallback( async (input: CheckoutWorkspaceInput) => { const hostUrl = input.hostTarget.kind === "local" ? activeHostUrl - : `${env.RELAY_URL}/hosts/${input.hostTarget.hostId}`; + : activeOrganizationId + ? `${env.RELAY_URL}/hosts/${buildHostRoutingKey(activeOrganizationId, input.hostTarget.hostId)}` + : null; if (!hostUrl) { throw new Error("Host service not available"); @@ -76,6 +82,6 @@ export function useCheckoutDashboardWorkspace() { linkedContext: input.linkedContext, }); }, - [activeHostUrl], + [activeHostUrl, activeOrganizationId], ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts index d5ba22a2d5c..ed890338300 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts @@ -1,5 +1,7 @@ +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { useCallback } from "react"; import { env } from "renderer/env.renderer"; +import { authClient } from "renderer/lib/auth-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import type { WorkspaceHostTarget } from "../../components/DashboardNewWorkspaceForm/components/DevicePicker"; @@ -42,13 +44,17 @@ export interface CreateWorkspaceInput { */ export function useCreateDashboardWorkspace() { const { activeHostUrl } = useLocalHostService(); + const { data: session } = authClient.useSession(); + const activeOrganizationId = session?.session?.activeOrganizationId ?? null; return useCallback( async (input: CreateWorkspaceInput) => { const hostUrl = input.hostTarget.kind === "local" ? activeHostUrl - : `${env.RELAY_URL}/hosts/${input.hostTarget.hostId}`; + : activeOrganizationId + ? `${env.RELAY_URL}/hosts/${buildHostRoutingKey(activeOrganizationId, input.hostTarget.hostId)}` + : null; if (!hostUrl) { throw new Error("Host service not available"); @@ -64,6 +70,6 @@ export function useCreateDashboardWorkspace() { linkedContext: input.linkedContext, }); }, - [activeHostUrl], + [activeHostUrl, activeOrganizationId], ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/GlobalTerminalLifecycle/hooks/useGlobalTerminalLifecycle/useGlobalTerminalLifecycle.ts b/apps/desktop/src/renderer/routes/_authenticated/components/GlobalTerminalLifecycle/hooks/useGlobalTerminalLifecycle/useGlobalTerminalLifecycle.ts index ee39bff4278..3491bfdd032 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/GlobalTerminalLifecycle/hooks/useGlobalTerminalLifecycle/useGlobalTerminalLifecycle.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/GlobalTerminalLifecycle/hooks/useGlobalTerminalLifecycle/useGlobalTerminalLifecycle.ts @@ -1,4 +1,5 @@ import type { WorkspaceState } from "@superset/panes"; +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { useEffect, useMemo, useRef } from "react"; @@ -133,10 +134,11 @@ export function useGlobalTerminalLifecycle() { query .from({ v2Workspaces: collections.v2Workspaces }) .leftJoin({ hosts: collections.v2Hosts }, ({ v2Workspaces, hosts }) => - eq(v2Workspaces.hostId, hosts.id), + eq(v2Workspaces.hostId, hosts.machineId), ) .select(({ v2Workspaces, hosts }) => ({ workspaceId: v2Workspaces.id, + organizationId: v2Workspaces.organizationId, hostId: v2Workspaces.hostId, hostMachineId: hosts?.machineId ?? null, })), @@ -156,7 +158,7 @@ export function useGlobalTerminalLifecycle() { if (workspace.hostId) { urls.set( workspace.workspaceId, - `${env.RELAY_URL}/hosts/${workspace.hostId}`, + `${env.RELAY_URL}/hosts/${buildHostRoutingKey(workspace.organizationId, workspace.hostId)}`, ); } } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/V2NotificationController/V2NotificationController.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/V2NotificationController/V2NotificationController.tsx index 5a5f21f9219..0d6ed75437b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/V2NotificationController/V2NotificationController.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/V2NotificationController/V2NotificationController.tsx @@ -1,4 +1,5 @@ import type { WorkspaceState } from "@superset/panes"; +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { useMemo } from "react"; @@ -13,6 +14,7 @@ import { interface WorkspaceHostRow { workspaceId: string; + organizationId: string; hostId: string; hostMachineId: string | null | undefined; } @@ -40,10 +42,12 @@ export function V2NotificationController() { .from({ v2Workspaces: collections.v2Workspaces }) .leftJoin( { v2Hosts: collections.v2Hosts }, - ({ v2Workspaces, v2Hosts }) => eq(v2Workspaces.hostId, v2Hosts.id), + ({ v2Workspaces, v2Hosts }) => + eq(v2Workspaces.hostId, v2Hosts.machineId), ) .select(({ v2Workspaces, v2Hosts }) => ({ workspaceId: v2Workspaces.id, + organizationId: v2Workspaces.organizationId, hostId: v2Workspaces.hostId, hostMachineId: v2Hosts?.machineId ?? null, })), @@ -107,6 +111,7 @@ function groupWorkspacesByHostUrl({ for (const workspace of workspaceHosts) { const hostUrl = getHostUrlForWorkspace({ + organizationId: workspace.organizationId, hostId: workspace.hostId, hostMachineId: workspace.hostMachineId, machineId, @@ -129,11 +134,13 @@ function groupWorkspacesByHostUrl({ } function getHostUrlForWorkspace({ + organizationId, hostId, hostMachineId, machineId, activeHostUrl, }: { + organizationId: string; hostId: string; hostMachineId: string | null | undefined; machineId: string | null; @@ -142,5 +149,5 @@ function getHostUrlForWorkspace({ if (hostMachineId && machineId && hostMachineId === machineId) { return activeHostUrl; } - return `${env.RELAY_URL}/hosts/${hostId}`; + return `${env.RELAY_URL}/hosts/${buildHostRoutingKey(organizationId, hostId)}`; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/hooks/useOptimisticCollectionActions/useOptimisticCollectionActions.ts b/apps/desktop/src/renderer/routes/_authenticated/hooks/useOptimisticCollectionActions/useOptimisticCollectionActions.ts index fcbb5f6657f..1a762a7434e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/hooks/useOptimisticCollectionActions/useOptimisticCollectionActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/hooks/useOptimisticCollectionActions/useOptimisticCollectionActions.ts @@ -212,7 +212,6 @@ export function useOptimisticCollectionActions() { runUsersHostsMutation("Failed to add member", () => { const now = new Date(); return collections.v2UsersHosts.insert({ - id: crypto.randomUUID(), hostId: input.hostId, userId: input.userId, organizationId: input.organizationId, @@ -221,13 +220,13 @@ export function useOptimisticCollectionActions() { updatedAt: now, }); }), - removeMember: (rowId: string) => + removeMember: (rowKey: string) => runUsersHostsMutation("Failed to remove member", () => - collections.v2UsersHosts.delete(rowId), + collections.v2UsersHosts.delete(rowKey), ), - setMemberRole: (rowId: string, role: V2UsersHostRole) => + setMemberRole: (rowKey: string, role: V2UsersHostRole) => runUsersHostsMutation("Failed to update role", () => - collections.v2UsersHosts.update(rowId, (draft) => { + collections.v2UsersHosts.update(rowKey, (draft) => { draft.role = role; }), ), diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts index 03a9b45f119..aba18dad1a0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts @@ -11,7 +11,6 @@ import type { SelectMember, SelectOrganization, SelectProject, - SelectSessionHost, SelectSubscription, SelectTask, SelectTaskStatus, @@ -101,7 +100,6 @@ export interface OrgCollections { subscriptions: Collection; apiKeys: Collection; chatSessions: Collection; - sessionHosts: Collection; githubRepositories: Collection; githubPullRequests: Collection; automations: Collection; @@ -302,7 +300,9 @@ function createOrgCollections(organizationId: string): OrgCollections { headers: electricHeaders, columnMapper, }, - getKey: (item) => item.id, + // Composite PK on (organization_id, machine_id); within an + // org-scoped collection, machineId alone is unique. + getKey: (item) => item.machineId, }), ); @@ -318,7 +318,9 @@ function createOrgCollections(organizationId: string): OrgCollections { headers: electricHeaders, columnMapper, }, - getKey: (item) => item.id, + // Composite PK on (organization_id, user_id, machine_id); within + // an org-scoped collection, (user_id, machine_id) is unique. + getKey: (item) => `${item.userId}:${item.machineId}`, }), ); @@ -334,11 +336,10 @@ function createOrgCollections(organizationId: string): OrgCollections { headers: electricHeaders, columnMapper, }, - getKey: (item) => item.id, + getKey: (item) => `${item.userId}:${item.hostId}`, onInsert: async ({ transaction }) => { const item = transaction.mutations[0].modified; const result = await apiClient.v2Host.addMember.mutate({ - id: item.id, hostId: item.hostId, userId: item.userId, role: item.role, @@ -557,22 +558,6 @@ function createOrgCollections(organizationId: string): OrgCollections { }), ); - const sessionHosts = createIndexedCollection( - electricCollectionOptions({ - id: `session_hosts-${organizationId}`, - shapeOptions: { - url: electricUrl, - params: { - table: "session_hosts", - organizationId, - }, - headers: electricHeaders, - columnMapper, - }, - getKey: (item) => item.id, - }), - ); - const githubRepositories = createIndexedCollection( electricCollectionOptions({ id: `github_repositories-${organizationId}`, @@ -712,7 +697,6 @@ function createOrgCollections(organizationId: string): OrgCollections { subscriptions, apiKeys, chatSessions, - sessionHosts, githubRepositories, githubPullRequests, automations, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx index 6253dc77f0c..cfc6d6b81c4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx @@ -38,7 +38,7 @@ export function HostSettings({ hostId }: HostSettingsProps) { (q) => q .from({ hosts: collections.v2Hosts }) - .where(({ hosts }) => eq(hosts.id, hostId)) + .where(({ hosts }) => eq(hosts.machineId, hostId)) .select(({ hosts }) => ({ ...hosts })), [collections, hostId], ); @@ -85,7 +85,7 @@ export function HostSettings({ hostId }: HostSettingsProps) { .map((row) => { const u = userMap.get(row.userId); return { - usersHostsId: row.id, + usersHostsId: `${row.userId}:${row.hostId}`, userId: row.userId, role: row.role as "owner" | "member", name: u?.name ?? "Unknown user", diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx index 4b224d58fb4..c30081b1170 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx @@ -39,7 +39,7 @@ export function HostsSettingsSidebar({ eq(hosts.organizationId, activeOrganizationId ?? ""), ) .select(({ hosts }) => ({ - id: hosts.id, + id: hosts.machineId, name: hosts.name, machineId: hosts.machineId, isOnline: hosts.isOnline, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx index d83aac0f6f1..643c8e8adbd 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx @@ -28,7 +28,7 @@ function HostsIndexPage() { eq(hosts.organizationId, activeOrganizationId ?? ""), ) .select(({ hosts }) => ({ - id: hosts.id, + id: hosts.machineId, name: hosts.name, isOnline: hosts.isOnline, })), diff --git a/apps/electric-proxy/src/where.ts b/apps/electric-proxy/src/where.ts index 717a58c5460..12c31e66b20 100644 --- a/apps/electric-proxy/src/where.ts +++ b/apps/electric-proxy/src/where.ts @@ -11,7 +11,6 @@ import { members, organizations, projects, - sessionHosts, subscriptions, taskStatuses, tasks, @@ -129,9 +128,6 @@ export function buildWhereClause( case "chat_sessions": return build(chatSessions, chatSessions.organizationId, organizationId); - case "session_hosts": - return build(sessionHosts, sessionHosts.organizationId, organizationId); - case "github_repositories": return build( githubRepositories, diff --git a/apps/relay/src/access.ts b/apps/relay/src/access.ts index b80a75556bc..b8b0d793718 100644 --- a/apps/relay/src/access.ts +++ b/apps/relay/src/access.ts @@ -15,7 +15,7 @@ export async function checkHostAccess( try { const client = createApiClient(token); - const result = await client.device.checkHostAccess.query({ hostId }); + const result = await client.host.checkAccess.query({ hostId }); if (result.allowed) { allowedCache.set(key, true); } diff --git a/apps/relay/src/tunnel.ts b/apps/relay/src/tunnel.ts index e9b8593333c..9eeaac1ce00 100644 --- a/apps/relay/src/tunnel.ts +++ b/apps/relay/src/tunnel.ts @@ -62,7 +62,7 @@ export class TunnelManager { }, PING_INTERVAL_MS); void createApiClient(token) - .device.setHostOnline.mutate({ hostId, isOnline: true }) + .host.setOnline.mutate({ hostId, isOnline: true }) .catch(() => {}); console.log(`[relay] tunnel registered: ${hostId}`); } @@ -83,7 +83,7 @@ export class TunnelManager { } void createApiClient(tunnel.token) - .device.setHostOnline.mutate({ hostId, isOnline: false }) + .host.setOnline.mutate({ hostId, isOnline: false }) .catch(() => {}); this.tunnels.delete(hostId); console.log(`[relay] tunnel unregistered: ${hostId}`); diff --git a/packages/db/drizzle/0039_consolidate_host_client_machine_id.sql b/packages/db/drizzle/0039_consolidate_host_client_machine_id.sql new file mode 100644 index 00000000000..7eabd44b84f --- /dev/null +++ b/packages/db/drizzle/0039_consolidate_host_client_machine_id.sql @@ -0,0 +1,69 @@ +-- session_hosts is dead (no writes anywhere); drop with CASCADE. +ALTER TABLE "session_hosts" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint +DROP TABLE "session_hosts" CASCADE;--> statement-breakpoint + +-- Delete orphan FK rows in NOT NULL columns BEFORE the type change. +-- Once host_id is text, we can't easily distinguish orphans from valid uuids, +-- and the post-translation rows would still have stringified-UUID values that +-- can't satisfy the new composite FK to v2_hosts(organization_id, machine_id). +DELETE FROM "v2_users_hosts" WHERE "host_id" NOT IN (SELECT "id" FROM "v2_hosts");--> statement-breakpoint +DELETE FROM "v2_workspaces" WHERE "host_id" NOT IN (SELECT "id" FROM "v2_hosts");--> statement-breakpoint + +-- Drop old unique constraints (replaced by composite PKs below). +ALTER TABLE "v2_clients" DROP CONSTRAINT "v2_clients_org_user_machine_unique";--> statement-breakpoint +ALTER TABLE "v2_hosts" DROP CONSTRAINT "v2_hosts_org_machine_id_unique";--> statement-breakpoint +ALTER TABLE "v2_users_hosts" DROP CONSTRAINT "v2_users_hosts_org_user_host_unique";--> statement-breakpoint + +-- Drop old FK constraints (column type change requires no incoming FK). +ALTER TABLE "automation_runs" DROP CONSTRAINT "automation_runs_host_id_v2_hosts_id_fk";--> statement-breakpoint +ALTER TABLE "automations" DROP CONSTRAINT "automations_target_host_id_v2_hosts_id_fk";--> statement-breakpoint +ALTER TABLE "v2_users_hosts" DROP CONSTRAINT "v2_users_hosts_host_id_v2_hosts_id_fk";--> statement-breakpoint +ALTER TABLE "v2_workspaces" DROP CONSTRAINT "v2_workspaces_host_id_v2_hosts_id_fk";--> statement-breakpoint + +-- Drop the partial unique index on v2_workspaces (project_id, host_id) WHERE +-- type='main' added by 0038. Recreated below after host_id becomes text. +DROP INDEX "v2_workspaces_one_main_per_host";--> statement-breakpoint + +-- Cast FK columns uuid -> text. PostgreSQL has no implicit uuid->text cast, +-- so USING is required. We stringify the uuid here and translate to +-- machine_id in the UPDATEs below. +ALTER TABLE "automation_runs" ALTER COLUMN "host_id" SET DATA TYPE text USING "host_id"::text;--> statement-breakpoint +ALTER TABLE "automations" ALTER COLUMN "target_host_id" SET DATA TYPE text USING "target_host_id"::text;--> statement-breakpoint +ALTER TABLE "v2_users_hosts" ALTER COLUMN "host_id" SET DATA TYPE text USING "host_id"::text;--> statement-breakpoint +ALTER TABLE "v2_workspaces" ALTER COLUMN "host_id" SET DATA TYPE text USING "host_id"::text;--> statement-breakpoint + +-- Translate stringified UUIDs to machine_ids via UPDATE FROM v2_hosts. +-- v2_hosts.id still exists at this point (dropped further down); cast to text +-- to compare against the now-text host_id columns. +UPDATE "automation_runs" SET "host_id" = h."machine_id" FROM "v2_hosts" h WHERE "automation_runs"."host_id" = h."id"::text;--> statement-breakpoint +UPDATE "automations" SET "target_host_id" = h."machine_id" FROM "v2_hosts" h WHERE "automations"."target_host_id" = h."id"::text;--> statement-breakpoint +UPDATE "v2_users_hosts" SET "host_id" = h."machine_id" FROM "v2_hosts" h WHERE "v2_users_hosts"."host_id" = h."id"::text;--> statement-breakpoint +UPDATE "v2_workspaces" SET "host_id" = h."machine_id" FROM "v2_hosts" h WHERE "v2_workspaces"."host_id" = h."id"::text;--> statement-breakpoint + +-- For nullable columns, NULL out any rows that don't have a matching +-- (organization_id, machine_id) pair in v2_hosts. NOT EXISTS with the +-- composite check rather than a global machine_id IN (...) so the migration +-- doesn't rely on an implicit "automation_runs.organization_id always matches +-- the host's organization_id" invariant — if any cross-org row exists, the +-- translation step above would have written the wrong-org's machine_id and +-- the new composite FK would reject it. +UPDATE "automation_runs" SET "host_id" = NULL WHERE "host_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "v2_hosts" h WHERE h."machine_id" = "automation_runs"."host_id" AND h."organization_id" = "automation_runs"."organization_id");--> statement-breakpoint +UPDATE "automations" SET "target_host_id" = NULL WHERE "target_host_id" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "v2_hosts" h WHERE h."machine_id" = "automations"."target_host_id" AND h."organization_id" = "automations"."organization_id");--> statement-breakpoint + +-- Drop old uuid `id` columns. Implicitly drops the old PRIMARY KEY constraints, +-- freeing the tables to receive composite PKs below. +ALTER TABLE "v2_clients" DROP COLUMN "id";--> statement-breakpoint +ALTER TABLE "v2_hosts" DROP COLUMN "id";--> statement-breakpoint +ALTER TABLE "v2_users_hosts" DROP COLUMN "id";--> statement-breakpoint + +-- Add new composite PRIMARY KEYs. +ALTER TABLE "v2_clients" ADD CONSTRAINT "v2_clients_organization_id_user_id_machine_id_pk" PRIMARY KEY("organization_id","user_id","machine_id");--> statement-breakpoint +ALTER TABLE "v2_hosts" ADD CONSTRAINT "v2_hosts_organization_id_machine_id_pk" PRIMARY KEY("organization_id","machine_id");--> statement-breakpoint +ALTER TABLE "v2_users_hosts" ADD CONSTRAINT "v2_users_hosts_organization_id_user_id_host_id_pk" PRIMARY KEY("organization_id","user_id","host_id");--> statement-breakpoint + +-- Add new composite FOREIGN KEYs (now that v2_hosts has its composite PK). +ALTER TABLE "v2_users_hosts" ADD CONSTRAINT "v2_users_hosts_host_fk" FOREIGN KEY ("organization_id","host_id") REFERENCES "public"."v2_hosts"("organization_id","machine_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "v2_workspaces" ADD CONSTRAINT "v2_workspaces_host_fk" FOREIGN KEY ("organization_id","host_id") REFERENCES "public"."v2_hosts"("organization_id","machine_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint + +-- Recreate the partial unique index on v2_workspaces against the now-text host_id. +CREATE UNIQUE INDEX "v2_workspaces_one_main_per_host" ON "v2_workspaces" USING btree ("project_id","host_id") WHERE "v2_workspaces"."type" = 'main'; diff --git a/packages/db/drizzle/meta/0039_snapshot.json b/packages/db/drizzle/meta/0039_snapshot.json new file mode 100644 index 00000000000..4428ce94939 --- /dev/null +++ b/packages/db/drizzle/meta/0039_snapshot.json @@ -0,0 +1,5616 @@ +{ + "id": "93706e33-59b4-4ec5-9266-49a93063f608", + "prevId": "ef3bd128-1ac9-4166-a066-b1aa6811772a", + "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": "text", + "primaryKey": false, + "notNull": false + }, + "v2_workspace_id": { + "name": "v2_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_kind": { + "name": "session_kind", + "type": "automation_session_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "chat_session_id": { + "name": "chat_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "terminal_session_id": { + "name": "terminal_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "automation_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_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": "text", + "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_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.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": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "v2_client_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_clients_organization_id_idx": { + "name": "v2_clients_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_clients_user_id_idx": { + "name": "v2_clients_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_clients_organization_id_organizations_id_fk": { + "name": "v2_clients_organization_id_organizations_id_fk", + "tableFrom": "v2_clients", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_clients_user_id_users_id_fk": { + "name": "v2_clients_user_id_users_id_fk", + "tableFrom": "v2_clients", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "v2_clients_organization_id_user_id_machine_id_pk": { + "name": "v2_clients_organization_id_user_id_machine_id_pk", + "columns": [ + "organization_id", + "user_id", + "machine_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_hosts": { + "name": "v2_hosts", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "v2_hosts_organization_id_machine_id_pk": { + "name": "v2_hosts_organization_id_machine_id_pk", + "columns": [ + "organization_id", + "machine_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_projects": { + "name": "v2_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_clone_url": { + "name": "repo_clone_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_projects_organization_id_idx": { + "name": "v2_projects_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_projects_organization_id_organizations_id_fk": { + "name": "v2_projects_organization_id_organizations_id_fk", + "tableFrom": "v2_projects", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_projects_github_repository_id_github_repositories_id_fk": { + "name": "v2_projects_github_repository_id_github_repositories_id_fk", + "tableFrom": "v2_projects", + "tableTo": "github_repositories", + "columnsFrom": [ + "github_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "v2_projects_org_slug_unique": { + "name": "v2_projects_org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_users_hosts": { + "name": "v2_users_hosts", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "v2_users_host_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_users_hosts_organization_id_idx": { + "name": "v2_users_hosts_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_users_hosts_user_id_idx": { + "name": "v2_users_hosts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_users_hosts_host_id_idx": { + "name": "v2_users_hosts_host_id_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_users_hosts_organization_id_organizations_id_fk": { + "name": "v2_users_hosts_organization_id_organizations_id_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_users_hosts_user_id_users_id_fk": { + "name": "v2_users_hosts_user_id_users_id_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_users_hosts_host_fk": { + "name": "v2_users_hosts_host_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "v2_hosts", + "columnsFrom": [ + "organization_id", + "host_id" + ], + "columnsTo": [ + "organization_id", + "machine_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "v2_users_hosts_organization_id_user_id_host_id_pk": { + "name": "v2_users_hosts_organization_id_user_id_host_id_pk", + "columns": [ + "organization_id", + "user_id", + "host_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_workspaces": { + "name": "v2_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "v2_workspace_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'worktree'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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_created_by_user_id_users_id_fk": { + "name": "v2_workspaces_created_by_user_id_users_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "v2_workspaces_host_fk": { + "name": "v2_workspaces_host_fk", + "tableFrom": "v2_workspaces", + "tableTo": "v2_hosts", + "columnsFrom": [ + "organization_id", + "host_id" + ], + "columnsTo": [ + "organization_id", + "machine_id" + ], + "onDelete": "no action", + "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 0c4ecb7875b..de6f9c6ca44 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -274,6 +274,13 @@ "when": 1777259447556, "tag": "0038_v2_workspaces_main_type", "breakpoints": true + }, + { + "idx": 39, + "version": "7", + "when": 1777266747895, + "tag": "0039_consolidate_host_client_machine_id", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/relations.ts b/packages/db/src/schema/relations.ts index fa9400cf189..49992b6e750 100644 --- a/packages/db/src/schema/relations.ts +++ b/packages/db/src/schema/relations.ts @@ -21,7 +21,6 @@ import { projects, sandboxImages, secrets, - sessionHosts, subscriptions, taskStatuses, tasks, @@ -314,8 +313,8 @@ export const v2UsersHostsRelations = relations(v2UsersHosts, ({ one }) => ({ references: [users.id], }), host: one(v2Hosts, { - fields: [v2UsersHosts.hostId], - references: [v2Hosts.id], + fields: [v2UsersHosts.organizationId, v2UsersHosts.hostId], + references: [v2Hosts.organizationId, v2Hosts.machineId], }), })); @@ -331,8 +330,8 @@ export const v2WorkspacesRelations = relations( references: [v2Projects.id], }), host: one(v2Hosts, { - fields: [v2Workspaces.hostId], - references: [v2Hosts.id], + fields: [v2Workspaces.organizationId, v2Workspaces.hostId], + references: [v2Hosts.organizationId, v2Hosts.machineId], }), createdBy: one(users, { fields: [v2Workspaces.createdByUserId], @@ -384,36 +383,21 @@ export const workspacesRelations = relations(workspaces, ({ one, many }) => ({ chatSessions: many(chatSessions), })); -export const chatSessionsRelations = relations( - chatSessions, - ({ one, many }) => ({ - organization: one(organizations, { - fields: [chatSessions.organizationId], - references: [organizations.id], - }), - createdBy: one(users, { - fields: [chatSessions.createdBy], - references: [users.id], - }), - workspace: one(workspaces, { - fields: [chatSessions.workspaceId], - references: [workspaces.id], - }), - v2Workspace: one(v2Workspaces, { - fields: [chatSessions.v2WorkspaceId], - references: [v2Workspaces.id], - }), - sessionHosts: many(sessionHosts), - }), -); - -export const sessionHostsRelations = relations(sessionHosts, ({ one }) => ({ - chatSession: one(chatSessions, { - fields: [sessionHosts.sessionId], - references: [chatSessions.id], - }), +export const chatSessionsRelations = relations(chatSessions, ({ one }) => ({ organization: one(organizations, { - fields: [sessionHosts.organizationId], + fields: [chatSessions.organizationId], references: [organizations.id], }), + createdBy: one(users, { + fields: [chatSessions.createdBy], + references: [users.id], + }), + workspace: one(workspaces, { + fields: [chatSessions.workspaceId], + references: [workspaces.id], + }), + v2Workspace: one(v2Workspaces, { + fields: [chatSessions.v2WorkspaceId], + references: [v2Workspaces.id], + }), })); diff --git a/packages/db/src/schema/schema.ts b/packages/db/src/schema/schema.ts index 4b3aa7ef70c..e56c0f2e02f 100644 --- a/packages/db/src/schema/schema.ts +++ b/packages/db/src/schema/schema.ts @@ -2,11 +2,13 @@ import type { ResolvedAgentConfig } from "@superset/shared/agent-settings"; import { sql } from "drizzle-orm"; import { boolean, + foreignKey, index, integer, jsonb, pgEnum, pgTable, + primaryKey, real, text, timestamp, @@ -248,7 +250,9 @@ export const subscriptions = pgTable( export type InsertSubscription = typeof subscriptions.$inferInsert; export type SelectSubscription = typeof subscriptions.$inferSelect; -// Device presence - tracks online devices for command routing +// Device presence — v1 concept. Tracks per-(user, machine) presence for +// MCP ownership verification. Untouched by the v2 host consolidation; will +// be retired when v1 is removed. export const devicePresence = pgTable( "device_presence", { @@ -419,7 +423,6 @@ export type SelectV2Project = typeof v2Projects.$inferSelect; export const v2Hosts = pgTable( "v2_hosts", { - id: uuid().primaryKey().defaultRandom(), organizationId: uuid("organization_id") .notNull() .references(() => organizations.id, { onDelete: "cascade" }), @@ -438,11 +441,8 @@ export const v2Hosts = pgTable( .$onUpdate(() => new Date()), }, (table) => [ + primaryKey({ columns: [table.organizationId, table.machineId] }), index("v2_hosts_organization_id_idx").on(table.organizationId), - unique("v2_hosts_org_machine_id_unique").on( - table.organizationId, - table.machineId, - ), ], ); @@ -452,7 +452,6 @@ export type SelectV2Host = typeof v2Hosts.$inferSelect; export const v2Clients = pgTable( "v2_clients", { - id: uuid().primaryKey().defaultRandom(), organizationId: uuid("organization_id") .notNull() .references(() => organizations.id, { onDelete: "cascade" }), @@ -470,13 +469,11 @@ export const v2Clients = pgTable( .$onUpdate(() => new Date()), }, (table) => [ + primaryKey({ + columns: [table.organizationId, table.userId, table.machineId], + }), index("v2_clients_organization_id_idx").on(table.organizationId), index("v2_clients_user_id_idx").on(table.userId), - unique("v2_clients_org_user_machine_unique").on( - table.organizationId, - table.userId, - table.machineId, - ), ], ); @@ -486,16 +483,13 @@ export type SelectV2Client = typeof v2Clients.$inferSelect; export const v2UsersHosts = pgTable( "v2_users_hosts", { - id: uuid().primaryKey().defaultRandom(), organizationId: uuid("organization_id") .notNull() .references(() => organizations.id, { onDelete: "cascade" }), userId: uuid("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), - hostId: uuid("host_id") - .notNull() - .references(() => v2Hosts.id, { onDelete: "cascade" }), + hostId: text("host_id").notNull(), role: v2UsersHostRole().notNull().default("member"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() @@ -506,14 +500,17 @@ export const v2UsersHosts = pgTable( .$onUpdate(() => new Date()), }, (table) => [ + primaryKey({ + columns: [table.organizationId, table.userId, table.hostId], + }), + foreignKey({ + columns: [table.organizationId, table.hostId], + foreignColumns: [v2Hosts.organizationId, v2Hosts.machineId], + name: "v2_users_hosts_host_fk", + }).onDelete("cascade"), index("v2_users_hosts_organization_id_idx").on(table.organizationId), index("v2_users_hosts_user_id_idx").on(table.userId), index("v2_users_hosts_host_id_idx").on(table.hostId), - unique("v2_users_hosts_org_user_host_unique").on( - table.organizationId, - table.userId, - table.hostId, - ), ], ); @@ -530,9 +527,7 @@ export const v2Workspaces = pgTable( projectId: uuid("project_id") .notNull() .references(() => v2Projects.id, { onDelete: "cascade" }), - hostId: uuid("host_id") - .notNull() - .references(() => v2Hosts.id), + hostId: text("host_id").notNull(), name: text().notNull(), branch: text().notNull(), type: v2WorkspaceType().notNull().default("worktree"), @@ -548,6 +543,11 @@ export const v2Workspaces = pgTable( .$onUpdate(() => new Date()), }, (table) => [ + foreignKey({ + columns: [table.organizationId, table.hostId], + foreignColumns: [v2Hosts.organizationId, v2Hosts.machineId], + name: "v2_workspaces_host_fk", + }), 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), @@ -686,29 +686,6 @@ export const chatSessions = pgTable( export type InsertChatSession = typeof chatSessions.$inferInsert; export type SelectChatSession = typeof chatSessions.$inferSelect; -export const sessionHosts = pgTable( - "session_hosts", - { - id: uuid().primaryKey().defaultRandom(), - sessionId: uuid("session_id") - .notNull() - .references(() => chatSessions.id, { onDelete: "cascade" }), - organizationId: uuid("organization_id") - .notNull() - .references(() => organizations.id, { onDelete: "cascade" }), - deviceId: text("device_id").notNull(), - createdAt: timestamp("created_at").notNull().defaultNow(), - }, - (table) => [ - index("session_hosts_session_id_idx").on(table.sessionId), - index("session_hosts_org_idx").on(table.organizationId), - index("session_hosts_device_id_idx").on(table.deviceId), - ], -); - -export type InsertSessionHost = typeof sessionHosts.$inferInsert; -export type SelectSessionHost = typeof sessionHosts.$inferSelect; - export const automationRunStatus = pgEnum( "automation_run_status", automationRunStatusValues, @@ -735,9 +712,7 @@ export const automations = pgTable( agentConfig: jsonb("agent_config").$type().notNull(), - targetHostId: uuid("target_host_id").references(() => v2Hosts.id, { - onDelete: "set null", - }), + targetHostId: text("target_host_id"), v2ProjectId: uuid("v2_project_id") .notNull() @@ -787,9 +762,7 @@ export const automationRuns = pgTable( scheduledFor: timestamp("scheduled_for", { withTimezone: true }).notNull(), - hostId: uuid("host_id").references(() => v2Hosts.id, { - onDelete: "set null", - }), + hostId: text("host_id"), v2WorkspaceId: uuid("v2_workspace_id"), sessionKind: automationSessionKind("session_kind"), diff --git a/packages/host-service/src/terminal/env-strip.ts b/packages/host-service/src/terminal/env-strip.ts index e5eab939cd4..5bf31f010b6 100644 --- a/packages/host-service/src/terminal/env-strip.ts +++ b/packages/host-service/src/terminal/env-strip.ts @@ -10,15 +10,15 @@ /** * Exact keys injected by desktop into host-service. * - * DESKTOP_* and DEVICE_* are exact keys (not prefixes) because - * DESKTOP_SESSION, DESKTOP_STARTUP_ID etc. are legitimate Linux vars. + * DESKTOP_* are exact keys (not prefixes) because DESKTOP_SESSION, + * DESKTOP_STARTUP_ID etc. are legitimate Linux vars. */ const HOST_SERVICE_RUNTIME_KEYS = new Set([ "AUTH_TOKEN", "CLOUD_API_URL", "DESKTOP_VITE_PORT", - "DEVICE_CLIENT_ID", - "DEVICE_NAME", + "HOST_CLIENT_ID", + "HOST_NAME", "KEEP_ALIVE_AFTER_PARENT", "ORGANIZATION_ID", ]); diff --git a/packages/host-service/src/terminal/env.test.ts b/packages/host-service/src/terminal/env.test.ts index 9a45c5e0388..3d646bd19c1 100644 --- a/packages/host-service/src/terminal/env.test.ts +++ b/packages/host-service/src/terminal/env.test.ts @@ -66,8 +66,8 @@ describe("stripTerminalRuntimeEnv", () => { AUTH_TOKEN: "secret-token", HOST_SERVICE_SECRET: "secret", ORGANIZATION_ID: "org-123", - DEVICE_CLIENT_ID: "device-abc", - DEVICE_NAME: "My Mac", + HOST_CLIENT_ID: "device-abc", + HOST_NAME: "My Mac", ELECTRON_RUN_AS_NODE: "1", HOST_DB_PATH: "/tmp/host.db", HOST_MANIFEST_DIR: "/tmp/manifests", @@ -110,7 +110,7 @@ describe("stripTerminalRuntimeEnv", () => { expect(result.AUTH_TOKEN).toBeUndefined(); expect(result.HOST_SERVICE_SECRET).toBeUndefined(); expect(result.ORGANIZATION_ID).toBeUndefined(); - expect(result.DEVICE_CLIENT_ID).toBeUndefined(); + expect(result.HOST_CLIENT_ID).toBeUndefined(); expect(result.ELECTRON_RUN_AS_NODE).toBeUndefined(); expect(result.HOST_DB_PATH).toBeUndefined(); expect(result.CLOUD_API_URL).toBeUndefined(); @@ -123,7 +123,7 @@ describe("stripTerminalRuntimeEnv", () => { expect(result.HOST_MIGRATIONS_PATH).toBeUndefined(); expect(result.HOST_SERVICE_VERSION).toBeUndefined(); expect(result.KEEP_ALIVE_AFTER_PARENT).toBeUndefined(); - expect(result.DEVICE_NAME).toBeUndefined(); + expect(result.HOST_NAME).toBeUndefined(); }); test("Node/app keys are stripped", () => { @@ -141,16 +141,16 @@ describe("stripTerminalRuntimeEnv", () => { expect(result.ELECTRON_ENABLE_LOGGING).toBeUndefined(); }); - test("HOST_* prefix is stripped, DESKTOP_*/DEVICE_* are exact-key only", () => { + test("HOST_* prefix is stripped, DESKTOP_* exact keys only", () => { const env: Record = { - // HOST_* prefix: all stripped + // HOST_* prefix: all stripped (including HOST_CLIENT_ID, HOST_NAME) HOST_DB_PATH: "/tmp/db", HOST_MANIFEST_DIR: "/tmp/manifests", HOST_SERVICE_SECRET: "secret", - // DESKTOP_* / DEVICE_*: only our exact keys stripped + HOST_CLIENT_ID: "abc", + HOST_NAME: "Mac", + // DESKTOP_*: only our exact key stripped DESKTOP_VITE_PORT: "5173", - DEVICE_CLIENT_ID: "abc", - DEVICE_NAME: "Mac", // Legitimate Linux desktop vars: must survive DESKTOP_SESSION: "gnome", DESKTOP_STARTUP_ID: "startup-123", @@ -161,8 +161,8 @@ describe("stripTerminalRuntimeEnv", () => { expect(result.HOST_MANIFEST_DIR).toBeUndefined(); expect(result.HOST_SERVICE_SECRET).toBeUndefined(); expect(result.DESKTOP_VITE_PORT).toBeUndefined(); - expect(result.DEVICE_CLIENT_ID).toBeUndefined(); - expect(result.DEVICE_NAME).toBeUndefined(); + expect(result.HOST_CLIENT_ID).toBeUndefined(); + expect(result.HOST_NAME).toBeUndefined(); // Linux desktop vars preserved expect(result.DESKTOP_SESSION).toBe("gnome"); expect(result.DESKTOP_STARTUP_ID).toBe("startup-123"); diff --git a/packages/host-service/src/trpc/router/host/host.ts b/packages/host-service/src/trpc/router/host/host.ts index 918bfe183a5..c8a62e4f464 100644 --- a/packages/host-service/src/trpc/router/host/host.ts +++ b/packages/host-service/src/trpc/router/host/host.ts @@ -1,14 +1,16 @@ import os from "node:os"; -import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { getHostId, getHostName } from "@superset/shared/host-info"; import { TRPCError } from "@trpc/server"; import type { ApiClient } from "../../../types"; import { protectedProcedure, router } from "../../index"; -// 0.2.0: `workspaceCreation.adopt` accepts optional `worktreePath` for -// adopting worktrees at arbitrary paths (not just /.worktrees/). -// The v1→v2 migration depends on this to adopt legacy ~/.superset/worktrees -// paths. Clients using the new param must refuse to adopt an older service. -const HOST_SERVICE_VERSION = "0.2.0"; +// 0.3.0: cloud `device.*` router renamed to `host.*`; `device.ensureV2Host` +// is now `host.ensure`, host registrations are keyed on (orgId, machineId) +// composite, and `targetHostId`/`v2_workspaces.host_id` are machineId text +// not uuid. Older host-service binaries call the now-removed `device.*` +// procedures and fail at registration. +// 0.2.0: `workspaceCreation.adopt` accepts optional `worktreePath`. +const HOST_SERVICE_VERSION = "0.3.0"; const ORGANIZATION_CACHE_TTL_MS = 60 * 60 * 1000; let cachedOrganization: { @@ -47,8 +49,8 @@ export const hostRouter = router({ const organization = await getOrganization(ctx.api, ctx.organizationId); return { - hostId: getHashedDeviceId(), - hostName: getDeviceName(), + hostId: getHostId(), + hostName: getHostName(), version: HOST_SERVICE_VERSION, organization, platform: os.platform(), 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 index 86cae761634..2899e6c7678 100644 --- 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 @@ -1,4 +1,4 @@ -import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { getHostId, getHostName } from "@superset/shared/host-info"; import { workspaces } from "../../../../db/schema"; import type { HostServiceContext } from "../../../../types"; @@ -49,10 +49,10 @@ export async function ensureMainWorkspace( return null; } - const host = await ctx.api.device.ensureV2Host.mutate({ + const host = await ctx.api.host.ensure.mutate({ organizationId: ctx.organizationId, - machineId: getHashedDeviceId(), - name: getDeviceName(), + machineId: getHostId(), + name: getHostName(), }); const cloudRow = await ctx.api.v2Workspace.create.mutate({ @@ -60,7 +60,7 @@ export async function ensureMainWorkspace( projectId, name: branch, branch, - hostId: host.id, + hostId: host.machineId, type: "main", }); 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 963493ba7c3..c03d239e460 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 @@ -1,4 +1,4 @@ -import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { getHostId, getHostName } from "@superset/shared/host-info"; import { TRPCError } from "@trpc/server"; import { and, eq, ne, or } from "drizzle-orm"; import { workspaces } from "../../../../db/schema"; @@ -115,8 +115,8 @@ async function recordBaseBranch( export const adopt = protectedProcedure .input(adoptInputSchema) .mutation(async ({ ctx, input }) => { - const deviceClientId = getHashedDeviceId(); - const deviceName = getDeviceName(); + const machineId = getHostId(); + const hostName = getHostName(); const localProject = requireLocalProject(ctx, input.projectId); await ensureMainWorkspace(ctx, input.projectId, localProject.repoPath); @@ -245,20 +245,16 @@ export const adopt = protectedProcedure deleteLocalWorkspace(ctx, existingLocalByPath.id); } - // Reuse an exact local/cloud match for rerun safety. If the local row - // points at a hard-deleted cloud row, drop it and create a fresh row below. - // Rows with the same branch but a different path are treated as stale and - // replaced after the new cloud row is created. - let host: { id: string }; + let host: { machineId: string }; try { - host = await ctx.api.device.ensureV2Host.mutate({ + host = await ctx.api.host.ensure.mutate({ organizationId: ctx.organizationId, - machineId: deviceClientId, - name: deviceName, + machineId, + name: hostName, }); } catch (err) { if (err instanceof TRPCError) throw err; - console.error("[workspaceCreation.adopt] ensureV2Host failed", err); + console.error("[workspaceCreation.adopt] host.ensure failed", err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Failed to register host: ${err instanceof Error ? err.message : String(err)}`, @@ -272,7 +268,7 @@ export const adopt = protectedProcedure projectId: input.projectId, name: input.workspaceName, branch, - hostId: host.id, + hostId: host.machineId, }); } catch (err) { if (err instanceof TRPCError) throw err; 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 109d34a6299..2faa576f5f0 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 @@ -1,6 +1,6 @@ import { mkdirSync } from "node:fs"; import { dirname } from "node:path"; -import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { getHostId, getHostName } from "@superset/shared/host-info"; import { TRPCError } from "@trpc/server"; import { workspaces } from "../../../../db/schema"; import { @@ -27,8 +27,8 @@ import { deduplicateBranchName } from "../utils/sanitize-branch"; export const create = protectedProcedure .input(createInputSchema) .mutation(async ({ ctx, input }) => { - const deviceClientId = getHashedDeviceId(); - const deviceName = getDeviceName(); + const machineId = getHostId(); + const hostName = getHostName(); setProgress(input.pendingId, "ensuring_repo"); const localProject = requireLocalProject(ctx, input.projectId); @@ -190,15 +190,15 @@ export const create = protectedProcedure } }; - let host: { id: string }; + let host: { machineId: string }; try { - host = await ctx.api.device.ensureV2Host.mutate({ + host = await ctx.api.host.ensure.mutate({ organizationId: ctx.organizationId, - machineId: deviceClientId, - name: deviceName, + machineId, + name: hostName, }); } catch (err) { - console.error("[workspaceCreation.create] ensureV2Host failed", err); + console.error("[workspaceCreation.create] host.ensure failed", err); clearProgress(input.pendingId); await rollbackWorktree(); throw new TRPCError({ @@ -213,7 +213,7 @@ export const create = protectedProcedure projectId: input.projectId, name: input.names.workspaceName, branch: branchName, - hostId: host.id, + hostId: host.machineId, }) .catch(async (err) => { console.error( diff --git a/packages/host-service/src/trpc/router/workspace-creation/shared/finish-checkout.ts b/packages/host-service/src/trpc/router/workspace-creation/shared/finish-checkout.ts index d35af97c39e..7be2be8f561 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/shared/finish-checkout.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/shared/finish-checkout.ts @@ -1,4 +1,4 @@ -import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { getHostId, getHostName } from "@superset/shared/host-info"; import { TRPCError } from "@trpc/server"; import { workspaces } from "../../../../db/schema"; import type { HostServiceContext } from "../../../../types"; @@ -61,18 +61,18 @@ export async function finishCheckout( } }; - const deviceClientId = getHashedDeviceId(); - const deviceName = getDeviceName(); + const machineId = getHostId(); + const hostName = getHostName(); - let host: { id: string }; + let host: { machineId: string }; try { - host = await ctx.api.device.ensureV2Host.mutate({ + host = await ctx.api.host.ensure.mutate({ organizationId: ctx.organizationId, - machineId: deviceClientId, - name: deviceName, + machineId, + name: hostName, }); } catch (err) { - console.error("[workspaceCreation.checkout] ensureV2Host failed", err); + console.error("[workspaceCreation.checkout] host.ensure failed", err); clearProgress(args.pendingId); await rollbackWorktree(); throw new TRPCError({ @@ -87,7 +87,7 @@ export async function finishCheckout( projectId: args.projectId, name: args.workspaceName, branch: args.branch, - hostId: host.id, + hostId: host.machineId, }) .catch(async (err) => { console.error( diff --git a/packages/host-service/src/trpc/router/workspace/workspace.ts b/packages/host-service/src/trpc/router/workspace/workspace.ts index a912cc18995..08f5b9cfcfa 100644 --- a/packages/host-service/src/trpc/router/workspace/workspace.ts +++ b/packages/host-service/src/trpc/router/workspace/workspace.ts @@ -1,6 +1,6 @@ import { existsSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; -import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { getHostId, getHostName } from "@superset/shared/host-info"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import simpleGit from "simple-git"; @@ -85,8 +85,8 @@ export const workspaceRouter = router({ ".worktrees", input.branch, ); - const deviceClientId = getHashedDeviceId(); - const deviceName = getDeviceName(); + const machineId = getHostId(); + const hostName = getHostName(); const git = await ctx.git(localProject.repoPath); try { @@ -95,10 +95,10 @@ export const workspaceRouter = router({ await git.raw(["worktree", "add", "-b", input.branch, worktreePath]); } - const host = await ctx.api.device.ensureV2Host.mutate({ + const host = await ctx.api.host.ensure.mutate({ organizationId: ctx.organizationId, - machineId: deviceClientId, - name: deviceName, + machineId, + name: hostName, }); const cloudRow = await ctx.api.v2Workspace.create @@ -107,7 +107,7 @@ export const workspaceRouter = router({ projectId: input.projectId, name: input.name, branch: input.branch, - hostId: host.id, + hostId: host.machineId, }) .catch(async (err) => { try { diff --git a/packages/host-service/src/tunnel/connect.ts b/packages/host-service/src/tunnel/connect.ts index 0c9d60bc0c8..b2aa15f2444 100644 --- a/packages/host-service/src/tunnel/connect.ts +++ b/packages/host-service/src/tunnel/connect.ts @@ -1,4 +1,5 @@ -import { getDeviceName, getHashedDeviceId } from "@superset/shared/device-info"; +import { getHostId, getHostName } from "@superset/shared/host-info"; +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import type { JwtApiAuthProvider } from "../providers/auth/JwtAuthProvider/JwtAuthProvider"; import type { ApiClient } from "../types"; import { TunnelClient } from "./tunnel-client"; @@ -16,16 +17,16 @@ export async function connectRelay( options: ConnectRelayOptions, ): Promise { try { - const host = await options.api.device.ensureV2Host.mutate({ + const host = await options.api.host.ensure.mutate({ organizationId: options.organizationId, - machineId: getHashedDeviceId(), - name: getDeviceName(), + machineId: getHostId(), + name: getHostName(), }); - console.log(`[host-service] registered as host ${host.id}`); + console.log(`[host-service] registered as host ${host.machineId}`); const tunnel = new TunnelClient({ relayUrl: options.relayUrl, - hostId: host.id, + hostId: buildHostRoutingKey(options.organizationId, host.machineId), getAuthToken: () => options.authProvider.getJwt(), localPort: options.localPort, hostServiceSecret: options.hostServiceSecret, diff --git a/packages/shared/package.json b/packages/shared/package.json index 5d73058e97c..da76421903e 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -84,9 +84,13 @@ "types": "./src/tunnel-protocol.ts", "default": "./src/tunnel-protocol.ts" }, - "./device-info": { - "types": "./src/device-info.ts", - "default": "./src/device-info.ts" + "./host-info": { + "types": "./src/host-info.ts", + "default": "./src/host-info.ts" + }, + "./host-routing": { + "types": "./src/host-routing.ts", + "default": "./src/host-routing.ts" }, "./github-remote": { "types": "./src/github-remote.ts", diff --git a/packages/shared/src/device-info.ts b/packages/shared/src/host-info.ts similarity index 76% rename from packages/shared/src/device-info.ts rename to packages/shared/src/host-info.ts index abd369c4479..449162efb29 100644 --- a/packages/shared/src/device-info.ts +++ b/packages/shared/src/host-info.ts @@ -3,7 +3,9 @@ import { createHmac } from "node:crypto"; import { readFileSync } from "node:fs"; import { homedir, hostname, platform } from "node:os"; -const APP_DEVICE_SALT = "superset-desktop-device-id-v1"; +// Salt value preserved verbatim across the rename to keep existing host ids +// stable for users already registered against the cloud. +const APP_HOST_SALT = "superset-desktop-device-id-v1"; function getRawMachineId(): string { try { @@ -48,7 +50,7 @@ let cachedMachineId: string | null = null; /** * Raw machine ID for local encryption key derivation. - * Do NOT send this to the cloud - use getHashedDeviceId() instead. + * Do NOT send this to the cloud - use getHostId() instead. */ export function getMachineId(): string { if (!cachedMachineId) { @@ -60,13 +62,14 @@ export function getMachineId(): string { let cachedHashedId: string | null = null; /** - * Hashed device ID safe for cloud transmission. - * Non-reversible, stable, and app-specific. + * Stable host id safe for cloud transmission. + * HMAC of the raw machine id; non-reversible and app-specific. + * This is the canonical identifier for a machine acting as a host or client. */ -export function getHashedDeviceId(): string { +export function getHostId(): string { if (!cachedHashedId) { const machineId = getMachineId(); - cachedHashedId = createHmac("sha256", APP_DEVICE_SALT) + cachedHashedId = createHmac("sha256", APP_HOST_SALT) .update(machineId) .digest("hex") .slice(0, 32); @@ -75,19 +78,16 @@ export function getHashedDeviceId(): string { } /** - * Sanitized device name for cloud transmission. + * Sanitized host name for cloud transmission. * Returns generic identifier instead of potentially PII-containing hostname. */ -export function getDeviceName(): string { +export function getHostName(): string { const os = platform(); const osName = os === "darwin" ? "Mac" : os === "win32" ? "Windows" : "Linux"; const rawHostname = hostname(); - // Use just the first segment if it looks like a local hostname - // e.g., "johns-macbook-pro.local" -> "johns-macbook-pro" const shortName = rawHostname.split(".")[0] || rawHostname; - // If hostname looks generic or is very short, use OS name if (shortName.length < 3 || shortName === "localhost") { return `${osName} Desktop`; } diff --git a/packages/shared/src/host-routing.ts b/packages/shared/src/host-routing.ts new file mode 100644 index 00000000000..c4e26c66554 --- /dev/null +++ b/packages/shared/src/host-routing.ts @@ -0,0 +1,25 @@ +/** + * Routing key the relay uses to identify a host service tunnel. The same + * physical machine can be a host in multiple orgs, so machineId alone is + * not unique on the relay's tunnel map — scope it by org. + * + * Lives in its own module (not host-info) so the renderer can import it + * without pulling in node:child_process / node:fs. + */ +export function buildHostRoutingKey( + organizationId: string, + machineId: string, +): string { + return `${organizationId}:${machineId}`; +} + +export function parseHostRoutingKey( + key: string, +): { organizationId: string; machineId: string } | null { + const idx = key.indexOf(":"); + if (idx <= 0 || idx === key.length - 1) return null; + return { + organizationId: key.slice(0, idx), + machineId: key.slice(idx + 1), + }; +} diff --git a/packages/trpc/src/root.ts b/packages/trpc/src/root.ts index 380e3d1ada4..65c9bca6d58 100644 --- a/packages/trpc/src/root.ts +++ b/packages/trpc/src/root.ts @@ -8,6 +8,7 @@ import { automationRouter } from "./router/automation"; import { billingRouter } from "./router/billing"; import { chatRouter } from "./router/chat"; import { deviceRouter } from "./router/device"; +import { hostRouter } from "./router/host"; import { integrationRouter } from "./router/integration"; import { organizationRouter } from "./router/organization"; import { projectRouter } from "./router/project"; @@ -29,6 +30,7 @@ export const appRouter = createTRPCRouter({ billing: billingRouter, chat: chatRouter, device: deviceRouter, + host: hostRouter, integration: integrationRouter, organization: organizationRouter, project: projectRouter, diff --git a/packages/trpc/src/router/automation/automation.ts b/packages/trpc/src/router/automation/automation.ts index dd03a20f364..d9b06d7df1c 100644 --- a/packages/trpc/src/router/automation/automation.ts +++ b/packages/trpc/src/router/automation/automation.ts @@ -31,12 +31,17 @@ async function verifyHostAccess( hostId: string, ): Promise { const [host] = await db - .select({ id: v2Hosts.id, organizationId: v2Hosts.organizationId }) + .select({ machineId: v2Hosts.machineId }) .from(v2Hosts) - .where(eq(v2Hosts.id, hostId)) + .where( + and( + eq(v2Hosts.organizationId, organizationId), + eq(v2Hosts.machineId, hostId), + ), + ) .limit(1); - if (!host || host.organizationId !== organizationId) { + if (!host) { throw new TRPCError({ code: "NOT_FOUND", message: "Host not found", @@ -44,10 +49,14 @@ async function verifyHostAccess( } const [membership] = await db - .select({ id: v2UsersHosts.id }) + .select({ hostId: v2UsersHosts.hostId }) .from(v2UsersHosts) .where( - and(eq(v2UsersHosts.userId, userId), eq(v2UsersHosts.hostId, hostId)), + and( + eq(v2UsersHosts.userId, userId), + eq(v2UsersHosts.organizationId, organizationId), + eq(v2UsersHosts.hostId, hostId), + ), ) .limit(1); diff --git a/packages/trpc/src/router/automation/dispatch.ts b/packages/trpc/src/router/automation/dispatch.ts index 62c28828656..a3144ea1ecf 100644 --- a/packages/trpc/src/router/automation/dispatch.ts +++ b/packages/trpc/src/router/automation/dispatch.ts @@ -14,6 +14,7 @@ import { getCommandFromAgentConfig, type TerminalResolvedAgentConfig, } from "@superset/shared/agent-settings"; +import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { deduplicateBranchName, sanitizeBranchNameWithMaxLength, @@ -57,7 +58,7 @@ export async function dispatchAutomation( const inserted = await recordSkipped( automation, scheduledFor, - host.id, + host.machineId, error, ); return { status: "skipped_offline", runId: inserted?.id ?? null, error }; @@ -70,7 +71,7 @@ export async function dispatchAutomation( organizationId: automation.organizationId, title: automation.name, scheduledFor, - hostId: host.id, + hostId: host.machineId, status: "dispatching", }) .onConflictDoNothing({ @@ -97,12 +98,17 @@ export async function dispatchAutomation( ttlSeconds: 300, }); + const routingKey = buildHostRoutingKey( + automation.organizationId, + host.machineId, + ); + if (automation.v2WorkspaceId) { workspaceId = automation.v2WorkspaceId; } else { const created = await createWorkspaceOnHost({ relayUrl, - hostId: host.id, + hostId: routingKey, jwt, projectId: automation.v2ProjectId, automation, @@ -121,7 +127,7 @@ export async function dispatchAutomation( if (agentConfig.kind === "chat") { const { sessionId } = await dispatchChatSession({ relayUrl, - hostId: host.id, + hostId: routingKey, jwt, workspaceId, prompt: automation.prompt, @@ -154,7 +160,7 @@ export async function dispatchAutomation( }); const { terminalId } = await dispatchTerminalSession({ relayUrl, - hostId: host.id, + hostId: routingKey, jwt, workspaceId, command, @@ -193,14 +199,18 @@ async function resolveTargetHost( const [host] = await dbWs .select() .from(v2Hosts) - .where(eq(v2Hosts.id, automation.targetHostId)) + .where( + and( + eq(v2Hosts.organizationId, automation.organizationId), + eq(v2Hosts.machineId, automation.targetHostId), + ), + ) .limit(1); return host ?? null; } const [host] = await dbWs .select({ - id: v2Hosts.id, organizationId: v2Hosts.organizationId, machineId: v2Hosts.machineId, name: v2Hosts.name, @@ -210,7 +220,13 @@ async function resolveTargetHost( updatedAt: v2Hosts.updatedAt, }) .from(v2Hosts) - .innerJoin(v2UsersHosts, eq(v2UsersHosts.hostId, v2Hosts.id)) + .innerJoin( + v2UsersHosts, + and( + eq(v2UsersHosts.organizationId, v2Hosts.organizationId), + eq(v2UsersHosts.hostId, v2Hosts.machineId), + ), + ) .where( and( eq(v2UsersHosts.userId, automation.ownerUserId), diff --git a/packages/trpc/src/router/automation/schema.ts b/packages/trpc/src/router/automation/schema.ts index 085e5cdeb16..a715ff013ef 100644 --- a/packages/trpc/src/router/automation/schema.ts +++ b/packages/trpc/src/router/automation/schema.ts @@ -38,7 +38,7 @@ export const createAutomationSchema = z.object({ name: z.string().min(1).max(200), prompt: z.string().min(1).max(20_000), agentConfig: agentConfigSchema, - targetHostId: z.string().uuid().nullish(), + targetHostId: z.string().min(1).nullish(), v2ProjectId: z.string().uuid(), v2WorkspaceId: z.string().uuid().nullish(), rrule: rruleBody, @@ -52,7 +52,7 @@ export const updateAutomationSchema = z.object({ name: z.string().min(1).max(200).optional(), prompt: z.string().min(1).max(20_000).optional(), agentConfig: agentConfigSchema.optional(), - targetHostId: z.string().uuid().nullish(), + targetHostId: z.string().min(1).nullish(), v2ProjectId: z.string().uuid().optional(), v2WorkspaceId: z.string().uuid().nullish(), rrule: rruleBody.optional(), diff --git a/packages/trpc/src/router/device/device.ts b/packages/trpc/src/router/device/device.ts index 00f11c20057..3a2478252bb 100644 --- a/packages/trpc/src/router/device/device.ts +++ b/packages/trpc/src/router/device/device.ts @@ -1,124 +1,15 @@ -import { db, dbWs } from "@superset/db/client"; -import { - devicePresence, - deviceTypeValues, - v2Clients, - v2ClientTypeValues, - v2Hosts, - v2UsersHosts, -} from "@superset/db/schema"; +import { db } from "@superset/db/client"; +import { devicePresence, deviceTypeValues } from "@superset/db/schema"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; -import { and, eq } from "drizzle-orm"; import { z } from "zod"; -import { jwtProcedure, protectedProcedure } from "../../trpc"; +import { protectedProcedure } from "../../trpc"; +/** + * v1 device-presence procedures. Kept separate from the v2 host router so the + * device_presence table stays an isolated v1 system that gets retired with + * the rest of v1. + */ export const deviceRouter = { - ensureV2Host: jwtProcedure - .input( - z.object({ - organizationId: z.string().uuid(), - machineId: z.string().min(1), - name: z.string().min(1), - }), - ) - .mutation(async ({ ctx, input }) => { - if (!ctx.organizationIds.includes(input.organizationId)) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "Not a member of this organization", - }); - } - - const [host] = await dbWs - .insert(v2Hosts) - .values({ - organizationId: input.organizationId, - machineId: input.machineId, - name: input.name, - createdByUserId: ctx.userId, - }) - .onConflictDoUpdate({ - target: [v2Hosts.organizationId, v2Hosts.machineId], - set: { - name: input.name, - }, - }) - .returning(); - - if (!host) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to ensure host", - }); - } - - await dbWs - .insert(v2UsersHosts) - .values({ - organizationId: input.organizationId, - userId: ctx.userId, - hostId: host.id, - role: "owner", - }) - .onConflictDoNothing({ - target: [ - v2UsersHosts.organizationId, - v2UsersHosts.userId, - v2UsersHosts.hostId, - ], - }); - - return host; - }), - - ensureV2Client: protectedProcedure - .input( - z.object({ - machineId: z.string().min(1), - type: z.enum(v2ClientTypeValues), - }), - ) - .mutation(async ({ ctx, input }) => { - const organizationId = ctx.activeOrganizationId; - if (!organizationId) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "No active organization selected", - }); - } - - const userId = ctx.session.user.id; - - const [client] = await dbWs - .insert(v2Clients) - .values({ - organizationId, - userId, - machineId: input.machineId, - type: input.type, - }) - .onConflictDoUpdate({ - target: [ - v2Clients.organizationId, - v2Clients.userId, - v2Clients.machineId, - ], - set: { - type: input.type, - }, - }) - .returning(); - - if (!client) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to ensure client", - }); - } - - return client; - }), - /** * @deprecated Kept for backwards compat with shipped desktop/mobile clients * that still call heartbeat on a 30s interval. Same logic as registerDevice. @@ -215,39 +106,4 @@ export const deviceRouter = { return { device, timestamp: now }; }), - checkHostAccess: jwtProcedure - .input(z.object({ hostId: z.string().uuid() })) - .query(async ({ ctx, input }) => { - const row = await db.query.v2UsersHosts.findFirst({ - where: and( - eq(v2UsersHosts.userId, ctx.userId), - eq(v2UsersHosts.hostId, input.hostId), - ), - columns: { id: true }, - }); - return { allowed: !!row }; - }), - - setHostOnline: jwtProcedure - .input(z.object({ hostId: z.string().uuid(), isOnline: z.boolean() })) - .mutation(async ({ ctx, input }) => { - const access = await db.query.v2UsersHosts.findFirst({ - where: and( - eq(v2UsersHosts.userId, ctx.userId), - eq(v2UsersHosts.hostId, input.hostId), - ), - columns: { id: true }, - }); - if (!access) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "No access to this host", - }); - } - await db - .update(v2Hosts) - .set({ isOnline: input.isOnline }) - .where(eq(v2Hosts.id, input.hostId)); - return { success: true }; - }), } satisfies TRPCRouterRecord; diff --git a/packages/trpc/src/router/host/host.ts b/packages/trpc/src/router/host/host.ts new file mode 100644 index 00000000000..9a3cc5e7fa0 --- /dev/null +++ b/packages/trpc/src/router/host/host.ts @@ -0,0 +1,182 @@ +import { db, dbWs } from "@superset/db/client"; +import { + v2Clients, + v2ClientTypeValues, + v2Hosts, + v2UsersHosts, +} from "@superset/db/schema"; +import { parseHostRoutingKey } from "@superset/shared/host-routing"; +import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; +import { jwtProcedure, protectedProcedure } from "../../trpc"; + +export const hostRouter = { + ensure: jwtProcedure + .input( + z.object({ + organizationId: z.string().uuid(), + machineId: z.string().min(1), + name: z.string().min(1), + }), + ) + .mutation(async ({ ctx, input }) => { + if (!ctx.organizationIds.includes(input.organizationId)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Not a member of this organization", + }); + } + + const [host] = await dbWs + .insert(v2Hosts) + .values({ + organizationId: input.organizationId, + machineId: input.machineId, + name: input.name, + createdByUserId: ctx.userId, + }) + .onConflictDoUpdate({ + target: [v2Hosts.organizationId, v2Hosts.machineId], + set: { + name: input.name, + }, + }) + .returning(); + + if (!host) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to ensure host", + }); + } + + if (host.createdByUserId === ctx.userId) { + await dbWs + .insert(v2UsersHosts) + .values({ + organizationId: input.organizationId, + userId: ctx.userId, + hostId: host.machineId, + role: "owner", + }) + .onConflictDoNothing({ + target: [ + v2UsersHosts.organizationId, + v2UsersHosts.userId, + v2UsersHosts.hostId, + ], + }); + } + + return host; + }), + + ensureClient: protectedProcedure + .input( + z.object({ + machineId: z.string().min(1), + type: z.enum(v2ClientTypeValues), + }), + ) + .mutation(async ({ ctx, input }) => { + const organizationId = ctx.activeOrganizationId; + if (!organizationId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "No active organization selected", + }); + } + + const userId = ctx.session.user.id; + + const [client] = await dbWs + .insert(v2Clients) + .values({ + organizationId, + userId, + machineId: input.machineId, + type: input.type, + }) + .onConflictDoUpdate({ + target: [ + v2Clients.organizationId, + v2Clients.userId, + v2Clients.machineId, + ], + set: { + type: input.type, + }, + }) + .returning(); + + if (!client) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to ensure client", + }); + } + + return client; + }), + + checkAccess: jwtProcedure + .input(z.object({ hostId: z.string().min(1) })) + .query(async ({ ctx, input }) => { + const parsed = parseHostRoutingKey(input.hostId); + if (!parsed) return { allowed: false }; + if (!ctx.organizationIds.includes(parsed.organizationId)) { + return { allowed: false }; + } + const row = await db.query.v2UsersHosts.findFirst({ + where: and( + eq(v2UsersHosts.userId, ctx.userId), + eq(v2UsersHosts.organizationId, parsed.organizationId), + eq(v2UsersHosts.hostId, parsed.machineId), + ), + columns: { hostId: true }, + }); + return { allowed: !!row }; + }), + + setOnline: jwtProcedure + .input(z.object({ hostId: z.string().min(1), isOnline: z.boolean() })) + .mutation(async ({ ctx, input }) => { + const parsed = parseHostRoutingKey(input.hostId); + if (!parsed) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid hostId" }); + } + if (!ctx.organizationIds.includes(parsed.organizationId)) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "No access to this host", + }); + } + + const access = await db.query.v2UsersHosts.findFirst({ + where: and( + eq(v2UsersHosts.userId, ctx.userId), + eq(v2UsersHosts.organizationId, parsed.organizationId), + eq(v2UsersHosts.hostId, parsed.machineId), + ), + columns: { hostId: true }, + }); + if (!access) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "No access to this host", + }); + } + + await db + .update(v2Hosts) + .set({ isOnline: input.isOnline }) + .where( + and( + eq(v2Hosts.organizationId, parsed.organizationId), + eq(v2Hosts.machineId, parsed.machineId), + ), + ); + return { success: true }; + }), +} satisfies TRPCRouterRecord; diff --git a/packages/trpc/src/router/host/index.ts b/packages/trpc/src/router/host/index.ts new file mode 100644 index 00000000000..edc31cbb4d0 --- /dev/null +++ b/packages/trpc/src/router/host/index.ts @@ -0,0 +1 @@ +export { hostRouter } from "./host"; diff --git a/packages/trpc/src/router/v2-host/v2-host.ts b/packages/trpc/src/router/v2-host/v2-host.ts index b06f733d0e2..e71284eb6dd 100644 --- a/packages/trpc/src/router/v2-host/v2-host.ts +++ b/packages/trpc/src/router/v2-host/v2-host.ts @@ -10,15 +10,15 @@ import { requireActiveOrgId } from "../utils/active-org"; async function requireHostOwner( userId: string, - hostId: string, + machineId: string, organizationId: string, ) { const host = await db.query.v2Hosts.findFirst({ where: and( - eq(v2Hosts.id, hostId), eq(v2Hosts.organizationId, organizationId), + eq(v2Hosts.machineId, machineId), ), - columns: { id: true, organizationId: true, createdByUserId: true }, + columns: { machineId: true, organizationId: true, createdByUserId: true }, }); if (!host) { @@ -30,8 +30,9 @@ async function requireHostOwner( const access = await db.query.v2UsersHosts.findFirst({ where: and( - eq(v2UsersHosts.hostId, hostId), + eq(v2UsersHosts.organizationId, organizationId), eq(v2UsersHosts.userId, userId), + eq(v2UsersHosts.hostId, machineId), ), columns: { role: true }, }); @@ -67,8 +68,7 @@ export const v2HostRouter = { addMember: protectedProcedure .input( z.object({ - id: z.string().uuid(), - hostId: z.string().uuid(), + hostId: z.string().min(1), userId: z.string().uuid(), role: z.enum(v2UsersHostRoleValues).optional(), }), @@ -82,7 +82,6 @@ export const v2HostRouter = { const [inserted] = await tx .insert(v2UsersHosts) .values({ - id: input.id, organizationId, userId: input.userId, hostId: input.hostId, @@ -113,7 +112,7 @@ export const v2HostRouter = { removeMember: protectedProcedure .input( z.object({ - hostId: z.string().uuid(), + hostId: z.string().min(1), userId: z.string().uuid(), }), ) @@ -136,8 +135,9 @@ export const v2HostRouter = { const txid = await dbWs.transaction(async (tx) => { const target = await tx.query.v2UsersHosts.findFirst({ where: and( - eq(v2UsersHosts.hostId, input.hostId), + eq(v2UsersHosts.organizationId, organizationId), eq(v2UsersHosts.userId, input.userId), + eq(v2UsersHosts.hostId, input.hostId), ), columns: { role: true }, }); @@ -148,10 +148,11 @@ export const v2HostRouter = { if (target.role === "owner") { const otherOwners = await tx - .select({ id: v2UsersHosts.id }) + .select({ userId: v2UsersHosts.userId }) .from(v2UsersHosts) .where( and( + eq(v2UsersHosts.organizationId, organizationId), eq(v2UsersHosts.hostId, input.hostId), eq(v2UsersHosts.role, "owner"), ne(v2UsersHosts.userId, input.userId), @@ -170,8 +171,9 @@ export const v2HostRouter = { .delete(v2UsersHosts) .where( and( - eq(v2UsersHosts.hostId, input.hostId), + eq(v2UsersHosts.organizationId, organizationId), eq(v2UsersHosts.userId, input.userId), + eq(v2UsersHosts.hostId, input.hostId), ), ); return await getCurrentTxid(tx); @@ -183,7 +185,7 @@ export const v2HostRouter = { setMemberRole: protectedProcedure .input( z.object({ - hostId: z.string().uuid(), + hostId: z.string().min(1), userId: z.string().uuid(), role: z.enum(v2UsersHostRoleValues), }), @@ -207,8 +209,9 @@ export const v2HostRouter = { const txid = await dbWs.transaction(async (tx) => { const target = await tx.query.v2UsersHosts.findFirst({ where: and( - eq(v2UsersHosts.hostId, input.hostId), + eq(v2UsersHosts.organizationId, organizationId), eq(v2UsersHosts.userId, input.userId), + eq(v2UsersHosts.hostId, input.hostId), ), columns: { role: true }, }); @@ -222,10 +225,11 @@ export const v2HostRouter = { if (input.role === "member" && target.role === "owner") { const otherOwners = await tx - .select({ id: v2UsersHosts.id }) + .select({ userId: v2UsersHosts.userId }) .from(v2UsersHosts) .where( and( + eq(v2UsersHosts.organizationId, organizationId), eq(v2UsersHosts.hostId, input.hostId), eq(v2UsersHosts.role, "owner"), ne(v2UsersHosts.userId, input.userId), @@ -245,8 +249,9 @@ export const v2HostRouter = { .set({ role: input.role }) .where( and( - eq(v2UsersHosts.hostId, input.hostId), + eq(v2UsersHosts.organizationId, organizationId), eq(v2UsersHosts.userId, input.userId), + eq(v2UsersHosts.hostId, input.hostId), ), ); return await getCurrentTxid(tx); diff --git a/packages/trpc/src/router/v2-workspace/v2-workspace.ts b/packages/trpc/src/router/v2-workspace/v2-workspace.ts index af6f406f198..df95e8bfe89 100644 --- a/packages/trpc/src/router/v2-workspace/v2-workspace.ts +++ b/packages/trpc/src/router/v2-workspace/v2-workspace.ts @@ -39,10 +39,13 @@ async function getScopedHost(organizationId: string, hostId: string) { () => dbWs.query.v2Hosts.findFirst({ columns: { - id: true, + machineId: true, organizationId: true, }, - where: eq(v2Hosts.id, hostId), + where: and( + eq(v2Hosts.organizationId, organizationId), + eq(v2Hosts.machineId, hostId), + ), }), { code: "BAD_REQUEST", @@ -106,7 +109,7 @@ export const v2WorkspaceRouter = { projectId: z.string().uuid(), name: z.string().min(1), branch: z.string().min(1), - hostId: z.string().uuid(), + hostId: z.string().min(1), type: z.enum(v2WorkspaceTypeValues).default("worktree"), }), ) @@ -135,7 +138,7 @@ export const v2WorkspaceRouter = { projectId: project.id, name: input.name, branch: input.branch, - hostId: host.id, + hostId: host.machineId, type: input.type, createdByUserId: ctx.userId, }) @@ -148,7 +151,7 @@ export const v2WorkspaceRouter = { const existing = await dbWs.query.v2Workspaces.findFirst({ where: and( eq(v2Workspaces.projectId, project.id), - eq(v2Workspaces.hostId, host.id), + eq(v2Workspaces.hostId, host.machineId), eq(v2Workspaces.type, "main"), ), }); @@ -177,7 +180,7 @@ export const v2WorkspaceRouter = { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: `Workspace insert returned no row (type=${input.type}, projectId=${project.id}, hostId=${host.id})`, + message: `Workspace insert returned no row (type=${input.type}, projectId=${project.id}, hostId=${host.machineId})`, }); }), @@ -212,7 +215,7 @@ export const v2WorkspaceRouter = { id: z.string().uuid(), name: z.string().min(1).optional(), branch: z.string().min(1).optional(), - hostId: z.string().uuid().optional(), + hostId: z.string().min(1).optional(), }), ) .mutation(async ({ ctx, input }) => {