feat(trash): /api/trash blueprint + flip REST DELETE handlers to soft-delete

This commit is contained in:
2026-05-28 21:13:31 -04:00
parent 580e4a2c0a
commit bfeed67cfe
9 changed files with 86 additions and 20 deletions
+2
View File
@@ -26,6 +26,7 @@ from fabledassistant.routes.search import search_bp
from fabledassistant.routes.profile import profile_bp
from fabledassistant.routes.knowledge import knowledge_bp
from fabledassistant.routes.rulebooks import rulebooks_bp
from fabledassistant.routes.trash import trash_bp
from fabledassistant.mcp import mount_mcp
STATIC_DIR = Path(__file__).parent / "static"
@@ -85,6 +86,7 @@ def create_app() -> Quart:
app.register_blueprint(profile_bp)
app.register_blueprint(knowledge_bp)
app.register_blueprint(rulebooks_bp)
app.register_blueprint(trash_bp)
@app.before_request
async def before_request():
+3 -9
View File
@@ -126,16 +126,10 @@ async def update_event(event_id: int):
@events_bp.delete("/<int:event_id>")
@login_required
async def delete_event(event_id: int):
event = await events_svc.get_event(
user_id=_get_current_user_id(),
event_id=event_id,
)
if event is None:
from fabledassistant.services.trash import delete as trash_delete
batch = await trash_delete(_get_current_user_id(), "event", event_id)
if batch is None:
return jsonify({"error": "Event not found"}), 404
await events_svc.delete_event(
user_id=_get_current_user_id(),
event_id=event_id,
)
return "", 204
+3 -2
View File
@@ -113,8 +113,9 @@ async def delete_milestone_route(project_id: int, milestone_id: int):
milestone = await get_milestone_in_project(project_id, milestone_id)
if milestone is None:
return not_found("Milestone")
deleted = await delete_milestone(milestone.user_id, milestone_id)
if not deleted:
from fabledassistant.services.trash import delete as trash_delete
batch = await trash_delete(milestone.user_id, "milestone", milestone_id)
if batch is None:
return not_found("Milestone")
return "", 204
+3 -2
View File
@@ -260,8 +260,9 @@ async def patch_note_route(note_id: int):
@login_required
async def delete_note_route(note_id: int):
uid = get_current_user_id()
deleted = await delete_note(uid, note_id)
if not deleted:
from fabledassistant.services.trash import delete as trash_delete
batch = await trash_delete(uid, "note", note_id)
if batch is None:
return not_found("Note")
return "", 204
+3 -2
View File
@@ -101,8 +101,9 @@ async def update_project_route(project_id: int):
@login_required
async def delete_project_route(project_id: int):
uid = get_current_user_id()
deleted = await delete_project(uid, project_id)
if not deleted:
from fabledassistant.services.trash import delete as trash_delete
batch = await trash_delete(uid, "project", project_id)
if batch is None:
return not_found("Project")
return "", 204
+4 -3
View File
@@ -9,6 +9,7 @@ from quart import Blueprint, g, jsonify, request
from fabledassistant.auth import login_required
import fabledassistant.services.rulebooks as rulebooks_svc
from fabledassistant.services.trash import delete as trash_delete
rulebooks_bp = Blueprint("rulebooks", __name__, url_prefix="/api")
@@ -64,7 +65,7 @@ async def update_rulebook(rulebook_id: int):
@rulebooks_bp.delete("/rulebooks/<int:rulebook_id>")
@login_required
async def delete_rulebook(rulebook_id: int):
await rulebooks_svc.delete_rulebook(rulebook_id, _uid())
await trash_delete(_uid(), "rulebook", rulebook_id)
return "", 204
@@ -117,7 +118,7 @@ async def update_topic(topic_id: int):
@rulebooks_bp.delete("/rulebook-topics/<int:topic_id>")
@login_required
async def delete_topic(topic_id: int):
await rulebooks_svc.delete_topic(topic_id, _uid())
await trash_delete(_uid(), "topic", topic_id)
return "", 204
@@ -195,7 +196,7 @@ async def update_rule(rule_id: int):
@rulebooks_bp.delete("/rules/<int:rule_id>")
@login_required
async def delete_rule(rule_id: int):
await rulebooks_svc.delete_rule(rule_id, _uid())
await trash_delete(_uid(), "rule", rule_id)
return "", 204
+3 -2
View File
@@ -300,8 +300,9 @@ async def delete_task_route(task_id: int):
task_note, _ = result
if not await can_write_note(uid, task_id):
return jsonify({"error": "Permission denied"}), 403
deleted = await delete_note(task_note.user_id, task_id)
if not deleted:
from fabledassistant.services.trash import delete as trash_delete
batch = await trash_delete(task_note.user_id, "task", task_id)
if batch is None:
return not_found("Task")
return "", 204
+33
View File
@@ -0,0 +1,33 @@
"""Trash REST API — list / restore / purge soft-deleted content by batch."""
from __future__ import annotations
from quart import Blueprint, g, jsonify
from fabledassistant.auth import login_required
import fabledassistant.services.trash as trash_svc
trash_bp = Blueprint("trash", __name__, url_prefix="/api/trash")
def _uid() -> int:
return g.user.id
@trash_bp.get("")
@login_required
async def list_trash():
return jsonify({"batches": await trash_svc.list_trash(_uid())})
@trash_bp.post("/<batch_id>/restore")
@login_required
async def restore_batch(batch_id: str):
n = await trash_svc.restore(_uid(), batch_id)
return jsonify({"restored": n})
@trash_bp.delete("/<batch_id>")
@login_required
async def purge_batch(batch_id: str):
n = await trash_svc.purge(_uid(), batch_id)
return jsonify({"purged": n})
+32
View File
@@ -0,0 +1,32 @@
"""Structural tests for the trash blueprint + delete-flip contracts."""
import inspect
def test_trash_blueprint_registered():
from fabledassistant.routes.trash import trash_bp
assert trash_bp.name == "trash"
assert trash_bp.url_prefix == "/api/trash"
def test_trash_blueprint_registered_in_app():
from fabledassistant.app import create_app
app = create_app()
assert "trash" in app.blueprints
def test_trash_handlers_callable():
from fabledassistant.routes import trash as trash_routes
for name in ("list_trash", "restore_batch", "purge_batch"):
assert callable(getattr(trash_routes, name))
def test_trash_service_surface():
from fabledassistant.services import trash as svc
for fn_name in ("delete", "restore", "purge", "list_trash", "purge_expired", "alive"):
assert hasattr(svc, fn_name), f"trash service missing {fn_name}"
def test_delete_service_signature():
from fabledassistant.services.trash import delete
params = inspect.signature(delete).parameters
assert "user_id" in params and "entity_type" in params and "entity_id" in params