Introduction

A simple TODO app that demostrates using SeaORM, SQLite and Postgres to build a simple TODO application. TCP connections are used instead of web frameworks for simplicity due to the APIs being available in the standard library, which is mirrored by async-std async library.

Let's get started.

Symbols Used

To show added or removed code from files, we will use comments or

+ to show added code

- to show removed code

... is used to show only part of the existing code instead of rewriting already existing code in the examples.

$ shows an operation is done on the console/shell

postgres=# shows a postgres prompt

This will make it easier to visualize changes to a file

First, install PostgreSQL and SQLite and ensure PostgreSQL server is running.

Create a cargo workspace for the server and frontend

  1. Create a new directory SeaORM-TODO-App, a Cargo.toml file, a TODO-Server and a TODO-Client. The TODO-Server will contain the source code for the TCP server while the TODO-Client will contain the source code for the front-end.

    $ mkdir SeaORM-TODO-App
    
    $ cd SeaORM-TODO-App
    
    $ cargo new TODO-Server --name todo-server
    
    $ cargo new TODO-Client --name todo-client
    

    Then register the cargo projects with the cargo workspace by creating a workspace file in the current directory.

    File:SeaORM-TODO-App/Cargo.toml file

    [workspace]
    members = [
    	"TODO-Server",
    	"TODO-Client",
    ]
    

Next, we will build the TCP server

Building The TCP Server

Install necessary dependencies

  1. Switch to the SeaORM-TODO-App/TODO-Server directory to build the TCP server

    $ cd TODO-Server
    
  2. Ensure you have installed Rust programming language https://www.rust-lang.org/tools/install

  3. Ensure you have sea-orm-cli installed https://crates.io/crates/sea-orm-cli

  4. async-std will be used as the async library

    $ cargo add async-std --features attributes
    

    This adds async-std src/Cargo.toml file

    [package]
    name = "sea-orm-todo"
    version = "0.1.0"
    edition = "2021"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [dependencies]
    + async-std = { version = "1.10.0", features = ["attributes"] }
    
  5. Add anyhow crate for error handling

    $ cargo add anyhow
    

    An entry in theCargo.toml file is added

    [package]
    name = "todo-client"
    version = "0.1.0"
    edition = "2021"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [dependencies]
    + anyhow = "1.0.53"
      async-std = { version = "1.10.0", features = ["attributes"] }
    
    
  6. Add sea-orm with the features to enable sql drivers for PostgreSQL backend

    $  cargo add sea-orm --no-default-features --features "runtime-async-std-rustls sqlx-postgres macros"
    

    This adds sea-orm to src/Cargo.toml

    [package]
    name = "sea-orm-todo"
    version = "0.1.0"
    edition = "2021"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [dependencies]
      anyhow = "1.0.53"
      async-std = { version = "1.10.0", features = ["attributes"] }
    + sea-orm = { version = "0.6.0", features = [
    +     "runtime-async-std-rustls",
    +     "sqlx-postgres",
    +     "macros",
    + ], default-features = false }
    
    
  7. Login to Postgres database and create a new user and database

    $ sudo -u postgres psql postgres
    

    Create a new user in the PostgreSQL prompt.

    postgres=# CREATE ROLE webmaster LOGIN PASSWORD 'master_char';
    

    Create the fruits_market database and assign it to the webmaster user

    postgres=# CREATE DATABASE fruits_market WITH OWNER = webmaster;
    
  8. Create a .env file in the workspace directory

    The file structure should look

    SeaORM-TODO-App
    	|-- Cargo.toml
    	|-- TODO-Client
    			|-- src
    			|-- Cargo.toml
    +   		|-- .env
    	|-- TODO-Server
    

    Then add the PostgreSQL configuration with the new user webmaster and database fruits_market we created earlier

    File: SeaORM-TODO-App/TODO-Server/.env

    + DATABASE_URL=postgres://webmaster:master_char@localhost/fruits_market
    

    Change the main function to async function using async-std

    - fn main() {
    -     println!("Hello, world!");
    - }
    
    + #[async_std::main]
    + async fn main() -> anyhow::Result<()> {
    +     Ok(())
    + }
    

    Next, we will create all the required tables and their relationships

Creating Tables

First, create a database config for the sea_orm::DatabaseConnection to use to connect and authenticate to the PostgreSQL server.

+ use async_std::sync::Arc;
+ use sea_orm::{Database, DbBackend};

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    //Define the database backend
    + let db_postgres = DbBackend::Postgres;

    // Read the database environment from the `.env` file
    + let env_database_url = include_str!("../.env").trim();
    // Split the env url
    + let split_url: Vec<&str> = env_database_url.split("=").collect();
    // Get item with the format `database_backend://username:password@localhost/database`
    + let database_url = split_url[1];

    // Perform a database connection
    + let db = Database::connect(database_url).await?;
    + let db = Arc::new(db);

    Ok(())
}

The include_str!("../.env").trim(); reads the .env file and loads it's content at compile time. This content is the PostgreSQL database configuration which we later split using split("=") and discard the DATABASE_URL= part since it's only needed by sea-orm-cli and not Database::connect() which only accepts database_backend://username:password@localhost/database.

Calling the Database::connect() on the parsed URL creates a DatabaseConnection that will perform all CRUD operations. This connection is kept behind an async_std::sync::Arc for thread safety when we spawn async tasks.

Add the code to create the three tables, todos, fruits and suppliers.

FILE:src/main.rs

  use async_std::sync::Arc;
- use sea_orm::{Database, DbBackend};
+ use sea_orm::{
+     sea_query::{Alias, ColumnDef, ForeignKey, ForeignKeyAction, Table},
+     ConnectionTrait, Database, DbBackend,
+ };

#[async_std::main]
async fn main() -> anyhow::Result<()> {

// --- code snippet ---

// Create the fruits table
    let fruits_table = Table::create()
        .table(Alias::new("fruits"))
        .if_not_exists()
        .col(
            ColumnDef::new(Alias::new("fruit_id"))
                .integer()
                .auto_increment()
                .primary_key()
                .not_null(),
        )
        .col(
            ColumnDef::new(Alias::new("fruit_name"))
                .string()
                .unique_key()
                .not_null(),
        )
        .to_owned();

    // Executing the SQL query to create the `fruits_table` in PostgreSQL
    let create_table_op = db.execute(db_postgres.build(&fruits_table)).await;
    // Print the result in a user friendly way
    println!(
        "`CREATE TABLE fruits` {:?}",
        match create_table_op {
            Ok(_) => "Operation Successful".to_owned(),
            Err(e) => format!("Unsuccessful - Error {:?}", e),
        }
    );

    // Create the `todos` table
    let todos_table = Table::create()
        .table(Alias::new("todos"))
        .if_not_exists()
        .col(
            ColumnDef::new(Alias::new("todo_id"))
                .integer()
                .auto_increment()
                .primary_key()
                .not_null(),
        )
        .col(
            ColumnDef::new(Alias::new("username"))
                .string()
                .unique_key()
                .not_null(),
        )
        .col(ColumnDef::new(Alias::new("todo_list")).string())
        .to_owned();

    // Executing the SQL query to create the `todos` table in PostgreSQL
    let create_table_op = db.execute(db_postgres.build(&todos_table)).await;
    // Print the result in a user friendly way
    println!(
        "`CREATE TABLE todos` {:?}",
        match create_table_op {
            Ok(_) => "Operation Successful".to_owned(),
            Err(e) => format!("Unsuccessful - Error {:?}", e),
        }
    );
    
	Ok(())
}

