Skip to content

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 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 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[] };
{
"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" }
]
}
}
}

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.

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.

Return distinct combinations of the listed fields:

{
"address": {
"$select": { "country": true, "city": true },
"$distinct": ["country", "city"]
}
}

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.

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.

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.