Files
hack-nation/backend/services/database.py
2026-02-03 21:58:25 +01:00

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")