feat: paginate filter and delete operation history
This commit is contained in:
@@ -14,6 +14,7 @@ from fastapi.openapi.docs import get_swagger_ui_html
|
||||
from fastapi.responses import FileResponse, JSONResponse, Response
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from pigway_frame_flow.api.history import install_history
|
||||
from pigway_frame_flow.core.accounts import install_accounts, seed_accounts
|
||||
from pigway_frame_flow.core.auth import require, seed_client
|
||||
from pigway_frame_flow.core.registry import DeviceRegistry
|
||||
@@ -132,6 +133,7 @@ def create_app(settings=None, start_worker=True):
|
||||
)
|
||||
install_extensions(app, service)
|
||||
install_accounts(app, db)
|
||||
install_history(app, service)
|
||||
app.state.service = service
|
||||
app.state.worker_enabled = start_worker
|
||||
app.state.scheduler = scheduler
|
||||
@@ -842,7 +844,7 @@ def create_app(settings=None, start_worker=True):
|
||||
return [
|
||||
service.operation(x["id"])
|
||||
for x in db.all(
|
||||
"SELECT id FROM operations WHERE (? OR client_id=?) ORDER BY created_at DESC LIMIT ?",
|
||||
"SELECT id FROM operations WHERE (? OR client_id=?) AND NOT EXISTS (SELECT 1 FROM hidden_history h WHERE h.operation_id=operations.id) ORDER BY created_at DESC,rowid DESC LIMIT ?",
|
||||
("admin" in request.state.permissions, request.state.client_id, limit),
|
||||
)
|
||||
]
|
||||
@@ -1001,7 +1003,10 @@ def create_app(settings=None, start_worker=True):
|
||||
dependencies=[Depends(require("history:read"))],
|
||||
)
|
||||
def history(limit: int = Query(default=100, ge=1, le=200)):
|
||||
return db.all("SELECT * FROM history ORDER BY id DESC LIMIT ?", (limit,))
|
||||
return db.all(
|
||||
"SELECT * FROM history WHERE NOT EXISTS (SELECT 1 FROM hidden_history h WHERE h.operation_id=history.operation_id) ORDER BY id DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
|
||||
@app.get(
|
||||
"/api/v1/settings",
|
||||
|
||||
@@ -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