Search with Filters — FindMany<Model>

A generated model’s list route already takes where/sort query parameters, but that’s REST-only, untyped (a string grammar), and tied to a plain CRUD list. FindMany<Model> gives a procedure the same filtering and sorting capability as a real, typed argument — usable over REST or RPC, and composable with a procedure’s own business logic before or after the query runs.

Schema syntax

Constraints enforced at parse/check time:
  1. FindMany<T> is valid only in procedure-argument position — a model field, a type block field, or a procedure return type can’t use it.
  2. T must be a declared model, not a type block. Filtering needs a real table’s columns; a type has none.

What gets generated

For every model in the schema (unconditionally — same as Create<Model>Input/Update<Model>Input, whether or not any procedure actually declares a FindMany<Model> argument), the server composer generates four things:
  • <Model>Where — one optional filter per filterable scalar field. PostWhere { id: Option<FieldFilterInput<i64>>, title: Option<FieldFilterInput<String>>, ... }
  • <Model>SortField — an enum with one variant per scalar field (every scalar field is sortable; unlike filtering, ordering has no type restriction).
  • <Model>OrderByClause{ field: <Model>SortField, direction: SortDirection }.
  • <Model>FindManyInput{ where: Option<<Model>Where>, orderBy: Option<Vec<<Model>OrderByClause>> }, what the FindMany<Model> argument actually decodes into, plus a build_<model>_query_from_find_many(db, input) function that turns a decoded input into a ready-to-run query builder.
None of this duplicates the REST list route’s filter grammar or field-name validation — <Model>Where::to_filters() calls straight into the same FieldRef field accessors (super::post::title(), super::post::published(), …) the REST ?where= route and every other typed query already use. A field that’s invalid to filter on is a compile error (it’s a struct field, not a string a caller could mistype), not a runtime 400. In your procedure implementation:

Filter operators

The six named filter types below (StringFilter, NumberFilter, etc.) are what the TypeScript and Dart generated clients expose — one interface/class per scalar family, each with only the operators that make sense for it. The Rust side is different: every <Model>Where field, regardless of scalar type, is the same single generic FieldFilterInput<V> (as shown above, PostWhere { id: Option<FieldFilterInput<i64>>, title: Option<FieldFilterInput<String>>, ... }) — there’s one Rust type, not six, and it carries every operator field (eq, ne, in, lt, lte, gt, gte, contains, starts_with, is_null) regardless of V, whether or not they’re meaningful for that particular V. The table below reflects which operators the TypeScript/Dart clients actually surface per scalar family — it does not describe distinct Rust types. Every generated TypeScript/Dart <Model>Where field is one of six shared filter shapes, matching whichever operators actually make sense for that scalar type: Notes:
  • contains/startsWith exist only on StringFilter — the only two types FieldRef’s own .contains()/.starts_with() are implemented for.
  • isNull only makes sense (and is only offered by the generated client types) for a field declared ? in the schema — a required field is never null, so there’s nothing to test.
  • Json, Bytes, enum, and custom type fields aren’t filterable at all — <Model>Where simply has no field for them, matching the untyped REST ?where= route’s own coverage.
  • Relation fields aren’t filterable either — PostWhere only ever covers Post’s own scalar columns, not author.name.

Wire format

Structured JSON, not a string grammar — a FindMany<Post> argument serializes as:
Every operator a filter doesn’t set is simply absent (or null) — the caller only sends what it’s actually filtering on. Multiple operators on the same field combine with AND ({ "gte": 10, "lt": 100 } means “between 10 and 100”); multiple filtered fields also combine with AND. orderBy is a list of { field, direction } clauses, not a field-keyed object. A JSON object’s key order isn’t guaranteed to survive every parser — serde_json::Map alphabetizes keys by default, and other languages’ map/dict implementations make no ordering promise either. A list is the only shape that reliably preserves “sort by published first, then title” instead of silently becoming “sort by title first.” This is identical over REST and RPC: on transport rpc, query is just another key in the unary call’s input object ({"op": "procedure.searchPosts", "input": {"query": {...}}}) — see RPC transport for the general request/response envelope this composes with.

Composing with pagination

FindMany<Model> and PageInput are separate, orthogonal arguments — filtering/sorting is one concern, pagination is another:
There’s no single convenience method that combines them — FindMany<'a, M, PK>’s .limit()/.offset() slice the result set, but computing totalCount for the Page<T> envelope is a second, separate query, same as a generated @@paged list route’s own handler does it. Reuse <Model>Where::to_filters() against both the list query and a .aggregate().count() query so the same predicate applies to both:
See Pagination for the full Page<T>/PageInfo contract this mirrors.

Generated clients

Rust (the same generated types the server uses — include_client_schema! generates the identical <Model>Where/<Model>SortField/ <Model>OrderByClause/<Model>FindManyInput structs, just without the DB-backed build_<model>_query_from_find_many function, which needs a live Cratestack handle a pure HTTP client doesn’t have):
TypeScript — per-model PostWhere/PostFindMany interfaces, backed by shared StringFilter/NumberFilter/BooleanFilter/UuidFilter/ DateTimeFilter/DecimalFilter interfaces (themselves built on shared EqualityFilter<V>/ComparableFilter<V> base shapes) — hardcoded once per package, the same way Page/PageInfo/PageInput are:
Dart (default and riverpod presets both emit the same per-model PostWhere/PostSortField/PostOrderByClause/PostFindMany classes; under riverpod, every one of them — plus the shared filter classes — is @MappableClass()-annotated, so a PostFindMany passed as a @riverpod family-provider argument gets real structural equality instead of comparing by identity):

What this is not

  1. not relation-awarePostWhere only covers Post’s own scalar fields. Filtering by a related model’s field (author.name) isn’t supported; build that inside your procedure’s own logic instead.
  2. not .select/.includeFindMany<Model> covers where/ orderBy only. Typed field-selection/relation-inclusion builders for procedure arguments are a documented future direction, not implemented today.
  3. not a replacement for the REST list route’s ?where=/?sort= — that untyped string grammar still exists, unchanged, for plain CRUD list routes. FindMany<Model> is specifically for procedures that want the same capability as a typed argument.
  4. not free of the same validation the list route already runs — a filter that references a field outside allowed_fields() (a @server_only field, for instance) simply isn’t representable: <Model>Where has no field for it, so there’s nothing to reject at runtime.
  1. PaginationPageInput/Page<T>, the argument/return-type pair FindMany<Model> composes with
  2. RPC Transport — the request/response envelope FindMany<Model> arguments travel in over RPC
  3. TypeScript Client Generation and Dart Client Generation — full per-language client codegen coverage