import files, add README & LICENSE

This commit is contained in:
2026-06-29 17:06:34 -07:00
commit aecd90eb84
17 changed files with 2552 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
# 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