Aller au contenu principal

8. Langage de requête de projection (filtres, tri et pagination)

Le langage de requête de projection (L3) est le contrat de lecture officiel et public d'AUSUS 2.0. C'est la manière stable de demander à une projection un sous-ensemble de ses lignes — filtré, trié et paginé — sans jamais exposer le moindre détail de stockage.

Il est purement additif : le modèle d'écriture (Kernel, EntityDefinition, EntitySchema, Engine, l'API d'écriture du Runtime) est inchangé, tous les tests existants passent encore, et une lecture de projection sans paramètre de requête renvoie exactement ce qu'elle renvoyait avant.

Portée de la v1. Ce contrat couvre where / orderBy / limit / offset sur les champs scalaires exposés d'une projection. Il ne couvre volontairement pas les jointures, les relations inverses, les agrégations, les champs calculés, le reporting, la disponibilité ni les anti-jointures. Ces capacités s'appuieront sur cette fondation dans des couches ultérieures.


1. Sa place dans le pipeline

findAll(tenant) → WHERE → ORDER BY → LIMIT/OFFSET → visibilité + expand → lignes

La requête est analysée et validée avant toute E/S, puis appliquée dans le runtime sur les lignes renvoyées par le driver. Cela la rend agnostique au driver : le driver mémoire fonctionne sans changement, et un futur driver SQLite/Postgres pourra pousser le même contrat jusqu'au SQL via le SPI existant Repository::findPaged(limit, offset, filters, sort) — sans changer un seul octet du contrat public.

Le filtrage et le tri opèrent sur les champs scalaires exposés de la projection. Les gardes de visibility par champ s'appliquent toujours à la sortie rendue : un champ masqué pour l'acteur courant est omis de chaque ligne, exactement comme avant.


2. L'objet requête

Le contrat canonique est un simple tableau associatif passé comme argument params de RuntimeEntity::read($projection, $params, $context). Il possède exactement quatre clés optionnelles :

CléFormeSens
whereun nœud de filtre (voir ci-dessous)prédicat de ligne
orderByliste de { field, dir } (dir = asc|desc)tri, première clé prioritaire
limitentier 0 … 200taille de page (max 200)
offsetentier ≥ 0lignes à ignorer

Toute autre clé de premier niveau est rejetée (fail-closed). Un tableau vide [] est sans effet — la valeur par défaut, sûre pour la non-régression.

Nœuds de filtre

Un where est un arbre booléen récursif :

  • Condition['field' => 'status', 'op' => 'eq', 'value' => 'open']
  • Groupe ET['and' => [ nœud, nœud, … ]]
  • Groupe OU['or' => [ nœud, nœud, … ]]
  • Liste simple[ nœud, nœud, … ] est un ET implicite

Les groupes s'imbriquent librement, donc (status = open ET (priority >= 3 OU assignee est null)) est exprimable.

Opérateurs

OpérateurS'applique àValeur
eq, netout scalairerequise
lt, lte, gt, gtenombres / chaînesrequise
contains, startsWith, endsWithchaînesrequise
isNull, isNotNulltout champ nullableomettre

Les comparaisons sont numériques quand les deux côtés sont numériques, sinon lexicographiques. Les valeurs null ne correspondent jamais à un opérateur d'ordre/relationnel et sont triées en dernier, quel que soit le sens.


3. Exemple PHP (intégré / tests)

use Ausus\Engine\Runtime\DefaultEntityEngine;

$runtime = $engine->bind($schema, $driver);

$rows = $runtime->read('list', [
'where' => ['and' => [
['field' => 'status', 'op' => 'eq', 'value' => 'open'],
['field' => 'priority', 'op' => 'gte', 'value' => 3],
]],
'orderBy' => [['field' => 'priority', 'dir' => 'desc']],
'limit' => 20,
'offset' => 0,
], $context);

null / OU / groupes imbriqués sont entièrement disponibles ici. Toute requête malformée lève Ausus\Engine\Query\QueryError — elle n'est jamais silencieusement coercée.


4. En HTTP

GET /api/entities/{entity}/projections/{projection} accepte la requête sous forme de paramètres plats de chaîne de requête. L'api-runtime les traduit dans le contrat structuré ci-dessus ; le runtime reste l'unique autorité fail-closed.

# raccourci : toute clé non réservée ⇒ eq
GET /api/entities/task/projections/list?status=open

# filtres explicites — virgule = ET
GET …/list?where=status:eq:open,priority:gte:3

# opérateurs sans valeur
GET …/list?where=assignee:isNull

# tri (alias : sort) + pagination
GET …/list?orderBy=priority:desc,title:asc&limit=20&offset=40

Clés réservées : where, orderBy, sort, limit, offset. Toute autre clé est traitée comme un filtre eq raccourci et fusionnée dans une seule liste ET. Le OU et les groupes imbriqués ne sont volontairement pas exprimables dans le raccourci HTTP (il est ET-seulement par conception) — utilisez les params structurés pour cela ; le contrat de lecture public reste volontairement petit.

L'enveloppe de réponse est inchangée : { "rows": [ … ] }.

Codes de statut

StatutQuand
200succès (y compris une liste rows vide)
400requête malformée — champ inconnu, opérateur inconnu, valeur manquante, sens de tri invalide, limit/offset hors bornes, paramètre inconnu
403 / 404 / 422erreurs inchangées d'autorisation / résolution / transition

Une requête malformée ne retombe jamais sur « toutes les lignes » : elle échoue fermée (fail-closed).


5. Depuis le renderer React

RuntimeClient.readProjection(entity, projection, params) transmet déjà une map de paramètres à la chaîne de requête : le contrat est donc opt-in et rétro- compatible. Un constructeur typé produit l'encodage exact :

import { RuntimeClient, buildProjectionParams } from '@ausus/react-renderer';

const params = buildProjectionParams({
where: [
{ field: 'status', op: 'eq', value: 'open' },
{ field: 'priority', op: 'gte', value: 3 },
],
orderBy: [{ field: 'priority', dir: 'desc' }],
limit: 20,
});

const { body } = await client.readProjection('task', 'list', params);
// → GET …/list?where=status:eq:open,priority:gte:3&orderBy=priority:desc&limit=20

buildProjectionParams est un pur encodeur — il ne valide jamais. Un champ ou un opérateur inconnu apparaît comme un 400 venu du serveur, pas comme une clause silencieusement ignorée.


6. Garanties de conception

  • Additif & sûr vis-à-vis du gel. Aucun type kernel/contrat/modèle d'écriture n'a changé ; le tableau params est le contrat, donc rien de nouveau n'est ajouté à la surface gelée.
  • Fail closed. Champ/opérateur/paramètre inconnu, valeur manquante, sens invalide ou pagination hors bornes est rejeté — jamais coercé, jamais élargi à « toutes les lignes ».
  • Agnostique au driver. Appliqué dans le runtime aujourd'hui ; un futur driver SQL réutilise le contrat identique via findPaged.
  • Sûr pour le tenant & la visibilité. La requête s'exécute à l'intérieur de la portée tenant et avant le rendu ; les gardes de visibilité par champ sont inchangés.
  • Borné. limit est plafonné à 200, de sorte qu'un balayage non borné n'est jamais à une requête de distance.

Voir aussi les références Capacités et Limites connues (barre latérale → Concepts / Reference).