filter

package
v0.37.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Index

Constants

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

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

type Condition struct {
	SQL  string
	Args []any
}

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

func BuildPredicate(p *Predicate) Condition

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

func SearchConditions(fields []string, term string) []Condition

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 FilterOp

type FilterOp string

FilterOp represents a comparison operator for query filtering.

const (
	OpEq   FilterOp = "eq"
	OpGt   FilterOp = "gt"
	OpLt   FilterOp = "lt"
	OpGte  FilterOp = "gte"
	OpLte  FilterOp = "lte"
	OpLike FilterOp = "like"
	OpIn   FilterOp = "in"
)

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

type FilterSuffixOp struct {
	Suffix string
	Op     FilterOp
}

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

type ParsedFilter struct {
	Field string
	Op    FilterOp
	Value string
}

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

type ParsedSort struct {
	Field string
	Desc  bool
}

ParsedSort represents sort direction for a field.

func ParseSort

func ParseSort(r *http.Request, fields []schema.Field) ([]ParsedSort, error)

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

func ParseSortValues(q url.Values, fields []schema.Field) ([]ParsedSort, error)

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

func ParseWhere(raw string, fields []schema.Field) (*Predicate, error)

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.

Jump to

Keyboard shortcuts

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