Domain isolation: replace cross-domain imports with service calls
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m3s

Replace direct Post query and CartItem imports with typed service calls.
Market registers all 4 services via domain_services_fn with has() guards.

Key changes:
- app.py: use domain_services_fn, Post query → services.blog,
  CartItem → services.cart, MarketPlace+Post join → separate queries,
  glue navigation → shared navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-19 04:30:22 +00:00
parent 33befd4c3d
commit acf352ee3b
3 changed files with 58 additions and 45 deletions

75
app.py
View File

@@ -18,46 +18,38 @@ async def market_context() -> dict:
Market app context processor. Market app context processor.
- menu_items: direct DB query via glue layer - menu_items: direct DB query via glue layer
- cart_count/cart_total: direct DB query (same shared DB) - cart_count/cart_total: via cart service (shared DB)
""" """
from shared.infrastructure.context import base_context from shared.infrastructure.context import base_context
from glue.services.navigation import get_navigation_tree from shared.services.navigation import get_navigation_tree
from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.cart_identity import current_cart_identity
from models.market import CartItem from shared.services.registry import services
from sqlalchemy.orm import selectinload
ctx = await base_context() ctx = await base_context()
ctx["menu_items"] = await get_navigation_tree(g.s) ctx["menu_items"] = await get_navigation_tree(g.s)
# Cart data: query shared DB directly (avoids stale cross-app API responses) # Cart data via service (replaces direct CartItem query)
ident = current_cart_identity() ident = current_cart_identity()
cart_filters = [CartItem.deleted_at.is_(None)] summary = await services.cart.cart_summary(
if ident["user_id"] is not None: g.s, user_id=ident["user_id"], session_id=ident["session_id"],
cart_filters.append(CartItem.user_id == ident["user_id"])
else:
cart_filters.append(CartItem.session_id == ident["session_id"])
cart_result = await g.s.execute(
select(CartItem)
.where(*cart_filters)
.options(selectinload(CartItem.product))
) )
cart_items = cart_result.scalars().all() ctx["cart"] = summary.items
ctx["cart_count"] = summary.count
from bp.cart.services import total ctx["cart_total"] = float(summary.total)
ctx["cart"] = list(cart_items)
ctx["cart_count"] = sum(ci.quantity for ci in cart_items)
ctx["cart_total"] = total(cart_items) or 0
return ctx return ctx
def create_app() -> "Quart": def create_app() -> "Quart":
from models.market_place import MarketPlace from models.market_place import MarketPlace
from shared.models.ghost_content import Post from services import register_domain_services
app = create_base_app("market", context_fn=market_context) app = create_base_app(
"market",
context_fn=market_context,
domain_services_fn=register_domain_services,
)
# App-specific templates override shared templates # App-specific templates override shared templates
app_templates = str(Path(__file__).resolve().parent / "templates") app_templates = str(Path(__file__).resolve().parent / "templates")
@@ -100,12 +92,8 @@ def create_app() -> "Quart":
if not post_slug or not market_slug: if not post_slug or not market_slug:
return return
# Load post by slug # Load post by slug via blog service
post = ( post = await services.blog.get_post_by_slug(g.s, post_slug)
await g.s.execute(
select(Post).where(Post.slug == post_slug)
)
).scalar_one_or_none()
if not post: if not post:
abort(404) abort(404)
@@ -147,23 +135,22 @@ def create_app() -> "Quart":
# --- Root route: market listing --- # --- Root route: market listing ---
@app.get("/") @app.get("/")
async def markets_listing(): async def markets_listing():
rows = ( result = await g.s.execute(
await g.s.execute( select(MarketPlace)
select(MarketPlace, Post) .where(MarketPlace.deleted_at.is_(None), MarketPlace.container_type == "page")
.join( .order_by(MarketPlace.name)
Post, )
(MarketPlace.container_type == "page") all_markets = result.scalars().all()
& (MarketPlace.container_id == Post.id),
)
.where(MarketPlace.deleted_at.is_(None))
.order_by(MarketPlace.name)
)
).all()
# Attach the joined post to each market for template access # Resolve page posts via blog service
post_ids = list({m.container_id for m in all_markets})
posts_by_id = {
p.id: p
for p in await services.blog.get_posts_by_ids(g.s, post_ids)
}
markets = [] markets = []
for market, post in rows: for market in all_markets:
market.page = post market.page = posts_by_id.get(market.container_id)
markets.append(market) markets.append(market)
html = await render_template( html = await render_template(

26
services/__init__.py Normal file
View File

@@ -0,0 +1,26 @@
"""Market app service registration."""
from __future__ import annotations
def register_domain_services() -> None:
"""Register services for the market app.
Market owns: Product, CartItem, MarketPlace, NavTop, NavSub,
Listing, ProductImage.
Standard deployment registers all 4 services as real DB impls
(shared DB). For composable deployments, swap non-owned services
with stubs from shared.services.stubs.
"""
from shared.services.registry import services
from shared.services.blog_impl import SqlBlogService
from shared.services.calendar_impl import SqlCalendarService
from shared.services.market_impl import SqlMarketService
from shared.services.cart_impl import SqlCartService
services.market = SqlMarketService()
if not services.has("blog"):
services.blog = SqlBlogService()
if not services.has("calendar"):
services.calendar = SqlCalendarService()
if not services.has("cart"):
services.cart = SqlCartService()

2
shared

Submodule shared updated: ea7dc9723a...70b1c7de10