The previous tutorial gave an introduction on creating tables. Table::create() is the method to do this. Then the db.execute() method performs the database operation for table creation.

Next, use sea-orm-cli to auto-generate the code for Entity, Model, Relation , etc...

$ sea-orm-cli generate entity -o src/todo_list_table -t todos #The todos table

$ sea-orm-cli generate entity -o src/fruits_list_table -t fruits #The fruits table

$ sea-orm-cli generate entity -o src/suppliers_list_table -t suppliers #The suppliers table

This generates new directories

SeaORM-TODO-App
	|-- Cargo.toml
	|-- .env
	|-- src
+ 	|-- suppliers_list_table
+ 		|-- mod.rs
+ 		|-- prelude.rs
+ 		|-- suppliers.rs
+ 	|-- fruits_list_table
+ 		|-- mod.rs
+ 		|-- prelude.rs
+ 		|-- fruits.rs
+ 	|-- todo_list_table
+ 		|-- mod.rs
+ 		|-- prelude.rs
+ 		|-- todos.rs

Modify the src/suppliers_list_table/prelude.rs and import the types using friendly names.

- pub use super::suppliers::Entity as Suppliers;

+ pub use super::suppliers::{
+     ActiveModel as SuppliersActiveModel, Column as SuppliersColumn, Entity as Suppliers,
+     Model as SuppliersModel, PrimaryKey as SuppliersPrimaryKey, Relation as SuppliersRelation,
+ };

Do the same to the src/fruits_table/prelude.rs

- pub use super::fruits::Entity as Fruits;

+ pub use super::fruits::{
+     ActiveModel as FruitsActiveModel, Column as FruitsColumn, Entity as Fruits,
+     Model as FruitsModel, PrimaryKey as FruitsPrimaryKey, Relation as FruitsRelation,
+ };

Do the same ot the src/todos_table/prelude.rs

//! SeaORM Entity. Generated by sea-orm-codegen 0.5.0
- pub use super::todos::Entity as Todos;

+ pub use super::todos::{
+     ActiveModel as TodosActiveModel, Column as TodosColumn, Entity as Todos, Model as TodosModel,
+     PrimaryKey as TodosPrimaryKey, Relation as TodosRelation,
+ };

Modify the Model and Relation part of Suppliers Entity to import Fruits entity properly

//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "suppliers")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub suppliers_id: i32,
    #[sea_orm(unique)]
    pub suppliers_name: String,
    pub fruit_id: i32,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(
-		belongs_to = "super::fruits::Entity",
+		belongs_to = "crate::Fruits",
        from = "Column::FruitId",
-		to = "super::fruits::Column::FruitId",
+		to = "crate::FruitsColumn::FruitId",
        on_update = "Cascade",
        on_delete = "Cascade"
    )]
    Fruits,
}

- impl Related<super::fruits::Entity> for Entity {
+ impl Related<crate::Fruits> for Entity {
    fn to() -> RelationDef {
        Relation::Fruits.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}

Import these modules into src/main.rs

// --- code snippet ---
+ mod fruits_list_table;
+ mod suppliers_list_table;
+ mod todo_list_table;

+ pub use fruits_list_table::prelude::*;
+ pub use suppliers_list_table::prelude::*;
+ pub use todo_list_table::prelude::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    // --- code snippet ---
    
    Ok(())
 }

Next, populate the fruits and suppliers tables with data.

Create a new file src/insert_values.rs and add the following code:

use crate::{Fruits, FruitsActiveModel, Suppliers, SuppliersActiveModel};
use sea_orm::{DatabaseConnection, EntityTrait, Set};

// Insert suppliers in the `suppliers` table
pub async fn insert_fruits(db: &DatabaseConnection) -> anyhow::Result<()> {

    let apple = FruitsActiveModel {
        fruit_name: Set("Apple".to_owned()),
        ..Default::default()
    };

    let orange = FruitsActiveModel {
        fruit_name: Set("Orange".to_owned()),
        ..Default::default()
    };

    let mango = FruitsActiveModel {
        fruit_name: Set("Mango".to_owned()),
        ..Default::default()
    };

    let pineapple = FruitsActiveModel {
        fruit_name: Set("Pineapple".to_owned()),
        ..Default::default()
    };

    let fruit_insert_operation = Fruits::insert_many(vec![apple, orange, mango, pineapple])
        .exec(db)
        .await;

    println!("INSERTED FRUITS: {:?}", fruit_insert_operation?);

    Ok(())
}

Here, ActiveModel is used to prepare the data for insertion into the database using Entity::insert() .

Import this module to the src/main.rs file and call these functions to perform insert operations

// --- code snippet ---
+ mod insert_values;
+ pub use insert_values::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    // --- code snippet ---
    
+	 insert_fruits(&db).await?;
+    insert_suppliers(&db).await?;
    
     Ok(())
}

Building Server Connections and Responses

Create a new director in the src folder called server.

Inside the server directory create mod.rs, tcp_api.rs and connection.rs

SeaORM-TODO-App/src
				|-- src/fruits_table
                |-- src/insert_values.rs
                |-- src/main.rs
                |-- src/suppliers_table
                |-- src/todos_table 
                |-- src/server
+                	|-- mod.rs
+                	|-- tcp_api.rs
+                	|-- connection.rs

Import the new submodules to src/server/mod.rs file


#![allow(unused)]
fn main() {
mod tcp_api;
mod connection;

pub use tcp_api::*;
pub use connection::*;
}

Then register the module to the src.main.rs file

// -- code snippet --

mod server;
pub use server::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
	// -- code snippet --
	
	Ok(())
}

The TCP API

Create the commands that the tcp api will handle. In the src/server/tcp_api.rs add:

use serde::{Serialize, Deserialize};

// The commands to use to perform CRUD operations on PostgreSQL
#[derive(Debug, Serialize, Deserialize)]
pub enum Command {
    Store { username: String, todo_list: String },
    UpdateTodoList { username: String, todo_list: String },
    Get(String),
    CreateUser(String),
    ListFruits,
}

Command::Store { username: String, todo_list: String } will handle an insert operation, inserting the todo_list in the row with the column labeled by username.

