snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
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
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
This commit is contained in:
112
oryx/pagination/keysetpagination/header.go
Normal file
112
oryx/pagination/keysetpagination/header.go
Normal 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
|
||||
}
|
||||
74
oryx/pagination/keysetpagination/page_token.go
Normal file
74
oryx/pagination/keysetpagination/page_token.go
Normal 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)
|
||||
258
oryx/pagination/keysetpagination/paginator.go
Normal file
258
oryx/pagination/keysetpagination/paginator.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user