107 lines
4 KiB
Python
107 lines
4 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Pagination models for server-side pagination, sorting, and filtering.
|
|
|
|
All models use camelStyle naming convention for consistency with frontend.
|
|
"""
|
|
|
|
from typing import List, Dict, Any, Optional, Generic, TypeVar
|
|
from pydantic import BaseModel, Field, ConfigDict
|
|
import math
|
|
|
|
T = TypeVar('T')
|
|
|
|
|
|
class SortField(BaseModel):
|
|
"""
|
|
Single sort field configuration.
|
|
"""
|
|
field: str = Field(..., description="Field name to sort by")
|
|
direction: str = Field(..., description="Sort direction: 'asc' or 'desc'")
|
|
|
|
|
|
class PaginationParams(BaseModel):
|
|
"""
|
|
Complete pagination state including page, sorting, and filters.
|
|
"""
|
|
page: int = Field(ge=1, description="Current page number (1-based)")
|
|
pageSize: int = Field(ge=1, le=1000, description="Number of items per page")
|
|
sort: List[SortField] = Field(default_factory=list, description="List of sort fields in priority order")
|
|
filters: Optional[Dict[str, Any]] = Field(
|
|
default=None,
|
|
description="""Filter criteria dictionary. Supports:
|
|
- General search: {"search": "text"} - searches across all text fields (case-insensitive)
|
|
- Field-specific filters:
|
|
- Simple equals: {"status": "running"}
|
|
- With operator: {"status": {"operator": "equals", "value": "running"}}
|
|
- Supported operators: equals/eq, contains, startsWith, endsWith, gt, gte, lt, lte, in, notIn
|
|
- Multiple filters are combined with AND logic"""
|
|
)
|
|
|
|
|
|
class PaginationRequest(BaseModel):
|
|
"""
|
|
Pagination request parameters sent from frontend to backend.
|
|
All fields are optional. If pagination=None, no pagination is applied.
|
|
"""
|
|
pagination: Optional[PaginationParams] = None
|
|
|
|
|
|
class PaginatedResult(BaseModel):
|
|
"""
|
|
Internal result structure from interface layer.
|
|
Used when pagination is requested.
|
|
"""
|
|
items: List[Any]
|
|
totalItems: int
|
|
totalPages: int # Calculated as: math.ceil(totalItems / pageSize)
|
|
|
|
|
|
class PaginationMetadata(BaseModel):
|
|
"""
|
|
Pagination metadata returned to frontend for rendering controls.
|
|
Contains all information needed to render pagination UI and handle user interactions.
|
|
"""
|
|
currentPage: int = Field(..., description="Current page number (1-based)")
|
|
pageSize: int = Field(..., description="Number of items per page")
|
|
totalItems: int = Field(..., description="Total number of items across all pages (after filters)")
|
|
totalPages: int = Field(..., description="Total number of pages (calculated from totalItems / pageSize)")
|
|
sort: List[SortField] = Field(..., description="Current sort configuration applied")
|
|
filters: Optional[Dict[str, Any]] = Field(default=None, description="Current filters applied (mirrors request filters)")
|
|
|
|
|
|
class PaginatedResponse(BaseModel, Generic[T]):
|
|
"""
|
|
Response containing paginated data and metadata.
|
|
"""
|
|
items: List[T] = Field(..., description="Array of items for current page")
|
|
pagination: Optional[PaginationMetadata] = Field(..., description="Pagination metadata (None if pagination not applied)")
|
|
|
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
|
|
|
|
def normalize_pagination_dict(pagination_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Normalize pagination dictionary to handle frontend variations.
|
|
Moves top-level "search" field into filters if present.
|
|
|
|
Args:
|
|
pagination_dict: Raw pagination dictionary from frontend
|
|
|
|
Returns:
|
|
Normalized pagination dictionary ready for PaginationParams parsing
|
|
"""
|
|
if not pagination_dict:
|
|
return pagination_dict
|
|
|
|
# Create a copy to avoid modifying the original
|
|
normalized = dict(pagination_dict)
|
|
|
|
# Move top-level "search" into filters if present
|
|
if "search" in normalized:
|
|
if "filters" not in normalized or normalized["filters"] is None:
|
|
normalized["filters"] = {}
|
|
normalized["filters"]["search"] = normalized.pop("search")
|
|
|
|
return normalized
|