mirror of
https://github.com/taylanbakircioglu/flowfish.git
synced 2026-09-12 05:48:55 +00:00
d7ca50b387
Multi-cluster dependency mapping, real-time network monitoring, impact analysis, and CI/CD integration capabilities. Made-with: Cursor
183 lines
6.5 KiB
Python
183 lines
6.5 KiB
Python
"""
|
|
PostgreSQL database connection and session management
|
|
"""
|
|
|
|
import asyncpg
|
|
from sqlalchemy import create_engine, MetaData, text
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
from sqlalchemy.pool import NullPool
|
|
import structlog
|
|
|
|
from config import settings
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
# Convert postgres:// to postgresql:// for async
|
|
async_database_url = settings.DATABASE_URL.replace('postgresql://', 'postgresql+asyncpg://')
|
|
|
|
# Async SQLAlchemy engine (SQLAlchemy 2.0)
|
|
async_engine = create_async_engine(
|
|
async_database_url,
|
|
pool_size=settings.DATABASE_POOL_SIZE,
|
|
max_overflow=20,
|
|
pool_timeout=settings.DATABASE_POOL_TIMEOUT,
|
|
pool_recycle=3600,
|
|
echo=settings.DEBUG
|
|
)
|
|
|
|
# Sync engine for migrations
|
|
sync_engine = create_engine(
|
|
settings.DATABASE_URL,
|
|
pool_size=5,
|
|
echo=settings.DEBUG
|
|
)
|
|
|
|
# Session for sync operations (migrations, etc.)
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=sync_engine)
|
|
|
|
# Async session factory
|
|
async_session = AsyncSession(async_engine)
|
|
|
|
# Base class for models
|
|
Base = declarative_base()
|
|
metadata = MetaData()
|
|
|
|
# Simple database operations
|
|
class DatabaseService:
|
|
@staticmethod
|
|
async def execute(query: str, values: dict = None):
|
|
"""Execute raw SQL query"""
|
|
async with AsyncSession(async_engine) as session:
|
|
result = await session.execute(text(query), values or {})
|
|
await session.commit()
|
|
return result
|
|
|
|
@staticmethod
|
|
async def fetch_one(query: str, values: dict = None):
|
|
"""Fetch single row - commits if query modifies data (INSERT/UPDATE/DELETE with RETURNING)"""
|
|
async with AsyncSession(async_engine) as session:
|
|
result = await session.execute(text(query), values or {})
|
|
row = result.fetchone()
|
|
# Commit for write operations (INSERT/UPDATE/DELETE with RETURNING)
|
|
await session.commit()
|
|
if row:
|
|
return dict(row._mapping)
|
|
return None
|
|
|
|
@staticmethod
|
|
async def fetch_all(query: str, values: dict = None):
|
|
"""Fetch all rows - commits if query modifies data"""
|
|
async with AsyncSession(async_engine) as session:
|
|
result = await session.execute(text(query), values or {})
|
|
rows = result.fetchall()
|
|
# Commit for write operations
|
|
await session.commit()
|
|
return [dict(row._mapping) for row in rows]
|
|
|
|
# Create database instance
|
|
database = DatabaseService()
|
|
|
|
def get_db_session():
|
|
"""Get sync database session for migrations/admin tasks"""
|
|
db = SessionLocal()
|
|
try:
|
|
return db
|
|
finally:
|
|
db.close()
|
|
|
|
# Connection test function
|
|
async def test_connection():
|
|
"""Test PostgreSQL connection"""
|
|
try:
|
|
result = await database.fetch_one("SELECT 1 as test")
|
|
logger.info("PostgreSQL connection test successful")
|
|
|
|
# Run auto-migrations for new tables
|
|
await run_auto_migrations()
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error("PostgreSQL connection test failed", error=str(e))
|
|
return False
|
|
|
|
|
|
async def run_auto_migrations():
|
|
"""Run auto-migrations to ensure required tables exist"""
|
|
try:
|
|
# Check and create api_keys table if not exists
|
|
await create_api_keys_table_if_not_exists()
|
|
logger.info("Auto-migrations completed successfully")
|
|
except Exception as e:
|
|
import traceback
|
|
logger.error("Auto-migration FAILED", error=str(e), traceback=traceback.format_exc())
|
|
# Don't raise - let the app continue but log the error prominently
|
|
|
|
|
|
async def create_api_keys_table_if_not_exists():
|
|
"""Create api_keys table if it doesn't exist"""
|
|
try:
|
|
# Check if table exists
|
|
check_query = """
|
|
SELECT EXISTS (
|
|
SELECT FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_name = 'api_keys'
|
|
);
|
|
"""
|
|
result = await database.fetch_one(check_query)
|
|
|
|
if result and result.get('exists'):
|
|
logger.debug("api_keys table already exists, checking for schema updates")
|
|
# Fix key_prefix column size if needed (was VARCHAR(10), should be VARCHAR(12))
|
|
try:
|
|
await database.execute(
|
|
"ALTER TABLE api_keys ALTER COLUMN key_prefix TYPE VARCHAR(12)"
|
|
)
|
|
logger.info("Updated key_prefix column to VARCHAR(12)")
|
|
except Exception:
|
|
pass # Column might already be correct size
|
|
return
|
|
|
|
# Create the table (asyncpg requires separate statements)
|
|
create_table_query = """
|
|
CREATE TABLE IF NOT EXISTS api_keys (
|
|
id SERIAL PRIMARY KEY,
|
|
key_id VARCHAR(50) UNIQUE NOT NULL,
|
|
key_hash VARCHAR(255) NOT NULL,
|
|
key_prefix VARCHAR(12) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
scopes TEXT[] DEFAULT ARRAY['blast-radius'],
|
|
cluster_ids INTEGER[],
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
expires_at TIMESTAMP,
|
|
last_used_at TIMESTAMP,
|
|
last_used_ip VARCHAR(45),
|
|
usage_count INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
revoked_at TIMESTAMP,
|
|
revoked_by INTEGER REFERENCES users(id),
|
|
revoke_reason TEXT,
|
|
metadata JSONB DEFAULT '{}'::jsonb
|
|
)
|
|
"""
|
|
await database.execute(create_table_query)
|
|
|
|
# Create indexes separately (asyncpg cannot execute multiple statements at once)
|
|
index_queries = [
|
|
"CREATE INDEX IF NOT EXISTS idx_api_keys_key_id ON api_keys(key_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash)",
|
|
"CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_api_keys_is_active ON api_keys(is_active)",
|
|
]
|
|
for index_query in index_queries:
|
|
await database.execute(index_query)
|
|
logger.info("api_keys table created successfully")
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
logger.error("Failed to create api_keys table", error=str(e), traceback=traceback.format_exc())
|