Simple TODO list with Rocket and SurrealDB
- TODO: why is the item.id.id shown as [object] in the web page while printing to the log shows the ID only.
Setup server:
docker volume create my-surreal-db docker run --detach --restart always --name surrealdb -p 127.0.0.1:8000:8000 --user root -v my-surreal-db:/database surrealdb/surrealdb:v2.0.1 start --user root --pass root --log trace file://database
Dependencies
examples/rocket/simple-todo-with-surrealdb/Cargo.toml
[package] name = "todo" version = "0.1.0" edition = "2021" [dependencies] rocket = { version = "0.5", features = ["secrets", "uuid"] } rocket_dyn_templates = { version = "0.1", features = ["tera"] } surrealdb = "2.0" tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } chrono = "0.4.38" serde = { version = "1.0", features = ["derive"] }
examples/rocket/simple-todo-with-surrealdb/Rocket.toml
[debug] port=8001
examples/rocket/simple-todo-with-surrealdb/src/main.rs
#[macro_use] extern crate rocket; use rocket::form::Form; use rocket::State; use rocket_dyn_templates::{context, Template}; use surrealdb::engine::remote::ws::Client; use surrealdb::Surreal; pub mod db; use crate::db::Item; #[derive(FromForm)] struct AddForm<'r> { title: &'r str, } #[derive(FromForm)] struct UpdateForm<'r> { id: &'r str, title: &'r str, text: &'r str, } async fn form_and_list(dbh: &State<Surreal<Client>>) -> Template { let items: Vec<Item> = db::get_items(dbh).await.unwrap(); for item in &items { rocket::info!("{} {}", item.id.id, item.title); } let pairs = items .iter() .map(|item| { let id = item.id.id.to_string(); (id, item) }) .collect::<Vec<_>>(); Template::render( "index", context! { title: "TODO", items: pairs, }, ) } #[get("/")] async fn get_index(dbh: &State<Surreal<Client>>) -> Template { form_and_list(dbh).await } #[get("/clear")] async fn clear_db(dbh: &State<Surreal<Client>>) -> Template { db::clear(dbh).await.unwrap(); form_and_list(dbh).await } #[post("/", data = "<input>")] async fn post_index(dbh: &State<Surreal<Client>>, input: Form<AddForm<'_>>) -> Template { rocket::info!("Add '{}'", input.title); let title = input.title.trim(); db::add_item(dbh, title).await.unwrap(); form_and_list(dbh).await } #[get("/item/<id>")] async fn get_item(dbh: &State<Surreal<Client>>, id: String) -> Option<Template> { if let Some(item) = db::get_item(dbh, &id).await.unwrap() { return Some(Template::render( "item", context! { title: item.title.clone(), id: item.id.clone().id.to_string(), item: item, }, )); } None } #[post("/update", data = "<input>")] async fn update_item(dbh: &State<Surreal<Client>>, input: Form<UpdateForm<'_>>) -> Template { rocket::info!("Update '{}'", input.id); let id = input.id.trim(); let title = input.title.trim(); let text = input.text.trim(); db::update_item(dbh, id, title, text).await.unwrap(); form_and_list(dbh).await } #[launch] fn rocket() -> _ { rocket::build() .mount( "/", routes![clear_db, get_index, post_index, get_item, update_item], ) .attach(Template::fairing()) .attach(db::fairing()) }
examples/rocket/simple-todo-with-surrealdb/src/db.rs
use chrono::{DateTime, Utc}; use rocket::fairing::AdHoc; use serde::{Deserialize, Serialize}; use surrealdb::engine::remote::ws::Client; use surrealdb::engine::remote::ws::Ws; use surrealdb::opt::auth::Root; use surrealdb::opt::Resource; use surrealdb::sql::{Id, Thing}; use surrealdb::Surreal; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct Item { pub id: Thing, pub title: String, pub text: Option<String>, pub date: DateTime<Utc>, } /// # Panics /// /// Panics when it fails to connect to the database #[must_use] pub fn fairing() -> AdHoc { AdHoc::on_ignite("Managed Database Connection", |rocket| async { let address = "127.0.0.1:8000"; let username = "root"; let password = "root"; let db_namespace = "todo"; let db_database = "todo"; let dbh = Surreal::new::<Ws>(address).await.unwrap(); dbh.signin(Root { username, password }).await.unwrap(); dbh.use_ns(db_namespace).use_db(db_database).await.unwrap(); rocket.manage(dbh) }) } pub async fn clear(dbh: &Surreal<Client>) -> surrealdb::Result<()> { dbh.query("DELETE FROM items;").await?; Ok(()) } pub async fn add_item(dbh: &Surreal<Client>, title: &str) -> surrealdb::Result<()> { let utc: DateTime<Utc> = Utc::now(); let id = Id::ulid(); let entry = Item { id: Thing::from(("items", id)), title: title.to_owned(), text: None, date: utc, }; dbh.create(Resource::from("items")).content(entry).await?; Ok(()) } pub async fn update_item( dbh: &Surreal<Client>, id: &str, title: &str, text: &str, ) -> surrealdb::Result<()> { rocket::info!("Update '{}' '{}'", id, title); #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] struct UpdateItem { title: String, text: String, } let _item: Option<Item> = dbh .update(("items", id)) .merge(UpdateItem { title: title.to_owned(), text: text.to_owned(), }) .await?; Ok(()) } pub async fn get_items(dbh: &Surreal<Client>) -> surrealdb::Result<Vec<Item>> { let mut response = dbh.query("SELECT * FROM items;").await?; let entries: Vec<Item> = response.take(0)?; Ok(entries) } pub async fn get_item(dbh: &Surreal<Client>, id: &str) -> surrealdb::Result<Option<Item>> { dbh.select(("items", id)).await }
examples/rocket/simple-todo-with-surrealdb/templates/index.html.tera
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{title}}</title> </head> <body> <h1><a href="/">{{title}}</a></h1> <form method="POST" action="/"> <input name="title"><br> <input type="submit" value="Add"><br> </form> <ul> {% for item in items %} <li><a href="/item/{{item.0}}">{{item.1.title}}</a></li> {% endfor %} </ul> <hr> <a href="/clear">remove all the items</a> </body> </html>
examples/rocket/simple-todo-with-surrealdb/templates/item.html.tera
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{title}}</title> </head> <body> <h1><a href="/">home</a></h1> <h2>Title: {{item.title}}</h2> <div>Date: {{item.date}}</div> <form method="POST" action="/update"> <input type="hidden" name="id" value="{{id}}"> <input name="title" value="{{item.title}}"><br> <textarea name="text">{{item.text}}</textarea><br> <input type="submit" value="Update"><br> </form> </body> </html>