pagination

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Nov 7, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package pagination reads list parameters off a request, with the bounds applied before anything reaches the database.

The bound is the point. `?limit=1000000` on a table with ten million rows is not a client being greedy, it is an outage; clamping at the edge means no repository has to remember to.

Example

Sorting is restricted to an allow-list the caller owns, because OrderBy is interpolated into SQL.

package main

import (
	"fmt"
	"net/http/httptest"

	"github.com/gofiber/fiber/v3"

	"github.com/IncepTools/go/src/web/pagination"
)

// serve runs one request through a handler and returns what it saw.
func serve(query string, opt pagination.Options) (pagination.Query, error) {
	var (
		got pagination.Query
		err error
	)
	app := fiber.New()
	app.Get("/", func(c fiber.Ctx) error {
		got, err = pagination.FromWith(c, opt)
		return c.SendStatus(200)
	})

	app.Test(httptest.NewRequest("GET", "/?"+query, nil))
	return got, err
}

func main() {
	q, err := serve("page=3&limit=10&sort=name&order=desc", pagination.Options{
		Sortable: []string{"created_at", "name"},
	})
	if err != nil {
		panic(err)
	}

	fmt.Println(q.Offset())
	fmt.Println(q.OrderBy())
}
Output:
20
name DESC

Index

Examples

Constants

View Source
const (
	DefaultPage  = 1
	DefaultLimit = 20
	MaxLimit     = 100
)

Defaults applied when the request says nothing.

Variables

This section is empty.

Functions

func Pages

func Pages(total int64, limit int) int

Pages is the total page count for a result size.

Types

type Direction

type Direction string

Direction is a sort order.

const (
	// Asc sorts ascending.
	Asc Direction = "asc"
	// Desc sorts descending.
	Desc Direction = "desc"
)

type Options

type Options struct {
	// DefaultLimit overrides DefaultLimit for this route.
	DefaultLimit int
	// MaxLimit overrides MaxLimit for this route.
	MaxLimit int
	// Sortable is the allow-list of column names a client may sort by. When
	// it is empty, sorting is refused entirely — fail closed, because the
	// alternative is interpolating client input into SQL.
	Sortable []string
	// DefaultSort is applied when the client asks for none. It is NOT checked
	// against Sortable; it comes from your code, not the request.
	DefaultSort  string
	DefaultOrder Direction
}

Options configures parsing.

type Query

type Query struct {
	Page  int       `json:"page"`
	Limit int       `json:"limit"`
	Sort  string    `json:"sort,omitempty"`
	Order Direction `json:"order,omitempty"`
	// Search is the free-text term, trimmed. Empty means no filter.
	Search string `json:"search,omitempty"`
}

Query is the parsed list request.

func From

func From(c fiber.Ctx) Query

From parses the query string with the defaults.

func FromWith

func FromWith(c fiber.Ctx, opt Options) (Query, error)

FromWith parses the query string and reports a client error for anything it refuses — an unsortable column, a non-numeric page.

Example (ClampsTheLimit)

An over-large limit is clamped rather than refused. Rejecting it would turn a working integration into a 400 the day the ceiling is lowered.

package main

import (
	"fmt"
	"net/http/httptest"

	"github.com/gofiber/fiber/v3"

	"github.com/IncepTools/go/src/web/pagination"
)

// serve runs one request through a handler and returns what it saw.
func serve(query string, opt pagination.Options) (pagination.Query, error) {
	var (
		got pagination.Query
		err error
	)
	app := fiber.New()
	app.Get("/", func(c fiber.Ctx) error {
		got, err = pagination.FromWith(c, opt)
		return c.SendStatus(200)
	})

	app.Test(httptest.NewRequest("GET", "/?"+query, nil))
	return got, err
}

func main() {
	q, err := serve("limit=100000", pagination.Options{MaxLimit: 50})
	fmt.Println(q.Limit, err)
}
Output:
50 <nil>
Example (RefusesAnUnknownSortKey)

A sort key that is not allow-listed is refused, with a message naming what is.

package main

import (
	"fmt"
	"net/http/httptest"

	"github.com/gofiber/fiber/v3"

	"github.com/IncepTools/go/src/core/errs"
	"github.com/IncepTools/go/src/web/pagination"
)

// serve runs one request through a handler and returns what it saw.
func serve(query string, opt pagination.Options) (pagination.Query, error) {
	var (
		got pagination.Query
		err error
	)
	app := fiber.New()
	app.Get("/", func(c fiber.Ctx) error {
		got, err = pagination.FromWith(c, opt)
		return c.SendStatus(200)
	})

	app.Test(httptest.NewRequest("GET", "/?"+query, nil))
	return got, err
}

func main() {
	_, err := serve("sort=password", pagination.Options{
		Sortable: []string{"created_at", "name"},
	})

	fmt.Println(errs.CodeOf(err))
	fmt.Println(errs.From(err).Details[0].Message)
}
Output:
INVALID_SORT
sort must be one of: created_at, name

func (Query) Offset

func (q Query) Offset() int

Offset is the SQL OFFSET for this page.

func (Query) OrderBy

func (q Query) OrderBy() string

OrderBy renders a SQL ORDER BY clause, or "" when no sort was requested.

It is safe to interpolate ONLY because From rejects any sort key that is not in the caller's allow-list. Never build one of these from a raw query parameter; that is a SQL injection with extra steps.

Jump to

Keyboard shortcuts

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