feat(trash): /api/trash blueprint + flip REST DELETE handlers to soft-delete
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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})
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user