Command::UpdateTodoList { username: String, todo_list: String } will handle an update operation, inserting the todo_list in the row with the column labeled by username.

Command::Get(String) will fetch the todo_list from the column username with the username in the String field.

Command::CreateUser(String) will create a new row with the String field being inserted in the username column.

Command::ListFruits will fetch all the fruits in the fruits table.

The Command enum will be deserialized by bincode crate. Add the bincode and serde crates to Cargo.toml file

$ cargo add bincode

$ cargo add serde --features derive

Add error handling capabilities incase the wrong command is invoked

File: src/server/tcp_api.rs

// -- code snippet --

#[derive(Debug)]
pub enum ServerErrors {
    InvalidCommand,
    ModelNotFound,
}

impl Error for ServerErrors {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ServerErrors::InvalidCommand => Some(&crate::ServerErrors::InvalidCommand),
            ServerErrors::ModelNotFound => Some(&crate::ServerErrors::ModelNotFound),
        }
    }
}

impl fmt::Display for ServerErrors {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{:?}",
            match self {
                ServerErrors::InvalidCommand => "Invalid command provided",
                ServerErrors::ModelNotFound => "The result of the query is `None`",
            }
        )
    }
}

The ServerErrors::InvalidCommand is returned when the method called on a Command is invalid while the ServerErrors::ModelNotFound is returned when a Model is not found in the database.

Then implement the methods for the Command enum that will handle database operations

File: src/server/tcp_api.rs

// -- code snippet --
+ use crate::{Fruits, Suppliers, Todos, TodosActiveModel, TodosColumn, TodosModel};
+ use sea_orm::{
+     ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, Set,
+ };
  use serde::{Deserialize, Serialize};
+ use std::{error::Error, fmt};

// The commands to use to perform CRUD operations on PostgreSQL
#[derive(Debug, Serialize, Deserialize)]
pub enum Command {
    Store { username: String, todo_list: String },
    UpdateTodoList { username: String, todo_list: String },
    Get(String),
    CreateUser(String),
    ListFruits,
}


Implement methods to handle the commands from the api

File: src/server/tcp_api.rs

// -- code snippet --
// The commands to use to perform CRUD operations on PostgreSQL
#[derive(Debug, Serialize, Deserialize)]
pub enum Command {
    Store { username: String, todo_list: String },
    UpdateTodoList { username: String, todo_list: String },
    Get(String),
    CreateUser(String),
    ListFruits,
}

impl Command {
    pub async fn get_fruits(&self, db: &DatabaseConnection) -> anyhow::Result<Vec<u8>> {
        let fruit_models = Fruits::find().all(db).await?;
        let fruits = fruit_models
            .iter()
            .map(|fruit_model| fruit_model.fruit_name.clone())
            .collect::<Vec<String>>();

        Ok(bincode::serialize(&fruits)?)
    }

    pub async fn store(&self, db: &DatabaseConnection) -> anyhow::Result<Vec<u8>> {
        match self {
            Self::Store {
                username,
                todo_list,
            } => {
                let todo_user = TodosActiveModel {
                    username: Set(username.to_owned()),
                    todo_list: Set(Some(todo_list.to_owned())),
                    ..Default::default()
                };
                Todos::insert(todo_user).exec(db).await?;

                Ok(bincode::serialize("INSERTED")?)
            }
            _ => Err(anyhow::Error::new(ServerErrors::InvalidCommand)),
        }
    }

    pub async fn create_new_user(&self, db: &DatabaseConnection) -> anyhow::Result<Vec<u8>> {
        match self {
            Self::CreateUser(username) => {
                let todo_user = TodosActiveModel {
                    username: Set(username.to_owned()),
                    ..Default::default()
                };
                Todos::insert(todo_user).exec(db).await?;

                Ok(bincode::serialize(&format!("CREATED_USER `{}`", username))?)
            }
            _ => Err(anyhow::Error::new(ServerErrors::InvalidCommand)),
        }
    }

    pub async fn get_user_todo(&self, db: &DatabaseConnection) -> anyhow::Result<Vec<u8>> {
        match self {
            Self::Get(user) => {
                let get_todo = Todos::find()
                    .filter(TodosColumn::Username.contains(user))
                    .one(db)
                    .await?;

                if let Some(found_todo) = get_todo {
                    Ok(bincode::serialize(&found_todo.todo_list)?)
                } else {
                    Ok(bincode::serialize(&Some("USER_NOT_FOUND"))?)
                }
            }
            _ => Err(anyhow::Error::new(ServerErrors::InvalidCommand)),
        }
    }

    pub async fn update_todo_list(&self, db: &DatabaseConnection) -> anyhow::Result<Vec<u8>> {
        match self {
            Self::UpdateTodoList {
                username,
                todo_list,
            } => {
                let found_todo: Option<TodosModel> = Todos::find()
                    .filter(TodosColumn::Username.contains(username))
                    .one(db)
                    .await?;

                match found_todo {
                    Some(todo_model) => {
                        let mut todo_model: TodosActiveModel = todo_model.into();
                        todo_model.todo_list = Set(Some(todo_list.to_owned()));
                        todo_model.update(db).await?;
                    }
                    None => return Err(anyhow::Error::new(ServerErrors::ModelNotFound)),
                };

                Ok(bincode::serialize("UPDATED_TODO")?)
            }
            _ => Err(anyhow::Error::new(ServerErrors::InvalidCommand)),
        }
    }
}

The get_fruits() method handles the Command::ListFruits command and it is responsible for fetching the list of fruits in the database.

The store() method handles the Command:: Store {..} command and it inserts the field todo_list in the username column corresponding to the username field.

The create_new_user() method handles Command::CreateUser(..) command, it creates a new user by inserting the String field data to the username column and an empty entry in the todo_list column.

The get_user_todo() method handles Command::Get(..) command. It is used mostly to check if the user in the String field exists in the username column.

The update_todo_list() method handles the Command:: UpdateTodoList {..} command and it updates the field todo_list in the username column corresponding to the username field.

The TCP API handler

The TcpStream will need to be handled. The src/server/connection.rs file contains the code for this.

File: src/server/connection.rs

// Import the necessary async versions of TcpStream and TcpListener
use crate::Command;
use async_std::{
    net::{Shutdown, SocketAddr, TcpListener, TcpStream},
    prelude::*,
    sync::Arc,
    task,
};

use sea_orm::DatabaseConnection;

const BUFFER_DATA_CAPACITY: usize = 1024 * 1024; // The todo list should not exceed 1MiB
const BUFFER_CAPACITY: usize = 64 * 1024; //64Kib

// function is called to create a new server on port 8080 localhost
pub async fn start_server(db: Arc<DatabaseConnection>) -> anyhow::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("Listening on 127.0.0.1:8080");

    while let Some(stream) = listener.incoming().next().await {
        let stream = stream?;
        let db = db.clone();

        task::spawn(async move {
            match process_stream(db, stream).await {
                Ok(addr) => {
                    println!("x → {addr:?} - DISCONNECTED")
                }
                Err(error) => {
                    eprintln!("{:?}", error);
                }
            }
        })
        .await;
    }

    Ok(())
}

