Documentation
¶
Index ¶
- Constants
- Variables
- func ApplySortToQuery(qb *query.QueryBuilder, sorts []ParsedSort)
- func ApplyToCountQuery(cb *query.CountBuilder, filters []ParsedFilter)
- func ApplyToQuery(qb *query.QueryBuilder, filters []ParsedFilter)
- type Condition
- type FilterOp
- type FilterOption
- type FilterSuffixOp
- type ParsedFilter
- type ParsedSort
- type Predicate
Constants ¶
const MaxSearchTerms = 8
MaxSearchTerms bounds the number of tokens a single ?q= value expands to. A search term is one whitespace-delimited token; beyond this cap, extra tokens are dropped. Keeps statement size bounded against an adversarial input.
Variables ¶
var FilterSuffixes = [...]FilterSuffixOp{ {"_gte", OpGte}, {"_lte", OpLte}, {"_gt", OpGt}, {"_lt", OpLt}, {"_like", OpLike}, {"_in", OpIn}, }
FilterSuffixes is the canonical operator-suffix table for the equality, comparison, LIKE, and IN operators. Order matters: longer suffixes MUST be tested before their shorter prefixes (e.g. `_gte` before `_gt`, otherwise `?score_gte=5` matches `_gt` and leaves an `e=` field-name fragment). The table is a pure function of the operator set, so it is hoisted to a package var — ParseFilters/ParseSort no longer rebuild it per call.
Functions ¶
func ApplySortToQuery ¶
func ApplySortToQuery(qb *query.QueryBuilder, sorts []ParsedSort)
applySortToQuery applies parsed sorts to a query builder.
func ApplyToCountQuery ¶
func ApplyToCountQuery(cb *query.CountBuilder, filters []ParsedFilter)
applyFiltersToCountQuery applies parsed filters to a count builder.
func ApplyToQuery ¶
func ApplyToQuery(qb *query.QueryBuilder, filters []ParsedFilter)
applyFiltersToQuery applies parsed filters to a query builder.
Types ¶
type Condition ¶ added in v0.18.0
Condition is a single WHERE fragment (SQL + args) produced by SearchConditions. Convert to a hook.WhereClause and append via qb.Where(c.SQL, c.Args…) — the query builder wraps each Where in parens and AND-composes them, so multi-field search clauses combine safely with owner/tenant/soft-delete scopes.
func BuildPredicate ¶ added in v0.20.0
BuildPredicate compiles a validated predicate tree into one WHERE Condition: a fully-parenthesized SQL string with sequential $N placeholders in depth-first order and a matching, same-order Args slice. Field names are interpolated (they came from the schema allow-list in ParseWhere); every value is a bound arg. Hand the result to qb.Where(c.SQL, c.Args...) ONCE — the builder wraps it in its own parens and AND-joins it to the framework scopes, so the user's boolean logic can never escape to widen a scope.
The $N numbers are positional only; core/query.renumberPlaceholders rewrites them left-to-right and advances by len(Args), so the sole invariant is that placeholders appear in the same order as Args — which building both in one DFS pass guarantees.
func SearchConditions ¶ added in v0.18.0
SearchConditions builds a slice of AND-composed search conditions from a free-text term over the given DB column names. Each whitespace- delimited token produces one Condition whose SQL is a parenthesized OR-group: (LOWER(f1) LIKE $1 ESCAPE '\' OR LOWER(f2) LIKE $2 ESCAPE '\'). Every Condition must match (AND); within one Condition, any field may match (OR). A blank/whitespace-only term returns nil (no conditions).
Case contract: LOWER() is ASCII-only on SQLite and locale-aware on Postgres, so matching is ASCII-case-insensitive everywhere. Unicode case folding is a Postgres bonus. The token itself is lowercased before building the LIKE pattern so the comparison is consistent across dialects.
LIKE metacharacters (%, _, \) in each token are escaped via the existing escaper so they match literally — a user searching for "50%" finds rows containing "50%", not every row with any character sequence. The args are ordered to match the $N placeholders left-to-right; the crud query builder renumbers $N on Build.
type FilterOption ¶ added in v0.35.0
type FilterOption func(*filterOpts)
FilterOption tunes ParseFilters behavior.
func Allow ¶ added in v0.35.0
func Allow(keys ...string) FilterOption
Allow declares extra query-param keys that are NOT entity fields but are legitimately consumed elsewhere on the request (a BeforeList hook, custom middleware). Strict parsing skips them instead of rejecting them, so a host keeps typo-protection for real fields without falling back to Lenient (which disables it entirely). Keys are matched exactly.
func Lenient ¶ added in v0.35.0
func Lenient() FilterOption
Lenient restores the pre-strict behavior: an unknown top-level filter key is silently dropped instead of returning an error. It exists as a migration escape hatch for apps that historically relied on unrecognized query params being ignored. Prefer the strict default — a dropped filter returns an UNFILTERED result set, which is a data-exposure and broken-client hazard.
type FilterSuffixOp ¶ added in v0.36.0
FilterSuffixOp pairs a query-string operator suffix (e.g. "_gt") with its FilterOp. Exported so the CRUD layer's nested-filter parser can share the same canonical table (no per-call rebuild, no duplicate literal to drift between packages).
type ParsedFilter ¶
ParsedFilter represents a single parsed filter from query parameters.
func ParseFilters ¶
func ParseFilters(r *http.Request, fields []schema.Field, opts ...FilterOption) ([]ParsedFilter, error)
ParseFilters extracts filters from query parameters based on entity fields. Supported patterns:
?field=value → equals ?field_gt=value → greater than ?field_lt=value → less than ?field_gte=value → greater than or equal ?field_lte=value → less than or equal ?field_like=value → LIKE (contains) ?field_in=v1,v2,v3 → IN
Only fields present in the schema are accepted. Hidden fields are excluded from the allow-list (mirroring ParseSort): building a WHERE predicate on a column the caller can't read turns row-count/result changes into a value-disclosure oracle — an attacker could probe a Hidden column (e.g. a password hash) via ?password_hash_like=… and exfiltrate it prefix by prefix. A Hidden field name is treated as an unknown filter param and never produces a ParsedFilter.
STRICT by default: an unknown top-level filter key (a typo like ?stauts=active, or a suffixed op on a non-field) returns a structured error rather than being silently dropped. Dropping it would return an UNFILTERED 200 — a broken client reads the whole table and an attacker's probe looks identical to the real query. Reserved list controls (sort, page, cursor, …) and nested relation filters (dotted keys like author.name, validated separately by parseNestedFilters) are skipped, not rejected. Pass Lenient to restore the old drop-silently behavior.
This is a thin wrapper around ParseFiltersValues that parses the request URL once. Callers that already have a url.Values (e.g. the CRUD List handler, which parses once and threads the result through every helper) should call ParseFiltersValues directly to avoid the re-parse.
func ParseFiltersValues ¶ added in v0.36.0
func ParseFiltersValues(q url.Values, fields []schema.Field, opts ...FilterOption) ([]ParsedFilter, error)
ParseFiltersValues is the allocation-conscious variant of ParseFilters: it accepts an already-parsed url.Values so a caller that parsed ?field=value once can reuse it across filter/sort/paginate/include helpers without re-paying url.URL.Query (which re-parses RawQuery and allocates a fresh url.Values on every call). Behaviour is identical to ParseFilters for the same underlying query string.
type ParsedSort ¶
ParsedSort represents sort direction for a field.
func ParseSort ¶
ParseSort extracts sort information from query parameters. Supported: ?sort=field (ascending), ?sort=-field (descending).
Hidden fields are excluded from the allow-list: sorting by a hidden column reveals row ordering by a value the caller can't read, which is an information-disclosure path. Unknown fields fail closed with a 400-shaped error rather than being silently ignored — silent drop turns probe attempts into "the API works the same with or without this param" oracles that mask broken client code.
Thin wrapper around ParseSortValues; callers that already hold a url.Values should call ParseSortValues directly.
func ParseSortValues ¶ added in v0.36.0
ParseSortValues is the allocation-conscious variant of ParseSort: it accepts an already-parsed url.Values so the CRUD List handler can thread the same parsed query through every helper.
type Predicate ¶ added in v0.20.0
type Predicate struct {
// Leaf fields (Children == nil):
Field string
Op FilterOp
Value string // for scalar ops
Values []string // for OpIn
// Group fields (Children != nil):
Or bool // false = AND, true = OR
Children []Predicate
}
Predicate is a node in a boolean filter tree: either a LEAF (one field/op/value comparison) or a GROUP (AND/OR of child predicates). It is the parsed, validated form of a `?where=<json>` request — every field has already been checked against the entity's schema allow-list and every operator against the supported set, so BuildPredicate may interpolate field names trusting they are safe while binding all values as placeholders.
Groups let callers express nested boolean logic the flat query-param filters cannot — e.g. `status = A OR (priority = high AND assignee = me)`. The whole tree compiles to ONE parenthesized WHERE clause that the query builder AND-composes with the framework's owner/tenant/ soft-delete scopes; a user OR-group can never widen past those scopes because each is a separate, individually-parenthesized clause.
func ParseWhere ¶ added in v0.20.0
ParseWhere parses a `?where=<json>` predicate tree and validates every leaf field against the schema allow-list (Hidden fields excluded, same value-disclosure-oracle rationale as ParseFilters) and every operator against the supported set. Returns (nil, nil) when raw is empty. Any unknown field, unknown operator, malformed JSON, empty/ambiguous node, or a tree that exceeds the depth/node bounds returns an error — the caller maps it to 400. On success the tree is safe for BuildPredicate to compile.