Skip to content

raykavin/gopagefy

Repository files navigation

gopagefy

Go Reference Go Version Go Report Card License

A generic, zero-dependency Go library for pagination and filtering. Provides type-safe paginated responses, a fluent filter builder, sort parsing, and a plug-and-play GORM adapter — all without boilerplate.


Features

🔢 Generic responses Result[T] works for any entity using Go generics
🔍 Fluent filter builder Compose WHERE clauses with a clean, chainable API
↕️ Sort builder Build and parse ORDER BY clauses from query strings
🗃️ GORM adapter Drop-in Scope() with automatic COUNT before LIMIT/OFFSET
🛡️ Safe defaults Normalizes page number, per-page, and enforces max limits
Zero dependencies Core package is pure Go; GORM adapter is opt-in

Installation

go get github.com/raykavin/gopagefy

Requires Go 1.21+.


Quick Start

package main

import (
    "fmt"

    "github.com/raykavin/gopagefy"
)

func main() {
    page := gopagefy.Page{Number: 1, PerPage: 10}

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

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

    query := gopagefy.NewQuery(page, filters, sorts)

    // Simulate a paginated result
    items := []string{"item1", "item2", "item3"}
    result := gopagefy.NewResult(items, 42, query.Page)

    fmt.Println(result.Page)       // 1
    fmt.Println(result.TotalPages) // 5
    fmt.Println(result.HasNext)    // true
}

Core Concepts

Page

Page holds the pagination request and normalizes itself on demand.

page := gopagefy.Page{Number: 2, PerPage: 20}
page.Normalize() // enforces defaults and MaxPerPage

fmt.Println(page.Offset()) // 20

Default values:

Constant Value
DefaultPage 1
DefaultPerPage 20
MaxPerPage 100

Result[T]

Result[T] is the generic paginated response. It works for any type — no type assertions needed.

type Transaction struct {
    ID     uint
    Amount float64
}

rows  := []Transaction{{ID: 1, Amount: 99.9}}
total := 150

result := gopagefy.NewResult(rows, total, page)
// result.Data       → []Transaction
// result.Total      → 150
// result.TotalPages → 8
// result.HasNext    → true
// result.HasPrev    → true

JSON response shape:

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

Query

Query aggregates Page, []Filter, and []Sort into a single object that travels through your application layers.

query := gopagefy.NewQuery(page, filters, sorts)
// query.Page    → normalized Page
// query.Filters → []Filter
// query.Sorts   → []Sort

Filtering

FilterBuilder

Build WHERE conditions with a fluent, readable API.

filters := gopagefy.NewFilterBuilder().
    Where("status", gopagefy.Eq, "active").
    Where("amount", gopagefy.Gte, 100).
    Where("deleted_at", gopagefy.IsNull, nil).
    WhereIf(userID != "", "user_id", gopagefy.Eq, userID). // only added if true
    Build()

Supported Operators

Operator SQL
Eq =
Neq <>
Gt >
Gte >=
Lt <
Lte <=
Like LIKE '%value%'
ILike ILIKE '%value%'
In IN (?)
NotIn NOT IN (?)
IsNull IS NULL
IsNotNull IS NOT NULL

Conditional Filters

WhereIf adds the filter only when the condition is true, keeping the builder clean without extra if blocks.

gopagefy.NewFilterBuilder().
    WhereIf(req.Status != "",    "status",     gopagefy.Eq,  req.Status).
    WhereIf(req.MinAmount > 0,   "amount",     gopagefy.Gte, req.MinAmount).
    WhereIf(req.Search != "",    "description",gopagefy.ILike, req.Search).
    Build()

Sorting

SortBuilder

sorts := gopagefy.NewSortBuilder().
    OrderBy("created_at", gopagefy.Desc).
    OrderBy("name", gopagefy.Asc).
    Build()

ParseSort

Parse a sort string directly from a query parameter (e.g. ?sort=name+asc,created_at+desc):

sorts := gopagefy.ParseSort("name asc,created_at desc")
// []Sort{
//   {Field: "name",       Direction: Asc},
//   {Field: "created_at", Direction: Desc},
// }

GORM Integration

Scope

gopagefy.Scope returns a GORM scope that applies filters, sorts, counts, and pagination in one call.

var rows  []Transaction
var total int64

query := gopagefy.NewQuery(page, filters, sorts)

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

result := gopagefy.NewResult(rows, int(total), query.Page)

The scope runs COUNT(*) before applying LIMIT and OFFSET, so total always reflects the full dataset.


HTTP Handler Example (Gin)

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

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

    minAmount, _ := strconv.ParseFloat(req.MinAmount, 64)

    filters := gopagefy.NewFilterBuilder().
        WhereIf(req.Status != "", "status", gopagefy.Eq, req.Status).
        WhereIf(minAmount > 0,    "amount", gopagefy.Gte, minAmount).
        Build()

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

    query := gopagefy.NewQuery(req.Page, filters, sorts)

    var rows  []Transaction
    var total int64

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

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

File Structure

gopagefy/
├── paginator.go   # Page, Query, Offset, Normalize
├── filter.go      # Filter, Sort, FilterBuilder, SortBuilder, ParseSort
├── paginator.go   # Result[T], NewResult[T]
└── gorm.go        # Scope() GORM adapter

Best Practices

Always call Normalize() (or use NewQuery()) before using Page, otherwise defaults won't be applied.

// ✅ Good
query := gopagefy.NewQuery(page, filters, sorts)

// ⚠️ Missing normalization
db.Limit(page.PerPage).Offset(page.Offset())

Use WhereIf over manual conditionals to keep your filter chains readable.

// ✅ Good
fb.WhereIf(req.Status != "", "status", gopagefy.Eq, req.Status)

// ❌ Avoid
if req.Status != "" {
    fb.Where("status", gopagefy.Eq, req.Status)
}

Pass Query across layers instead of individual Page, []Filter, []Sort to keep function signatures clean.

// ✅ Good
func (r *repo) List(ctx context.Context, q gopagefy.Query) ([]T, int, error)

// ❌ Avoid
func (r *repo) List(ctx context.Context, page int, perPage int, filters []Filter, ...) 

Contributing

Contributions to gopagefy are welcome! Here are some ways you can help improve the project:

  • Report bugs and suggest features by opening issues on GitHub
  • Submit pull requests with bug fixes or new features
  • Improve documentation to help other users and developers
  • Share your custom strategies with the community

License

gopagefy is distributed under the MIT License.
For complete license terms and conditions, see the LICENSE file in the repository.


Contact

For support, collaboration, or questions about gopagefy:

Email: raykavin.meireles@gmail.com
GitHub: @raykavin

About

Generic pagination and filtering library for Go, fluent filter builder, sort parsing, and GORM adapter with zero boilerplate.

Topics

Resources

License

Stars

Watchers

Forks

Contributors