pagination

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 29, 2026 License: MIT Imports: 4 Imported by: 0

README

pagination

Offset-based pagination for Go + GORM with a fluent filter/sort API.

Features

  • Page struct that embeds directly into request DTOs and binds from query-string params
  • Generic Result[T] response envelope with metadata (total, total_pages, has_next, has_prev)
  • Fluent FilterBuilder with conditional helpers (WhereIf)
  • Fluent SortBuilder and a ParseSort helper for user-supplied sort strings
  • Single Scope function that plugs into any *gorm.DB chain

Installation

go get github.com/raykavin/gobox/pagination

Usage

1. Embed Page in your request DTO
import "github.com/raykavin/gobox/pagination"

type ListTransactionsRequest struct {
    pagination.Page
    Status    string `form:"status"`
    MinAmount string `form:"min_amount"`
    Sort      string `form:"sort"` // e.g. "created_at desc,amount asc"
}

Query-string params page and per_page bind automatically. Missing or invalid values are normalised to safe defaults.

2. Build filters
fb := pagination.NewFilterBuilder().
    WhereIf(req.Status != "", "status", pagination.Eq, req.Status)

if req.MinAmount != "" {
    if v, err := strconv.ParseFloat(req.MinAmount, 64); err == nil {
        fb.Where("amount", pagination.Gte, v)
    }
}

filters := fb.Build()

WhereIf only appends the condition when the first argument (cond) is true, making optional filters concise.

3. Build sorts

Parse from a user-supplied string:

sorts := pagination.ParseSort(req.Sort) // "name asc,created_at desc"

Or build programmatically with fallback defaults:

if len(sorts) == 0 {
    sorts = pagination.NewSortBuilder().
        OrderBy("created_at", pagination.Desc).
        Build()
}
4. Assemble a Query and execute
query := pagination.NewQuery(req.Page, filters, sorts)

var rows []Transaction
var total int64

db.Model(&Transaction{}).
    Scopes(pagination.Scope(query, &total)).
    Find(&rows)

Scope applies filters, sorts, counts the total rows (without LIMIT/OFFSET) and then applies LIMIT/OFFSET in one shot.

5. Return the response envelope
result := pagination.NewResult(rows, int(total), query.Page)
c.JSON(http.StatusOK, result)

JSON output:

{
  "data": [...],
  "total": 42,
  "page": 2,
  "per_page": 20,
  "total_pages": 3,
  "has_next": true,
  "has_prev": true
}

Full handler example (Gin)

package main

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    "github.com/raykavin/gobox/pagination"
    "gorm.io/gorm"
)

type Transaction struct {
    ID          uint    `json:"id"          gorm:"primaryKey"`
    Description string  `json:"description"`
    Amount      float64 `json:"amount"`
    Status      string  `json:"status"`
}

type ListTransactionsRequest struct {
    pagination.Page
    Status    string `form:"status"`
    MinAmount string `form:"min_amount"`
    Sort      string `form:"sort"`
}

type TransactionHandler struct{ db *gorm.DB }

