Query controls
MeshQL JSON queries put field selection and read controls on the entity node.
Control names start with $ on the wire, so they are signed together with the
selection when integrity is enabled.
{ "user": { "$select": { "id": true, "name": true }, "$where": { "field": "active", "op": "eq", "value": true }, "$orderBy": [{ "field": "name", "direction": "asc" }], "$page": { "first": 20 } }}The @meshql/client API accepts this exact query object. There is no separate
client-only selection or controls shape:
const users = await client.query( { user: { $select: { id: true, name: true }, $where: { field: "active", op: "eq", value: true }, $orderBy: [{ field: "name", direction: "asc" }], $page: { first: 20 }, }, },);$select
Section titled “$select”Select scalar fields with true. Select a relation with another read node, so
many relations can have their own controls.
{ "post": { "$select": { "id": true, "title": true, "comments": { "$select": { "id": true, "body": true }, "$page": { "first": 5 } } } }}Use $select on every root and nested relation node.
$where
Section titled “$where”$where is a recursive boolean tree. A leaf comparison has field, op, and
usually value. Combine multiple leaves with and, or, or not nodes, which
nest to any depth.
type WhereExpr = | { and: WhereExpr[] } | { or: WhereExpr[] } | { not: WhereExpr } | { field: string; op: ComparisonOp; value?: JsonScalar | JsonScalar[] };Single condition
Section titled “Single condition”{ "user": { "$select": { "id": true, "name": true }, "$where": { "field": "active", "op": "eq", "value": true } }}and takes an array of expressions; all must match.
{ "user": { "$select": { "id": true, "name": true }, "$where": { "and": [ { "field": "active", "op": "eq", "value": true }, { "field": "age", "op": "gte", "value": 18 } ] } }}or takes an array of expressions; any may match.
{ "user": { "$select": { "id": true, "name": true }, "$where": { "or": [ { "field": "role", "op": "eq", "value": "admin" }, { "field": "role", "op": "eq", "value": "owner" } ] } }}Nested AND / OR / NOT
Section titled “Nested AND / OR / NOT”Each branch is itself a $where expression, so you can mix combinators freely.
not takes a single expression.
{ "user": { "$select": { "id": true, "name": true }, "$where": { "and": [ { "field": "active", "op": "eq", "value": true }, { "or": [ { "field": "role", "op": "eq", "value": "admin" }, { "field": "age", "op": "gte", "value": 21 } ] }, { "not": { "field": "status", "op": "eq", "value": "banned" } } ] } }}This renders to SQL like:
(active = $1 AND (role = $2 OR age >= $3) AND (NOT status = $4))Supported operators are eq, ne, gt, gte, lt, lte, in, nin,
like, ilike, isNull, and isNotNull. in and nin take an array
value; isNull and isNotNull take no value.
Filters reference fields on the current entity. Relation paths such as
author.name are not supported. Computed fields are projection-only and cannot
be used in $where; see Computed fields.
$orderBy
Section titled “$orderBy”Provide one or more sort entries. nulls is optional.
{ "user": { "$select": { "id": true, "name": true }, "$orderBy": [ { "field": "createdAt", "direction": "desc", "nulls": "last" }, { "field": "name", "direction": "asc" } ] }}MeshQL adds a deterministic identifier tiebreaker when needed.
MeshQL uses forward keyset pagination:
{ "user": { "$select": { "id": true, "name": true }, "$page": { "first": 20, "after": "opaque-cursor" } }}first defaults to 50 and cannot exceed 200. Omit after for the first page,
then pass the previous response’s pageInfo.endCursor.
const nextPage = await client.query({ user: { $select: { id: true, name: true }, $page: { first: 20, after: previous.pageInfo.endCursor }, },});Collection reads return { items, pageInfo }. Cursors are opaque and scoped
to the entity, relation path, filtering, and ordering used to create them.
$distinct
Section titled “$distinct”Return distinct combinations of the listed fields:
{ "address": { "$select": { "country": true, "city": true }, "$distinct": ["country", "city"] }}$groupBy and $aggregate
Section titled “$groupBy and $aggregate”Group rows and define named aggregate projections:
{ "order": { "$select": { "status": true }, "$groupBy": ["status"], "$aggregate": { "orderCount": { "fn": "count", "field": "*" }, "revenue": { "fn": "sum", "field": "total" } }, "$orderBy": [{ "aggregate": "revenue", "direction": "desc" }] }}Aggregate functions are count, sum, avg, min, and max. An aggregate
may set "distinct": true.
$having
Section titled “$having”Filter grouped results by an aggregate alias or grouped field:
{ "order": { "$select": { "status": true }, "$groupBy": ["status"], "$aggregate": { "orderCount": { "fn": "count", "field": "*" } }, "$having": { "and": [ { "aggregate": "orderCount", "op": "gte", "value": 10 }, { "field": "status", "op": "ne", "value": "cancelled" } ] } }}$having supports the same comparison and boolean operators as $where.
Where controls are allowed
Section titled “Where controls are allowed”Collection roots and many relations accept $where, $orderBy, and
$page. A one relation rejects those controls. Grouping, aggregation,
$having, and $distinct apply to collection roots.
QL brace syntax is selection-only and requires an explicit format: "ql" /
X-Mesh-Format: ql. JSON is the default and full query format — use it when a
query needs read controls. See the query format spec for
the wire contract and read controls spec for limits and
conformance rules.