""" Databaskonfiguration och session-hantering för LandveX Admin Backend. Inkluderar tenant schema-isolering via PostgreSQL search_path. """ from contextlib import asynccontextmanager from typing import AsyncGenerator, Optional from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.orm import declarative_base from sqlalchemy import text from app.config import get_settings settings = get_settings() # SQLite stödjer inte pool_size/max_overflow if settings.DATABASE_URL.startswith("sqlite"): engine = create_async_engine( settings.DATABASE_URL, echo=settings.DEBUG, future=True, ) else: engine = create_async_engine( settings.DATABASE_URL, pool_size=settings.DATABASE_POOL_SIZE, max_overflow=settings.DATABASE_MAX_OVERFLOW, pool_pre_ping=True, echo=settings.DEBUG, future=True, ) AsyncSessionLocal = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, autocommit=False, ) Base = declarative_base() async def get_db() -> AsyncSession: """Dependency för FastAPI-endpoints.""" async with AsyncSessionLocal() as session: try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() @asynccontextmanager async def get_db_context() -> AsyncGenerator[AsyncSession, None]: """Context manager för databassessioner.""" async with AsyncSessionLocal() as session: try: yield session await session.commit() except Exception: await session.rollback() raise finally: await session.close() class TenantContext: """Context manager för tenant schema-isolering. Använder PostgreSQL search_path och application context för att säkerställa dataisolering mellan tenants. """ def __init__(self, session: AsyncSession, tenant_schema: Optional[str] = None): self.session = session self.tenant_schema = tenant_schema or settings.DEFAULT_TENANT_SCHEMA async def __aenter__(self): """Sätt tenant context på databasanslutningen.""" await self.session.execute( text(f'SET search_path TO "{self.tenant_schema}", public') ) await self.session.execute( text("SELECT set_config('app.current_tenant', :tenant, true)"), {"tenant": self.tenant_schema} ) return self async def __aexit__(self, exc_type, exc_val, exc_tb): """Återställ tenant context.""" await self.session.execute(text('SET search_path TO public')) await self.session.execute( text("SELECT set_config('app.current_tenant', '', true)") ) return False async def set_tenant_schema(session: AsyncSession, tenant_schema: str) -> None: """Sätt tenant schema för aktuell session.""" await session.execute( text(f'SET search_path TO "{tenant_schema}", public') ) await session.execute( text("SELECT set_config('app.current_tenant', :tenant, true)"), {"tenant": tenant_schema} ) async def reset_tenant_schema(session: AsyncSession) -> None: """Återställ schema till default.""" await session.execute(text('SET search_path TO public')) await session.execute( text("SELECT set_config('app.current_tenant', '', true)") ) async def create_tenant_schema(schema_name: str) -> None: """Skapa ett nytt schema för en tenant.""" async with engine.begin() as conn: await conn.execute(text(f'CREATE SCHEMA IF NOT EXISTS "{schema_name}"')) await conn.execute(text(f'GRANT USAGE ON SCHEMA "{schema_name}" TO PUBLIC')) async def drop_tenant_schema(schema_name: str) -> None: """Ta bort ett tenant schema.""" async with engine.begin() as conn: await conn.execute(text(f'DROP SCHEMA IF EXISTS "{schema_name}" CASCADE')) async def schema_exists(schema_name: str) -> bool: """Kolla om ett schema finns.""" async with engine.connect() as conn: result = await conn.execute( text( "SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = :schema)" ), {"schema": schema_name} ) return result.scalar()