Documentation
¶
Overview ¶
Package filtering is wowapi's allowlist-driven filter/sort builder — the mechanism behind docs/blueprint/05 §2, "Pagination / filtering / sorting (allowlist-driven; SQL injection impossible by construction)".
Security invariant (enforced by construction, exercised by the tests):
- Column names in emitted SQL come ONLY from FieldSpec.Col / SortSpec.Col, which are framework-controlled. A client picks a *key* into an Allowlist; an unknown key is rejected with a KindValidation error. Client text never becomes a column or an operator.
- Operators are validated against a fixed internal set AND the per-field FieldSpec.Ops permit-list, then rendered from an internal map. Client text never becomes an operator token.
- Client VALUES are never concatenated into SQL. Every value — including each element of an "in" list — is emitted as a $N placeholder and appended to the args slice. The literal value therefore cannot appear in the SQL text.
The result: a caller can only ever produce SQL fragments that reference allowlisted physical columns with parameter placeholders, so injection is impossible regardless of what a client sends.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func KeysetClause ¶
func KeysetClause(s Sort, cur pagination.Cursor, startArg int) (sql string, args []any, nextArg int, err error)
KeysetClause builds the "rows strictly after the cursor" predicate for keyset pagination, matching the given Sort's columns and directions exactly (blueprint 05 §2). It is injection-proof by the same construction as the filter/sort builders: column names come only from the Sort's allowlisted terms, never from the cursor; the cursor supplies only VALUES, bound as $N placeholders (review findings ARCH-31, SEC-22).
For a sort (c1 d1, c2 d2, …) the predicate is the standard lexicographic expansion, correct for mixed directions:
(c1 OP1 v1) OR (c1 = v1 AND c2 OP2 v2) OR (c1 = v1 AND c2 = v2 AND c3 OP3 v3) …
where OPi is ">" for ascending and "<" for descending. It returns "" (no predicate) when the sort or cursor is empty. Placeholders start at startArg; nextArg is the next free placeholder index.
The cursor MUST carry a value for every sort column (it was minted from a row under this sort); a missing value is a KindValidation error, which also guards against a forged cursor whose keys do not match the sort (SEC-22).
func NextCursor ¶
NextCursor mints the opaque keyset cursor for the last row returned under sort s, binding s's signature so KeysetClause rejects it if a later request changes the sort order (roadmap R7). values must carry exactly one entry per sort column. An empty sort yields a signatureless cursor.
Types ¶
type Allowlist ¶
Allowlist maps client-facing field names to their FieldSpec. A field absent from the allowlist cannot be filtered.
type Condition ¶
Condition is one parsed, validated filter predicate. Field is the client-facing name (kept for introspection); the resolved physical column is held privately so only Parse can set it — external code cannot forge a column.
type FieldSpec ¶
FieldSpec maps a client-facing field to a physical column and the operators permitted on it. Col is framework-controlled and is the only text that can reach the SQL column position.
type Set ¶
type Set struct {
// contains filtered or unexported fields
}
Set is an AND-combined collection of validated Conditions.
func Parse ¶
Parse validates raw client filter input against the allowlist and returns a Set. Wire format (documented contract): raw maps a client field name to one or more "op:value" entries.
{"status": {"eq:active"}} → status = $n
{"age": {"gte:18"}} → age >= $n
{"status": {"in:active,pending"}} → status IN ($n, $n+1) (comma-separated)
{"name": {"like:ac%"}} → name LIKE $n
Multiple entries for one field, and multiple fields, are AND-combined. Fields are processed in sorted order so placeholder numbering is deterministic.
Errors (all KindValidation): unknown field, missing "op:" prefix, unknown operator, an operator not permitted by the field's FieldSpec.Ops, or an empty "in" list.
func (Set) Conditions ¶
Conditions returns a copy of the parsed conditions (introspection only).
func (Set) SQL ¶
SQL renders the conditions into a boolean SQL fragment (no leading WHERE), appending each value to args as a $N placeholder. startArg is the next placeholder number (1-based); the returned nextArg is the following free number. An empty Set returns "", nil, startArg.
Columns are taken from the resolved FieldSpec.Col; values only ever appear as $N. See the package doc for the injection-proof invariant.
type Sort ¶
type Sort struct {
// contains filtered or unexported fields
}
Sort is an ordered list of validated sort terms.
func ParseSort ¶
func ParseSort(raw string, allow SortAllowlist) (Sort, error)
ParseSort validates a raw sort string against the allowlist. Wire format (documented contract): a comma-separated list of "key[:dir]" terms, e.g. "created_at:desc,id:asc". A term without ":dir" defaults to ascending. An empty raw string yields an empty Sort (SQL == "").
Errors (all KindValidation): unknown sort key, or a direction other than "asc"/"desc". Client text only ever selects an allowlisted key and one of the two direction constants — it never reaches the SQL text.
func (Sort) SQL ¶
SQL renders the sort as an "ORDER BY <col> <DIR>, ..." clause using only allowlisted physical columns and validated direction keywords. An empty Sort returns "". The output contains no client-supplied text and needs no args.
func (Sort) Signature ¶
Signature is a canonical, stable string identifying this exact sort order — ordered columns and their directions, e.g. "created_at:desc,id:asc". Two sorts share a signature iff they produce the same ORDER BY. Keyset cursors carry it so a later request under a changed sort fails loudly instead of silently returning wrong pages (roadmap R7). An empty Sort has the empty signature.
type SortAllowlist ¶
SortAllowlist maps client-facing sort keys to their SortSpec.