snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package keysetpagination
import (
"cmp"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/pkg/errors"
)
// Pagination Request Parameters
//
// The `Link` HTTP header contains multiple links (`first`, `next`) formatted as:
// `<https://{project-slug}.projects.oryapis.com/admin/sessions?page_size=250&page_token=>; rel="first"`
//
// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination).
//
// swagger:model keysetPaginationRequestParameters
type RequestParameters struct {
// Items per Page
//
// This is the number of items per page to return.
// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination).
//
// required: false
// in: query
// default: 250
// min: 1
// max: 1000
PageSize int `json:"page_size"`
// Next Page Token
//
// The next page token.
// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination).
//
// required: false
// in: query
PageToken string `json:"page_token"`
}
// Pagination Response Header
//
// The `Link` HTTP header contains multiple links (`first`, `next`) formatted as:
// `<https://{project-slug}.projects.oryapis.com/admin/sessions?page_size=250&page_token=>; rel="first"`
//
// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination).
//
// swagger:model keysetPaginationResponseHeaders
type ResponseHeaders struct {
// The Link HTTP Header
//
// The `Link` header contains a comma-delimited list of links to the following pages:
//
// - first: The first page of results.
// - next: The next page of results.
//
// Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. Examples:
//
// </admin/sessions?page_size=250&page_token={last_item_uuid}; rel="first",/admin/sessions?page_size=250&page_token=>; rel="next"
//
Link string `json:"link"`
}
func header(u *url.URL, rel, token string, size int) string {
q := u.Query()
q.Set("page_token", token)
q.Set("page_size", strconv.Itoa(size))
u.RawQuery = q.Encode()
return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel)
}
// Header adds the Link header for the page encoded by the paginator.
// It contains links to the first and next page, if one exists.
func Header(w http.ResponseWriter, u *url.URL, p *Paginator) {
size := p.Size()
link := []string{header(u, "first", p.defaultToken.Encode(), size)}
if !p.isLast {
link = append(link, header(u, "next", p.Token().Encode(), size))
}
w.Header().Set("Link", strings.Join(link, ","))
}
// Parse returns the pagination options from the URL query.
func Parse(q url.Values, p PageTokenConstructor) ([]Option, error) {
var opts []Option
if pt := cmp.Or(q["page_token"]...); pt != "" {
pageToken, err := url.QueryUnescape(pt)
if err != nil {
return nil, errors.WithStack(err)
}
parsed, err := p(pageToken)
if err != nil {
return nil, errors.WithStack(err)
}
opts = append(opts, WithToken(parsed))
}
if ps := cmp.Or(q["page_size"]...); ps != "" {
size, err := strconv.Atoi(ps)
if err != nil {
return nil, errors.WithStack(err)
}
opts = append(opts, WithSize(size))
}
return opts, nil
}

View File

@@ -0,0 +1,74 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package keysetpagination
import (
"encoding/base64"
"fmt"
"strings"
)
type PageToken = interface {
Parse(string) map[string]string
Encode() string
}
var _ PageToken = new(StringPageToken)
var _ PageToken = new(MapPageToken)
type StringPageToken string
func (s StringPageToken) Parse(idField string) map[string]string {
return map[string]string{idField: string(s)}
}
func (s StringPageToken) Encode() string {
return string(s)
}
func NewStringPageToken(s string) (PageToken, error) {
return StringPageToken(s), nil
}
type MapPageToken map[string]string
func (m MapPageToken) Parse(_ string) map[string]string {
return map[string]string(m)
}
const pageTokenColumnDelim = "/"
func (m MapPageToken) Encode() string {
elems := make([]string, 0, len(m))
for k, v := range m {
elems = append(elems, fmt.Sprintf("%s=%s", k, v))
}
// For now: use Base64 instead of URL escaping, as the Timestamp format we need to use can contain a `+` sign,
// which represents a space in URLs, so it's not properly encoded by the Go library.
return base64.RawStdEncoding.EncodeToString([]byte(strings.Join(elems, pageTokenColumnDelim)))
}
func NewMapPageToken(s string) (PageToken, error) {
b, err := base64.RawStdEncoding.DecodeString(s)
if err != nil {
return nil, err
}
tokens := strings.Split(string(b), pageTokenColumnDelim)
r := map[string]string{}
for _, p := range tokens {
if columnName, value, found := strings.Cut(p, "="); found {
r[columnName] = value
}
}
return MapPageToken(r), nil
}
var _ PageTokenConstructor = NewMapPageToken
var _ PageTokenConstructor = NewStringPageToken
type PageTokenConstructor = func(string) (PageToken, error)

View File

@@ -0,0 +1,258 @@
// 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
}