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" } includeTotal := c.QueryParam("include_total") == "true" return &Pagination{ Limit: limit, Offset: offset, Cursor: cursor, SortBy: sortBy, SortOrder: sortOrder, IncludeTotal: includeTotal, }, 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) }