Files

208 lines
5.1 KiB
Go

package paginate
import (
"context"
"fmt"
"math/big"
"reflect"
"strings"
"time"
"github.com/uptrace/bun"
libtime "github.com/hanzo-fi/go-libs/v5/pkg/types/time"
)
func UsingColumn[FILTERS any, ENTITY any](ctx context.Context,
sb *bun.SelectQuery,
query ColumnPaginatedQuery[FILTERS]) (*Cursor[ENTITY], error) {
ret := make([]ENTITY, 0)
// The column comes from the client-provided cursor, so it must be validated
// against the entity known pagination fields before being used in any SQL
// expression to prevent SQL injection.
var v ENTITY
fields := findPaginationFieldPath(v, query.Column)
if len(fields) == 0 {
return nil, fmt.Errorf("invalid pagination column %q", query.Column)
}
sb = sb.Model(&ret)
sb = sb.Limit(int(query.PageSize) + 1) // Fetch one additional item to find the next token
order := query.Order
if query.Reverse {
order = order.Reverse()
}
sb = sb.OrderExpr("? ?", bun.Ident(query.Column), bun.Safe(order.String()))
if query.PaginationID != nil {
if query.Reverse {
switch query.Order {
case OrderAsc:
sb = sb.Where("? < ?", bun.Ident(query.Column), query.PaginationID)
case OrderDesc:
sb = sb.Where("? > ?", bun.Ident(query.Column), query.PaginationID)
}
} else {
switch query.Order {
case OrderAsc:
sb = sb.Where("? >= ?", bun.Ident(query.Column), query.PaginationID)
case OrderDesc:
sb = sb.Where("? <= ?", bun.Ident(query.Column), query.PaginationID)
}
}
}
if err := sb.Scan(ctx); err != nil {
return nil, err
}
var (
paginationIDs = make([]*big.Int, 0)
)
for _, t := range ret {
paginationID := findPaginationField(t, fields...)
if query.Bottom == nil {
query.Bottom = paginationID
}
paginationIDs = append(paginationIDs, paginationID)
}
hasMore := len(ret) > int(query.PageSize)
if hasMore {
ret = ret[:len(ret)-1]
}
if query.Reverse {
for i := 0; i < len(ret)/2; i++ {
ret[i], ret[len(ret)-i-1] = ret[len(ret)-i-1], ret[i]
}
}
var previous, next *ColumnPaginatedQuery[FILTERS]
if query.Reverse {
cp := query
cp.Reverse = false
next = &cp
if hasMore {
cp := query
cp.PaginationID = paginationIDs[len(paginationIDs)-2]
previous = &cp
}
} else {
if hasMore {
cp := query
cp.PaginationID = paginationIDs[len(paginationIDs)-1]
next = &cp
}
if query.PaginationID != nil {
if (query.Order == OrderAsc && query.PaginationID.Cmp(query.Bottom) > 0) || (query.Order == OrderDesc && query.PaginationID.Cmp(query.Bottom) < 0) {
cp := query
cp.Reverse = true
previous = &cp
}
}
}
return &Cursor[ENTITY]{
PageSize: int(query.PageSize),
HasMore: next != nil,
Previous: previous.EncodeAsCursor(),
Next: next.EncodeAsCursor(),
Data: ret,
}, nil
}
func findPaginationFieldPath(v any, paginationColumn string) []reflect.StructField {
typeOfT := reflect.TypeOf(v)
for i := 0; i < typeOfT.NumField(); i++ {
field := typeOfT.Field(i)
fieldType := field.Type
// If the field is a pointer, we unreference it to target the concrete type
// For example:
// type Object struct {
// *AnotherObject
// }
for {
if field.Type.Kind() == reflect.Ptr {
fieldType = field.Type.Elem()
}
break
}
switch fieldType.Kind() {
case reflect.Struct:
if fieldType.AssignableTo(reflect.TypeOf(time.Time{})) ||
fieldType.AssignableTo(reflect.TypeOf(libtime.Time{})) ||
fieldType.AssignableTo(reflect.TypeOf(big.Int{})) ||
fieldType.AssignableTo(reflect.TypeOf(BigInt{})) {
if fields := checkTag(field, paginationColumn); len(fields) > 0 {
return fields
}
} else {
fields := findPaginationFieldPath(reflect.New(fieldType).Elem().Interface(), paginationColumn)
if len(fields) > 0 {
return fields
}
}
default:
if fields := checkTag(field, paginationColumn); len(fields) > 0 {
return fields
}
}
}
return nil
}
func checkTag(field reflect.StructField, paginationColumn string) []reflect.StructField {
tag := field.Tag.Get("bun")
column := strings.Split(tag, ",")[0]
if column == paginationColumn {
return []reflect.StructField{field}
}
return nil
}
func findPaginationField(v any, fields ...reflect.StructField) *big.Int {
vOf := reflect.ValueOf(v)
field := vOf.FieldByName(fields[0].Name)
if len(fields) == 1 {
switch rawPaginationID := field.Interface().(type) {
case time.Time:
return big.NewInt(rawPaginationID.UTC().UnixMicro())
case *time.Time:
return big.NewInt(rawPaginationID.UTC().UnixMicro())
case *libtime.Time:
return big.NewInt(rawPaginationID.UTC().UnixMicro())
case libtime.Time:
return big.NewInt(rawPaginationID.UTC().UnixMicro())
case *BigInt:
return (*big.Int)(rawPaginationID)
case BigInt:
return (*big.Int)(&rawPaginationID)
case *big.Int:
return rawPaginationID
case big.Int:
return &rawPaginationID
case int64:
return big.NewInt(rawPaginationID)
case int:
return big.NewInt(int64(rawPaginationID))
case *int:
return big.NewInt(int64(*rawPaginationID))
case *int64:
return big.NewInt(*rawPaginationID)
default:
panic(fmt.Sprintf("invalid paginationID, type %T not handled", rawPaginationID))
}
}
return findPaginationField(v, fields[1:]...)
}