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.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; + +`;