Supabase support: client, auth & SQL
This commit is contained in:
@@ -12,6 +12,9 @@ import {
|
|||||||
Package,
|
Package,
|
||||||
FileX,
|
FileX,
|
||||||
SendToBack,
|
SendToBack,
|
||||||
|
Database,
|
||||||
|
ChevronsUpDown,
|
||||||
|
ChevronsDownUp,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
@@ -41,6 +44,8 @@ import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
|||||||
import { useRunApp } from "@/hooks/useRunApp";
|
import { useRunApp } from "@/hooks/useRunApp";
|
||||||
import { AutoApproveSwitch } from "../AutoApproveSwitch";
|
import { AutoApproveSwitch } from "../AutoApproveSwitch";
|
||||||
import { usePostHog } from "posthog-js/react";
|
import { usePostHog } from "posthog-js/react";
|
||||||
|
import { CodeHighlight } from "./CodeHighlight";
|
||||||
|
|
||||||
export function ChatInput({ chatId }: { chatId?: number }) {
|
export function ChatInput({ chatId }: { chatId?: number }) {
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
|
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
|
||||||
@@ -418,6 +423,18 @@ function ChatInputActions({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{proposal.sqlQueries?.length > 0 && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<h4 className="font-semibold mb-1">SQL Queries</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{proposal.sqlQueries.map((query, index) => (
|
||||||
|
<SqlQueryItem key={index} query={query} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{proposal.packagesAdded?.length > 0 && (
|
{proposal.packagesAdded?.length > 0 && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<h4 className="font-semibold mb-1">Packages Added</h4>
|
<h4 className="font-semibold mb-1">Packages Added</h4>
|
||||||
@@ -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 (
|
||||||
|
<li
|
||||||
|
className="bg-(--background-lightest) hover:bg-(--background-lighter) rounded-lg px-3 py-2 border border-border cursor-pointer"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database size={16} className="text-muted-foreground flex-shrink-0" />
|
||||||
|
<span className="text-sm font-medium">SQL Query</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronsDownUp size={18} className="text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronsUpDown size={18} className="text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-2 text-xs max-h-[200px] overflow-auto">
|
||||||
|
<CodeHighlight className="language-sql ">{query}</CodeHighlight>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
79
src/components/chat/DyadExecuteSql.tsx
Normal file
79
src/components/chat/DyadExecuteSql.tsx
Normal file
@@ -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<DyadExecuteSqlProps> = ({
|
||||||
|
children,
|
||||||
|
node,
|
||||||
|
}) => {
|
||||||
|
const [isContentVisible, setIsContentVisible] = useState(false);
|
||||||
|
const state = node?.properties?.state as CustomTagState;
|
||||||
|
const inProgress = state === "pending";
|
||||||
|
const aborted = state === "aborted";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`bg-(--background-lightest) hover:bg-(--background-lighter) rounded-lg px-4 py-2 border my-2 cursor-pointer ${
|
||||||
|
inProgress
|
||||||
|
? "border-amber-500"
|
||||||
|
: aborted
|
||||||
|
? "border-red-500"
|
||||||
|
: "border-border"
|
||||||
|
}`}
|
||||||
|
onClick={() => setIsContentVisible(!isContentVisible)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database size={16} />
|
||||||
|
<span className="text-gray-700 dark:text-gray-300 font-medium text-sm">
|
||||||
|
SQL Query
|
||||||
|
</span>
|
||||||
|
{inProgress && (
|
||||||
|
<div className="flex items-center text-amber-600 text-xs">
|
||||||
|
<Loader size={14} className="mr-1 animate-spin" />
|
||||||
|
<span>Executing...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{aborted && (
|
||||||
|
<div className="flex items-center text-red-600 text-xs">
|
||||||
|
<CircleX size={14} className="mr-1" />
|
||||||
|
<span>Did not finish</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{isContentVisible ? (
|
||||||
|
<ChevronsDownUp
|
||||||
|
size={20}
|
||||||
|
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronsUpDown
|
||||||
|
size={20}
|
||||||
|
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isContentVisible && (
|
||||||
|
<div className="text-xs">
|
||||||
|
<CodeHighlight className="language-sql">{children}</CodeHighlight>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { DyadWrite } from "./DyadWrite";
|
|||||||
import { DyadRename } from "./DyadRename";
|
import { DyadRename } from "./DyadRename";
|
||||||
import { DyadDelete } from "./DyadDelete";
|
import { DyadDelete } from "./DyadDelete";
|
||||||
import { DyadAddDependency } from "./DyadAddDependency";
|
import { DyadAddDependency } from "./DyadAddDependency";
|
||||||
|
import { DyadExecuteSql } from "./DyadExecuteSql";
|
||||||
import { CodeHighlight } from "./CodeHighlight";
|
import { CodeHighlight } from "./CodeHighlight";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { isStreamingAtom } from "@/atoms/chatAtoms";
|
import { isStreamingAtom } from "@/atoms/chatAtoms";
|
||||||
@@ -73,6 +74,7 @@ function preprocessUnclosedTags(content: string): {
|
|||||||
"dyad-rename",
|
"dyad-rename",
|
||||||
"dyad-delete",
|
"dyad-delete",
|
||||||
"dyad-add-dependency",
|
"dyad-add-dependency",
|
||||||
|
"dyad-execute-sql",
|
||||||
];
|
];
|
||||||
|
|
||||||
let processedContent = content;
|
let processedContent = content;
|
||||||
@@ -131,6 +133,7 @@ function parseCustomTags(content: string): ContentPiece[] {
|
|||||||
"dyad-rename",
|
"dyad-rename",
|
||||||
"dyad-delete",
|
"dyad-delete",
|
||||||
"dyad-add-dependency",
|
"dyad-add-dependency",
|
||||||
|
"dyad-execute-sql",
|
||||||
];
|
];
|
||||||
|
|
||||||
const tagPattern = new RegExp(
|
const tagPattern = new RegExp(
|
||||||
@@ -271,6 +274,19 @@ function renderCustomTag(
|
|||||||
</DyadAddDependency>
|
</DyadAddDependency>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "dyad-execute-sql":
|
||||||
|
return (
|
||||||
|
<DyadExecuteSql
|
||||||
|
node={{
|
||||||
|
properties: {
|
||||||
|
state: getState({ isStreaming, inProgress }),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</DyadExecuteSql>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { getGitAuthor } from "../utils/git_author";
|
|||||||
import killPort from "kill-port";
|
import killPort from "kill-port";
|
||||||
import util from "util";
|
import util from "util";
|
||||||
import log from "electron-log";
|
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");
|
const logger = log.scope("app_handlers");
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { db } from "../../db";
|
|||||||
import { chats, messages } from "../../db/schema";
|
import { chats, messages } from "../../db/schema";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { SYSTEM_PROMPT } from "../../prompts/system_prompt";
|
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 { getDyadAppPath } from "../../paths/paths";
|
||||||
import { readSettings } from "../../main/settings";
|
import { readSettings } from "../../main/settings";
|
||||||
import type { ChatResponseEnd, ChatStreamParams } from "../ipc_types";
|
import type { ChatResponseEnd, ChatStreamParams } from "../ipc_types";
|
||||||
@@ -13,6 +17,10 @@ import { streamTestResponse } from "./testing_chat_handlers";
|
|||||||
import { getTestResponse } from "./testing_chat_handlers";
|
import { getTestResponse } from "./testing_chat_handlers";
|
||||||
import { getModelClient } from "../utils/get_model_client";
|
import { getModelClient } from "../utils/get_model_client";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
|
import {
|
||||||
|
getSupabaseContext,
|
||||||
|
getSupabaseClientCode,
|
||||||
|
} from "../../supabase_admin/supabase_context";
|
||||||
|
|
||||||
const logger = log.scope("chat_stream_handlers");
|
const logger = log.scope("chat_stream_handlers");
|
||||||
|
|
||||||
@@ -158,12 +166,23 @@ export function registerChatStreamHandlers() {
|
|||||||
) {
|
) {
|
||||||
messageHistory.pop();
|
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({
|
const { textStream } = streamText({
|
||||||
maxTokens: 8_000,
|
maxTokens: 8_000,
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
model: modelClient,
|
model: modelClient,
|
||||||
system: SYSTEM_PROMPT,
|
system: systemPrompt,
|
||||||
messages: [
|
messages: [
|
||||||
...messageHistory,
|
...messageHistory,
|
||||||
// Add the enhanced user prompt
|
// Add the enhanced user prompt
|
||||||
@@ -190,6 +209,18 @@ export function registerChatStreamHandlers() {
|
|||||||
try {
|
try {
|
||||||
for await (const textPart of textStream) {
|
for await (const textPart of textStream) {
|
||||||
fullResponse += textPart;
|
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
|
// Store the current partial response
|
||||||
partialResponses.set(req.chatId, fullResponse);
|
partialResponses.set(req.chatId, fullResponse);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
getDyadAddDependencyTags,
|
getDyadAddDependencyTags,
|
||||||
getDyadChatSummaryTag,
|
getDyadChatSummaryTag,
|
||||||
getDyadDeleteTags,
|
getDyadDeleteTags,
|
||||||
|
getDyadExecuteSqlTags,
|
||||||
getDyadRenameTags,
|
getDyadRenameTags,
|
||||||
getDyadWriteTags,
|
getDyadWriteTags,
|
||||||
processFullResponseActions,
|
processFullResponseActions,
|
||||||
@@ -76,7 +77,7 @@ const getProposalHandler = async (
|
|||||||
const proposalWriteFiles = getDyadWriteTags(messageContent);
|
const proposalWriteFiles = getDyadWriteTags(messageContent);
|
||||||
const proposalRenameFiles = getDyadRenameTags(messageContent);
|
const proposalRenameFiles = getDyadRenameTags(messageContent);
|
||||||
const proposalDeleteFiles = getDyadDeleteTags(messageContent);
|
const proposalDeleteFiles = getDyadDeleteTags(messageContent);
|
||||||
|
const proposalExecuteSqlQueries = getDyadExecuteSqlTags(messageContent);
|
||||||
const packagesAdded = getDyadAddDependencyTags(messageContent);
|
const packagesAdded = getDyadAddDependencyTags(messageContent);
|
||||||
|
|
||||||
const filesChanged = [
|
const filesChanged = [
|
||||||
@@ -108,6 +109,7 @@ const getProposalHandler = async (
|
|||||||
securityRisks: [], // Keep empty
|
securityRisks: [], // Keep empty
|
||||||
filesChanged,
|
filesChanged,
|
||||||
packagesAdded,
|
packagesAdded,
|
||||||
|
sqlQueries: proposalExecuteSqlQueries,
|
||||||
};
|
};
|
||||||
logger.log(
|
logger.log(
|
||||||
"Generated code proposal. title=",
|
"Generated code proposal. title=",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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 "../utils/supabase_management_client";
|
import { getSupabaseClient } from "../../supabase_admin/supabase_management_client";
|
||||||
|
|
||||||
const logger = log.scope("supabase_handlers");
|
const logger = log.scope("supabase_handlers");
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getGithubUser } from "../handlers/github_handlers";
|
|||||||
import { getGitAuthor } from "../utils/git_author";
|
import { getGitAuthor } from "../utils/git_author";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { executeAddDependency } from "./executeAddDependency";
|
import { executeAddDependency } from "./executeAddDependency";
|
||||||
|
import { executeSupabaseSql } from "../../supabase_admin/supabase_management_client";
|
||||||
|
|
||||||
const logger = log.scope("response_processor");
|
const logger = log.scope("response_processor");
|
||||||
|
|
||||||
@@ -101,6 +102,31 @@ export function getDyadChatSummaryTag(fullResponse: string): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDyadExecuteSqlTags(fullResponse: string): string[] {
|
||||||
|
const dyadExecuteSqlRegex =
|
||||||
|
/<dyad-execute-sql>([\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(
|
export async function processFullResponseActions(
|
||||||
fullResponse: string,
|
fullResponse: string,
|
||||||
chatId: number,
|
chatId: number,
|
||||||
@@ -134,6 +160,9 @@ export async function processFullResponseActions(
|
|||||||
const dyadRenameTags = getDyadRenameTags(fullResponse);
|
const dyadRenameTags = getDyadRenameTags(fullResponse);
|
||||||
const dyadDeletePaths = getDyadDeleteTags(fullResponse);
|
const dyadDeletePaths = getDyadDeleteTags(fullResponse);
|
||||||
const dyadAddDependencyPackages = getDyadAddDependencyTags(fullResponse);
|
const dyadAddDependencyPackages = getDyadAddDependencyTags(fullResponse);
|
||||||
|
const dyadExecuteSqlQueries = chatWithApp.app.supabaseProjectId
|
||||||
|
? getDyadExecuteSqlTags(fullResponse)
|
||||||
|
: [];
|
||||||
|
|
||||||
const message = await db.query.messages.findFirst({
|
const message = await db.query.messages.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
@@ -148,6 +177,17 @@ export async function processFullResponseActions(
|
|||||||
return {};
|
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
|
// TODO: Handle add dependency tags
|
||||||
if (dyadAddDependencyPackages.length > 0) {
|
if (dyadAddDependencyPackages.length > 0) {
|
||||||
await executeAddDependency({
|
await executeAddDependency({
|
||||||
@@ -249,7 +289,8 @@ export async function processFullResponseActions(
|
|||||||
writtenFiles.length > 0 ||
|
writtenFiles.length > 0 ||
|
||||||
renamedFiles.length > 0 ||
|
renamedFiles.length > 0 ||
|
||||||
deletedFiles.length > 0 ||
|
deletedFiles.length > 0 ||
|
||||||
dyadAddDependencyPackages.length > 0;
|
dyadAddDependencyPackages.length > 0 ||
|
||||||
|
dyadExecuteSqlQueries.length > 0;
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
// Stage all written files
|
// Stage all written files
|
||||||
for (const file of writtenFiles) {
|
for (const file of writtenFiles) {
|
||||||
@@ -272,6 +313,8 @@ export async function processFullResponseActions(
|
|||||||
changes.push(
|
changes.push(
|
||||||
`added ${dyadAddDependencyPackages.join(", ")} package(s)`
|
`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
|
// Use chat summary, if provided, or default for commit message
|
||||||
await git.commit({
|
await git.commit({
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export interface CodeProposal {
|
|||||||
securityRisks: SecurityRisk[];
|
securityRisks: SecurityRisk[];
|
||||||
filesChanged: FileChange[];
|
filesChanged: FileChange[];
|
||||||
packagesAdded: string[];
|
packagesAdded: string[];
|
||||||
|
sqlQueries: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SuggestedAction {
|
export interface SuggestedAction {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import started from "electron-squirrel-startup";
|
|||||||
import { updateElectronApp } from "update-electron-app";
|
import { updateElectronApp } from "update-electron-app";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { readSettings, writeSettings } from "./main/settings";
|
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.errorHandler.startCatching();
|
||||||
log.eventLogger.startLogging();
|
log.eventLogger.startLogging();
|
||||||
|
|||||||
280
src/prompts/supabase_prompt.ts
Normal file
280
src/prompts/supabase_prompt.ts
Normal file
@@ -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.
|
||||||
|
|
||||||
|
<dyad-write path="src/integrations/supabase/client.ts" description="Creating a supabase client.">
|
||||||
|
$$SUPABASE_CLIENT_CODE$$
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
<dyad-write path="src/pages/Login.tsx" description="Creating a login page.">
|
||||||
|
import { Auth } from '@supabase/auth-ui-react';
|
||||||
|
import { ThemeSupa } from '@supabase/auth-ui-shared';
|
||||||
|
function Login() {
|
||||||
|
// Other code here
|
||||||
|
return (
|
||||||
|
<Auth
|
||||||
|
supabaseClient={supabase}
|
||||||
|
providers={[]}
|
||||||
|
appearance={{
|
||||||
|
theme: ThemeSupa,
|
||||||
|
}}
|
||||||
|
theme="light"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
If the user wants to use the database, use the following code:
|
||||||
|
|
||||||
|
<dyad-execute-sql>
|
||||||
|
SELECT * FROM users;
|
||||||
|
</dyad-execute-sql>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
<dyad-execute-sql>
|
||||||
|
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 );
|
||||||
|
</dyad-execute-sql>
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
<dyad-execute-sql>
|
||||||
|
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();
|
||||||
|
</dyad-execute-sql>
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
<code>
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
|
||||||
|
};
|
||||||
|
</code>
|
||||||
|
|
||||||
|
- Implement OPTIONS request handler:
|
||||||
|
|
||||||
|
<code>
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
</code>
|
||||||
|
|
||||||
|
|
||||||
|
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 <resource-link> for guidance
|
||||||
|
|
||||||
|
8. Logging:
|
||||||
|
- Implement comprehensive logging for debugging purposes
|
||||||
|
|
||||||
|
9. Linking:
|
||||||
|
Use <resource-link> 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:
|
||||||
|
|
||||||
|
<dyad-write path="src/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'
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
</dyad-write>
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
<dyad-add-integration provider="supabase"></dyad-add-integration>
|
||||||
|
`;
|
||||||
66
src/supabase_admin/supabase_context.ts
Normal file
66
src/supabase_admin/supabase_context.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { readSettings } from "../../main/settings";
|
import { readSettings } from "../main/settings";
|
||||||
import { SupabaseManagementAPI } from "supabase-management-js";
|
import { SupabaseManagementAPI } from "supabase-management-js";
|
||||||
|
|
||||||
// Function to get the Supabase Management API client
|
// Function to get the Supabase Management API client
|
||||||
@@ -26,3 +26,15 @@ export async function getSupabaseProjectName(
|
|||||||
const project = projects?.find((p) => p.id === projectId);
|
const project = projects?.find((p) => p.id === projectId);
|
||||||
return project?.name || `<project not found for: ${projectId}>`;
|
return project?.name || `<project not found for: ${projectId}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function executeSupabaseSql({
|
||||||
|
supabaseProjectId,
|
||||||
|
query,
|
||||||
|
}: {
|
||||||
|
supabaseProjectId: string;
|
||||||
|
query: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
const supabase = await getSupabaseClient();
|
||||||
|
const result = await supabase.runQuery(supabaseProjectId, query);
|
||||||
|
return JSON.stringify(result);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { writeSettings } from "./settings";
|
import { writeSettings } from "../main/settings";
|
||||||
|
|
||||||
export function handleSupabaseOAuthReturn({
|
export function handleSupabaseOAuthReturn({
|
||||||
token,
|
token,
|
||||||
107
src/supabase_admin/supabase_schema_query.ts
Normal file
107
src/supabase_admin/supabase_schema_query.ts
Normal file
@@ -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;
|
||||||
|
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user