async fn process_stream(
    db: Arc<DatabaseConnection>,
    mut stream: TcpStream,
) -> anyhow::Result<SocketAddr> {
    let peer = stream.peer_addr()?;
    println!("← → {peer:?} - CONNECTED");
    let mut buffer = [0u8; BUFFER_CAPACITY];
    let mut command_buffer: Vec<u8> = Vec::new();
    let bytes_read = stream.read(&mut buffer).await?;
    while bytes_read != 0 {
        if command_buffer.len() > BUFFER_DATA_CAPACITY {
            handle_response(&mut stream, b"BUFFER_CAPACITY_EXCEEDED_1MiB".to_vec()).await?;
        }

        // Check if the current stream is less than the buffer capacity, if so all data has been received
        if buffer[..bytes_read].len() < BUFFER_CAPACITY {
            // Ensure that the data is appended before being deserialized by bincode
            command_buffer.append(&mut buffer[..bytes_read].to_owned());
            let dbop_result = process_database_op(&db, &command_buffer).await?;
            handle_response(&mut stream, dbop_result).await?;
            break;
        }
        // Append data to buffer
        command_buffer.append(&mut buffer[..bytes_read].to_owned());
    }

    let peer = stream.peer_addr()?;
    //Shutdown the TCP address
    stream.shutdown(Shutdown::Both)?;
    // Terminate the stream if the client terminates the connection by sending 0 bytes
    return Ok(peer);
}

async fn handle_response(stream: &mut TcpStream, reponse_data: Vec<u8>) -> anyhow::Result<()> {
    stream.write_all(&reponse_data).await?;

    stream.flush().await?;

    Ok(())
}

async fn process_database_op(
    db: &DatabaseConnection,
    command_buffer: &[u8],
) -> anyhow::Result<Vec<u8>> {
    let command: Command = bincode::deserialize(command_buffer)?;

    let db_op = match command {
        Command::Get(..) => command.get_user_todo(db).await,
        Command::CreateUser(..) => command.create_new_user(db).await,
        Command::ListFruits => command.get_fruits(db).await,
        Command::ListSuppliers => command.get_suppliers(db).await,
        Command::Store { .. } => command.store(db).await,
        Command::UpdateTodoList { .. } => command.update_todo_list(db).await,
        Command::DeleteUser(..) => command.delete_user(db).await,
    };

    match db_op {
        Ok(value) => Ok(value),
        Err(error) => Ok(bincode::serialize(&error.to_string())?),
    }
}

Here, the BUFFER_DATA_CAPACITY caps the TODO list data at 1MiB and limits the buffer capacity for the TCP stream using BUFFER_CAPACITY capped at 64KiB.

The start_server() function creates a TcpListener at port 8080 localhost IP 127.0.0.1. It accepts a database connection inside an Arc<DatabaseConnection> for thread safety when we spawn a task to handle the stream. Each TcpStream is handled asynchronously using a async::task::spawn() method.

stream.read(&mut buffer).await?; reads the stream. The while loop loops until the stream returns a 0_usize indicating the connection has been closed by the peer and if data has been received, it checks if the data has exceeded the

BUFFER_DATA_CAPACITY of 1MiB, if not it decodes the buffer using bincode and passes the data to the process_database_op() function which matches the deserialized Command and calls the appropriate method which in turn performs the database operation, encodes the result of the database operation and writes it back to the peer using the handle_response() function.

Start the server

Lastly, inside the src/main.rs file, start the server

File: src/main.rs

use async_std::sync::Arc;
use sea_orm::{
    sea_query::{Alias, ColumnDef, ForeignKey, ForeignKeyAction, Table},
    ConnectionTrait, Database, DbBackend,
};

// -- code snippet --

#[async_std::main]
async fn main() -> anyhow::Result<()> {

	// -- code snippet --
	
	
    insert_fruits(&db).await?;
    insert_suppliers(&db).await?;

+	start_server(db).await?;

    Ok(())
}

Run the program using cargo run. It print the following to the terminal

$ Running `/media/su43/IGIED-01/Rust-Projects/SeaQL/SeaORM-TODO-App/target/debug/todo-server`
`CREATE TABLE fruits` "Operation Successful"
`CREATE TABLE suppliers` "Operation Successful"
`CREATE TABLE todos` "Operation Successful"
INSERTED FRUITS: InsertResult { last_insert_id: 1 }
INSERTED SUPPLIERS: InsertResult { last_insert_id: 1 }
Listening on 127.0.0.1:8080

The server is now listening on 127.0.0.1:8080 for incoming TcpStreams.

Next, we build the client.

Building The TODO TCP Client

This chapter focuses on creating the TCP client. Switch to the TODO-Client directory in the workspace.

Configuration

Add the necessary dependencies to create the client.

$ cargo add async-std --features attributes

$ cargo add anyhow

$ cargo add bincode

$ cargo add serde --features derive

$ cargo add serde_json

bincode crate will be used to prepare the bytes of Command to send over the wire. serde_json crate will serialize the TODO list data structure that contains queued and completed TODOs into a JSON string for remote storage in the PostgreSQL database.

The TCP client will also store local cache, simulating a real world setup especially for a desktop or mobile client. SQLite will be the preferred database for this tutorial due to it's popularity. A command line frontend and a TCP stream will be used to keep the tutorial simple and easy to port to other domains like mobile device connection, desktop clients or HTTP clients if you wish to explore other domains.

Add sea-orm crate with the SQLite features enabled for the local persistent cache. The runtime features runtime-async-std-rustls are used since the async library for this client is async-std crate.

$ cargo add sea-orm  --features "runtime-async-std-rustls sqlx-sqlite macros" --no-default-features

Modify the main function in src/main.rs to use async-std

- fn main() {
-     println!("Hello, world!");
- }

+ #[async_std::main]
+ async fn main() -> anyhow::Result<()>{
+     Ok(())
+ }

Next, create a .env file in the current directory. This will contain the database configuration.

File: TODO-Client/.env

DATABASE_URL=sqlite://my_todos.db

Here, the sqlite URL does not take a username, password and IP since SQLite does not have have a server, just the database name my_todos.db.

Create an empty SQLite database using the command:

$ sqlite3 my_todos.db "VACUUM;"

The "VACUUM;" part of the command will ensure the created database is not just held in memory but also persisted to the file system even though it is empty.

Local SQLite Database Operations

Create a file src/db_ops.rs which will contain functions to perform database operations.

use async_std::{
    io::{ReadExt, WriteExt},
    net::TcpStream,
};
use sea_orm::{
    sea_query::{Alias, ColumnDef, Table},
    ActiveModelTrait, ConnectionTrait, Database, DatabaseConnection, EntityTrait, Set,
};

