173 lines
5.2 KiB
Python
173 lines
5.2 KiB
Python
"""
|
|
=============================================================================
|
|
DATABASE SERVICE (SUPABASE)
|
|
=============================================================================
|
|
|
|
This module provides Supabase database integration.
|
|
|
|
Supabase Features Used:
|
|
- Database: PostgreSQL with auto-generated REST API
|
|
- Auth: User authentication (handled in frontend mostly)
|
|
- Real-time: Websocket subscriptions (used in frontend)
|
|
- Storage: File uploads (not shown here, but easy to add)
|
|
|
|
Why Supabase?
|
|
- Free tier is generous (great for hackathons!)
|
|
- Built-in auth that works with frontend
|
|
- Real-time subscriptions out of the box
|
|
- Full PostgreSQL (can run raw SQL)
|
|
- Great documentation
|
|
"""
|
|
|
|
from supabase import create_client, Client
|
|
from config import settings
|
|
from functools import lru_cache
|
|
|
|
|
|
@lru_cache
|
|
def get_supabase_client() -> Client:
|
|
"""
|
|
Get a cached Supabase client instance.
|
|
|
|
Using lru_cache ensures we reuse the same client,
|
|
avoiding connection overhead.
|
|
|
|
Note: We use the SERVICE KEY here (not anon key) because
|
|
the backend needs admin access to the database.
|
|
The frontend uses the anon key with Row Level Security.
|
|
"""
|
|
if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_KEY:
|
|
raise ValueError(
|
|
"Supabase not configured. "
|
|
"Set SUPABASE_URL and SUPABASE_SERVICE_KEY in .env"
|
|
)
|
|
|
|
return create_client(
|
|
settings.SUPABASE_URL,
|
|
settings.SUPABASE_SERVICE_KEY,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# DATABASE HELPERS
|
|
# =============================================================================
|
|
|
|
async def get_user_by_id(user_id: str) -> dict | None:
|
|
"""
|
|
Get a user by their ID.
|
|
|
|
Example:
|
|
user = await get_user_by_id("123-456-789")
|
|
print(user["email"])
|
|
"""
|
|
supabase = get_supabase_client()
|
|
|
|
response = supabase.auth.admin.get_user_by_id(user_id)
|
|
|
|
return response.user if response else None
|
|
|
|
|
|
async def verify_jwt_token(token: str) -> dict | None:
|
|
"""
|
|
Verify a JWT token and get the user.
|
|
|
|
Use this to authenticate API requests that include
|
|
the Supabase access token.
|
|
|
|
Example:
|
|
@app.get("/protected")
|
|
async def protected_route(authorization: str = Header()):
|
|
token = authorization.replace("Bearer ", "")
|
|
user = await verify_jwt_token(token)
|
|
if not user:
|
|
raise HTTPException(status_code=401)
|
|
"""
|
|
supabase = get_supabase_client()
|
|
|
|
try:
|
|
response = supabase.auth.get_user(token)
|
|
return response.user if response else None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
# =============================================================================
|
|
# GENERIC CRUD HELPERS
|
|
# =============================================================================
|
|
|
|
class DatabaseTable:
|
|
"""
|
|
Helper class for common database operations.
|
|
|
|
Example:
|
|
projects = DatabaseTable("projects")
|
|
all_projects = await projects.get_all()
|
|
project = await projects.get_by_id("123")
|
|
new_project = await projects.create({"name": "My Project"})
|
|
"""
|
|
|
|
def __init__(self, table_name: str):
|
|
self.table_name = table_name
|
|
self.client = get_supabase_client()
|
|
|
|
async def get_all(self, filters: dict = None) -> list[dict]:
|
|
"""Get all records, optionally filtered."""
|
|
query = self.client.table(self.table_name).select("*")
|
|
|
|
if filters:
|
|
for key, value in filters.items():
|
|
query = query.eq(key, value)
|
|
|
|
response = query.execute()
|
|
return response.data
|
|
|
|
async def get_by_id(self, record_id: str) -> dict | None:
|
|
"""Get a single record by ID."""
|
|
response = (
|
|
self.client.table(self.table_name)
|
|
.select("*")
|
|
.eq("id", record_id)
|
|
.single()
|
|
.execute()
|
|
)
|
|
return response.data
|
|
|
|
async def create(self, data: dict) -> dict:
|
|
"""Create a new record."""
|
|
response = (
|
|
self.client.table(self.table_name)
|
|
.insert(data)
|
|
.execute()
|
|
)
|
|
return response.data[0]
|
|
|
|
async def update(self, record_id: str, data: dict) -> dict:
|
|
"""Update an existing record."""
|
|
response = (
|
|
self.client.table(self.table_name)
|
|
.update(data)
|
|
.eq("id", record_id)
|
|
.execute()
|
|
)
|
|
return response.data[0] if response.data else None
|
|
|
|
async def delete(self, record_id: str) -> bool:
|
|
"""Delete a record."""
|
|
self.client.table(self.table_name).delete().eq("id", record_id).execute()
|
|
return True
|
|
|
|
|
|
# =============================================================================
|
|
# EXAMPLE: Projects Table Helper
|
|
# =============================================================================
|
|
|
|
# Create a helper instance for the projects table
|
|
projects_db = DatabaseTable("projects")
|
|
|
|
# You can now use:
|
|
# await projects_db.get_all()
|
|
# await projects_db.get_by_id("123")
|
|
# await projects_db.create({"name": "Test", "user_id": "abc"})
|
|
# await projects_db.update("123", {"name": "Updated"})
|
|
# await projects_db.delete("123")
|