munch-ease-backend/api/recipes_v2.py

135 lines
4.6 KiB
Python

from __future__ import annotations
from typing import List, Optional
import aiosqlite
from fastapi import APIRouter, Depends, Query, Response
import ingredients as ingredients_mod
import recipes
from api.deps import error_response, get_db, get_household_from_slug
from common import Page, ProblemDetails, ApiModel, Field
router = APIRouter(prefix="/households/{householdSlug}/recipes", tags=["recipes-v2"])
class RecipeOut(ApiModel):
id: int = -1
name: str
link: str
serves: int
image_urls: List[str] = Field(min_length=0, json_schema_extra={"minItems": 0})
ingredients: List[ingredients_mod.Ingredient] = Field(
min_length=0, json_schema_extra={"minItems": 0}
)
class RecipeCreate(ApiModel):
name: str
link: str
serves: int
image_urls: List[str] = Field(min_length=0, json_schema_extra={"minItems": 0})
ingredients: List[ingredients_mod.Ingredient] = Field(
min_length=0, json_schema_extra={"minItems": 0}
)
@router.get("", response_model=Page[RecipeOut])
async def list_recipes(
household=Depends(get_household_from_slug),
q: Optional[str] = Query(default=None),
cursor: Optional[str] = Query(default=None),
limit: int = Query(50, ge=1, le=200),
conn: aiosqlite.Connection = Depends(get_db),
):
last_id = None
if cursor:
try:
last_id = int(cursor)
except ValueError:
last_id = None
fetch_limit = limit + 1
hid = household["id"]
paged: List[recipes.Recipe] = []
if q:
async for r in recipes.find_recipes_by_name_paged_scoped(
conn, q, last_id, fetch_limit, hid
):
paged.append(r)
else:
async for r in recipes.get_all_paged_scoped(conn, last_id, fetch_limit, hid):
paged.append(r)
# Filter by household_id once repositories are fully updated; currently placeholder until repo changes land.
has_more = len(paged) > limit
items = paged[:limit]
if items:
recipe_ids = [r.id for r in items]
by_recipe = await ingredients_mod.find_ingredients_by_recipe_ids(conn, recipe_ids)
for r in items:
r.ingredients = by_recipe.get(r.id, [])
next_cursor = str(items[-1].id) if has_more and items else None
total = await (
recipes.count_by_name_scoped(conn, q, hid) if q else recipes.count_all_scoped(conn, hid)
)
return Page(items=items, nextCursor=next_cursor, prevCursor=None, total=total)
@router.get("/{recipe_id}", response_model=RecipeOut, responses={404: {"model": ProblemDetails}})
async def get_recipe(
recipe_id: int,
household=Depends(get_household_from_slug),
conn: aiosqlite.Connection = Depends(get_db),
):
r = await recipes.find_recipe_by_id_scoped(conn, recipe_id, household["id"])
if not r:
return error_response(None, 404, "Recipe not found")
return r
@router.delete("/{recipe_id}", response_model=RecipeOut, responses={404: {"model": ProblemDetails}})
async def delete_recipe(
recipe_id: int,
household=Depends(get_household_from_slug),
conn: aiosqlite.Connection = Depends(get_db),
):
# Load recipe in-scope
r = await recipes.find_recipe_by_id_scoped(conn, recipe_id, household["id"])
if not r:
return error_response(None, 404, "Recipe not found")
# Hide within household (no hidden_by in v2 yet)
from recipes.repository import hide_recipe_scoped
ok = await hide_recipe_scoped(conn, recipe_id, household["id"])
if not ok:
return error_response(None, 404, "Recipe not found")
return r
@router.post("", response_model=RecipeOut, responses={400: {"model": ProblemDetails}})
async def create_recipe(
recipe: RecipeCreate,
response: Response,
household=Depends(get_household_from_slug),
conn: aiosqlite.Connection = Depends(get_db),
):
if not recipe.ingredients:
return error_response(None, 400, "Recipe must have at least one ingredient")
hid = household["id"]
# v1 Recipe model requires created_by_id; use 0 placeholder until users replace persons
r = recipes.Recipe(
id=-1,
name=recipe.name,
link=recipe.link,
serves=recipe.serves,
image_urls=recipe.image_urls,
ingredients=list(recipe.ingredients),
created_by_id=0,
)
await recipes.insert_recipe_scoped(conn, r, hid)
for ingredient in recipe.ingredients:
ingredient.recipe_id = r.id
if ingredient.product:
ingredient.product_id = ingredient.product.id
await ingredients_mod.insert_ingredient(conn, ingredient)
response.headers["Location"] = f"/api/v1/households/{household['slug']}/recipes/{r.id}"
return r