func (h *TransactionHandler) List(c *gin.Context) {
    var req ListTransactionsRequest
    if err := c.ShouldBindQuery(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    fb := pagination.NewFilterBuilder().
        WhereIf(req.Status != "", "status", pagination.Eq, req.Status)

    if req.MinAmount != "" {
        if v, err := strconv.ParseFloat(req.MinAmount, 64); err == nil {
            fb.Where("amount", pagination.Gte, v)
        }
    }

    sorts := pagination.ParseSort(req.Sort)
    if len(sorts) == 0 {
        sorts = pagination.NewSortBuilder().
            OrderBy("created_at", pagination.Desc).
            Build()
    }

    query := pagination.NewQuery(req.Page, fb.Build(), sorts)

    var rows []Transaction
    var total int64
    h.db.Model(&Transaction{}).
        Scopes(pagination.Scope(query, &total)).
        Find(&rows)

    c.JSON(http.StatusOK, pagination.NewResult(rows, int(total), query.Page))
}

Reference

Constants
Constant Value Description
DefPage 1 Default page number
DefPerPage 20 Default items per page
MaxPerPage 100 Maximum allowed value for per_page
Filter operators
Operator SQL equivalent
Eq =
Neq <>
Gt >
Gte >=
Lt <
Lte <=
Like LIKE '%…%'
ILike ILIKE '%…%'
In IN (?)
NotIn NOT IN (?)
IsNull IS NULL
IsNotNull IS NOT NULL
Sort directions
Constant SQL equivalent
Asc ASC
Desc DESC
ParseSort string format

A comma-separated list of field [asc|desc] tokens. Direction is case-insensitive and defaults to ASC when omitted.

"created_at desc,name asc,amount"

License

Same as the parent module.

Documentation

Overview

Package pagination provides building blocks for cursor-free, offset-based pagination with filtering and sorting, designed to work seamlessly with GORM.

Overview

The package exposes three concerns:

  • Page / Result request params and response envelope
  • Filter / Sort builders fluent APIs to compose WHERE and ORDER BY clauses
  • Scope a GORM scope that wires everything together in a single call

Quick start

// 1. Embed Page in your request DTO so it binds from query-string params.
type ListReq struct {
    pagination.Page
    Status string `form:"status"`
}

// 2. Build filters and sorts.
filters := pagination.NewFilterBuilder().
    WhereIf(req.Status != "", "status", pagination.Eq, req.Status).
    Build()

sorts := pagination.ParseSort(req.Sort) // "created_at desc,amount asc"
if len(sorts) == 0 {
    sorts = pagination.NewSortBuilder().
        OrderBy("created_at", pagination.Desc).
        Build()
}

// 3. Assemble a Query (normalises the page automatically).
query := pagination.NewQuery(req.Page, filters, sorts)

// 4. Execute with GORM.
var rows []MyModel
var total int64
db.Model(&MyModel{}).
    Scopes(pagination.Scope(query, &total)).
    Find(&rows)

// 5. Build the response envelope.
result := pagination.NewResult(rows, int(total), query.Page)

Defaults and limits

When the caller omits pagination params, Page.Normalize applies safe defaults:

Supported filter operators

pagination.Eq        // =
pagination.Neq       // <>
pagination.Gt        // >
pagination.Gte       // >=
pagination.Lt        // <
pagination.Lte       // <=
pagination.Like      // LIKE  (value is automatically wrapped with %)
pagination.ILike     // ILIKE (value is automatically wrapped with %)
pagination.In        // IN (?)
pagination.NotIn     // NOT IN (?)
pagination.IsNull    // IS NULL
pagination.IsNotNull // IS NOT NULL

Index

Constants

View Source
const (
	DefPage    = 1
	DefPerPage = 20
	MaxPerPage = 100
)

Variables

This section is empty.

Functions

func Scope

func Scope(q Query, total *int64) func(*gorm.DB) *gorm.DB

Scope returns a GORM scope that applies filters, sorts and pagination from a Query

var users []User
var total int64

q := paginator.NewQuery(page, filters, sorts)

db.Model(&User{}).
    Scopes(paginator.Scope(q, &total)).
    Find(&users)

result := paginator.NewResult(users, int(total), q.Page)

Types

type Filter

type Filter struct {
	Field string
	Op    Operator
	Value any
}

Filter represents a single WHERE condition

type FilterBuilder

type FilterBuilder struct {
	// contains filtered or unexported fields
}

FilterBuilder provides a fluent API to build []Filter

filters := paginator.NewFilterBuilder()
    Where("status", paginator.Eq, "active")
    Where("amount", paginator.Gte, 100)
    Build()

func NewFilterBuilder

func NewFilterBuilder() *FilterBuilder

func (*FilterBuilder) Build

func (b *FilterBuilder) Build() []Filter

func (*FilterBuilder) Where

func (b *FilterBuilder) Where(field string, op Operator, value any) *FilterBuilder

func (*FilterBuilder) WhereIf

func (b *FilterBuilder) WhereIf(cond bool, field string, op Operator, value any) *FilterBuilder

type Operator

type Operator string
const (
	Eq        Operator = "="
	Neq       Operator = "<>"
	Gt        Operator = ">"
	Gte       Operator = ">="
	Lt        Operator = "<"
	Lte       Operator = "<="
	Like      Operator = "LIKE"
	ILike     Operator = "ILIKE"
	In        Operator = "IN"
	NotIn     Operator = "NOT IN"
	IsNull    Operator = "IS NULL"
	IsNotNull Operator = "IS NOT NULL"
)

type Page

type Page struct {
	Number  int `json:"page"     gorm:"page"`
	PerPage int `json:"per_page" gorm:"per_page"`
}

Page holds pagination request params.

func (*Page) Normalize

func (p *Page) Normalize()

Normalize ensures sane defaults and enforces MaxPerPage

func (*Page) Offset

func (p *Page) Offset() int

Offset returns the SQL offset for the current page

type Query

type Query struct {
	Page    Page
	Filters []Filter
	Sorts   []Sort
}

Query aggregates Page, Filters and Sorts in a single request object

func NewQuery

func NewQuery(page Page, filters []Filter, sorts []Sort) Query

NewQuery returns a Query with normalized defaults

type Result

type Result[T any] struct {
	Data       []T  `json:"data"`
	Total      int  `json:"total"`
	Page       int  `json:"page"`
	PerPage    int  `json:"per_page"`
	TotalPages int  `json:"total_pages"`
	HasNext    bool `json:"has_next"`
	HasPrev    bool `json:"has_prev"`
}

Result is the generic paginated response

func NewResult

func NewResult[T any](data []T, total int, p Page) Result[T]

NewResult builds a Result from a slice, total count and page config

type Sort

type Sort struct {
	Field     string
	Direction SortDirection
}

Sort represents a single ORDER BY clause

func ParseSort

func ParseSort(raw string) []Sort

ParseSort parses a comma-separated sort string like "name asc,created_at desc"

type SortBuilder

type SortBuilder struct {
	// contains filtered or unexported fields
}

SortBuilder provides a fluent API to build []Sort

sorts := paginator.NewSortBuilder()
    OrderBy("created_at", paginator.Desc)
    Build()

func NewSortBuilder

func NewSortBuilder() *SortBuilder

func (*SortBuilder) Build

func (b *SortBuilder) Build() []Sort

func (*SortBuilder) OrderBy

func (b *SortBuilder) OrderBy(field string, dir SortDirection) *SortBuilder

type SortDirection

type SortDirection string

SortDirection for ORDER BY clauses

const (
	Asc  SortDirection = "ASC"
	Desc SortDirection = "DESC"
)

Jump to

Keyboard shortcuts

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