From 6e1935bbba7e506d7e235182f3392acc8fa69141 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 23 Apr 2025 10:23:26 -0700 Subject: [PATCH] Support supabase function deployment --- package-lock.json | 24 +++--- package.json | 4 +- src/ipc/handlers/proposal_handlers.ts | 4 + src/ipc/processors/response_processor.ts | 13 +++- src/lib/schemas.ts | 1 + src/prompts/supabase_prompt.ts | 7 +- .../supabase_management_client.ts | 73 ++++++++++++++++++- src/supabase_admin/supabase_utils.ts | 3 + 8 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 src/supabase_admin/supabase_utils.ts diff --git a/package-lock.json b/package-lock.json index 0813fb0..fa1092c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@ai-sdk/google": "^1.2.10", "@ai-sdk/openai": "^1.3.7", "@biomejs/biome": "^1.9.4", + "@dyad-sh/supabase-management-js": "v1.0.0", "@monaco-editor/react": "^4.7.0-rc.0", "@openrouter/ai-sdk-provider": "^0.4.5", "@radix-ui/react-accordion": "^1.2.4", @@ -62,7 +63,6 @@ "shell-env": "^4.0.1", "shiki": "^3.2.1", "sonner": "^2.0.3", - "supabase-management-js": "^1.0.0", "tailwind-merge": "^3.1.0", "tailwindcss": "^4.1.3", "tree-kill": "^1.2.2", @@ -853,6 +853,17 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@dyad-sh/supabase-management-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@dyad-sh/supabase-management-js/-/supabase-management-js-1.0.0.tgz", + "integrity": "sha512-v/DupITKhM0/pmXJPFYFtjCoKzD5ZAQn1Haay1hXTSBP3SoEv1Ff7bQS7q7Ee30JCZllHy6dQFa6loqJKdkXOg==", + "dependencies": { + "openapi-fetch": "^0.6.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@electron-forge/cli": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.8.0.tgz", @@ -19601,17 +19612,6 @@ "node": ">= 8.0" } }, - "node_modules/supabase-management-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supabase-management-js/-/supabase-management-js-1.0.0.tgz", - "integrity": "sha512-CjtUcT1i3lsIyRwS3/HG6BUB8EaJTEdAPP0GQu8dRDi61HvJeBrswCfG/GRxebC0ZZFov+oHqFIWPe1ztyUqYA==", - "dependencies": { - "openapi-fetch": "^0.6.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index ced895b..4b569bd 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "shell-env": "^4.0.1", "shiki": "^3.2.1", "sonner": "^2.0.3", - "supabase-management-js": "^1.0.0", + "@dyad-sh/supabase-management-js": "v1.0.0", "tailwind-merge": "^3.1.0", "tailwindcss": "^4.1.3", "tree-kill": "^1.2.2", @@ -124,4 +124,4 @@ "update-electron-app": "^3.1.1", "uuid": "^11.1.0" } -} +} \ No newline at end of file diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts index 9ac3422..2f8acb5 100644 --- a/src/ipc/handlers/proposal_handlers.ts +++ b/src/ipc/handlers/proposal_handlers.ts @@ -19,6 +19,7 @@ import { processFullResponseActions, } from "../processors/response_processor"; import log from "electron-log"; +import { isServerFunction } from "../../supabase_admin/supabase_utils"; const logger = log.scope("proposal_handlers"); @@ -86,18 +87,21 @@ const getProposalHandler = async ( path: tag.path, summary: tag.description ?? "(no change summary found)", // Generic summary type: "write" as const, + isServerFunction: isServerFunction(tag.path), })), ...proposalRenameFiles.map((tag) => ({ name: path.basename(tag.to), path: tag.to, summary: `Rename from ${tag.from} to ${tag.to}`, type: "rename" as const, + isServerFunction: isServerFunction(tag.to), })), ...proposalDeleteFiles.map((tag) => ({ name: path.basename(tag), path: tag, summary: `Delete file`, type: "delete" as const, + isServerFunction: isServerFunction(tag), })), ]; // Check if we have enough information to create a proposal diff --git a/src/ipc/processors/response_processor.ts b/src/ipc/processors/response_processor.ts index 2c6ae00..d925bd0 100644 --- a/src/ipc/processors/response_processor.ts +++ b/src/ipc/processors/response_processor.ts @@ -9,7 +9,11 @@ import { getGithubUser } from "../handlers/github_handlers"; import { getGitAuthor } from "../utils/git_author"; import log from "electron-log"; import { executeAddDependency } from "./executeAddDependency"; -import { executeSupabaseSql } from "../../supabase_admin/supabase_management_client"; +import { + deploySupabaseFunctions, + executeSupabaseSql, +} from "../../supabase_admin/supabase_management_client"; +import { isServerFunction } from "../../supabase_admin/supabase_utils"; const logger = log.scope("response_processor"); @@ -220,6 +224,13 @@ export async function processFullResponseActions( fs.writeFileSync(fullFilePath, content); logger.log(`Successfully wrote file: ${fullFilePath}`); writtenFiles.push(filePath); + if (isServerFunction(filePath)) { + await deploySupabaseFunctions({ + supabaseProjectId: chatWithApp.app.supabaseProjectId!, + functionName: path.basename(path.dirname(filePath)), + content: content, + }); + } } // Process all file renames diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index dc7e8b5..e3f254c 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -129,6 +129,7 @@ export interface FileChange { path: string; summary: string; type: "write" | "rename" | "delete"; + isServerFunction: boolean; } export interface CodeProposal { diff --git a/src/prompts/supabase_prompt.ts b/src/prompts/supabase_prompt.ts index a84ee4d..75b9e38 100644 --- a/src/prompts/supabase_prompt.ts +++ b/src/prompts/supabase_prompt.ts @@ -162,9 +162,8 @@ CREATE TRIGGER on_auth_user_created 1. Location: - Write functions in the supabase/functions folder -- Each function should be a standalone, self-inclusive file (e.g., function-name.ts) -- Avoid using folder/index.ts structure patterns -- Functions will be deployed automatically and you will be notified +- Each function should be in a standalone directory where the main file is index.ts (e.g., supabase/functions/hello/index.ts) +- Functions will require approval by the user before they are deployed 2. Configuration: - DO NOT edit config.toml @@ -225,7 +224,7 @@ Use to link to the relevant edge function 11. Edge Function Template: - + import { serve } from "https://deno.land/std@0.190.0/http/server.ts" import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.45.0' diff --git a/src/supabase_admin/supabase_management_client.ts b/src/supabase_admin/supabase_management_client.ts index f32d09d..81a79b1 100644 --- a/src/supabase_admin/supabase_management_client.ts +++ b/src/supabase_admin/supabase_management_client.ts @@ -1,6 +1,9 @@ import { withLock } from "../ipc/utils/lock_utils"; import { readSettings, writeSettings } from "../main/settings"; -import { SupabaseManagementAPI } from "supabase-management-js"; +import { + SupabaseManagementAPI, + SupabaseManagementAPIError, +} from "@dyad-sh/supabase-management-js"; /** * Checks if the Supabase access token is expired or about to expire @@ -133,3 +136,71 @@ export async function executeSupabaseSql({ const result = await supabase.runQuery(supabaseProjectId, query); return JSON.stringify(result); } + +export async function deploySupabaseFunctions({ + supabaseProjectId, + functionName, + content, +}: { + supabaseProjectId: string; + functionName: string; + content: string; +}): Promise { + const supabase = await getSupabaseClient(); + const formData = new FormData(); + formData.append( + "metadata", + JSON.stringify({ + entrypoint_path: "index.ts", + name: functionName, + }) + ); + formData.append("file", new Blob([content]), "index.ts"); + + const response = await fetch( + `https://api.supabase.com/v1/projects/${supabaseProjectId}/functions/deploy?slug=${functionName}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${(supabase as any).options.accessToken}`, + }, + body: formData, + } + ); + + if (response.status !== 201) { + throw await createResponseError(response, "create function"); + } + + return response.json(); +} + +async function createResponseError(response: Response, action: string) { + const errorBody = await safeParseErrorResponseBody(response); + + return new SupabaseManagementAPIError( + `Failed to ${action}: ${response.statusText} (${response.status})${ + errorBody ? `: ${errorBody.message}` : "" + }`, + response + ); +} + +async function safeParseErrorResponseBody( + response: Response +): Promise<{ message: string } | undefined> { + try { + const body = await response.json(); + + if ( + typeof body === "object" && + body !== null && + "message" in body && + typeof body.message === "string" + ) { + return { message: body.message }; + } + } catch (error) { + return; + } +} diff --git a/src/supabase_admin/supabase_utils.ts b/src/supabase_admin/supabase_utils.ts new file mode 100644 index 0000000..198dcb5 --- /dev/null +++ b/src/supabase_admin/supabase_utils.ts @@ -0,0 +1,3 @@ +export function isServerFunction(filePath: string) { + return filePath.startsWith("supabase/functions/"); +}