feat: paginate filter and delete operation history
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""Paginated history without removing operation receipts used by other services."""
|
||||
|
||||
from datetime import UTC, date, datetime, time, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from fastapi import Depends, Query, Request
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from pigway_frame_flow.core.auth import require
|
||||
from pigway_frame_flow.errors import ErrorCode as E
|
||||
from pigway_frame_flow.errors import FlowError
|
||||
from pigway_frame_flow.models import Operation, OperationStatus
|
||||
|
||||
|
||||
class HistoryPage(BaseModel):
|
||||
items: list[Operation]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class HistoryDelete(BaseModel):
|
||||
operation_ids: list[str] = Field(min_length=1, max_length=200)
|
||||
|
||||
|
||||
def install_history(app, service):
|
||||
db = service.db
|
||||
|
||||
@app.get(
|
||||
"/api/v1/operation-history",
|
||||
response_model=HistoryPage,
|
||||
tags=["Operations"],
|
||||
dependencies=[Depends(require("history:read"))],
|
||||
)
|
||||
def list_history(
|
||||
request: Request,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
status: OperationStatus | None = None,
|
||||
action: str = Query("", max_length=80),
|
||||
device_id: str = Query("", max_length=120),
|
||||
q: str = Query("", max_length=200),
|
||||
start: date | None = None,
|
||||
end: date | None = None,
|
||||
):
|
||||
if start and end and start > end:
|
||||
raise FlowError(
|
||||
E.INVALID_REQUEST, "Start date must not follow end date", 422
|
||||
)
|
||||
clauses = [
|
||||
"NOT EXISTS (SELECT 1 FROM hidden_history h WHERE h.operation_id=o.id)"
|
||||
]
|
||||
args: list = []
|
||||
if "admin" not in request.state.permissions:
|
||||
clauses.append("o.client_id=?")
|
||||
args.append(request.state.client_id)
|
||||
for column, value in [
|
||||
("status", status),
|
||||
("action", action),
|
||||
("device_id", device_id),
|
||||
]:
|
||||
if value:
|
||||
clauses.append(f"o.{column}=?")
|
||||
args.append(value)
|
||||
if q.strip():
|
||||
clauses.append(
|
||||
"instr(lower(o.id||' '||o.request_id||' '||o.client_id||' '||coalesce(o.result,'')),lower(?))>0"
|
||||
)
|
||||
args.append(q.strip())
|
||||
for day, operator, offset in [(start, ">=", 0), (end, "<", 1)]:
|
||||
if day:
|
||||
boundary = datetime.combine(
|
||||
day, time(), ZoneInfo("Asia/Shanghai")
|
||||
) + timedelta(days=offset)
|
||||
clauses.append(f"o.created_at{operator}?")
|
||||
args.append(boundary.astimezone(UTC).isoformat())
|
||||
where = " AND ".join(clauses)
|
||||
# Keep count and rows consistent if a worker finishes or a deletion occurs.
|
||||
with db.lock:
|
||||
total = db.one(
|
||||
"SELECT count(*) AS total FROM operations o WHERE " + where, args
|
||||
)["total"]
|
||||
page = min(page, max(1, (total + page_size - 1) // page_size))
|
||||
rows = db.all(
|
||||
"SELECT o.id FROM operations o WHERE "
|
||||
+ where
|
||||
+ " ORDER BY o.created_at DESC,o.rowid DESC LIMIT ? OFFSET ?",
|
||||
[*args, page_size, (page - 1) * page_size],
|
||||
)
|
||||
return HistoryPage(
|
||||
items=[service.operation(row["id"]) for row in rows],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
@app.delete(
|
||||
"/api/v1/operation-history",
|
||||
tags=["Operations"],
|
||||
dependencies=[Depends(require("admin"))],
|
||||
)
|
||||
def delete_history(payload: HistoryDelete):
|
||||
ids = list(dict.fromkeys(payload.operation_ids))
|
||||
placeholders = ",".join("?" for _ in ids)
|
||||
with db.transaction() as connection:
|
||||
rows = connection.execute(
|
||||
f"SELECT id,status FROM operations WHERE id IN ({placeholders})", ids
|
||||
).fetchall()
|
||||
if len(rows) != len(ids):
|
||||
raise FlowError(E.NOT_FOUND, "History record not found", 404)
|
||||
if any(
|
||||
row["status"] not in {"COMPLETED", "FAILED", "UNKNOWN"} for row in rows
|
||||
):
|
||||
raise FlowError(E.CONFLICT, "Running operations cannot be deleted", 409)
|
||||
before = connection.total_changes
|
||||
connection.executemany(
|
||||
"INSERT OR IGNORE INTO hidden_history(operation_id) VALUES(?)",
|
||||
[(key,) for key in ids],
|
||||
)
|
||||
deleted = connection.total_changes - before
|
||||
return {"deleted": deleted}
|
||||
Reference in New Issue
Block a user