177 lines
6.3 KiB
Python
177 lines
6.3 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
CRUD endpoints for saved table views (TableListView).
|
|
|
|
A view stores a named preset of filters, sort order, and groupByLevels for a
|
|
specific table (identified by contextKey). Views are per-user and optionally
|
|
per-mandate.
|
|
|
|
Route prefix: /api/table-views
|
|
"""
|
|
|
|
import logging
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Request
|
|
from fastapi import status
|
|
|
|
from modules.auth import limiter, getCurrentUser
|
|
from modules.datamodels.datamodelUam import User
|
|
from modules.datamodels.datamodelPagination import TableListView
|
|
import modules.interfaces.interfaceDbApp as interfaceDbApp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(
|
|
prefix="/api/table-views",
|
|
tags=["Table Views"],
|
|
responses={404: {"description": "Not found"}},
|
|
)
|
|
|
|
|
|
def _ownedOrRaise(view: Optional[TableListView], viewId: str, userId: str):
|
|
"""Raise 404 when view is missing; ownership is implicitly guaranteed by the
|
|
interface layer (views are always queried with the current userId)."""
|
|
if view is None:
|
|
raise HTTPException(status_code=404, detail=f"View '{viewId}' not found")
|
|
return view
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# List views for a context
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("")
|
|
@limiter.limit("60/minute")
|
|
def list_views(
|
|
request: Request,
|
|
contextKey: str = Query(..., description="Table context key, e.g. 'connections', 'files/list'"),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
):
|
|
"""List all saved views for the current user and contextKey."""
|
|
iface = interfaceDbApp.getInterface(currentUser)
|
|
views = iface.getTableListViews(contextKey=contextKey)
|
|
return [v.model_dump() if hasattr(v, "model_dump") else v for v in views]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Get one view
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/{viewKey}")
|
|
@limiter.limit("60/minute")
|
|
def get_view(
|
|
request: Request,
|
|
viewKey: str = Path(..., description="View slug"),
|
|
contextKey: str = Query(..., description="Table context key"),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
):
|
|
"""Return a single saved view by its viewKey."""
|
|
iface = interfaceDbApp.getInterface(currentUser)
|
|
view = iface.getTableListView(contextKey=contextKey, viewKey=viewKey)
|
|
if view is None:
|
|
raise HTTPException(status_code=404, detail=f"View '{viewKey}' not found for context '{contextKey}'")
|
|
return view.model_dump() if hasattr(view, "model_dump") else view
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Create a view
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.post("", status_code=status.HTTP_201_CREATED)
|
|
@limiter.limit("30/minute")
|
|
def create_view(
|
|
request: Request,
|
|
body: dict = Body(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
):
|
|
"""
|
|
Create a new saved view.
|
|
|
|
Body fields:
|
|
- contextKey (required): table context key
|
|
- viewKey (required): short slug, unique per (user, contextKey)
|
|
- displayName (required): human-readable label
|
|
- config (optional): view config dict with keys:
|
|
schemaVersion, filters, sort, groupByLevels
|
|
"""
|
|
contextKey = body.get("contextKey")
|
|
viewKey = body.get("viewKey")
|
|
displayName = body.get("displayName")
|
|
config = body.get("config") or {}
|
|
|
|
if not contextKey:
|
|
raise HTTPException(status_code=400, detail="contextKey is required")
|
|
if not viewKey:
|
|
raise HTTPException(status_code=400, detail="viewKey is required")
|
|
if not displayName:
|
|
raise HTTPException(status_code=400, detail="displayName is required")
|
|
|
|
iface = interfaceDbApp.getInterface(currentUser)
|
|
try:
|
|
view = iface.createTableListView(
|
|
contextKey=contextKey,
|
|
viewKey=viewKey,
|
|
displayName=displayName,
|
|
config=config,
|
|
)
|
|
return view.model_dump() if hasattr(view, "model_dump") else view
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=409, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"create_view failed: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to create view")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Update a view (by id)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.put("/{viewId}")
|
|
@limiter.limit("30/minute")
|
|
def update_view(
|
|
request: Request,
|
|
viewId: str = Path(..., description="View primary-key id (not viewKey)"),
|
|
body: dict = Body(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
):
|
|
"""
|
|
Update an existing view.
|
|
|
|
Updatable fields: displayName, viewKey, config.
|
|
The contextKey cannot be changed after creation.
|
|
"""
|
|
allowed = {"displayName", "viewKey", "config"}
|
|
updates = {k: v for k, v in body.items() if k in allowed}
|
|
if not updates:
|
|
raise HTTPException(status_code=400, detail=f"No updatable fields provided. Allowed: {allowed}")
|
|
|
|
iface = interfaceDbApp.getInterface(currentUser)
|
|
try:
|
|
updated = iface.updateTableListView(viewId=viewId, updates=updates)
|
|
except Exception as e:
|
|
logger.error(f"update_view failed: {e}")
|
|
raise HTTPException(status_code=500, detail="Failed to update view")
|
|
|
|
if updated is None:
|
|
raise HTTPException(status_code=404, detail=f"View id='{viewId}' not found")
|
|
return updated.model_dump() if hasattr(updated, "model_dump") else updated
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Delete a view (by id)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.delete("/{viewId}", status_code=status.HTTP_204_NO_CONTENT)
|
|
@limiter.limit("30/minute")
|
|
def delete_view(
|
|
request: Request,
|
|
viewId: str = Path(..., description="View primary-key id"),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
):
|
|
"""Delete a saved view by its primary-key id."""
|
|
iface = interfaceDbApp.getInterface(currentUser)
|
|
deleted = iface.deleteTableListView(viewId=viewId)
|
|
if not deleted:
|
|
raise HTTPException(status_code=404, detail=f"View id='{viewId}' not found or could not be deleted")
|