"""Relations app action endpoints.""" from __future__ import annotations from quart import Blueprint, g, jsonify, request from shared.infrastructure.actions import ACTION_HEADER def register() -> Blueprint: bp = Blueprint("actions", __name__, url_prefix="/internal/actions") @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("/") 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 # --- 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"), relation_type=data.get("relation_type"), metadata=data.get("metadata"), ) 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, "relation_type": rel.relation_type, } _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"], relation_type=data.get("relation_type"), ) return {"deleted": deleted} _handlers["detach-child"] = _detach_child # --- relate (registry-aware) --- async def _relate(): """Create a typed relation with registry validation and cardinality enforcement.""" from shared.services.relationships import attach_child, get_children from shared.sx.relations import get_relation data = await request.get_json(force=True) rel_type = data.get("relation_type") if not rel_type: return {"error": "relation_type is required"}, 400 defn = get_relation(rel_type) if defn is None: return {"error": f"unknown relation_type: {rel_type}"}, 400 from_id = data["from_id"] to_id = data["to_id"] # Cardinality enforcement if defn.cardinality == "one-to-one": existing = await get_children( g.s, parent_type=defn.from_type, parent_id=from_id, child_type=defn.to_type, relation_type=rel_type, ) if existing: return {"error": "one-to-one relation already exists", "existing_id": existing[0].child_id}, 409 rel = await attach_child( g.s, parent_type=defn.from_type, parent_id=from_id, child_type=defn.to_type, child_id=to_id, label=data.get("label"), sort_order=data.get("sort_order"), relation_type=rel_type, metadata=data.get("metadata"), ) return { "id": rel.id, "relation_type": rel.relation_type, "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["relate"] = _relate # --- unrelate (registry-aware) --- async def _unrelate(): """Remove a typed relation with registry validation.""" from shared.services.relationships import detach_child from shared.sx.relations import get_relation data = await request.get_json(force=True) rel_type = data.get("relation_type") if not rel_type: return {"error": "relation_type is required"}, 400 defn = get_relation(rel_type) if defn is None: return {"error": f"unknown relation_type: {rel_type}"}, 400 deleted = await detach_child( g.s, parent_type=defn.from_type, parent_id=data["from_id"], child_type=defn.to_type, child_id=data["to_id"], relation_type=rel_type, ) return {"deleted": deleted} _handlers["unrelate"] = _unrelate # --- can-relate (pre-flight check) --- async def _can_relate(): """Check if a relation can be created (cardinality, registry validation).""" from shared.services.relationships import get_children from shared.sx.relations import get_relation data = await request.get_json(force=True) rel_type = data.get("relation_type") if not rel_type: return {"error": "relation_type is required"}, 400 defn = get_relation(rel_type) if defn is None: return {"allowed": False, "reason": f"unknown relation_type: {rel_type}"} from_id = data["from_id"] if defn.cardinality == "one-to-one": existing = await get_children( g.s, parent_type=defn.from_type, parent_id=from_id, child_type=defn.to_type, relation_type=rel_type, ) if existing: return {"allowed": False, "reason": "one-to-one relation already exists"} return {"allowed": True} _handlers["can-relate"] = _can_relate return bp