Support Supabase branches (#1394)
<!-- This is an auto-generated description by cubic. -->
## Summary by cubic
Adds Supabase database branch selection per app, with a new schema field
and UI to choose a branch after connecting a project. Resets branch when
changing or disconnecting the project to keep state consistent.
- **New Features**
- Added apps.supabase_branch_id column.
- Branch dropdown in SupabaseConnector shown after a project is
connected; selection persists and triggers app refresh.
- New state and hooks: supabaseBranchesAtom, loadBranches(projectId),
setAppBranch(branchId).
- IPC endpoints: supabase:list-branches and supabase:set-app-branch;
setting/unsetting project also clears the branch.
- **Migration**
- Apply drizzle migration 0013_supabase_branch.sql to add the
supabase_branch_id column (defaults to null).
<!-- End of auto-generated description by cubic. -->
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Adds Supabase database branch selection per app, including parent
project tracking, new IPC endpoints, UI dropdown, and an accompanying DB
migration with e2e tests.
>
> - **Database**:
> - Add `apps.supabase_parent_project_id` via migration
`drizzle/0015_complete_old_lace.sql`; snapshot and journal updated.
> - **IPC/Main**:
> - New `supabase:list-branches` handler and management client
`listSupabaseBranches` (real API + test stubs).
> - Update `supabase:set-app-project` to accept `{ projectId,
parentProjectId?, appId }`; unset clears both IDs.
> - `get-app` resolves `supabaseProjectName` using
`supabase_parent_project_id` when present.
> - **Types & Client**:
> - Add `SupabaseBranch`, `SetSupabaseAppProjectParams`, and
`App.supabaseParentProjectId`; expose `listSupabaseBranches` and updated
`setSupabaseAppProject` in `ipc_client` and preload whitelist.
> - **UI/Hooks**:
> - Supabase UI: branch dropdown in `SupabaseConnector` with
`loadBranches`, selection persists via updated `setAppProject`.
> - State: add `supabaseBranchesAtom`; `useSupabase` gets `branches`,
`loadBranches`, new param shape for `setAppProject`.
> - TokenBar/ChatInput: add `data-testid` for token bar and toggle.
> - **Supabase Context (tests)**:
> - Test build returns large context for `test-branch-project-id` to
validate branch selection.
> - **E2E Tests**:
> - Add `supabase_branch.spec.ts` and snapshot verifying branch
selection affects token usage.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
33054278db8396b4371ed6e8224105cb5684b7ac. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
@@ -724,7 +724,9 @@ export function registerAppHandlers() {
|
||||
let supabaseProjectName: string | null = null;
|
||||
const settings = readSettings();
|
||||
if (app.supabaseProjectId && settings.supabase?.accessToken?.value) {
|
||||
supabaseProjectName = await getSupabaseProjectName(app.supabaseProjectId);
|
||||
supabaseProjectName = await getSupabaseProjectName(
|
||||
app.supabaseParentProjectId || app.supabaseProjectId,
|
||||
);
|
||||
}
|
||||
|
||||
let vercelTeamSlug: string | null = null;
|
||||
|
||||
@@ -2,7 +2,10 @@ import log from "electron-log";
|
||||
import { db } from "../../db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { apps } from "../../db/schema";
|
||||
import { getSupabaseClient } from "../../supabase_admin/supabase_management_client";
|
||||
import {
|
||||
getSupabaseClient,
|
||||
listSupabaseBranches,
|
||||
} from "../../supabase_admin/supabase_management_client";
|
||||
import {
|
||||
createLoggedHandler,
|
||||
createTestOnlyLoggedHandler,
|
||||
@@ -10,6 +13,8 @@ import {
|
||||
import { handleSupabaseOAuthReturn } from "../../supabase_admin/supabase_return_handler";
|
||||
import { safeSend } from "../utils/safe_sender";
|
||||
|
||||
import { SetSupabaseAppProjectParams, SupabaseBranch } from "../ipc_types";
|
||||
|
||||
const logger = log.scope("supabase_handlers");
|
||||
const handle = createLoggedHandler(logger);
|
||||
const testOnlyHandle = createTestOnlyLoggedHandler(logger);
|
||||
@@ -20,16 +25,44 @@ export function registerSupabaseHandlers() {
|
||||
return supabase.getProjects();
|
||||
});
|
||||
|
||||
// List branches for a Supabase project (database branches)
|
||||
handle(
|
||||
"supabase:list-branches",
|
||||
async (
|
||||
_,
|
||||
{ projectId }: { projectId: string },
|
||||
): Promise<Array<SupabaseBranch>> => {
|
||||
const branches = await listSupabaseBranches({
|
||||
supabaseProjectId: projectId,
|
||||
});
|
||||
return branches.map((branch) => ({
|
||||
id: branch.id,
|
||||
name: branch.name,
|
||||
isDefault: branch.is_default,
|
||||
projectRef: branch.project_ref,
|
||||
parentProjectRef: branch.parent_project_ref,
|
||||
}));
|
||||
},
|
||||
);
|
||||
|
||||
// Set app project - links a Dyad app to a Supabase project
|
||||
handle(
|
||||
"supabase:set-app-project",
|
||||
async (_, { project, app }: { project: string; app: number }) => {
|
||||
async (
|
||||
_,
|
||||
{ projectId, appId, parentProjectId }: SetSupabaseAppProjectParams,
|
||||
) => {
|
||||
await db
|
||||
.update(apps)
|
||||
.set({ supabaseProjectId: project })
|
||||
.where(eq(apps.id, app));
|
||||
.set({
|
||||
supabaseProjectId: projectId,
|
||||
supabaseParentProjectId: parentProjectId,
|
||||
})
|
||||
.where(eq(apps.id, appId));
|
||||
|
||||
logger.info(`Associated app ${app} with Supabase project ${project}`);
|
||||
logger.info(
|
||||
`Associated app ${appId} with Supabase project ${projectId} ${parentProjectId ? `and parent project ${parentProjectId}` : ""}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -37,7 +70,7 @@ export function registerSupabaseHandlers() {
|
||||
handle("supabase:unset-app-project", async (_, { app }: { app: number }) => {
|
||||
await db
|
||||
.update(apps)
|
||||
.set({ supabaseProjectId: null })
|
||||
.set({ supabaseProjectId: null, supabaseParentProjectId: null })
|
||||
.where(eq(apps.id, app));
|
||||
|
||||
logger.info(`Removed Supabase project association for app ${app}`);
|
||||
|
||||
@@ -66,6 +66,8 @@ import type {
|
||||
McpServerUpdate,
|
||||
CreateMcpServer,
|
||||
CloneRepoParams,
|
||||
SupabaseBranch,
|
||||
SetSupabaseAppProjectParams,
|
||||
} from "./ipc_types";
|
||||
import type { Template } from "../shared/templates";
|
||||
import type {
|
||||
@@ -961,14 +963,16 @@ export class IpcClient {
|
||||
return this.ipcRenderer.invoke("supabase:list-projects");
|
||||
}
|
||||
|
||||
public async listSupabaseBranches(params: {
|
||||
projectId: string;
|
||||
}): Promise<SupabaseBranch[]> {
|
||||
return this.ipcRenderer.invoke("supabase:list-branches", params);
|
||||
}
|
||||
|
||||
public async setSupabaseAppProject(
|
||||
project: string,
|
||||
app: number,
|
||||
params: SetSupabaseAppProjectParams,
|
||||
): Promise<void> {
|
||||
await this.ipcRenderer.invoke("supabase:set-app-project", {
|
||||
project,
|
||||
app,
|
||||
});
|
||||
await this.ipcRenderer.invoke("supabase:set-app-project", params);
|
||||
}
|
||||
|
||||
public async unsetSupabaseAppProject(app: number): Promise<void> {
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface App {
|
||||
githubRepo: string | null;
|
||||
githubBranch: string | null;
|
||||
supabaseProjectId: string | null;
|
||||
supabaseParentProjectId: string | null;
|
||||
supabaseProjectName: string | null;
|
||||
neonProjectId: string | null;
|
||||
neonDevelopmentBranchId: string | null;
|
||||
@@ -508,3 +509,17 @@ export type CloneRepoReturnType =
|
||||
| {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export interface SupabaseBranch {
|
||||
id: string;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
projectRef: string;
|
||||
parentProjectRef: string;
|
||||
}
|
||||
|
||||
export interface SetSupabaseAppProjectParams {
|
||||
projectId: string;
|
||||
parentProjectId?: string;
|
||||
appId: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user