Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
259 lines
5.9 KiB
Go
259 lines
5.9 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package keysetpagination
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/ory/pop/v6"
|
|
"github.com/ory/pop/v6/columns"
|
|
)
|
|
|
|
type (
|
|
Item = interface{ PageToken() PageToken }
|
|
|
|
Order string
|
|
|
|
columnOrdering struct {
|
|
name string
|
|
order Order
|
|
}
|
|
Paginator struct {
|
|
token, defaultToken PageToken
|
|
size, defaultSize, maxSize int
|
|
isLast bool
|
|
additionalColumn columnOrdering
|
|
}
|
|
Option func(*Paginator) *Paginator
|
|
)
|
|
|
|
var ErrUnknownOrder = errors.New("unknown order")
|
|
|
|
const (
|
|
OrderDescending Order = "DESC"
|
|
OrderAscending Order = "ASC"
|
|
|
|
DefaultSize = 100
|
|
DefaultMaxSize = 500
|
|
)
|
|
|
|
func (o Order) extract() (string, string, error) {
|
|
switch o {
|
|
case OrderAscending:
|
|
return ">", string(o), nil
|
|
case OrderDescending:
|
|
return "<", string(o), nil
|
|
default:
|
|
return "", "", ErrUnknownOrder
|
|
}
|
|
}
|
|
|
|
func (p *Paginator) Token() PageToken {
|
|
if p.token == nil {
|
|
return p.defaultToken
|
|
}
|
|
return p.token
|
|
}
|
|
|
|
func (p *Paginator) Size() int {
|
|
size := p.size
|
|
if size <= 0 {
|
|
size = p.defaultSize
|
|
if size == 0 {
|
|
size = 100
|
|
}
|
|
}
|
|
if size > p.maxSize {
|
|
size = p.maxSize
|
|
}
|
|
return size
|
|
}
|
|
|
|
func (p *Paginator) IsLast() bool {
|
|
return p.isLast
|
|
}
|
|
|
|
func (p *Paginator) ToOptions() []Option {
|
|
opts := make([]Option, 0, 7)
|
|
if p.token != nil {
|
|
opts = append(opts, WithToken(p.token))
|
|
}
|
|
if p.defaultToken != nil {
|
|
opts = append(opts, WithDefaultToken(p.defaultToken))
|
|
}
|
|
if p.size > 0 {
|
|
opts = append(opts, WithSize(p.size))
|
|
}
|
|
if p.defaultSize != DefaultSize {
|
|
opts = append(opts, WithDefaultSize(p.defaultSize))
|
|
}
|
|
if p.maxSize != DefaultMaxSize {
|
|
opts = append(opts, WithMaxSize(p.maxSize))
|
|
}
|
|
if p.additionalColumn.name != "" {
|
|
opts = append(opts, WithColumn(p.additionalColumn.name, p.additionalColumn.order))
|
|
}
|
|
if p.isLast {
|
|
opts = append(opts, withIsLast(p.isLast))
|
|
}
|
|
return opts
|
|
}
|
|
|
|
func (p *Paginator) multipleOrderFieldsQuery(q *pop.Query, idField string, cols map[string]*columns.Column, quoteAndContextualize func(string) string) {
|
|
tokenParts := p.Token().Parse(idField)
|
|
idValue := tokenParts[idField]
|
|
|
|
column, ok := cols[p.additionalColumn.name]
|
|
if !ok {
|
|
q.Where(fmt.Sprintf(`%s > ?`, quoteAndContextualize(idField)), idValue)
|
|
return
|
|
}
|
|
|
|
quoteName := quoteAndContextualize(column.Name)
|
|
|
|
value, ok := tokenParts[column.Name]
|
|
|
|
if !ok {
|
|
q.Where(fmt.Sprintf(`%s > ?`, quoteAndContextualize(idField)), idValue)
|
|
return
|
|
}
|
|
|
|
sign, keyword, err := p.additionalColumn.order.extract()
|
|
if err != nil {
|
|
q.Where(fmt.Sprintf(`%s > ?`, quoteAndContextualize(idField)), idValue)
|
|
return
|
|
}
|
|
|
|
q.
|
|
Where(fmt.Sprintf("(%s %s ? OR (%s = ? AND %s > ?))", quoteName, sign, quoteName, quoteAndContextualize(idField)), value, value, idValue).
|
|
Order(fmt.Sprintf("%s %s", quoteName, keyword))
|
|
|
|
}
|
|
|
|
// Paginate returns a function that paginates a pop.Query.
|
|
// Usage:
|
|
//
|
|
// q := c.Where("foo = ?", foo).Scope(keysetpagination.Paginate[MyItemType](paginator))
|
|
//
|
|
// This function works regardless of whether your type implements the Item
|
|
// interface with pointer or value receivers. To understand the type parameters,
|
|
// see this document:
|
|
// https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#pointer-method-example
|
|
func Paginate[I any, PI interface {
|
|
Item
|
|
*I
|
|
}](p *Paginator) pop.ScopeFunc {
|
|
model := pop.Model{Value: new(I)}
|
|
id := model.IDField()
|
|
tableName := model.Alias()
|
|
return func(q *pop.Query) *pop.Query {
|
|
quote := q.Connection.Dialect.Quote
|
|
eid := quote(tableName) + "." + quote(id)
|
|
|
|
quoteAndContextualize := func(name string) string {
|
|
return quote(tableName) + "." + quote(name)
|
|
}
|
|
p.multipleOrderFieldsQuery(q, id, model.Columns().Cols, quoteAndContextualize)
|
|
|
|
return q.
|
|
Limit(p.Size() + 1).
|
|
// we always need to order by the id field last
|
|
Order(fmt.Sprintf(`%s ASC`, eid))
|
|
}
|
|
}
|
|
|
|
// Result removes the last item (if applicable) and returns the paginator for the next page.
|
|
//
|
|
// This function works regardless of whether your type implements the Item
|
|
// interface with pointer or value receivers. To understand the type parameters,
|
|
// see this document:
|
|
// https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#pointer-method-example
|
|
func Result[I any, PI interface {
|
|
Item
|
|
*I
|
|
}](items []I, p *Paginator) ([]I, *Paginator) {
|
|
if len(items) > p.Size() {
|
|
items = items[:p.Size()]
|
|
return items, &Paginator{
|
|
token: PI(&items[len(items)-1]).PageToken(),
|
|
defaultToken: p.defaultToken,
|
|
size: p.size,
|
|
defaultSize: p.defaultSize,
|
|
maxSize: p.maxSize,
|
|
}
|
|
}
|
|
return items, &Paginator{
|
|
defaultToken: p.defaultToken,
|
|
size: p.size,
|
|
defaultSize: p.defaultSize,
|
|
maxSize: p.maxSize,
|
|
isLast: true,
|
|
}
|
|
}
|
|
|
|
func WithDefaultToken(t PageToken) Option {
|
|
return func(opts *Paginator) *Paginator {
|
|
opts.defaultToken = t
|
|
return opts
|
|
}
|
|
}
|
|
|
|
func WithDefaultSize(size int) Option {
|
|
return func(opts *Paginator) *Paginator {
|
|
opts.defaultSize = size
|
|
return opts
|
|
}
|
|
}
|
|
|
|
func WithMaxSize(size int) Option {
|
|
return func(opts *Paginator) *Paginator {
|
|
opts.maxSize = size
|
|
return opts
|
|
}
|
|
}
|
|
|
|
func WithToken(t PageToken) Option {
|
|
return func(opts *Paginator) *Paginator {
|
|
opts.token = t
|
|
return opts
|
|
}
|
|
}
|
|
|
|
func WithSize(size int) Option {
|
|
return func(opts *Paginator) *Paginator {
|
|
opts.size = size
|
|
return opts
|
|
}
|
|
}
|
|
|
|
func WithColumn(name string, order Order) Option {
|
|
return func(opts *Paginator) *Paginator {
|
|
opts.additionalColumn = columnOrdering{
|
|
name: name,
|
|
order: order,
|
|
}
|
|
return opts
|
|
}
|
|
}
|
|
|
|
func withIsLast(isLast bool) Option {
|
|
return func(opts *Paginator) *Paginator {
|
|
opts.isLast = isLast
|
|
return opts
|
|
}
|
|
}
|
|
|
|
func GetPaginator(modifiers ...Option) *Paginator {
|
|
opts := &Paginator{
|
|
// these can still be overridden by the modifiers, but they should never be unset
|
|
maxSize: DefaultMaxSize,
|
|
defaultSize: DefaultSize,
|
|
}
|
|
for _, f := range modifiers {
|
|
opts = f(opts)
|
|
}
|
|
return opts
|
|
}
|