diff --git a/src/fabledassistant/app.py b/src/fabledassistant/app.py index f22c3fd..8c4cbfb 100644 --- a/src/fabledassistant/app.py +++ b/src/fabledassistant/app.py @@ -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(): diff --git a/src/fabledassistant/routes/events.py b/src/fabledassistant/routes/events.py index e0f5cfa..20a9fa1 100644 --- a/src/fabledassistant/routes/events.py +++ b/src/fabledassistant/routes/events.py @@ -126,16 +126,10 @@ async def update_event(event_id: int): @events_bp.delete("/") @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 diff --git a/src/fabledassistant/routes/milestones.py b/src/fabledassistant/routes/milestones.py index dfedbc0..61bb9c7 100644 --- a/src/fabledassistant/routes/milestones.py +++ b/src/fabledassistant/routes/milestones.py @@ -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 diff --git a/src/fabledassistant/routes/notes.py b/src/fabledassistant/routes/notes.py index 775f146..acac8c2 100644 --- a/src/fabledassistant/routes/notes.py +++ b/src/fabledassistant/routes/notes.py @@ -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 diff --git a/src/fabledassistant/routes/projects.py b/src/fabledassistant/routes/projects.py index 0a94e2e..c90ec66 100644 --- a/src/fabledassistant/routes/projects.py +++ b/src/fabledassistant/routes/projects.py @@ -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 diff --git a/src/fabledassistant/routes/rulebooks.py b/src/fabledassistant/routes/rulebooks.py index 2d8d99d..c16b405 100644 --- a/src/fabledassistant/routes/rulebooks.py +++ b/src/fabledassistant/routes/rulebooks.py @@ -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/") @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/") @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/") @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 diff --git a/src/fabledassistant/routes/tasks.py b/src/fabledassistant/routes/tasks.py index 85dc591..c008453 100644 --- a/src/fabledassistant/routes/tasks.py +++ b/src/fabledassistant/routes/tasks.py @@ -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 diff --git a/src/fabledassistant/routes/trash.py b/src/fabledassistant/routes/trash.py new file mode 100644 index 0000000..47bba62 --- /dev/null +++ b/src/fabledassistant/routes/trash.py @@ -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("//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("/") +@login_required +async def purge_batch(batch_id: str): + n = await trash_svc.purge(_uid(), batch_id) + return jsonify({"purged": n}) diff --git a/tests/test_routes_trash.py b/tests/test_routes_trash.py new file mode 100644 index 0000000..6d17e1c --- /dev/null +++ b/tests/test_routes_trash.py @@ -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