fix: resolve frontend/backend integration bugs
- Rename skincare route prefix /skin-snapshots → /skincare to match API client - Add redirect_slashes=False to FastAPI app; change collection routes from "/" to "" to eliminate 307 redirects on POST/GET without trailing slash - Fix redirect() inside try/catch in products/new and routines/new server actions (SvelteKit redirect() throws and was being caught as a 500 error) - Eagerly load inventory and steps relationships via explicit SELECT + model_dump(mode="json"), working around SQLModel 0.0.37 not serializing Relationship fields in response_model - Add field_validator for product_effect_profile to coerce DB-returned dict → ProductEffectProfile, eliminating Pydantic serializer warning - Update all tests to use routes without trailing slash Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8d4f9d1fc6
commit
9bf94a979c
11 changed files with 85 additions and 68 deletions
|
|
@ -188,7 +188,7 @@ def get_or_404(session: Session, model, record_id) -> object:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/", response_model=list[Product])
|
||||
@router.get("", response_model=list[Product])
|
||||
def list_products(
|
||||
category: Optional[ProductCategory] = None,
|
||||
brand: Optional[str] = None,
|
||||
|
|
@ -224,7 +224,7 @@ def list_products(
|
|||
return products
|
||||
|
||||
|
||||
@router.post("/", response_model=Product, status_code=201)
|
||||
@router.post("", response_model=Product, status_code=201)
|
||||
def create_product(data: ProductCreate, session: Session = Depends(get_session)):
|
||||
product = Product(
|
||||
id=uuid4(),
|
||||
|
|
@ -236,9 +236,13 @@ def create_product(data: ProductCreate, session: Session = Depends(get_session))
|
|||
return product
|
||||
|
||||
|
||||
@router.get("/{product_id}", response_model=Product)
|
||||
@router.get("/{product_id}")
|
||||
def get_product(product_id: UUID, session: Session = Depends(get_session)):
|
||||
return get_or_404(session, Product, product_id)
|
||||
product = get_or_404(session, Product, product_id)
|
||||
inventory = session.exec(select(ProductInventory).where(ProductInventory.product_id == product_id)).all()
|
||||
data = product.model_dump(mode="json")
|
||||
data["inventory"] = [item.model_dump(mode="json") for item in inventory]
|
||||
return data
|
||||
|
||||
|
||||
@router.patch("/{product_id}", response_model=Product)
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ def get_or_404(session: Session, model, record_id) -> object:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/", response_model=list[Routine])
|
||||
@router.get("", response_model=list[Routine])
|
||||
def list_routines(
|
||||
from_date: Optional[date] = None,
|
||||
to_date: Optional[date] = None,
|
||||
|
|
@ -93,7 +93,7 @@ def list_routines(
|
|||
return session.exec(stmt).all()
|
||||
|
||||
|
||||
@router.post("/", response_model=Routine, status_code=201)
|
||||
@router.post("", response_model=Routine, status_code=201)
|
||||
def create_routine(data: RoutineCreate, session: Session = Depends(get_session)):
|
||||
routine = Routine(id=uuid4(), **data.model_dump())
|
||||
session.add(routine)
|
||||
|
|
@ -108,9 +108,13 @@ def list_grooming_schedule(session: Session = Depends(get_session)):
|
|||
return session.exec(select(GroomingSchedule)).all()
|
||||
|
||||
|
||||
@router.get("/{routine_id}", response_model=Routine)
|
||||
@router.get("/{routine_id}")
|
||||
def get_routine(routine_id: UUID, session: Session = Depends(get_session)):
|
||||
return get_or_404(session, Routine, routine_id)
|
||||
routine = get_or_404(session, Routine, routine_id)
|
||||
steps = session.exec(select(RoutineStep).where(RoutineStep.routine_id == routine_id)).all()
|
||||
data = routine.model_dump(mode="json")
|
||||
data["steps"] = [step.model_dump(mode="json") for step in steps]
|
||||
return data
|
||||
|
||||
|
||||
@router.patch("/{routine_id}", response_model=Routine)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ def get_or_404(session: Session, model, record_id) -> object:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/", response_model=list[SkinConditionSnapshot])
|
||||
@router.get("", response_model=list[SkinConditionSnapshot])
|
||||
def list_snapshots(
|
||||
from_date: Optional[date] = None,
|
||||
to_date: Optional[date] = None,
|
||||
|
|
@ -95,7 +95,7 @@ def list_snapshots(
|
|||
return session.exec(stmt).all()
|
||||
|
||||
|
||||
@router.post("/", response_model=SkinConditionSnapshot, status_code=201)
|
||||
@router.post("", response_model=SkinConditionSnapshot, status_code=201)
|
||||
def create_snapshot(
|
||||
data: SnapshotCreate, session: Session = Depends(get_session)
|
||||
):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from datetime import date, datetime
|
|||
from typing import ClassVar, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import model_validator
|
||||
from pydantic import field_validator, model_validator
|
||||
from sqlalchemy import JSON, Column, DateTime
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
|
|
@ -188,6 +188,13 @@ class Product(SQLModel, table=True):
|
|||
|
||||
inventory: list["ProductInventory"] = Relationship(back_populates="product")
|
||||
|
||||
@field_validator("product_effect_profile", mode="before")
|
||||
@classmethod
|
||||
def coerce_effect_profile(cls, v: object) -> object:
|
||||
if isinstance(v, dict):
|
||||
return ProductEffectProfile(**v)
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_business_rules(self) -> "Product":
|
||||
if (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue