mirror of
https://github.com/zap-proto/fiber.git
synced 2026-10-30 15:06:23 +00:00
191 lines
5.1 KiB
Go
191 lines
5.1 KiB
Go
package paginate
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"github.com/gofiber/utils/v2"
|
|
)
|
|
|
|
// ErrCursorEncode is returned when cursor values cannot be encoded.
|
|
var ErrCursorEncode = errors.New("paginate: failed to encode cursor values")
|
|
|
|
// SortOrder represents sort order.
|
|
type SortOrder string
|
|
|
|
const (
|
|
ASC SortOrder = "asc"
|
|
DESC SortOrder = "desc"
|
|
)
|
|
|
|
// SortField represents a sort field with direction.
|
|
type SortField struct {
|
|
Field string `json:"field"`
|
|
Order SortOrder `json:"order"`
|
|
}
|
|
|
|
// SortOrderFromString returns a SortOrder from a string (case-insensitive).
|
|
func SortOrderFromString(s string) SortOrder {
|
|
if utils.EqualFold(s, "desc") {
|
|
return DESC
|
|
}
|
|
return ASC
|
|
}
|
|
|
|
// PageInfo contains pagination information.
|
|
type PageInfo struct {
|
|
cursorData map[string]any
|
|
jsonMarshal utils.JSONMarshal
|
|
jsonUnmarshal utils.JSONUnmarshal
|
|
Cursor string `json:"cursor,omitempty"`
|
|
NextCursor string `json:"next_cursor,omitempty"`
|
|
Sort []SortField `json:"sort"`
|
|
Page int `json:"page"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
HasMore bool `json:"has_more,omitempty"`
|
|
}
|
|
|
|
// NewPageInfo creates a new PageInfo.
|
|
func NewPageInfo(page, limit, offset int, sort []SortField) *PageInfo {
|
|
return &PageInfo{
|
|
Page: page,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
Sort: sort,
|
|
}
|
|
}
|
|
|
|
// Start returns the start index based on page/limit or offset.
|
|
func (p *PageInfo) Start() int {
|
|
if p.Offset > 0 {
|
|
return p.Offset
|
|
}
|
|
if p.Page < 1 {
|
|
return 0
|
|
}
|
|
return (p.Page - 1) * p.Limit
|
|
}
|
|
|
|
// SortBy adds a sort field. Chainable.
|
|
func (p *PageInfo) SortBy(field string, order SortOrder) *PageInfo {
|
|
p.Sort = append(p.Sort, SortField{Field: field, Order: order})
|
|
return p
|
|
}
|
|
|
|
// NextPageURLWithKeys returns the URL for the next page using custom query keys.
|
|
func (p *PageInfo) NextPageURLWithKeys(baseURL, pageKey, limitKey string) string {
|
|
return buildPaginationURL(baseURL, pageKey, utils.FormatInt(int64(p.Page+1)), limitKey, utils.FormatInt(int64(p.Limit)))
|
|
}
|
|
|
|
// NextPageURL returns the URL for the next page.
|
|
func (p *PageInfo) NextPageURL(baseURL string) string {
|
|
return p.NextPageURLWithKeys(baseURL, "page", "limit")
|
|
}
|
|
|
|
// PreviousPageURLWithKeys returns the URL for the previous page using custom query keys.
|
|
// Returns empty string if on page 1.
|
|
func (p *PageInfo) PreviousPageURLWithKeys(baseURL, pageKey, limitKey string) string {
|
|
if p.Page > 1 {
|
|
return buildPaginationURL(baseURL, pageKey, utils.FormatInt(int64(p.Page-1)), limitKey, utils.FormatInt(int64(p.Limit)))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// PreviousPageURL returns the URL for the previous page.
|
|
// Returns empty string if on page 1.
|
|
func (p *PageInfo) PreviousPageURL(baseURL string) string {
|
|
return p.PreviousPageURLWithKeys(baseURL, "page", "limit")
|
|
}
|
|
|
|
// NextCursorURLWithKeys returns the URL for the next cursor page using custom query keys.
|
|
// Returns empty string if HasMore is false.
|
|
func (p *PageInfo) NextCursorURLWithKeys(baseURL, cursorKey, limitKey string) string {
|
|
if !p.HasMore {
|
|
return ""
|
|
}
|
|
return buildPaginationURL(baseURL, cursorKey, p.NextCursor, limitKey, utils.FormatInt(int64(p.Limit)))
|
|
}
|
|
|
|
// NextCursorURL returns the URL for the next cursor page.
|
|
// Returns empty string if HasMore is false.
|
|
func (p *PageInfo) NextCursorURL(baseURL string) string {
|
|
return p.NextCursorURLWithKeys(baseURL, "cursor", "limit")
|
|
}
|
|
|
|
// buildPaginationURL parses baseURL and sets/replaces two query parameters,
|
|
// preserving any existing query string values.
|
|
func buildPaginationURL(baseURL, pageParam, pageValue, limitParam, limitValue string) string {
|
|
u, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return baseURL
|
|
}
|
|
q := u.Query()
|
|
q.Set(pageParam, pageValue)
|
|
q.Set(limitParam, limitValue)
|
|
u.RawQuery = q.Encode()
|
|
return u.String()
|
|
}
|
|
|
|
// CursorValues returns the decoded cursor key-value map.
|
|
// If the cursor was parsed by the middleware, the pre-parsed data is returned.
|
|
// Otherwise it decodes the opaque cursor string.
|
|
// Returns nil if cursor is empty or invalid.
|
|
func (p *PageInfo) CursorValues() map[string]any {
|
|
if p.cursorData != nil {
|
|
return p.cursorData
|
|
}
|
|
|
|
if p.Cursor == "" {
|
|
return nil
|
|
}
|
|
|
|
if len(p.Cursor) > maxCursorLen {
|
|
return nil
|
|
}
|
|
|
|
data, err := base64.RawURLEncoding.DecodeString(p.Cursor)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var values map[string]any
|
|
unmarshal := p.jsonUnmarshal
|
|
if unmarshal == nil {
|
|
unmarshal = json.Unmarshal
|
|
}
|
|
if err := unmarshal(data, &values); err != nil {
|
|
return nil
|
|
}
|
|
|
|
p.cursorData = values
|
|
|
|
return values
|
|
}
|
|
|
|
// SetNextCursor encodes a key-value map into an opaque cursor token
|
|
// and sets both NextCursor and HasMore on the PageInfo.
|
|
func (p *PageInfo) SetNextCursor(values map[string]any) error {
|
|
marshal := p.jsonMarshal
|
|
if marshal == nil {
|
|
marshal = json.Marshal
|
|
}
|
|
data, err := marshal(values)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrCursorEncode, err)
|
|
}
|
|
|
|
encoded := base64.RawURLEncoding.EncodeToString(data)
|
|
if len(encoded) > maxCursorLen {
|
|
return fmt.Errorf("%w: cursor token exceeds maximum length (%d)", ErrCursorEncode, maxCursorLen)
|
|
}
|
|
|
|
p.NextCursor = encoded
|
|
p.HasMore = true
|
|
|
|
return nil
|
|
}
|