Documentation API « data-mutate » et « data-create »
Updated on Published on
Résumé de l'article
Les API « data-create » et « data-mutate » de DAZZM permettent d’interagir avec les données dans la base de données DAZZM. Contrairement à une approche REST classique, qui permet de modifier une ressource en fournissant une nouvelle version complète de celle-ci, l’API DAZZM repose sur des commandes nommées spécifiques. Par exemple, un aggregate peut offrir des commandes telles que « create », « updateIdentification », « updateAddress », « deactivate » et « delete ». Cette…
Contrairement à une approche REST classique, qui permet de modifier une ressource en fournissant une nouvelle version complète de celle-ci, l’API DAZZM repose sur des commandes nommées spécifiques. Par exemple, un aggregate peut offrir des commandes telles que « create », « updateIdentification », « updateAddress », « deactivate » et « delete ». Cette architecture, inspirée du Domain Driven Design, de CQRS et de l’Event Sourcing, favorise une gestion granulaire des opérations, permettant de modifier des aspects ciblés d’une ressource. Chaque commande génère un événement de domaine parlant, assurant une traçabilité détaillée des modifications et un suivi précis des actions dans le système.
Cette approche renforce également l’intégrité du système en imposant des commandes et des paramètres strictement définis, réduisant ainsi les erreurs et les incohérences dans les données. Chaque aggregate peut donc être perçu comme un microservice disposant de ses propres API spécifiques.
Pour modifier un champ particulier, il est nécessaire d’identifier la commande correspondant à ce champ, puis de fournir les paramètres requis par cette commande.
Note sur les exemples
Section intitulée « Note sur les exemples »Les exemples dans ce document utilisent le modèle de « User », un aggregate présent dans toutes les applications DAZZM.
Création d’un aggregate avec « data-create »
Section intitulée « Création d’un aggregate avec « data-create » »Pour créer un aggregate, on doit invoquer le service « data-create » en passant en paramètre le nom exact de la commande de création, l’identifiant du type de la donnée, ainsi que les paramètres que cette commande demande.
Requêtes et réponses
Section intitulée « Requêtes et réponses »Structure d’une requête avec fetch
Section intitulée « Structure d’une requête avec fetch »Voici un exemple pour créer un utilisateur avec la commande nommée « create »:
fetch('https://votre_environement.octopus-esm.com/api/prod/data-create', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=UTF-8', 'api-key': 'votre_token' }, body: JSON.stringify({ "typeId":"776e839a-6e1c-42b9-8efd-8b6db21b4797", "commandName":"create", "commandArgs":{ "firstName":"Michel", "lastName":"Roberge", "email":"mr@ici.com", "language":"fr"} })}) .then(response => response.json()) .then(body => console.log(body)) .catch(error => console.error(error));Notez le mot « create » à deux endroits. Le premier est « data-create » dans l’URL. Celui-ci est statique et ne change jamais, c’est le nom du endpoint de création. Le second est dans le body de la requête (“commandName”:“create”) et celui-là peut changer, par exemple dans une application dont le modèle de données est en français, on pourrait indiquer « créer ». Chaque application à un modèle de données et des noms de commandes différents.
Structure de la réponse du serveur
Section intitulée « Structure de la réponse du serveur »Remarquez que la valeur de retour est au format JSON et contient toujours deux éléments. Le premier, nommé « event » contient la transaction elle-même, alors que « data » contient la donnée nouvellement crée, incluant les valeurs par défaut qui ont été appliquées par le système. On voit dans cet exemple que isActive a été initialisé à « true » par la commande « create ».
{ "event": { "type": "USER_CREATED", "aggregateTypename": "User", "data": { "firstName": "Michel", "lastName": "Roberge", "email": "mr@ici.com", "isActive": true, "language": "fr", "id": "88f1525c-d105-47b9-a5e4-5e54ac3562b4" }, "aggregateId": "88f1525c-d105-47b9-a5e4-5e54ac3562b4", "aggregateVersion": 1, "created": 1730757148637, "requestId": "ad22d17c-f92e-447d-912b-ad9eb6b28e01", "dbId": "999999999", "appId": "999999999", "appVersion": "dev", "userId": "user" }, "data": { "_typename": "User", "_version": 1, "firstName": "Michel", "lastName": "Roberge", "email": "mr@ici.com", "isActive": true, "language": "fr", "id": "88f1525c-d105-47b9-a5e4-5e54ac3562b4" }}Modification d’un aggregate avec « data-mutate»
Section intitulée « Modification d’un aggregate avec « data-mutate» »Pour modifier un aggregate existant, on doit invoquer le service « data-mutate » en passant en paramètre le nom exact de la commande de modification, l’identifiant de l’enregistrement à modifier, ainsi que les paramètre que cette commande demande.
Requêtes et réponses
Section intitulée « Requêtes et réponses »Structure d’une requête avec fetch
Section intitulée « Structure d’une requête avec fetch »Voici un exemple pour modifier un utilisateur avec la commande nommée « updateIdentification ».
fetch('https://votre_environement.octopus-esm.com/api/prod/data-mutate, { method: 'POST', headers: { 'Content-Type': 'application/json;charset=UTF-8', 'api-key': 'votre_token' }, body: JSON.stringify({ "typeId":"776e839a-6e1c-42b9-8efd-8b6db21b4797", "commandName":"updateIdentification", "id": "88f1525c-d105-47b9-a5e4-5e54ac3562b4", "commandArgs":{ "firstName":"Michel", "lastName":"Roberge", "email":"mr@ici.com"} })}) .then(response => response.json()) .then(body => console.log(body)) .catch(error => console.error(error));Structure de la réponse du serveur
Section intitulée « Structure de la réponse du serveur »Remarquez que la valeur de retour est au format JSON et contient toujours deux éléments. Le premier, nommé « event » contient la transaction elle-même, alors que « data » contient la donnée modifiée.
{ "event": { "type": "USER_IDENTIFICATION_UPDATED", "data": { "firstName": "Michel", "lastName": "Dupuis", "email": "mr@ici.com" }, "aggregateId": "88f1525c-d105-47b9-a5e4-5e54ac3562b4", "aggregateTypename": "User", "aggregateVersion": 2, "created": 1730757965432, "requestId": "e1618d64-6c6b-4e27-8ef5-f18a57505191", "previousData": { "lastName": "Roberge" }, "dbId": "99999999", "appId": "99999999", "appVersion": "dev", "userId": "user" }, "data": { "isActive": true, "_version": 2, "lastName": "Dupuis", "_typename": "User", "email": "mr@ici.com", "id": "88f1525c-d105-47b9-a5e4-5e54ac3562b4", "firstName": "Michel", "language": "fr" }}Exécution d’une commande sur une entité
Section intitulée « Exécution d’une commande sur une entité »En plus d’un aggregate, un modèle peut définir des entités : des éléments qui vivent à l’intérieur d’une collection de l’aggregate (par exemple, une adresse dans la collection « addresses » d’un User). Une entité n’a pas d’existence autonome ni de point d’entrée propre dans l’API : son type n’a pas de typeId utilisable directement avec data-create ou data-mutate. Pour agir sur une entité, on cible tout de même l’aggregate (typeId + id), et on ajoute un champ entityKey précisant quelle entité, dans quelle collection, est ciblée :
entityKey = "<collectionField>.<entityId>"C’est la chaîne complète qui constitue l’entityKey : collectionField est le nom du champ, sur l’aggregate, qui contient la collection (par exemple « addresses »), et id est l’id de l’entité ciblée dans cette collection, le même id que l’on retrouve dans la donnée propre de cette entité (sa propriété « id »). Ainsi, dans « addresses.bKLA7YBO9 », « addresses » est le champ et « bKLA7YBO9 » est l’id de cette adresse en particulier, et les deux ensemble forment l’entityKey. Si entityKey est absent, la commande s’applique directement à l’aggregate, le comportement décrit ci-dessus. commandName et commandArgs s’appliquent toujours au type résolu par entityKey (ou à l’aggregate, si entityKey est absent) : chaque type, aggregate ou entité, a son propre ensemble de commandes disponibles, il n’existe pas de commande générique disponible partout.
Requêtes et réponses
Section intitulée « Requêtes et réponses »Structure d’une requête avec fetch
Section intitulée « Structure d’une requête avec fetch »Voici un exemple pour modifier une adresse existante dans la collection « addresses » de notre utilisateur, avec la commande nommée « updateAddress ».
fetch('https://votre_environement.octopus-esm.com/api/prod/data-mutate', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=UTF-8', 'api-key': 'votre_token' }, body: JSON.stringify({ "typeId":"776e839a-6e1c-42b9-8efd-8b6db21b4797", "commandName":"updateAddress", "id": "88f1525c-d105-47b9-a5e4-5e54ac3562b4", "entityKey": "addresses.bKLA7YBO9", "commandArgs":{ "street":"123 rue Principale", "city":"Montreal"} })}) .then(response => response.json()) .then(body => console.log(body)) .catch(error => console.error(error));typeId et id restent ceux de l’aggregate User; entityKey précise que la commande s’applique à l’adresse identifiée par son propre id (« bKLA7YBO9 ») dans la collection « addresses », plutôt qu’à l’utilisateur lui-même.
Structure de la réponse du serveur
Section intitulée « Structure de la réponse du serveur »Comme pour data-create et data-mutate sur un aggregate, la valeur de retour est au format JSON et contient toujours deux éléments. Le premier, nommé « event » contient la transaction elle-même, alors que « data » contient la donnée modifiée. Remarquez que « data » correspond toujours à l’aggregate User complet, mis à jour, avec sa collection « addresses » mise à jour, jamais seulement à l’adresse ciblée.
{ "event": { "type": "USER_ADDRESS_UPDATED", "data": { "street": "123 rue Principale", "city": "Montreal" }, "aggregateId": "88f1525c-d105-47b9-a5e4-5e54ac3562b4", "aggregateTypename": "User", "aggregateVersion": 3, "created": 1730758421903, "requestId": "f2a9c3d1-8b4e-4a6f-9c2d-1e7b5a3f9d82", "previousData": { "street": "100 boul. Saint-Laurent" }, "dbId": "99999999", "appId": "99999999", "appVersion": "dev", "userId": "user" }, "data": { "isActive": true, "_version": 3, "lastName": "Dupuis", "_typename": "User", "email": "mr@ici.com", "id": "88f1525c-d105-47b9-a5e4-5e54ac3562b4", "firstName": "Michel", "language": "fr", "addresses": [ { "id": "bKLA7YBO9", "street": "123 rue Principale", "city": "Montreal" } ] }}Entités imbriquées sur plusieurs niveaux
Section intitulée « Entités imbriquées sur plusieurs niveaux »Une entité peut elle-même contenir une collection, avec ses propres entités. Le chemin de entityKey peut alors descendre sur plusieurs niveaux, en alternant champ de collection et id à chaque niveau :
entityKey = "<collectionField>.<entityId>.<subCollectionField>.<entityId>"Voici un exemple pour ajouter une note d’accès dans la collection « accessNotes » de l’adresse qu’on vient de modifier, avec la commande nommée « addAccessNote » :
fetch('https://votre_environement.octopus-esm.com/api/prod/data-mutate', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=UTF-8', 'api-key': 'votre_token' }, body: JSON.stringify({ "typeId":"776e839a-6e1c-42b9-8efd-8b6db21b4797", "commandName":"addAccessNote", "id": "88f1525c-d105-47b9-a5e4-5e54ac3562b4", "entityKey": "addresses.bKLA7YBO9", "commandArgs":{ "id": "mZ3xQ1RtP", "text":"Access code: 1234"} })}) .then(response => response.json()) .then(body => console.log(body)) .catch(error => console.error(error));Remarquez que entityKey pointe encore vers l’adresse, pas vers la note. Cela peut sembler surprenant, alors il est utile de se poser une question simple : à qui appartient cette commande? entityKey cible toujours l’entité à laquelle appartient la commande, pas ce qu’on présumerait qu’elle « affecte ». « addAccessNote » est une commande que l’adresse sait exécuter, puisqu’ajouter une note, c’est en réalité demander à l’adresse de faire grandir sa propre collection accessNotes. La commande s’adresse donc à l’adresse, et entityKey s’arrête à « addresses.bKLA7YBO9 ».
Une fois la note créée, « updateAccessNote » est une autre histoire : cette commande appartient à la note elle-même, puisque c’est la note qui est modifiée. Cette fois, entityKey descend donc un niveau plus loin, directement jusqu’à la note :
"entityKey": "addresses.bKLA7YBO9.accessNotes.mZ3xQ1RtP"Même règle, appliquée à n’importe quelle profondeur : entityKey s’arrête à l’entité sur laquelle la commande a été définie, et tout ce qui suit (commandName/commandArgs) est répondu par cette entité. La réponse suit toujours la même règle qu’énoncée plus haut : « data » est l’aggregate User complet, addresses et accessNotes inclus, jamais seulement la note ajoutée ou modifiée.
| Élément | Cible toujours |
|---|---|
| typeId, id | L’aggregate (jamais un type d’entité) |
| entityKey (optionnel) | L’entité ciblée par la commande, via un chemin à partir de l’aggregate, un ou plusieurs niveaux |
| commandName | Une commande définie sur le type résolu par entityKey (ou sur l’aggregate, si entityKey est absent) |
| data dans la réponse | Toujours l’aggregate complet, mis à jour, jamais seulement l’entité modifiée/ajoutée |
Ne construisez jamais typeId / id à partir du type et de l’identifiant d’une entité elle-même. Les entités ne sont pas chargées de façon indépendante par l’API; seul l’aggregate a un typeId valide pour data-create et data-mutate, toute commande ciblant une entité passe par le typeId/id de l’aggregate accompagné d’entityKey.