diff --git a/apps/web/app/(app)/[workspaceSlug]/settings/repos/page.tsx b/apps/web/app/(app)/[workspaceSlug]/settings/repos/page.tsx new file mode 100644 index 0000000..034398d --- /dev/null +++ b/apps/web/app/(app)/[workspaceSlug]/settings/repos/page.tsx @@ -0,0 +1,312 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { GitBranch, Loader2, Plus, X } from "lucide-react"; +import { useParams } from "next/navigation"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { api } from "@/lib/trpc"; +import type { ForgeType } from "@tasks/shared/connected-repos"; + +const FORGE_LABELS: Record = { + github: "GitHub", + forgejo: "Forgejo / Gitea", + gitlab: "GitLab", + bitbucket: "Bitbucket", +}; + +type ConnectForm = { + remoteUrl: string; + displayName: string; + tokenEnvVar: string; +}; + +const EMPTY_FORM: ConnectForm = { + remoteUrl: "", + displayName: "", + tokenEnvVar: "", +}; + +export default function ConnectedReposSettingsPage() { + const params = useParams(); + const workspaceSlug = params?.workspaceSlug as string | undefined; + const utils = api.useUtils(); + + const listQuery = api.connectedRepos.list.useQuery( + { workspace: workspaceSlug ?? "" }, + { enabled: Boolean(workspaceSlug) }, + ); + + const [dialogOpen, setDialogOpen] = React.useState(false); + const [form, setForm] = React.useState(EMPTY_FORM); + const [parsedPreview, setParsedPreview] = React.useState<{ + forgeType: ForgeType; + owner: string; + repoName: string; + apiBaseUrl: string; + } | null>(null); + const [testResult, setTestResult] = React.useState>({}); + + const parseMut = api.connectedRepos.parseRemote.useMutation({ + onSuccess: (parsed) => { + setParsedPreview({ + forgeType: parsed.forgeType, + owner: parsed.owner, + repoName: parsed.repoName, + apiBaseUrl: parsed.apiBaseUrl, + }); + setForm((prev) => ({ + ...prev, + displayName: prev.displayName || `${parsed.owner}/${parsed.repoName}`, + })); + }, + onError: () => setParsedPreview(null), + }); + + const connectMut = api.connectedRepos.connect.useMutation({ + onSuccess: () => { + void utils.connectedRepos.list.invalidate(); + setDialogOpen(false); + setForm(EMPTY_FORM); + setParsedPreview(null); + }, + }); + + const disconnectMut = api.connectedRepos.disconnect.useMutation({ + onSuccess: () => void utils.connectedRepos.list.invalidate(), + }); + + const testMut = api.connectedRepos.testConnection.useMutation({ + onSuccess: (result, vars) => { + setTestResult((prev) => ({ + ...prev, + [vars.repoId]: result.ok + ? `OK — default branch ${result.defaultBranch} (${result.latencyMs}ms)` + : `Failed: ${result.error ?? "unknown"}`, + })); + if (result.ok) { + void utils.connectedRepos.list.invalidate(); + } + }, + }); + + const canManage = listQuery.data?.canManage ?? false; + const repos = listQuery.data?.repos ?? []; + + const openCreate = () => { + setForm(EMPTY_FORM); + setParsedPreview(null); + setDialogOpen(true); + }; + + const handleParse = () => { + if (!workspaceSlug || !form.remoteUrl.trim()) { + return; + } + parseMut.mutate({ workspace: workspaceSlug, remoteUrl: form.remoteUrl.trim() }); + }; + + const handleConnect = () => { + if (!workspaceSlug || !form.remoteUrl.trim()) { + return; + } + connectMut.mutate({ + workspace: workspaceSlug, + remoteUrl: form.remoteUrl.trim(), + displayName: form.displayName.trim() || undefined, + tokenEnvVar: form.tokenEnvVar.trim() || null, + }); + }; + + return ( +
+
+
+

Connected repos

+

+ Paste a git remote URL to link Forgejo, GitHub, or other forges. API tokens are + referenced by env var name — never stored in the database. +

+
+ {canManage && ( + + )} +
+ + {listQuery.isLoading ? ( +
+ + Loading repos… +
+ ) : repos.length === 0 ? ( +
+ No connected repos yet. + {canManage && " Connect one to prepare for orchestrator and worker automation."} +
+ ) : ( +
    + {repos.map((repo) => ( +
  • +
    +
    + + {repo.displayName} + + {FORGE_LABELS[repo.forgeType]} + +
    +

    {repo.gitRemoteUrl}

    +

    + {repo.owner}/{repo.repoName} + {repo.defaultBranch ? ` · ${repo.defaultBranch}` : ""} + {repo.tokenEnvVar ? ` · token: ${repo.tokenEnvVar}` : ""} +

    + {testResult[repo.id] && ( +

    {testResult[repo.id]}

    + )} +
    + {canManage && ( +
    + + +
    + )} +
  • + ))} +
+ )} + + + + + +
+ + Connect repository + + + + +
+ +
+
+ +
+ setForm((f) => ({ ...f, remoteUrl: e.target.value }))} + /> + +
+ {parseMut.error && ( +

{parseMut.error.message}

+ )} +
+ + {parsedPreview && ( +
+

+ Detected{" "} + {FORGE_LABELS[parsedPreview.forgeType]} —{" "} + {parsedPreview.owner}/{parsedPreview.repoName} +

+

{parsedPreview.apiBaseUrl}

+
+ )} + +
+ + setForm((f) => ({ ...f, displayName: e.target.value }))} + /> +
+ +
+ + setForm((f) => ({ ...f, tokenEnvVar: e.target.value }))} + /> +
+ +
+ + + + +
+ {connectMut.error && ( +

{connectMut.error.message}

+ )} +
+
+
+
+
+ ); +} diff --git a/apps/web/components/layout/top-header.tsx b/apps/web/components/layout/top-header.tsx index 6f67233..51e9a22 100644 --- a/apps/web/components/layout/top-header.tsx +++ b/apps/web/components/layout/top-header.tsx @@ -10,6 +10,7 @@ import { CheckSquare, ChevronDown, FileText, + GitBranch, LayoutGrid, LayoutTemplate, LogOut, @@ -163,6 +164,15 @@ export function TopHeader({ onOpenSearch, onQuickAction }: TopHeaderProps) { Role routing + + workspace?.slug && router.push(`/${workspace.slug}/settings/repos`) + } + > + + Connected repos + diff --git a/apps/web/server/lib/forge/adapters.ts b/apps/web/server/lib/forge/adapters.ts new file mode 100644 index 0000000..0cb8659 --- /dev/null +++ b/apps/web/server/lib/forge/adapters.ts @@ -0,0 +1,145 @@ +import type { ForgeType, RepoMetadata } from "@tasks/shared/connected-repos"; + +export type ForgeAdapterContext = { + apiBaseUrl: string; + owner: string; + repo: string; + token?: string; +}; + +export interface ForgeAdapter { + readonly forgeType: ForgeType; + fetchRepoMetadata(ctx: ForgeAdapterContext): Promise; +} + +async function readJsonResponse(response: Response): Promise { + const text = await response.text(); + if (!text) { + return null; + } + try { + return JSON.parse(text) as unknown; + } catch { + throw new Error(`Forge API returned non-JSON response (${response.status}).`); + } +} + +function authHeaders(token: string | undefined): HeadersInit { + if (!token) { + return {}; + } + return { Authorization: `Bearer ${token}` }; +} + +export const githubForgeAdapter: ForgeAdapter = { + forgeType: "github", + async fetchRepoMetadata(ctx) { + const url = `${ctx.apiBaseUrl.replace(/\/$/, "")}/repos/${ctx.owner}/${ctx.repo}`; + const response = await fetch(url, { + headers: { + Accept: "application/vnd.github+json", + ...authHeaders(ctx.token), + }, + }); + const body = await readJsonResponse(response); + if (!response.ok) { + const message = + typeof body === "object" && + body !== null && + "message" in body && + typeof body.message === "string" + ? body.message + : `GitHub API error (${response.status})`; + throw new Error(message); + } + if ( + typeof body !== "object" || + body === null || + !("default_branch" in body) || + typeof body.default_branch !== "string" + ) { + throw new Error("GitHub API response missing default_branch."); + } + const cloneUrl = + typeof body === "object" && + body !== null && + "clone_url" in body && + typeof body.clone_url === "string" + ? body.clone_url + : `https://github.com/${ctx.owner}/${ctx.repo}.git`; + const description = + typeof body === "object" && + body !== null && + "description" in body && + typeof body.description === "string" + ? body.description + : undefined; + return { + defaultBranch: body.default_branch, + cloneUrl, + description, + }; + }, +}; + +export const forgejoForgeAdapter: ForgeAdapter = { + forgeType: "forgejo", + async fetchRepoMetadata(ctx) { + const url = `${ctx.apiBaseUrl.replace(/\/$/, "")}/repos/${ctx.owner}/${ctx.repo}`; + const response = await fetch(url, { + headers: { + Accept: "application/json", + ...authHeaders(ctx.token), + }, + }); + const body = await readJsonResponse(response); + if (!response.ok) { + const message = + typeof body === "object" && + body !== null && + "message" in body && + typeof body.message === "string" + ? body.message + : `Forge API error (${response.status})`; + throw new Error(message); + } + if ( + typeof body !== "object" || + body === null || + !("default_branch" in body) || + typeof body.default_branch !== "string" + ) { + throw new Error("Forge API response missing default_branch."); + } + const cloneUrl = + typeof body === "object" && + body !== null && + "clone_url" in body && + typeof body.clone_url === "string" + ? body.clone_url + : `${ctx.apiBaseUrl.replace(/\/api\/v1$/, "")}/${ctx.owner}/${ctx.repo}.git`; + const description = + typeof body === "object" && + body !== null && + "description" in body && + typeof body.description === "string" + ? body.description + : undefined; + return { + defaultBranch: body.default_branch, + cloneUrl, + description, + }; + }, +}; + +const ADAPTERS: Record = { + github: githubForgeAdapter, + forgejo: forgejoForgeAdapter, + gitlab: forgejoForgeAdapter, + bitbucket: forgejoForgeAdapter, +}; + +export function getForgeAdapter(forgeType: ForgeType): ForgeAdapter { + return ADAPTERS[forgeType]; +} diff --git a/apps/web/server/root.ts b/apps/web/server/root.ts index f1d594e..3e5d906 100644 --- a/apps/web/server/root.ts +++ b/apps/web/server/root.ts @@ -15,6 +15,7 @@ import { invitesRouter } from "@/server/routers/invites"; import { auditRouter } from "@/server/routers/audit"; import { runsRouter } from "@/server/routers/runs"; import { backlogRouter } from "@/server/routers/backlog"; +import { connectedReposRouter } from "@/server/routers/connected-repos"; export const appRouter = router({ health: healthRouter, @@ -33,6 +34,7 @@ export const appRouter = router({ audit: auditRouter, runs: runsRouter, backlog: backlogRouter, + connectedRepos: connectedReposRouter, }); export type AppRouter = typeof appRouter; diff --git a/apps/web/server/routers/connected-repos.ts b/apps/web/server/routers/connected-repos.ts new file mode 100644 index 0000000..42f3704 --- /dev/null +++ b/apps/web/server/routers/connected-repos.ts @@ -0,0 +1,272 @@ +import { TRPCError } from "@trpc/server"; +import { and, asc, eq } from "drizzle-orm"; +import { z } from "zod"; + +import { connectedRepos } from "@tasks/database/schema"; +import { + connectedRepoSchema, + forgeTypeSchema, + parseGitRemoteUrl, + type ConnectedRepo, +} from "@tasks/shared/connected-repos"; + +import { getForgeAdapter } from "@/server/lib/forge/adapters"; +import { router, workspaceProcedure } from "@/server/trpc"; +import type { WorkspaceProcedureContext } from "@/server/trpc"; + +type RepoRow = typeof connectedRepos.$inferSelect; + +function assertWorkspaceAdmin(ctx: WorkspaceProcedureContext): void { + if (ctx.workspace.role !== "owner" && ctx.workspace.role !== "admin") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Only workspace owners and admins can manage connected repos.", + }); + } +} + +function canManageWorkspace(ctx: WorkspaceProcedureContext): boolean { + return ctx.workspace.role === "owner" || ctx.workspace.role === "admin"; +} + +function readToken(envVarName: string | null | undefined): string | undefined { + if (!envVarName?.trim()) { + return undefined; + } + return process.env[envVarName]?.trim() || undefined; +} + +function mapRepoRow(row: RepoRow): ConnectedRepo { + return connectedRepoSchema.parse({ + id: row.id, + workspaceId: row.workspaceId, + displayName: row.displayName, + gitRemoteUrl: row.gitRemoteUrl, + forgeType: row.forgeType, + apiBaseUrl: row.apiBaseUrl, + owner: row.owner, + repoName: row.repoName, + defaultBranch: row.defaultBranch, + tokenEnvVar: row.tokenEnvVar, + metadata: row.metadata, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }); +} + +const connectInputSchema = z.object({ + workspace: z.string().min(1), + remoteUrl: z.string().min(1), + displayName: z.string().min(1).max(200).optional(), + tokenEnvVar: z.string().min(1).max(120).nullable().optional(), + forgeType: forgeTypeSchema.optional(), +}); + +export const connectedReposRouter = router({ + list: workspaceProcedure.query(async ({ ctx }) => { + const rows = await ctx.db + .select() + .from(connectedRepos) + .where(eq(connectedRepos.workspaceId, ctx.workspace.id)) + .orderBy(asc(connectedRepos.displayName)); + + return { + repos: rows.map(mapRepoRow), + canManage: canManageWorkspace(ctx), + }; + }), + + parseRemote: workspaceProcedure + .input( + z.object({ + workspace: z.string().min(1), + remoteUrl: z.string().min(1), + }), + ) + .mutation(({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + try { + return parseGitRemoteUrl(input.remoteUrl); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error.message : "Invalid remote URL.", + }); + } + }), + + connect: workspaceProcedure.input(connectInputSchema).mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + + let parsed; + try { + parsed = parseGitRemoteUrl(input.remoteUrl); + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error.message : "Invalid remote URL.", + }); + } + + const forgeType = input.forgeType ?? parsed.forgeType; + const displayName = + input.displayName?.trim() || + `${parsed.owner}/${parsed.repoName}`; + + const [row] = await ctx.db + .insert(connectedRepos) + .values({ + workspaceId: ctx.workspace.id, + displayName, + gitRemoteUrl: parsed.gitRemoteUrl, + forgeType, + apiBaseUrl: parsed.apiBaseUrl, + owner: parsed.owner, + repoName: parsed.repoName, + tokenEnvVar: input.tokenEnvVar ?? null, + updatedAt: new Date(), + }) + .returning(); + + if (!row) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to create connected repo.", + }); + } + + return mapRepoRow(row); + }), + + update: workspaceProcedure + .input( + z.object({ + workspace: z.string().min(1), + repoId: z.string().uuid(), + displayName: z.string().min(1).max(200).optional(), + tokenEnvVar: z.string().min(1).max(120).nullable().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + + const [row] = await ctx.db + .update(connectedRepos) + .set({ + ...(input.displayName !== undefined ? { displayName: input.displayName } : {}), + ...(input.tokenEnvVar !== undefined ? { tokenEnvVar: input.tokenEnvVar } : {}), + updatedAt: new Date(), + }) + .where( + and( + eq(connectedRepos.id, input.repoId), + eq(connectedRepos.workspaceId, ctx.workspace.id), + ), + ) + .returning(); + + if (!row) { + throw new TRPCError({ code: "NOT_FOUND", message: "Connected repo not found." }); + } + + return mapRepoRow(row); + }), + + disconnect: workspaceProcedure + .input( + z.object({ + workspace: z.string().min(1), + repoId: z.string().uuid(), + }), + ) + .mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + + const [row] = await ctx.db + .delete(connectedRepos) + .where( + and( + eq(connectedRepos.id, input.repoId), + eq(connectedRepos.workspaceId, ctx.workspace.id), + ), + ) + .returning({ id: connectedRepos.id }); + + if (!row) { + throw new TRPCError({ code: "NOT_FOUND", message: "Connected repo not found." }); + } + + return { ok: true as const }; + }), + + testConnection: workspaceProcedure + .input( + z.object({ + workspace: z.string().min(1), + repoId: z.string().uuid(), + }), + ) + .mutation(async ({ ctx, input }) => { + assertWorkspaceAdmin(ctx); + + const [row] = await ctx.db + .select() + .from(connectedRepos) + .where( + and( + eq(connectedRepos.id, input.repoId), + eq(connectedRepos.workspaceId, ctx.workspace.id), + ), + ) + .limit(1); + + if (!row) { + throw new TRPCError({ code: "NOT_FOUND", message: "Connected repo not found." }); + } + + const started = Date.now(); + const adapter = getForgeAdapter(forgeTypeSchema.parse(row.forgeType)); + const token = readToken(row.tokenEnvVar); + + try { + const metadata = await adapter.fetchRepoMetadata({ + apiBaseUrl: row.apiBaseUrl, + owner: row.owner, + repo: row.repoName, + token, + }); + + await ctx.db + .update(connectedRepos) + .set({ + defaultBranch: metadata.defaultBranch, + metadata: { + ...(row.metadata ?? {}), + cloneUrl: metadata.cloneUrl, + description: metadata.description ?? null, + lastTestedAt: new Date().toISOString(), + }, + updatedAt: new Date(), + }) + .where( + and( + eq(connectedRepos.id, row.id), + eq(connectedRepos.workspaceId, ctx.workspace.id), + ), + ); + + return { + ok: true as const, + latencyMs: Date.now() - started, + defaultBranch: metadata.defaultBranch, + cloneUrl: metadata.cloneUrl, + }; + } catch (error) { + return { + ok: false as const, + latencyMs: Date.now() - started, + error: error instanceof Error ? error.message : "Connection test failed.", + }; + } + }), +}); diff --git a/packages/database/migrations/0012_uneven_captain_stacy.sql b/packages/database/migrations/0012_uneven_captain_stacy.sql new file mode 100644 index 0000000..aa44385 --- /dev/null +++ b/packages/database/migrations/0012_uneven_captain_stacy.sql @@ -0,0 +1,19 @@ +CREATE TABLE "connected_repos" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "workspace_id" uuid NOT NULL, + "display_name" varchar(200) NOT NULL, + "git_remote_url" text NOT NULL, + "forge_type" varchar(40) NOT NULL, + "api_base_url" text NOT NULL, + "owner" varchar(200) NOT NULL, + "repo_name" varchar(200) NOT NULL, + "default_branch" varchar(200), + "token_env_var" varchar(120), + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "connected_repos" ADD CONSTRAINT "connected_repos_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "connected_repos_workspace_id_idx" ON "connected_repos" USING btree ("workspace_id");--> statement-breakpoint +CREATE UNIQUE INDEX "connected_repos_workspace_repo_unique" ON "connected_repos" USING btree ("workspace_id","api_base_url","owner","repo_name"); \ No newline at end of file diff --git a/packages/database/migrations/meta/0012_snapshot.json b/packages/database/migrations/meta/0012_snapshot.json new file mode 100644 index 0000000..b69f1e2 --- /dev/null +++ b/packages/database/migrations/meta/0012_snapshot.json @@ -0,0 +1,3532 @@ +{ + "id": "003cc1b8-19b2-4605-9d3a-db1990b06002", + "prevId": "1e188bcd-0e55-4da9-bca9-ded2ab16f0ff", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.workspace_invites": { + "name": "workspace_invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now() + interval '14 days'" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_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": { + "workspace_invites_workspace_id_idx": { + "name": "workspace_invites_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_invites_token_unique": { + "name": "workspace_invites_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_invites_open_email_unique": { + "name": "workspace_invites_open_email_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_invites\".\"accepted_at\" IS NULL AND \"workspace_invites\".\"revoked_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_invites_workspace_id_workspaces_id_fk": { + "name": "workspace_invites_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_invites", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invites_invited_by_user_id_users_id_fk": { + "name": "workspace_invites_invited_by_user_id_users_id_fk", + "tableFrom": "workspace_invites", + "tableTo": "users", + "columnsFrom": [ + "invited_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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()" + }, + "slug": { + "name": "slug", + "type": "varchar(60)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plan_tier": { + "name": "plan_tier", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspaces_slug_unique": { + "name": "workspaces_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_owner_user_id_idx": { + "name": "workspaces_owner_user_id_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspaces_owner_user_id_users_id_fk": { + "name": "workspaces_owner_user_id_users_id_fk", + "tableFrom": "workspaces", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.object_assignees": { + "name": "object_assignees", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'assignee'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "object_assignees_object_id_user_id_unique": { + "name": "object_assignees_object_id_user_id_unique", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_assignees_object_id_idx": { + "name": "object_assignees_object_id_idx", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_assignees_user_id_idx": { + "name": "object_assignees_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "object_assignees_object_id_objects_id_fk": { + "name": "object_assignees_object_id_objects_id_fk", + "tableFrom": "object_assignees", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "object_assignees_user_id_users_id_fk": { + "name": "object_assignees_user_id_users_id_fk", + "tableFrom": "object_assignees", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.objects": { + "name": "objects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "objects_parent_id_idx": { + "name": "objects_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_type_idx": { + "name": "objects_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_workspace_id_idx": { + "name": "objects_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_template_id_idx": { + "name": "objects_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_created_by_idx": { + "name": "objects_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_type_workspace_id_idx": { + "name": "objects_type_workspace_id_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "objects_workspace_id_workspaces_id_fk": { + "name": "objects_workspace_id_workspaces_id_fk", + "tableFrom": "objects", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "objects_created_by_users_id_fk": { + "name": "objects_created_by_users_id_fk", + "tableFrom": "objects", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "objects_parent_id_objects_id_fk": { + "name": "objects_parent_id_objects_id_fk", + "tableFrom": "objects", + "tableTo": "objects", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "objects_template_id_templates_id_fk": { + "name": "objects_template_id_templates_id_fk", + "tableFrom": "objects", + "tableTo": "templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_members": { + "name": "workspace_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_members_workspace_id_user_id_unique": { + "name": "workspace_members_workspace_id_user_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_members_workspace_id_idx": { + "name": "workspace_members_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_members_user_id_idx": { + "name": "workspace_members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_members_workspace_id_workspaces_id_fk": { + "name": "workspace_members_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_members", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_members_user_id_users_id_fk": { + "name": "workspace_members_user_id_users_id_fk", + "tableFrom": "workspace_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.object_type_defs": { + "name": "object_type_defs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "layout": { + "name": "layout", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "default_properties": { + "name": "default_properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "object_type_defs_workspace_id_idx": { + "name": "object_type_defs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_type_defs_slug_idx": { + "name": "object_type_defs_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "object_type_defs_workspace_id_workspaces_id_fk": { + "name": "object_type_defs_workspace_id_workspaces_id_fk", + "tableFrom": "object_type_defs", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_definitions": { + "name": "property_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "property_definitions_workspace_id_idx": { + "name": "property_definitions_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "property_definitions_workspace_id_name_idx": { + "name": "property_definitions_workspace_id_name_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_definitions_workspace_id_workspaces_id_fk": { + "name": "property_definitions_workspace_id_workspaces_id_fk", + "tableFrom": "property_definitions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_values": { + "name": "property_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "property_def_id": { + "name": "property_def_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "property_values_object_id_property_def_id_unique": { + "name": "property_values_object_id_property_def_id_unique", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_def_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "property_values_object_id_idx": { + "name": "property_values_object_id_idx", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "property_values_property_def_id_idx": { + "name": "property_values_property_def_id_idx", + "columns": [ + { + "expression": "property_def_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_values_object_id_objects_id_fk": { + "name": "property_values_object_id_objects_id_fk", + "tableFrom": "property_values", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "property_values_property_def_id_property_definitions_id_fk": { + "name": "property_values_property_def_id_property_definitions_id_fk", + "tableFrom": "property_values", + "tableTo": "property_definitions", + "columnsFrom": [ + "property_def_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.views": { + "name": "views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "view_type": { + "name": "view_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "views_object_id_idx": { + "name": "views_object_id_idx", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "views_object_id_objects_id_fk": { + "name": "views_object_id_objects_id_fk", + "tableFrom": "views", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "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 + }, + "type": { + "name": "type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "accounts_provider_provider_account_id_unique": { + "name": "accounts_provider_provider_account_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_token": { + "name": "session_token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "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", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_session_token_unique": { + "name": "sessions_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "session_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_email_identities": { + "name": "user_email_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_identities_user_id_idx": { + "name": "user_email_identities_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_email_identities_email_idx": { + "name": "user_email_identities_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_email_identities_user_id_email_unique": { + "name": "user_email_identities_user_id_email_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_email_identities_verified_email_unique": { + "name": "user_email_identities_verified_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_email_identities\".\"verified_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_email_identities_user_id_users_id_fk": { + "name": "user_email_identities_user_id_users_id_fk", + "tableFrom": "user_email_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_lower_unique": { + "name": "users_email_lower_unique", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification_tokens": { + "name": "verification_tokens", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_tokens_identifier_token_pk": { + "name": "verification_tokens_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.object_relations": { + "name": "object_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "relation_type": { + "name": "relation_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "object_relations_source_id_idx": { + "name": "object_relations_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_relations_target_id_idx": { + "name": "object_relations_target_id_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_relations_relation_type_idx": { + "name": "object_relations_relation_type_idx", + "columns": [ + { + "expression": "relation_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_relations_source_target_type_unique": { + "name": "object_relations_source_target_type_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "relation_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "object_relations_source_id_objects_id_fk": { + "name": "object_relations_source_id_objects_id_fk", + "tableFrom": "object_relations", + "tableTo": "objects", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "object_relations_target_id_objects_id_fk": { + "name": "object_relations_target_id_objects_id_fk", + "tableFrom": "object_relations", + "tableTo": "objects", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_workspace_id_idx": { + "name": "templates_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workspace_id_workspaces_id_fk": { + "name": "templates_workspace_id_workspaces_id_fk", + "tableFrom": "templates", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_responses": { + "name": "form_responses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "form_id": { + "name": "form_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "respondent_id": { + "name": "respondent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_object_id": { + "name": "created_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_responses_form_id_idx": { + "name": "form_responses_form_id_idx", + "columns": [ + { + "expression": "form_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_responses_respondent_id_idx": { + "name": "form_responses_respondent_id_idx", + "columns": [ + { + "expression": "respondent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_responses_form_id_forms_id_fk": { + "name": "form_responses_form_id_forms_id_fk", + "tableFrom": "form_responses", + "tableTo": "forms", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_responses_respondent_id_users_id_fk": { + "name": "form_responses_respondent_id_users_id_fk", + "tableFrom": "form_responses", + "tableTo": "users", + "columnsFrom": [ + "respondent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "form_responses_created_object_id_objects_id_fk": { + "name": "form_responses_created_object_id_objects_id_fk", + "tableFrom": "form_responses", + "tableTo": "objects", + "columnsFrom": [ + "created_object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.forms": { + "name": "forms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "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": { + "forms_workspace_id_idx": { + "name": "forms_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "forms_object_id_idx": { + "name": "forms_object_id_idx", + "columns": [ + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "forms_workspace_id_workspaces_id_fk": { + "name": "forms_workspace_id_workspaces_id_fk", + "tableFrom": "forms", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "forms_object_id_objects_id_fk": { + "name": "forms_object_id_objects_id_fk", + "tableFrom": "forms", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "forms_created_by_users_id_fk": { + "name": "forms_created_by_users_id_fk", + "tableFrom": "forms", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_favorites": { + "name": "user_favorites", + "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 + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_favorites_user_object_idx": { + "name": "user_favorites_user_object_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_favorites_user_id_users_id_fk": { + "name": "user_favorites_user_id_users_id_fk", + "tableFrom": "user_favorites", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_favorites_object_id_objects_id_fk": { + "name": "user_favorites_object_id_objects_id_fk", + "tableFrom": "user_favorites", + "tableTo": "objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.markdown_backlog_items": { + "name": "markdown_backlog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "plan_slug": { + "name": "plan_slug", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "epic_slug": { + "name": "epic_slug", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_path": { + "name": "repo_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frontmatter": { + "name": "frontmatter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "body_markdown": { + "name": "body_markdown", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content_hash": { + "name": "content_hash", + "type": "varchar(64)", + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "workflow_prompt": { + "name": "workflow_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "markdown_backlog_workspace_repo_path_unique": { + "name": "markdown_backlog_workspace_repo_path_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "markdown_backlog_workspace_plan_idx": { + "name": "markdown_backlog_workspace_plan_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "markdown_backlog_parent_id_idx": { + "name": "markdown_backlog_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "markdown_backlog_workspace_kind_idx": { + "name": "markdown_backlog_workspace_kind_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "markdown_backlog_items_workspace_id_workspaces_id_fk": { + "name": "markdown_backlog_items_workspace_id_workspaces_id_fk", + "tableFrom": "markdown_backlog_items", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "markdown_backlog_items_parent_id_markdown_backlog_items_id_fk": { + "name": "markdown_backlog_items_parent_id_markdown_backlog_items_id_fk", + "tableFrom": "markdown_backlog_items", + "tableTo": "markdown_backlog_items", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cursor_sync_mappings": { + "name": "cursor_sync_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "backlog_item_id": { + "name": "backlog_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cursor_plan_id": { + "name": "cursor_plan_id", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "cursor_item_id": { + "name": "cursor_item_id", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "last_pulled_at": { + "name": "last_pulled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_pushed_at": { + "name": "last_pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_content_hash": { + "name": "sync_content_hash", + "type": "varchar(64)", + "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": { + "cursor_sync_mappings_backlog_item_id_unique": { + "name": "cursor_sync_mappings_backlog_item_id_unique", + "columns": [ + { + "expression": "backlog_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cursor_sync_mappings_workspace_id_idx": { + "name": "cursor_sync_mappings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cursor_sync_mappings_workspace_id_workspaces_id_fk": { + "name": "cursor_sync_mappings_workspace_id_workspaces_id_fk", + "tableFrom": "cursor_sync_mappings", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cursor_sync_mappings_backlog_item_id_markdown_backlog_items_id_fk": { + "name": "cursor_sync_mappings_backlog_item_id_markdown_backlog_items_id_fk", + "tableFrom": "cursor_sync_mappings", + "tableTo": "markdown_backlog_items", + "columnsFrom": [ + "backlog_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_id_created_at_idx": { + "name": "audit_log_workspace_id_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_user_id_idx": { + "name": "audit_log_actor_user_id_idx", + "columns": [ + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspaces_id_fk": { + "name": "audit_log_workspace_id_workspaces_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_log_actor_user_id_users_id_fk": { + "name": "audit_log_actor_user_id_users_id_fk", + "tableFrom": "audit_log", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runs": { + "name": "agent_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "backlog_item_id": { + "name": "backlog_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_input": { + "name": "tokens_input", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_output": { + "name": "tokens_output", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_total": { + "name": "tokens_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model_alias": { + "name": "model_alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "model_id": { + "name": "model_id", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "model_tier": { + "name": "model_tier", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_runs_workspace_id_started_at_idx": { + "name": "agent_runs_workspace_id_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runs_backlog_item_id_started_at_idx": { + "name": "agent_runs_backlog_item_id_started_at_idx", + "columns": [ + { + "expression": "backlog_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runs_workspace_id_workspaces_id_fk": { + "name": "agent_runs_workspace_id_workspaces_id_fk", + "tableFrom": "agent_runs", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_runs_backlog_item_id_markdown_backlog_items_id_fk": { + "name": "agent_runs_backlog_item_id_markdown_backlog_items_id_fk", + "tableFrom": "agent_runs", + "tableTo": "markdown_backlog_items", + "columnsFrom": [ + "backlog_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_runs_actor_user_id_users_id_fk": { + "name": "agent_runs_actor_user_id_users_id_fk", + "tableFrom": "agent_runs", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_providers": { + "name": "model_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "provider_type": { + "name": "provider_type", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_env_var": { + "name": "api_key_env_var", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "extra_headers": { + "name": "extra_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "default_params": { + "name": "default_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "model_providers_workspace_id_idx": { + "name": "model_providers_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "model_providers_workspace_id_alias_unique": { + "name": "model_providers_workspace_id_alias_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "alias", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_providers_workspace_id_workspaces_id_fk": { + "name": "model_providers_workspace_id_workspaces_id_fk", + "tableFrom": "model_providers", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_model_routes": { + "name": "role_model_routes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "provider_alias_id": { + "name": "provider_alias_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "role_model_routes_workspace_id_idx": { + "name": "role_model_routes_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "role_model_routes_workspace_id_role_id_unique": { + "name": "role_model_routes_workspace_id_role_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "role_model_routes_provider_alias_id_idx": { + "name": "role_model_routes_provider_alias_id_idx", + "columns": [ + { + "expression": "provider_alias_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_model_routes_workspace_id_workspaces_id_fk": { + "name": "role_model_routes_workspace_id_workspaces_id_fk", + "tableFrom": "role_model_routes", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_model_routes_provider_alias_id_model_providers_id_fk": { + "name": "role_model_routes_provider_alias_id_model_providers_id_fk", + "tableFrom": "role_model_routes", + "tableTo": "model_providers", + "columnsFrom": [ + "provider_alias_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connected_repos": { + "name": "connected_repos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "git_remote_url": { + "name": "git_remote_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "forge_type": { + "name": "forge_type", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "api_base_url": { + "name": "api_base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "token_env_var": { + "name": "token_env_var", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "connected_repos_workspace_id_idx": { + "name": "connected_repos_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connected_repos_workspace_repo_unique": { + "name": "connected_repos_workspace_repo_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "api_base_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connected_repos_workspace_id_workspaces_id_fk": { + "name": "connected_repos_workspace_id_workspaces_id_fk", + "tableFrom": "connected_repos", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 734682c..f6c76f7 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1783451144455, "tag": "0011_cool_captain_america", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1783451965716, + "tag": "0012_uneven_captain_stacy", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/database/src/schema/connected_repos.ts b/packages/database/src/schema/connected_repos.ts new file mode 100644 index 0000000..b6971e9 --- /dev/null +++ b/packages/database/src/schema/connected_repos.ts @@ -0,0 +1,47 @@ +import { + index, + jsonb, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, + varchar, +} from "drizzle-orm/pg-core"; + +import { workspaces } from "./workspaces"; + +/** Workspace-scoped git remote; API tokens live in env vars referenced by token_env_var. */ +export const connectedRepos = pgTable( + "connected_repos", + { + id: uuid("id").primaryKey().defaultRandom(), + workspaceId: uuid("workspace_id") + .notNull() + .references(() => workspaces.id, { onDelete: "cascade" }), + displayName: varchar("display_name", { length: 200 }).notNull(), + gitRemoteUrl: text("git_remote_url").notNull(), + forgeType: varchar("forge_type", { length: 40 }).notNull(), + apiBaseUrl: text("api_base_url").notNull(), + owner: varchar("owner", { length: 200 }).notNull(), + repoName: varchar("repo_name", { length: 200 }).notNull(), + defaultBranch: varchar("default_branch", { length: 200 }), + tokenEnvVar: varchar("token_env_var", { length: 120 }), + metadata: jsonb("metadata").$type | null>(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }, + (table) => ({ + workspaceIdIdx: index("connected_repos_workspace_id_idx").on(table.workspaceId), + workspaceRepoUnique: uniqueIndex("connected_repos_workspace_repo_unique").on( + table.workspaceId, + table.apiBaseUrl, + table.owner, + table.repoName, + ), + }), +); diff --git a/packages/database/src/schema/index.ts b/packages/database/src/schema/index.ts index 0e57ad5..efbc5ea 100644 --- a/packages/database/src/schema/index.ts +++ b/packages/database/src/schema/index.ts @@ -15,3 +15,4 @@ export * from "./audit"; export * from "./agent_runs"; export * from "./model_providers"; export * from "./role_model_routes"; +export * from "./connected_repos"; diff --git a/packages/database/src/schema/relations.ts b/packages/database/src/schema/relations.ts index 3bbbcc7..b0cbaaa 100644 --- a/packages/database/src/schema/relations.ts +++ b/packages/database/src/schema/relations.ts @@ -21,6 +21,7 @@ import { auditLog } from "./audit"; import { agentRuns } from "./agent_runs"; import { modelProviders } from "./model_providers"; import { roleModelRoutes } from "./role_model_routes"; +import { connectedRepos } from "./connected_repos"; export const objectRelations = pgTable( "object_relations", @@ -120,6 +121,7 @@ export const workspacesRelations = relations(workspaces, ({ one, many }) => ({ markdownBacklogItems: many(markdownBacklogItems), modelProviders: many(modelProviders), roleModelRoutes: many(roleModelRoutes), + connectedRepos: many(connectedRepos), })); export const objectsRelations = relations(objects, ({ one, many }) => ({ @@ -277,6 +279,13 @@ export const modelProvidersRelations = relations(modelProviders, ({ one, many }) roleRoutes: many(roleModelRoutes), })); +export const connectedReposRelations = relations(connectedRepos, ({ one }) => ({ + workspace: one(workspaces, { + fields: [connectedRepos.workspaceId], + references: [workspaces.id], + }), +})); + export const roleModelRoutesRelations = relations(roleModelRoutes, ({ one }) => ({ workspace: one(workspaces, { fields: [roleModelRoutes.workspaceId], diff --git a/packages/shared/package.json b/packages/shared/package.json index ce1e22a..496b86c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -7,6 +7,7 @@ "types": "./src/index.ts", "exports": { ".": "./src/index.ts", + "./connected-repos": "./src/connected-repos/index.ts", "./model-routing": "./src/model-routing/index.ts", "./types": "./src/types/index.ts", "./utils": "./src/utils/index.ts" diff --git a/packages/shared/src/connected-repos/index.ts b/packages/shared/src/connected-repos/index.ts new file mode 100644 index 0000000..3e17043 --- /dev/null +++ b/packages/shared/src/connected-repos/index.ts @@ -0,0 +1,11 @@ +export { + connectedRepoSchema, + forgeTypeSchema, + parsedGitRemoteSchema, + repoMetadataSchema, + type ConnectedRepo, + type ForgeType, + type ParsedGitRemote, + type RepoMetadata, +} from "./schemas"; +export { parseGitRemoteUrl } from "./parse-git-remote"; diff --git a/packages/shared/src/connected-repos/parse-git-remote.test.ts b/packages/shared/src/connected-repos/parse-git-remote.test.ts new file mode 100644 index 0000000..255b647 --- /dev/null +++ b/packages/shared/src/connected-repos/parse-git-remote.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; + +import { parseGitRemoteUrl } from "./parse-git-remote"; + +describe("parseGitRemoteUrl", () => { + it("parses GitHub HTTPS remotes", () => { + const parsed = parseGitRemoteUrl("https://github.com/stwl-labs/ubiquitous-invention.git"); + expect(parsed).toMatchObject({ + forgeType: "github", + owner: "stwl-labs", + repoName: "ubiquitous-invention", + apiBaseUrl: "https://api.github.com", + webUrl: "https://github.com/stwl-labs/ubiquitous-invention", + }); + }); + + it("parses GitHub SSH remotes", () => { + const parsed = parseGitRemoteUrl("git@github.com:stwl-labs/ubiquitous-invention.git"); + expect(parsed.forgeType).toBe("github"); + expect(parsed.owner).toBe("stwl-labs"); + expect(parsed.repoName).toBe("ubiquitous-invention"); + }); + + it("parses self-hosted forge as forgejo", () => { + const parsed = parseGitRemoteUrl("https://git.example.com/acme/widget.git"); + expect(parsed).toMatchObject({ + forgeType: "forgejo", + owner: "acme", + repoName: "widget", + apiBaseUrl: "https://git.example.com/api/v1", + }); + }); + + it("parses GitLab.com remotes", () => { + const parsed = parseGitRemoteUrl("https://gitlab.com/group/project"); + expect(parsed.forgeType).toBe("gitlab"); + expect(parsed.apiBaseUrl).toBe("https://gitlab.com/api/v4"); + }); + + it("rejects empty input", () => { + expect(() => parseGitRemoteUrl(" ")).toThrow(/required/i); + }); + + it("rejects owner-only paths", () => { + expect(() => parseGitRemoteUrl("https://github.com/stwl-labs")).toThrow( + /owner and repository/i, + ); + }); +}); diff --git a/packages/shared/src/connected-repos/parse-git-remote.ts b/packages/shared/src/connected-repos/parse-git-remote.ts new file mode 100644 index 0000000..1b2eff0 --- /dev/null +++ b/packages/shared/src/connected-repos/parse-git-remote.ts @@ -0,0 +1,115 @@ +import { forgeTypeSchema, type ForgeType, type ParsedGitRemote } from "./schemas"; + +const HOST_FORGE_MAP: Record = { + "github.com": "github", + "gitlab.com": "gitlab", + "bitbucket.org": "bitbucket", +}; + +function detectForgeType(host: string): ForgeType { + const normalized = host.toLowerCase(); + return HOST_FORGE_MAP[normalized] ?? "forgejo"; +} + +function stripGitSuffix(value: string): string { + return value.replace(/\.git$/i, ""); +} + +function buildParsedRemote(input: { + forgeType: ForgeType; + host: string; + protocol: string; + owner: string; + repo: string; + gitRemoteUrl: string; +}): ParsedGitRemote { + const owner = stripGitSuffix(input.owner); + const repoName = stripGitSuffix(input.repo); + const webUrl = `${input.protocol}//${input.host}/${owner}/${repoName}`; + + let apiBaseUrl: string; + switch (input.forgeType) { + case "github": + apiBaseUrl = "https://api.github.com"; + break; + case "gitlab": + apiBaseUrl = `${input.protocol}//${input.host}/api/v4`; + break; + case "bitbucket": + apiBaseUrl = "https://api.bitbucket.org/2.0"; + break; + case "forgejo": + apiBaseUrl = `${input.protocol}//${input.host}/api/v1`; + break; + } + + return { + forgeType: forgeTypeSchema.parse(input.forgeType), + gitRemoteUrl: input.gitRemoteUrl, + webUrl, + apiBaseUrl, + owner, + repoName, + }; +} + +/** + * Parse a pasted git remote (HTTPS or git@host:owner/repo) into forge metadata. + */ +export function parseGitRemoteUrl(input: string): ParsedGitRemote { + const raw = input.trim(); + if (!raw) { + throw new Error("Remote URL is required."); + } + + const scpMatch = /^git@([^:]+):([^/]+)\/(.+)$/i.exec(raw); + if (scpMatch) { + const [, host, owner, repo] = scpMatch; + if (!host || !owner || !repo) { + throw new Error("Invalid SSH git remote URL."); + } + const forgeType = detectForgeType(host); + return buildParsedRemote({ + forgeType, + host, + protocol: "https:", + owner, + repo, + gitRemoteUrl: raw, + }); + } + + let url: URL; + try { + url = new URL(raw); + } catch { + try { + url = new URL(`https://${raw}`); + } catch { + throw new Error("Invalid git remote URL."); + } + } + + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new Error("Git remote URL must use http(s) or git@host:owner/repo form."); + } + + const segments = url.pathname.split("/").filter(Boolean); + if (segments.length < 2) { + throw new Error("Remote URL must include owner and repository name."); + } + + const owner = segments[0]!; + const repo = segments[1]!; + const forgeType = detectForgeType(url.hostname); + const gitRemoteUrl = url.toString(); + + return buildParsedRemote({ + forgeType, + host: url.hostname, + protocol: url.protocol, + owner, + repo, + gitRemoteUrl, + }); +} diff --git a/packages/shared/src/connected-repos/schemas.ts b/packages/shared/src/connected-repos/schemas.ts new file mode 100644 index 0000000..8c5ce12 --- /dev/null +++ b/packages/shared/src/connected-repos/schemas.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +export const forgeTypeSchema = z.enum([ + "github", + "forgejo", + "gitlab", + "bitbucket", +]); + +export type ForgeType = z.infer; + +export const parsedGitRemoteSchema = z.object({ + forgeType: forgeTypeSchema, + gitRemoteUrl: z.string().url(), + webUrl: z.string().url(), + apiBaseUrl: z.string().url(), + owner: z.string().min(1).max(200), + repoName: z.string().min(1).max(200), +}); + +export type ParsedGitRemote = z.infer; + +export const connectedRepoSchema = z.object({ + id: z.string().uuid(), + workspaceId: z.string().uuid(), + displayName: z.string().min(1).max(200), + gitRemoteUrl: z.string().url(), + forgeType: forgeTypeSchema, + apiBaseUrl: z.string().url(), + owner: z.string().min(1).max(200), + repoName: z.string().min(1).max(200), + defaultBranch: z.string().min(1).max(200).nullable(), + tokenEnvVar: z.string().min(1).max(120).nullable(), + metadata: z.record(z.unknown()).nullable(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), +}); + +export type ConnectedRepo = z.infer; + +export const repoMetadataSchema = z.object({ + defaultBranch: z.string().min(1), + cloneUrl: z.string().url(), + description: z.string().optional(), +}); + +export type RepoMetadata = z.infer; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index f440e17..e5f6343 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,3 +1,4 @@ +export * from "./connected-repos"; export * from "./model-routing"; export * from "./types"; export * from "./utils"; diff --git a/plans/Plan-automation-foundation/Epic-connected-repos/Epic-connected-repos.md b/plans/Plan-automation-foundation/Epic-connected-repos/Epic-connected-repos.md index 9e81be2..c376eea 100644 --- a/plans/Plan-automation-foundation/Epic-connected-repos/Epic-connected-repos.md +++ b/plans/Plan-automation-foundation/Epic-connected-repos/Epic-connected-repos.md @@ -3,7 +3,7 @@ kind: epic slug: connected-repos title: Connected repos — paste remote URL, forge adapters plan_slug: automation-foundation -status: draft +status: done priority: P1 tenant_id: global cursor_epic_id: null @@ -40,9 +40,12 @@ See automation foundation discussion: Vercel/Coolify-style “show me your repo. ## Acceptance criteria -- [ ] Admin pastes Forgejo URL; Echodo stores link and detects `forge_type`. -- [ ] Worker/orchestrator can read `git_remote_url` + default branch from API. +- [x] Admin pastes Forgejo URL; Echodo stores link and detects `forge_type`. +- [x] Worker/orchestrator can read `git_remote_url` + default branch from API. ## Tasks -_To be decomposed when epic graduates to `ready`._ +- [x] [Task-shared-forge-schemas](./Task-shared-forge-schemas.md) +- [x] [Task-db-connected-repos-migration](./Task-db-connected-repos-migration.md) +- [x] [Task-forge-adapters-trpc](./Task-forge-adapters-trpc.md) +- [x] [Task-settings-connected-repos-ui](./Task-settings-connected-repos-ui.md) diff --git a/plans/Plan-automation-foundation/Epic-connected-repos/Task-db-connected-repos-migration.md b/plans/Plan-automation-foundation/Epic-connected-repos/Task-db-connected-repos-migration.md new file mode 100644 index 0000000..2eab6a6 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-connected-repos/Task-db-connected-repos-migration.md @@ -0,0 +1,23 @@ +--- +kind: task +slug: db-connected-repos-migration +title: Drizzle schema and migration for connected_repos +plan_slug: automation-foundation +epic_slug: connected-repos +status: done +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-07-07" +--- + +# Task summary + +Workspace-scoped `connected_repos` table with forge metadata and env-var token reference. + +## Acceptance criteria + +- [x] Migration `0012` generated via `pnpm db:generate` +- [x] Unique constraint per workspace + api base + owner + repo +- [x] `workspace_id` index diff --git a/plans/Plan-automation-foundation/Epic-connected-repos/Task-forge-adapters-trpc.md b/plans/Plan-automation-foundation/Epic-connected-repos/Task-forge-adapters-trpc.md new file mode 100644 index 0000000..427ad85 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-connected-repos/Task-forge-adapters-trpc.md @@ -0,0 +1,23 @@ +--- +kind: task +slug: forge-adapters-trpc +title: Forge adapters and connectedRepos tRPC router +plan_slug: automation-foundation +epic_slug: connected-repos +status: done +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-07-07" +--- + +# Task summary + +GitHub and Forgejo/Gitea API adapters plus `api.connectedRepos.*` CRUD and test connection. + +## Acceptance criteria + +- [x] `getForgeAdapter()` for github and forgejo +- [x] `testConnection` fetches default branch and persists metadata +- [x] Admin-only mutations; all queries filter by `workspace_id` diff --git a/plans/Plan-automation-foundation/Epic-connected-repos/Task-settings-connected-repos-ui.md b/plans/Plan-automation-foundation/Epic-connected-repos/Task-settings-connected-repos-ui.md new file mode 100644 index 0000000..50aeede --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-connected-repos/Task-settings-connected-repos-ui.md @@ -0,0 +1,23 @@ +--- +kind: task +slug: settings-connected-repos-ui +title: Settings UI — connect / disconnect / test repo +plan_slug: automation-foundation +epic_slug: connected-repos +status: done +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-07-07" +--- + +# Task summary + +Paste-remote-URL UX at `/{workspaceSlug}/settings/repos` with parse preview and connection test. + +## Acceptance criteria + +- [x] List connected repos with forge badge and default branch +- [x] Connect dialog with parse + token env var field +- [x] Top header link under workspace menu diff --git a/plans/Plan-automation-foundation/Epic-connected-repos/Task-shared-forge-schemas.md b/plans/Plan-automation-foundation/Epic-connected-repos/Task-shared-forge-schemas.md new file mode 100644 index 0000000..fc1a606 --- /dev/null +++ b/plans/Plan-automation-foundation/Epic-connected-repos/Task-shared-forge-schemas.md @@ -0,0 +1,23 @@ +--- +kind: task +slug: shared-forge-schemas +title: Shared forge types, connected repo schemas, and parseGitRemoteUrl +plan_slug: automation-foundation +epic_slug: connected-repos +status: done +priority: P1 +tenant_id: global +owner: unassigned +cursor_todo_id: null +updated_at: "2026-07-07" +--- + +# Task summary + +Zod schemas and pure URL parser for pasted git remotes (GitHub, Forgejo, GitLab, Bitbucket). + +## Acceptance criteria + +- [x] `parseGitRemoteUrl` handles HTTPS and `git@host:owner/repo` forms +- [x] Vitest coverage for GitHub, Forgejo self-hosted, GitLab.com +- [x] Exported from `@tasks/shared/connected-repos` diff --git a/plans/Plan-automation-foundation/Plan-automation-foundation.md b/plans/Plan-automation-foundation/Plan-automation-foundation.md index 454c9ef..ef3a621 100644 --- a/plans/Plan-automation-foundation/Plan-automation-foundation.md +++ b/plans/Plan-automation-foundation/Plan-automation-foundation.md @@ -164,7 +164,7 @@ flowchart LR |---|------|--------|----------|------------| | 1 | [Model provider registry](./Epic-model-provider-registry/Epic-model-provider-registry.md) | **ready** (6 tasks) | `resolveModel()`, Settings AI pages, run model logging | — | | 2 | [Pipeline convoy scaffolding](./Epic-pipeline-convoy-scaffolding/Epic-pipeline-convoy-scaffolding.md) | done | `.convoys/`, `log-convoy-event.sh`, `convoy-planning.mdc` — dogfood metrics | — (parallel with 1) | -| 3 | [Connected repos](./Epic-connected-repos/Epic-connected-repos.md) | draft | `repo_links` table, “paste remote URL”, forge adapter interface | — | +| 3 | [Connected repos](./Epic-connected-repos/Epic-connected-repos.md) | done | `repo_links` table, “paste remote URL”, forge adapter interface | — | | 4 | [Convoy lifecycle MCP](./Epic-convoy-lifecycle-mcp/Epic-convoy-lifecycle-mcp.md) | draft | `create_convoy`, `transition_convoy_status`, state machine §7, `convoy_events` | 2, 3 | | 5 | [Mission control gates UI](./Epic-mission-control-gates-ui/Epic-mission-control-gates-ui.md) | draft | Convoy kanban, gate inbox, approve/deny/revise, gate co-pilot chat | 4 | | 6 | [Orchestrator](./Epic-apps-orchestrator/Epic-apps-orchestrator.md) | draft | `apps/orchestrator`, Redis queue, role dispatch, pause at gates | 1, 3, 4 |