pub async fn database_config() -> Result<DatabaseConnection, sea_orm::DbErr> {
    // Read the database environment from the `.env` file
    let env_database_url = include_str!("../.env").trim();
    // Split the env url
    let split_url: Vec<&str> = env_database_url.split("=").collect();
    // Get item with the format `database_backend://username:password@localhost/database`
    let database_url = split_url[1];

    Database::connect(database_url).await
}


pub async fn create_todo_table(db: &DatabaseConnection) -> anyhow::Result<()> {
    let database_backend = db.get_database_backend();
    // Create the `todos` table
    let todos_table = Table::create()
        .table(Alias::new("todo_list"))
        .if_not_exists()
        .col(
            ColumnDef::new(Alias::new("todo_id"))
                .integer()
                .primary_key()
                .not_null()
                .auto_increment(),
        )
        .col(
            ColumnDef::new(Alias::new("todo_name"))
                .string()
                .unique_key()
                .not_null(),
        )
        .col(ColumnDef::new(Alias::new("quantity")).string().not_null())
        .col(ColumnDef::new(Alias::new("status")).boolean().not_null())
        .to_owned();
    let create_table_op = db.execute(database_backend.build(&todos_table)).await;

    // Executing the SQL query to create the `todos` table in SQLite
    let create_table_op = db.execute(database_backend.build(&todos_table)).await;
    // Print the result in a user friendly way
    println!(
        "`CREATE TABLE todo_list` {:?}",
        match create_table_op {
            Ok(_) => "Operation Successful".to_owned(),
            Err(e) => format!("Unsuccessful - Error {:?}", e),
        }
    );

    Ok(())
}

database_config() reads the .env file and parses the database URL, creates a database connection with the URL using Database::connect() and then returns a DatabaseConnection.

create_todo_table() when invoked will create a new todo_list table in the local SQLite database specified by the URL.

Import the db_ops module into src/main.rs and call both functions.

+ mod db_ops;
+ pub use db_ops::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
+	let db = database_config().await?;
+	create_todo_table(&db).await?;
    
    Ok(())
}

Next is to auto-generate the Model, ActiveModel , Entity, etc... using the sea-orm-cli and pass in --with-serde both feature flag to auto-generate serde::Serialize and serde::Deserialize on the Entity.

$ sea-orm-cli generate entity -o src/todo_list_table -t todo_list --with-serde both

This will create a new directory todo_list_table in the src/ directory.

Open the src/todo_list_table/prelude.rs file and import the Entity, Model and ActiveModel using friendly names.

File:src/todo_list_table/prelude.rs

//! SeaORM Entity. Generated by sea-orm-codegen 0.6.0

- pub use super::todo_list::Entity as TodoList;

+ pub use super::todo_list::{
+     ActiveModel as MyTodosActiveModel, Column as MyTodosColumn, Entity as MyTodos,
+     Model as MyTodosModel, PrimaryKey as MyTodosPrimaryKey, Relation as MyTodosRelation,
+ };

Import the modules to the src/main.rs file

  mod db_ops;
+ mod todo_list_table;

  pub use db_ops::*;
+ pub use todo_list_table::prelude::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    let db = database_config().await?;
    create_todo_table(&db).await?;

    Ok(())
}

Common Data Structures

To perform more database operations, create a common.rs file in the src directory. This file will contain common data structures for use throughout database operations and TCP connections.

File: src/common.rs

use crate::MyTodosModel;
use serde::{Deserialize, Serialize}; // The commands to use to perform CRUD operations on PostgreSQL

// The commands to use to perform CRUD operations on PostgreSQL
#[derive(Debug, Serialize, Deserialize)]
pub enum Command {
    Store { username: String, todo_list: String },
    UpdateTodoList { username: String, todo_list: String },
    Get(String),
    CreateUser(String),
    ListFruits,
}

//  The structure for a TodoList
#[derive(Debug, Serialize, Default, Deserialize)]
pub struct TodoList {
    pub queued: Vec<MyTodosModel>,
    pub completed: Vec<MyTodosModel>,
}

The enum Command mirrors the Command created in the previous chapter in the TODO-Server/src/tcp_api.rs file.

The TodoList struct contains the Models MyTodoModel sorted either as queued which are TODOs not done or completed which are TODOs that are done.

Import this file to the src/main.rs file

+ mod common;
  mod db_ops;
  mod todo_list_table;

+ pub use common::*;
  pub use db_ops::*;
  pub use todo_list_table::prelude::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    let db = database_config().await?;
    create_todo_table(&db).await?;

    Ok(())
}

Formating Utilities

To create a better user experience, data will be formatted before it is displayed to the command line on stdout.

Overview of Code Formatting

Create a utils.rs file which will hold the utilities and add the following code blocks:

File: src/utils.rs

use crate::MyTodosModel;
use async_std::sync::Mutex;
use std::collections::HashMap;

pub(crate) const TITLE: &str = "FRUITS AVAILABLE";
pub(crate) const NUMBER: &str = "No.";
pub(crate) const ADD_COMMAND: &str = "ADD";
pub(crate) const DONE_COMMAND: &str = "DONE";
pub(crate) const UNDO_COMMAND: &str = "UNDO";
pub(crate) const EDIT_COMMAND: &str = "EDIT";
pub(crate) const EXIT_COMMAND: &str = "EXIT";


const DONE: &str = "DONE TODOS";
const NOT_DONE: &str = "NOT DONE";
const QUANTITY: &str = "QUANTITY";

pub(crate) type MemDB = Mutex<HashMap<String, MyTodosModel>>;

pub fn clear_terminal() {
    print!("\x1B[2J\x1B[1;1H");
}

pub fn synching() {
    clear_terminal();
    println!("SYNCING TO DATABASE...");
}
pub fn synching_to_server() {
    println!("SYNCING TO SERVER...");
}

pub fn loading() {
    clear_terminal();
    println!("LOADING FROM DATABASE...");
}

pub fn convert_case(word: &str) -> String {
    let word = word.to_lowercase();
    let mut chars = word
        .chars()
        .map(|character| character.to_string())
        .collect::<Vec<String>>();

    chars[0] = chars[0].to_uppercase().to_string();

    chars.into_iter().collect::<String>()
}

pub fn split_words(user_input: String) -> Vec<String> {
    user_input
        .split(" ")
        .map(|word| word.to_owned())
        .collect::<Vec<String>>()
}

The TITLE and NUMBER constants are used to format the headings for the fruits table which displays the list of fruits on the command-line interface. The constants DONE, NOT_DONE and QUANTITY are used as the headings of the TODO list.

Interaction Commands

To interact with the client, a user will input a command, similar to pressing a button in a GUI or any other GUI event that performs an operation based on user input. The current list of commands are:

The ADD_COMMAND constant holds the ADD command. This command allows a user to queue a task in the TODO list. The format is ADD QUANTITY_IN_KG FRUIT_NAME.

