Replace inter-service _handlers dicts with declarative sx defquery/defaction
The inter-service data layer (fetch_data/call_action) was the least structured part of the codebase — Python _handlers dicts with ad-hoc param extraction scattered across 16 route files. This replaces them with declarative .sx query/action definitions that make the entire inter-service protocol self-describing and greppable. Infrastructure: - defquery/defaction special forms in the sx evaluator - Query/action registry with load, lookup, and schema introspection - Query executor using async_eval with I/O primitives - Blueprint factories (create_data_blueprint/create_action_blueprint) with sx-first dispatch and Python fallback - /internal/schema endpoint on every service - parse-datetime and split-ids primitives for type coercion Service extractions: - LikesService (toggle, is_liked, liked_slugs, liked_ids) - PageConfigService (ensure, get_by_container, get_by_id, get_batch, update) - RelationsService (wraps module-level functions) - AccountDataService (user_by_email, newsletters) - CartItemsService, MarketDataService (raw SQLAlchemy lookups) 50 of 54 handlers converted to sx, 4 Python fallbacks remain (ghost-sync/push-member, clear-cart-for-order, create-order). Net: -1,383 lines Python, +251 lines modified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
10
cart/actions.sx
Normal file
10
cart/actions.sx
Normal file
@@ -0,0 +1,10 @@
|
||||
;; Cart service — inter-service action endpoints
|
||||
|
||||
(defaction adopt-cart-for-user (&key user-id session-id)
|
||||
"Transfer anonymous cart items to a logged-in user."
|
||||
(do
|
||||
(service "cart" "adopt-cart-for-user"
|
||||
:user-id user-id :session-id session-id)
|
||||
{"ok" true}))
|
||||
|
||||
;; clear-cart-for-order: remains as Python fallback (complex object construction)
|
||||
@@ -1,64 +1,26 @@
|
||||
"""Cart app action endpoints.
|
||||
|
||||
Exposes write operations at ``/internal/actions/<action_name>`` for
|
||||
cross-app callers (login handler) via the internal action client.
|
||||
adopt-cart-for-user is defined in ``cart/actions.sx``.
|
||||
clear-cart-for-order remains as a Python fallback (complex object construction).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import Blueprint, g, jsonify, request
|
||||
from quart import Blueprint, g, request
|
||||
|
||||
from shared.infrastructure.actions import ACTION_HEADER
|
||||
from shared.services.registry import services
|
||||
from shared.infrastructure.query_blueprint import create_action_blueprint
|
||||
|
||||
|
||||
def register() -> Blueprint:
|
||||
bp = Blueprint("actions", __name__, url_prefix="/internal/actions")
|
||||
bp, _handlers = create_action_blueprint("cart")
|
||||
|
||||
@bp.before_request
|
||||
async def _require_action_header():
|
||||
if not request.headers.get(ACTION_HEADER):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
from shared.infrastructure.internal_auth import validate_internal_request
|
||||
if not validate_internal_request():
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
|
||||
_handlers: dict[str, object] = {}
|
||||
|
||||
@bp.post("/<action_name>")
|
||||
async def handle_action(action_name: str):
|
||||
handler = _handlers.get(action_name)
|
||||
if handler is None:
|
||||
return jsonify({"error": "unknown action"}), 404
|
||||
try:
|
||||
result = await handler()
|
||||
return jsonify(result)
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logging.getLogger(__name__).exception("Action %s failed", action_name)
|
||||
return jsonify({"error": str(exc)}), 500
|
||||
|
||||
# --- adopt-cart-for-user ---
|
||||
async def _adopt_cart():
|
||||
data = await request.get_json()
|
||||
await services.cart.adopt_cart_for_user(
|
||||
g.s, data["user_id"], data["session_id"],
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
_handlers["adopt-cart-for-user"] = _adopt_cart
|
||||
|
||||
# --- clear-cart-for-order ---
|
||||
async def _clear_cart_for_order():
|
||||
"""Soft-delete cart items after an order is paid. Called by orders service."""
|
||||
from bp.cart.services.clear_cart_for_order import clear_cart_for_order
|
||||
from shared.models.order import Order
|
||||
|
||||
data = await request.get_json()
|
||||
user_id = data.get("user_id")
|
||||
session_id = data.get("session_id")
|
||||
page_post_id = data.get("page_post_id")
|
||||
|
||||
# Build a minimal order-like object with the fields clear_cart_for_order needs
|
||||
order = type("_Order", (), {
|
||||
"user_id": user_id,
|
||||
"session_id": session_id,
|
||||
|
||||
@@ -1,79 +1,14 @@
|
||||
"""Cart app data endpoints.
|
||||
|
||||
Exposes read-only JSON queries at ``/internal/data/<query_name>`` for
|
||||
cross-app callers via the internal data client.
|
||||
All queries are defined in ``cart/queries.sx``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import Blueprint, g, jsonify, request
|
||||
from quart import Blueprint
|
||||
|
||||
from shared.infrastructure.data_client import DATA_HEADER
|
||||
from shared.contracts.dtos import dto_to_dict
|
||||
from shared.services.registry import services
|
||||
from shared.infrastructure.query_blueprint import create_data_blueprint
|
||||
|
||||
|
||||
def register() -> Blueprint:
|
||||
bp = Blueprint("data", __name__, url_prefix="/internal/data")
|
||||
|
||||
@bp.before_request
|
||||
async def _require_data_header():
|
||||
if not request.headers.get(DATA_HEADER):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
from shared.infrastructure.internal_auth import validate_internal_request
|
||||
if not validate_internal_request():
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
|
||||
_handlers: dict[str, object] = {}
|
||||
|
||||
@bp.get("/<query_name>")
|
||||
async def handle_query(query_name: str):
|
||||
handler = _handlers.get(query_name)
|
||||
if handler is None:
|
||||
return jsonify({"error": "unknown query"}), 404
|
||||
result = await handler()
|
||||
return jsonify(result)
|
||||
|
||||
# --- cart-summary ---
|
||||
async def _cart_summary():
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
session_id = request.args.get("session_id")
|
||||
page_slug = request.args.get("page_slug")
|
||||
summary = await services.cart.cart_summary(
|
||||
g.s, user_id=user_id, session_id=session_id, page_slug=page_slug,
|
||||
)
|
||||
return dto_to_dict(summary)
|
||||
|
||||
_handlers["cart-summary"] = _cart_summary
|
||||
|
||||
# --- cart-items (product slugs + quantities for template rendering) ---
|
||||
async def _cart_items():
|
||||
from sqlalchemy import select
|
||||
from shared.models.market import CartItem
|
||||
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
session_id = request.args.get("session_id")
|
||||
|
||||
filters = [CartItem.deleted_at.is_(None)]
|
||||
if user_id is not None:
|
||||
filters.append(CartItem.user_id == user_id)
|
||||
elif session_id is not None:
|
||||
filters.append(CartItem.session_id == session_id)
|
||||
else:
|
||||
return []
|
||||
|
||||
result = await g.s.execute(
|
||||
select(CartItem).where(*filters)
|
||||
)
|
||||
items = result.scalars().all()
|
||||
return [
|
||||
{
|
||||
"product_id": item.product_id,
|
||||
"product_slug": item.product_slug,
|
||||
"quantity": item.quantity,
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
_handlers["cart-items"] = _cart_items
|
||||
|
||||
bp, _handlers = create_data_blueprint("cart")
|
||||
return bp
|
||||
|
||||
11
cart/queries.sx
Normal file
11
cart/queries.sx
Normal file
@@ -0,0 +1,11 @@
|
||||
;; Cart service — inter-service data queries
|
||||
|
||||
(defquery cart-summary (&key user-id session-id page-slug)
|
||||
"Cart summary for a user or session, optionally filtered by page."
|
||||
(service "cart" "cart-summary"
|
||||
:user-id user-id :session-id session-id :page-slug page-slug))
|
||||
|
||||
(defquery cart-items (&key user-id session-id)
|
||||
"Product slugs and quantities in the cart."
|
||||
(service "cart-data" "cart-items"
|
||||
:user-id user-id :session-id session-id))
|
||||
@@ -13,5 +13,8 @@ def register_domain_services() -> None:
|
||||
|
||||
services.cart = SqlCartService()
|
||||
|
||||
from shared.services.cart_items_impl import SqlCartItemsService
|
||||
services.register("cart_data", SqlCartItemsService())
|
||||
|
||||
from .cart_page import CartPageService
|
||||
services.register("cart_page", CartPageService())
|
||||
|
||||
Reference in New Issue
Block a user