145 lines
4.2 KiB
Markdown
145 lines
4.2 KiB
Markdown
# 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 Relay–compatible `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
|