Documentation
¶
Overview ¶
Package filterx is the reusable column-allowlist registry for list queries.
It translates caller-supplied filter values and a chosen sort into squirrel conditions, ORDER BY clauses, and keyset pagination predicates. Column identifiers and SQL expressions live in the registry as fixed constants — the registry *is* the allowlist (ADR-007): callers supply only values, which squirrel parameterises. Views declare one Registry and route their filters, sort, and cursor through it instead of hand-writing the SQL each time.
Index ¶
- Constants
- Variables
- func Int64Values[T ~int64](values *[]T) []any
- func StringValues[T ~string](values *[]T) []any
- type ColumnSpec
- type Cursor
- type Filter
- type Operator
- type Registry
- func (r *Registry) Condition(f Filter) (sq.Sqlizer, error)
- func (r *Registry) DecodeCursor(token string) (Cursor, error)
- func (r *Registry) EncodeCursor(sortKey, order string, value any, id int64) (string, error)
- func (r *Registry) Keyset(c Cursor) (sq.Sqlizer, error)
- func (r *Registry) OrderBy(sortKey, order string) (string, error)
- func (r *Registry) SortableExists(sortKey string) bool
- func (r *Registry) Validate(f Filter) error
- type Relational
- type SortSpec
- type ValueKind
Constants ¶
const MaxValues = 200
MaxValues caps the number of values accepted in a single multi-value filter. It keeps request URLs small and the bound-parameter count far below SQLite's per-statement variable limit. Callers needing more should switch to a POST search endpoint with a JSON body.
Variables ¶
var ErrInvalidFilter = errors.New("invalid filter")
ErrInvalidFilter is returned for an unknown column, an operator a column does not allow, a missing value list where one is required, or an over-cap value list. Callers map it to a 400.
Functions ¶
func Int64Values ¶
Int64Values converts a caller-supplied slice of an int64-kinded API type (typically a typed ID) into the []any values a Filter expects. Returns nil for a nil input.
func StringValues ¶
StringValues converts a caller-supplied slice of a string-kinded API type (a generated enum, a typed string) into the []any values a Filter expects. Returns nil for a nil input, which parseFilter-style callers treat as "no filter supplied".
Types ¶
type ColumnSpec ¶
type ColumnSpec struct {
// Expr is the scalar SQL column expression, e.g. "g.country_code". Empty when
// Rel is set.
Expr string
// Nullable enables the NULL-safe rewrite for not_in / not_contains (so NULL
// rows are not silently dropped) and is required for is_null / not_null.
Nullable bool
// Rel, when set, translates the column as an EXISTS subquery instead of a
// scalar comparison.
Rel *Relational
// Ops is the set of operators this column allows.
Ops []Operator
}
ColumnSpec defines how one filterable column translates to SQL.
type Cursor ¶
Cursor is the decoded form of an opaque pagination token: the sort it was issued under, the last row's sort value (nil for a NULL value), and the last row's tiebreaker id.
type Filter ¶
Filter is a request to constrain one column. Values is empty for is_null / not_null. For scalar string operators the values must be strings; for relational and scalar-id columns they are the column's value type.
func ParseFilter ¶
ParseFilter resolves a value column's operator and decides whether it contributes a filter. A present operator with no values (and no null check) is treated as no filter; null operators apply with no values.
type Operator ¶
type Operator string
Operator is the closed set of filter operators a column may permit.
func ParseOperator ¶
ParseOperator maps a request string to an Operator, defaulting to OpIn when empty. An unrecognised value is rejected with ErrInvalidFilter.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the per-view allowlist of filterable columns and sortable columns.
func NewRegistry ¶
func NewRegistry(cols map[string]ColumnSpec, sorts map[string]SortSpec, tieBreaker string) *Registry
NewRegistry builds a registry from a column allowlist, a sortable-column allowlist, and the tiebreaker column appended to every ORDER BY and keyset predicate (a unique, non-null column such as the primary key).
func (*Registry) DecodeCursor ¶
DecodeCursor parses a token, validating the embedded sort/order against the registry and coercing the value to the sort column's kind.
func (*Registry) EncodeCursor ¶
EncodeCursor produces an opaque token from the last row of a page: the active sort, order, the row's sort value, and its tiebreaker id. The token is server-issued only; clients pass it back verbatim.
func (*Registry) Keyset ¶
Keyset returns the WHERE predicate that selects rows strictly after the cursor row under its sort and order. The tiebreaker id breaks ties on the (non-unique) sort column so adjacent pages never skip or repeat rows. For nullable sort columns the predicate mirrors SQLite's default NULL placement (NULLs first in ASC, last in DESC).
func (*Registry) OrderBy ¶
OrderBy returns the ORDER BY clause for a sort/order pair, always appending the tiebreaker so non-unique sort columns page deterministically.
func (*Registry) SortableExists ¶
SortableExists reports whether sortKey is an allowlisted sortable column.
type Relational ¶
type Relational struct {
// "SELECT 1 FROM access_log_contributors c WHERE c.access_log_id = ral.id"
// in / not_in append "AND <ValueCol> IN (…)"; is_null / not_null wrap it in
// NOT EXISTS / EXISTS as-is.
Correlated string
// ValueCol is the column the IN list is matched against, e.g. "c.device_id".
ValueCol string
}
Relational describes a column filtered via an EXISTS subquery correlated to the parent row, rather than a scalar comparison — e.g. matching any access_log_contributors row for the parent access_log entry.