docs: add package godoc and clarify public API
Prepare the extracted library for external consumption with grouped godoc, extension point docs, and setup requirements. Wire ErrBadCursorString into decode paths and drop redundant Paginate*Conds aliases. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrBadCursor is returned when a cursor is invalid.
|
||||
// ErrBadCursorString is returned when a cursor string cannot be decoded.
|
||||
ErrBadCursorString = errors.New("bad cursor string")
|
||||
|
||||
columnRegistry = make(map[ColumnKey]mysql.Column)
|
||||
@@ -52,6 +52,9 @@ func (k ColumnKey) String() string {
|
||||
}
|
||||
|
||||
// RegisterColumn registers one or more columns with the cursor registry.
|
||||
//
|
||||
// Required at application startup before encoding, decoding, or SQL generation.
|
||||
// Every index column, order column, and OrderValue column must be registered.
|
||||
func RegisterColumn(columns ...mysql.Column) {
|
||||
columnRegistryLock.Lock()
|
||||
defer columnRegistryLock.Unlock()
|
||||
@@ -61,6 +64,7 @@ func RegisterColumn(columns ...mysql.Column) {
|
||||
}
|
||||
|
||||
// RegisterColumnList registers a list of columns with the cursor registry.
|
||||
// See [RegisterColumn] for setup requirements.
|
||||
func RegisterColumnList(columns mysql.ColumnList) {
|
||||
RegisterColumn([]mysql.Column(columns)...)
|
||||
}
|
||||
@@ -142,13 +146,18 @@ var _ ExprMarshaler[mysql.IntegerExpression, mysql.ColumnInteger] = (*Int64Value
|
||||
var _ ExprMarshaler[mysql.IntegerExpression, mysql.ColumnInteger] = (*Uint64Value)(nil)
|
||||
var _ ExprMarshaler[mysql.TimestampExpression, mysql.ColumnTimestamp] = (*TimestampValue)(nil)
|
||||
|
||||
// ExprMarshaler is the typed index-value extension point. Implement GenericExpr plus
|
||||
// Expr and Col to produce go-jet expressions. Col panics when the column is not
|
||||
// registered via [RegisterColumn].
|
||||
type ExprMarshaler[E mysql.Expression, C mysql.Column] interface {
|
||||
GenericExpr
|
||||
Expr() E
|
||||
Col() C
|
||||
}
|
||||
|
||||
// GenericExpr is the untyped cursor value contract shared by index and order values.
|
||||
// GenericExpr is the cursor value extension point shared by index and order columns.
|
||||
// Implement ColumnKey, IsEmpty, and driver.Valuer. Order values decoded from cursor
|
||||
// JSON must match a registered go-jet column kind; see package documentation.
|
||||
type GenericExpr interface {
|
||||
ColumnKey() ColumnKey
|
||||
IsEmpty() bool
|
||||
@@ -165,6 +174,8 @@ type StringValue struct {
|
||||
Val string `json:"val"`
|
||||
}
|
||||
|
||||
// NewStringValue creates a cursor value for a string column (including UUIDs).
|
||||
// The column must be registered via [RegisterColumn] before Col or decode use it.
|
||||
func NewStringValue(val string, col mysql.ColumnString) *StringValue {
|
||||
return &StringValue{
|
||||
Key: NewColumnKey(col),
|
||||
@@ -201,6 +212,8 @@ type Int64Value struct {
|
||||
Val int64 `json:"val"`
|
||||
}
|
||||
|
||||
// NewInt64Value creates a cursor value for a signed integer column.
|
||||
// The column must be registered via [RegisterColumn] before Col or decode use it.
|
||||
func NewInt64Value(val int64, col mysql.ColumnInteger) *Int64Value {
|
||||
return &Int64Value{
|
||||
Key: NewColumnKey(col),
|
||||
@@ -237,6 +250,8 @@ type Uint64Value struct {
|
||||
Val uint64 `json:"val"`
|
||||
}
|
||||
|
||||
// NewUint64Value creates a cursor value for an unsigned integer column.
|
||||
// The column must be registered via [RegisterColumn] before Col or decode use it.
|
||||
func NewUint64Value(val uint64, col mysql.ColumnInteger) *Uint64Value {
|
||||
return &Uint64Value{
|
||||
Key: NewColumnKey(col),
|
||||
@@ -275,6 +290,7 @@ type TimestampValue struct {
|
||||
}
|
||||
|
||||
// NewTimestampValue creates a timestamp cursor value for the given column.
|
||||
// The column must be registered via [RegisterColumn] before Col or decode use it.
|
||||
func NewTimestampValue(val time.Time, col mysql.ColumnTimestamp) *TimestampValue {
|
||||
return &TimestampValue{
|
||||
Key: NewColumnKey(col),
|
||||
@@ -310,9 +326,9 @@ func (t *TimestampValue) IsEmpty() bool {
|
||||
return t == nil || t.Key.Table == "" || t.Key.Column == "" || t.Val.IsZero()
|
||||
}
|
||||
|
||||
// GenericCursor is an interface for a cursor that can be used with any type of
|
||||
// expression and column. Only the methods that do not depend on the specific
|
||||
// types of expressions and columns are defined here.
|
||||
// GenericCursor is the cursor extension point used by edge building, page counting,
|
||||
// and pagination helpers without concrete type parameters. [Cursor] implements this
|
||||
// interface.
|
||||
type GenericCursor interface {
|
||||
IsEmpty() bool
|
||||
IsComposite() bool
|
||||
@@ -365,7 +381,7 @@ func NewCursor[IE mysql.Expression, IC mysql.Column](index ExprMarshaler[IE, IC]
|
||||
}
|
||||
|
||||
// NewCursorFromAfterPtr decodes a Relay-style after cursor. Nil or empty after
|
||||
// returns (nil, nil).
|
||||
// returns (nil, nil). Returns [ErrBadCursorString] when after is not valid cursor JSON.
|
||||
func NewCursorFromAfterPtr[IE mysql.Expression, IC mysql.Column](
|
||||
newZero func() *Cursor[IE, IC],
|
||||
after *string,
|
||||
@@ -378,6 +394,7 @@ func NewCursorFromAfterPtr[IE mysql.Expression, IC mysql.Column](
|
||||
}
|
||||
|
||||
// NewCursorFromJSON returns a Cursor from a JSON representation.
|
||||
// Returns [ErrBadCursorString] when src is not valid cursor JSON.
|
||||
func NewCursorFromJSON[IE mysql.Expression, IC mysql.Column](zeroIndex ExprMarshaler[IE, IC], src []byte) (*Cursor[IE, IC], error) {
|
||||
cursor := Cursor[IE, IC]{
|
||||
Index: zeroIndex,
|
||||
@@ -389,13 +406,15 @@ func NewCursorFromJSON[IE mysql.Expression, IC mysql.Column](zeroIndex ExprMarsh
|
||||
return &cursor, nil
|
||||
}
|
||||
|
||||
// CopyWithVal returns a new cursor with the specified value and the current ordering.
|
||||
// CopyWithVal returns a new cursor with the specified index value and the current
|
||||
// ordering. Use for simple pagination where the index and order column are the same.
|
||||
func (c *Cursor[IE, IC]) CopyWithVal(val ExprMarshaler[IE, IC]) *Cursor[IE, IC] {
|
||||
return NewCursor(val, c.OrderCol(), c.OrderDir)
|
||||
}
|
||||
|
||||
// CopyWithVals returns a new cursor with the specified index and order values.
|
||||
// Use this when IsComposite() is true.
|
||||
// Use when building edge cursors under tuple ordering: set both values so
|
||||
// [IsComposite] is true and after pagination compares (order_col, index_col).
|
||||
func (c *Cursor[IE, IC]) CopyWithVals(index ExprMarshaler[IE, IC], orderVal GenericExpr) *Cursor[IE, IC] {
|
||||
result := NewCursor(index, c.OrderCol(), c.OrderDir)
|
||||
result.OrderValue = orderVal
|
||||
@@ -403,8 +422,10 @@ func (c *Cursor[IE, IC]) CopyWithVals(index ExprMarshaler[IE, IC], orderVal Gene
|
||||
}
|
||||
|
||||
// UsesTupleOrdering reports whether results are sorted by (order_col, index_col).
|
||||
// Unlike IsComposite, this does not require OrderValue and applies to default
|
||||
// cursors on the first page.
|
||||
//
|
||||
// True when the order column differs from the index column. Applies to default
|
||||
// cursors on the first page and drives [OrderByClauses]. Unlike [IsComposite],
|
||||
// OrderValue is not required.
|
||||
func (c *Cursor[IE, IC]) UsesTupleOrdering() bool {
|
||||
if c == nil || c.Index == nil {
|
||||
return false
|
||||
@@ -416,7 +437,11 @@ func (c *Cursor[IE, IC]) UsesTupleOrdering() bool {
|
||||
return c.OrderColumnKey != indexKey
|
||||
}
|
||||
|
||||
// IsComposite reports whether pagination uses (order_col, index_col) tuple comparison.
|
||||
// IsComposite reports whether after-pagination filters on (order_col, index_col).
|
||||
//
|
||||
// True when tuple ordering is active and OrderValue is set—typical for encoded edge
|
||||
// cursors when the sort column is non-unique. Drives [PaginateConds] and page
|
||||
// counting via lexicographic tuple comparison.
|
||||
func (c *Cursor[IE, IC]) IsComposite() bool {
|
||||
if c == nil || c.Index == nil || c.Index.IsEmpty() {
|
||||
return false
|
||||
@@ -471,6 +496,7 @@ func (c *Cursor[IE, IC]) Encode() (string, error) {
|
||||
}
|
||||
|
||||
// Decode decodes a stringified JSON representation of the cursor into this object.
|
||||
// Returns [ErrBadCursorString] when the input is not valid cursor JSON.
|
||||
func (c *Cursor[IE, IC]) Decode(src string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cursor is nil")
|
||||
@@ -478,8 +504,8 @@ func (c *Cursor[IE, IC]) Decode(src string) error {
|
||||
return decodeCursorJSON(c, []byte(src), false, OrderAscending)
|
||||
}
|
||||
|
||||
// DecodeAndOrder decodes a stringified JSON representation of the cursor into
|
||||
// this object and applies a new order direction.
|
||||
// DecodeAndOrder decodes a cursor string like [Cursor.Decode] but replaces the
|
||||
// encoded order direction with orderDir.
|
||||
func (c *Cursor[IE, IC]) DecodeAndOrder(src string, orderDir OrderDirection) error {
|
||||
return decodeCursorJSON(c, []byte(src), true, orderDir)
|
||||
}
|
||||
@@ -492,11 +518,11 @@ func decodeCursorJSON[IE mysql.Expression, IC mysql.Column](
|
||||
) error {
|
||||
var raw cursorJSON
|
||||
if err := json.Unmarshal(src, &raw); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal cursor: %w", err)
|
||||
return fmt.Errorf("%w: invalid JSON: %v", ErrBadCursorString, err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(raw.Index, c.Index); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal cursor index: %w", err)
|
||||
return fmt.Errorf("%w: invalid index: %v", ErrBadCursorString, err)
|
||||
}
|
||||
|
||||
c.OrderColumnKey = raw.OrderColumnKey
|
||||
@@ -508,7 +534,7 @@ func decodeCursorJSON[IE mysql.Expression, IC mysql.Column](
|
||||
if len(raw.OrderValue) > 0 {
|
||||
orderVal, err := unmarshalGenericExpr(raw.OrderValue, raw.OrderColumnKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal cursor order value: %w", err)
|
||||
return fmt.Errorf("%w: invalid order value: %v", ErrBadCursorString, err)
|
||||
}
|
||||
c.OrderValue = orderVal
|
||||
} else {
|
||||
@@ -521,30 +547,30 @@ func decodeCursorJSON[IE mysql.Expression, IC mysql.Column](
|
||||
func unmarshalGenericExpr(data []byte, key ColumnKey) (GenericExpr, error) {
|
||||
col, err := GetColumnByKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w: %v", ErrBadCursorString, err)
|
||||
}
|
||||
|
||||
switch col.(type) {
|
||||
case mysql.ColumnTimestamp:
|
||||
var v TimestampValue
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w: %v", ErrBadCursorString, err)
|
||||
}
|
||||
return &v, nil
|
||||
case mysql.ColumnInteger:
|
||||
var v Uint64Value
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w: %v", ErrBadCursorString, err)
|
||||
}
|
||||
return &v, nil
|
||||
case mysql.ColumnString:
|
||||
var v StringValue
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w: %v", ErrBadCursorString, err)
|
||||
}
|
||||
return &v, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported order column type for %s", key)
|
||||
return nil, fmt.Errorf("%w: unsupported order column type for %s", ErrBadCursorString, key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user