Initial commit: MCP Hub Community Edition v3.0.0
Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
23
plugins/supabase/handlers/__init__.py
Normal file
23
plugins/supabase/handlers/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Supabase Plugin Handlers"""
|
||||
|
||||
from plugins.supabase.handlers import (
|
||||
admin,
|
||||
# Phase G.2
|
||||
auth,
|
||||
database,
|
||||
# Phase G.3
|
||||
functions,
|
||||
storage,
|
||||
system,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"database",
|
||||
"system",
|
||||
# Phase G.2
|
||||
"auth",
|
||||
"storage",
|
||||
# Phase G.3
|
||||
"functions",
|
||||
"admin",
|
||||
]
|
||||
625
plugins/supabase/handlers/admin.py
Normal file
625
plugins/supabase/handlers/admin.py
Normal file
@@ -0,0 +1,625 @@
|
||||
"""Admin Handler - manages Supabase database administration via postgres-meta"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.supabase.client import SupabaseClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (12 tools)"""
|
||||
return [
|
||||
# Extension Management
|
||||
{
|
||||
"name": "enable_extension",
|
||||
"method_name": "enable_extension",
|
||||
"description": "Enable a PostgreSQL extension (e.g., pgvector, postgis, uuid-ossp).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Extension name to enable"},
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "Schema to install extension in",
|
||||
"default": "extensions",
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "disable_extension",
|
||||
"method_name": "disable_extension",
|
||||
"description": "Disable (drop) a PostgreSQL extension.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Extension name to disable"},
|
||||
"cascade": {
|
||||
"type": "boolean",
|
||||
"description": "Drop dependent objects",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# RLS Policy Management
|
||||
{
|
||||
"name": "create_policy",
|
||||
"method_name": "create_policy",
|
||||
"description": "Create a Row Level Security (RLS) policy on a table.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"name": {"type": "string", "description": "Policy name"},
|
||||
"definition": {
|
||||
"type": "string",
|
||||
"description": "USING clause expression (e.g., 'auth.uid() = user_id')",
|
||||
},
|
||||
"check": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "WITH CHECK clause expression (for INSERT/UPDATE)",
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"enum": ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
"description": "Command this policy applies to",
|
||||
"default": "ALL",
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Roles this policy applies to",
|
||||
"default": ["authenticated"],
|
||||
},
|
||||
"schema": {"type": "string", "default": "public"},
|
||||
},
|
||||
"required": ["table", "name", "definition"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_policy",
|
||||
"method_name": "update_policy",
|
||||
"description": "Update an existing RLS policy. Drops and recreates the policy.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"name": {"type": "string", "description": "Policy name to update"},
|
||||
"definition": {"type": "string", "description": "New USING clause expression"},
|
||||
"check": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New WITH CHECK clause expression",
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"enum": ["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
"default": "ALL",
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"default": ["authenticated"],
|
||||
},
|
||||
"schema": {"type": "string", "default": "public"},
|
||||
},
|
||||
"required": ["table", "name", "definition"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_policy",
|
||||
"method_name": "delete_policy",
|
||||
"description": "Delete an RLS policy from a table.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"name": {"type": "string", "description": "Policy name to delete"},
|
||||
"schema": {"type": "string", "default": "public"},
|
||||
},
|
||||
"required": ["table", "name"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# RLS Table Management
|
||||
{
|
||||
"name": "enable_rls",
|
||||
"method_name": "enable_rls",
|
||||
"description": "Enable Row Level Security on a table.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"schema": {"type": "string", "default": "public"},
|
||||
},
|
||||
"required": ["table"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "disable_rls",
|
||||
"method_name": "disable_rls",
|
||||
"description": "Disable Row Level Security on a table. Warning: This removes all RLS protection.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"schema": {"type": "string", "default": "public"},
|
||||
},
|
||||
"required": ["table"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# Table Management
|
||||
{
|
||||
"name": "create_table",
|
||||
"method_name": "create_table",
|
||||
"description": "Create a new database table with specified columns.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Table name"},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"type": {"type": "string"},
|
||||
"nullable": {"type": "boolean", "default": True},
|
||||
"default": {"type": "string"},
|
||||
"primary_key": {"type": "boolean", "default": False},
|
||||
},
|
||||
"required": ["name", "type"],
|
||||
},
|
||||
"description": "Column definitions",
|
||||
},
|
||||
"schema": {"type": "string", "default": "public"},
|
||||
"enable_rls": {
|
||||
"type": "boolean",
|
||||
"description": "Enable RLS on the new table",
|
||||
"default": True,
|
||||
},
|
||||
},
|
||||
"required": ["name", "columns"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "drop_table",
|
||||
"method_name": "drop_table",
|
||||
"description": "Drop (delete) a database table. Warning: This is irreversible.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Table name to drop"},
|
||||
"schema": {"type": "string", "default": "public"},
|
||||
"cascade": {
|
||||
"type": "boolean",
|
||||
"description": "Drop dependent objects",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# Column Management
|
||||
{
|
||||
"name": "add_column",
|
||||
"method_name": "add_column",
|
||||
"description": "Add a new column to an existing table.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"column_name": {"type": "string", "description": "New column name"},
|
||||
"column_type": {
|
||||
"type": "string",
|
||||
"description": "PostgreSQL data type (e.g., 'text', 'integer', 'uuid')",
|
||||
},
|
||||
"nullable": {"type": "boolean", "default": True},
|
||||
"default_value": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Default value expression",
|
||||
},
|
||||
"schema": {"type": "string", "default": "public"},
|
||||
},
|
||||
"required": ["table", "column_name", "column_type"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "drop_column",
|
||||
"method_name": "drop_column",
|
||||
"description": "Remove a column from a table.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"column_name": {"type": "string", "description": "Column name to drop"},
|
||||
"schema": {"type": "string", "default": "public"},
|
||||
"cascade": {"type": "boolean", "default": False},
|
||||
},
|
||||
"required": ["table", "column_name"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# Database Stats
|
||||
{
|
||||
"name": "get_database_size",
|
||||
"method_name": "get_database_size",
|
||||
"description": "Get the size of the database and tables.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# Admin Operations (12 tools)
|
||||
# =====================
|
||||
|
||||
async def enable_extension(client: SupabaseClient, name: str, schema: str = "extensions") -> str:
|
||||
"""Enable a PostgreSQL extension"""
|
||||
try:
|
||||
query = f'CREATE EXTENSION IF NOT EXISTS "{name}" SCHEMA "{schema}";'
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Extension '{name}' enabled in schema '{schema}'",
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def disable_extension(client: SupabaseClient, name: str, cascade: bool = False) -> str:
|
||||
"""Disable (drop) a PostgreSQL extension"""
|
||||
try:
|
||||
cascade_sql = "CASCADE" if cascade else ""
|
||||
query = f'DROP EXTENSION IF EXISTS "{name}" {cascade_sql};'
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Extension '{name}' disabled",
|
||||
"cascade": cascade,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def create_policy(
|
||||
client: SupabaseClient,
|
||||
table: str,
|
||||
name: str,
|
||||
definition: str,
|
||||
check: str | None = None,
|
||||
command: str = "ALL",
|
||||
roles: list[str] | None = None,
|
||||
schema: str = "public",
|
||||
) -> str:
|
||||
"""Create an RLS policy"""
|
||||
try:
|
||||
if roles is None:
|
||||
roles = ["authenticated"]
|
||||
|
||||
roles_sql = ", ".join(roles)
|
||||
check_sql = f"WITH CHECK ({check})" if check else ""
|
||||
|
||||
query = f"""
|
||||
CREATE POLICY "{name}" ON "{schema}"."{table}"
|
||||
FOR {command}
|
||||
TO {roles_sql}
|
||||
USING ({definition})
|
||||
{check_sql};
|
||||
"""
|
||||
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Policy '{name}' created on table '{table}'",
|
||||
"table": f"{schema}.{table}",
|
||||
"policy": {
|
||||
"name": name,
|
||||
"command": command,
|
||||
"roles": roles,
|
||||
"using": definition,
|
||||
"check": check,
|
||||
},
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def update_policy(
|
||||
client: SupabaseClient,
|
||||
table: str,
|
||||
name: str,
|
||||
definition: str,
|
||||
check: str | None = None,
|
||||
command: str = "ALL",
|
||||
roles: list[str] | None = None,
|
||||
schema: str = "public",
|
||||
) -> str:
|
||||
"""Update an RLS policy by dropping and recreating"""
|
||||
try:
|
||||
# First drop the existing policy
|
||||
drop_query = f'DROP POLICY IF EXISTS "{name}" ON "{schema}"."{table}";'
|
||||
await client.execute_sql(drop_query)
|
||||
|
||||
# Then create the new one
|
||||
return await create_policy(
|
||||
client=client,
|
||||
table=table,
|
||||
name=name,
|
||||
definition=definition,
|
||||
check=check,
|
||||
command=command,
|
||||
roles=roles,
|
||||
schema=schema,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def delete_policy(
|
||||
client: SupabaseClient, table: str, name: str, schema: str = "public"
|
||||
) -> str:
|
||||
"""Delete an RLS policy"""
|
||||
try:
|
||||
query = f'DROP POLICY IF EXISTS "{name}" ON "{schema}"."{table}";'
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Policy '{name}' deleted from table '{table}'",
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def enable_rls(client: SupabaseClient, table: str, schema: str = "public") -> str:
|
||||
"""Enable RLS on a table"""
|
||||
try:
|
||||
query = f'ALTER TABLE "{schema}"."{table}" ENABLE ROW LEVEL SECURITY;'
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"RLS enabled on table '{schema}.{table}'",
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def disable_rls(client: SupabaseClient, table: str, schema: str = "public") -> str:
|
||||
"""Disable RLS on a table"""
|
||||
try:
|
||||
query = f'ALTER TABLE "{schema}"."{table}" DISABLE ROW LEVEL SECURITY;'
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"RLS disabled on table '{schema}.{table}'",
|
||||
"warning": "Table is now accessible without RLS protection",
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def create_table(
|
||||
client: SupabaseClient,
|
||||
name: str,
|
||||
columns: list[dict],
|
||||
schema: str = "public",
|
||||
enable_rls: bool = True,
|
||||
) -> str:
|
||||
"""Create a new database table"""
|
||||
try:
|
||||
# Build column definitions
|
||||
col_defs = []
|
||||
for col in columns:
|
||||
col_def = f'"{col["name"]}" {col["type"]}'
|
||||
|
||||
if col.get("primary_key"):
|
||||
col_def += " PRIMARY KEY"
|
||||
|
||||
if not col.get("nullable", True):
|
||||
col_def += " NOT NULL"
|
||||
|
||||
if "default" in col:
|
||||
col_def += f' DEFAULT {col["default"]}'
|
||||
|
||||
col_defs.append(col_def)
|
||||
|
||||
columns_sql = ",\n ".join(col_defs)
|
||||
|
||||
query = f"""
|
||||
CREATE TABLE "{schema}"."{name}" (
|
||||
{columns_sql}
|
||||
);
|
||||
"""
|
||||
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
# Enable RLS if requested
|
||||
if enable_rls:
|
||||
rls_query = f'ALTER TABLE "{schema}"."{name}" ENABLE ROW LEVEL SECURITY;'
|
||||
await client.execute_sql(rls_query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Table '{schema}.{name}' created",
|
||||
"columns": columns,
|
||||
"rls_enabled": enable_rls,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def drop_table(
|
||||
client: SupabaseClient, name: str, schema: str = "public", cascade: bool = False
|
||||
) -> str:
|
||||
"""Drop a database table"""
|
||||
try:
|
||||
cascade_sql = "CASCADE" if cascade else ""
|
||||
query = f'DROP TABLE IF EXISTS "{schema}"."{name}" {cascade_sql};'
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Table '{schema}.{name}' dropped",
|
||||
"cascade": cascade,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def add_column(
|
||||
client: SupabaseClient,
|
||||
table: str,
|
||||
column_name: str,
|
||||
column_type: str,
|
||||
nullable: bool = True,
|
||||
default_value: str | None = None,
|
||||
schema: str = "public",
|
||||
) -> str:
|
||||
"""Add a column to a table"""
|
||||
try:
|
||||
nullable_sql = "" if nullable else "NOT NULL"
|
||||
default_sql = f"DEFAULT {default_value}" if default_value else ""
|
||||
|
||||
query = f"""
|
||||
ALTER TABLE "{schema}"."{table}"
|
||||
ADD COLUMN "{column_name}" {column_type} {nullable_sql} {default_sql};
|
||||
"""
|
||||
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Column '{column_name}' added to table '{schema}.{table}'",
|
||||
"column": {
|
||||
"name": column_name,
|
||||
"type": column_type,
|
||||
"nullable": nullable,
|
||||
"default": default_value,
|
||||
},
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def drop_column(
|
||||
client: SupabaseClient,
|
||||
table: str,
|
||||
column_name: str,
|
||||
schema: str = "public",
|
||||
cascade: bool = False,
|
||||
) -> str:
|
||||
"""Drop a column from a table"""
|
||||
try:
|
||||
cascade_sql = "CASCADE" if cascade else ""
|
||||
query = f"""
|
||||
ALTER TABLE "{schema}"."{table}"
|
||||
DROP COLUMN "{column_name}" {cascade_sql};
|
||||
"""
|
||||
|
||||
result = await client.execute_sql(query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Column '{column_name}' dropped from table '{schema}.{table}'",
|
||||
"cascade": cascade,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_database_size(client: SupabaseClient) -> str:
|
||||
"""Get database and table sizes"""
|
||||
try:
|
||||
# Get database size
|
||||
db_query = """
|
||||
SELECT
|
||||
pg_database.datname AS database_name,
|
||||
pg_size_pretty(pg_database_size(pg_database.datname)) AS size
|
||||
FROM pg_database
|
||||
WHERE pg_database.datname = current_database();
|
||||
"""
|
||||
|
||||
db_result = await client.execute_sql(db_query)
|
||||
|
||||
# Get table sizes
|
||||
tables_query = """
|
||||
SELECT
|
||||
schemaname AS schema,
|
||||
tablename AS table,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) AS total_size,
|
||||
pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) AS data_size,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename) - pg_relation_size(schemaname || '.' || tablename)) AS index_size
|
||||
FROM pg_tables
|
||||
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
||||
ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC
|
||||
LIMIT 20;
|
||||
"""
|
||||
|
||||
tables_result = await client.execute_sql(tables_query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"database": db_result[0] if db_result else {},
|
||||
"top_tables": tables_result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
540
plugins/supabase/handlers/auth.py
Normal file
540
plugins/supabase/handlers/auth.py
Normal file
@@ -0,0 +1,540 @@
|
||||
"""Auth Handler - manages Supabase authentication via GoTrue Admin API"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.supabase.client import SupabaseClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (14 tools)"""
|
||||
return [
|
||||
{
|
||||
"name": "list_users",
|
||||
"method_name": "list_users",
|
||||
"description": "List all users with pagination. Returns user details including email, phone, metadata, and confirmation status.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Page number (1-indexed)",
|
||||
"default": 1,
|
||||
},
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Users per page (max 1000)",
|
||||
"default": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "get_user",
|
||||
"method_name": "get_user",
|
||||
"description": "Get detailed information about a specific user by their ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"user_id": {"type": "string", "description": "User UUID"}},
|
||||
"required": ["user_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_user",
|
||||
"method_name": "create_user",
|
||||
"description": "Create a new user with email and password. Can optionally auto-confirm email and set metadata.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "User email address",
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"minLength": 6,
|
||||
"description": "Password (min 6 characters)",
|
||||
},
|
||||
"phone": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Phone number in E.164 format",
|
||||
},
|
||||
"email_confirm": {
|
||||
"type": "boolean",
|
||||
"description": "Auto-confirm email without verification",
|
||||
"default": False,
|
||||
},
|
||||
"user_metadata": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Custom user metadata (name, avatar, etc.)",
|
||||
},
|
||||
"app_metadata": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "App-specific metadata (roles, permissions)",
|
||||
},
|
||||
},
|
||||
"required": ["email", "password"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_user",
|
||||
"method_name": "update_user",
|
||||
"description": "Update user details including email, password, phone, metadata, or ban status.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_id": {"type": "string", "description": "User UUID"},
|
||||
"email": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New email address",
|
||||
},
|
||||
"password": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New password",
|
||||
},
|
||||
"phone": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "New phone number",
|
||||
},
|
||||
"email_confirm": {
|
||||
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||
"description": "Set email confirmation status",
|
||||
},
|
||||
"phone_confirm": {
|
||||
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||
"description": "Set phone confirmation status",
|
||||
},
|
||||
"user_metadata": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Update user metadata",
|
||||
},
|
||||
"app_metadata": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Update app metadata",
|
||||
},
|
||||
},
|
||||
"required": ["user_id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_user",
|
||||
"method_name": "delete_user",
|
||||
"description": "Permanently delete a user and all their data.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"user_id": {"type": "string", "description": "User UUID to delete"}},
|
||||
"required": ["user_id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "invite_user",
|
||||
"method_name": "invite_user",
|
||||
"description": "Send an email invitation to a new user. Creates user in 'invited' state.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "Email to invite",
|
||||
},
|
||||
"redirect_to": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "URL to redirect after accepting invitation",
|
||||
},
|
||||
"user_metadata": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Initial user metadata",
|
||||
},
|
||||
},
|
||||
"required": ["email"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "generate_link",
|
||||
"method_name": "generate_link",
|
||||
"description": "Generate a magic link, recovery link, or invite link for a user.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {"type": "string", "format": "email", "description": "User email"},
|
||||
"link_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"magiclink",
|
||||
"recovery",
|
||||
"invite",
|
||||
"signup",
|
||||
"email_change_new",
|
||||
"email_change_current",
|
||||
],
|
||||
"description": "Type of link to generate",
|
||||
"default": "magiclink",
|
||||
},
|
||||
"redirect_to": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "URL to redirect after link is used",
|
||||
},
|
||||
},
|
||||
"required": ["email"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "ban_user",
|
||||
"method_name": "ban_user",
|
||||
"description": "Ban a user for a specified duration. Banned users cannot sign in.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_id": {"type": "string", "description": "User UUID"},
|
||||
"duration": {
|
||||
"type": "string",
|
||||
"description": "Ban duration (e.g., '24h', '7d', '1y', 'none' for permanent)",
|
||||
"default": "none",
|
||||
},
|
||||
},
|
||||
"required": ["user_id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "unban_user",
|
||||
"method_name": "unban_user",
|
||||
"description": "Remove ban from a user, allowing them to sign in again.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"user_id": {"type": "string", "description": "User UUID"}},
|
||||
"required": ["user_id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "list_user_factors",
|
||||
"method_name": "list_user_factors",
|
||||
"description": "List all MFA factors (TOTP, phone) for a user.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"user_id": {"type": "string", "description": "User UUID"}},
|
||||
"required": ["user_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "delete_user_factor",
|
||||
"method_name": "delete_user_factor",
|
||||
"description": "Delete an MFA factor from a user.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_id": {"type": "string", "description": "User UUID"},
|
||||
"factor_id": {"type": "string", "description": "Factor UUID to delete"},
|
||||
},
|
||||
"required": ["user_id", "factor_id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "get_auth_config",
|
||||
"method_name": "get_auth_config",
|
||||
"description": "Get current GoTrue authentication configuration.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "search_users",
|
||||
"method_name": "search_users",
|
||||
"description": "Search users by email or phone number.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query (email or phone)"},
|
||||
"page": {"type": "integer", "default": 1},
|
||||
"per_page": {"type": "integer", "default": 50},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_user_by_email",
|
||||
"method_name": "get_user_by_email",
|
||||
"description": "Find a user by their email address.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "Email to search for",
|
||||
}
|
||||
},
|
||||
"required": ["email"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# Auth Operations (14 tools)
|
||||
# =====================
|
||||
|
||||
async def list_users(client: SupabaseClient, page: int = 1, per_page: int = 50) -> str:
|
||||
"""List all users with pagination"""
|
||||
try:
|
||||
result = await client.list_users(page=page, per_page=per_page)
|
||||
|
||||
users = result.get("users", []) if isinstance(result, dict) else result
|
||||
result.get("aud", len(users)) if isinstance(result, dict) else len(users)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"count": len(users) if isinstance(users, list) else 0,
|
||||
"users": users,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_user(client: SupabaseClient, user_id: str) -> str:
|
||||
"""Get user by ID"""
|
||||
try:
|
||||
result = await client.get_user(user_id)
|
||||
|
||||
return json.dumps({"success": True, "user": result}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def create_user(
|
||||
client: SupabaseClient,
|
||||
email: str,
|
||||
password: str,
|
||||
phone: str | None = None,
|
||||
email_confirm: bool = False,
|
||||
user_metadata: dict | None = None,
|
||||
app_metadata: dict | None = None,
|
||||
) -> str:
|
||||
"""Create a new user"""
|
||||
try:
|
||||
result = await client.create_user(
|
||||
email=email,
|
||||
password=password,
|
||||
phone=phone,
|
||||
email_confirm=email_confirm,
|
||||
user_metadata=user_metadata,
|
||||
app_metadata=app_metadata,
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": "User created successfully", "user": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def update_user(
|
||||
client: SupabaseClient,
|
||||
user_id: str,
|
||||
email: str | None = None,
|
||||
password: str | None = None,
|
||||
phone: str | None = None,
|
||||
email_confirm: bool | None = None,
|
||||
phone_confirm: bool | None = None,
|
||||
user_metadata: dict | None = None,
|
||||
app_metadata: dict | None = None,
|
||||
) -> str:
|
||||
"""Update user details"""
|
||||
try:
|
||||
result = await client.update_user(
|
||||
user_id=user_id,
|
||||
email=email,
|
||||
password=password,
|
||||
phone=phone,
|
||||
email_confirm=email_confirm,
|
||||
phone_confirm=phone_confirm,
|
||||
user_metadata=user_metadata,
|
||||
app_metadata=app_metadata,
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": "User updated successfully", "user": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def delete_user(client: SupabaseClient, user_id: str) -> str:
|
||||
"""Delete a user"""
|
||||
try:
|
||||
await client.delete_user(user_id)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"User {user_id} deleted successfully"},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def invite_user(
|
||||
client: SupabaseClient,
|
||||
email: str,
|
||||
redirect_to: str | None = None,
|
||||
user_metadata: dict | None = None,
|
||||
) -> str:
|
||||
"""Invite a user by email"""
|
||||
try:
|
||||
result = await client.generate_link(
|
||||
email=email, link_type="invite", redirect_to=redirect_to
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Invitation sent to {email}", "result": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def generate_link(
|
||||
client: SupabaseClient, email: str, link_type: str = "magiclink", redirect_to: str | None = None
|
||||
) -> str:
|
||||
"""Generate auth link (magic link, recovery, invite)"""
|
||||
try:
|
||||
result = await client.generate_link(
|
||||
email=email, link_type=link_type, redirect_to=redirect_to
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "link_type": link_type, "email": email, "result": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def ban_user(client: SupabaseClient, user_id: str, duration: str = "none") -> str:
|
||||
"""Ban a user"""
|
||||
try:
|
||||
result = await client.update_user(user_id=user_id, ban_duration=duration)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"User {user_id} banned for {duration}", "user": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def unban_user(client: SupabaseClient, user_id: str) -> str:
|
||||
"""Unban a user"""
|
||||
try:
|
||||
result = await client.update_user(user_id=user_id, ban_duration="0")
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"User {user_id} unbanned", "user": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def list_user_factors(client: SupabaseClient, user_id: str) -> str:
|
||||
"""List user MFA factors"""
|
||||
try:
|
||||
result = await client.list_user_factors(user_id)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "user_id": user_id, "factors": result}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def delete_user_factor(client: SupabaseClient, user_id: str, factor_id: str) -> str:
|
||||
"""Delete an MFA factor"""
|
||||
try:
|
||||
await client.delete_user_factor(user_id, factor_id)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Factor {factor_id} deleted from user {user_id}"},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_auth_config(client: SupabaseClient) -> str:
|
||||
"""Get auth configuration"""
|
||||
try:
|
||||
# Get health which includes some config info
|
||||
result = await client.request("GET", "/auth/v1/health", use_service_role=False)
|
||||
|
||||
return json.dumps({"success": True, "config": result}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def search_users(
|
||||
client: SupabaseClient, query: str, page: int = 1, per_page: int = 50
|
||||
) -> str:
|
||||
"""Search users by email or phone"""
|
||||
try:
|
||||
# Get all users and filter
|
||||
result = await client.list_users(page=page, per_page=per_page)
|
||||
|
||||
users = result.get("users", []) if isinstance(result, dict) else result
|
||||
|
||||
# Filter by query
|
||||
query_lower = query.lower()
|
||||
filtered = [
|
||||
u
|
||||
for u in users
|
||||
if (
|
||||
u.get("email", "").lower().find(query_lower) >= 0
|
||||
or u.get("phone", "").find(query) >= 0
|
||||
)
|
||||
]
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "query": query, "count": len(filtered), "users": filtered},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_user_by_email(client: SupabaseClient, email: str) -> str:
|
||||
"""Find user by email"""
|
||||
try:
|
||||
# Get all users and find by email
|
||||
result = await client.list_users(page=1, per_page=1000)
|
||||
|
||||
users = result.get("users", []) if isinstance(result, dict) else result
|
||||
|
||||
# Find exact match
|
||||
user = next((u for u in users if u.get("email", "").lower() == email.lower()), None)
|
||||
|
||||
if user:
|
||||
return json.dumps(
|
||||
{"success": True, "found": True, "user": user}, indent=2, ensure_ascii=False
|
||||
)
|
||||
else:
|
||||
return json.dumps(
|
||||
{"success": True, "found": False, "message": f"No user found with email: {email}"},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
940
plugins/supabase/handlers/database.py
Normal file
940
plugins/supabase/handlers/database.py
Normal file
@@ -0,0 +1,940 @@
|
||||
"""Database Handler - manages Supabase database operations via PostgREST and postgres-meta"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.supabase.client import SupabaseClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (18 tools)"""
|
||||
return [
|
||||
# =====================
|
||||
# PostgREST Operations (6)
|
||||
# =====================
|
||||
{
|
||||
"name": "query_table",
|
||||
"method_name": "query_table",
|
||||
"description": "Query data from a table with filters, sorting, and pagination. Uses PostgREST operators: eq, neq, gt, gte, lt, lte, like, ilike, in, is, cs (contains), cd (contained).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name to query"},
|
||||
"select": {
|
||||
"type": "string",
|
||||
"description": "Columns to select (default: *). Use * for all, or comma-separated list",
|
||||
"default": "*",
|
||||
},
|
||||
"filters": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"column": {"type": "string"},
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"eq",
|
||||
"neq",
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
"like",
|
||||
"ilike",
|
||||
"in",
|
||||
"is",
|
||||
"cs",
|
||||
"cd",
|
||||
],
|
||||
},
|
||||
"value": {},
|
||||
},
|
||||
"required": ["column", "value"],
|
||||
},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"description": "Filter conditions. Each filter has column, operator (default: eq), and value",
|
||||
},
|
||||
"order": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Order by clause (e.g., 'created_at.desc' or 'name.asc')",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum rows to return",
|
||||
"default": 100,
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "Number of rows to skip for pagination",
|
||||
"default": 0,
|
||||
},
|
||||
"use_service_role": {
|
||||
"type": "boolean",
|
||||
"description": "Use service_role key to bypass RLS policies",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["table"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "insert_rows",
|
||||
"method_name": "insert_rows",
|
||||
"description": "Insert one or more rows into a table. Supports upsert mode for insert-or-update operations.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
"description": "Array of row objects to insert",
|
||||
},
|
||||
"upsert": {
|
||||
"type": "boolean",
|
||||
"description": "Enable upsert mode (insert or update on conflict)",
|
||||
"default": False,
|
||||
},
|
||||
"on_conflict": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Column(s) to check for conflict (for upsert)",
|
||||
},
|
||||
"use_service_role": {
|
||||
"type": "boolean",
|
||||
"description": "Use service_role key to bypass RLS policies",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["table", "rows"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_rows",
|
||||
"method_name": "update_rows",
|
||||
"description": "Update rows matching filter conditions. Always requires at least one filter to prevent accidental full-table updates.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"data": {"type": "object", "description": "Fields to update with new values"},
|
||||
"filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"column": {"type": "string"},
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"eq",
|
||||
"neq",
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
"like",
|
||||
"ilike",
|
||||
"in",
|
||||
"is",
|
||||
],
|
||||
},
|
||||
"value": {},
|
||||
},
|
||||
"required": ["column", "value"],
|
||||
},
|
||||
"description": "Filter conditions to match rows for update",
|
||||
"minItems": 1,
|
||||
},
|
||||
"use_service_role": {
|
||||
"type": "boolean",
|
||||
"description": "Use service_role key to bypass RLS policies",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["table", "data", "filters"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_rows",
|
||||
"method_name": "delete_rows",
|
||||
"description": "Delete rows matching filter conditions. Always requires at least one filter to prevent accidental full-table deletion.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"column": {"type": "string"},
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"eq",
|
||||
"neq",
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
"like",
|
||||
"ilike",
|
||||
"in",
|
||||
"is",
|
||||
],
|
||||
},
|
||||
"value": {},
|
||||
},
|
||||
"required": ["column", "value"],
|
||||
},
|
||||
"description": "Filter conditions to match rows for deletion",
|
||||
"minItems": 1,
|
||||
},
|
||||
"use_service_role": {
|
||||
"type": "boolean",
|
||||
"description": "Use service_role key to bypass RLS policies",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["table", "filters"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "execute_rpc",
|
||||
"method_name": "execute_rpc",
|
||||
"description": "Execute a stored procedure or database function via RPC. Functions must be exposed through PostgREST.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the stored procedure/function",
|
||||
},
|
||||
"params": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Parameters to pass to the function",
|
||||
},
|
||||
"use_service_role": {
|
||||
"type": "boolean",
|
||||
"description": "Use service_role key to bypass RLS policies",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["function_name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "count_rows",
|
||||
"method_name": "count_rows",
|
||||
"description": "Count rows in a table, optionally matching filter conditions.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"column": {"type": "string"},
|
||||
"operator": {"type": "string"},
|
||||
"value": {},
|
||||
},
|
||||
"required": ["column", "value"],
|
||||
},
|
||||
"description": "Optional filter conditions (omit for full count)",
|
||||
"default": [],
|
||||
},
|
||||
"use_service_role": {
|
||||
"type": "boolean",
|
||||
"description": "Use service_role key to bypass RLS policies",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["table"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
# =====================
|
||||
# Admin Operations via postgres-meta (12)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_tables",
|
||||
"method_name": "list_tables",
|
||||
"description": "List all tables in the database with pagination. Returns table names, schemas, and basic metadata.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "Schema to list tables from",
|
||||
"default": "public",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum tables to return (default: 50)",
|
||||
"default": 50,
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "Number of tables to skip for pagination",
|
||||
"default": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_table_schema",
|
||||
"method_name": "get_table_schema",
|
||||
"description": "Get detailed schema information for a table including all columns, data types, constraints, and defaults.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"schema": {"type": "string", "description": "Schema name", "default": "public"},
|
||||
},
|
||||
"required": ["table"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "list_schemas",
|
||||
"method_name": "list_schemas",
|
||||
"description": "List all database schemas (namespaces).",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "list_extensions",
|
||||
"method_name": "list_extensions",
|
||||
"description": "List installed PostgreSQL extensions (e.g., pgvector, postgis, uuid-ossp).",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "list_policies",
|
||||
"method_name": "list_policies",
|
||||
"description": "List Row Level Security (RLS) policies. Can filter by table name.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Filter policies for specific table",
|
||||
}
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "list_roles",
|
||||
"method_name": "list_roles",
|
||||
"description": "List all database roles with their attributes and permissions.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "list_triggers",
|
||||
"method_name": "list_triggers",
|
||||
"description": "List database triggers. Can filter by table name.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Filter triggers for specific table",
|
||||
}
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "list_functions",
|
||||
"method_name": "list_functions",
|
||||
"description": "List database functions/stored procedures in a schema with pagination.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "Schema to list functions from",
|
||||
"default": "public",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum functions to return (default: 50)",
|
||||
"default": 50,
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "Number of functions to skip for pagination",
|
||||
"default": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "execute_sql",
|
||||
"method_name": "execute_sql",
|
||||
"description": "Execute raw SQL query. Use with caution - bypasses RLS. Returns query results.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string", "description": "SQL query to execute"}},
|
||||
"required": ["query"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "get_table_indexes",
|
||||
"method_name": "get_table_indexes",
|
||||
"description": "Get indexes for a table including primary keys, unique constraints, and custom indexes.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"schema": {"type": "string", "description": "Schema name", "default": "public"},
|
||||
},
|
||||
"required": ["table"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_table_constraints",
|
||||
"method_name": "get_table_constraints",
|
||||
"description": "Get all constraints (primary key, foreign key, unique, check) for a table.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"schema": {"type": "string", "description": "Schema name", "default": "public"},
|
||||
},
|
||||
"required": ["table"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_table_relationships",
|
||||
"method_name": "get_table_relationships",
|
||||
"description": "Get foreign key relationships for a table (both references to and from this table).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table": {"type": "string", "description": "Table name"},
|
||||
"schema": {"type": "string", "description": "Schema name", "default": "public"},
|
||||
},
|
||||
"required": ["table"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# PostgREST Operations (6)
|
||||
# =====================
|
||||
|
||||
async def query_table(
|
||||
client: SupabaseClient,
|
||||
table: str,
|
||||
select: str = "*",
|
||||
filters: list[dict] | None = None,
|
||||
order: str | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
use_service_role: bool = False,
|
||||
) -> str:
|
||||
"""Query data from a table"""
|
||||
try:
|
||||
result = await client.query_table(
|
||||
table=table,
|
||||
select=select,
|
||||
filters=filters,
|
||||
order=order,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
use_service_role=use_service_role,
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"table": table,
|
||||
"count": len(result) if isinstance(result, list) else 1,
|
||||
"data": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def insert_rows(
|
||||
client: SupabaseClient,
|
||||
table: str,
|
||||
rows: list[dict],
|
||||
upsert: bool = False,
|
||||
on_conflict: str | None = None,
|
||||
use_service_role: bool = False,
|
||||
) -> str:
|
||||
"""Insert rows into a table"""
|
||||
try:
|
||||
result = await client.insert_rows(
|
||||
table=table,
|
||||
rows=rows,
|
||||
upsert=upsert,
|
||||
on_conflict=on_conflict,
|
||||
use_service_role=use_service_role,
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"table": table,
|
||||
"inserted": len(result) if isinstance(result, list) else 1,
|
||||
"data": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def update_rows(
|
||||
client: SupabaseClient,
|
||||
table: str,
|
||||
data: dict,
|
||||
filters: list[dict],
|
||||
use_service_role: bool = False,
|
||||
) -> str:
|
||||
"""Update rows in a table"""
|
||||
try:
|
||||
if not filters:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "At least one filter is required to prevent accidental full-table updates",
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
result = await client.update_rows(
|
||||
table=table, data=data, filters=filters, use_service_role=use_service_role
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"table": table,
|
||||
"updated": len(result) if isinstance(result, list) else 1,
|
||||
"data": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def delete_rows(
|
||||
client: SupabaseClient, table: str, filters: list[dict], use_service_role: bool = False
|
||||
) -> str:
|
||||
"""Delete rows from a table"""
|
||||
try:
|
||||
if not filters:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": "At least one filter is required to prevent accidental full-table deletion",
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
result = await client.delete_rows(
|
||||
table=table, filters=filters, use_service_role=use_service_role
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"table": table,
|
||||
"deleted": len(result) if isinstance(result, list) else 1,
|
||||
"data": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def execute_rpc(
|
||||
client: SupabaseClient,
|
||||
function_name: str,
|
||||
params: dict | None = None,
|
||||
use_service_role: bool = False,
|
||||
) -> str:
|
||||
"""Execute a stored procedure/function"""
|
||||
try:
|
||||
result = await client.execute_rpc(
|
||||
function_name=function_name, params=params, use_service_role=use_service_role
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "function": function_name, "result": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def count_rows(
|
||||
client: SupabaseClient,
|
||||
table: str,
|
||||
filters: list[dict] | None = None,
|
||||
use_service_role: bool = False,
|
||||
) -> str:
|
||||
"""Count rows in a table"""
|
||||
try:
|
||||
count = await client.count_rows(
|
||||
table=table, filters=filters, use_service_role=use_service_role
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"table": table,
|
||||
"count": count,
|
||||
"filters_applied": len(filters) if filters else 0,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
# =====================
|
||||
# Admin Operations via postgres-meta (12)
|
||||
# =====================
|
||||
|
||||
async def list_tables(
|
||||
client: SupabaseClient, schema: str = "public", limit: int = 50, offset: int = 0
|
||||
) -> str:
|
||||
"""List all tables in the database with pagination"""
|
||||
try:
|
||||
result = await client.list_tables(schema=schema)
|
||||
|
||||
# Filter by schema if needed
|
||||
if isinstance(result, list):
|
||||
tables = [t for t in result if t.get("schema") == schema]
|
||||
else:
|
||||
tables = result if result else []
|
||||
|
||||
# Apply pagination
|
||||
total_count = len(tables)
|
||||
paginated = tables[offset : offset + limit] if isinstance(tables, list) else tables
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"schema": schema,
|
||||
"total_count": total_count,
|
||||
"count": len(paginated) if isinstance(paginated, list) else 0,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"has_more": (offset + limit) < total_count,
|
||||
"tables": paginated,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_table_schema(client: SupabaseClient, table: str, schema: str = "public") -> str:
|
||||
"""Get table schema/columns"""
|
||||
try:
|
||||
result = await client.get_table_schema(table=table, schema=schema)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"table": table,
|
||||
"schema": schema,
|
||||
"columns": result.get("columns", []),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def list_schemas(client: SupabaseClient) -> str:
|
||||
"""List all database schemas"""
|
||||
try:
|
||||
result = await client.list_schemas()
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"count": len(result) if isinstance(result, list) else 0,
|
||||
"schemas": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def list_extensions(client: SupabaseClient) -> str:
|
||||
"""List installed extensions"""
|
||||
try:
|
||||
result = await client.list_extensions()
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"count": len(result) if isinstance(result, list) else 0,
|
||||
"extensions": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def list_policies(client: SupabaseClient, table: str | None = None) -> str:
|
||||
"""List RLS policies"""
|
||||
try:
|
||||
result = await client.list_policies(table=table)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"table_filter": table,
|
||||
"count": len(result) if isinstance(result, list) else 0,
|
||||
"policies": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def list_roles(client: SupabaseClient) -> str:
|
||||
"""List database roles"""
|
||||
try:
|
||||
result = await client.list_roles()
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"count": len(result) if isinstance(result, list) else 0,
|
||||
"roles": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def list_triggers(client: SupabaseClient, table: str | None = None) -> str:
|
||||
"""List database triggers"""
|
||||
try:
|
||||
result = await client.list_triggers(table=table)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"table_filter": table,
|
||||
"count": len(result) if isinstance(result, list) else 0,
|
||||
"triggers": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def list_functions(
|
||||
client: SupabaseClient, schema: str = "public", limit: int = 50, offset: int = 0
|
||||
) -> str:
|
||||
"""List database functions with pagination"""
|
||||
try:
|
||||
result = await client.list_functions(schema=schema)
|
||||
|
||||
# Apply pagination
|
||||
functions = result if isinstance(result, list) else []
|
||||
total_count = len(functions)
|
||||
paginated = functions[offset : offset + limit]
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"schema": schema,
|
||||
"total_count": total_count,
|
||||
"count": len(paginated),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"has_more": (offset + limit) < total_count,
|
||||
"functions": paginated,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def execute_sql(client: SupabaseClient, query: str) -> str:
|
||||
"""Execute raw SQL query"""
|
||||
try:
|
||||
result = await client.execute_sql(query=query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"query": query[:200] + "..." if len(query) > 200 else query,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_table_indexes(client: SupabaseClient, table: str, schema: str = "public") -> str:
|
||||
"""Get indexes for a table"""
|
||||
try:
|
||||
query = f"""
|
||||
SELECT
|
||||
i.relname AS index_name,
|
||||
a.attname AS column_name,
|
||||
am.amname AS index_type,
|
||||
ix.indisunique AS is_unique,
|
||||
ix.indisprimary AS is_primary
|
||||
FROM
|
||||
pg_index ix
|
||||
JOIN pg_class i ON i.oid = ix.indexrelid
|
||||
JOIN pg_class t ON t.oid = ix.indrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
JOIN pg_am am ON am.oid = i.relam
|
||||
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
||||
WHERE
|
||||
t.relname = '{table}'
|
||||
AND n.nspname = '{schema}'
|
||||
ORDER BY i.relname;
|
||||
"""
|
||||
result = await client.execute_sql(query=query)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "table": table, "schema": schema, "indexes": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_table_constraints(client: SupabaseClient, table: str, schema: str = "public") -> str:
|
||||
"""Get constraints for a table"""
|
||||
try:
|
||||
query = f"""
|
||||
SELECT
|
||||
con.conname AS constraint_name,
|
||||
con.contype AS constraint_type,
|
||||
CASE con.contype
|
||||
WHEN 'p' THEN 'PRIMARY KEY'
|
||||
WHEN 'f' THEN 'FOREIGN KEY'
|
||||
WHEN 'u' THEN 'UNIQUE'
|
||||
WHEN 'c' THEN 'CHECK'
|
||||
WHEN 'x' THEN 'EXCLUSION'
|
||||
END AS constraint_type_name,
|
||||
pg_get_constraintdef(con.oid) AS definition
|
||||
FROM
|
||||
pg_constraint con
|
||||
JOIN pg_class rel ON rel.oid = con.conrelid
|
||||
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
||||
WHERE
|
||||
rel.relname = '{table}'
|
||||
AND nsp.nspname = '{schema}'
|
||||
ORDER BY con.contype;
|
||||
"""
|
||||
result = await client.execute_sql(query=query)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "table": table, "schema": schema, "constraints": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_table_relationships(
|
||||
client: SupabaseClient, table: str, schema: str = "public"
|
||||
) -> str:
|
||||
"""Get foreign key relationships for a table"""
|
||||
try:
|
||||
# Get foreign keys from this table
|
||||
outgoing_query = f"""
|
||||
SELECT
|
||||
con.conname AS constraint_name,
|
||||
att.attname AS column_name,
|
||||
ref_class.relname AS referenced_table,
|
||||
ref_att.attname AS referenced_column
|
||||
FROM
|
||||
pg_constraint con
|
||||
JOIN pg_class rel ON rel.oid = con.conrelid
|
||||
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
||||
JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey)
|
||||
JOIN pg_class ref_class ON ref_class.oid = con.confrelid
|
||||
JOIN pg_attribute ref_att ON ref_att.attrelid = con.confrelid AND ref_att.attnum = ANY(con.confkey)
|
||||
WHERE
|
||||
con.contype = 'f'
|
||||
AND rel.relname = '{table}'
|
||||
AND nsp.nspname = '{schema}';
|
||||
"""
|
||||
|
||||
# Get foreign keys referencing this table
|
||||
incoming_query = f"""
|
||||
SELECT
|
||||
con.conname AS constraint_name,
|
||||
src_class.relname AS source_table,
|
||||
att.attname AS source_column,
|
||||
ref_att.attname AS referenced_column
|
||||
FROM
|
||||
pg_constraint con
|
||||
JOIN pg_class src_class ON src_class.oid = con.conrelid
|
||||
JOIN pg_class ref_class ON ref_class.oid = con.confrelid
|
||||
JOIN pg_namespace nsp ON nsp.oid = ref_class.relnamespace
|
||||
JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey)
|
||||
JOIN pg_attribute ref_att ON ref_att.attrelid = con.confrelid AND ref_att.attnum = ANY(con.confkey)
|
||||
WHERE
|
||||
con.contype = 'f'
|
||||
AND ref_class.relname = '{table}'
|
||||
AND nsp.nspname = '{schema}';
|
||||
"""
|
||||
|
||||
outgoing = await client.execute_sql(query=outgoing_query)
|
||||
incoming = await client.execute_sql(query=incoming_query)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"table": table,
|
||||
"schema": schema,
|
||||
"outgoing_references": outgoing,
|
||||
"incoming_references": incoming,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
383
plugins/supabase/handlers/functions.py
Normal file
383
plugins/supabase/handlers/functions.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""Functions Handler - manages Supabase Edge Functions"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.supabase.client import SupabaseClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (8 tools)"""
|
||||
return [
|
||||
{
|
||||
"name": "invoke_function",
|
||||
"method_name": "invoke_function",
|
||||
"description": "Invoke a Supabase Edge Function with POST method. Pass data in the body parameter.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the Edge Function to invoke",
|
||||
},
|
||||
"body": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "JSON body to send to the function",
|
||||
},
|
||||
"headers": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Additional headers to send",
|
||||
},
|
||||
},
|
||||
"required": ["function_name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "invoke_function_get",
|
||||
"method_name": "invoke_function_get",
|
||||
"description": "Invoke a Supabase Edge Function with GET method. Use for read-only operations.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the Edge Function to invoke",
|
||||
},
|
||||
"params": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Query parameters to send",
|
||||
},
|
||||
},
|
||||
"required": ["function_name"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "list_edge_functions",
|
||||
"method_name": "list_edge_functions",
|
||||
"description": "List all deployed Edge Functions. Note: This reads from the functions volume in Self-Hosted.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_function_info",
|
||||
"method_name": "get_function_info",
|
||||
"description": "Get information about a specific Edge Function including its configuration.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function_name": {"type": "string", "description": "Name of the function"}
|
||||
},
|
||||
"required": ["function_name"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "test_function",
|
||||
"method_name": "test_function",
|
||||
"description": "Test an Edge Function with sample data and return the response.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the function to test",
|
||||
},
|
||||
"test_data": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Test data to send",
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"],
|
||||
"description": "HTTP method",
|
||||
"default": "POST",
|
||||
},
|
||||
},
|
||||
"required": ["function_name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "get_function_url",
|
||||
"method_name": "get_function_url",
|
||||
"description": "Get the full URL for invoking an Edge Function.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function_name": {"type": "string", "description": "Name of the function"}
|
||||
},
|
||||
"required": ["function_name"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "check_function_health",
|
||||
"method_name": "check_function_health",
|
||||
"description": "Check if an Edge Function is responding correctly.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function_name": {"type": "string", "description": "Name of the function"}
|
||||
},
|
||||
"required": ["function_name"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "invoke_function_batch",
|
||||
"method_name": "invoke_function_batch",
|
||||
"description": "Invoke an Edge Function multiple times with different payloads. Useful for batch processing.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"function_name": {"type": "string", "description": "Name of the function"},
|
||||
"payloads": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
"description": "Array of payloads to send",
|
||||
},
|
||||
},
|
||||
"required": ["function_name", "payloads"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# Functions Operations (8 tools)
|
||||
# =====================
|
||||
|
||||
async def invoke_function(
|
||||
client: SupabaseClient,
|
||||
function_name: str,
|
||||
body: dict | None = None,
|
||||
headers: dict | None = None,
|
||||
) -> str:
|
||||
"""Invoke an Edge Function with POST"""
|
||||
try:
|
||||
result = await client.invoke_function(function_name=function_name, body=body, method="POST")
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "function": function_name, "method": "POST", "response": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def invoke_function_get(
|
||||
client: SupabaseClient, function_name: str, params: dict | None = None
|
||||
) -> str:
|
||||
"""Invoke an Edge Function with GET"""
|
||||
try:
|
||||
# Build query string
|
||||
endpoint = f"/functions/v1/{function_name}"
|
||||
if params:
|
||||
query = "&".join(f"{k}={v}" for k, v in params.items())
|
||||
endpoint = f"{endpoint}?{query}"
|
||||
|
||||
result = await client.request("GET", endpoint, use_service_role=False)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"function": function_name,
|
||||
"method": "GET",
|
||||
"params": params,
|
||||
"response": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def list_edge_functions(client: SupabaseClient) -> str:
|
||||
"""List deployed Edge Functions"""
|
||||
try:
|
||||
# In self-hosted, we can try to list functions via the API
|
||||
# or check the functions volume. We'll try a health-check approach.
|
||||
functions_info = {
|
||||
"note": "In Self-Hosted Supabase, Edge Functions are deployed in volumes/functions/",
|
||||
"endpoint": f"{client.base_url}/functions/v1/",
|
||||
"available": True,
|
||||
}
|
||||
|
||||
# Try to verify the functions endpoint is available
|
||||
try:
|
||||
await client.request("GET", "/functions/v1/", use_service_role=False)
|
||||
functions_info["status"] = "Functions endpoint available"
|
||||
except Exception as e:
|
||||
functions_info["status"] = f"Functions endpoint check: {str(e)}"
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"functions_info": functions_info,
|
||||
"tip": "Deploy functions by adding them to volumes/functions/ directory",
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_function_info(client: SupabaseClient, function_name: str) -> str:
|
||||
"""Get function information"""
|
||||
try:
|
||||
info = {
|
||||
"name": function_name,
|
||||
"url": f"{client.base_url}/functions/v1/{function_name}",
|
||||
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
"auth_required": True,
|
||||
"cors_enabled": True,
|
||||
}
|
||||
|
||||
# Try to check if function exists by making a request
|
||||
try:
|
||||
await client.request(
|
||||
"OPTIONS", f"/functions/v1/{function_name}", use_service_role=False
|
||||
)
|
||||
info["status"] = "available"
|
||||
except Exception as e:
|
||||
if "404" in str(e):
|
||||
info["status"] = "not_found"
|
||||
else:
|
||||
info["status"] = "unknown"
|
||||
info["check_error"] = str(e)
|
||||
|
||||
return json.dumps({"success": True, "function_info": info}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def test_function(
|
||||
client: SupabaseClient, function_name: str, test_data: dict | None = None, method: str = "POST"
|
||||
) -> str:
|
||||
"""Test an Edge Function"""
|
||||
try:
|
||||
if method.upper() == "GET":
|
||||
result = await client.request(
|
||||
"GET", f"/functions/v1/{function_name}", params=test_data, use_service_role=False
|
||||
)
|
||||
else:
|
||||
result = await client.invoke_function(
|
||||
function_name=function_name, body=test_data, method="POST"
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"function": function_name,
|
||||
"method": method.upper(),
|
||||
"test_data": test_data,
|
||||
"response": result,
|
||||
"status": "Function responded successfully",
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"function": function_name,
|
||||
"method": method.upper(),
|
||||
"test_data": test_data,
|
||||
"error": str(e),
|
||||
"status": "Function test failed",
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
async def get_function_url(client: SupabaseClient, function_name: str) -> str:
|
||||
"""Get the full URL for a function"""
|
||||
try:
|
||||
url = f"{client.base_url}/functions/v1/{function_name}"
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"function": function_name,
|
||||
"url": url,
|
||||
"curl_example": f"curl -X POST '{url}' -H 'Authorization: Bearer <anon_key>' -H 'Content-Type: application/json' -d '{{}}'",
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def check_function_health(client: SupabaseClient, function_name: str) -> str:
|
||||
"""Check if a function is healthy"""
|
||||
try:
|
||||
healthy = False
|
||||
response_time = None
|
||||
error = None
|
||||
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
|
||||
try:
|
||||
# Try to invoke with empty body
|
||||
await client.invoke_function(function_name=function_name, body={}, method="POST")
|
||||
healthy = True
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
# 400 errors might mean the function is running but needs valid input
|
||||
if "400" in str(e) or "422" in str(e):
|
||||
healthy = True
|
||||
error = "Function running (returned validation error)"
|
||||
|
||||
response_time = round((time.time() - start) * 1000, 2)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"function": function_name,
|
||||
"healthy": healthy,
|
||||
"response_time_ms": response_time,
|
||||
"error": error,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def invoke_function_batch(
|
||||
client: SupabaseClient, function_name: str, payloads: list[dict]
|
||||
) -> str:
|
||||
"""Invoke a function with multiple payloads"""
|
||||
try:
|
||||
results = []
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for i, payload in enumerate(payloads):
|
||||
try:
|
||||
result = await client.invoke_function(
|
||||
function_name=function_name, body=payload, method="POST"
|
||||
)
|
||||
results.append({"index": i, "success": True, "response": result})
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
results.append({"index": i, "success": False, "error": str(e)})
|
||||
error_count += 1
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"function": function_name,
|
||||
"total": len(payloads),
|
||||
"succeeded": success_count,
|
||||
"failed": error_count,
|
||||
"results": results,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
484
plugins/supabase/handlers/storage.py
Normal file
484
plugins/supabase/handlers/storage.py
Normal file
@@ -0,0 +1,484 @@
|
||||
"""Storage Handler - manages Supabase Storage (buckets and files)"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.supabase.client import SupabaseClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (12 tools)"""
|
||||
return [
|
||||
{
|
||||
"name": "list_buckets",
|
||||
"method_name": "list_buckets",
|
||||
"description": "List all storage buckets. Returns bucket names, public/private status, and settings.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_bucket",
|
||||
"method_name": "get_bucket",
|
||||
"description": "Get detailed information about a specific bucket including settings and limits.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"bucket_id": {"type": "string", "description": "Bucket name/ID"}},
|
||||
"required": ["bucket_id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_bucket",
|
||||
"method_name": "create_bucket",
|
||||
"description": "Create a new storage bucket. Can be public (accessible without auth) or private.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Bucket name (lowercase, no spaces)",
|
||||
"pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"description": "Make bucket publicly accessible",
|
||||
"default": False,
|
||||
},
|
||||
"file_size_limit": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Max file size in bytes (e.g., 52428800 for 50MB)",
|
||||
},
|
||||
"allowed_mime_types": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Allowed MIME types (e.g., ['image/png', 'image/jpeg'])",
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_bucket",
|
||||
"method_name": "update_bucket",
|
||||
"description": "Update bucket settings like public access, file size limit, or allowed MIME types.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_id": {"type": "string", "description": "Bucket name/ID"},
|
||||
"public": {
|
||||
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
||||
"description": "Set public access",
|
||||
},
|
||||
"file_size_limit": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"description": "Max file size in bytes",
|
||||
},
|
||||
"allowed_mime_types": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Allowed MIME types",
|
||||
},
|
||||
},
|
||||
"required": ["bucket_id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_bucket",
|
||||
"method_name": "delete_bucket",
|
||||
"description": "Delete a storage bucket. Bucket must be empty first (use empty_bucket).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_id": {"type": "string", "description": "Bucket name/ID to delete"}
|
||||
},
|
||||
"required": ["bucket_id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "empty_bucket",
|
||||
"method_name": "empty_bucket",
|
||||
"description": "Delete all files in a bucket. Use before delete_bucket.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket_id": {"type": "string", "description": "Bucket name/ID to empty"}
|
||||
},
|
||||
"required": ["bucket_id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "list_files",
|
||||
"method_name": "list_files",
|
||||
"description": "List files in a bucket or folder path. Returns file names, sizes, and metadata.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket": {"type": "string", "description": "Bucket name"},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Folder path prefix (empty for root)",
|
||||
"default": "",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max files to return",
|
||||
"default": 100,
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "Offset for pagination",
|
||||
"default": 0,
|
||||
},
|
||||
},
|
||||
"required": ["bucket"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "upload_file",
|
||||
"method_name": "upload_file",
|
||||
"description": "Upload a file to storage. Content should be base64 encoded.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket": {"type": "string", "description": "Bucket name"},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File path including filename (e.g., 'users/123/avatar.png')",
|
||||
},
|
||||
"content_base64": {
|
||||
"type": "string",
|
||||
"description": "File content encoded in base64",
|
||||
},
|
||||
"content_type": {
|
||||
"type": "string",
|
||||
"description": "MIME type (e.g., 'image/png', 'application/pdf')",
|
||||
"default": "application/octet-stream",
|
||||
},
|
||||
"upsert": {
|
||||
"type": "boolean",
|
||||
"description": "Overwrite if file exists",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["bucket", "path", "content_base64"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "download_file",
|
||||
"method_name": "download_file",
|
||||
"description": "Download a file from storage. Returns base64 encoded content.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket": {"type": "string", "description": "Bucket name"},
|
||||
"path": {"type": "string", "description": "File path"},
|
||||
},
|
||||
"required": ["bucket", "path"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "delete_files",
|
||||
"method_name": "delete_files",
|
||||
"description": "Delete one or more files from a bucket.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket": {"type": "string", "description": "Bucket name"},
|
||||
"paths": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of file paths to delete",
|
||||
},
|
||||
},
|
||||
"required": ["bucket", "paths"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "move_file",
|
||||
"method_name": "move_file",
|
||||
"description": "Move or rename a file within the same bucket.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket": {"type": "string", "description": "Bucket name"},
|
||||
"from_path": {"type": "string", "description": "Current file path"},
|
||||
"to_path": {"type": "string", "description": "New file path"},
|
||||
},
|
||||
"required": ["bucket", "from_path", "to_path"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "get_public_url",
|
||||
"method_name": "get_public_url",
|
||||
"description": "Get the public URL for a file. Only works for files in public buckets.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bucket": {"type": "string", "description": "Bucket name (must be public)"},
|
||||
"path": {"type": "string", "description": "File path"},
|
||||
},
|
||||
"required": ["bucket", "path"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# Storage Operations (12 tools)
|
||||
# =====================
|
||||
|
||||
async def list_buckets(client: SupabaseClient) -> str:
|
||||
"""List all storage buckets"""
|
||||
try:
|
||||
result = await client.list_buckets()
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"count": len(result) if isinstance(result, list) else 0,
|
||||
"buckets": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_bucket(client: SupabaseClient, bucket_id: str) -> str:
|
||||
"""Get bucket details"""
|
||||
try:
|
||||
result = await client.get_bucket(bucket_id)
|
||||
|
||||
return json.dumps({"success": True, "bucket": result}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def create_bucket(
|
||||
client: SupabaseClient,
|
||||
name: str,
|
||||
public: bool = False,
|
||||
file_size_limit: int | None = None,
|
||||
allowed_mime_types: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Create a new bucket"""
|
||||
try:
|
||||
result = await client.create_bucket(
|
||||
name=name,
|
||||
public=public,
|
||||
file_size_limit=file_size_limit,
|
||||
allowed_mime_types=allowed_mime_types,
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Bucket '{name}' created successfully", "bucket": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def update_bucket(
|
||||
client: SupabaseClient,
|
||||
bucket_id: str,
|
||||
public: bool | None = None,
|
||||
file_size_limit: int | None = None,
|
||||
allowed_mime_types: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Update bucket settings"""
|
||||
try:
|
||||
result = await client.update_bucket(
|
||||
bucket_id=bucket_id,
|
||||
public=public,
|
||||
file_size_limit=file_size_limit,
|
||||
allowed_mime_types=allowed_mime_types,
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Bucket '{bucket_id}' updated successfully",
|
||||
"bucket": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def delete_bucket(client: SupabaseClient, bucket_id: str) -> str:
|
||||
"""Delete a bucket"""
|
||||
try:
|
||||
await client.delete_bucket(bucket_id)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Bucket '{bucket_id}' deleted successfully"},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def empty_bucket(client: SupabaseClient, bucket_id: str) -> str:
|
||||
"""Empty a bucket (delete all files)"""
|
||||
try:
|
||||
await client.empty_bucket(bucket_id)
|
||||
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Bucket '{bucket_id}' emptied successfully"},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def list_files(
|
||||
client: SupabaseClient, bucket: str, path: str = "", limit: int = 100, offset: int = 0
|
||||
) -> str:
|
||||
"""List files in bucket/path"""
|
||||
try:
|
||||
result = await client.list_files(bucket=bucket, path=path, limit=limit, offset=offset)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"bucket": bucket,
|
||||
"path": path,
|
||||
"count": len(result) if isinstance(result, list) else 0,
|
||||
"files": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def upload_file(
|
||||
client: SupabaseClient,
|
||||
bucket: str,
|
||||
path: str,
|
||||
content_base64: str,
|
||||
content_type: str = "application/octet-stream",
|
||||
upsert: bool = False,
|
||||
) -> str:
|
||||
"""Upload a file"""
|
||||
try:
|
||||
# Decode base64 content
|
||||
try:
|
||||
content = base64.b64decode(content_base64)
|
||||
except Exception:
|
||||
return json.dumps(
|
||||
{"success": False, "error": "Invalid base64 content"}, indent=2, ensure_ascii=False
|
||||
)
|
||||
|
||||
result = await client.upload_file(
|
||||
bucket=bucket, path=path, content=content, content_type=content_type, upsert=upsert
|
||||
)
|
||||
|
||||
# Build public URL if bucket is public
|
||||
public_url = await client.get_public_url(bucket, path)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"File uploaded to {bucket}/{path}",
|
||||
"size": len(content),
|
||||
"content_type": content_type,
|
||||
"public_url": public_url,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def download_file(client: SupabaseClient, bucket: str, path: str) -> str:
|
||||
"""Download a file (returns base64)"""
|
||||
try:
|
||||
result = await client.download_file(bucket, path)
|
||||
|
||||
if isinstance(result, dict) and "data" in result:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"bucket": bucket,
|
||||
"path": path,
|
||||
"content_type": result.get("content_type", "application/octet-stream"),
|
||||
"size": result.get("size", 0),
|
||||
"data_base64": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
else:
|
||||
return json.dumps(
|
||||
{"success": True, "bucket": bucket, "path": path, "result": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def delete_files(client: SupabaseClient, bucket: str, paths: list[str]) -> str:
|
||||
"""Delete files from bucket"""
|
||||
try:
|
||||
result = await client.delete_files(bucket, paths)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Deleted {len(paths)} file(s) from {bucket}",
|
||||
"deleted_paths": paths,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def move_file(client: SupabaseClient, bucket: str, from_path: str, to_path: str) -> str:
|
||||
"""Move/rename a file"""
|
||||
try:
|
||||
result = await client.move_file(bucket, from_path, to_path)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"File moved from {from_path} to {to_path}",
|
||||
"bucket": bucket,
|
||||
"from_path": from_path,
|
||||
"to_path": to_path,
|
||||
"result": result,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_public_url(client: SupabaseClient, bucket: str, path: str) -> str:
|
||||
"""Get public URL for a file"""
|
||||
try:
|
||||
url = await client.get_public_url(bucket, path)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"bucket": bucket,
|
||||
"path": path,
|
||||
"public_url": url,
|
||||
"note": "URL only works if bucket is public",
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
342
plugins/supabase/handlers/system.py
Normal file
342
plugins/supabase/handlers/system.py
Normal file
@@ -0,0 +1,342 @@
|
||||
"""System Handler - manages Supabase system operations (health, stats, info)"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.supabase.client import SupabaseClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (6 tools)"""
|
||||
return [
|
||||
{
|
||||
"name": "health_check",
|
||||
"method_name": "health_check",
|
||||
"description": "Check health of all Supabase services (PostgREST, GoTrue, Storage). Returns status for each service.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_service_status",
|
||||
"method_name": "get_service_status",
|
||||
"description": "Get detailed status of a specific Supabase service (postgrest, gotrue, storage, postgres-meta).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service": {
|
||||
"type": "string",
|
||||
"enum": ["postgrest", "gotrue", "storage", "postgres-meta"],
|
||||
"description": "Service name to check",
|
||||
}
|
||||
},
|
||||
"required": ["service"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_database_stats",
|
||||
"method_name": "get_database_stats",
|
||||
"description": "Get database statistics including table count, total size, connection info, and PostgreSQL version.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_storage_stats",
|
||||
"method_name": "get_storage_stats",
|
||||
"description": "Get storage statistics including bucket count, total files, and usage information.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_auth_stats",
|
||||
"method_name": "get_auth_stats",
|
||||
"description": "Get authentication statistics including total users, confirmed users, and provider breakdown.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_instance_info",
|
||||
"method_name": "get_instance_info",
|
||||
"description": "Get Supabase instance information including base URL and available API endpoints.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
async def health_check(client: SupabaseClient) -> str:
|
||||
"""Check health of all Supabase services"""
|
||||
try:
|
||||
result = await client.health_check()
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"healthy": result.get("healthy", False),
|
||||
"instance_url": client.base_url,
|
||||
"services": result.get("services", {}),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{"success": False, "healthy": False, "error": str(e), "instance_url": client.base_url},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
async def get_service_status(client: SupabaseClient, service: str) -> str:
|
||||
"""Get status of a specific service"""
|
||||
try:
|
||||
status = {"service": service, "status": "unknown"}
|
||||
|
||||
if service == "postgrest":
|
||||
# Check PostgREST by hitting root endpoint
|
||||
try:
|
||||
await client.request("GET", "/rest/v1/", use_service_role=True)
|
||||
status["status"] = "ok"
|
||||
status["endpoint"] = f"{client.base_url}/rest/v1/"
|
||||
except Exception as e:
|
||||
status["status"] = "error"
|
||||
status["error"] = str(e)
|
||||
|
||||
elif service == "gotrue":
|
||||
# Check GoTrue health endpoint
|
||||
try:
|
||||
result = await client.request("GET", "/auth/v1/health", use_service_role=False)
|
||||
status["status"] = "ok"
|
||||
status["endpoint"] = f"{client.base_url}/auth/v1/"
|
||||
status["details"] = result
|
||||
except Exception as e:
|
||||
status["status"] = "error"
|
||||
status["error"] = str(e)
|
||||
|
||||
elif service == "storage":
|
||||
# Check Storage by listing buckets
|
||||
try:
|
||||
buckets = await client.list_buckets()
|
||||
status["status"] = "ok"
|
||||
status["endpoint"] = f"{client.base_url}/storage/v1/"
|
||||
status["bucket_count"] = len(buckets) if isinstance(buckets, list) else 0
|
||||
except Exception as e:
|
||||
status["status"] = "error"
|
||||
status["error"] = str(e)
|
||||
|
||||
elif service == "postgres-meta":
|
||||
# Check postgres-meta by listing schemas
|
||||
try:
|
||||
schemas = await client.list_schemas()
|
||||
status["status"] = "ok"
|
||||
status["endpoint"] = f"{client.base_url}/pg/"
|
||||
status["schema_count"] = len(schemas) if isinstance(schemas, list) else 0
|
||||
except Exception as e:
|
||||
status["status"] = "error"
|
||||
status["error"] = str(e)
|
||||
|
||||
else:
|
||||
status["status"] = "unknown"
|
||||
status["error"] = f"Unknown service: {service}"
|
||||
|
||||
return json.dumps(
|
||||
{"success": status["status"] == "ok", **status}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_database_stats(client: SupabaseClient) -> str:
|
||||
"""Get database statistics"""
|
||||
try:
|
||||
stats = {
|
||||
"tables": 0,
|
||||
"schemas": 0,
|
||||
"extensions": 0,
|
||||
"size": "unknown",
|
||||
"version": "unknown",
|
||||
}
|
||||
|
||||
# Get table count
|
||||
try:
|
||||
tables = await client.list_tables()
|
||||
stats["tables"] = len(tables) if isinstance(tables, list) else 0
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get schema count
|
||||
try:
|
||||
schemas = await client.list_schemas()
|
||||
stats["schemas"] = len(schemas) if isinstance(schemas, list) else 0
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get extension count
|
||||
try:
|
||||
extensions = await client.list_extensions()
|
||||
stats["extensions"] = len(extensions) if isinstance(extensions, list) else 0
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get database size and version
|
||||
try:
|
||||
size_result = await client.execute_sql(
|
||||
"SELECT pg_size_pretty(pg_database_size(current_database())) as size, version() as version"
|
||||
)
|
||||
if isinstance(size_result, list) and len(size_result) > 0:
|
||||
stats["size"] = size_result[0].get("size", "unknown")
|
||||
stats["version"] = size_result[0].get("version", "unknown")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get connection info
|
||||
try:
|
||||
conn_result = await client.execute_sql(
|
||||
"SELECT count(*) as connections FROM pg_stat_activity WHERE state = 'active'"
|
||||
)
|
||||
if isinstance(conn_result, list) and len(conn_result) > 0:
|
||||
stats["active_connections"] = conn_result[0].get("connections", 0)
|
||||
except:
|
||||
pass
|
||||
|
||||
return json.dumps({"success": True, "database_stats": stats}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_storage_stats(client: SupabaseClient) -> str:
|
||||
"""Get storage statistics"""
|
||||
try:
|
||||
stats = {
|
||||
"buckets": 0,
|
||||
"public_buckets": 0,
|
||||
"private_buckets": 0,
|
||||
"total_files": 0,
|
||||
"bucket_details": [],
|
||||
}
|
||||
|
||||
try:
|
||||
buckets = await client.list_buckets()
|
||||
|
||||
if isinstance(buckets, list):
|
||||
stats["buckets"] = len(buckets)
|
||||
stats["public_buckets"] = sum(1 for b in buckets if b.get("public", False))
|
||||
stats["private_buckets"] = stats["buckets"] - stats["public_buckets"]
|
||||
|
||||
# Get file counts per bucket
|
||||
for bucket in buckets:
|
||||
bucket_id = bucket.get("id") or bucket.get("name")
|
||||
bucket_info = {
|
||||
"name": bucket_id,
|
||||
"public": bucket.get("public", False),
|
||||
"file_count": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
files = await client.list_files(bucket_id, limit=1000)
|
||||
if isinstance(files, list):
|
||||
bucket_info["file_count"] = len(files)
|
||||
stats["total_files"] += len(files)
|
||||
except:
|
||||
pass
|
||||
|
||||
stats["bucket_details"].append(bucket_info)
|
||||
except Exception as e:
|
||||
stats["error"] = str(e)
|
||||
|
||||
return json.dumps({"success": True, "storage_stats": stats}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_auth_stats(client: SupabaseClient) -> str:
|
||||
"""Get authentication statistics"""
|
||||
try:
|
||||
stats = {
|
||||
"total_users": 0,
|
||||
"confirmed_users": 0,
|
||||
"unconfirmed_users": 0,
|
||||
"users_with_mfa": 0,
|
||||
"providers": {},
|
||||
}
|
||||
|
||||
try:
|
||||
# Get users (first page)
|
||||
users_response = await client.list_users(page=1, per_page=1000)
|
||||
|
||||
if isinstance(users_response, dict):
|
||||
users = users_response.get("users", [])
|
||||
else:
|
||||
users = users_response if isinstance(users_response, list) else []
|
||||
|
||||
stats["total_users"] = len(users)
|
||||
|
||||
for user in users:
|
||||
# Count confirmed/unconfirmed
|
||||
if user.get("email_confirmed_at") or user.get("confirmed_at"):
|
||||
stats["confirmed_users"] += 1
|
||||
else:
|
||||
stats["unconfirmed_users"] += 1
|
||||
|
||||
# Count MFA enabled
|
||||
factors = user.get("factors", [])
|
||||
if factors and len(factors) > 0:
|
||||
stats["users_with_mfa"] += 1
|
||||
|
||||
# Count by provider
|
||||
identities = user.get("identities", [])
|
||||
for identity in identities:
|
||||
provider = identity.get("provider", "email")
|
||||
stats["providers"][provider] = stats["providers"].get(provider, 0) + 1
|
||||
|
||||
# If no identities, count as email
|
||||
if not stats["providers"]:
|
||||
stats["providers"]["email"] = stats["total_users"]
|
||||
|
||||
except Exception as e:
|
||||
stats["error"] = str(e)
|
||||
|
||||
return json.dumps({"success": True, "auth_stats": stats}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
|
||||
async def get_instance_info(client: SupabaseClient) -> str:
|
||||
"""Get Supabase instance information"""
|
||||
try:
|
||||
info = {
|
||||
"base_url": client.base_url,
|
||||
"api_endpoints": {
|
||||
"rest": f"{client.base_url}/rest/v1",
|
||||
"auth": f"{client.base_url}/auth/v1",
|
||||
"storage": f"{client.base_url}/storage/v1",
|
||||
"functions": f"{client.base_url}/functions/v1",
|
||||
"realtime": f"{client.base_url}/realtime/v1",
|
||||
"postgres_meta": f"{client.base_url}/pg",
|
||||
},
|
||||
"deployment_type": "self-hosted",
|
||||
"services_available": [],
|
||||
}
|
||||
|
||||
# Check which services are available
|
||||
services_to_check = ["postgrest", "gotrue", "storage", "postgres-meta"]
|
||||
|
||||
for service in services_to_check:
|
||||
try:
|
||||
if service == "postgrest":
|
||||
await client.request("GET", "/rest/v1/", use_service_role=True)
|
||||
elif service == "gotrue":
|
||||
await client.request("GET", "/auth/v1/health", use_service_role=False)
|
||||
elif service == "storage":
|
||||
await client.list_buckets()
|
||||
elif service == "postgres-meta":
|
||||
await client.list_schemas()
|
||||
|
||||
info["services_available"].append(service)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to get PostgreSQL version
|
||||
try:
|
||||
version_result = await client.execute_sql("SELECT version()")
|
||||
if isinstance(version_result, list) and len(version_result) > 0:
|
||||
info["postgres_version"] = version_result[0].get("version", "unknown")
|
||||
except:
|
||||
pass
|
||||
|
||||
return json.dumps({"success": True, "instance_info": info}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
||||
Reference in New Issue
Block a user