The DONE_COMMAND constant holds the DONE command. This command allows a user to mark a task as completed in the TODO list. The format is DONE FRUIT_NAME.

The UNDO_COMMAND constant holds the UNDO command. This command allows a user to move a completed task back into the queue in the TODO list. The format is UNDO FRUIT_NAME.

The EDIT_COMMAND constant holds the EDIT command. This command allows a user to modify a task in the TODO list by changing it's quantity. The format is EDIT QUANTITY_IN_KG FRUIT_NAME.

The EXIT_COMMAND constant holds the EXIT command. This command allows a user to exit the client gracefully and sync the local database cache with the remote PostgreSQL server. The format is EXIT .

Word formating

A number of functions are presented in the code block above:

clear_terminal() is used to clear the terminal using the command line specific flags \x1B[2J\x1B[1;1H

synching() is used to show that the TODO list is being synced to the local SQLite database cache.

synching_to_server() is used to show that the TODO list is being synced to the remote PostgreSQL database using the TCP API built in the previous chapter.

loading() is used to show that information about the user is being fetched from the remote PostgreSQL database.

convert_case() is used to format the fruit name to Title Case, for example, a user can enter a fruit named Apple as apple, Apple, aPPLe, ApplE, etc... This makes the user experience much smoother.

split_words() is used to split the text buffer from the user input into individual parts that correspond with the format specified in the Commands like COMMAND QUANTITY_IN_KG FRUIT_NAME.

In-memory Database

Instead of doing database I/O by querying SQLite database every time we need to check the existence of data, we will use an in-memory database described by MemDB which contains a Mutex<HashMap<String, MyTodosModel>> scoped to the internals of the crate. This is a HashMap indexed using a String which is the name of the todo in the Model and the value of the indexing key set to the MyTodosModel. The HashMap is protected by a Mutex for thread-safety.

Formatting the TODO List

To format the list of TODOs in local cache and display them to the command-line interface, add the following to the

File: src/utils.rs

pub async fn format_todos(todo_models: &MemDB) {
    println!("\n\n\n");
    if todo_models.lock().await.is_empty() {
        println!("Oh My! There are no TODOs");
    } else {
        let mut done = Vec::<MyTodosModel>::default();
        let mut not_done = Vec::<MyTodosModel>::default();

        todo_models.lock().await.iter().for_each(|todo| {
            if todo.1.status == 0 {
                not_done.push(todo.1.to_owned());
            } else {
                done.push(todo.1.to_owned());
            }
        });

        if not_done.is_empty() {
            println!("Wohooo! All TODOs are Completed.")
        } else {
            println!("{QUANTITY:9}| {NOT_DONE:10}");
            println!("----------------");
            not_done.iter().for_each(|todo| {
                println!("{:>8} | {:10}", todo.quantity, todo.todo_name);
            });
            println!("----------------\n");
        }

        if done.is_empty() {
            println!("----------------");
            println!("Bummer :( You Have Not Completed Any TODOs!");
            println!("----------------\n\n");
        } else {
            println!("{QUANTITY:9}| {DONE:10}");
            println!("----------------");
            done.iter().for_each(|todo| {
                println!("{:>8} | {:10}", todo.quantity, todo.todo_name);
            });
            println!("----------------\n");
        }
    }
}

format_todos() functions takes the in-memory database and loops through it, first checking if there are no TODOs and prints "Oh My! There are no TODOs" . If TODOs are found, it iterates through them and sorts the completed todos into the done Vector declared by let mut done = Vec::<MyTodosModel>::default(); or the queued into the not_done declared by let mut not_done = Vec::<MyTodosModel>::default(); There are no completed TODOs but there are queued ones, it prints "Bummer :( You Have Not Completed Any TODOs!" and if there are no queued TODOs but completed ones, it prints "Wohooo! All TODOs are Completed.".

The MyTodosModel is the Model for the Entity table todo_list in the local SQLite database cache.

Import the utils module in the src/main.rs file

  mod common;
  mod db_ops;
  mod todo_list_table;
+ mod utils;

  pub use common::*;
  pub use db_ops::*;
  pub use todo_list_table::prelude::*;
+ pub use utils::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    let db = database_config().await?;
    create_todo_table(&db).await?;

    Ok(())
}

Remote Database Operations

To persist data remotely in the PostgreSQL database, modify the src/db_ops.rs file and add thew following code

File: src/db_ops.rs

// -- code snippet --
use crate::{
    synching_to_server, Command, MemDB, MyTodos, MyTodosActiveModel, MyTodosModel, TodoList,
};

pub async fn get_fruits() -> anyhow::Result<Vec<String>> {
    // Get the fruits first
    let get_fruits = Command::ListFruits;
    let serialized_command = bincode::serialize(&get_fruits)?;
    let mut fruits_list: Vec<String>;

    let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
    stream.write_all(&serialized_command).await?;

    let mut fruits_buf = vec![0u8; 4096];
    loop {
        let n = stream.read(&mut fruits_buf).await?;
        let rx: Vec<_> = bincode::deserialize(&fruits_buf).unwrap();

        fruits_list = rx;

        if n != 0 {
            break;
        }
    }

    Ok(fruits_list)
}

pub async fn store(db: &DatabaseConnection, quantity: &str, todo_name: &str) -> anyhow::Result<()> {
    let my_todo = MyTodosActiveModel {
        todo_name: Set(todo_name.to_owned()),
        quantity: Set(quantity.to_owned()),
        status: Set(0),
        ..Default::default()
    };

    MyTodos::insert(my_todo).exec(db).await?;

    Ok(())
}

pub async fn get(db: &DatabaseConnection) -> Result<Vec<MyTodosModel>, sea_orm::DbErr> {
    MyTodos::find().all(db).await
}

pub async fn edit(
    db: &DatabaseConnection,
    todo_model: &MyTodosModel,
    quantity: String,
) -> Result<MyTodosModel, sea_orm::DbErr> {
    let mut todos_active_model: MyTodosActiveModel = todo_model.to_owned().into();
    todos_active_model.quantity = Set(quantity);

    Ok(todos_active_model.update(db).await?)
}

pub async fn done(
    db: &DatabaseConnection,
    todo_model: &MyTodosModel,
) -> Result<MyTodosModel, sea_orm::DbErr> {
    let mut todos_active_model: MyTodosActiveModel = todo_model.to_owned().into();
    todos_active_model.status = Set(1);

    Ok(todos_active_model.update(db).await?)
}

pub async fn undo(
    db: &DatabaseConnection,
    todo_model: &MyTodosModel,
) -> Result<MyTodosModel, sea_orm::DbErr> {
    let mut todos_active_model: MyTodosActiveModel = todo_model.to_owned().into();
    todos_active_model.status = Set(0);

    Ok(todos_active_model.update(db).await?)
}

pub(crate) async fn load_sqlite_cache(
    db: &DatabaseConnection,
    memdb: &mut MemDB,
) -> Result<(), sea_orm::DbErr> {
    let sqlite_cache = get(&db).await?;
    memdb.lock().await.clear();
    for mytodo_model in sqlite_cache {
        memdb
            .lock()
            .await
            .insert(mytodo_model.todo_name.clone(), mytodo_model);
    }

    Ok(())
}

pub async fn update_remote_storage(memdb: &MemDB, username: &str) -> anyhow::Result<()> {
    let mut temp_list = TodoList::default();
    memdb.lock().await.values().for_each(|todo| {
        if todo.status == 0 {
            temp_list.queued.push(todo.to_owned());
        } else {
            temp_list.completed.push(todo.to_owned());
        }
    });

    synching_to_server();

    // Update a todo_list
    let update_todo = Command::UpdateTodoList {
        username: username.to_owned(),
        todo_list: serde_json::to_string(&temp_list)?,
    };
    let serialized_command = bincode::serialize(&update_todo)?;

    let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
    stream.write_all(&serialized_command).await?;

    let mut buffer = vec![0u8; 4096];
    stream.read(&mut buffer).await?;

    bincode::deserialize::<String>(&buffer)?;

    Ok(())
}

pub async fn get_user_remote_storage(username: &str) -> anyhow::Result<Option<String>> {
    let get_user = Command::Get(username.to_owned());
    let serialized_command = bincode::serialize(&get_user)?;

    let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
    stream.write_all(&serialized_command).await?;

    let mut buffer = vec![0u8; 4096];
    stream.read(&mut buffer).await?;

    Ok(bincode::deserialize::<Option<String>>(&buffer)?)
}

pub async fn create_new_user(username: &str) -> anyhow::Result<String> {
    let create_user = Command::CreateUser(username.to_owned());
    let serialized_command = bincode::serialize(&create_user)?;

    let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
    stream.write_all(&serialized_command).await?;

    let mut buffer = vec![0u8; 4096];
    stream.read(&mut buffer).await?;

    Ok(bincode::deserialize::<String>(&buffer)?)
}

get_fruits() queries the list of fruits from the remote database.

store() will persist the contents of the in-memory database to local SQLite cache.

load_sqlite_cache() queries the local database a list of TODOs. This is useful when the client starts, since it fetches the cached TODOs and loads them into the in-memory database MemDB.

edit() persists the edits to the TODOs to the SQLite cache.

done() persists the state of the in-memory database with the completed TODOs in the SQLite cache.

undo() persists the state of the in-memory database with the queued TODOs in the SQLite cache reflecting the TODOs which have been moved from the completed Vector to the queued Vector.

update_remote_storage() updates the remote PostgreSQL database with the new changes in the TODO list.

get_user_remote_storage() checks if the username provided is in the remote PostgreSQL database.

create_new_user() creates a new user in the remote PostgreSQL database with the given username.

Up next is reading from the terminal and performing database operations bases on the command.

Reading User Input

Rust standard library provides an easy way of reading from and writing to the command-line commonly known as stdout. First, create a file in the src folder called user_input.rs.

File: src/user_input.rs

use crate::{
    format_todos, MemDB, ADD_COMMAND, DONE_COMMAND, EDIT_COMMAND, EXIT_COMMAND, NUMBER, TITLE,
    UNDO_COMMAND,
};
use async_std::io;

pub async fn read_line(
    buffer: &mut String,
    fruits_list: &Vec<String>,
    memdb: &MemDB,
    //todo_list: &Vec<String>,
) -> anyhow::Result<String> {
    crate::clear_terminal();
    buffer.clear();
    println!("+--------------------------+");
    println!("+ {:^5}{:17}+", "COMMANDS", " ");
    println!("+{:26}+", " ");
    println!("→   {ADD_COMMAND:5}{:18}+", " ");
    println!("→   {DONE_COMMAND:23}+");
    println!("→   {UNDO_COMMAND:23}+");
    println!("→   {EDIT_COMMAND:23}+");
    println!("→   {EXIT_COMMAND:23}+");
    println!("+{:26}+", " ");
    println!("+--------------------------+");

    println!("{NUMBER}| {TITLE:10}");
    println!("----------------");
    for (mut index, item) in fruits_list.iter().enumerate() {
        index += 1;
        println!("{index:2} | {item:10}");
    }
    println!("--------------------------------------------");
    format_todos(&memdb).await;

    println!("Enter a fruit that is available.",);
    let stdin = io::stdin(); // We get `Stdin` here.
    stdin.read_line(buffer).await?;

    Ok(buffer.to_owned())
}

read_line() is responsible for reading stdout for the user input and returning the user input as a String. It always clears the terminal using utils::clear_terminal(); before the next input, clears the buffer to prevent stale commands using buffer.clear(), lists the list of fruits that the user can add and formats the TODOs printing the sorted TODO list and a set of commands that the user can input to interact with the client.

User Input Handler

To handle the input create a file in the src directory called handler.rs

File: src/handler.rs

use crate::{
    convert_case, create_new_user, done, edit, get_fruits, get_user_remote_storage,
    load_sqlite_cache, loading, read_line, split_words, store, synching, undo,
    update_remote_storage, MemDB,
};
use async_std::io;
use sea_orm::DatabaseConnection;
use std::collections::HashMap;

pub async fn input_handler(db: &DatabaseConnection) -> anyhow::Result<()> {
    let mut username_buffer = String::default();
    println!("What is Your Username...",);
    let stdin = io::stdin(); // We get `Stdin` here.
    stdin.read_line(&mut username_buffer).await?;
    let username = username_buffer.trim().to_string();

    let fruits_list: Vec<String> = get_fruits().await?;

    let mut buffer = String::new();
    let mut text_buffer: String;
    let mut memdb = MemDB::new(HashMap::default());
    loading();
    load_sqlite_cache(db, &mut memdb).await?;

    let remote_result = get_user_remote_storage(&username).await?;
    if let Some(result_data) = remote_result {
        if result_data == "USER_NOT_FOUND" {
            create_new_user(&username).await?;
        }
    }

    loop {
        read_line(&mut buffer, fruits_list.as_ref(), &memdb).await?;
        buffer = buffer.trim().to_owned();
        let words = split_words(buffer.clone());
        let command = words[0].to_lowercase().to_string();
        let mut quantity: &str = "";
        if command.as_str() == "done" || command.as_str() == "undo" {
            text_buffer = convert_case(&words[1]);
        } else if command.as_str() == "exit" {
            update_remote_storage(&memdb, &username).await?;
            println!("SYNCED SUCCESSFULLY.");
            println!("Bye! :)");
            break;
        } else {
            quantity = &words[1];
            text_buffer = convert_case(&words[2]);
        }

        if !text_buffer.is_empty() {
            match fruits_list.iter().find(|&fruit| *fruit == text_buffer) {
                None => {
                    if !text_buffer.is_empty() {
                        println!("The fruit `{buffer}` is not available.\n",);
                    }
                    continue;
                }
                Some(_) => {
                    if command.as_str() == "add" {
                        if memdb.lock().await.contains_key(&text_buffer) {
                            continue;
                            //TODO
                        } else {
                            synching();
                            store(&db, quantity, &text_buffer).await?;
                            load_sqlite_cache(&db, &mut memdb).await?;
                        }
                    } else if command.as_str() == "edit" {
                        if let Some(mut todo_model) = memdb.lock().await.get_mut(&text_buffer) {
                            if todo_model.status != 1 {
                                synching();
                                edit(&db, todo_model, quantity.to_owned()).await?;
                                todo_model.quantity = quantity.to_owned();
                            }
                        } else {
                            continue;
                        }
                    } else if command.as_str() == "done" {
                        if let Some(todo_model) = memdb.lock().await.get_mut(&text_buffer) {
                            if todo_model.status == 0 {
                                synching();
                                let updated_model = done(&db, todo_model).await?;
                                *todo_model = updated_model;
                            }
                            continue;
                        } else {
                            continue;
                        }
                    } else if command.as_str() == "undo" {
                        if let Some(todo_model) = memdb.lock().await.get_mut(&text_buffer) {
                            if todo_model.status == 1 {
                                synching();
                                let updated_model = undo(&db, todo_model).await?;
                                *todo_model = updated_model;
                            }
                            continue;
                        } else {
                            continue;
                        }
                    } else {
                        dbg!("Unsupported Command");
                        break;
                    }
                }
            }
        }
    }

    Ok(())
}

The code block above is nested and there are comments to help understanding it. Simply, it:

1. reads the `username`
1. looks up the `username` from the remote PostgreSQL database
1. Loads the local TODO list cache from the local SQLite database
1. Stores the loaded local TODO list cache into `MemDB` in-memory database
1. reads `stdin` for user input into a `buffer`
1. splits the buffer into individual constituents and stores them in an array
1. reads the first index of the array to get the command
1. performs conditional operations on the command and performs the necessary database operations
1. If the command it not available it exits the program
1. If the fruit provided is not available, it clears the buffer and reads `stdin` again
1. if the command is `EXIT` , it syncs the local SQLite cache with the remote PostgreSQL database and exits.

Lastly, import the modules into src/main.rs

File: src/main.rs

  mod common;
  mod db_ops;
+ mod handler;
  mod todo_list_table;
+ mod user_input;
  mod utils;

  pub use common::*;
  pub use db_ops::*;
+ pub use handler::*;
  pub use todo_list_table::prelude::*;
+ pub use user_input::*;
  pub use utils::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    let db = database_config().await?;
    create_todo_table(&db).await?;

+	input_handler(&db).await?;

    Ok(())
}

