import files, add README & LICENSE
This commit is contained in:
@@ -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 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
|
||||
Reference in New Issue
Block a user