Spunto CMS
L'API

Lire le contenu

Lire des cartes par l'API REST avec une définition de vue — filtres, tris, curseurs keyset, pages groupées, comptages et recherche.

Lire du contenu, c'est envoyer une définition de vue et recevoir une page keyset en retour.

C'est un POST plutôt qu'un GET, parce qu'un filtre est un arbre, et qu'un arbre ne survit à une query string sous aucune forme lisible. Malgré le verbe, ces routes ne font que lire — et c'est précisément pour cela que la table des scopes qui encadre les apps installées les nomme une à une au lieu de deviner d'après la méthode.

Une page de cartes

POST /workspaces/{workspaceId}/collections/{collectionId}/query
{
  "definition": {
    "filter": { "op": "eq", "field": { "fieldId": "sys:status" }, "value": "<statusId>" },
    "sort": [{ "field": { "fieldId": "sys:order" }, "direction": "asc" }],
    "includeSecondaryLists": true
  },
  "cursor": null,
  "limit": 50
}

limit vaut 50 par défaut et plafonne à 200. La réponse est :

{ "rows": [ /* Item */ ], "nextCursor": "…" }

Bouclez jusqu'à ce que nextCursor vaille null, en le renvoyant chaque fois comme cursor.

Les cartes d'une page n'ont pas de corps

rows contient des Item, et Item n'a ni body ni blocks. Récupérez une carte seule avec GET /workspaces/{workspaceId}/items/{itemId} pour obtenir un ItemDetail, qui les porte. Une page de 50 lignes ne transporte pas 50 documents.

Filtres

Un filtre est soit un prédicat, soit un nœud booléen :

{ "and": [ <filter>, <filter> ] }
{ "or":  [ <filter>, <filter> ] }

Un prédicat nomme un champ et un opérateur :

OpérateursForme
eq neq lt lte gt gte{ op, field, value }
contains starts_with{ op, field, value } — valeur chaîne
in not_in{ op, field, values } — tableau
is_empty is_not_empty{ op, field }

Un champ s'écrit { "fieldId": "…" } — soit l'identifiant d'une propriété du registre, soit un champ système comme sys:title, sys:status, sys:order ou sys:created_at. La collection publie les deux listes, si bien qu'un client n'a jamais à les coder en dur.

Trois comportements faciles à mal deviner de l'extérieur, et tranchés ici :

  • neq garde les valeurs vides. Il se compile en is distinct from, donc une carte sans valeur pour la propriété correspond bien à « différent de X ». Tout autre choix surprend quiconque l'essaie.
  • contains et starts_with sont littéraux. Les % et _ de votre valeur sont échappés. Un terme de recherche est une donnée, pas un motif.
  • in avec une liste vide ne correspond à rien ; not_in avec une liste vide correspond à tout. C'est la lecture qui permet de construire un filtre à partir d'une sélection sans traiter zéro comme un cas particulier.

Sur une propriété tableau (multi_select, multi_person), eq et contains signifient tous deux l'inclusion, neq signifie « ne contient pas celle-ci », et in / not_in sont une chaîne de or sur des inclusions. Tout le reste vaut un 400, avec un message qui nomme les trois qui fonctionnent.

Tris

sort est une liste ordonnée de { field, direction }, asc ou desc.

On ne peut pas trier sur une propriété tableau, et le demander renvoie un 400 plutôt qu'un ordre arbitraire. Un select se trie alphabétiquement, pas dans l'ordre de ses options — un piège connu.

Curseurs

Un curseur encode la position et le tri pour lequel il a été émis. En renvoyer un avec un tri différent échoue bruyamment plutôt que de rendre une page fausse mais plausible.

Ne les analysez pas. Ne les fabriquez pas. Leur seule promesse est qu'en renvoyer un vous donne la page suivante de la même requête.

Lectures groupées

Deux routes servent les mises en page groupées, et toutes deux compilent la même définition — c'est ce qui fait que la somme des colonnes vaut la vue :

POST …/group-counts     — nombre de cartes par groupe
POST …/grouped-query    — une page par groupe, chacune avec son propre curseur

Toutes deux exigent un groupBy dans la définition, et toutes deux renvoient null comme une clé de groupe ordinaire : les cartes sans valeur.

grouped-query prend les groups que vous voulez afficher, dans cet ordre, y compris les vides — un GROUP BY ne renvoie que les valeurs qui ont des lignes, et une colonne vide doit tout de même exister pour qu'on puisse y déposer une carte (voir pourquoi). Omettez-les et vous obtenez les groupes que les données se trouvent contenir.

Là, limit s'entend par groupe, pas par réponse, et vaut 20 par défaut. Chaque groupe revient avec son propre nextCursor et avec count — la taille totale du groupe, pas le nombre de lignes renvoyées. Charger la suite d'une colonne, c'est envoyer le curseur de ce groupe dans cursors, indexé par groupe.

Fenêtres de dates

query comme grouped-query acceptent un range — la fenêtre que regarde un calendrier ou une chronologie. Elle s'ajoute au filtre de la vue, et n'est jamais enregistrée avec elle :

{ "range": { "field": { "fieldId": "<fieldId date>" }, "endField": { "fieldId": "<fieldId date>" }, "from": "2026-11-01", "to": "2026-11-30" } }

from et to sont inclus tous les deux. Sans endField, une carte est dans la fenêtre quand sa date y tombe ; avec, quand sa durée la chevauche (une fin absente, ou antérieure au début, vaut un jour). field doit être une propriété date ou sys:created_at ; tout le reste est un 400, comme une fenêtre qui finit avant de commencer. Pour une grille de mois, groupez par la même date et passez les jours de la grille en groups — la plage borne aussi les compteurs.

Index suggérés

POST …/index-hints

À partir d'une définition de vue, l'API indique quels index la serviraient bien, chacun avec la raison pour laquelle il est souhaité. C'est la réponse honnête à « pourquoi cette vue est-elle lente » — un filtre sur une propriété JSON sans index d'expression derrière ne se lit pas du tout comme un filtre qui en a un.

Décrits, pas créés

Cette route informe. Elle ne crée rien : la tâche qui appliquerait les suggestions n'existe pas encore.

Recherche

GET /workspaces/{workspaceId}/search?q=…&limit=…

La recherche traverse toutes les listes de l'espace de travail et renvoie deux tableaux, jamais mélangés : les cartes et les collections qui correspondent.

Ce qu'elle couvre est étroit, et c'est dit plutôt que sous-entendu :

  • les titres des cartes et les noms des listes — jamais le corps des cartes ;
  • une clé lisible exacte (DOC-12) fait passer cette carte en tête, même si le titre ne correspond pas ;
  • les cartes archivées sont exclues ;
  • les % et _ de votre requête sont échappés.

Elle ne passe pas par le compilateur de vues : elle ne porte ni filtre, ni tri, ni curseur. L'y faire passer aurait exigé un champ virtuel que rien d'autre n'utilise.

S'il vous faut une recherche dans le corps des cartes, aucun index ne la permet — voir ce qu'il ne fait pas.

Sur cette page