Running the Client and Server

Running both the todo-server in the TODO-Server directory prints

$ ../target/debug/todo-server
`CREATE TABLE fruits` "Operation Successful"
`CREATE TABLE todos` "Operation Successful"
Listening on 127.0.0.1:8080

Running the todo-client in the current directory prints.

$ Running `target/debug/todo_client`
`CREATE TABLE todo_list` "Operation Successful"
What is Your Username...

Enter a username like user001

This creates a new user in the PostgreSQL database since the user currently does not exist. Querying the PostgreSQL database prints

fruits_market=# SELECT * FROM todos;
 todo_id | username | todo_list 
---------+----------+-----------
       2 | user001  | 
(1 row)

The client then prints a list of fruits, commands and a TODO section:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




Oh My! There are no TODOs
Enter a fruit that is available.

Adding a fruit, like ADD 5kg Apple prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     5kg | Apple     
----------------

----------------
Bummer :( You Have Not Completed Any TODOs!
----------------


Enter a fruit that is available.

A NOT DONE table is added and below that the statement Bummer :( You Have Not Completed Any TODOs! is printed showing that we have TODOs that are not done yet.

Add another fruit like ADD 1kg OraNGe will print:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     5kg | Apple     
     1kg | Orange    
----------------

----------------
Bummer :( You Have Not Completed Any TODOs!
----------------


Enter a fruit that is available.

Here, even though the fruit Orange is typed as OraNGe, it is still added since we handle this in the code using convert_case() function.

Now, edit the orange from 1Kg to 3kg with EDIT 3kg Orange. This prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     5kg | Apple     
     3kg | Orange    
----------------

----------------
Bummer :( You Have Not Completed Any TODOs!
----------------


Enter a fruit that is available.

Next, mark the Apple TODO as done using DONE apple. This prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     3kg | Orange    
----------------

QUANTITY | DONE TODOS
----------------
     5kg | Apple     
----------------

Enter a fruit that is available.

A DONE TODOS table is created with the Apple as a member.

Next, mark the Apple as undone with UNDO Apple. This prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     5kg | Apple     
     3kg | Orange    
----------------

----------------
Bummer :( You Have Not Completed Any TODOs!
----------------


Enter a fruit that is available.


The Apple is moved back to the NOT DONE table and since there are no DONE TODOs, the DONE TODO table is replaced by Bummer :( You Have Not Completed Any TODOs! .

Next, complete all TODOs by marking both the Orange and Apple as done with:

1. `DONE Apple`
1. `DONE orange`

This prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




Wohooo! All TODOs are Completed.
QUANTITY | DONE TODOS
----------------
     5kg | Apple     
     3kg | Orange    
----------------

Enter a fruit that is available.



All TODOs are moved to the DONE TODOS table and the NOT DONE table is replaced by Wohooo! All TODOs are Completed. since all TODOs are done. This proves that our logic works.

Lastly, exit the todo-client gracefully with the command EXIT. This syncs the in-memory database to the remote PostgreSQL server and then exits the program. It prints:

SYNCING TO SERVER...
SYNCED SUCCESSFULLY.
Bye! :)

The state of the SQLite cache is:

sqlite> SELECT * FROM todo_list ;
1|Apple|5kg|1
2|Orange|3kg|1
sqlite> 

The state of the PostgreSQL server is:

fruits_market=# SELECT * FROM todos;
 todo_id | username |                                                                        todo_list                                                                         
---------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------
       2 | user001  | {"queued":[],"completed":[{"todo_id":2,"todo_name":"Orange","quantity":"3kg","status":1},{"todo_id":1,"todo_name":"Apple","quantity":"5kg","status":1}]}
(1 row)

This shows that the TODO list has been successfully synced to remote storage. Running the client again with the same username user001 should print the DONE TODOS from the persisted SQLite cache:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




Wohooo! All TODOs are Completed.
QUANTITY | DONE TODOS
----------------
     5kg | Apple     
     3kg | Orange    
----------------

Enter a fruit that is available.


That's it for this tutorial. :)