Écrire du contenu
Créer et modifier des cartes par l'API REST, ce que la validation refuse, l'édition en masse et des imports que l'on peut rejouer sans risque.
Rien n'est réservé à l'interface. Chaque écriture que fait l'application est une route qu'une clé API peut appeler.
Créer une carte
POST /workspaces/{workspaceId}/collections/{collectionId}/items{
"title": "Release notes, 2.4",
"blocks": { "blocks": [ /* … */ ] },
"statusId": "<statusId>",
"parentId": null,
"properties": { "<fieldKey>": "value" },
"afterItemId": null
}Seul title est requis. La collection dans laquelle vous postez devient la
liste d'origine de la carte, et l'API compose sa clé lisible à partir du préfixe
de cette liste et du numéro de la carte.
Deux façons de nommer une propriété, et ce ne sont pas les mêmes
properties est indexé par la key d'un champ — son nom court et stable.
Une définition de vue, elle, référence un champ par
son fieldId, son UUID. Les deux figurent sur l'objet Field que renvoie
la collection ; les confondre vous vaut un 400 indiquant que la propriété est
inconnue, ce qui a au moins le mérite d'échouer vite.
afterItemId place la carte dans l'ordre manuel ; omettez-le et la carte
arrive en fin de liste.
Modifier une carte
PATCH /workspaces/{workspaceId}/items/{itemId}properties est fusionné, pas remplacé. Modifier une propriété laisse les
autres intactes — c'est ce qu'on attend d'un PATCH, et c'est ce qui permet à
deux clients de modifier sans risque des propriétés différentes de la même
carte.
archived: true retire une carte de toutes les vues et de la recherche, de
façon réversible. DELETE est définitif et emporte le sous-arbre de la carte.
Le corps : jamais les deux
Envoyez blocks ou body, pas les deux.
blocks pose le document et recalcule sa projection texte. body pose le texte
et fait de nouveau dériver le document de celui-ci. Voir
le corps des cartes.
Ce que la validation refuse
Les valeurs des propriétés sont vérifiées contre le registre à l'écriture. Un 400 signifie que la valeur n'a atterri nulle part.
- une clé absente du registre — les propriétés se déclarent avant d'être utilisées, elles ne s'inventent jamais au détour d'une écriture ;
- une valeur du mauvais type pour sa propriété ;
- une valeur de
selectou demulti_selecthors desoptionsdéclarées, quand des options sont déclarées ; - un
personou unmulti_personqui n'est pas membre de cet espace de travail ; - un
filequi n'est pas un fichier de cet espace de travail ; - une
urlqui n'est pas une URL absolue enhttp,httpsoumailto; - un document en blocs qui enfreint le format — type de bloc inconnu, marque qui ne fait pas partie des cinq, retrait hors limites.
Pourquoi cela ne peut pas être tolérant
Une valeur mal typée ne casse pas sa propre carte. Elle casse toute la requête de chaque vue qui caste cette propriété — un seul nombre erroné et c'est une liste entière qui ne se charge plus, pour tous ceux qui l'ouvrent. Aucune récupération à la lecture ne vaudrait mieux que de refuser l'écriture.
Déplacer des cartes
POST /workspaces/{workspaceId}/items/{itemId}/move
POST /workspaces/{workspaceId}/items/{itemId}/collections
DELETE /workspaces/{workspaceId}/items/{itemId}/collections/{collectionId}move change la place d'une carte — son parent, son rang, sa liste d'origine.
Les deux autres rattachent une carte à des listes secondaires et l'en détachent : la même carte qui apparaît à un second endroit, pas une copie. Sa liste d'origine, et donc sa clé, ne change pas.
Dupliquer une carte
POST /workspaces/{workspaceId}/items/{itemId}/duplicatePas de corps de requête. La copie est renvoyée en entier, et elle se pose juste après l'original dans l'ordre manuel — pas en fin de liste, ce qui, sur une liste de quatre mille cartes, revient à nulle part.
Elle emmène le sous-arbre de la carte, en miroir de DELETE : les sous-tâches
sont recréées sous la copie, chacune avec son propre numéro, et rattachées à
elle. Les sous-tâches archivées restent où elles sont — une copie naît
désarchivée, donc les copier remettrait dans une vue exactement ce que quelqu'un
en avait retiré.
Ce qui voyage : le titre (suffixé de (copie) sur la racine seulement), l'icône,
le statut, le corps (blocs et texte, tels qu'ils sont stockés), les propriétés et
les rattachements à des listes secondaires. Ce qui ne voyage pas :
- le numéro, et donc la clé.
SPR-12n'est jamais partagé entre deux cartes ; externalSource/externalId. La copie ne vient pas de l'import dont vient l'original, et cette paire est une clé d'idempotence unique ;archivedAt, qui part ànull;createdBy, qui est l'appelant — la même règle que pour toute autre création.
Les propriétés sont copiées telles qu'elles sont stockées, sans être revalidées
contre le registre. Elles l'ont été une fois, à l'entrée ; les revérifier
refuserait de copier une carte que l'API accepte parfaitement de vous montrer,
au motif qu'une option de select a été retirée entre-temps.
Un événement item.created par carte copiée. Celui de la racine porte une clé
duplicatedFrom en plus, qui contient l'identifiant de la source — la seule
trace du lien, puisque la copie elle-même ne garde aucune référence à son
original.
Au-delà de 200 cartes dans le sous-arbre, la route refuse avec un 400 plutôt que
de tenir une transaction ouverte sur des centaines d'insertions. Renommer la
copie se fait avec un PATCH.
Modifier en masse
POST …/bulk-preview { definition }
POST …/bulk-update { definition, patch, expectedCount }La sélection est une définition de vue — le filtre même que l'utilisateur avait sous les yeux, compilé par le même code. Sans cela, « ce qu'il voyait » et « ce que la mise à jour touche » finiraient par diverger.
La forme est volontairement malcommode à un endroit, et cela vaut la peine de comprendre pourquoi avant d'y brancher une interface :
- d'abord l'aperçu.
bulk-previewrenvoiecount— ce que le filtre sélectionne à cet instant — ainsi queconfirmAboveetmax; - au-delà de
confirmAbove(200 cartes),expectedCountest obligatoire. Envoyez le nombre que l'utilisateur a réellement vu. Si la réalité a bougé entre-temps, vous recevez un 409 plutôt qu'une modification silencieuse d'un autre ensemble ; - au-delà de
max(5 000 cartes), l'API refuse. Elle ne met pas de tâche en file et ne prétend pas être atomique. Cette tâche n'existe pas encore, et le dire vaut mieux qu'une requête qui réussit à moitié.
Un seul UPDATE … WHERE, une seule transaction, un événement par carte, et une
seule livraison de webhook pour le lot. La réponse porte le batchId, pour que
vous puissiez relire exactement ce qu'elle a fait :
GET /workspaces/{workspaceId}/events?batchId=….
Des imports que l'on peut rejouer
POST /workspaces/{workspaceId}/importL'import est la seule écriture conçue autour de l'échec. Non qu'elle soit fragile — mais un import échoue toujours au moins une fois, et un import qu'on ne peut pas rejouer est un import qu'on ne peut pas réparer.
Le contenu venu d'ailleurs porte donc l'identité de son origine :
externalSource et externalId, dans des colonnes indexées sur les cartes, les
collections et les fichiers. C'est cette paire qui décide entre créer et mettre
à jour, et ce qui n'a pas réellement changé n'est pas réécrit — ni écriture,
ni événement, ni webhook. Relancer un import réussi ne fait rien, et cela se
constate : dans le résultat, unchanged vaut le total.
Pourquoi l'identité externe n'est pas une propriété
Une clé d'idempotence qui vit dans la donnée utilisateur n'est pas une clé d'idempotence. N'importe quelle écriture ordinaire la modifierait, et supprimer un champ l'emporterait avec lui. Elle doit être une colonne, hors de portée de l'édition.
Les imports s'annoncent aussi comme tels. Leurs événements partagent tous un
même batchId et portent cause: "import", pour qu'un fil d'activité puisse
demander ?cause=edit et ne pas enterrer six mois de travail sous trois mille
cartes importées. Les webhooks sont livrés par lot et par type, jamais un appel
par carte.
Un lot porte au plus 500 éléments. Au-delà, rappelez la route avec le batchId
qui vous a été remis : trois mille cartes restent une seule action dans le
journal.