Creating the write model
First, you need to create a new directory for your application. Call it chat
:
$ mkdir chat
Inside of this directory you will store the wolkenkit application as well as any related files, such as documentation, images, and so on. The actual wolkenkit code must be in a directory called server
, so you need to create it as well:
$ mkdir chat/server
Finally, for the write model, you need to create another directory called writeModel
inside of the server
directory:
$ mkdir chat/server/writeModel
To have a valid directory structure, you also need to add three more directories that you are going to need later, readModel
, readModel/lists
, and flows
:
$ mkdir chat/server/readModel
$ mkdir chat/server/readModel/lists
$ mkdir chat/server/flows
What do write model, read model, and flows mean?
If you are unsure about what the write model, the read model, and flows are, have a look at the core concepts.
As a result, your directory structure should look like this:
chat
server
flows
readModel
lists
writeModel
For more details, see creating the directory structure.
Configuring the application
One thing every wolkenkit application needs is a package.json
file within the application's directory. This file contains some configuration options that wolkenkit needs to start the application.
Create a package.json
file within the chat
directory:
$ touch chat/package.json
Then, open the file and add the following code:
{
"name": "chat",
"version": "0.0.0",
"wolkenkit": {
"application": "chat",
"runtime": {
"version": "1.2.0"
},
"environments": {
"default": {
"api": {
"address": {
"host": "local.wolkenkit.io",
"port": 3000
},
"allowAccessFrom": "*"
},
"node": {
"environment": "development"
}
}
}
}
}
For more details, see configuring an application.
Creating the communication context
To create the communication context, create an appropriate directory within the writeModel
directory:
$ mkdir chat/server/writeModel/communication
For more details, see defining contexts.
Creating the message aggregate
To create the message aggregate, create a message.js
file within the communication
directory:
$ touch chat/server/writeModel/communication/message.js
Then, open the file and add the following base structure:
'use strict';
const initialState = {
isAuthorized: {
commands: {},
events: {}
}
};
const commands = {};
const events = {};
module.exports = { initialState, commands, events };
For more details, see defining aggregates.
Initializing the state
As you have learned while modeling, a message has a text
and a number of likes
. For a new message it makes sense to initialize those values to an empty string, and the number 0
respectively. So, add the following two properties to the initialState
:
const initialState = {
text: '',
likes: 0,
// ...
};
For more details, see defining the initial state.
Implementing the send command
Now let's create the send command by adding a send
function to the commands
object. It receives three parameters, the message
itself, the actual command
, and a mark
object. For details on the structure of the command
object, see the data structure of commands.
Inside of this function you need to figure out whether the command is valid, and if so, publish an event. Afterwards you need to mark the command as handled by calling the mark.asDone
function. In the simplest case your code looks like this:
const commands = {
send (message, command, mark) {
message.events.publish('sent', {
text: command.data.text
});
mark.asDone();
}
};
Please note that you need to add the text that is contained within the command to the event, because the event is responsible for updating the state. Additionally, this gets sent to the read model and to the client, and they both are probably interested in the message's text.
Although this is going to work, it has one major drawback. The code also publishes the sent
event for empty messages, as there is no validation. To add this, check the command's data
property and reject the command if the text is missing:
send (message, command, mark) {
if (!command.data.text) {
return mark.asRejected('Text is missing.');
}
// ...
}
For more details, see defining commands and using command middleware. For details on what's inside a command, see the data structure of commands.
Implementing the sent event
To make things work, you also need to implement a handler that reacts to the sent event and updates the aggregate's state. For that add a sent
function to the events
object. It receives two parameters, the message
itself, and the actual event
.
Inside of this function you need to update the state of the message. To set the text to its new value, use the setState
function of the message object:
const events = {
sent (message, event) {
message.setState({
text: event.data.text
});
}
};
For more details, see defining events. For details on what's inside an event, see the data structure of events.
Implementing the like command
Implementing the like command is basically the same as implementing the send command. There is one exception, because like is self-sufficient and has no additional data. Hence the like
command could look like this:
const commands = {
// ...
like (message, command, mark) {
message.events.publish('liked');
mark.asDone();
}
};
Anyway, this raises the question how an event handler should figure out the new number of likes. This is especially true for a client that does not have the current state at hand, but might also be interested in the liked event. To fix this, calculate the new number of likes and add this information when publishing the liked event:
// ...
message.events.publish('liked', {
likes: message.state.likes + 1
});
// ...
Implementing the liked event
Implementing the liked event is exactly the same as implementing the sent event. Hence, your code looks like this:
const events = {
// ...
liked (message, event) {
message.setState({
likes: event.data.likes
});
}
};
Configuring the authorization
By default, you will not be able to run the new commands or receive any events for security reasons. As we are not going to implement authentication for this application, you need to allow access for public users. For that, add the following lines to the isAuthorized
section of the initialState
:
const initialState = {
// ...
isAuthorized: {
commands: {
send: { forPublic: true },
like: { forPublic: true }
},
events: {
sent: { forPublic: true },
liked: { forPublic: true }
}
}
};
For more details, see configuring authorization.
Safety check
Before you proceed, make sure that your aggregate looks like this:
'use strict';
const initialState = {
text: '',
likes: 0,
isAuthorized: {
commands: {
send: { forPublic: true },
like: { forPublic: true }
},
events: {
sent: { forPublic: true },
liked: { forPublic: true }
}
}
};
const commands = {
send (message, command, mark) {
if (!command.data.text) {
return mark.asRejected('Text is missing.');
}
message.events.publish('sent', {
text: command.data.text
});
mark.asDone();
},
like (message, command, mark) {
message.events.publish('liked', {
likes: message.state.likes + 1
});
mark.asDone();
}
};
const events = {
sent (message, event) {
message.setState({
text: event.data.text
});
},
liked (message, event) {
message.setState({
likes: event.data.likes
});
}
};
module.exports = { initialState, commands, events };
Test driving the write model
Now, start your application by running the following command from inside the chat
directory, and wait until a success message is shown:
$ wolkenkit start
For more details, see controlling the lifecycle.
Yay, congratulations!
You have created your first write model, and you are now also running a cloud-native application that is powered by an HTTP API that streams events in real-time, all securely encrypted using TLS!
Let's try it. First subscribe to the real-time events. Open a new terminal and run the following command:
$ curl -X POST https://local.wolkenkit.io:3000/v1/events
Now, open another terminal side-by-side, and send your first chat message. As soon as you send it, you can watch the events that are being published:
$ AGGREGATE_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
$ TIMESTAMP="$(date +%s000)"
$ curl \
-X POST \
-H "content-type: application/json" \
-d '{
"context": {
"name": "communication"
},
"aggregate": {
"name": "message",
"id": "'"$AGGREGATE_ID"'"
},
"name": "send",
"id": "'"$AGGREGATE_ID"'",
"data": {
"text": "Hello wolkenkit!"
},
"custom": {},
"user": null,
"metadata": {
"timestamp": '"$TIMESTAMP"',
"causationId": "'"$AGGREGATE_ID"'",
"correlationId": "'"$AGGREGATE_ID"'"
}
}' \
https://local.wolkenkit.io:3000/v1/command
Before you proceed, cancel the running curl
process, and stop your application by running the following command:
$ wolkenkit stop
For the client, we are now missing the list of messages, so let's go ahead and start creating the read model!