feat(v2): expand household scoping — meals get-by-id and shopping current, with tests; spec updated
This commit is contained in:
parent
6105d5dadf
commit
bb480d696b
5 changed files with 75 additions and 3 deletions
|
|
@ -1,12 +1,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query, Request, Response
|
||||||
|
|
||||||
import meals
|
import meals
|
||||||
|
from common import ProblemDetails
|
||||||
|
from api.deps import error_response
|
||||||
from api.deps import get_db, get_household_from_slug
|
from api.deps import get_db, get_household_from_slug
|
||||||
|
|
||||||
router = APIRouter(prefix="/households/{householdSlug}/meals", tags=["meals-v2"])
|
router = APIRouter(prefix="/households/{householdSlug}/meals", tags=["meals-v2"])
|
||||||
|
|
@ -59,3 +61,22 @@ async def get_upcoming_meals_scoped(
|
||||||
await meals.load_extra_ingredients(conn, meal)
|
await meals.load_extra_ingredients(conn, meal)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{meal_id}",
|
||||||
|
operation_id="getMealV2",
|
||||||
|
summary="Get a meal by id (scoped)",
|
||||||
|
responses={404: {"model": ProblemDetails}},
|
||||||
|
)
|
||||||
|
async def get_meal_scoped(
|
||||||
|
meal_id: int,
|
||||||
|
request: Request,
|
||||||
|
household=Depends(get_household_from_slug),
|
||||||
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
|
) -> meals.Meal | Response:
|
||||||
|
hid = household["id"]
|
||||||
|
meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||||
|
if not meal:
|
||||||
|
return error_response(request, 404, "Meal not found")
|
||||||
|
return meal
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,10 @@ Impact on existing routes (exact files to refactor):
|
||||||
- Added `api/recipes_v2.py` providing `/api/v1/households/{householdSlug}/recipes` with list/get/create.
|
- Added `api/recipes_v2.py` providing `/api/v1/households/{householdSlug}/recipes` with list/get/create.
|
||||||
- Added scoped repo helpers in `recipes/repository.py` and exported via `recipes/__init__.py`.
|
- Added scoped repo helpers in `recipes/repository.py` and exported via `recipes/__init__.py`.
|
||||||
- Tests in `tests/test_recipes_household_v2.py` validate isolation across households (PASS).
|
- Tests in `tests/test_recipes_household_v2.py` validate isolation across households (PASS).
|
||||||
- ✅ Meals (partial): Added `api/meals_v2.py` with `/api/v1/households/{householdSlug}/meals/upcoming` filtering by `household_id`; repository function `find_upcoming_meals_by_date_range_scoped` added. Test `tests/test_meals_household_v2.py` verifies isolation.
|
- ✅ Meals (partial): Added `api/meals_v2.py` with:
|
||||||
|
- `/api/v1/households/{householdSlug}/meals/upcoming` filtering by `household_id`.
|
||||||
|
- `/api/v1/households/{householdSlug}/meals/{id}` returns 404 across households.
|
||||||
|
- Repository functions `find_upcoming_meals_by_date_range_scoped` and `find_meal_by_id_scoped` added. Tests verify isolation.
|
||||||
- ✅ Shopping (partial): Added `api/shopping_v2.py` with `/api/v1/households/{householdSlug}/shopping/current`; added scoped helpers in `shopping/repository.py` and `shopping/__init__.py` to filter by `household_id`. Test `tests/test_shopping_household_v2.py` verifies isolation of outstanding items.
|
- ✅ Shopping (partial): Added `api/shopping_v2.py` with `/api/v1/households/{householdSlug}/shopping/current`; added scoped helpers in `shopping/repository.py` and `shopping/__init__.py` to filter by `household_id`. Test `tests/test_shopping_household_v2.py` verifies isolation of outstanding items.
|
||||||
- ⏳ Update Repositories: ingredients, products to accept `household_id` and filter accordingly; extend meals create/update/consumed/delete and shopping write flows (purchase, requests) with scoping.
|
- ⏳ Update Repositories: ingredients, products to accept `household_id` and filter accordingly; extend meals create/update/consumed/delete and shopping write flows (purchase, requests) with scoping.
|
||||||
- ⏳ Update Routers: move/duplicate remaining routers under the household router and wire `household_id` through.
|
- ⏳ Update Routers: move/duplicate remaining routers under the household router and wire `household_id` through.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from meals.repository import (
|
||||||
create as create,
|
create as create,
|
||||||
delete_meal as delete_meal,
|
delete_meal as delete_meal,
|
||||||
find_meal_by_id as find_meal_by_id,
|
find_meal_by_id as find_meal_by_id,
|
||||||
|
find_meal_by_id_scoped as find_meal_by_id_scoped,
|
||||||
find_upcoming_meals_by_date_range as find_upcoming_meals_by_date_range,
|
find_upcoming_meals_by_date_range as find_upcoming_meals_by_date_range,
|
||||||
find_upcoming_meals_by_date_range_scoped as find_upcoming_meals_by_date_range_scoped,
|
find_upcoming_meals_by_date_range_scoped as find_upcoming_meals_by_date_range_scoped,
|
||||||
insert_meal as insert_meal,
|
insert_meal as insert_meal,
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,25 @@ async def find_meal_by_id(conn, meal_id: int) -> Optional[Meal]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def find_meal_by_id_scoped(conn, meal_id: int, household_id: int) -> Optional[Meal]:
|
||||||
|
async with conn.execute(
|
||||||
|
f"""
|
||||||
|
SELECT {",".join(Meal.KEYS)} FROM Meal
|
||||||
|
WHERE id = ? AND household_id = ?
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(meal_id, household_id),
|
||||||
|
) as cursor:
|
||||||
|
async for row in cursor:
|
||||||
|
meal = Meal(**{k: v for k, v in zip(Meal.KEYS, row)})
|
||||||
|
|
||||||
|
await load_participants(conn, meal)
|
||||||
|
await load_recipes(conn, meal)
|
||||||
|
await load_extra_ingredients(conn, meal)
|
||||||
|
return meal
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def find_upcoming_meals_by_date_range(
|
async def find_upcoming_meals_by_date_range(
|
||||||
conn, start: datetime.datetime, end: datetime.datetime
|
conn, start: datetime.datetime, end: datetime.datetime
|
||||||
) -> AsyncIterator[Meal]:
|
) -> AsyncIterator[Meal]:
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,20 @@ class TestMealsHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
||||||
)
|
)
|
||||||
await self.conn.commit()
|
await self.conn.commit()
|
||||||
|
|
||||||
|
# Capture meal ids for each household
|
||||||
|
async with self.conn.execute(
|
||||||
|
"SELECT id FROM Meal WHERE household_id = ? ORDER BY id LIMIT 1", (self.h1_id,)
|
||||||
|
) as c:
|
||||||
|
row = await c.fetchone()
|
||||||
|
assert row is not None
|
||||||
|
self.meal_h1_id = int(row[0])
|
||||||
|
async with self.conn.execute(
|
||||||
|
"SELECT id FROM Meal WHERE household_id = ? ORDER BY id LIMIT 1", (self.h2_id,)
|
||||||
|
) as c:
|
||||||
|
row = await c.fetchone()
|
||||||
|
assert row is not None
|
||||||
|
self.meal_h2_id = int(row[0])
|
||||||
|
|
||||||
async def asyncTearDown(self):
|
async def asyncTearDown(self):
|
||||||
await self.conn.close()
|
await self.conn.close()
|
||||||
main.app.dependency_overrides.clear()
|
main.app.dependency_overrides.clear()
|
||||||
|
|
@ -87,3 +101,17 @@ class TestMealsHouseholdV2(unittest.IsolatedAsyncioTestCase):
|
||||||
assert len(m2) == 1
|
assert len(m2) == 1
|
||||||
# Ensure different meal ids per household
|
# Ensure different meal ids per household
|
||||||
assert m1[0]["id"] != m2[0]["id"]
|
assert m1[0]["id"] != m2[0]["id"]
|
||||||
|
|
||||||
|
def test_get_meal_scoped(self):
|
||||||
|
# Correct household should succeed
|
||||||
|
r_ok = self.client.get(
|
||||||
|
f"/api/v1/households/{self.h1}/meals/{self.meal_h1_id}", headers=self.headers
|
||||||
|
)
|
||||||
|
assert r_ok.status_code == 200, r_ok.text
|
||||||
|
assert r_ok.json()["id"] == self.meal_h1_id
|
||||||
|
|
||||||
|
# Cross-household should 404
|
||||||
|
r_404 = self.client.get(
|
||||||
|
f"/api/v1/households/{self.h2}/meals/{self.meal_h1_id}", headers=self.headers
|
||||||
|
)
|
||||||
|
assert r_404.status_code == 404
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue