refactor(http): drop categories/subcategories shim
Remove the transitional layer added in commits 1+2:
models.py
- Drop Category, Subcategory, CreateCategory, CreateSubcategory.
crud.py
- Drop create_category / update_category / get_category /
get_categories / delete_category and the subcategory variants
along with the _node_row_to_category / _node_row_to_subcategory
helpers. Tree state is owned exclusively by menu_node CRUD now.
views_api.py
- Remove old endpoints:
GET /api/v1/restaurants/{id}/categories
POST /api/v1/categories
DELETE /api/v1/categories/{id}
GET /api/v1/categories/{id}/subcategories
POST /api/v1/subcategories
DELETE /api/v1/subcategories/{id}
Hits return 404 now.
- GET /api/v1/restaurants/{id}/menu loses the synthetic
'categories' projection. Response is {restaurant, tree, items}.
static/js/api.js
- Drop listCategories / createCategory / deleteCategory and the
subcategory wrappers.
The CMS menu builder is broken between this commit and commit 4.
The plan acknowledged this trade-off: keeping commits revertible
beats the cost of an unshipped UI page rendering a stale empty
sidebar for one commit's lifetime.
This commit is contained in:
parent
ab87ddb2da
commit
b7fa1aec4a
4 changed files with 8 additions and 300 deletions
127
crud.py
127
crud.py
|
|
@ -21,15 +21,12 @@ from lnbits.helpers import urlsafe_short_hash
|
||||||
from .models import (
|
from .models import (
|
||||||
MAX_MENU_DEPTH,
|
MAX_MENU_DEPTH,
|
||||||
AvailabilityWindow,
|
AvailabilityWindow,
|
||||||
Category,
|
|
||||||
CreateAvailabilityWindow,
|
CreateAvailabilityWindow,
|
||||||
CreateCategory,
|
|
||||||
CreateMenuItem,
|
CreateMenuItem,
|
||||||
CreateMenuNode,
|
CreateMenuNode,
|
||||||
CreateModifier,
|
CreateModifier,
|
||||||
CreateModifierGroup,
|
CreateModifierGroup,
|
||||||
CreateRestaurant,
|
CreateRestaurant,
|
||||||
CreateSubcategory,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuNode,
|
MenuNode,
|
||||||
MenuNodeRow,
|
MenuNodeRow,
|
||||||
|
|
@ -41,7 +38,6 @@ from .models import (
|
||||||
Restaurant,
|
Restaurant,
|
||||||
RestaurantSettings,
|
RestaurantSettings,
|
||||||
SelectedModifier,
|
SelectedModifier,
|
||||||
Subcategory,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db = Database("ext_restaurant")
|
db = Database("ext_restaurant")
|
||||||
|
|
@ -397,129 +393,6 @@ async def delete_menu_node(node_id: str, cascade: bool = False) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------- #
|
|
||||||
# Categories / subcategories — transitional shims (drop in commit 3) #
|
|
||||||
# --------------------------------------------------------------------- #
|
|
||||||
# These keep the old /categories and /subcategories REST endpoints
|
|
||||||
# working over the new menu_nodes table for one commit's lifetime.
|
|
||||||
# Drop entirely in the next commit once the new endpoints are live.
|
|
||||||
|
|
||||||
|
|
||||||
def _node_row_to_category(row: MenuNodeRow) -> Category:
|
|
||||||
return Category(
|
|
||||||
id=row.id,
|
|
||||||
restaurant_id=row.restaurant_id,
|
|
||||||
name=row.name,
|
|
||||||
description=row.description,
|
|
||||||
sort_order=row.sort_order,
|
|
||||||
image_url=row.image_url,
|
|
||||||
time=row.time,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _node_row_to_subcategory(row: MenuNodeRow) -> Subcategory:
|
|
||||||
# Subcategory carries the parent category id, not its own restaurant.
|
|
||||||
return Subcategory(
|
|
||||||
id=row.id,
|
|
||||||
category_id=row.parent_id or "",
|
|
||||||
name=row.name,
|
|
||||||
sort_order=row.sort_order,
|
|
||||||
time=row.time,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_category(data: CreateCategory) -> Category:
|
|
||||||
node = await create_menu_node(
|
|
||||||
CreateMenuNode(
|
|
||||||
restaurant_id=data.restaurant_id,
|
|
||||||
parent_id=None,
|
|
||||||
name=data.name,
|
|
||||||
description=data.description,
|
|
||||||
sort_order=data.sort_order,
|
|
||||||
image_url=data.image_url,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return _node_row_to_category(node)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_category(category: Category) -> Category:
|
|
||||||
row = await get_menu_node(category.id)
|
|
||||||
if not row:
|
|
||||||
raise ValueError("Category not found")
|
|
||||||
row.name = category.name
|
|
||||||
row.description = category.description
|
|
||||||
row.sort_order = category.sort_order
|
|
||||||
row.image_url = category.image_url
|
|
||||||
await update_menu_node(row)
|
|
||||||
return category
|
|
||||||
|
|
||||||
|
|
||||||
async def get_category(category_id: str) -> Optional[Category]:
|
|
||||||
row = await get_menu_node(category_id)
|
|
||||||
if not row or row.depth != 0:
|
|
||||||
return None
|
|
||||||
return _node_row_to_category(row)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_categories(restaurant_id: str) -> list[Category]:
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"""
|
|
||||||
SELECT * FROM restaurant.menu_nodes
|
|
||||||
WHERE restaurant_id = :rid AND depth = 0
|
|
||||||
ORDER BY sort_order, time
|
|
||||||
""",
|
|
||||||
{"rid": restaurant_id},
|
|
||||||
model=MenuNodeRow,
|
|
||||||
)
|
|
||||||
return [_node_row_to_category(r) for r in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_category(category_id: str) -> None:
|
|
||||||
await delete_menu_node(category_id, cascade=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_subcategory(data: CreateSubcategory) -> Subcategory:
|
|
||||||
parent = await get_menu_node(data.category_id)
|
|
||||||
if not parent:
|
|
||||||
raise ValueError("Category not found")
|
|
||||||
node = await create_menu_node(
|
|
||||||
CreateMenuNode(
|
|
||||||
restaurant_id=parent.restaurant_id,
|
|
||||||
parent_id=parent.id,
|
|
||||||
name=data.name,
|
|
||||||
sort_order=data.sort_order,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return _node_row_to_subcategory(node)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_subcategory(subcategory: Subcategory) -> Subcategory:
|
|
||||||
row = await get_menu_node(subcategory.id)
|
|
||||||
if not row:
|
|
||||||
raise ValueError("Subcategory not found")
|
|
||||||
row.name = subcategory.name
|
|
||||||
row.sort_order = subcategory.sort_order
|
|
||||||
await update_menu_node(row)
|
|
||||||
return subcategory
|
|
||||||
|
|
||||||
|
|
||||||
async def get_subcategories(category_id: str) -> list[Subcategory]:
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"""
|
|
||||||
SELECT * FROM restaurant.menu_nodes
|
|
||||||
WHERE parent_id = :pid
|
|
||||||
ORDER BY sort_order, time
|
|
||||||
""",
|
|
||||||
{"pid": category_id},
|
|
||||||
model=MenuNodeRow,
|
|
||||||
)
|
|
||||||
return [_node_row_to_subcategory(r) for r in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_subcategory(subcategory_id: str) -> None:
|
|
||||||
await delete_menu_node(subcategory_id, cascade=True)
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
# Menu items #
|
# Menu items #
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
|
|
||||||
40
models.py
40
models.py
|
|
@ -213,46 +213,6 @@ class MenuNode(MenuNodeRow):
|
||||||
items: list["MenuItem"] = Field(default_factory=list)
|
items: list["MenuItem"] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------- #
|
|
||||||
# Transitional shims (kept until commit 3) #
|
|
||||||
# --------------------------------------------------------------------- #
|
|
||||||
# These let the old /categories and /subcategories endpoints keep
|
|
||||||
# working over the new menu_nodes table for one commit's lifetime.
|
|
||||||
# Drop in commit 3.
|
|
||||||
|
|
||||||
|
|
||||||
class CreateCategory(BaseModel):
|
|
||||||
restaurant_id: str
|
|
||||||
name: str
|
|
||||||
description: Optional[str] = None
|
|
||||||
sort_order: int = 0
|
|
||||||
image_url: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class Category(BaseModel):
|
|
||||||
id: str
|
|
||||||
restaurant_id: str
|
|
||||||
name: str
|
|
||||||
description: Optional[str] = None
|
|
||||||
sort_order: int = 0
|
|
||||||
image_url: Optional[str] = None
|
|
||||||
time: datetime
|
|
||||||
|
|
||||||
|
|
||||||
class CreateSubcategory(BaseModel):
|
|
||||||
category_id: str
|
|
||||||
name: str
|
|
||||||
sort_order: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
class Subcategory(BaseModel):
|
|
||||||
id: str
|
|
||||||
category_id: str
|
|
||||||
name: str
|
|
||||||
sort_order: int = 0
|
|
||||||
time: datetime
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
# Menu items #
|
# Menu items #
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
|
|
||||||
|
|
@ -23,17 +23,6 @@
|
||||||
deleteRestaurant: (key, id) =>
|
deleteRestaurant: (key, id) =>
|
||||||
call(key, 'DELETE', `/restaurants/${id}`),
|
call(key, 'DELETE', `/restaurants/${id}`),
|
||||||
|
|
||||||
// Categories (transitional shim, drop in commit 3)
|
|
||||||
listCategories: (id) => call(null, 'GET', `/restaurants/${id}/categories`),
|
|
||||||
createCategory: (key, data) => call(key, 'POST', '/categories', data),
|
|
||||||
deleteCategory: (key, id) => call(key, 'DELETE', `/categories/${id}`),
|
|
||||||
|
|
||||||
// Subcategories (transitional shim, drop in commit 3)
|
|
||||||
listSubcategories: (catId) =>
|
|
||||||
call(null, 'GET', `/categories/${catId}/subcategories`),
|
|
||||||
createSubcategory: (key, data) => call(key, 'POST', '/subcategories', data),
|
|
||||||
deleteSubcategory: (key, id) => call(key, 'DELETE', `/subcategories/${id}`),
|
|
||||||
|
|
||||||
// Menu nodes (the tree)
|
// Menu nodes (the tree)
|
||||||
listMenuNodes: (restaurantId) =>
|
listMenuNodes: (restaurantId) =>
|
||||||
call(null, 'GET', `/restaurants/${restaurantId}/menu_nodes`),
|
call(null, 'GET', `/restaurants/${restaurantId}/menu_nodes`),
|
||||||
|
|
|
||||||
128
views_api.py
128
views_api.py
|
|
@ -34,24 +34,18 @@ from lnbits.decorators import (
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_availability_window,
|
create_availability_window,
|
||||||
create_category,
|
|
||||||
create_menu_item,
|
create_menu_item,
|
||||||
create_menu_node,
|
create_menu_node,
|
||||||
create_modifier,
|
create_modifier,
|
||||||
create_modifier_group,
|
create_modifier_group,
|
||||||
create_restaurant,
|
create_restaurant,
|
||||||
create_subcategory,
|
|
||||||
delete_availability_window,
|
delete_availability_window,
|
||||||
delete_category,
|
|
||||||
delete_menu_item,
|
delete_menu_item,
|
||||||
delete_menu_node,
|
delete_menu_node,
|
||||||
delete_modifier,
|
delete_modifier,
|
||||||
delete_modifier_group,
|
delete_modifier_group,
|
||||||
delete_restaurant,
|
delete_restaurant,
|
||||||
delete_subcategory,
|
|
||||||
get_availability_windows,
|
get_availability_windows,
|
||||||
get_categories,
|
|
||||||
get_category,
|
|
||||||
get_menu_item,
|
get_menu_item,
|
||||||
get_menu_items,
|
get_menu_items,
|
||||||
get_menu_node,
|
get_menu_node,
|
||||||
|
|
@ -67,7 +61,6 @@ from .crud import (
|
||||||
get_restaurant,
|
get_restaurant,
|
||||||
get_restaurants,
|
get_restaurants,
|
||||||
get_settings,
|
get_settings,
|
||||||
get_subcategories,
|
|
||||||
move_menu_node,
|
move_menu_node,
|
||||||
update_menu_item,
|
update_menu_item,
|
||||||
update_menu_node,
|
update_menu_node,
|
||||||
|
|
@ -77,18 +70,14 @@ from .crud import (
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
AvailabilityWindow,
|
AvailabilityWindow,
|
||||||
Category,
|
|
||||||
CreateAvailabilityWindow,
|
CreateAvailabilityWindow,
|
||||||
CreateCategory,
|
|
||||||
CreateMenuItem,
|
CreateMenuItem,
|
||||||
CreateMenuNode,
|
CreateMenuNode,
|
||||||
CreateModifier,
|
CreateModifier,
|
||||||
CreateModifierGroup,
|
CreateModifierGroup,
|
||||||
CreateOrder,
|
CreateOrder,
|
||||||
CreateRestaurant,
|
CreateRestaurant,
|
||||||
CreateSubcategory,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuNode,
|
|
||||||
MenuNodeRow,
|
MenuNodeRow,
|
||||||
Modifier,
|
Modifier,
|
||||||
ModifierGroup,
|
ModifierGroup,
|
||||||
|
|
@ -97,7 +86,6 @@ from .models import (
|
||||||
OrderWithItems,
|
OrderWithItems,
|
||||||
Restaurant,
|
Restaurant,
|
||||||
RestaurantSettings,
|
RestaurantSettings,
|
||||||
Subcategory,
|
|
||||||
)
|
)
|
||||||
from .nostr_publisher import (
|
from .nostr_publisher import (
|
||||||
build_delete_event,
|
build_delete_event,
|
||||||
|
|
@ -421,72 +409,6 @@ async def api_delete_menu_node(
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
@restaurant_api_router.get("/api/v1/restaurants/{restaurant_id}/categories")
|
|
||||||
async def api_list_categories(restaurant_id: str) -> list[Category]:
|
|
||||||
return await get_categories(restaurant_id)
|
|
||||||
|
|
||||||
|
|
||||||
@restaurant_api_router.post("/api/v1/categories")
|
|
||||||
async def api_create_category(
|
|
||||||
data: CreateCategory,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
) -> Category:
|
|
||||||
restaurant = await get_restaurant(data.restaurant_id)
|
|
||||||
if not restaurant:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Restaurant not found."
|
|
||||||
)
|
|
||||||
_require_owner(restaurant, wallet)
|
|
||||||
return await create_category(data)
|
|
||||||
|
|
||||||
|
|
||||||
@restaurant_api_router.delete("/api/v1/categories/{category_id}")
|
|
||||||
async def api_delete_category(
|
|
||||||
category_id: str,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
):
|
|
||||||
cat = await get_category(category_id)
|
|
||||||
if not cat:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Category not found."
|
|
||||||
)
|
|
||||||
restaurant = await get_restaurant(cat.restaurant_id)
|
|
||||||
if restaurant:
|
|
||||||
_require_owner(restaurant, wallet)
|
|
||||||
await delete_category(category_id)
|
|
||||||
return "", HTTPStatus.NO_CONTENT
|
|
||||||
|
|
||||||
|
|
||||||
@restaurant_api_router.get("/api/v1/categories/{category_id}/subcategories")
|
|
||||||
async def api_list_subcategories(category_id: str) -> list[Subcategory]:
|
|
||||||
return await get_subcategories(category_id)
|
|
||||||
|
|
||||||
|
|
||||||
@restaurant_api_router.post("/api/v1/subcategories")
|
|
||||||
async def api_create_subcategory(
|
|
||||||
data: CreateSubcategory,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
) -> Subcategory:
|
|
||||||
cat = await get_category(data.category_id)
|
|
||||||
if not cat:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Category not found."
|
|
||||||
)
|
|
||||||
restaurant = await get_restaurant(cat.restaurant_id)
|
|
||||||
if restaurant:
|
|
||||||
_require_owner(restaurant, wallet)
|
|
||||||
return await create_subcategory(data)
|
|
||||||
|
|
||||||
|
|
||||||
@restaurant_api_router.delete("/api/v1/subcategories/{subcategory_id}")
|
|
||||||
async def api_delete_subcategory(
|
|
||||||
subcategory_id: str,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
):
|
|
||||||
await delete_subcategory(subcategory_id)
|
|
||||||
return "", HTTPStatus.NO_CONTENT
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
# Menu items #
|
# Menu items #
|
||||||
# --------------------------------------------------------------------- #
|
# --------------------------------------------------------------------- #
|
||||||
|
|
@ -495,18 +417,16 @@ async def api_delete_subcategory(
|
||||||
@restaurant_api_router.get("/api/v1/restaurants/{restaurant_id}/menu")
|
@restaurant_api_router.get("/api/v1/restaurants/{restaurant_id}/menu")
|
||||||
async def api_get_menu(restaurant_id: str) -> dict:
|
async def api_get_menu(restaurant_id: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Public composite endpoint: returns the menu in three shapes in one
|
Public composite endpoint: returns the menu in two shapes in one
|
||||||
round trip.
|
round trip.
|
||||||
|
|
||||||
* ``tree`` — the full hydrated tree (root nodes with
|
* ``tree`` — the full hydrated tree (root nodes with nested
|
||||||
nested children + items, depth, path).
|
children + items, depth, path). Each item is the
|
||||||
|
bare MenuItem (no modifier hydration).
|
||||||
* ``items`` — flat enriched list (modifier groups, modifier
|
* ``items`` — flat enriched list (modifier groups, modifier
|
||||||
options, availability windows attached);
|
options, availability windows attached); useful
|
||||||
useful for search / filter.
|
for search / filter and for hydrating the items
|
||||||
* ``categories`` — depth-0 nodes only, with their direct items.
|
referenced from ``tree``.
|
||||||
A transitional projection so the existing
|
|
||||||
CMS keeps rendering until commit 4 swaps it
|
|
||||||
for q-tree. Drops in commit 3.
|
|
||||||
|
|
||||||
The webapp loads this once and then trusts Nostr events for
|
The webapp loads this once and then trusts Nostr events for
|
||||||
incremental updates.
|
incremental updates.
|
||||||
|
|
@ -535,44 +455,10 @@ async def api_get_menu(restaurant_id: str) -> dict:
|
||||||
]
|
]
|
||||||
enriched_items.append(item_dict)
|
enriched_items.append(item_dict)
|
||||||
|
|
||||||
# Synthetic transitional "categories" projection: depth-0 nodes
|
|
||||||
# plus their immediate items, mapped to the legacy shape the CMS
|
|
||||||
# still consumes. Removed in commit 3.
|
|
||||||
items_by_node: dict[str, list[dict]] = {}
|
|
||||||
for it in enriched_items:
|
|
||||||
nid = it.get("node_id")
|
|
||||||
if nid:
|
|
||||||
items_by_node.setdefault(nid, []).append(it)
|
|
||||||
|
|
||||||
legacy_categories: list[dict] = []
|
|
||||||
for root in tree:
|
|
||||||
cat_dict = {
|
|
||||||
"id": root.id,
|
|
||||||
"restaurant_id": root.restaurant_id,
|
|
||||||
"name": root.name,
|
|
||||||
"description": root.description,
|
|
||||||
"sort_order": root.sort_order,
|
|
||||||
"image_url": root.image_url,
|
|
||||||
"time": root.time,
|
|
||||||
"subcategories": [
|
|
||||||
{
|
|
||||||
"id": child.id,
|
|
||||||
"category_id": root.id,
|
|
||||||
"name": child.name,
|
|
||||||
"sort_order": child.sort_order,
|
|
||||||
"time": child.time,
|
|
||||||
}
|
|
||||||
for child in root.children
|
|
||||||
],
|
|
||||||
"items": items_by_node.get(root.id, []),
|
|
||||||
}
|
|
||||||
legacy_categories.append(cat_dict)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"restaurant": restaurant.dict(),
|
"restaurant": restaurant.dict(),
|
||||||
"tree": [t.dict() for t in tree],
|
"tree": [t.dict() for t in tree],
|
||||||
"items": enriched_items,
|
"items": enriched_items,
|
||||||
"categories": legacy_categories, # transitional, drop in commit 3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue