Documentation
¶
Index ¶
- Constants
- func ApplyCursor[T any](items []T, params ListParams, fieldValue FieldFunc[T]) ([]T, error)
- func CompareAny(a, b any) (int, error)
- func QueryContext(pairs ...string) string
- func Sort[T any](items []T, cols []SortColumn, compareField CompareFunc[T])
- func SortScope(cols []SortColumn) string
- func ValidateCursorScope(cur Cursor, sort []SortColumn, queryCtx string) error
- type CompareFunc
- type Cursor
- type CursorCodec
- type CursorErrorFunc
- type FieldFunc
- type ListParams
- type PageParams
- type PageResult
- func BuildPageResult[T any](items []T, limit int, codec *CursorCodec, sort []SortColumn, queryCtx string, ...) (PageResult[T], error)
- func ExecutePagedQuery[T any](ctx context.Context, cfg PagedQueryConfig[T]) (PageResult[T], error)
- func MapPageResult[T any, U any](src PageResult[T], fn func(T) U) PageResult[U]
- type PagedQueryConfig
- type RunMode
- type SortColumn
- type SortDir
Constants ¶
const ( // DefaultPageSize is the default number of items per page. DefaultPageSize = 50 // MaxPageSize is the maximum allowed page size. MaxPageSize = 500 )
const ( CursorPhaseDecode = "decode" CursorPhaseScope = "scope" )
CursorPhase identifies the cursor validation stage that failed.
const MaxCursorTokenBytes = 4096
MaxCursorTokenBytes is the maximum accepted length of an opaque cursor token. Bounds decode work to protect against oversize-cursor DoS amplification. 4 KiB easily accommodates several keyset values plus signature overhead. ref: kubernetes apiserver continue-token guidance.
Variables ¶
This section is empty.
Functions ¶
func ApplyCursor ¶
func ApplyCursor[T any](items []T, params ListParams, fieldValue FieldFunc[T]) ([]T, error)
ApplyCursor skips items at or before the cursor position, then limits to FetchLimit() (Limit+1 for N+1 hasMore detection).
Precondition: items must already be sorted by params.Sort columns (via Sort). Behavior is undefined on unsorted input.
Returns ErrCursorInvalid if CursorValues length does not match Sort columns, if Sort is empty when CursorValues is present, or if cursor value types are incompatible.
func CompareAny ¶
CompareAny compares two values that may be string, float64, or time.Time. It handles cross-type comparison between time.Time and RFC3339Nano strings, which occurs when fieldValue returns time.Time but cursor values are strings from JSON decode.
Supported type pairs: string↔string, float64↔float64, int↔float64, time.Time↔time.Time, time.Time↔string (parsed as RFC3339Nano). All other combinations return ErrCursorInvalid.
func QueryContext ¶
QueryContext computes a fingerprint of the query context (endpoint identity + filter parameters). Cursors are only valid for the same query context. Pass key-value pairs describing the query identity, e.g.:
QueryContext("endpoint", "order-query")
QueryContext("endpoint", "audit-query", "eventType", "login", "actorId", "user-1")
QueryContext("endpoint", "device-command", "deviceId", "dev-1")
func Sort ¶
func Sort[T any](items []T, cols []SortColumn, compareField CompareFunc[T])
Sort sorts items in-place by the given sort columns using compareField.
func SortScope ¶
func SortScope(cols []SortColumn) string
SortScope computes a short hex fingerprint of the sort column definition. Cursors are only valid for queries with the same sort scope.
func ValidateCursorScope ¶
func ValidateCursorScope(cur Cursor, sort []SortColumn, queryCtx string) error
ValidateCursorScope checks that the decoded cursor carries the expected sort scope and query context, and that the value count matches. Both fields are mandatory on both sides — empty scope/context is rejected regardless of whether it comes from the cursor or from the caller.
Types ¶
type CompareFunc ¶
CompareFunc compares a single named field of two entities, returning -1/0/+1.
type Cursor ¶
type Cursor struct {
Values []any `json:"v"`
Scope string `json:"s"` // hex hash of sort column definition (mandatory)
Context string `json:"c"` // query context fingerprint (mandatory)
}
Cursor holds the keyset values at a pagination boundary. Values correspond 1:1 with the SortColumns of the query.
type CursorCodec ¶
type CursorCodec struct {
// contains filtered or unexported fields
}
CursorCodec encodes and decodes cursors with HMAC-SHA256 tamper protection. The HMAC key must be at least 32 bytes. Supports key rotation: verification tries the current key first, then the previous key (if set).
func NewCursorCodec ¶
func NewCursorCodec(current []byte, previous ...[]byte) (*CursorCodec, error)
NewCursorCodec creates a CursorCodec. current is used for signing; verification tries current first, then previous (if set). Both keys must be at least 32 bytes. current and previous must differ — using the same value for both would silently degrade key rotation to a no-op and is therefore rejected at construction time.
type CursorErrorFunc ¶
CursorErrorFunc is called when cursor decode or scope validation fails.
func LogCursorError ¶
func LogCursorError(logger *slog.Logger, sliceName string) CursorErrorFunc
LogCursorError returns a CursorErrorFunc that emits a structured slog.Info record when a cursor validation fails. Returns nil if logger is nil.
The raw cursor string is intentionally omitted — opaque base64 that may encode internal offsets; aligned with k8s apiserver / etcd / MinIO practice.
Level is Info (not Warn) because cursor errors are client input failures, not server-side degradation.
type FieldFunc ¶
FieldFunc extracts a cursor-comparable value from an entity by field name. Returned values must be string, int, float64, or time.Time — other types will cause CompareAny to return an error. Time fields should return time.Time (not a formatted string) so that CompareAny uses temporal comparison.
type ListParams ¶
type ListParams struct {
Limit int // user-requested limit (normalized)
CursorValues []any // decoded cursor keyset values; nil for first page
Sort []SortColumn // sort columns in order of priority
}
ListParams holds pagination parameters for repository list operations. Passed from the service layer (after cursor decoding) to the repository.
func (ListParams) FetchLimit ¶
func (lp ListParams) FetchLimit() int
FetchLimit returns Limit+1 for the N+1 hasMore detection pattern.
type PageParams ¶
type PageParams struct {
Limit int // requested page size
Cursor string // opaque cursor token; empty for first page
}
PageParams holds pagination parameters parsed from an HTTP request.
func (*PageParams) Normalize ¶
func (p *PageParams) Normalize()
Normalize clamps Limit to [1, MaxPageSize], applying DefaultPageSize for zero or negative values.
type PageResult ¶
type PageResult[T any] struct { Items []T `json:"data"` NextCursor string `json:"nextCursor"` HasMore bool `json:"hasMore"` }
PageResult wraps a page of results with cursor metadata.
func BuildPageResult ¶
func BuildPageResult[T any]( items []T, limit int, codec *CursorCodec, sort []SortColumn, queryCtx string, extractCursor func(T) []any, ) (PageResult[T], error)
BuildPageResult processes raw query results (which may contain limit+1 rows for N+1 hasMore detection), trims to the requested limit, and encodes the cursor for the next page from the last visible item.
sort defines the sort columns; the generated cursor embeds a sort scope fingerprint so cursors cannot be reused across different sort definitions.
queryCtx is the query context fingerprint (from QueryContext). The cursor embeds this so it cannot be replayed against a different query context (e.g. different endpoint, different filter values).
extractCursor is called on the last item to extract the keyset values for the next-page cursor. It must return values corresponding 1:1 to the sort columns used in the query.
func ExecutePagedQuery ¶
func ExecutePagedQuery[T any](ctx context.Context, cfg PagedQueryConfig[T]) (PageResult[T], error)
ExecutePagedQuery normalizes the request, decodes and validates the cursor, calls Fetch, and builds the paginated result. It replaces the ~15-line pattern repeated across 5 service List methods.
func MapPageResult ¶
func MapPageResult[T any, U any](src PageResult[T], fn func(T) U) PageResult[U]
MapPageResult transforms each item in a PageResult using fn, preserving pagination metadata (NextCursor, HasMore). fn must not panic; if an item may be nil, the caller should handle that within fn.
type PagedQueryConfig ¶
type PagedQueryConfig[T any] struct { // Codec signs and verifies cursor tokens. Codec *CursorCodec // PageParams holds the client-supplied limit and cursor token. PageParams PageParams // Sort defines the keyset column ordering for this query. Sort []SortColumn // QueryCtx is the fingerprint from QueryContext(); must match between page requests. QueryCtx string // Fetch retrieves items from the data store using the decoded ListParams. Fetch func(context.Context, ListParams) ([]T, error) // Extract returns cursor keyset values from the last item; count must match Sort. Extract func(T) []any // OnCursorErr is called when cursor decode or scope validation fails; nil is safe. OnCursorErr CursorErrorFunc // RunMode controls fail-open vs fail-closed semantics. Zero value is // RunModeProd (fail-closed) so callers who forget to set this get strict // cursor validation. RunModeDemo only absorbs decode failures (stale key // after restart); scope/context mismatches still return an error because // they indicate a client bug. // // ref: ThreeDotsLabs/watermill — noop is injected at call site, not // inferred from data. Callers declare the mode explicitly. RunMode RunMode }
PagedQueryConfig holds all parameters for ExecutePagedQuery. ref: ent MultiCursorsOptions struct pattern; dispatcherConfig in kernel/assembly
type RunMode ¶
type RunMode uint8
RunMode controls fail-open vs fail-closed semantics in the paged-query helper. The zero value is RunModeProd, so a caller that forgets to set this field defaults to fail-closed (strict cursor validation).
ref: zeromicro/go-zero core/service/serviceconf.go — explicit Mode enum drives behavior; defaults to the strictest value (ProMode).
ref: ThreeDotsLabs/watermill — nopPublisher lives only in _test.go; demo vs production is decided at the injection site, not by sniffing data at runtime. GoCell follows the same principle: callers declare the mode explicitly instead of inferring it from key material.
const ( // RunModeProd rejects malformed or stale cursor tokens with an error. // This is the zero value — unset callers get strict behavior. RunModeProd RunMode = 0 // RunModeDemo allows the paged-query helper to silently fall back to // the first page when a cursor fails to decode (e.g. after a key rotation // in a demo deployment with no operator). Scope/context mismatches still // return errors even in demo mode because they indicate a client bug. RunModeDemo RunMode = 1 )
func RunModeForDemo ¶
RunModeForDemo returns RunModeDemo when demo is true, RunModeProd otherwise. Convenience helper for callers that already track their demo-mode decision as a boolean (e.g. translating kernel/outbox.DurabilityMode at wire time).
Do not extend: this function is the ONLY permitted translation point between kernel/outbox.DurabilityMode (or any other "is-demo" signal) and pkg/query.RunMode. Call it exactly once at Cell Init() time and pass the resulting RunMode down to slice services and PagedQueryConfig via constructor parameters. Do NOT call it again inside individual slice methods, handlers, or repositories — that scatters demo semantics across the call graph and defeats the single wire-time decision. Do NOT add a new RunMode value without a corresponding change in the calling layer; the two enums must stay in 1-to-1 correspondence.
ref: zeromicro/go-zero core/service/serviceconf.go — ServiceConf.Mode is resolved once at MustSetUp() and propagated by injection, not re-sniffed.
type SortColumn ¶
type SortColumn struct {
Name string // SQL column name — must be a trusted identifier, never user input
Direction SortDir // SortASC or SortDESC
}
SortColumn defines a column used in keyset ordering.