Files
DumiFlex 9a976137f0 feat(cascade): per-entity wildcard-delete cleanup via cleanup_ids
Wildcard-delete now lets the user opt in per affected entity which dead
@{uuid} references the engine strips, instead of a single all-or-nothing
cascade_refs flag.

- CascadeConfirmDialog: wildcard-delete branch renders per-row controls —
  constraints are informational ("reattach to heal", never cleaned),
  nested-ref wildcards/derivations get a default-unchecked "strip dead
  reference" checkbox. Checked ids form cleanup_ids on confirm. Other
  delete kinds keep the single cascade_refs checkbox unchanged.
- useCascadeApply: add cleanup_ids?: string[] to CascadeApplyRequest;
  apply() forwards it when present (dryRun unchanged).
- wp_api/cascade.py: document cleanup_ids in the body shape (already
  forwarded verbatim to apply_cascade; no per-field plumbing needed).
- Tests: dialog cases for the mixed affected set + cascade_refs
  regression-lock; server test proving cleanup_ids reaches the engine
  end-to-end (strips opted-in ref, leaves omitted ref broken).
2026-06-12 17:52:10 +03:00

96 lines
3.5 KiB
Python

"""HTTP routes for cascade-edit mutations.
POST /wp/api/cascade/apply → engine.cascade.orchestrator.apply_cascade
POST /wp/api/cascade/undo → engine.cascade.undo.undo_cascade
"""
from __future__ import annotations
from aiohttp import web
from engine.cascade.orchestrator import apply_cascade as _apply
from engine.cascade.undo import undo_cascade as _undo
from wp_api._helpers import db_session, json_error, json_ok
async def cascade_apply(request: web.Request) -> web.Response:
"""POST /wp/api/cascade/apply — apply cascade-edit mutations.
Body shape::
{
"kind": str, # "wildcard", "category", etc.
"id": str, # target entity UUID
"action": str, # "delete", "rename", etc.
"dry_run": bool, # optional, default False
"cascade_refs": bool, # optional, default True
"cleanup_ids": list[str], # optional, wildcard-delete only — ids of
# nested-ref wildcards/derivations whose dead
# @{uuid} token to strip; default []
"new_name": str, # optional, for rename actions
"extra": dict, # optional, for subcategory/combine_var ops
}
The whole body dict is forwarded verbatim to ``apply_cascade``; any
field the engine reads (incl. ``cleanup_ids``, which it pulls via
``req.get("cleanup_ids") or []``) flows through without per-field
plumbing here.
On success: ``{"ok": True, "undo_entry_id": str, "affected_count": int, "diff": list}``
(dry_run mode returns ``affected_count`` and ``affected_entities`` instead of ``diff``).
On error: ``{"error": str}`` with status 400.
"""
try:
body = await request.json()
except Exception:
return json_error("invalid JSON body", status=400)
if not isinstance(body, dict):
return json_error("body must be a JSON object", status=400)
with db_session(request) as conn:
result = _apply(conn, body)
if not result.get("ok"):
return json_error(result.get("error", "apply failed"), status=400)
return json_ok(result)
async def cascade_undo(request: web.Request) -> web.Response:
"""POST /wp/api/cascade/undo — reverse a previous cascade-edit apply.
Body shape::
{"undo_entry_id": str}
On success: ``{"ok": True}`` (200).
Missing or non-string ``undo_entry_id`` → 400.
Unknown ``undo_entry_id`` → **404** (engine returns "not found" in error).
Other failures → 400.
"""
try:
body = await request.json()
except Exception:
return json_error("invalid JSON body", status=400)
if not isinstance(body, dict):
return json_error("body must be a JSON object", status=400)
undo_id = body.get("undo_entry_id")
if not isinstance(undo_id, str) or not undo_id:
return json_error("undo_entry_id required", status=400)
with db_session(request) as conn:
result = _undo(conn, undo_id)
if not result.get("ok"):
error = result.get("error", "undo failed")
# Engine returns "undo entry not found" for missing entries;
# map to 404 for clean distinction from other errors.
status = 404 if "not found" in error.lower() else 400
return json_error(error, status=status)
return json_ok(result)
def register(router) -> None:
"""Register cascade routes on the given aiohttp router."""
router.add_post("/wp/api/cascade/apply", cascade_apply)
router.add_post("/wp/api/cascade/undo", cascade_undo)