Support supabase function deployment

This commit is contained in:
Will Chen
2025-04-23 10:23:26 -07:00
parent 922ee7d90a
commit 6e1935bbba
8 changed files with 109 additions and 20 deletions

24
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -129,6 +129,7 @@ export interface FileChange {
path: string;
summary: string;
type: "write" | "rename" | "delete";
isServerFunction: boolean;
}
export interface CodeProposal {

View File

@@ -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 <resource-link> to link to the relevant edge function
11. Edge Function Template:
<dyad-write path="src/supabase/functions/hello.ts" description="Creating a hello world edge function.">
<dyad-write path="supabase/functions/hello.ts" description="Creating a hello world edge function.">
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'

View File

@@ -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<void> {
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;
}
}

View File

@@ -0,0 +1,3 @@
export function isServerFunction(filePath: string) {
return filePath.startsWith("supabase/functions/");
}