TICKET-007 (backend): meals — from-log, unpack, components, recursive nutrition, cycle detection

This commit is contained in:
Craig
2026-07-26 17:27:15 +01:00
parent 4dd44b08d0
commit a8aed9a84f
9 changed files with 1720 additions and 73 deletions
+28 -5
View File
@@ -2,6 +2,9 @@
ORM objects never leave this module; functions return Pydantic schemas.
Handlers stay thin (~15 lines) by calling into these functions.
Meal foods are returned as MealRead (with components + computed nutrition)
via get_meal(). get_food() returns the appropriate type based on is_meal.
"""
from datetime import datetime, timezone
@@ -10,7 +13,8 @@ from sqlalchemy import desc, func, select
from sqlalchemy.orm import Session
from models import DailyLogEntry, Food
from schemas import FoodCreate, FoodRead, FoodUpdate
from schemas import FoodCreate, FoodRead, FoodUpdate, MealRead
from services.meals import _build_meal_read, load_meal_with_components
# ── Shared query helpers (§8.1 rule 7) ───────────────────────────────────────
@@ -56,13 +60,32 @@ def list_foods(
return [FoodRead.model_validate(f) for f in foods]
def get_food(db: Session, food_id: int) -> FoodRead | None:
def get_food(db: Session, food_id: int) -> FoodRead | MealRead | None:
"""Get a single food by id. INCLUDES soft-deleted foods (historical log
rendering depends on this — spec §2.1). Returns None for unknown id."""
food = db.get(Food, food_id)
rendering depends on this — spec §2.1).
Returns MealRead (with components + computed nutrition) for meal foods,
plain FoodRead for non-meals. Returns None for unknown id.
"""
food = load_meal_with_components(db, food_id)
if food is None:
# Not a meal — try as a plain food
food = db.get(Food, food_id)
if food is None:
return None
return FoodRead.model_validate(food)
# Meal food: return MealRead with components
return _build_meal_read(food)
def get_meal(db: Session, meal_id: int) -> MealRead | None:
"""Get a meal food with components and computed nutrition.
Returns None if the food doesn't exist or is not a meal."""
meal = load_meal_with_components(db, meal_id)
if meal is None:
return None
return FoodRead.model_validate(food)
return _build_meal_read(meal)
def create_food(db: Session, data: FoodCreate) -> FoodRead: