diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx
index c167267..da415c4 100644
--- a/src/components/chat/ChatInput.tsx
+++ b/src/components/chat/ChatInput.tsx
@@ -12,6 +12,9 @@ import {
Package,
FileX,
SendToBack,
+ Database,
+ ChevronsUpDown,
+ ChevronsDownUp,
} from "lucide-react";
import type React from "react";
import { useCallback, useEffect, useRef, useState } from "react";
@@ -41,6 +44,8 @@ import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useRunApp } from "@/hooks/useRunApp";
import { AutoApproveSwitch } from "../AutoApproveSwitch";
import { usePostHog } from "posthog-js/react";
+import { CodeHighlight } from "./CodeHighlight";
+
export function ChatInput({ chatId }: { chatId?: number }) {
const posthog = usePostHog();
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
@@ -418,6 +423,18 @@ function ChatInputActions({
)}
+
+ {proposal.sqlQueries?.length > 0 && (
+
+
SQL Queries
+
+ {proposal.sqlQueries.map((query, index) => (
+
+ ))}
+
+
+ )}
+
{proposal.packagesAdded?.length > 0 && (
Packages Added
@@ -485,3 +502,34 @@ function getIconForFileChange(file: FileChange) {
);
}
}
+
+// SQL Query item with expandable functionality
+function SqlQueryItem({ query }: { query: string }) {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ return (
+
setIsExpanded(!isExpanded)}
+ >
+
+
+
+ SQL Query
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+ {isExpanded && (
+
+ {query}
+
+ )}
+
+ );
+}
diff --git a/src/components/chat/DyadExecuteSql.tsx b/src/components/chat/DyadExecuteSql.tsx
new file mode 100644
index 0000000..8c204b0
--- /dev/null
+++ b/src/components/chat/DyadExecuteSql.tsx
@@ -0,0 +1,79 @@
+import type React from "react";
+import type { ReactNode } from "react";
+import { useState } from "react";
+import {
+ ChevronsDownUp,
+ ChevronsUpDown,
+ Database,
+ Loader,
+ CircleX,
+} from "lucide-react";
+import { CodeHighlight } from "./CodeHighlight";
+import { CustomTagState } from "./stateTypes";
+
+interface DyadExecuteSqlProps {
+ children?: ReactNode;
+ node?: any;
+}
+
+export const DyadExecuteSql: React.FC
= ({
+ children,
+ node,
+}) => {
+ const [isContentVisible, setIsContentVisible] = useState(false);
+ const state = node?.properties?.state as CustomTagState;
+ const inProgress = state === "pending";
+ const aborted = state === "aborted";
+
+ return (
+ setIsContentVisible(!isContentVisible)}
+ >
+
+
+
+
+ SQL Query
+
+ {inProgress && (
+
+
+ Executing...
+
+ )}
+ {aborted && (
+
+
+ Did not finish
+
+ )}
+
+
+ {isContentVisible ? (
+
+ ) : (
+
+ )}
+
+
+ {isContentVisible && (
+
+ {children}
+
+ )}
+
+ );
+};
diff --git a/src/components/chat/DyadMarkdownParser.tsx b/src/components/chat/DyadMarkdownParser.tsx
index 58dfde5..ab21d97 100644
--- a/src/components/chat/DyadMarkdownParser.tsx
+++ b/src/components/chat/DyadMarkdownParser.tsx
@@ -5,6 +5,7 @@ import { DyadWrite } from "./DyadWrite";
import { DyadRename } from "./DyadRename";
import { DyadDelete } from "./DyadDelete";
import { DyadAddDependency } from "./DyadAddDependency";
+import { DyadExecuteSql } from "./DyadExecuteSql";
import { CodeHighlight } from "./CodeHighlight";
import { useAtomValue } from "jotai";
import { isStreamingAtom } from "@/atoms/chatAtoms";
@@ -73,6 +74,7 @@ function preprocessUnclosedTags(content: string): {
"dyad-rename",
"dyad-delete",
"dyad-add-dependency",
+ "dyad-execute-sql",
];
let processedContent = content;
@@ -131,6 +133,7 @@ function parseCustomTags(content: string): ContentPiece[] {
"dyad-rename",
"dyad-delete",
"dyad-add-dependency",
+ "dyad-execute-sql",
];
const tagPattern = new RegExp(
@@ -271,6 +274,19 @@ function renderCustomTag(
);
+ case "dyad-execute-sql":
+ return (
+
+ {content}
+
+ );
+
default:
return null;
}
diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts
index 2b4f203..4074ea3 100644
--- a/src/ipc/handlers/app_handlers.ts
+++ b/src/ipc/handlers/app_handlers.ts
@@ -37,7 +37,7 @@ import { getGitAuthor } from "../utils/git_author";
import killPort from "kill-port";
import util from "util";
import log from "electron-log";
-import { getSupabaseProjectName } from "../utils/supabase_management_client";
+import { getSupabaseProjectName } from "../../supabase_admin/supabase_management_client";
const logger = log.scope("app_handlers");
diff --git a/src/ipc/handlers/chat_stream_handlers.ts b/src/ipc/handlers/chat_stream_handlers.ts
index b4039e6..4ec5df8 100644
--- a/src/ipc/handlers/chat_stream_handlers.ts
+++ b/src/ipc/handlers/chat_stream_handlers.ts
@@ -4,6 +4,10 @@ import { db } from "../../db";
import { chats, messages } from "../../db/schema";
import { and, eq, isNull } from "drizzle-orm";
import { SYSTEM_PROMPT } from "../../prompts/system_prompt";
+import {
+ SUPABASE_AVAILABLE_SYSTEM_PROMPT,
+ SUPABASE_NOT_AVAILABLE_SYSTEM_PROMPT,
+} from "../../prompts/supabase_prompt";
import { getDyadAppPath } from "../../paths/paths";
import { readSettings } from "../../main/settings";
import type { ChatResponseEnd, ChatStreamParams } from "../ipc_types";
@@ -13,6 +17,10 @@ import { streamTestResponse } from "./testing_chat_handlers";
import { getTestResponse } from "./testing_chat_handlers";
import { getModelClient } from "../utils/get_model_client";
import log from "electron-log";
+import {
+ getSupabaseContext,
+ getSupabaseClientCode,
+} from "../../supabase_admin/supabase_context";
const logger = log.scope("chat_stream_handlers");
@@ -158,12 +166,23 @@ export function registerChatStreamHandlers() {
) {
messageHistory.pop();
}
-
+ let systemPrompt = SYSTEM_PROMPT;
+ if (updatedChat.app?.supabaseProjectId) {
+ systemPrompt +=
+ "\n\n" +
+ SUPABASE_AVAILABLE_SYSTEM_PROMPT +
+ "\n\n" +
+ (await getSupabaseContext({
+ supabaseProjectId: updatedChat.app.supabaseProjectId,
+ }));
+ } else {
+ systemPrompt += "\n\n" + SUPABASE_NOT_AVAILABLE_SYSTEM_PROMPT;
+ }
const { textStream } = streamText({
maxTokens: 8_000,
temperature: 0,
model: modelClient,
- system: SYSTEM_PROMPT,
+ system: systemPrompt,
messages: [
...messageHistory,
// Add the enhanced user prompt
@@ -190,6 +209,18 @@ export function registerChatStreamHandlers() {
try {
for await (const textPart of textStream) {
fullResponse += textPart;
+ if (
+ fullResponse.includes("$$SUPABASE_CLIENT_CODE$$") &&
+ updatedChat.app?.supabaseProjectId
+ ) {
+ const supabaseClientCode = await getSupabaseClientCode({
+ projectId: updatedChat.app?.supabaseProjectId,
+ });
+ fullResponse = fullResponse.replace(
+ "$$SUPABASE_CLIENT_CODE$$",
+ supabaseClientCode
+ );
+ }
// Store the current partial response
partialResponses.set(req.chatId, fullResponse);
diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts
index aa0845a..9ac3422 100644
--- a/src/ipc/handlers/proposal_handlers.ts
+++ b/src/ipc/handlers/proposal_handlers.ts
@@ -13,6 +13,7 @@ import {
getDyadAddDependencyTags,
getDyadChatSummaryTag,
getDyadDeleteTags,
+ getDyadExecuteSqlTags,
getDyadRenameTags,
getDyadWriteTags,
processFullResponseActions,
@@ -76,7 +77,7 @@ const getProposalHandler = async (
const proposalWriteFiles = getDyadWriteTags(messageContent);
const proposalRenameFiles = getDyadRenameTags(messageContent);
const proposalDeleteFiles = getDyadDeleteTags(messageContent);
-
+ const proposalExecuteSqlQueries = getDyadExecuteSqlTags(messageContent);
const packagesAdded = getDyadAddDependencyTags(messageContent);
const filesChanged = [
@@ -108,6 +109,7 @@ const getProposalHandler = async (
securityRisks: [], // Keep empty
filesChanged,
packagesAdded,
+ sqlQueries: proposalExecuteSqlQueries,
};
logger.log(
"Generated code proposal. title=",
diff --git a/src/ipc/handlers/supabase_handlers.ts b/src/ipc/handlers/supabase_handlers.ts
index b2a51a5..cc7eea0 100644
--- a/src/ipc/handlers/supabase_handlers.ts
+++ b/src/ipc/handlers/supabase_handlers.ts
@@ -5,7 +5,7 @@ import log from "electron-log";
import { db } from "../../db";
import { eq } from "drizzle-orm";
import { apps } from "../../db/schema";
-import { getSupabaseClient } from "../utils/supabase_management_client";
+import { getSupabaseClient } from "../../supabase_admin/supabase_management_client";
const logger = log.scope("supabase_handlers");
diff --git a/src/ipc/processors/response_processor.ts b/src/ipc/processors/response_processor.ts
index 8abd76c..2c6ae00 100644
--- a/src/ipc/processors/response_processor.ts
+++ b/src/ipc/processors/response_processor.ts
@@ -9,6 +9,7 @@ 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";
const logger = log.scope("response_processor");
@@ -101,6 +102,31 @@ export function getDyadChatSummaryTag(fullResponse: string): string | null {
return null;
}
+export function getDyadExecuteSqlTags(fullResponse: string): string[] {
+ const dyadExecuteSqlRegex =
+ /([\s\S]*?)<\/dyad-execute-sql>/g;
+ let match;
+ const queries: string[] = [];
+
+ while ((match = dyadExecuteSqlRegex.exec(fullResponse)) !== null) {
+ let content = match[1].trim();
+
+ // Handle markdown code blocks if present
+ const contentLines = content.split("\n");
+ if (contentLines[0]?.startsWith("```")) {
+ contentLines.shift();
+ }
+ if (contentLines[contentLines.length - 1]?.startsWith("```")) {
+ contentLines.pop();
+ }
+ content = contentLines.join("\n");
+
+ queries.push(content);
+ }
+
+ return queries;
+}
+
export async function processFullResponseActions(
fullResponse: string,
chatId: number,
@@ -134,6 +160,9 @@ export async function processFullResponseActions(
const dyadRenameTags = getDyadRenameTags(fullResponse);
const dyadDeletePaths = getDyadDeleteTags(fullResponse);
const dyadAddDependencyPackages = getDyadAddDependencyTags(fullResponse);
+ const dyadExecuteSqlQueries = chatWithApp.app.supabaseProjectId
+ ? getDyadExecuteSqlTags(fullResponse)
+ : [];
const message = await db.query.messages.findFirst({
where: and(
@@ -148,6 +177,17 @@ export async function processFullResponseActions(
return {};
}
+ // Handle SQL execution tags
+ if (dyadExecuteSqlQueries.length > 0) {
+ for (const query of dyadExecuteSqlQueries) {
+ const result = await executeSupabaseSql({
+ supabaseProjectId: chatWithApp.app.supabaseProjectId!,
+ query,
+ });
+ }
+ logger.log(`Executed ${dyadExecuteSqlQueries.length} SQL queries`);
+ }
+
// TODO: Handle add dependency tags
if (dyadAddDependencyPackages.length > 0) {
await executeAddDependency({
@@ -249,7 +289,8 @@ export async function processFullResponseActions(
writtenFiles.length > 0 ||
renamedFiles.length > 0 ||
deletedFiles.length > 0 ||
- dyadAddDependencyPackages.length > 0;
+ dyadAddDependencyPackages.length > 0 ||
+ dyadExecuteSqlQueries.length > 0;
if (hasChanges) {
// Stage all written files
for (const file of writtenFiles) {
@@ -272,6 +313,8 @@ export async function processFullResponseActions(
changes.push(
`added ${dyadAddDependencyPackages.join(", ")} package(s)`
);
+ if (dyadExecuteSqlQueries.length > 0)
+ changes.push(`executed ${dyadExecuteSqlQueries.length} SQL queries`);
// Use chat summary, if provided, or default for commit message
await git.commit({
diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts
index 12f8317..5989a77 100644
--- a/src/lib/schemas.ts
+++ b/src/lib/schemas.ts
@@ -129,6 +129,7 @@ export interface CodeProposal {
securityRisks: SecurityRisk[];
filesChanged: FileChange[];
packagesAdded: string[];
+ sqlQueries: string[];
}
export interface SuggestedAction {
diff --git a/src/main.ts b/src/main.ts
index bf8c82a..a9b48f6 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -7,7 +7,7 @@ import started from "electron-squirrel-startup";
import { updateElectronApp } from "update-electron-app";
import log from "electron-log";
import { readSettings, writeSettings } from "./main/settings";
-import { handleSupabaseOAuthReturn } from "./main/supabase_return_handler";
+import { handleSupabaseOAuthReturn } from "./supabase_admin/supabase_return_handler";
log.errorHandler.startCatching();
log.eventLogger.startLogging();
diff --git a/src/prompts/supabase_prompt.ts b/src/prompts/supabase_prompt.ts
new file mode 100644
index 0000000..a84ee4d
--- /dev/null
+++ b/src/prompts/supabase_prompt.ts
@@ -0,0 +1,280 @@
+// System prompt based on https://github.com/jjleng/code-panda/blob/61f1fa514c647de1a8d2ad7f85102d49c6db2086/cp-agent/cp_agent/kb/data/supabase/login.txt
+// which is Apache 2.0 licensed and copyrighted to Jijun Leng
+// https://github.com/jjleng/code-panda/blob/61f1fa514c647de1a8d2ad7f85102d49c6db2086/LICENSE
+
+export const SUPABASE_AVAILABLE_SYSTEM_PROMPT = `
+# Supabase Instructions
+
+The user has Supabase available for their app so use it for any auth, database or server-side functions.
+
+Make sure supabase client exists at src/integrations/supabase/client.ts. If it doesn't exist, create it.
+
+NOTE: I will replace $$SUPABASE_CLIENT_CODE$$ with the actual code.
+
+
+$$SUPABASE_CLIENT_CODE$$
+
+
+## Auth
+
+When asked to add authentication or login feature to the app, always follow these steps:
+
+1. User Profile Assessment:
+ - Confirm if user profile data storage is needed (username, roles, avatars)
+ - If yes: Create profiles table migration
+ - If no: Proceed with basic auth setup
+
+2. Core Authentication Setup:
+ a. UI Components:
+ - Use @supabase/auth-ui-react Auth component
+ - Apply light theme (unless dark theme exists)
+ - Style to match application design
+ - Skip third-party providers unless specified
+
+ b. Session Management:
+ - Wrap app with SessionContextProvider from @supabase/auth-ui-react
+ - Import supabase client from @/lib/supabaseClient
+ - Implement auth state monitoring using supabase.auth.onAuthStateChange
+ - Add automatic redirects:
+ - Authenticated users → main page
+ - Unauthenticated users → login page
+
+ c. Error Handling:
+ - Implement AuthApiError handling utility
+ - Monitor auth state changes for errors
+ - Clear errors on sign-out
+ - DO NOT use onError prop (unsupported)
+
+IMPORTANT! You cannot skip step 1.
+
+Below code snippets are provided for reference:
+
+Login state management:
+
+useEffect(() => {
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
+ if (event === 'USER_UPDATED' || event === 'SIGNED_IN') {
+ const { error } = await supabase.auth.getSession();
+ // Other code here
+ }
+ if (event === 'SIGNED_OUT') {
+ // Other code here
+ }
+ });
+
+ return () => subscription.unsubscribe();
+}, []);
+
+
+Login page:
+
+
+import { Auth } from '@supabase/auth-ui-react';
+import { ThemeSupa } from '@supabase/auth-ui-shared';
+function Login() {
+ // Other code here
+ return (
+
+ );
+}
+
+
+
+## Database
+
+If the user wants to use the database, use the following code:
+
+
+SELECT * FROM users;
+
+
+You will need to setup the database schema.
+
+## Creating User Profiles
+
+If the user wants to create a user profile, use the following code:
+
+### Create profiles table in public schema
+
+
+CREATE TABLE public.profiles (
+ id UUID NOT NULL REFERENCES auth.users ON DELETE CASCADE,
+ first_name TEXT,
+ last_name TEXT,
+ PRIMARY KEY (id)
+);
+
+alter table public.profiles enable row level security;
+
+create policy "Public profiles are viewable by everyone." on profiles for select using ( true );
+
+create policy "Users can insert their own profile." on profiles for insert with check ( auth.uid() = id );
+
+create policy "Users can update own profile." on profiles for update using ( auth.uid() = id );
+
+
+**IMPORTANT:** For security, Auth schema isn't exposed in the API. Create user tables in public schema to access user data via API.
+
+**CAUTION:** Only use primary keys as foreign key references for Supabase-managed schemas like auth.users. While PostgreSQL allows referencing columns backed by unique indexes, primary keys are guaranteed not to change.
+
+
+## Auto-Update Profiles on Signup
+
+
+### Function to insert profile when user signs up
+
+
+CREATE FUNCTION public.handle_new_user()
+RETURNS TRIGGER
+LANGUAGE PLPGSQL
+SECURITY DEFINER SET search_path = ''
+AS $$
+BEGIN
+ INSERT INTO public.profiles (id, first_name, last_name)
+ VALUES (new.id, new.raw_user_meta_data ->> 'first_name', new.raw_user_meta_data ->> 'last_name');
+ RETURN new;
+END;
+$$;
+
+-- Trigger the function on user creation
+CREATE TRIGGER on_auth_user_created
+ AFTER INSERT ON auth.users
+ FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();
+
+
+## Server-side Edge Functions
+
+## When to Use Edge Functions
+
+- Use edge functions for:
+ - API-to-API communications
+ - Handling sensitive API tokens or secrets
+ - Typical backend work requiring server-side logic
+
+## Key Implementation Principles
+
+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
+
+2. Configuration:
+- DO NOT edit config.toml
+
+3. Supabase Client:
+- Do not import code from supabase/
+- Functions operate in their own context
+
+4. Function Invocation:
+- Use supabase.functions.invoke() method
+- Avoid raw HTTP requests like fetch or axios
+
+5. CORS Configuration:
+- Always include CORS headers:
+
+
+const corsHeaders = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
+};
+
+
+- Implement OPTIONS request handler:
+
+
+if (req.method === 'OPTIONS') {
+ return new Response(null, { headers: corsHeaders });
+}
+
+
+
+6. Function Design:
+- Include all core application logic within the edge function
+- Do not import code from other project files
+
+7. Secrets Management:
+- Pre-configured secrets, no need to set up manually:
+ - SUPABASE_URL
+ - SUPABASE_ANON_KEY
+ - SUPABASE_SERVICE_ROLE_KEY
+ - SUPABASE_DB_URL
+
+- For new secrets/API tokens:
+ - Inform user to set up via Supabase Console
+ - Direct them to: Project -> Edge Functions -> Manage Secrets
+ - Use for guidance
+
+8. Logging:
+- Implement comprehensive logging for debugging purposes
+
+9. Linking:
+Use to link to the relevant edge function
+
+10. Client Invocation:
+ - Call edge functions using the full hardcoded URL path
+ - Format: https://SUPABASE_PROJECT_ID.supabase.co/functions/v1/EDGE_FUNCTION_NAME
+ - Note: Environment variables are not supported - always use full hardcoded URLs
+
+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'
+
+const corsHeaders = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+}
+
+serve(async (req) => {
+ if (req.method === 'OPTIONS') {
+ return new Response(null, { headers: corsHeaders })
+ }
+ // ... function logic
+})
+
+
+`;
+
+export const SUPABASE_NOT_AVAILABLE_SYSTEM_PROMPT = `
+If the user wants to use supabase or do something that requires auth, database or server-side functions (e.g. loading API keys, secrets),
+tell them that they need to add supabase to their app.
+
+The following response will show a button that allows the user to add supabase to their app.
+
+
+
+# Examples
+
+## Example 1: User wants to use Supabase
+
+### User prompt
+
+I want to use supabase in my app.
+
+### Assistant response
+
+You need to first add Supabase to your app.
+
+
+
+## Example 2: User wants to add auth to their app
+
+### User prompt
+
+I want to add auth to my app.
+
+### Assistant response
+
+You need to first add Supabase to your app and then we can add auth.
+
+
+`;
diff --git a/src/supabase_admin/supabase_context.ts b/src/supabase_admin/supabase_context.ts
new file mode 100644
index 0000000..7891707
--- /dev/null
+++ b/src/supabase_admin/supabase_context.ts
@@ -0,0 +1,66 @@
+import { getSupabaseClient } from "./supabase_management_client";
+import { SUPABASE_SCHEMA_QUERY } from "./supabase_schema_query";
+
+async function getPublishableKey({ projectId }: { projectId: string }) {
+ const supabase = await getSupabaseClient();
+ const keys = await supabase.getProjectApiKeys(projectId);
+ if (!keys) {
+ throw new Error("No keys found for project");
+ }
+ const publishableKey = keys.find((key) => (key as any)["name"] === "anon");
+
+ if (!publishableKey) {
+ throw new Error("No publishable key found for project");
+ }
+ return publishableKey.api_key;
+}
+export const getSupabaseClientCode = async function ({
+ projectId,
+}: {
+ projectId: string;
+}) {
+ const publishableKey = await getPublishableKey({ projectId });
+ return `
+// This file is automatically generated. Do not edit it directly.
+import { createClient } from '@supabase/supabase-js';
+
+const SUPABASE_URL = "https://${projectId}.supabase.co";
+const SUPABASE_PUBLISHABLE_KEY = "${publishableKey}";
+
+// Import the supabase client like this:
+// import { supabase } from "@/integrations/supabase/client";
+
+export const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY);`;
+};
+
+export async function getSupabaseContext({
+ supabaseProjectId,
+}: {
+ supabaseProjectId: string;
+}) {
+ const supabase = await getSupabaseClient();
+ const publishableKey = await getPublishableKey({
+ projectId: supabaseProjectId,
+ });
+ const schema = await supabase.runQuery(
+ supabaseProjectId,
+ SUPABASE_SCHEMA_QUERY
+ );
+
+ // TODO: include EDGE FUNCTIONS and SECRETS!
+
+ const context = `
+ # Supabase Context
+
+ ## Supabase Project ID
+ ${supabaseProjectId}
+
+ ## Publishable key (aka anon key)
+ ${publishableKey}
+
+ ## Schema
+ ${JSON.stringify(schema)}
+ `;
+
+ return context;
+}
diff --git a/src/ipc/utils/supabase_management_client.ts b/src/supabase_admin/supabase_management_client.ts
similarity index 71%
rename from src/ipc/utils/supabase_management_client.ts
rename to src/supabase_admin/supabase_management_client.ts
index 310b429..38a02f3 100644
--- a/src/ipc/utils/supabase_management_client.ts
+++ b/src/supabase_admin/supabase_management_client.ts
@@ -1,4 +1,4 @@
-import { readSettings } from "../../main/settings";
+import { readSettings } from "../main/settings";
import { SupabaseManagementAPI } from "supabase-management-js";
// Function to get the Supabase Management API client
@@ -26,3 +26,15 @@ export async function getSupabaseProjectName(
const project = projects?.find((p) => p.id === projectId);
return project?.name || ``;
}
+
+export async function executeSupabaseSql({
+ supabaseProjectId,
+ query,
+}: {
+ supabaseProjectId: string;
+ query: string;
+}): Promise {
+ const supabase = await getSupabaseClient();
+ const result = await supabase.runQuery(supabaseProjectId, query);
+ return JSON.stringify(result);
+}
diff --git a/src/main/supabase_return_handler.ts b/src/supabase_admin/supabase_return_handler.ts
similarity index 86%
rename from src/main/supabase_return_handler.ts
rename to src/supabase_admin/supabase_return_handler.ts
index f114a6b..3de3a37 100644
--- a/src/main/supabase_return_handler.ts
+++ b/src/supabase_admin/supabase_return_handler.ts
@@ -1,4 +1,4 @@
-import { writeSettings } from "./settings";
+import { writeSettings } from "../main/settings";
export function handleSupabaseOAuthReturn({
token,
diff --git a/src/supabase_admin/supabase_schema_query.ts b/src/supabase_admin/supabase_schema_query.ts
new file mode 100644
index 0000000..a0c7743
--- /dev/null
+++ b/src/supabase_admin/supabase_schema_query.ts
@@ -0,0 +1,107 @@
+// Schema query based on https://github.com/jjleng/code-panda/blob/61f1fa514c647de1a8d2ad7f85102d49c6db2086/cp-agent/cp_agent/utils/supabase_utils.py#L521
+// which is Apache 2.0 licensed and copyrighted to Jijun Leng
+// https://github.com/jjleng/code-panda/blob/61f1fa514c647de1a8d2ad7f85102d49c6db2086/LICENSE
+
+export const SUPABASE_SCHEMA_QUERY = `
+ WITH table_info AS (
+ SELECT
+ tables.table_name,
+ pd.description as table_description
+ FROM information_schema.tables tables
+ LEFT JOIN pg_stat_user_tables psut ON tables.table_name = psut.relname
+ LEFT JOIN pg_description pd ON psut.relid = pd.objoid AND pd.objsubid = 0
+ WHERE tables.table_schema = 'public'
+ ),
+ column_info AS (
+ SELECT
+ c.table_name,
+ jsonb_agg(
+ jsonb_build_object(
+ 'column_name', c.column_name,
+ 'data_type', c.data_type,
+ 'is_nullable', c.is_nullable,
+ 'column_default', c.column_default
+ ) ORDER BY c.ordinal_position
+ ) as columns
+ FROM information_schema.columns c
+ WHERE c.table_schema = 'public'
+ GROUP BY c.table_name
+ ),
+ tables_result AS (
+ SELECT
+ 'tables' as result_type,
+ jsonb_build_object(
+ 'name', ti.table_name::text,
+ 'description', ti.table_description::text,
+ 'columns', COALESCE(ci.columns, '[]'::jsonb)
+ )::text as data
+ FROM table_info ti
+ LEFT JOIN column_info ci ON ti.table_name = ci.table_name
+ ),
+ policies_result AS (
+ SELECT
+ 'policies' as result_type,
+ jsonb_build_object(
+ 'name', pol.polname::text,
+ 'table', cls.relname::text,
+ 'command', CASE
+ WHEN pol.polcmd = 'r' THEN 'SELECT'
+ WHEN pol.polcmd = 'w' THEN 'UPDATE'
+ WHEN pol.polcmd = 'a' THEN 'INSERT'
+ WHEN pol.polcmd = 'd' THEN 'DELETE'
+ ELSE pol.polcmd::text
+ END,
+ 'permissive', pol.polpermissive,
+ 'definition', pg_get_expr(pol.polqual, pol.polrelid)::text
+ )::text as data
+ FROM pg_policy pol
+ JOIN pg_class cls ON pol.polrelid = cls.oid
+ WHERE cls.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
+ ),
+ functions_result AS (
+ SELECT
+ 'functions' as result_type,
+ jsonb_build_object(
+ 'name', p.proname::text,
+ 'description', d.description::text,
+ 'arguments', pg_get_function_arguments(p.oid)::text,
+ 'return_type', pg_get_function_result(p.oid)::text,
+ 'language', l.lanname::text,
+ 'volatility', CASE p.provolatile
+ WHEN 'i' THEN 'IMMUTABLE'
+ WHEN 's' THEN 'STABLE'
+ WHEN 'v' THEN 'VOLATILE'
+ END,
+ 'source_code', pg_get_functiondef(p.oid)::text
+ )::text as data
+ FROM pg_proc p
+ LEFT JOIN pg_description d ON p.oid = d.objoid
+ LEFT JOIN pg_language l ON p.prolang = l.oid
+ WHERE p.pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
+ ),
+ triggers_result AS (
+ SELECT
+ 'triggers' as result_type,
+ jsonb_build_object(
+ 'name', t.trigger_name::text,
+ 'table', t.event_object_table::text,
+ 'timing', t.action_timing::text,
+ 'event', t.event_manipulation::text,
+ 'action_statement', t.action_statement::text,
+ 'function_name', p.proname::text
+ )::text as data
+ FROM information_schema.triggers t
+ LEFT JOIN pg_trigger pg_t ON t.trigger_name = pg_t.tgname
+ LEFT JOIN pg_proc p ON pg_t.tgfoid = p.oid
+ WHERE t.trigger_schema = 'public'
+ )
+ SELECT result_type, data
+ FROM (
+ SELECT * FROM tables_result
+ UNION ALL SELECT * FROM policies_result
+ UNION ALL SELECT * FROM functions_result
+ UNION ALL SELECT * FROM triggers_result
+ ) combined_results
+ ORDER BY result_type;
+
+`;