Initial project scaffold: FastAPI backend + Svelte 5 frontend
Backend (uv): FastAPI app with routers (foods, log, targets, OFF proxy), SQLAlchemy models, Pydantic schemas, migration runner + 0001 initial schema, example pytest suite (7 tests). Frontend (npm): Vite 7 + Svelte 5 runes, lib/ (api, stores, scanner, format) per spec §7, placeholder components, example vitest suite (4 tests). SPEC.md §1: registered uvicorn and Vitest (rule §8.3.3).
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""SQLAlchemy ORM models (SPEC.md §2). Separate from Pydantic schemas (spec §1)."""
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import CheckConstraint, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from database import Base
|
||||
|
||||
|
||||
class Food(Base):
|
||||
__tablename__ = "foods"
|
||||
__table_args__ = (
|
||||
# Meal nutrition is derived from components, never stored (spec §2.1)
|
||||
CheckConstraint("is_meal = 1 OR calories_per_unit IS NOT NULL"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str]
|
||||
brand: Mapped[str | None]
|
||||
barcode: Mapped[str | None] = mapped_column(unique=True)
|
||||
source: Mapped[str] # "openfoodfacts" | "manual" | "meal"
|
||||
is_meal: Mapped[bool] = mapped_column(default=False)
|
||||
unit_type: Mapped[str] = mapped_column(default="weight") # "weight" | "count"
|
||||
calories_per_unit: Mapped[float | None]
|
||||
protein_per_unit: Mapped[float | None]
|
||||
carbs_per_unit: Mapped[float | None]
|
||||
fat_per_unit: Mapped[float | None]
|
||||
fiber_per_unit: Mapped[float | None]
|
||||
saturated_fat_per_unit: Mapped[float | None]
|
||||
sugars_per_unit: Mapped[float | None]
|
||||
sodium_per_unit: Mapped[float | None]
|
||||
serving_size_g: Mapped[float | None]
|
||||
serving_name: Mapped[str | None]
|
||||
off_data: Mapped[str | None] # raw OFF JSON blob as TEXT
|
||||
deleted_at: Mapped[datetime | None]
|
||||
created_at: Mapped[datetime]
|
||||
updated_at: Mapped[datetime]
|
||||
|
||||
components: Mapped[list["MealComponent"]] = relationship(
|
||||
foreign_keys="MealComponent.meal_id", back_populates="meal"
|
||||
)
|
||||
|
||||
|
||||
class MealComponent(Base):
|
||||
__tablename__ = "meal_components"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
meal_id: Mapped[int] = mapped_column(ForeignKey("foods.id"))
|
||||
food_id: Mapped[int] = mapped_column(ForeignKey("foods.id"))
|
||||
quantity: Mapped[float]
|
||||
|
||||
meal: Mapped[Food] = relationship(foreign_keys=[meal_id], back_populates="components")
|
||||
food: Mapped[Food] = relationship(foreign_keys=[food_id])
|
||||
|
||||
|
||||
class DailyLogEntry(Base):
|
||||
__tablename__ = "daily_log"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
date: Mapped[date]
|
||||
food_id: Mapped[int] = mapped_column(ForeignKey("foods.id"))
|
||||
quantity: Mapped[float]
|
||||
meal_slot: Mapped[str | None] # "breakfast" | "lunch" | "dinner" | "snack"
|
||||
sort_order: Mapped[int] = mapped_column(default=0)
|
||||
created_at: Mapped[datetime]
|
||||
|
||||
food: Mapped[Food] = relationship()
|
||||
|
||||
|
||||
class Target(Base):
|
||||
__tablename__ = "targets"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
start_date: Mapped[date]
|
||||
end_date: Mapped[date | None] # null = currently active
|
||||
calories: Mapped[int]
|
||||
protein_g: Mapped[float | None]
|
||||
carbs_g: Mapped[float | None]
|
||||
fat_g: Mapped[float | None]
|
||||
Reference in New Issue
Block a user