# 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` | ## 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