Files
end 683b0ddbf4 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>
2026-06-29 17:14:05 -07:00

153 lines
4.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# cursor
Relay-style cursor pagination for [go-jet](https://github.com/go-jet/jet) MySQL queries.
Encodes opaque cursor strings from column values, applies consistent `WHERE` / `ORDER BY` clauses, and builds GraphQL Relaycompatible `Connection` results with `PageInfo` and total counts.
## Requirements
- Go 1.25+
- [go-jet/v2/mysql](https://github.com/go-jet/jet) — generated table/column types
- [go-toolkit/dbx](https://gitea.auvem.com/go-toolkit/dbx) — `Queryable` for running queries
```bash
go get gitea.auvem.com/go-toolkit/cursor
```
## Setup
Register every column referenced by a cursor at application startup. The registry resolves columns when encoding, decoding, and building SQL.
```go
import (
"gitea.auvem.com/go-toolkit/cursor"
"yourapp/.gen/yourdb/table"
)
func init() {
cursor.RegisterColumn(table.User.ID, table.Meeting.StartTime)
}
```
## Concepts
| Term | Meaning |
|------|---------|
| **Index** | Stable position column (usually the primary key). |
| **Order column** | Column users sort by. May differ from the index. |
| **Simple cursor** | Index and order column are the same; paginate on one value. |
| **Tuple ordering** | Order column ≠ index; results sort by `(order_col, index_col)`. |
| **Composite cursor** | Tuple ordering plus an encoded `OrderValue` on each edge — required for correct `after` pagination when sort values can repeat. |
## Quick start
### 1. Define a cursor factory
```go
func newUserCursor() *cursor.Cursor[mysql.IntegerExpression, mysql.ColumnInteger] {
return cursor.NewCursor(
cursor.NewInt64Value(0, table.User.ID),
table.User.ID,
cursor.OrderDescending,
)
}
```
### 2. Paginate a query
```go
active := newUserCursor()
if after != nil {
_ = active.Decode(*after)
}
stmt := table.User.
SELECT(table.User.AllColumns).
WHERE(filters).
WHERE(cursor.PaginateConds(active)).
ORDER_BY(cursor.OrderByClauses(active)...).
LIMIT(int64(limit))
var rows []*User
dbx.MustQuery(db, stmt, &rows, nil)
```
Or use `PageQuery` to run the query and build edges in one step:
```go
conn, err := cursor.PageQuery[User, mysql.IntegerExpression, mysql.ColumnInteger]{
Sqlo: db,
Stmt: table.User.SELECT(table.User.AllColumns),
Conds: filters,
Cursor: decodedCursor, // nil for first page
Default: newUserCursor,
Limit: limit,
CountFn: cursor.BuildQueryCountFn(table.User.ID, table.User, filters),
ToEdge: func(_ *cursor.Cursor[...], item *User) (cursor.GenericCursor, error) {
return newUserCursor().CopyWithVal(
cursor.NewInt64Value(item.ID, table.User.ID),
), nil
},
}.Run()
```
### 3. Relay `after` / `first` args
```go
conn, err := cursor.ConnectionFromRelayArgs(
after, first, newUserCursor,
func(c *cursor.Cursor[...], limit int) (*cursor.Connection[User], error) {
return listUsers(c, limit)
},
)
```
### 4. Composite (multi-column) sort
When sorting by a non-unique column, include both index and order values on each edge:
```go
base := cursor.NewCursor(
cursor.NewStringValue("", table.Meeting.ID),
table.Meeting.StartTime,
cursor.OrderDescending,
)
edgeCursor := base.CopyWithVals(
cursor.NewStringValue(row.ID, table.Meeting.ID),
cursor.NewTimestampValue(row.StartTime, table.Meeting.StartTime),
)
```
## API overview
| Layer | Types / functions |
|-------|-------------------|
| Column registry | `RegisterColumn`, `RegisterColumnList`, `GetColumn`, `GetColumnByKey` |
| Cursor values | `NewInt64Value`, `NewUint64Value`, `NewStringValue`, `NewTimestampValue` |
| Cursor | `NewCursor`, `CopyWithVal`, `CopyWithVals`, `Encode`, `Decode`, `NewCursorFromAfterPtr` |
| SQL helpers | `PaginateConds`, `OrderByClauses`, `QueryCount`, `BuildQueryCountFn` |
| Relay output | `BuildEdges`, `PageQuery`, `ConnectionFromRelayArgs`, `Connection`, `PageInfo` |
## Value types
| Index / order type | Constructor |
|--------------------|-------------|
| `int64` | `NewInt64Value` |
| `uint64` | `NewUint64Value` |
| `string` / UUID | `NewStringValue` |
| `time.Time` | `NewTimestampValue` |
## Extension points
Custom cursor value types implement [GenericExpr] (order values) and [ExprMarshaler] (index values). See package godoc for requirements.
## Errors
Decode failures return `ErrBadCursorString`; use `errors.Is` to detect invalid client cursors.
## Utilities
- `ExtractNodes` — nodes from a `Connection`
- `DereferenceSlice``[]*T``[]T`, skipping nils