query

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// DefaultPageSize is the default number of items per page.
	DefaultPageSize = 50
	// MaxPageSize is the maximum allowed page size.
	MaxPageSize = 500
)
View Source
const (
	CursorPhaseDecode = "decode"
	CursorPhaseScope  = "scope"
)

CursorPhase identifies the cursor validation stage that failed.

View Source
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

func CompareAny(a, b any) (int, error)

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

func QueryContext(pairs ...string) string

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

type CompareFunc[T any] func(a, b T, field string) int

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.

func (*CursorCodec) Decode

func (c *CursorCodec) Decode(token string) (Cursor, error)

Decode verifies and deserializes an opaque cursor token. Returns ErrCursorInvalid on any failure (tamper, format, etc.).

func (*CursorCodec) Encode

func (c *CursorCodec) Encode(cur Cursor) (string, error)

Encode serializes a Cursor into an opaque, HMAC-signed token. Wire format: base64url(json_bytes + "." + hex(hmac-sha256(json_bytes))).

type CursorErrorFunc

type CursorErrorFunc func(ctx context.Context, phase string, err error)

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

type FieldFunc[T any] func(item T, field string) any

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

func RunModeForDemo(demo bool) RunMode

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.

func (RunMode) IsDemo

func (m RunMode) IsDemo() bool

IsDemo reports whether the run mode allows demo-only fallbacks.

func (RunMode) String

func (m RunMode) String() string

String returns a stable lowercase label suitable for structured logs.

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.

type SortDir

type SortDir string

SortDir represents a sort direction.

const (
	// SortASC sorts ascending.
	SortASC SortDir = "ASC"
	// SortDESC sorts descending.
	SortDESC SortDir = "DESC"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL