181 lines
7.1 KiB
Python
181 lines
7.1 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')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Table Grouping models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TableGroupNode(BaseModel):
|
|
"""
|
|
A single node in a user-defined group tree for a FormGeneratorTable.
|
|
|
|
Items belong to exactly one group (no multi-membership).
|
|
Groups can be nested to arbitrary depth via subGroups.
|
|
"""
|
|
id: str
|
|
name: str
|
|
itemIds: List[str] = Field(default_factory=list)
|
|
subGroups: List['TableGroupNode'] = Field(default_factory=list)
|
|
order: int = 0
|
|
isExpanded: bool = True
|
|
|
|
TableGroupNode.model_rebuild()
|
|
|
|
|
|
class TableGrouping(BaseModel):
|
|
"""
|
|
Persisted grouping configuration for one (user, contextKey) pair.
|
|
Stored in table_groupings in poweron_app (auto-created).
|
|
|
|
contextKey convention: API path without /api/ prefix and without trailing slash.
|
|
Examples: "connections", "prompts", "admin/users", "trustee/{instanceId}/documents"
|
|
"""
|
|
id: str
|
|
userId: str
|
|
contextKey: str
|
|
rootGroups: List[TableGroupNode] = Field(default_factory=list)
|
|
updatedAt: Optional[float] = None
|
|
|
|
|
|
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.
|
|
|
|
Grouping extensions (both optional — omit when not using grouping):
|
|
groupId — Scope the request to items belonging to this group.
|
|
The backend resolves it to an itemIds IN-filter before
|
|
applying normal pagination/search/filter logic.
|
|
Also applied for mode=ids and mode=filterValues so that
|
|
bulk-select and filter-dropdowns respect the group scope.
|
|
saveGroupTree — If present the backend persists this tree for the current
|
|
(user, contextKey) pair *before* fetching, then returns
|
|
the confirmed tree in the response groupTree field.
|
|
Omit on every request that does not change the group tree.
|
|
"""
|
|
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"""
|
|
)
|
|
groupId: Optional[str] = Field(
|
|
default=None,
|
|
description="Scope request to items of this group (resolved server-side to itemIds IN-filter)",
|
|
)
|
|
saveGroupTree: Optional[List[Dict[str, Any]]] = Field(
|
|
default=None,
|
|
description="If set, persist this group tree before fetching (optimistic save)",
|
|
)
|
|
|
|
|
|
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.
|
|
|
|
groupTree is included when the endpoint supports table grouping and the
|
|
current user has a saved group tree for the requested contextKey.
|
|
It is None when grouping is not configured for the endpoint or the user
|
|
has not created any groups yet. Frontend must treat None as an empty tree.
|
|
"""
|
|
items: List[T] = Field(..., description="Array of items for current page")
|
|
pagination: Optional[PaginationMetadata] = Field(..., description="Pagination metadata (None if pagination not applied)")
|
|
groupTree: Optional[List[TableGroupNode]] = Field(
|
|
default=None,
|
|
description="Current group tree for this (user, contextKey) pair — None if no grouping configured",
|
|
)
|
|
|
|
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.
|
|
Grouping fields (groupId, saveGroupTree) are passed through as-is.
|
|
|
|
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)
|
|
|
|
# Ensure required fields have sensible defaults
|
|
if "page" not in normalized:
|
|
normalized["page"] = 1
|
|
if "pageSize" not in normalized:
|
|
normalized["pageSize"] = 25
|
|
|
|
# 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")
|
|
|
|
# groupId / saveGroupTree are valid PaginationParams fields — pass through unchanged.
|
|
# No transformation needed; Pydantic will validate them.
|
|
|
|
return normalized
|