In the previous article we added a simple back end that kept the data in memory only. In this article we will be persisting the data to a PostgreSQL database.
We are still keeping all messages in a single pool, so we only need a single data model to store those messages. We will store the messages in a PostgreSQL database. After setting up Postgres you need to import the Rows
ORM library, and this can be done by adding this line nexto the where we are importing WebPlatform:
Apm.importFile("Alusus/Rows", { "Rows.alusus", "Drivers/Postgresql.alusus" });
Notice that we are telling Alusus Package Manager to import the Postgresql driver in addition to the Rows library itself. This driver is part of the Rows library but is not imported by default in case the user wants to use a different database (MySQL for example) instead of Postgres.
Now that we have our ORM ready, let’s build our persistence layer. Again, we can keep everything in the same file, chat.alusus
. Our persistence layer looks like this:
//==============================================================================
// Database
macro envVarOrDefault[varName, defaultVal] CharsPtr().{
this = System.getEnv(varName);
if this == 0 this = defaultVal;
}
@model["messages", 1]
class MessageModel {
Rows.define_model_essentials[];
@VarChar["1024"]
@notNull
@column
def message: String;
@BigInteger
@notNull
@column["creation_date"]
def creationDate: Int[64];
}
def db: Rows.Db;
def schema: {
MessageModel
};
function initializeDatabase {
db.init(closure(d: ref[SrdRef[Rows.Driver]]) {
d = getDbDriver();
});
db.schemaBuilder[schema].migrate();
}
function getDbDriver(): SrdRef[Rows.Driver] {
def driver: SrdRef[Rows.Driver] = Rows.PostgresqlDriver(Rows.ConnectionParams().{
dbName = String(envVarOrDefault["CHAT_DB_NAME", "alusus"]).trim();
userName = String(envVarOrDefault["CHAT_DB_USERNAME", "alusus"]).trim();
password = String(envVarOrDefault["CHAT_DB_PASSWORD", "alusus"]).trim();
host = String(envVarOrDefault["CHAT_DB_HOST", "0.0.0.0"]).trim();
port = String.parseInt(envVarOrDefault["CHAT_DB_PORT", "5432"]);
});
if !driver.isConnected() {
System.fail(1, String("Error connecting to DB: ") + driver.getLastError());
}
return driver;
}
Our model class is called MessageModel
and it has two fields: message
and creationDate
. The getDbDriver
function instantiates the Postgres driver with the right connection params. The initializeDatabase
initializes the driver and also calls the migrate
function of the schema builder to make sure the table is created.
With our data model ready, it’s time to use it in the backend endpoints. We will no longer need the global array. Instead, we’ll write the data to the database. We also won’t need the mutex since the database engine internally handles synchronization. The backend will now look like the following:
//==============================================================================
// Backend
def MAX_MESSAGES: 12;
@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
def postData: array[Char, 1024];
def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
def msg: MessageModel;
msg.message = String(postData~ptr, postDataSize);
msg.creationDate = Time.getTimestamp(0);
db.save[MessageModel](msg);
Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}
@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {
// Fetch messages from DB.
def messages: Array[SrdRef[MessageModel]] = db
.from[MessageModel]
.order[-creationDate]
.select()
.slice(0, MAX_MESSAGES);
def response: StringBuilder(1024 * 10, 1024 * 5);
def i: Int;
for i = messages.getLength() - 1, i >= 0, --i {
if i != messages.getLength() - 1 response += "\n";
response += "- ";
response += messages(i).message;
}
// Delete older messages.
if messages.getLength() > 0 {
db.from[MessageModel].where[creationDate < messages(messages.getLength() - 1).creationDate].delete();
}
// Send response.
Http.print(conn, "HTTP/1.1 200 Ok\r\n");
Http.print(conn, "Content-Type: text/plain\r\n");
Http.print(conn, "Cache-Control: no-cache\r\n");
Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength());
Http.print(conn, response);
}
The main difference from the prev version is in how we save and load the data. We now save the data by instantiating a MessageModel object, set its member vars with the right values, then call db.save[MessageModel](msg)
. Loading the data is done using the db.from[MessageModel].order[-creationDate].select()
statement, which loads it ordered descendingly by the creation date. For this example we continue to limit the messages to 12, but this limit can be changed or removed altogether. Deleting the old records is done by the following:
db.from[MessageModel].where[creationDate < messages(messages.getLength() - 1).creationDate].delete();
Which deletes all the messages that have a creation date older than the 12th message in the loaded data. Now the full source code looks like this:
import "Srl/String";
import "Srl/Time";
import "Apm";
Apm.importFile("Alusus/Rows", { "Rows.alusus", "Drivers/Postgresql.alusus" });
Apm.importFile("Alusus/WebPlatform");
use Srl;
use WebPlatform;
//==============================================================================
// Database
macro envVarOrDefault[varName, defaultVal] CharsPtr().{
this = System.getEnv(varName);
if this == 0 this = defaultVal;
}
@model["messages", 1]
class MessageModel {
Rows.define_model_essentials[];
@VarChar["1024"]
@notNull
@column
def message: String;
@BigInteger
@notNull
@column["creation_date"]
def creationDate: Int[64];
}
def db: Rows.Db;
def schema: {
MessageModel
};
function initializeDatabase {
db.init(closure(d: ref[SrdRef[Rows.Driver]]) {
d = getDbDriver();
});
db.schemaBuilder[schema].migrate();
}
function getDbDriver(): SrdRef[Rows.Driver] {
def driver: SrdRef[Rows.Driver] = Rows.PostgresqlDriver(Rows.ConnectionParams().{
dbName = envVarOrDefault["CHAT_DB_NAME", "alusus"];
userName = envVarOrDefault["CHAT_DB_USERNAME", "alusus"];
password = envVarOrDefault["CHAT_DB_PASSWORD", "alusus"];;
host = envVarOrDefault["CHAT_DB_HOST", "0.0.0.0"];
port = String.parseInt(envVarOrDefault["CHAT_DB_PORT", "5432"]);
});
if !driver.isConnected() {
System.fail(1, String("Error connecting to DB: ") + driver.getLastError());
}
return driver;
}
//==============================================================================
// Backend
def MAX_MESSAGES: 12;
@beEndpoint["POST", "/messages"]
func postMessages (conn: ptr[Http.Connection]) {
def postData: array[Char, 1024];
def postDataSize: Int = Http.read(conn, postData~ptr, postData~size);
def msg: MessageModel;
msg.message = String(postData~ptr, postDataSize);
msg.creationDate = Time.getTimestamp(0);
db.save[MessageModel](msg);
Http.print(conn, "HTTP/1.1 200 Ok\r\n\r\n");
}
@beEndpoint["GET", "/messages"]
func getMessages (conn: ptr[Http.Connection]) {
// Fetch messages from DB.
def messages: Array[SrdRef[MessageModel]] = db
.from[MessageModel]
.order[-creationDate]
.select()
.slice(0, MAX_MESSAGES);
def response: StringBuilder(1024 * 10, 1024 * 5);
def i: Int;
for i = messages.getLength() - 1, i >= 0, --i {
if i != messages.getLength() - 1 response += "\n";
response += "- ";
response += messages(i).message;
}
// Delete older messages.
if messages.getLength() > 0 {
db.from[MessageModel].where[creationDate < messages(messages.getLength() - 1).creationDate].delete();
}
// Send response.
Http.print(conn, "HTTP/1.1 200 Ok\r\n");
Http.print(conn, "Content-Type: text/plain\r\n");
Http.print(conn, "Cache-Control: no-cache\r\n");
Http.print(conn, "Content-Length: %d\r\n\r\n", response.getLength());
Http.print(conn, response);
}
//==============================================================================
// Frontend Components
class Header {
@injection def component: Component;
handler this~init() {
this.view = Box({}).{
style.{
padding = Length4.pt(4);
borderWidth = Length4.pt(0, 0, 1.5, 0);
borderStyle = BorderStyle.SOLID;
borderColor = Color("000");
justify = Justify.START;
display = Display.FLEX;
layout = Layout.COLUMN;
background = Background(Color("fff"));
};
addChildren({
Text(String("Wasm Chat")).{
style.fontSize = Length.pt(18.0);
}
});
};
}
handler this_type(): SrdRef[Header] {
return SrdRef[Header].construct();
}
}
class TextEntry {
@injection def component: Component;
def onNewEntry: closure (String);
def textInput: SrdRef[TextInput];
handler this~init() {
def self: ref[this_type](this);
this.view = Box({}).{
style.{
display = Display.FLEX;
layout = Layout.ROW;
justify = Justify.SPACE_BETWEEN;
};
addChildren({
TextInput().{
self.textInput = this;
style.{
flex = Flex(1);
margin = Length4.px(10);
height = Length.px(50);
fontSize = Length.pt(12.0);
borderStyle = BorderStyle.SOLID;
borderColor = Color("000");
borderWidth = Length4.px(1.5);
};
onKeyUp.connect(closure (widget: ref[TextInput], payload: ref[String]) {
if payload == "Shift+Enter" {
def newData: String = widget.getText().trim();
widget.setText(String());
if not self.onNewEntry.isNull() self.onNewEntry(newData);
}
});
},
Button(String("Send")).{
style.{
height = Length.px(50);
width = Length.px(50);
fontSize = Length.px(16.0);
justify = Justify.CENTER;
margin = Length4.px(10, 10, 10, 0);
};
onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
def newData: String = self.textInput.getText().trim();
self.textInput.setText(String());
if not self.onNewEntry.isNull() self.onNewEntry(newData);
});
}
});
};
}
handler this_type(): SrdRef[TextEntry] {
return SrdRef[TextEntry].construct();
}
}
//==============================================================================
// Frontend Pages
@uiEndpoint["/"]
@title["Wasm Chat"]
func main {
def onFetch: closure (json: Json);
Window.instance.style.{
padding = Length4.pt(0);
margin = Length4.pt(0);
};
Window.instance.setView(Box({}).{
style.{
height = Length.percent(100);
justify = Justify.SPACE_BETWEEN;
display = Display.FLEX;
layout = Layout.COLUMN;
background = Background(Color("aaa"));
};
addChildren({
Header(),
Text(String()).{
style.{
flex = Flex(1);
width = Length.percent(100) - Length.px(20);
margin = Length4.px(10, 10, 0, 10);
fontSize = Length.pt(20.0);
borderStyle = BorderStyle.SOLID;
borderColor = Color("000");
borderWidth = Length4.px(1.5);
background = Background(Color("fff"));
};
onFetch = closure (json: Json) {
def status: Int[64] = json("eventData")("status");
if status >= 200 and status < 300 {
def data: String = json("eventData")("body");
if this.getText() != data {
this.setText(data);
}
}
};
},
TextEntry().{
onNewEntry = closure (newData: String) {
sendRequest(
"POST", "/messages", "Content-Type: application/text", newData, 10000,
closure (Json) {
sendRequest("GET", "/messages", null, null, 500, onFetch);
}
);
};
};
})
});
startTimer(500000, closure (json: Json) {
sendRequest("GET", "/messages", null, null, 500, onFetch);
});
sendRequest("GET", "/messages", null, null, 500, onFetch);
runEventLoop();
}
//==============================================================================
// Entry Point
initializeDatabase();
Console.print("Starting server on port 8000...\nURL: http://localhost:8000/\n");
buildAndRunServer(Array[CharsPtr]({ "listening_ports", "8000", "static_file_max_age", "0" }));
That’s it, the app is ready to save messages. You just need to make sure a Postgres server is running and the right connection params set in the code, then you can run the code. In the next article we will be adding support for users and rooms.