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 ¶
const ( DefaultPage = 1 DefaultLimit = 20 MaxLimit = 100 )
Defaults applied when the request says nothing.
Variables ¶
This section is empty.
Functions ¶
Types ¶
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 FromWith ¶
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