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:
1
drizzle/0015_complete_old_lace.sql
Normal file
1
drizzle/0015_complete_old_lace.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `apps` ADD `supabase_parent_project_id` text;
|
||||||
753
drizzle/meta/0015_snapshot.json
Normal file
753
drizzle/meta/0015_snapshot.json
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "41549b62-b247-48d5-90e1-6bfc70f02040",
|
||||||
|
"prevId": "340e33e4-c82c-44fb-afda-29943bd6bf62",
|
||||||
|
"tables": {
|
||||||
|
"apps": {
|
||||||
|
"name": "apps",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"github_org": {
|
||||||
|
"name": "github_org",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"github_repo": {
|
||||||
|
"name": "github_repo",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"github_branch": {
|
||||||
|
"name": "github_branch",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"supabase_project_id": {
|
||||||
|
"name": "supabase_project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"supabase_parent_project_id": {
|
||||||
|
"name": "supabase_parent_project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"neon_project_id": {
|
||||||
|
"name": "neon_project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"neon_development_branch_id": {
|
||||||
|
"name": "neon_development_branch_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"neon_preview_branch_id": {
|
||||||
|
"name": "neon_preview_branch_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vercel_project_id": {
|
||||||
|
"name": "vercel_project_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vercel_project_name": {
|
||||||
|
"name": "vercel_project_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vercel_team_id": {
|
||||||
|
"name": "vercel_team_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vercel_deployment_url": {
|
||||||
|
"name": "vercel_deployment_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"install_command": {
|
||||||
|
"name": "install_command",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"start_command": {
|
||||||
|
"name": "start_command",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"chat_context": {
|
||||||
|
"name": "chat_context",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_favorite": {
|
||||||
|
"name": "is_favorite",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"chats": {
|
||||||
|
"name": "chats",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"app_id": {
|
||||||
|
"name": "app_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"initial_commit_hash": {
|
||||||
|
"name": "initial_commit_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"chats_app_id_apps_id_fk": {
|
||||||
|
"name": "chats_app_id_apps_id_fk",
|
||||||
|
"tableFrom": "chats",
|
||||||
|
"tableTo": "apps",
|
||||||
|
"columnsFrom": [
|
||||||
|
"app_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"language_model_providers": {
|
||||||
|
"name": "language_model_providers",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"api_base_url": {
|
||||||
|
"name": "api_base_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"env_var_name": {
|
||||||
|
"name": "env_var_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"language_models": {
|
||||||
|
"name": "language_models",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"display_name": {
|
||||||
|
"name": "display_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"api_name": {
|
||||||
|
"name": "api_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"builtin_provider_id": {
|
||||||
|
"name": "builtin_provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_provider_id": {
|
||||||
|
"name": "custom_provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"max_output_tokens": {
|
||||||
|
"name": "max_output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"context_window": {
|
||||||
|
"name": "context_window",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"language_models_custom_provider_id_language_model_providers_id_fk": {
|
||||||
|
"name": "language_models_custom_provider_id_language_model_providers_id_fk",
|
||||||
|
"tableFrom": "language_models",
|
||||||
|
"tableTo": "language_model_providers",
|
||||||
|
"columnsFrom": [
|
||||||
|
"custom_provider_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"mcp_servers": {
|
||||||
|
"name": "mcp_servers",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"name": "transport",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"name": "command",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"name": "args",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"env_json": {
|
||||||
|
"name": "env_json",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "0"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"mcp_tool_consents": {
|
||||||
|
"name": "mcp_tool_consents",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"server_id": {
|
||||||
|
"name": "server_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tool_name": {
|
||||||
|
"name": "tool_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"consent": {
|
||||||
|
"name": "consent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'ask'"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"uniq_mcp_consent": {
|
||||||
|
"name": "uniq_mcp_consent",
|
||||||
|
"columns": [
|
||||||
|
"server_id",
|
||||||
|
"tool_name"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"mcp_tool_consents_server_id_mcp_servers_id_fk": {
|
||||||
|
"name": "mcp_tool_consents_server_id_mcp_servers_id_fk",
|
||||||
|
"tableFrom": "mcp_tool_consents",
|
||||||
|
"tableTo": "mcp_servers",
|
||||||
|
"columnsFrom": [
|
||||||
|
"server_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"name": "messages",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"chat_id": {
|
||||||
|
"name": "chat_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"approval_state": {
|
||||||
|
"name": "approval_state",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"commit_hash": {
|
||||||
|
"name": "commit_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"request_id": {
|
||||||
|
"name": "request_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"messages_chat_id_chats_id_fk": {
|
||||||
|
"name": "messages_chat_id_chats_id_fk",
|
||||||
|
"tableFrom": "messages",
|
||||||
|
"tableTo": "chats",
|
||||||
|
"columnsFrom": [
|
||||||
|
"chat_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"name": "prompts",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"name": "versions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"app_id": {
|
||||||
|
"name": "app_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"commit_hash": {
|
||||||
|
"name": "commit_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"neon_db_timestamp": {
|
||||||
|
"name": "neon_db_timestamp",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(unixepoch())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"versions_app_commit_unique": {
|
||||||
|
"name": "versions_app_commit_unique",
|
||||||
|
"columns": [
|
||||||
|
"app_id",
|
||||||
|
"commit_hash"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"versions_app_id_apps_id_fk": {
|
||||||
|
"name": "versions_app_id_apps_id_fk",
|
||||||
|
"tableFrom": "versions",
|
||||||
|
"tableTo": "apps",
|
||||||
|
"columnsFrom": [
|
||||||
|
"app_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,6 +106,13 @@
|
|||||||
"when": 1760034009367,
|
"when": 1760034009367,
|
||||||
"tag": "0014_needy_vertigo",
|
"tag": "0014_needy_vertigo",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1760474402750,
|
||||||
|
"tag": "0015_complete_old_lace",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- text: "/Tokens: \\d+,\\d+ \\d+% of [\\d,.]+[bkmBKM]+ Optimize your tokens with Dyad Pro's Smart Context/"
|
||||||
25
e2e-tests/supabase_branch.spec.ts
Normal file
25
e2e-tests/supabase_branch.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { testSkipIfWindows } from "./helpers/test_helper";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
testSkipIfWindows("supabase branch selection works", async ({ po }) => {
|
||||||
|
await po.setUp({ autoApprove: true });
|
||||||
|
await po.importApp("minimal");
|
||||||
|
await po.sendPrompt("tc=add-supabase");
|
||||||
|
|
||||||
|
// Connect to Supabase
|
||||||
|
await po.page.getByText("Set up supabase").click();
|
||||||
|
await po.clickConnectSupabaseButton();
|
||||||
|
await po.clickBackButton();
|
||||||
|
await po.page.getByTestId("token-bar-toggle").click();
|
||||||
|
// The default branch has a small context.
|
||||||
|
await expect(po.page.getByTestId("token-bar")).toContainText("6% of 128K");
|
||||||
|
|
||||||
|
await po.getTitleBarAppNameButton().click();
|
||||||
|
await po.page.getByTestId("supabase-branch-select").click();
|
||||||
|
await po.page.getByRole("option", { name: "Test Branch" }).click();
|
||||||
|
|
||||||
|
await po.clickBackButton();
|
||||||
|
// The test branch has a large context (200k tokens) so it'll hit the 100% limit.
|
||||||
|
// This is to make sure we're connecting to the right supabase project for the branch.
|
||||||
|
await expect(po.page.getByTestId("token-bar")).toContainText("100% of 128K");
|
||||||
|
});
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
|
import { SupabaseBranch } from "@/ipc/ipc_types";
|
||||||
|
|
||||||
// Define atom for storing the list of Supabase projects
|
// Define atom for storing the list of Supabase projects
|
||||||
export const supabaseProjectsAtom = atom<any[]>([]);
|
export const supabaseProjectsAtom = atom<any[]>([]);
|
||||||
|
export const supabaseBranchesAtom = atom<SupabaseBranch[]>([]);
|
||||||
|
|
||||||
// Define atom for tracking loading state
|
// Define atom for tracking loading state
|
||||||
export const supabaseLoadingAtom = atom<boolean>(false);
|
export const supabaseLoadingAtom = atom<boolean>(false);
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export function SupabaseConnector({ appId }: { appId: number }) {
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
loadProjects,
|
loadProjects,
|
||||||
|
branches,
|
||||||
|
loadBranches,
|
||||||
setAppProject,
|
setAppProject,
|
||||||
unsetAppProject,
|
unsetAppProject,
|
||||||
} = useSupabase();
|
} = useSupabase();
|
||||||
@@ -70,7 +72,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
|
|||||||
|
|
||||||
const handleProjectSelect = async (projectId: string) => {
|
const handleProjectSelect = async (projectId: string) => {
|
||||||
try {
|
try {
|
||||||
await setAppProject(projectId, appId);
|
await setAppProject({ projectId, appId });
|
||||||
toast.success("Project connected to app successfully");
|
toast.success("Project connected to app successfully");
|
||||||
await refreshApp();
|
await refreshApp();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -78,6 +80,14 @@ export function SupabaseConnector({ appId }: { appId: number }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectIdForBranches =
|
||||||
|
app?.supabaseParentProjectId || app?.supabaseProjectId;
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectIdForBranches) {
|
||||||
|
loadBranches(projectIdForBranches);
|
||||||
|
}
|
||||||
|
}, [projectIdForBranches, loadBranches]);
|
||||||
|
|
||||||
const handleUnsetProject = async () => {
|
const handleUnsetProject = async () => {
|
||||||
try {
|
try {
|
||||||
await unsetAppProject(appId);
|
await unsetAppProject(appId);
|
||||||
@@ -122,9 +132,56 @@ export function SupabaseConnector({ appId }: { appId: number }) {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="supabase-branch-select">Database Branch</Label>
|
||||||
|
<Select
|
||||||
|
value={app.supabaseProjectId || ""}
|
||||||
|
onValueChange={async (supabaseBranchProjectId) => {
|
||||||
|
try {
|
||||||
|
const branch = branches.find(
|
||||||
|
(b) => b.projectRef === supabaseBranchProjectId,
|
||||||
|
);
|
||||||
|
if (!branch) {
|
||||||
|
throw new Error("Branch not found");
|
||||||
|
}
|
||||||
|
await setAppProject({
|
||||||
|
projectId: branch.projectRef,
|
||||||
|
parentProjectId: branch.parentProjectRef,
|
||||||
|
appId,
|
||||||
|
});
|
||||||
|
toast.success("Branch selected");
|
||||||
|
await refreshApp();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to set branch: " + error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="supabase-branch-select"
|
||||||
|
data-testid="supabase-branch-select"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select a branch" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{branches.map((branch) => (
|
||||||
|
<SelectItem
|
||||||
|
key={branch.projectRef}
|
||||||
|
value={branch.projectRef}
|
||||||
|
>
|
||||||
|
{branch.name}
|
||||||
|
{branch.isDefault && " (Default)"}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button variant="destructive" onClick={handleUnsetProject}>
|
<Button variant="destructive" onClick={handleUnsetProject}>
|
||||||
Disconnect Project
|
Disconnect Project
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
|||||||
showTokenBar ? "text-purple-500 bg-purple-100" : ""
|
showTokenBar ? "text-purple-500 bg-purple-100" : ""
|
||||||
}`}
|
}`}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
data-testid="token-bar-toggle"
|
||||||
>
|
>
|
||||||
<ChartColumnIncreasing size={14} />
|
<ChartColumnIncreasing size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function TokenBar({ chatId }: TokenBarProps) {
|
|||||||
const inputPercent = (inputTokens / contextWindow) * 100;
|
const inputPercent = (inputTokens / contextWindow) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 pb-2 text-xs">
|
<div className="px-4 pb-2 text-xs" data-testid="token-bar">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ export const apps = sqliteTable("apps", {
|
|||||||
githubRepo: text("github_repo"),
|
githubRepo: text("github_repo"),
|
||||||
githubBranch: text("github_branch"),
|
githubBranch: text("github_branch"),
|
||||||
supabaseProjectId: text("supabase_project_id"),
|
supabaseProjectId: text("supabase_project_id"),
|
||||||
|
// If supabaseProjectId is a branch, then the parent project id set.
|
||||||
|
// This is because there's no way to retrieve ALL the branches for ALL projects
|
||||||
|
// in a single API call
|
||||||
|
// This is only used for display purposes but is NOT used for any actual
|
||||||
|
// supabase management logic.
|
||||||
|
supabaseParentProjectId: text("supabase_parent_project_id"),
|
||||||
neonProjectId: text("neon_project_id"),
|
neonProjectId: text("neon_project_id"),
|
||||||
neonDevelopmentBranchId: text("neon_development_branch_id"),
|
neonDevelopmentBranchId: text("neon_development_branch_id"),
|
||||||
neonPreviewBranchId: text("neon_preview_branch_id"),
|
neonPreviewBranchId: text("neon_preview_branch_id"),
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import { useCallback } from "react";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
supabaseProjectsAtom,
|
supabaseProjectsAtom,
|
||||||
|
supabaseBranchesAtom,
|
||||||
supabaseLoadingAtom,
|
supabaseLoadingAtom,
|
||||||
supabaseErrorAtom,
|
supabaseErrorAtom,
|
||||||
selectedSupabaseProjectAtom,
|
selectedSupabaseProjectAtom,
|
||||||
} from "@/atoms/supabaseAtoms";
|
} from "@/atoms/supabaseAtoms";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
import { SetSupabaseAppProjectParams } from "@/ipc/ipc_types";
|
||||||
|
|
||||||
export function useSupabase() {
|
export function useSupabase() {
|
||||||
const [projects, setProjects] = useAtom(supabaseProjectsAtom);
|
const [projects, setProjects] = useAtom(supabaseProjectsAtom);
|
||||||
|
const [branches, setBranches] = useAtom(supabaseBranchesAtom);
|
||||||
const [loading, setLoading] = useAtom(supabaseLoadingAtom);
|
const [loading, setLoading] = useAtom(supabaseLoadingAtom);
|
||||||
const [error, setError] = useAtom(supabaseErrorAtom);
|
const [error, setError] = useAtom(supabaseErrorAtom);
|
||||||
const [selectedProject, setSelectedProject] = useAtom(
|
const [selectedProject, setSelectedProject] = useAtom(
|
||||||
@@ -35,14 +38,34 @@ export function useSupabase() {
|
|||||||
}
|
}
|
||||||
}, [ipcClient, setProjects, setError, setLoading]);
|
}, [ipcClient, setProjects, setError, setLoading]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load branches for a Supabase project
|
||||||
|
*/
|
||||||
|
const loadBranches = useCallback(
|
||||||
|
async (projectId: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const list = await ipcClient.listSupabaseBranches({ projectId });
|
||||||
|
setBranches(Array.isArray(list) ? list : []);
|
||||||
|
setError(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading Supabase branches:", error);
|
||||||
|
setError(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ipcClient, setBranches, setError, setLoading],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Associate a Supabase project with an app
|
* Associate a Supabase project with an app
|
||||||
*/
|
*/
|
||||||
const setAppProject = useCallback(
|
const setAppProject = useCallback(
|
||||||
async (projectId: string, appId: number) => {
|
async (params: SetSupabaseAppProjectParams) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await ipcClient.setSupabaseAppProject(projectId, appId);
|
await ipcClient.setSupabaseAppProject(params);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting Supabase project for app:", error);
|
console.error("Error setting Supabase project for app:", error);
|
||||||
@@ -87,10 +110,12 @@ export function useSupabase() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
|
branches,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
loadProjects,
|
loadProjects,
|
||||||
|
loadBranches,
|
||||||
setAppProject,
|
setAppProject,
|
||||||
unsetAppProject,
|
unsetAppProject,
|
||||||
selectProject,
|
selectProject,
|
||||||
|
|||||||
@@ -724,7 +724,9 @@ export function registerAppHandlers() {
|
|||||||
let supabaseProjectName: string | null = null;
|
let supabaseProjectName: string | null = null;
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
if (app.supabaseProjectId && settings.supabase?.accessToken?.value) {
|
if (app.supabaseProjectId && settings.supabase?.accessToken?.value) {
|
||||||
supabaseProjectName = await getSupabaseProjectName(app.supabaseProjectId);
|
supabaseProjectName = await getSupabaseProjectName(
|
||||||
|
app.supabaseParentProjectId || app.supabaseProjectId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let vercelTeamSlug: string | null = null;
|
let vercelTeamSlug: string | null = null;
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import log from "electron-log";
|
|||||||
import { db } from "../../db";
|
import { db } from "../../db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { apps } from "../../db/schema";
|
import { apps } from "../../db/schema";
|
||||||
import { getSupabaseClient } from "../../supabase_admin/supabase_management_client";
|
import {
|
||||||
|
getSupabaseClient,
|
||||||
|
listSupabaseBranches,
|
||||||
|
} from "../../supabase_admin/supabase_management_client";
|
||||||
import {
|
import {
|
||||||
createLoggedHandler,
|
createLoggedHandler,
|
||||||
createTestOnlyLoggedHandler,
|
createTestOnlyLoggedHandler,
|
||||||
@@ -10,6 +13,8 @@ import {
|
|||||||
import { handleSupabaseOAuthReturn } from "../../supabase_admin/supabase_return_handler";
|
import { handleSupabaseOAuthReturn } from "../../supabase_admin/supabase_return_handler";
|
||||||
import { safeSend } from "../utils/safe_sender";
|
import { safeSend } from "../utils/safe_sender";
|
||||||
|
|
||||||
|
import { SetSupabaseAppProjectParams, SupabaseBranch } from "../ipc_types";
|
||||||
|
|
||||||
const logger = log.scope("supabase_handlers");
|
const logger = log.scope("supabase_handlers");
|
||||||
const handle = createLoggedHandler(logger);
|
const handle = createLoggedHandler(logger);
|
||||||
const testOnlyHandle = createTestOnlyLoggedHandler(logger);
|
const testOnlyHandle = createTestOnlyLoggedHandler(logger);
|
||||||
@@ -20,16 +25,44 @@ export function registerSupabaseHandlers() {
|
|||||||
return supabase.getProjects();
|
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
|
// Set app project - links a Dyad app to a Supabase project
|
||||||
handle(
|
handle(
|
||||||
"supabase:set-app-project",
|
"supabase:set-app-project",
|
||||||
async (_, { project, app }: { project: string; app: number }) => {
|
async (
|
||||||
|
_,
|
||||||
|
{ projectId, appId, parentProjectId }: SetSupabaseAppProjectParams,
|
||||||
|
) => {
|
||||||
await db
|
await db
|
||||||
.update(apps)
|
.update(apps)
|
||||||
.set({ supabaseProjectId: project })
|
.set({
|
||||||
.where(eq(apps.id, app));
|
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 }) => {
|
handle("supabase:unset-app-project", async (_, { app }: { app: number }) => {
|
||||||
await db
|
await db
|
||||||
.update(apps)
|
.update(apps)
|
||||||
.set({ supabaseProjectId: null })
|
.set({ supabaseProjectId: null, supabaseParentProjectId: null })
|
||||||
.where(eq(apps.id, app));
|
.where(eq(apps.id, app));
|
||||||
|
|
||||||
logger.info(`Removed Supabase project association for app ${app}`);
|
logger.info(`Removed Supabase project association for app ${app}`);
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ import type {
|
|||||||
McpServerUpdate,
|
McpServerUpdate,
|
||||||
CreateMcpServer,
|
CreateMcpServer,
|
||||||
CloneRepoParams,
|
CloneRepoParams,
|
||||||
|
SupabaseBranch,
|
||||||
|
SetSupabaseAppProjectParams,
|
||||||
} from "./ipc_types";
|
} from "./ipc_types";
|
||||||
import type { Template } from "../shared/templates";
|
import type { Template } from "../shared/templates";
|
||||||
import type {
|
import type {
|
||||||
@@ -961,14 +963,16 @@ export class IpcClient {
|
|||||||
return this.ipcRenderer.invoke("supabase:list-projects");
|
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(
|
public async setSupabaseAppProject(
|
||||||
project: string,
|
params: SetSupabaseAppProjectParams,
|
||||||
app: number,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.ipcRenderer.invoke("supabase:set-app-project", {
|
await this.ipcRenderer.invoke("supabase:set-app-project", params);
|
||||||
project,
|
|
||||||
app,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unsetSupabaseAppProject(app: number): Promise<void> {
|
public async unsetSupabaseAppProject(app: number): Promise<void> {
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export interface App {
|
|||||||
githubRepo: string | null;
|
githubRepo: string | null;
|
||||||
githubBranch: string | null;
|
githubBranch: string | null;
|
||||||
supabaseProjectId: string | null;
|
supabaseProjectId: string | null;
|
||||||
|
supabaseParentProjectId: string | null;
|
||||||
supabaseProjectName: string | null;
|
supabaseProjectName: string | null;
|
||||||
neonProjectId: string | null;
|
neonProjectId: string | null;
|
||||||
neonDevelopmentBranchId: string | null;
|
neonDevelopmentBranchId: string | null;
|
||||||
@@ -508,3 +509,17 @@ export type CloneRepoReturnType =
|
|||||||
| {
|
| {
|
||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface SupabaseBranch {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
projectRef: string;
|
||||||
|
parentProjectRef: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetSupabaseAppProjectParams {
|
||||||
|
projectId: string;
|
||||||
|
parentProjectId?: string;
|
||||||
|
appId: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const validInvokeChannels = [
|
|||||||
"reject-proposal",
|
"reject-proposal",
|
||||||
"get-system-debug-info",
|
"get-system-debug-info",
|
||||||
"supabase:list-projects",
|
"supabase:list-projects",
|
||||||
|
"supabase:list-branches",
|
||||||
"supabase:set-app-project",
|
"supabase:set-app-project",
|
||||||
"supabase:unset-app-project",
|
"supabase:unset-app-project",
|
||||||
"local-models:list-ollama",
|
"local-models:list-ollama",
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ export async function getSupabaseContext({
|
|||||||
supabaseProjectId: string;
|
supabaseProjectId: string;
|
||||||
}) {
|
}) {
|
||||||
if (IS_TEST_BUILD) {
|
if (IS_TEST_BUILD) {
|
||||||
|
if (supabaseProjectId === "test-branch-project-id") {
|
||||||
|
return "1234".repeat(200_000);
|
||||||
|
}
|
||||||
return "[[TEST_BUILD_SUPABASE_CONTEXT]]";
|
return "[[TEST_BUILD_SUPABASE_CONTEXT]]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,61 @@ export async function deleteSupabaseFunction({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listSupabaseBranches({
|
||||||
|
supabaseProjectId,
|
||||||
|
}: {
|
||||||
|
supabaseProjectId: string;
|
||||||
|
}): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
is_default: boolean;
|
||||||
|
project_ref: string;
|
||||||
|
parent_project_ref: string;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
if (IS_TEST_BUILD) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "default-branch-id",
|
||||||
|
name: "Default Branch",
|
||||||
|
is_default: true,
|
||||||
|
project_ref: "fake-project-id",
|
||||||
|
parent_project_ref: "fake-project-id",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "test-branch-id",
|
||||||
|
name: "Test Branch",
|
||||||
|
is_default: false,
|
||||||
|
project_ref: "test-branch-project-id",
|
||||||
|
parent_project_ref: "fake-project-id",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Listing Supabase branches for project: ${supabaseProjectId}`);
|
||||||
|
const supabase = await getSupabaseClient();
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.supabase.com/v1/projects/${supabaseProjectId}/branches`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${(supabase as any).options.accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw await createResponseError(response, "list branches");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Listed Supabase branches for project: ${supabaseProjectId}`);
|
||||||
|
const jsonResponse = await response.json();
|
||||||
|
return jsonResponse;
|
||||||
|
}
|
||||||
|
|
||||||
export async function deploySupabaseFunctions({
|
export async function deploySupabaseFunctions({
|
||||||
supabaseProjectId,
|
supabaseProjectId,
|
||||||
functionName,
|
functionName,
|
||||||
|
|||||||
Reference in New Issue
Block a user