from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import text from .config import get_settings from .database import Base, SessionLocal, engine from .routers import ( api_tokens, auth, branding, categories, dashboard, groups, locations, maintenance, products, settings as settings_router, stock, transfer, units, users, views, ) from .seed import ensure_builtin_categories, ensure_builtin_units, ensure_first_admin from .services.group_codes import backfill as backfill_group_codes settings = get_settings() def _ensure_schema() -> None: """Schonende Migration: fehlende Spalten auf bestehenden Tabellen nachziehen. create_all() legt nur fehlende Tabellen an, ändert aber keine bestehenden. Auf Postgres holen wir neue nullable-Spalten per ADD COLUMN IF NOT EXISTS nach, damit vorhandene Installationen ihre Daten behalten. Auf SQLite (Tests) sind die Spalten bereits durch create_all vorhanden. """ if engine.dialect.name != "postgresql": return stmts = [ "ALTER TABLE products ADD COLUMN IF NOT EXISTS display_unit_id INTEGER " "REFERENCES units(id) ON DELETE SET NULL", "ALTER TABLE groups ADD COLUMN IF NOT EXISTS min_stock_unit_id INTEGER " "REFERENCES units(id) ON DELETE SET NULL", "ALTER TABLE products ADD COLUMN IF NOT EXISTS min_stock_unit_id INTEGER " "REFERENCES units(id) ON DELETE SET NULL", "ALTER TABLE products ADD COLUMN IF NOT EXISTS min_stock_in_packages BOOLEAN " "NOT NULL DEFAULT FALSE", "ALTER TABLE products ADD COLUMN IF NOT EXISTS package_label VARCHAR(32)", "ALTER TABLE products ADD COLUMN IF NOT EXISTS date_precision VARCHAR(8) " "NOT NULL DEFAULT 'day'", "ALTER TABLE lots ADD COLUMN IF NOT EXISTS best_before_precision VARCHAR(8) " "NOT NULL DEFAULT 'day'", "ALTER TABLE products ADD COLUMN IF NOT EXISTS category_id INTEGER " "REFERENCES categories(id) ON DELETE SET NULL", ] with engine.begin() as conn: for stmt in stmts: conn.execute(text(stmt)) @asynccontextmanager async def lifespan(app: FastAPI): # Tabellen anlegen (MVP: create_all statt Alembic-Migrationen). Base.metadata.create_all(bind=engine) _ensure_schema() db = SessionLocal() try: ensure_builtin_units(db) ensure_builtin_categories(db) ensure_first_admin(db) # Codes bestehender Gruppen-Zuordnungen nachziehen. backfill_group_codes(db) finally: db.close() yield app = FastAPI(title="Vorrania – Lebensmittel-Lagerverwaltung", version="1.0.0", lifespan=lifespan) origins = ["*"] if settings.cors_origins.strip() == "*" else [ o.strip() for o in settings.cors_origins.split(",") if o.strip() ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/health", tags=["meta"]) def health() -> dict: return {"status": "ok"} app.include_router(auth.router) app.include_router(users.router) app.include_router(products.router) app.include_router(stock.router) app.include_router(locations.router) app.include_router(groups.router) app.include_router(units.router) app.include_router(views.router) app.include_router(transfer.router) app.include_router(api_tokens.router) app.include_router(settings_router.router) app.include_router(branding.router) app.include_router(categories.router) app.include_router(maintenance.router) app.include_router(dashboard.router)