Files
cursor/README.md
T

145 lines
4.2 KiB
Markdown
Raw 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` |
## Utilities
- `ExtractNodes` — nodes from a `Connection`
- `DereferenceSlice``[]*T``[]T`, skipping nils