Querying¶
List requests accept four query parameters: where, sort, limit/offset,
and schema_only/schema_exclude. Only fields declared in Meta are accepted.
Filters¶
where is a JSON object of field names and values:
GET /api/user?where={"name": "muffin", "age": {"$gt": 18}}
The value can be a scalar, which means equality, or an object of operators.
Only fields listed in Meta.filters are applied. An unknown field is ignored.
class Users(PWRESTHandler):
class Meta:
model = User
filters = ("name", "age", ("created", {"field": "created_at"}))
Each entry can be:
- a field name: the filter name and column are the same
- a
(name, options)tuple:fieldmaps the filter name to another column,schema_fieldoverrides the field used to deserialize values, andoperatorsets the default operator for scalar values - a
Filterinstance for full control - a Peewee
Fieldor SQLAlchemyColumndirectly
Mapping a public filter name to a different column:
filters = (("user", {"field": "user_id"}),)
# GET /api/order?where={"user": 42} filters on user_id
Changing the default operator:
filters = (("name", {"operator": "$like"}),)
# GET /api/user?where={"name": "muff%"}
Operators¶
These operators work on Peewee, SQLAlchemy, and Mongo. Mongo accepts only the
$-prefixed names; the aliases are for Peewee and SQLAlchemy.
| Operator | Alias | Meaning |
|---|---|---|
$eq |
== |
equal |
$ne |
!= |
not equal |
$lt |
< |
less than |
$le |
<=, $lte |
less than or equal |
$gt |
> |
greater than |
$ge |
>=, $gte |
greater than or equal |
$in |
<< |
value is in a list |
$nin |
value is not in a list | |
$starts |
string starts with | |
$ends |
string ends with |
Backend-specific operators:
| Backend | Extra operators |
|---|---|
| Peewee | $like, $ilike, $contains, $regexp, $between, $null, $none |
| SQLAlchemy | $like, $ilike, $notlike, $notilike, $match, $between |
| Mongo | none |
$in and $nin take a list. $between takes a two-item list on Peewee and
SQLAlchemy, and $null takes a boolean on Peewee. $like takes a SQL pattern
on Peewee and SQLAlchemy. Mongo has no $like; use $starts and $ends, which
escape the value before building a regex.
Peewee's $nin is not implemented. Use $not with $in instead:
{"name": {"$not": [{"$in": ["a", "b"]}]}}
Logic operators¶
Peewee and SQLAlchemy support $or, $and, $not, and $nor. The value is a
list of conditions:
{"age": {"$or": [{"$lt": 18}, {"$gt": 65}]}}
Mongo does not support these operators.
Forgiving by design¶
A bad filter does not fail the request:
- invalid JSON, or a
wherevalue that is not an object, logs a warning and applies no filters - an unknown field is ignored
- a value that fails schema validation skips that one filter; the rest still apply
This keeps exploratory queries from turning into 400s. Validate the values
you care about in a custom filter if you need strict behavior.
Custom filters¶
Subclass the backend filter and override filter:
from muffin_rest.peewee.filters import PWFilter
class CaseInsensitiveFilter(PWFilter):
async def filter(self, collection, *ops, **ctx):
column = self.field
return collection.where(column.ilike(f"%{ops[0][1]}%"))
class Users(PWRESTHandler):
class Meta:
model = User
filters = (CaseInsensitiveFilter("name"),)
The base class parses values with self.schema_field, which is taken from the
generated schema field with the same name. Set schema_field explicitly when
the filter name does not match a schema field.
Sorting¶
sort is a comma-separated list. A leading - means descending:
GET /api/user?sort=name,-created_at
Fields are applied in the order given. Only fields listed in Meta.sorting are
used; unknown fields are ignored.
class Users(PWRESTHandler):
class Meta:
model = User
sorting = ("name", ("created_at", {"default": "desc"}))
A default entry sets the order used when the request has no sort parameter.
The value "desc" sorts descending; any other truthy value sorts ascending.
Default sorting is not applied when a request provides its own sort.
Peewee sorters put NULL values last. SQLAlchemy and Mongo use the database's
default null ordering.
A custom sorter subclasses the backend sorter and returns the modified query:
import sqlalchemy as sa
from muffin_rest.sqlalchemy.sorting import SASort
class LengthSort(SASort):
async def apply(self, collection, *, desc=False, **ctx):
expr = sa.func.length(self.field)
return collection.order_by(expr.desc() if desc else expr)
Use PWSort or MongoSort for the other backends.
Pagination¶
Pagination is active only when Meta.limit is greater than zero:
class Users(PWRESTHandler):
class Meta:
model = User
limit = 20 # default page size
limit_min = 1
limit_max = 100
limit_total = True
GET /api/user?limit=50&offset=100
limit is clamped to [limit_min, limit_max] and offset to >= 0. Invalid
numbers return 400. A limit of 0 is clamped up to limit_min, so it
returns one row by default. Set limit_min = 0 to let limit=0 return all
rows.
Responses carry:
| Header | Meaning |
|---|---|
x-limit |
applied page size |
x-offset |
applied offset |
x-total |
total rows, unless limit_total = False |
With limit_total = False, the backend skips the count query and the x-total
header. This is the usual choice for large tables where an exact count is
expensive. Mongo aggregation pipelines always compute the count.
Setting Meta.limit = 0 disables pagination entirely: no count, no headers,
and no limit/offset handling.
Field selection¶
schema_only and schema_exclude take comma-separated schema field names:
GET /api/user?schema_only=id,name
GET /api/user?schema_exclude=password
The same schema is used for responses and request bodies, so a field excluded
here is also dropped from an incoming POST or PUT. Names must exist in the
schema. Use Meta.schema_unknown = EXCLUDE (the default) to silently drop
unknown request fields.