pagination

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package pagination provides generic, ORM-agnostic types and utilities for paginated API responses.

The package defines request binding structs, a generic result type, and sort validation — all with zero external dependencies. Actual query execution (COUNT + LIMIT/OFFSET) lives in ORM-specific adapters such as the github.com/credo-go/credo/store/sqldb.SelectQuery.Page terminal.

Request Binding

PageRequest and SortRequest are embeddable structs that work with BindQuery via query tags:

type BranchFilter struct {
    pagination.PageRequest
    pagination.SortRequest
    Search string `query:"search"`
}

PageRequest.Normalize and PageRequest.Validate are forgiving input-policy operations: they apply defaults and clamp PerPage in place. Execution is a separate, strict boundary. PageRequest.Offset returns (int, error), never mutates or normalizes the request, and wraps ErrInvalidPageRequest when Page or PerPage is non-positive or the multiplication would overflow int. ORM adapters may impose a narrower execution range; sqldb additionally enforces Bun v1.2.18's signed-int32 LIMIT/OFFSET representation before COUNT.

Page Construction and Mapping

A Page carries pagination metadata (Total, Page, PerPage, TotalPages) that is computed once — by NewPage or by a query terminal such as sqldb's SelectQuery.Page — and never recomputed or hand-copied afterward. NewPage computes the ceiling without an addition that can overflow at MaxInt64:

page := pagination.NewPage(dtos, total, filter.Page, filter.PerPage)

To turn a page of database models into a page of response DTOs, use Page.Map; it transforms the records and carries the metadata over:

dtoPage := modelPage.Map(func(m Model) DTO { return toDTO(m) })

JSON Response

Page.ToDataMeta splits the page into a data slice and a Meta struct suitable for JSON serialization:

data, meta := page.ToDataMeta()

Sort Safety

SortRequest.ValidateSort rejects unknown sort fields via a whitelist, preventing SQL injection through ORDER BY clauses:

col, ord := filter.SortRequest.ValidateSort(sortCfg)

Index

Constants

View Source
const (
	DefaultPage    = 1
	DefaultPerPage = 50
	MinPerPage     = 1
	MaxPerPage     = 50
)

Default pagination constants.

Variables

View Source
var ErrInvalidPageRequest = errors.New("pagination: invalid page request")

ErrInvalidPageRequest reports that pagination execution values violate the strict Page/PerPage or offset invariants. Normalize and Validate apply input defaults; Offset uses this error instead of normalizing or clamping.

Functions

This section is empty.

Types

type Meta

type Meta struct {
	Total      int64 `json:"total_count"`
	Page       int   `json:"page"`
	PerPage    int   `json:"per_page"`
	TotalPages int64 `json:"total_pages"`
	HasNext    bool  `json:"has_next"`
	HasPrev    bool  `json:"has_prev"`
}

Meta is pagination metadata for JSON serialization.

type Page

type Page[T any] struct {
	Records    []T
	Total      int64
	Page       int
	PerPage    int
	TotalPages int64
}

Page is a generic paginated result. Its pagination metadata (Total, Page, PerPage, TotalPages) is computed once — by NewPage or by a query terminal such as sqldb's SelectQuery.Page — and carried unchanged through any later reshaping: Page.Map turns a Page of database models into a Page of response DTOs while preserving that metadata, so it is never recomputed or hand-copied.

func NewEmpty

func NewEmpty[T any]() *Page[T]

NewEmpty creates an empty Page with default pagination values.

func NewPage

func NewPage[T any](records []T, total int64, page, perPage int) *Page[T]

NewPage creates a Page from raw values. TotalPages is computed automatically. A nil records slice is replaced with an empty slice for safe JSON serialization ([] instead of null).

func (*Page[T]) HasNext

func (p *Page[T]) HasNext() bool

HasNext reports whether there is a page after the current one.

func (*Page[T]) HasPrev

func (p *Page[T]) HasPrev() bool

HasPrev reports whether there is a page before the current one.

func (*Page[T]) Map

func (p *Page[T]) Map[U any](fn func(T) U) *Page[U]

Map returns a new Page[U] whose records are produced by applying fn to each record of p, carrying the pagination metadata (Total, Page, PerPage, TotalPages) over unchanged. It is the canonical way to turn a page of database models into a page of response DTOs:

modelPage, err := db.Select().Where(...).Page[Model](ctx, req)
if err != nil {
	return err
}
dtoPage := modelPage.Map(func(m Model) DTO { return toDTO(m) })

fn is applied once per record, in order, and must be pure: for a conversion that can fail, map in the service layer and build the page with NewPage. fn must not be nil — a nil mapping is always a programming error, so Map panics on a nil fn even when the page is empty, rather than silently returning an empty Page[U].

func (*Page[T]) ToDataMeta

func (p *Page[T]) ToDataMeta() ([]T, *Meta)

ToDataMeta splits the page into a records slice and a Meta struct for JSON response serialization.

type PageRequest

type PageRequest struct {
	Page    int `query:"page"`
	PerPage int `query:"per_page"`
}

PageRequest is an embeddable struct for pagination query parameters. It works with BindQuery via query tags.

func (*PageRequest) Normalize

func (r *PageRequest) Normalize()

Normalize applies pagination defaults and limits in place. Zero or negative values are replaced with defaults. PerPage is clamped to [MinPerPage, MaxPerPage]. For a custom upper bound, use PageRequest.NormalizeWithMax.

func (*PageRequest) NormalizeWithMax

func (r *PageRequest) NormalizeWithMax(maxPerPage int)

NormalizeWithMax is like PageRequest.Normalize but clamps PerPage to the given maximum instead of the package default MaxPerPage. maxPerPage values <= 0 fall back to MaxPerPage.

PageRequest's own Validate — and therefore BindQuery's auto-validation — always applies the package default. To raise the cap for a specific endpoint, shadow Validate on the embedding struct:

type ListQuery struct {
	pagination.PageRequest
}

func (q *ListQuery) Validate() error {
	q.NormalizeWithMax(200)
	return nil
}

func (PageRequest) Offset

func (r PageRequest) Offset() (int, error)

Offset returns the zero-based offset for LIMIT/OFFSET queries. Unlike Normalize and Validate, Offset is strict and never mutates, defaults, or clamps the request. Page and PerPage must both be positive, and their offset product must fit in an int; violations wrap ErrInvalidPageRequest. Adapters must additionally enforce any narrower driver or ORM limit.

func (*PageRequest) Validate

func (r *PageRequest) Validate() error

Validate implements [validation.Validatable] so that BindQuery automatically normalizes pagination values after decoding.

type SortConfig

type SortConfig struct {
	DefaultField  string
	DefaultOrder  string            // "ASC" or "DESC"
	AllowedFields map[string]string // API field name → DB column name
}

SortConfig defines allowed sort fields for SQL injection prevention. AllowedFields maps API field names to DB column names.

type SortRequest

type SortRequest struct {
	SortBy    string `query:"sort_by"`
	SortOrder string `query:"sort_order"`
}

SortRequest is an embeddable struct for sort query parameters.

func (*SortRequest) ValidateSort

func (r *SortRequest) ValidateSort(cfg *SortConfig) (column, order string)

ValidateSort validates sort parameters against allowed fields. It returns the DB column name and normalized sort order (uppercase). Unknown sort fields fall back to cfg.DefaultField. Invalid sort orders fall back to cfg.DefaultOrder. If cfg is nil, both return values are empty strings. A nil receiver is safe to call.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL