PageConfig (db_blog) decoupling: - Blog: add page-config, page-config-by-id, page-configs-batch data endpoints - Blog: add update-page-config action endpoint for events payment admin - Cart: hydrate_page, resolve_page_config, get_cart_grouped_by_page all fetch PageConfig from blog via HTTP instead of direct DB query - Cart: check_sumup_status auto-fetches page_config from blog when needed - Events: payment routes read/write PageConfig via blog HTTP endpoints - Order model: remove cross-domain page_config ORM relationship (keep column) Cart + Market DB merge: - Cart tables (cart_items, orders, order_items) moved into db_market - Cart app DATABASE_URL now points to db_market (same bounded context) - CartItem.product / CartItem.market_place relationships work again (same database, no cross-domain join issues) - Updated split-databases.sh, init-databases.sql, docker-compose.yml Ghost sync fix: - Wrap PostAuthor/PostTag delete+re-add in no_autoflush block - Use synchronize_session="fetch" to keep identity map consistent - Prevents query-invoked autoflush IntegrityError on composite PK Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
143 lines
4.5 KiB
Python
143 lines
4.5 KiB
Python
"""Blog app data endpoints.
|
|
|
|
Exposes read-only JSON queries at ``/internal/data/<query_name>`` for
|
|
cross-app callers via the internal data client.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from quart import Blueprint, g, jsonify, request
|
|
|
|
from sqlalchemy import select
|
|
|
|
from shared.infrastructure.data_client import DATA_HEADER
|
|
from shared.contracts.dtos import dto_to_dict
|
|
from shared.services.registry import services
|
|
from shared.models.page_config import PageConfig
|
|
|
|
|
|
def _page_config_dict(pc: PageConfig) -> dict:
|
|
"""Serialize PageConfig to a JSON-safe dict."""
|
|
return {
|
|
"id": pc.id,
|
|
"container_type": pc.container_type,
|
|
"container_id": pc.container_id,
|
|
"features": pc.features or {},
|
|
"sumup_merchant_code": pc.sumup_merchant_code,
|
|
"sumup_api_key": pc.sumup_api_key,
|
|
"sumup_checkout_prefix": pc.sumup_checkout_prefix,
|
|
}
|
|
|
|
|
|
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
|
|
|
|
_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)
|
|
|
|
# --- post-by-slug ---
|
|
async def _post_by_slug():
|
|
slug = request.args.get("slug", "")
|
|
post = await services.blog.get_post_by_slug(g.s, slug)
|
|
if not post:
|
|
return None
|
|
return dto_to_dict(post)
|
|
|
|
_handlers["post-by-slug"] = _post_by_slug
|
|
|
|
# --- post-by-id ---
|
|
async def _post_by_id():
|
|
post_id = int(request.args.get("id", 0))
|
|
post = await services.blog.get_post_by_id(g.s, post_id)
|
|
if not post:
|
|
return None
|
|
return dto_to_dict(post)
|
|
|
|
_handlers["post-by-id"] = _post_by_id
|
|
|
|
# --- posts-by-ids ---
|
|
async def _posts_by_ids():
|
|
ids_raw = request.args.get("ids", "")
|
|
if not ids_raw:
|
|
return []
|
|
ids = [int(x.strip()) for x in ids_raw.split(",") if x.strip()]
|
|
posts = await services.blog.get_posts_by_ids(g.s, ids)
|
|
return [dto_to_dict(p) for p in posts]
|
|
|
|
_handlers["posts-by-ids"] = _posts_by_ids
|
|
|
|
# --- search-posts ---
|
|
async def _search_posts():
|
|
query = request.args.get("query", "")
|
|
page = int(request.args.get("page", 1))
|
|
per_page = int(request.args.get("per_page", 10))
|
|
posts, total = await services.blog.search_posts(g.s, query, page, per_page)
|
|
return {"posts": [dto_to_dict(p) for p in posts], "total": total}
|
|
|
|
_handlers["search-posts"] = _search_posts
|
|
|
|
# --- page-config ---
|
|
async def _page_config():
|
|
"""Return a single PageConfig by container_type + container_id."""
|
|
ct = request.args.get("container_type", "page")
|
|
cid = request.args.get("container_id", type=int)
|
|
if cid is None:
|
|
return None
|
|
pc = (await g.s.execute(
|
|
select(PageConfig).where(
|
|
PageConfig.container_type == ct,
|
|
PageConfig.container_id == cid,
|
|
)
|
|
)).scalar_one_or_none()
|
|
if not pc:
|
|
return None
|
|
return _page_config_dict(pc)
|
|
|
|
_handlers["page-config"] = _page_config
|
|
|
|
# --- page-config-by-id ---
|
|
async def _page_config_by_id():
|
|
"""Return a single PageConfig by its primary key."""
|
|
pc_id = request.args.get("id", type=int)
|
|
if pc_id is None:
|
|
return None
|
|
pc = await g.s.get(PageConfig, pc_id)
|
|
if not pc:
|
|
return None
|
|
return _page_config_dict(pc)
|
|
|
|
_handlers["page-config-by-id"] = _page_config_by_id
|
|
|
|
# --- page-configs-batch ---
|
|
async def _page_configs_batch():
|
|
"""Return PageConfigs for multiple container_ids (comma-separated)."""
|
|
ct = request.args.get("container_type", "page")
|
|
ids_raw = request.args.get("ids", "")
|
|
if not ids_raw:
|
|
return []
|
|
ids = [int(x.strip()) for x in ids_raw.split(",") if x.strip()]
|
|
if not ids:
|
|
return []
|
|
result = await g.s.execute(
|
|
select(PageConfig).where(
|
|
PageConfig.container_type == ct,
|
|
PageConfig.container_id.in_(ids),
|
|
)
|
|
)
|
|
return [_page_config_dict(pc) for pc in result.scalars().all()]
|
|
|
|
_handlers["page-configs-batch"] = _page_configs_batch
|
|
|
|
return bp
|