hybrid pagination

This commit is contained in:
Mazyar
2026-05-17 17:21:36 +03:30
parent 42d0a47226
commit 6dac5b482a
39 changed files with 696 additions and 386 deletions
+105
View File
@@ -0,0 +1,105 @@
package response
import (
"errors"
"fmt"
"strconv"
"github.com/labstack/echo/v4"
)
const (
// DefaultListLimit is the default page size for admin list endpoints.
DefaultListLimit = 10
// MaxListLimit is the maximum allowed page size.
MaxListLimit = 100
)
var (
// ErrInvalidPagination is returned when pagination query parameters are invalid.
ErrInvalidPagination = errors.New("invalid pagination parameters")
// ErrPaginationConflict is returned when cursor and offset are both supplied.
ErrPaginationConflict = errors.New("cursor and offset pagination cannot be used together")
)
// NewPagination parses and validates hybrid pagination query parameters from the request.
func NewPagination(c echo.Context) (*Pagination, error) {
limit := DefaultListLimit
if limitStr := c.QueryParam("limit"); limitStr != "" {
parsed, err := strconv.Atoi(limitStr)
if err != nil {
return nil, fmt.Errorf("%w: limit must be an integer", ErrInvalidPagination)
}
limit = parsed
}
if limit < 1 || limit > MaxListLimit {
return nil, fmt.Errorf("%w: limit must be between 1 and %d", ErrInvalidPagination, MaxListLimit)
}
cursor := c.QueryParam("cursor")
offsetParam := c.QueryParam("offset")
if cursor != "" && offsetParam != "" {
return nil, ErrPaginationConflict
}
offset := 0
if offsetParam != "" {
parsed, err := strconv.Atoi(offsetParam)
if err != nil {
return nil, fmt.Errorf("%w: offset must be an integer", ErrInvalidPagination)
}
if parsed < 0 {
return nil, fmt.Errorf("%w: offset cannot be negative", ErrInvalidPagination)
}
offset = parsed
}
sortBy := c.QueryParam("sort_by")
if sortBy == "" {
sortBy = "created_at"
}
sortOrder := c.QueryParam("sort_order")
if sortOrder == "" {
sortOrder = "desc"
}
return &Pagination{
Limit: limit,
Offset: offset,
Cursor: cursor,
SortBy: sortBy,
SortOrder: sortOrder,
}, nil
}
// ListMeta builds pagination metadata for a list page.
// pageOffset is the zero-based index of the first item in this page (from offset or cursor).
func (p *Pagination) ListMeta(total int64, nextCursor string, hasMore bool, pageOffset int) *Meta {
meta := &Meta{
Limit: p.Limit,
Offset: pageOffset,
HasMore: hasMore,
}
if hasMore && nextCursor != "" {
meta.NextCursor = nextCursor
}
if total >= 0 {
meta.Total = int(total)
if p.Limit > 0 {
meta.Page = pageOffset/p.Limit + 1
meta.Pages = int((total + int64(p.Limit) - 1) / int64(p.Limit))
}
}
return meta
}
// ListMetaFromPage builds metadata from a paginated repository result.
func (p *Pagination) ListMetaFromPage(total int64, nextCursor string, hasMore bool, pageOffset int) *Meta {
return p.ListMeta(total, nextCursor, hasMore, pageOffset)
}
// IsPaginationError reports whether err is a client-facing pagination error.
func IsPaginationError(err error) bool {
return errors.Is(err, ErrInvalidPagination) || errors.Is(err, ErrPaginationConflict)
}