Skip to content

feat(v0.2): seek-predicate builder, signed cursors, net/http + GraphQL helpers, Postgres example - #6

Open
hpower2 wants to merge 5 commits into
mainfrom
feat/v0.2-good-first-issues
Open

hpower2 wants to merge 5 commits into
mainfrom
feat/v0.2-good-first-issues

Conversation

@hpower2

@hpower2 hpower2 commented Jun 11, 2026

Copy link
Copy Markdown
Owner

v0.2 — the seek builder + wire-format helpers

Implements five v0.2 "good first issues" for the cursor pagination library. All
work stays in the dependency-free core module (stdlib only); no sub-modules.

Closes

How each was implemented & tested

#1 Seek-predicate builder (seek.go) — SeekKey/Column/Direction/Dialect
emit the row-wise seek WHERE and matching ORDER BY from a typed composite key
with per-column ASC/DESC. Expands to the portable lexicographic form
(a > ?) OR (a = ? AND b > ?); supports database/sql (?) and pgx ($N)
placeholders; backward paging flips effective direction and comparison operators.
Golden tests assert exact SQL strings for forward/backward × ASC/DESC.

#2 HMAC-signed cursors (sign.go) — SignedCodec signs tokens as
base64url(payload).base64url(hmac-sha256); Decode verifies via constant-time
hmac.Equal and returns ErrTampered on forgery/wrong key. Tests: round-trip,
payload/signature tamper, wrong-key rejection, malformed input, key-copy isolation.

#3 net/http param helper (httpparam.go) — ParseParams parses ?cursor=&limit=
and clamps the limit (default/max caps, safe zero-value config). httptest-based
tests cover over-cap, missing, zero, negative, non-numeric, and custom param names.

#4 GraphQL adapter (graphql.go) — Connect builds a Relay Connection
(edges + pageInfo with startCursor/endCursor/hasNextPage/hasPreviousPage),
spec-compliant JSON tags, pure structs. Tests cover basic/empty/single/multi + JSON shape.

#5 Example (example_postgres_test.go) — runnable Example_postgresREST uses the
seek builder to paginate a (created_at, id)-sorted table behind an httptest handler
against an in-memory fake (no live DB), walks three pages, and prints the exact pgx SQL.

Verification (core module)

gofmt -l .   -> (empty)
go vet ./... -> ok
go test -race ./... -> ok  github.com/hpower2/cursor

CI keeps the Go 1.22 + 1.23 matrix and adds a "go mod tidy is clean" check.

🤖 Generated with Claude Code

hpower2 added 5 commits June 12, 2026 01:46
Add a typed composite sort key (SeekKey/Column/Direction) that emits the
row-wise seek WHERE predicate and the matching ORDER BY from a key with
per-column ASC/DESC. The predicate expands into the portable lexicographic
form ((a > ?) OR (a = ? AND b > ?)) so it works on every SQL engine, and
supports both database/sql "?" and pgx "$N" placeholders via Dialect.
Backward paging flips each column's effective direction (and comparison
operator) so the database walks toward the previous page. Args() expands
cursor values to match the placeholders. Golden tests assert exact SQL
strings for forward/backward and ASC/DESC combinations.

Closes #1
Add SignedCodec, which encodes cursors as base64url(payload).base64url(hmac)
using crypto/hmac with SHA-256 so tokens cannot be forged or probed. Decode
verifies the signature with a constant-time hmac.Equal before unmarshalling,
returning ErrTampered for altered or wrong-key tokens and ErrInvalidCursor
for structurally malformed ones. The key is copied on construction so later
mutation of the caller's slice is harmless. Tests cover round-trip,
payload/signature tampering, wrong-key rejection, malformed input, and key
isolation.

Closes #2
Add ParseParams, which reads ?cursor=&limit= from an *http.Request and
returns a PageParams with the limit clamped per LimitConfig: missing, empty,
non-numeric, or non-positive limits fall back to the default; values above
the max are clamped down. A zero-value LimitConfig stays safe (default 20,
max 100) and a default larger than max is itself clamped. Param names are
configurable. httptest-based tests cover over-cap, missing, zero, negative,
non-numeric, and custom-param cases.

Closes #3
Add Connect, which builds a Relay-style Connection (edges + pageInfo) from a
page of items, a per-item cursor function, and hasNext/hasPrev flags. PageInfo
carries startCursor, endCursor, hasNextPage, and hasPreviousPage with JSON
tags matching the GraphQL Cursor Connections spec; start/end are derived from
the first/last edges and blank for an empty page. Pure structs, no deps. Tests
cover basic, empty, single, multi-item, and JSON-shape (spec field names).

Closes #4
Add example_postgres_test.go: a runnable Example that uses the seek-predicate
builder to paginate a (created_at, id)-sorted table behind an httptest
handler, walking three pages against an in-memory fake data source (no live
DB) and printing the exact pgx SQL the builder emits. Document all v0.2
helpers in the README, add an Unreleased CHANGELOG entry, and extend CI with a
"go mod tidy is clean" check while keeping the Go 1.22 + 1.23 matrix and the
gofmt/vet/test -race gates.

Closes #5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant