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.profile import profile_bp
|
||||||
from fabledassistant.routes.knowledge import knowledge_bp
|
from fabledassistant.routes.knowledge import knowledge_bp
|
||||||
from fabledassistant.routes.rulebooks import rulebooks_bp
|
from fabledassistant.routes.rulebooks import rulebooks_bp
|
||||||
|
from fabledassistant.routes.trash import trash_bp
|
||||||
from fabledassistant.mcp import mount_mcp
|
from fabledassistant.mcp import mount_mcp
|
||||||
|
|
||||||
STATIC_DIR = Path(__file__).parent / "static"
|
STATIC_DIR = Path(__file__).parent / "static"
|
||||||
@@ -85,6 +86,7 @@ def create_app() -> Quart:
|
|||||||
app.register_blueprint(profile_bp)
|
app.register_blueprint(profile_bp)
|
||||||
app.register_blueprint(knowledge_bp)
|
app.register_blueprint(knowledge_bp)
|
||||||
app.register_blueprint(rulebooks_bp)
|
app.register_blueprint(rulebooks_bp)
|
||||||
|
app.register_blueprint(trash_bp)
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
async def before_request():
|
async def before_request():
|
||||||
|
|||||||
@@ -126,16 +126,10 @@ async def update_event(event_id: int):
|
|||||||
@events_bp.delete("/<int:event_id>")
|
@events_bp.delete("/<int:event_id>")
|
||||||
@login_required
|
@login_required
|
||||||
async def delete_event(event_id: int):
|
async def delete_event(event_id: int):
|
||||||
event = await events_svc.get_event(
|
from fabledassistant.services.trash import delete as trash_delete
|
||||||
user_id=_get_current_user_id(),
|
batch = await trash_delete(_get_current_user_id(), "event", event_id)
|
||||||
event_id=event_id,
|
if batch is None:
|
||||||
)
|
|
||||||
if event is None:
|
|
||||||
return jsonify({"error": "Event not found"}), 404
|
return jsonify({"error": "Event not found"}), 404
|
||||||
await events_svc.delete_event(
|
|
||||||
user_id=_get_current_user_id(),
|
|
||||||
event_id=event_id,
|
|
||||||
)
|
|
||||||
return "", 204
|
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)
|
milestone = await get_milestone_in_project(project_id, milestone_id)
|
||||||
if milestone is None:
|
if milestone is None:
|
||||||
return not_found("Milestone")
|
return not_found("Milestone")
|
||||||
deleted = await delete_milestone(milestone.user_id, milestone_id)
|
from fabledassistant.services.trash import delete as trash_delete
|
||||||
if not deleted:
|
batch = await trash_delete(milestone.user_id, "milestone", milestone_id)
|
||||||
|
if batch is None:
|
||||||
return not_found("Milestone")
|
return not_found("Milestone")
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|||||||
@@ -260,8 +260,9 @@ async def patch_note_route(note_id: int):
|
|||||||
@login_required
|
@login_required
|
||||||
async def delete_note_route(note_id: int):
|
async def delete_note_route(note_id: int):
|
||||||
uid = get_current_user_id()
|
uid = get_current_user_id()
|
||||||
deleted = await delete_note(uid, note_id)
|
from fabledassistant.services.trash import delete as trash_delete
|
||||||
if not deleted:
|
batch = await trash_delete(uid, "note", note_id)
|
||||||
|
if batch is None:
|
||||||
return not_found("Note")
|
return not_found("Note")
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|||||||
@@ -101,8 +101,9 @@ async def update_project_route(project_id: int):
|
|||||||
@login_required
|
@login_required
|
||||||
async def delete_project_route(project_id: int):
|
async def delete_project_route(project_id: int):
|
||||||
uid = get_current_user_id()
|
uid = get_current_user_id()
|
||||||
deleted = await delete_project(uid, project_id)
|
from fabledassistant.services.trash import delete as trash_delete
|
||||||
if not deleted:
|
batch = await trash_delete(uid, "project", project_id)
|
||||||
|
if batch is None:
|
||||||
return not_found("Project")
|
return not_found("Project")
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from quart import Blueprint, g, jsonify, request
|
|||||||
|
|
||||||
from fabledassistant.auth import login_required
|
from fabledassistant.auth import login_required
|
||||||
import fabledassistant.services.rulebooks as rulebooks_svc
|
import fabledassistant.services.rulebooks as rulebooks_svc
|
||||||
|
from fabledassistant.services.trash import delete as trash_delete
|
||||||
|
|
||||||
rulebooks_bp = Blueprint("rulebooks", __name__, url_prefix="/api")
|
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>")
|
@rulebooks_bp.delete("/rulebooks/<int:rulebook_id>")
|
||||||
@login_required
|
@login_required
|
||||||
async def delete_rulebook(rulebook_id: int):
|
async def delete_rulebook(rulebook_id: int):
|
||||||
await rulebooks_svc.delete_rulebook(rulebook_id, _uid())
|
await trash_delete(_uid(), "rulebook", rulebook_id)
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|
||||||
@@ -117,7 +118,7 @@ async def update_topic(topic_id: int):
|
|||||||
@rulebooks_bp.delete("/rulebook-topics/<int:topic_id>")
|
@rulebooks_bp.delete("/rulebook-topics/<int:topic_id>")
|
||||||
@login_required
|
@login_required
|
||||||
async def delete_topic(topic_id: int):
|
async def delete_topic(topic_id: int):
|
||||||
await rulebooks_svc.delete_topic(topic_id, _uid())
|
await trash_delete(_uid(), "topic", topic_id)
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|
||||||
@@ -195,7 +196,7 @@ async def update_rule(rule_id: int):
|
|||||||
@rulebooks_bp.delete("/rules/<int:rule_id>")
|
@rulebooks_bp.delete("/rules/<int:rule_id>")
|
||||||
@login_required
|
@login_required
|
||||||
async def delete_rule(rule_id: int):
|
async def delete_rule(rule_id: int):
|
||||||
await rulebooks_svc.delete_rule(rule_id, _uid())
|
await trash_delete(_uid(), "rule", rule_id)
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -300,8 +300,9 @@ async def delete_task_route(task_id: int):
|
|||||||
task_note, _ = result
|
task_note, _ = result
|
||||||
if not await can_write_note(uid, task_id):
|
if not await can_write_note(uid, task_id):
|
||||||
return jsonify({"error": "Permission denied"}), 403
|
return jsonify({"error": "Permission denied"}), 403
|
||||||
deleted = await delete_note(task_note.user_id, task_id)
|
from fabledassistant.services.trash import delete as trash_delete
|
||||||
if not deleted:
|
batch = await trash_delete(task_note.user_id, "task", task_id)
|
||||||
|
if batch is None:
|
||||||
return not_found("Task")
|
return not_found("Task")
|
||||||
return "", 204
|
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