Skip to content

API Documentation: data-mutate and data-create

Article summary

The data-create and data-mutate APIs in DAZZM allow interaction with data in the DAZZM database. Unlike a traditional REST approach, which modifies a resource by providing a complete new version, DAZZM's API relies on specific named commands. For example, an aggregate may offer commands such as "create", "updateIdentification", "updateAddress", "deactivate", and "delete". This architecture, inspired by Domain-Driven Design, CQRS, and Event Sourcing, enables granular control…

This document explains how the data-create and data-mutate APIs in DAZZM work, allowing interaction with data in the DAZZM database.

Unlike a traditional REST approach, which modifies a resource by providing a complete new version, DAZZM’s API relies on specific named commands. For example, an aggregate may offer commands such as “create”, “updateIdentification”, “updateAddress”, “deactivate”, and “delete”. This architecture, inspired by Domain-Driven Design, CQRS, and Event Sourcing, enables granular control over operations, allowing targeted modifications to a resource. Each command generates a domain event, ensuring detailed traceability of changes and precise tracking of actions within the system.

This approach also enhances system integrity by enforcing strictly defined commands and parameters, reducing errors and data inconsistencies. Each aggregate can be seen as a microservice with its own dedicated APIs.

To modify a specific field, you must identify the appropriate command and provide the necessary parameters.

The examples in this document use the “User” model, an aggregate present in all DAZZM applications.

Creating an Aggregate with “data-create”

Section titled “Creating an Aggregate with “data-create””

To create an aggregate, invoke the data-create service, specifying the exact name of the creation command, the type identifier, and the required parameters.

Here is an example to create a user with the command named “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));

Notice the word “create” in two places. The first is “data-create” in the URL. This is static and never changes, it is the name of the creation endpoint. The second is in the body of the request (“commandName”:“create”) and this one can change, for example in an application whose data model is in French, we could indicate “create”. Each application has a different data model and command names.

Notice that the return value is in JSON format and always contains two elements. The first one, named “event” contains the transaction itself, while “data” contains the newly created data, including the default values ​​that were applied by the system. We see in this example that isActive was initialized to “true” by the “create” command.

{
"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"
}
}

Modifying an aggregate with “data-mutate”

Section titled “Modifying an aggregate with “data-mutate””

To modify an existing aggregate, we must invoke the “data-mutate” service by passing as parameters the exact name of the modification command, the identifier of the record to modify, as well as the parameters that this command requests.

Here is an example to modify a user with the command named “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));

Note that the return value is in JSON format and always contains two elements. The first one, named “event” contains the transaction itself, while “data” contains the modified data.

{
"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"
}
}

In addition to an aggregate, a model can define entities: items that live inside a collection of the aggregate (for example, an address in the “addresses” collection of a User). An entity has no standalone existence and no entry point of its own in the API: its type has no typeId usable directly with data-create or data-mutate. To act on an entity, you still target the aggregate (typeId + id), and add an entityKey field specifying which entity, in which collection, is targeted:

entityKey = "<collectionField>.<entityId>"

The full string is the entityKey: collectionField is the name of the field, on the aggregate, that holds the collection (e.g. “addresses”), and id is the id of the targeted entity within that collection, the same id you’d find on that entity’s own data (its “id” property). So in “addresses.bKLA7YBO9”, “addresses” is the field and “bKLA7YBO9” is the id of that particular address, and together they form the entityKey. If entityKey is absent, the command runs directly on the aggregate, the behavior described above. commandName and commandArgs always apply to the type resolved by entityKey (or to the aggregate, if entityKey is absent): each type, aggregate or entity, has its own available commands, there is no generic command available everywhere.

Here is an example to update an existing address in the “addresses” collection of our user, with the command named “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 and id remain those of the User aggregate; entityKey specifies that the command applies to the address identified by its own id (“bKLA7YBO9”) within the “addresses” collection, rather than to the user itself.

As with data-create and data-mutate on an aggregate, the return value is in JSON format and always contains two elements. The first one, named “event” contains the transaction itself, while “data” contains the modified data. Notice that “data” always corresponds to the full, updated User aggregate, with its “addresses” collection updated, never just the targeted address.

{
"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"
}
]
}
}

An entity can itself contain a collection, with its own entities. The entityKey path can then go down multiple levels, alternating collection field and id at each level:

entityKey = "<collectionField>.<entityId>.<subCollectionField>.<entityId>"

Here is an example to add an access note in the “accessNotes” collection of the address we just updated, with the command named “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));

Notice that entityKey still points to the address, not to the note. That can feel surprising, so it helps to ask a simple question: whose command is this? entityKey always targets the entity the command belongs to, not the thing you’d assume it “affects.” “addAccessNote” is a command the address knows how to do, since adding a note is really something you’re asking the address to do (grow its own accessNotes collection). So the command is addressed to the address, and entityKey stops at “addresses.bKLA7YBO9”.

Once the note exists, “updateAccessNote” is a different story: that command belongs to the note itself, since it’s the note that’s being changed. So this time entityKey goes one level deeper, straight to the note:

"entityKey": "addresses.bKLA7YBO9.accessNotes.mZ3xQ1RtP"

Same rule, applied at any depth: entityKey stops at whichever entity the command was defined on, and everything after commandName/commandArgs is answered by that entity. The response always follows the same rule as above too: “data” is the full User aggregate, addresses and accessNotes included, never just the note that was added or modified.

ElementAlways targets
typeId, idThe aggregate (never an entity type)
entityKey (optional)The entity targeted by the command, via a path from the aggregate, one or more levels
commandNameA command defined on the type resolved by entityKey (or on the aggregate, if entityKey is absent)
data in the responseAlways the full aggregate, updated, not just the modified/added entity

Never build typeId / id from the type and identifier of an entity itself. Entities are not loaded independently by the API; only the aggregate has a typeId valid for data-create and data-mutate, any command targeting an entity goes through the aggregate’s typeId/id plus entityKey.