ObjectQL
Le format de requête structuré utilisé par /api/v1/data/*, les vues, les rapports et les outils d'IA.
ObjectQL
ObjectQL est le format de requête JSON consommé par le moteur de données. C'est ce que
/api/v1/data/:object accepte, ce vers quoi les vues sont compilées, ce que les rapports
sérialisent, et ce que l'outil d'IA query_data produit.
Deux formes :
- Liste simple — passez
where,orderBy,limit,expandetc. comme paramètres de chaîne de requête àGET /api/v1/data/:object. - Requête avancée — envoyez le corps de requête complet en POST vers
/api/v1/data/:object/query(requis pourgroupByetaggregations).
Forme de la requête
{
object: string, // required — target object name
fields?: string[], // projection (default: all visible fields)
where?: FilterCondition, // see Filters
orderBy?: SortNode[], // [{ field, order: 'asc' | 'desc' }]
limit?: number, // page size
offset?: number, // offset pagination
cursor?: string, // cursor pagination (preferred)
expand?: string[], // batch-resolve relation fields
joins?: JoinNode[], // explicit joins (inner | left | right | full)
groupBy?: (string | GroupByNode)[],
aggregations?: AggregationNode[], // sum/avg/count/min/max/...
having?: FilterCondition, // filter after aggregation
distinct?: boolean
}Source du schéma : packages/spec/src/data/query.zod.ts.
Filtres
where est un arbre de conditions. Les opérateurs de champ sont préfixés par
$ ; les combinateurs logiques ($and, $or, $not) se situent au niveau supérieur.
Comparaison
| Opérateur | Exemple | Notes |
|---|---|---|
$eq | { status: { $eq: "open" } } | Égalité |
$ne | { priority: { $ne: "low" } } | Inégalité |
$gt, $gte, $lt, $lte | { amount: { $gte: 1000 } } | Comparaisons |
Tous les opérateurs de comparaison acceptent aussi une référence de champ pour
la comparaison inter-champs : { end_at: { $gt: { $field: "start_at" } } }.
Ensemble et intervalle
| Opérateur | Exemple |
|---|---|
$in | { status: { $in: ["new","open"] } } |
$nin | { owner_id: { $nin: ["u_1","u_2"] } } |
$between | { created_at: { $between: ["2026-01-01","2026-02-01"] } } |
Chaîne de caractères
| Opérateur | Exemple | Notes |
|---|---|---|
$contains | { subject: { $contains: "refund" } } | Insensible à la casse |
$notContains | { notes: { $notContains: "test" } } | |
$startsWith | { email: { $startsWith: "@" } } | |
$endsWith | { email: { $endsWith: "@acme.com" } } |
Null et existence
| Opérateur | Exemple |
|---|---|
$null | { closed_at: { $null: true } } |
$exists | { external_id: { $exists: false } } |
Combinateurs logiques
{
"$and": [
{ "status": { "$eq": "open" } },
{
"$or": [
{ "priority": { "$in": ["high", "urgent"] } },
{ "due_at": { "$lt": { "$cel": "now()" } } }
]
}
]
}Les expressions CEL peuvent être intégrées avec { "$cel": "..." } — utile pour
les filtres « relatifs à maintenant » que le serveur évalue au moment de la requête.
Tri
"orderBy": [
{ "field": "priority", "order": "desc" },
{ "field": "created_at", "order": "asc" }
]Forme abrégée en chaîne de requête : ?orderBy=-priority,created_at.
Pagination
Curseur (recommandé). La réponse inclut nextCursor ; renvoyez-le
en tant que cursor sur la requête suivante.
GET /api/v1/data/ticket?limit=50&orderBy=-created_at
→ { items: [...], nextCursor: "eyJ..." }
GET /api/v1/data/ticket?limit=50&cursor=eyJ...Décalage. Plus simple, mais se dégrade sur les grandes tables.
GET /api/v1/data/ticket?limit=50&offset=200Le runtime plafonne limit par objet via ObjectSpec.maxPageSize
(par défaut 200).
Relations — expand
expand résout les clés étrangères par lots pour éviter le N+1.
{
"object": "support_ticket",
"expand": ["assignee", "customer.account"],
"limit": 20
}Renvoie chaque ticket avec assignee et customer.account matérialisés
sous forme d'objets imbriqués au lieu de simples identifiants.
Jointures
Pour les jointures ad hoc en dehors du graphe de métadonnées :
"joins": [
{ "type": "left", "object": "user", "as": "u", "on": "assignee_id = u.id" }
]type : inner | left | right | full. Les tables jointes sont
accessibles dans where, orderBy et aggregations via l'alias as.
Agrégation
POST /api/v1/data/:object/query uniquement.
{
"object": "order",
"where": { "status": { "$ne": "cancelled" } },
"groupBy": [
"customer_id",
{ "field": "created_at", "dateGranularity": "month" }
],
"aggregations": [
{ "function": "sum", "field": "amount", "alias": "total_sales" },
{ "function": "count", "alias": "order_count" }
],
"having": { "total_sales": { "$gt": 10000 } },
"orderBy": [{ "field": "total_sales", "order": "desc" }],
"limit": 25
}Fonctions : count, sum, avg, min, max, count_distinct,
array_agg, string_agg.
Granularité temporelle (pour le regroupement par tranches de temps) :
day | week | month | quarter | year.
Distinct
{ "object": "ticket", "fields": ["status"], "distinct": true }Recherche
La recherche en texte intégral sur les champs searchable: true est exposée à
GET /api/v1/search?q=...&object=ticket. Les règles de scoring par objet
sont configurées sur le spec de l'objet.
Où ObjectQL apparaît
GET /api/v1/data/:object— forme en chaîne de requêtePOST /api/v1/data/:object/query— corps complet, prend en charge les agrégations- Définitions de vues (
filter,sort) — compilées en ObjectQL - Rapports — sérialisés en ObjectQL
- L'outil d'IA
query_data— produit un corps ObjectQL pour la file d'attente d'approbation
Voir aussi
- REST API — points de terminaison qui consomment ObjectQL
- Field types — ce qui est interrogeable
- CEL — langage d'expression utilisé dans les filtres
$cel @objectstack/spec/data/query.zod.ts— schéma faisant autorité