Add cursor pagination to admin lists
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.contracts import ILogger
|
||||
@@ -7,6 +8,15 @@ from src.application.domain.exceptions import NotFoundException
|
||||
from src.infrastructure.database.decorators import transactional
|
||||
|
||||
|
||||
def _parse_cursor(cursor: str | None) -> datetime | None:
|
||||
if not cursor:
|
||||
return None
|
||||
parsed = datetime.fromisoformat(cursor.replace('Z','+00:00'))
|
||||
if parsed.tzinfo is None:
|
||||
return parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed
|
||||
|
||||
|
||||
class ListUserOrdersCommand:
|
||||
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
|
||||
self._unit_of_work = unit_of_work
|
||||
@@ -14,11 +24,12 @@ class ListUserOrdersCommand:
|
||||
|
||||
|
||||
@transactional
|
||||
async def __call__(self, *, user_id: str, limit: int, offset: int) -> tuple[list[OrderEntity], int]:
|
||||
async def __call__(self, *, user_id: str, limit: int, offset: int, cursor: str | None = None) -> tuple[list[OrderEntity], int]:
|
||||
orders = await self._unit_of_work.order_repository.list_by_user_id(
|
||||
user_id=user_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
cursor_created_at=_parse_cursor(cursor),
|
||||
)
|
||||
total = await self._unit_of_work.order_repository.count_all(search=user_id)
|
||||
return orders, total
|
||||
@@ -32,11 +43,12 @@ class ListOrdersCommand:
|
||||
|
||||
|
||||
@transactional
|
||||
async def __call__(self, *, limit: int, offset: int, search: str | None = None) -> tuple[list[OrderEntity], int]:
|
||||
async def __call__(self, *, limit: int, offset: int, search: str | None = None, cursor: str | None = None) -> tuple[list[OrderEntity], int]:
|
||||
orders = await self._unit_of_work.order_repository.list_all(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
search=search
|
||||
search=search,
|
||||
cursor_created_at=_parse_cursor(cursor),
|
||||
)
|
||||
|
||||
total = await self._unit_of_work.order_repository.count_all(search=search)
|
||||
@@ -57,13 +69,15 @@ class ListOrdersByDateCommand:
|
||||
limit: int,
|
||||
offset: int,
|
||||
year: int,
|
||||
month: int | None = None
|
||||
month: int | None = None,
|
||||
cursor: str | None = None,
|
||||
) -> tuple[list[OrderEntity], int, Decimal]:
|
||||
orders = await self._unit_of_work.order_repository.list_by_date(
|
||||
year=year,
|
||||
month=month,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
offset=offset,
|
||||
cursor_created_at=_parse_cursor(cursor),
|
||||
)
|
||||
|
||||
total = await self._unit_of_work.order_repository.count_by_month(year=year, month=month)
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.contracts import ILogger
|
||||
from src.application.domain.entities.user import UserEntity
|
||||
from src.infrastructure.database.decorators import transactional
|
||||
|
||||
|
||||
def _parse_cursor(cursor: str | None) -> datetime | None:
|
||||
if not cursor:
|
||||
return None
|
||||
parsed = datetime.fromisoformat(cursor.replace('Z','+00:00'))
|
||||
if parsed.tzinfo is None:
|
||||
return parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed
|
||||
|
||||
|
||||
class ListUsersCommand:
|
||||
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger):
|
||||
self._unit_of_work = unit_of_work
|
||||
@@ -18,11 +29,13 @@ class ListUsersCommand:
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
search: str | None = None,
|
||||
cursor: str | None = None,
|
||||
) -> tuple[list[UserEntity], int]:
|
||||
items = await self._unit_of_work.user_repository.list_all(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
search=search,
|
||||
cursor_created_at=_parse_cursor(cursor),
|
||||
)
|
||||
total = await self._unit_of_work.user_repository.count_all(search=search)
|
||||
return items, total
|
||||
|
||||
@@ -71,15 +71,19 @@ class OrderRepository(IOrderRepository):
|
||||
*,
|
||||
user_id: str,
|
||||
limit: int,
|
||||
offset: int
|
||||
offset: int,
|
||||
cursor_created_at=None
|
||||
) -> list[OrderEntity]:
|
||||
stmt=(
|
||||
select(OrderModel)
|
||||
.where(OrderModel.user_id==user_id)
|
||||
.order_by(desc(OrderModel.created_at))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
if cursor_created_at is not None:
|
||||
stmt = stmt.where(OrderModel.created_at < cursor_created_at)
|
||||
else:
|
||||
stmt = stmt.offset(offset)
|
||||
result=await self._session.scalars(stmt)
|
||||
return [self._to_entity(model) for model in result.all()]
|
||||
|
||||
@@ -89,14 +93,18 @@ class OrderRepository(IOrderRepository):
|
||||
*,
|
||||
limit: int,
|
||||
offset: int,
|
||||
search: str | None = None
|
||||
search: str | None = None,
|
||||
cursor_created_at=None
|
||||
) -> list[OrderEntity]:
|
||||
stmt=(
|
||||
select(OrderModel)
|
||||
.order_by(desc(OrderModel.created_at))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
if cursor_created_at is not None:
|
||||
stmt = stmt.where(OrderModel.created_at < cursor_created_at)
|
||||
else:
|
||||
stmt = stmt.offset(offset)
|
||||
search_filter = self._search_filter(search)
|
||||
if search_filter is not None:
|
||||
stmt = stmt.where(search_filter)
|
||||
@@ -111,6 +119,7 @@ class OrderRepository(IOrderRepository):
|
||||
month: int,
|
||||
limit: int,
|
||||
offset: int,
|
||||
cursor_created_at=None,
|
||||
) -> list[OrderEntity]:
|
||||
stmt=(
|
||||
select(OrderModel)
|
||||
@@ -120,8 +129,11 @@ class OrderRepository(IOrderRepository):
|
||||
)
|
||||
.order_by(desc(OrderModel.created_at))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
if cursor_created_at is not None:
|
||||
stmt = stmt.where(OrderModel.created_at < cursor_created_at)
|
||||
else:
|
||||
stmt = stmt.offset(offset)
|
||||
res = await self._session.execute(stmt)
|
||||
return [self._to_entity(model) for model in res.scalars().all()]
|
||||
|
||||
|
||||
@@ -156,14 +156,18 @@ class UserRepository(IUserRepository):
|
||||
limit: int,
|
||||
offset: int,
|
||||
search: str | None = None,
|
||||
cursor_created_at=None,
|
||||
) -> list[UserEntity]:
|
||||
stmt = (
|
||||
select(UserModel)
|
||||
.where(UserModel.is_deleted.is_(False))
|
||||
.order_by(UserModel.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
if cursor_created_at is not None:
|
||||
stmt = stmt.where(UserModel.created_at < cursor_created_at)
|
||||
else:
|
||||
stmt = stmt.offset(offset)
|
||||
search_filter = self._search_filter(search)
|
||||
if search_filter is not None:
|
||||
stmt = stmt.where(search_filter)
|
||||
|
||||
@@ -26,14 +26,16 @@ orders_router = APIRouter(prefix='/orders', tags=['orders'])
|
||||
async def list_orders(
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
cursor: str | None = Query(default=None),
|
||||
q: str | None = Query(default=None, min_length=1, max_length=255),
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: ListOrdersCommand = Depends(get_list_orders_command),
|
||||
):
|
||||
items, total = await command(limit=limit, offset=offset, search=q)
|
||||
items, total = await command(limit=limit, offset=offset, search=q, cursor=cursor)
|
||||
return OrdersResponse(
|
||||
items=[order_to_response(x) for x in items],
|
||||
total=total,
|
||||
next_cursor=items[-1].created_at.isoformat() if items and items[-1].created_at else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -75,12 +77,14 @@ async def list_orders_by_date(
|
||||
month: int = Query(..., ge=1, le=12),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
cursor: str | None = Query(default=None),
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: ListOrdersByDateCommand = Depends(get_list_orders_by_date_command),
|
||||
):
|
||||
orders, total, summary = await command(year=year, month=month, limit=limit, offset=offset)
|
||||
orders, total, summary = await command(year=year, month=month, limit=limit, offset=offset, cursor=cursor)
|
||||
return OrdersByDateResponse(
|
||||
items=[order_to_response(o) for o in orders],
|
||||
total=total,
|
||||
summed_service_fees=summary,
|
||||
next_cursor=orders[-1].created_at.isoformat() if orders and orders[-1].created_at else None,
|
||||
)
|
||||
|
||||
@@ -54,14 +54,16 @@ users_router = APIRouter(prefix='/users', tags=['users'])
|
||||
async def list_users(
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
cursor: str | None = Query(default=None),
|
||||
q: str | None = Query(default=None, min_length=1, max_length=255),
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: ListUsersCommand = Depends(get_list_users_command),
|
||||
):
|
||||
items, total = await command(limit=limit, offset=offset, search=q)
|
||||
items, total = await command(limit=limit, offset=offset, search=q, cursor=cursor)
|
||||
return UserListResponse(
|
||||
items=[user_to_response(x) for x in items],
|
||||
total=total,
|
||||
next_cursor=items[-1].created_at.isoformat() if items and items[-1].created_at else None,
|
||||
)
|
||||
|
||||
@users_router.get('/{user_id}', response_model=UserResponse)
|
||||
@@ -88,13 +90,15 @@ async def list_user_orders(
|
||||
user_id: str,
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
cursor: str | None = Query(default=None),
|
||||
auth: AdminAuthContext = Depends(require_admin_access),
|
||||
command: ListUserOrdersCommand = Depends(get_list_user_orders_command),
|
||||
):
|
||||
orders, total = await command(user_id=user_id, limit=limit, offset=offset)
|
||||
orders, total = await command(user_id=user_id, limit=limit, offset=offset, cursor=cursor)
|
||||
return OrdersResponse(
|
||||
items=[order_to_response(o) for o in orders],
|
||||
total=total,
|
||||
next_cursor=orders[-1].created_at.isoformat() if orders and orders[-1].created_at else None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -41,8 +41,10 @@ class OrderResponse(BaseModel):
|
||||
class OrdersResponse(BaseModel):
|
||||
items: list[OrderResponse]
|
||||
total: int
|
||||
next_cursor: str | None = None
|
||||
|
||||
class OrdersByDateResponse(BaseModel):
|
||||
items: list[OrderResponse]
|
||||
total: int
|
||||
summed_service_fees: Decimal
|
||||
next_cursor: str | None = None
|
||||
@@ -27,3 +27,4 @@ class UserResponse(BaseModel):
|
||||
class UserListResponse(BaseModel):
|
||||
items: list[UserResponse]
|
||||
total: int
|
||||
next_cursor: str | None = None
|
||||
|
||||
Reference in New Issue
Block a user