refactor: split table models into Base/Table/Public for proper FastAPI serialization

Add ProductBase, ProductPublic, ProductWithInventory and
SkinConditionSnapshotBase, SkinConditionSnapshotPublic. Table models now inherit
from their Base counterpart and override JSON fields with sa_column. All
field_serializer hacks removed; FastAPI response models use the non-table Public
classes so Pydantic coerces raw DB dicts → typed models cleanly. ProductCreate
and SnapshotCreate now simply inherit their respective Base classes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Piotr Oleszczyk 2026-02-27 15:37:46 +01:00
parent 479be25112
commit c09acc7c81
15 changed files with 225 additions and 198 deletions

View file

@ -2,12 +2,16 @@ from datetime import date
from typing import Optional
from uuid import UUID, uuid4
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends
from sqlmodel import Session, SQLModel, select
from db import get_session
from innercontext.api.utils import get_or_404
from innercontext.models import SkinConditionSnapshot
from innercontext.models import (
SkinConditionSnapshot,
SkinConditionSnapshotBase,
SkinConditionSnapshotPublic,
)
from innercontext.models.enums import (
BarrierState,
OverallSkinState,
@ -24,23 +28,8 @@ router = APIRouter()
# ---------------------------------------------------------------------------
class SnapshotCreate(SQLModel):
snapshot_date: date
overall_state: Optional[OverallSkinState] = None
trend: Optional[SkinTrend] = None
skin_type: Optional[SkinType] = None
hydration_level: Optional[int] = None
sebum_tzone: Optional[int] = None
sebum_cheeks: Optional[int] = None
sensitivity_level: Optional[int] = None
barrier_state: Optional[BarrierState] = None
active_concerns: list[SkinConcern] = []
risks: list[str] = []
priorities: list[str] = []
notes: Optional[str] = None
class SnapshotCreate(SkinConditionSnapshotBase):
pass
class SnapshotUpdate(SQLModel):
@ -62,17 +51,12 @@ class SnapshotUpdate(SQLModel):
notes: Optional[str] = None
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@router.get("", response_model=list[SkinConditionSnapshot])
@router.get("", response_model=list[SkinConditionSnapshotPublic])
def list_snapshots(
from_date: Optional[date] = None,
to_date: Optional[date] = None,
@ -89,10 +73,8 @@ def list_snapshots(
return session.exec(stmt).all()
@router.post("", response_model=SkinConditionSnapshot, status_code=201)
def create_snapshot(
data: SnapshotCreate, session: Session = Depends(get_session)
):
@router.post("", response_model=SkinConditionSnapshotPublic, status_code=201)
def create_snapshot(data: SnapshotCreate, session: Session = Depends(get_session)):
snapshot = SkinConditionSnapshot(id=uuid4(), **data.model_dump())
session.add(snapshot)
session.commit()
@ -100,12 +82,12 @@ def create_snapshot(
return snapshot
@router.get("/{snapshot_id}", response_model=SkinConditionSnapshot)
@router.get("/{snapshot_id}", response_model=SkinConditionSnapshotPublic)
def get_snapshot(snapshot_id: UUID, session: Session = Depends(get_session)):
return get_or_404(session, SkinConditionSnapshot, snapshot_id)
@router.patch("/{snapshot_id}", response_model=SkinConditionSnapshot)
@router.patch("/{snapshot_id}", response_model=SkinConditionSnapshotPublic)
def update_snapshot(
snapshot_id: UUID,
data: SnapshotUpdate,