Renumber the 15 reference docs into pillar groups (recipe -> anatomy): - 01 Build Recipe (entry), 02 Composition Root - domains: 10 Layering Rules - contract: 20 Spec-First Routing, 21 CRUD Template, 22 Auth Integration - interfaces/infra: 30 Infra Adapters - UI: 40 UI: Auto-Rendered Admin & Custom Pages (new; @aetoskia packages, client template, x- catalog via react-openapi MCP module) - pipelines: 50 Pipeline Scaffold, 51 Worker & Progress, 52 Human-in-the-Loop - delivery/meta: 90 Drone Pipelines, 91 Versioning & Changelog, 92 Genericity Matrix, 93 Library Registry, 94 Repository Inventory MCP module names are unchanged (NN_ prefix stripped); all cross-references (wiki, templates, hooks, registry) updated; lib index rewritten pillar-first; build recipe bootstraps the client template. Bundle + HTML sites regenerated.
147 lines
4.5 KiB
Python
147 lines
4.5 KiB
Python
"""# Summary
|
|
|
|
Shared interface helpers — service resolution, serialization, pagination and
|
|
foreign-key expansion used by every route module.
|
|
|
|
---
|
|
|
|
FK fields on the wire may arrive either as a bare integer id (from an
|
|
``x-fk`` picker) or as an already-expanded object. The wire contract always
|
|
*returns* fully expanded objects, so create/update handlers normalize the
|
|
input through :func:`expand_ref` before persisting.
|
|
"""
|
|
from fastapi import HTTPException, Request
|
|
|
|
|
|
def svc(request: Request, name: str):
|
|
"""Resolve a CRUD service from app state by resource name.
|
|
|
|
Args:
|
|
request (Request): Incoming request whose app.state carries resources.
|
|
name (str): Resource key suffix, e.g. ``"parent"`` → ``parent_svc``.
|
|
|
|
Returns:
|
|
Any: The named CRUD service.
|
|
"""
|
|
return request.app.state.db_resources[f"{name}_svc"]
|
|
|
|
|
|
def dump(doc) -> dict:
|
|
"""Serialize a document to a JSON-ready mapping with an integer id.
|
|
|
|
Args:
|
|
doc (Any): Pydantic document to serialize.
|
|
|
|
Returns:
|
|
dict: JSON-mode mapping (``by_alias`` so ``_id`` maps back to ``id``).
|
|
"""
|
|
return doc.model_dump(mode="json", by_alias=True)
|
|
|
|
|
|
def paginate(items: list, limit: int, offset: int) -> dict:
|
|
"""Slice ``items`` into the ``{total, items}`` pagination envelope.
|
|
|
|
A ``limit`` of 0 means "all remaining items" (used by react-openapi for
|
|
FK option lists and counts).
|
|
|
|
Args:
|
|
items (list): Full result list.
|
|
limit (int): Page size; ``0`` returns everything after ``offset``.
|
|
offset (int): Number of items to skip.
|
|
|
|
Returns:
|
|
dict: ``{"total": int, "items": list}``.
|
|
"""
|
|
offset = max(offset, 0)
|
|
page = items[offset:] if limit == 0 else items[offset : offset + limit]
|
|
return {"total": len(items), "items": page}
|
|
|
|
|
|
async def expand_ref(svc_obj, value, label: str) -> dict | None:
|
|
"""Normalize a foreign-key reference into a fully expanded object.
|
|
|
|
Args:
|
|
svc_obj (Any): Target resource CRUD service used for lookups.
|
|
value (dict | int | None): Bare id, ``{"id": N}`` stub, or full object.
|
|
label (str): Human label used in 422 error messages.
|
|
|
|
Returns:
|
|
dict | None: Expanded object, or ``None`` when ``value`` is ``None``.
|
|
|
|
Raises:
|
|
HTTPException: 422 when the reference cannot be resolved to a document.
|
|
"""
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, dict):
|
|
if set(value.keys()) == {"id"}:
|
|
value = value["id"]
|
|
else:
|
|
return value
|
|
if isinstance(value, int):
|
|
doc = await svc_obj.read(value)
|
|
if doc is None:
|
|
raise HTTPException(status_code=422, detail=f"{label} {value} not found")
|
|
return dump(doc)
|
|
raise HTTPException(status_code=422, detail=f"invalid {label} reference")
|
|
|
|
|
|
async def expand_ref_list(svc_obj, value, label: str) -> list[dict]:
|
|
"""Normalize a list foreign-key reference into expanded objects.
|
|
|
|
Args:
|
|
svc_obj (Any): Target resource CRUD service used for lookups.
|
|
value (list | None): List of bare ids, stubs, or full objects.
|
|
label (str): Human label used in 422 error messages.
|
|
|
|
Returns:
|
|
list[dict]: Expanded objects.
|
|
|
|
Raises:
|
|
HTTPException: 422 when any reference cannot be resolved.
|
|
"""
|
|
if value is None:
|
|
return []
|
|
if not isinstance(value, list):
|
|
raise HTTPException(status_code=422, detail=f"invalid {label} list")
|
|
results = []
|
|
for entry in value:
|
|
results.append(await expand_ref(svc_obj, entry, label))
|
|
return results
|
|
|
|
|
|
async def read_or_404(svc_obj, id: int, label: str):
|
|
"""Read a document by id or raise a 404 HTTPException.
|
|
|
|
Args:
|
|
svc_obj (Any): CRUD service to read from.
|
|
id (int): Document identifier.
|
|
label (str): Human label used in the 404 detail message.
|
|
|
|
Returns:
|
|
Any: The found document.
|
|
|
|
Raises:
|
|
HTTPException: 404 when no document matches ``id``.
|
|
"""
|
|
doc = await svc_obj.read(id)
|
|
if doc is None:
|
|
raise HTTPException(status_code=404, detail=f"{label} not found")
|
|
return doc
|
|
|
|
|
|
async def delete_or_404(svc_obj, id: int, label: str) -> None:
|
|
"""Delete a document by id or raise a 404 HTTPException.
|
|
|
|
Args:
|
|
svc_obj (Any): CRUD service to delete from.
|
|
id (int): Document identifier.
|
|
label (str): Human label used in the 404 detail message.
|
|
|
|
Raises:
|
|
HTTPException: 404 when no document matches ``id``.
|
|
"""
|
|
ok = await svc_obj.delete(id)
|
|
if not ok:
|
|
raise HTTPException(status_code=404, detail=f"{label} not found")
|