Add Relay cursor pagination guide
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
101
contrib/claude/relay.md
Normal file
101
contrib/claude/relay.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Relay Cursor Pagination
|
||||||
|
|
||||||
|
This project implements [Relay-style cursor pagination](https://relay.dev/graphql/connections.htm) for all list fields across GraphQL APIs.
|
||||||
|
|
||||||
|
## Schema types
|
||||||
|
|
||||||
|
Every paginated field uses the same set of types:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type PageInfo {
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
startCursor: CursorKey
|
||||||
|
endCursor: CursorKey
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrderDirection {
|
||||||
|
ASC
|
||||||
|
DESC
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each entity defines its own order field enum, order input, connection, and edge:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
enum VendorOrderField {
|
||||||
|
CREATED_AT
|
||||||
|
NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
input VendorOrder {
|
||||||
|
direction: OrderDirection!
|
||||||
|
field: VendorOrderField!
|
||||||
|
}
|
||||||
|
|
||||||
|
type VendorConnection {
|
||||||
|
totalCount: Int!
|
||||||
|
edges: [VendorEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type VendorEdge {
|
||||||
|
cursor: CursorKey!
|
||||||
|
node: Vendor!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field arguments
|
||||||
|
|
||||||
|
Connection fields on parent types always use the standard Relay arguments:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type Organization {
|
||||||
|
vendors(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
orderBy: VendorOrder
|
||||||
|
filter: VendorFilter
|
||||||
|
): VendorConnection!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `first` / `after` — forward pagination (returns `Head` position)
|
||||||
|
- `last` / `before` — backward pagination (returns `Tail` position)
|
||||||
|
- `orderBy` — optional, defaults to `CREATED_AT` / `DESC`
|
||||||
|
- `filter` — optional, entity-specific filtering
|
||||||
|
|
||||||
|
## Cursor format
|
||||||
|
|
||||||
|
Cursors are opaque `CursorKey` scalars. Internally they encode as base64url(JSON):
|
||||||
|
|
||||||
|
```
|
||||||
|
["<entity_global_id>", <sort_field_value>]
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, a cursor sorting by `created_at` encodes the entity ID and its `created_at` timestamp. This enables keyset pagination — the database uses the cursor values to seek directly to the right position instead of using OFFSET.
|
||||||
|
|
||||||
|
## Keyset pagination
|
||||||
|
|
||||||
|
The database query uses the cursor to build a WHERE clause that skips to the correct position:
|
||||||
|
|
||||||
|
- For `DESC` ordering: rows where `(field <= cursor_value) AND NOT (field = cursor_value AND id > cursor_id)`
|
||||||
|
- For `ASC` ordering: rows where `(field >= cursor_value) AND NOT (field = cursor_value AND id < cursor_id)`
|
||||||
|
|
||||||
|
The query fetches `size + 1` (or `size + 2` when a cursor is provided) rows to detect whether more pages exist in either direction. `NewPage` trims the extra rows and sets `hasNextPage` / `hasPreviousPage` accordingly.
|
||||||
|
|
||||||
|
For backward pagination (`last` / `before`), the SQL sort direction is reversed, and the result slice is reversed back to the correct order before building edges.
|
||||||
|
|
||||||
|
## Default page size
|
||||||
|
|
||||||
|
When neither `first` nor `last` is provided, the default page size is **25**.
|
||||||
|
|
||||||
|
## Adding a new paginated field — checklist
|
||||||
|
|
||||||
|
1. **Schema** — add `enum XxxOrderField`, `input XxxOrder`, `type XxxConnection`, `type XxxEdge`, and the connection field with Relay arguments on the parent type
|
||||||
|
2. **Coredata** — add `*_order_field.go` (with `Column()`, `IsValid()`, marshaling), `CursorKey(field)` method on the entity, and the `LoadAllBy*` query using cursor SQL fragments + `page.NewPage()`
|
||||||
|
3. **API types** — add `*_connection.go` with `OrderBy` alias, connection struct, `NewXxxConnection`, `NewXxxEdge`
|
||||||
|
4. **Resolver** — implement the resolver (authorize, build order, build cursor, call service, build connection)
|
||||||
|
5. **Codegen** — run `go generate` for the relevant API package
|
||||||
Reference in New Issue
Block a user