gateway/modules/routes/routeTableViews.py

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")