refactor: restructure API pagination metadata for better frontend usability

- Create PaginationMetadata model with computed derived fields (total_pages, has_next_page, has_prev_page)
- Update PaginatedListResponse to embed pagination metadata in a separate 'pagination' object
- Add create_pagination_metadata() factory function in utils/pagination.py for consistent computation
- Update all paginated service functions to use new structure:
  - history_service.list_history()
  - blocklist_service.get_import_logs()
  - jail_service.get_jail_banned_ips()
  - ban_mappers.map_domain_dashboard_ban_list_to_response()
- Update response model docstrings with new structure examples
- Update Backend-Development.md documentation with new pagination patterns
- Update test fixtures to work with new response structure

Response shape changes from:
  {"items": [...], "total": 100, "page": 1, "page_size": 50}
To:
  {"items": [...], "pagination": {"page": 1, "page_size": 50, "total": 100, "total_pages": 2, "has_next_page": true, "has_prev_page": false}}

Benefits:
- Frontend receives all pagination state needed for UI controls
- No need for frontend to calculate total_pages or page navigation logic
- Consolidated pagination metadata reduces field sprawl
- OpenAPI schema automatically reflects changes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-30 22:24:42 +02:00
parent 05c3b564ae
commit 73021429f7
9 changed files with 156 additions and 96 deletions

View File

@@ -6,11 +6,11 @@ ensure a uniform API contract.
Standard Pagination Contract:
Query parameters: page (1-based), page_size (1-500)
Response: PaginatedListResponse[T] with items, total, page, page_size
Response: PaginatedListResponse[T] with items and pagination metadata
Usage in routers:
```python
from app.utils.pagination import PAGINATION_DEFAULTS
from app.utils.pagination import PAGINATION_DEFAULTS, create_pagination_metadata
@router.get("/items")
async def get_items(
@@ -21,13 +21,19 @@ Usage in routers:
le=PAGINATION_DEFAULTS["max_page_size"],
),
):
...
items = [...]
total = 100
pagination = create_pagination_metadata(total, page, page_size)
return MyListResponse(items=items, pagination=pagination)
```
"""
from typing import Final
from typing import TYPE_CHECKING, Final
__all__ = ["PAGINATION_DEFAULTS", "get_offset", "compute_total_pages"]
if TYPE_CHECKING:
from app.models.response import PaginationMetadata
__all__ = ["PAGINATION_DEFAULTS", "get_offset", "compute_total_pages", "create_pagination_metadata"]
# Standardized pagination defaults
PAGINATION_DEFAULTS: Final[dict[str, int]] = {
@@ -100,3 +106,45 @@ def compute_total_pages(total: int, page_size: int) -> int:
# Ceiling division: (total + page_size - 1) // page_size
return (total + page_size - 1) // page_size
def create_pagination_metadata(total: int, page: int, page_size: int) -> "PaginationMetadata":
"""Create pagination metadata with computed derived fields.
This factory function computes all pagination-related information in a single
place, ensuring consistency across all paginated endpoints.
Args:
total: Total number of items across all pages.
page: Current page number (1-based).
page_size: Items per page.
Returns:
:class:`~app.models.response.PaginationMetadata` with computed fields:
- total_pages: Computed total number of pages needed
- has_next_page: Whether there is a next page
- has_prev_page: Whether there is a previous page
Example:
```python
metadata = create_pagination_metadata(total=150, page=2, page_size=50)
assert metadata.total_pages == 3
assert metadata.has_next_page is True
assert metadata.has_prev_page is True
```
"""
from app.models.response import PaginationMetadata
total_pages = compute_total_pages(total, page_size)
has_next_page = page < total_pages
has_prev_page = page > 1
return PaginationMetadata(
page=page,
page_size=page_size,
total=total,
total_pages=total_pages,
has_next_page=has_next_page,
has_prev_page=has_prev_page,
)