Move container_relations to cart service for cross-service ownership
container_relations is a generic parent/child graph used by blog (menu_nodes), market (marketplaces), and events (calendars). Move it to cart as shared infrastructure. All services now call cart actions (attach-child/detach-child) instead of querying the table directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from shared.models.menu_node import MenuNode
|
||||
from models.ghost_content import Post
|
||||
from shared.services.relationships import attach_child, detach_child
|
||||
from shared.infrastructure.actions import call_action
|
||||
|
||||
|
||||
class MenuItemError(ValueError):
|
||||
@@ -80,7 +80,10 @@ async def create_menu_item(
|
||||
)
|
||||
session.add(menu_node)
|
||||
await session.flush()
|
||||
await attach_child(session, "page", post_id, "menu_node", menu_node.id)
|
||||
await call_action("cart", "attach-child", payload={
|
||||
"parent_type": "page", "parent_id": post_id,
|
||||
"child_type": "menu_node", "child_id": menu_node.id,
|
||||
})
|
||||
|
||||
return menu_node
|
||||
|
||||
@@ -131,8 +134,14 @@ async def update_menu_item(
|
||||
await session.flush()
|
||||
|
||||
if post_id is not None and post_id != old_post_id:
|
||||
await detach_child(session, "page", old_post_id, "menu_node", menu_node.id)
|
||||
await attach_child(session, "page", post_id, "menu_node", menu_node.id)
|
||||
await call_action("cart", "detach-child", payload={
|
||||
"parent_type": "page", "parent_id": old_post_id,
|
||||
"child_type": "menu_node", "child_id": menu_node.id,
|
||||
})
|
||||
await call_action("cart", "attach-child", payload={
|
||||
"parent_type": "page", "parent_id": post_id,
|
||||
"child_type": "menu_node", "child_id": menu_node.id,
|
||||
})
|
||||
|
||||
return menu_node
|
||||
|
||||
@@ -145,7 +154,10 @@ async def delete_menu_item(session: AsyncSession, item_id: int) -> bool:
|
||||
|
||||
menu_node.deleted_at = func.now()
|
||||
await session.flush()
|
||||
await detach_child(session, "page", menu_node.container_id, "menu_node", menu_node.id)
|
||||
await call_action("cart", "detach-child", payload={
|
||||
"parent_type": "page", "parent_id": menu_node.container_id,
|
||||
"child_type": "menu_node", "child_id": menu_node.id,
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ MODELS = [
|
||||
"shared.models.market", # CartItem lives here
|
||||
"shared.models.order",
|
||||
"shared.models.page_config",
|
||||
"shared.models.container_relation",
|
||||
]
|
||||
|
||||
TABLES = frozenset({
|
||||
"cart_items", "orders", "order_items", "page_configs",
|
||||
"cart_items", "orders", "order_items", "page_configs", "container_relations",
|
||||
})
|
||||
|
||||
run_alembic(context.config, MODELS, TABLES)
|
||||
|
||||
40
cart/alembic/versions/0003_add_container_relations.py
Normal file
40
cart/alembic/versions/0003_add_container_relations.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Add container_relations table to cart DB.
|
||||
|
||||
Generic parent/child graph for inter-service container relationships
|
||||
(page→market, page→calendar, page→menu_node, etc.).
|
||||
|
||||
Revision ID: cart_0003
|
||||
Revises: cart_0002
|
||||
Create Date: 2026-02-26
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "cart_0003"
|
||||
down_revision = "cart_0002"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"container_relations",
|
||||
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column("parent_type", sa.String(32), nullable=False),
|
||||
sa.Column("parent_id", sa.Integer, nullable=False),
|
||||
sa.Column("child_type", sa.String(32), nullable=False),
|
||||
sa.Column("child_id", sa.Integer, nullable=False),
|
||||
sa.Column("sort_order", sa.Integer, nullable=False, server_default="0"),
|
||||
sa.Column("label", sa.String(255), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.UniqueConstraint("parent_type", "parent_id", "child_type", "child_id",
|
||||
name="uq_container_relations_parent_child"),
|
||||
)
|
||||
op.create_index("ix_container_relations_parent", "container_relations", ["parent_type", "parent_id"])
|
||||
op.create_index("ix_container_relations_child", "container_relations", ["child_type", "child_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("container_relations")
|
||||
@@ -109,4 +109,47 @@ def register() -> Blueprint:
|
||||
|
||||
_handlers["update-page-config"] = _update_page_config
|
||||
|
||||
# --- attach-child ---
|
||||
async def _attach_child():
|
||||
"""Create or revive a ContainerRelation."""
|
||||
from shared.services.relationships import attach_child
|
||||
|
||||
data = await request.get_json(force=True)
|
||||
rel = await attach_child(
|
||||
g.s,
|
||||
parent_type=data["parent_type"],
|
||||
parent_id=data["parent_id"],
|
||||
child_type=data["child_type"],
|
||||
child_id=data["child_id"],
|
||||
label=data.get("label"),
|
||||
sort_order=data.get("sort_order"),
|
||||
)
|
||||
return {
|
||||
"id": rel.id,
|
||||
"parent_type": rel.parent_type,
|
||||
"parent_id": rel.parent_id,
|
||||
"child_type": rel.child_type,
|
||||
"child_id": rel.child_id,
|
||||
"sort_order": rel.sort_order,
|
||||
}
|
||||
|
||||
_handlers["attach-child"] = _attach_child
|
||||
|
||||
# --- detach-child ---
|
||||
async def _detach_child():
|
||||
"""Soft-delete a ContainerRelation."""
|
||||
from shared.services.relationships import detach_child
|
||||
|
||||
data = await request.get_json(force=True)
|
||||
deleted = await detach_child(
|
||||
g.s,
|
||||
parent_type=data["parent_type"],
|
||||
parent_id=data["parent_id"],
|
||||
child_type=data["child_type"],
|
||||
child_id=data["child_id"],
|
||||
)
|
||||
return {"deleted": deleted}
|
||||
|
||||
_handlers["detach-child"] = _detach_child
|
||||
|
||||
return bp
|
||||
|
||||
@@ -171,6 +171,32 @@ def register() -> Blueprint:
|
||||
|
||||
_handlers["page-configs-batch"] = _page_configs_batch
|
||||
|
||||
# --- get-children ---
|
||||
async def _get_children():
|
||||
"""Return ContainerRelation children for a parent."""
|
||||
from shared.services.relationships import get_children
|
||||
|
||||
parent_type = request.args.get("parent_type", "")
|
||||
parent_id = request.args.get("parent_id", type=int)
|
||||
child_type = request.args.get("child_type")
|
||||
if not parent_type or parent_id is None:
|
||||
return []
|
||||
rels = await get_children(g.s, parent_type, parent_id, child_type)
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"parent_type": r.parent_type,
|
||||
"parent_id": r.parent_id,
|
||||
"child_type": r.child_type,
|
||||
"child_id": r.child_id,
|
||||
"sort_order": r.sort_order,
|
||||
"label": r.label,
|
||||
}
|
||||
for r in rels
|
||||
]
|
||||
|
||||
_handlers["get-children"] = _get_children
|
||||
|
||||
return bp
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from models.calendars import Calendar
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||
from shared.services.relationships import attach_child, detach_child
|
||||
from shared.infrastructure.actions import call_action
|
||||
import unicodedata
|
||||
import re
|
||||
|
||||
@@ -71,7 +71,10 @@ async def soft_delete(sess: AsyncSession, post_slug: str, calendar_slug: str) ->
|
||||
|
||||
cal.deleted_at = utcnow()
|
||||
await sess.flush()
|
||||
await detach_child(sess, "page", cal.container_id, "calendar", cal.id)
|
||||
await call_action("cart", "detach-child", payload={
|
||||
"parent_type": "page", "parent_id": cal.container_id,
|
||||
"child_type": "calendar", "child_id": cal.id,
|
||||
})
|
||||
return True
|
||||
|
||||
async def create_calendar(sess: AsyncSession, post_id: int, name: str) -> Calendar:
|
||||
@@ -105,14 +108,20 @@ async def create_calendar(sess: AsyncSession, post_id: int, name: str) -> Calend
|
||||
if existing.deleted_at is not None:
|
||||
existing.deleted_at = None # revive
|
||||
await sess.flush()
|
||||
await attach_child(sess, "page", post_id, "calendar", existing.id)
|
||||
await call_action("cart", "attach-child", payload={
|
||||
"parent_type": "page", "parent_id": post_id,
|
||||
"child_type": "calendar", "child_id": existing.id,
|
||||
})
|
||||
return existing
|
||||
raise CalendarError(f'Calendar with slug "{slug}" already exists for post {post_id}.')
|
||||
|
||||
cal = Calendar(container_type="page", container_id=post_id, name=name, slug=slug)
|
||||
sess.add(cal)
|
||||
await sess.flush()
|
||||
await attach_child(sess, "page", post_id, "calendar", cal.id)
|
||||
await call_action("cart", "attach-child", payload={
|
||||
"parent_type": "page", "parent_id": post_id,
|
||||
"child_type": "calendar", "child_id": cal.id,
|
||||
})
|
||||
return cal
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from shared.models.market import Product
|
||||
from shared.models.market_place import MarketPlace
|
||||
from shared.browser.app.utils import utcnow
|
||||
from shared.contracts.dtos import MarketPlaceDTO, ProductDTO
|
||||
from shared.services.relationships import attach_child, detach_child
|
||||
from shared.infrastructure.actions import call_action
|
||||
|
||||
|
||||
def _mp_to_dto(mp: MarketPlace) -> MarketPlaceDTO:
|
||||
@@ -93,7 +93,10 @@ class SqlMarketService:
|
||||
existing.deleted_at = None # revive
|
||||
existing.name = name
|
||||
await session.flush()
|
||||
await attach_child(session, container_type, container_id, "market", existing.id)
|
||||
await call_action("cart", "attach-child", payload={
|
||||
"parent_type": container_type, "parent_id": container_id,
|
||||
"child_type": "market", "child_id": existing.id,
|
||||
})
|
||||
return _mp_to_dto(existing)
|
||||
raise ValueError(f'Market with slug "{slug}" already exists for this container.')
|
||||
|
||||
@@ -103,7 +106,10 @@ class SqlMarketService:
|
||||
)
|
||||
session.add(market)
|
||||
await session.flush()
|
||||
await attach_child(session, container_type, container_id, "market", market.id)
|
||||
await call_action("cart", "attach-child", payload={
|
||||
"parent_type": container_type, "parent_id": container_id,
|
||||
"child_type": "market", "child_id": market.id,
|
||||
})
|
||||
return _mp_to_dto(market)
|
||||
|
||||
async def soft_delete_marketplace(
|
||||
@@ -124,5 +130,8 @@ class SqlMarketService:
|
||||
|
||||
market.deleted_at = utcnow()
|
||||
await session.flush()
|
||||
await detach_child(session, container_type, container_id, "market", market.id)
|
||||
await call_action("cart", "detach-child", payload={
|
||||
"parent_type": container_type, "parent_id": container_id,
|
||||
"child_type": "market", "child_id": market.id,
|
||||
})
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user