Minimalistic todo app powered by SeaORM-based connection to Postgres. Seaorm is async-first, dynamic and includes powerful tools for testing. Also it supports Seaography - library that can automatically build graphql endpoints from seaorm entities. Overall SeaQL provides pretty much everything necessary to work with postgres, mysql and sqlite, and currently it is the main competitor of diesel.
To work with it you'll need the sea-orm-cli, running postgres instance and a connection string defined in .env
or environment variables. Usually entities are generated using the cli from the database schema - you write migrations, run them, invoke cli and get the entities. A command to do that:
(cd examples/databases/postgres-seaorm
)
sea-orm-cli generate entity -u postgres://postgres:password@localhost/prest -o ./entities --with-serde both
Source code of the example:
Cargo.toml
[package]
name = "postgres-seaorm"
edition = "2021"
[[bin]]
name = "serve"
path = "./serve.rs"
[dependencies]
prest = { path = "../../../", version = "0.4" }
sea-orm = { version = "0.12", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros", "with-uuid"] }
sea-orm-migration = "0.12"
migrator.rs
use sea_orm_migration::prelude::*;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(MigrationCreateTodos)]
}
}
struct MigrationCreateTodos;
impl MigrationName for MigrationCreateTodos {
fn name(&self) -> &str {
"m_20231106_000001_create_todos_table"
}
}
#[async_trait::async_trait]
impl MigrationTrait for MigrationCreateTodos {
// Define how to apply this migration: Create the Bakery table.
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Todos::Table)
.col(ColumnDef::new(Todos::Uuid).uuid().not_null().primary_key())
.col(ColumnDef::new(Todos::Task).string().not_null())
.col(ColumnDef::new(Todos::Done).boolean().not_null())
.to_owned(),
)
.await
}
// Define how to rollback this migration: Drop the Bakery table.
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Todos::Table).to_owned())
.await
}
}
#[derive(Iden)]
pub enum Todos {
Table,
Uuid,
Task,
Done,
}
serve.rs
mod entities;
mod migrator;
use prest::*;
use entities::{prelude::*, *};
use sea_orm::{ActiveModelTrait, ActiveValue, Database, DatabaseConnection, EntityTrait};
use sea_orm_migration::migrator::MigratorTrait;
state!(DB: DatabaseConnection = async {
let db = Database::connect("postgres://postgres:password@localhost/prest").await?;
migrator::Migrator::refresh(&db).await?;
db
});
#[derive(Deserialize)]
struct NewTodo {
task: String,
}
#[derive(Deserialize)]
struct ToggleTodo {
uuid: Uuid,
done: bool,
}
#[derive(Deserialize)]
struct DeleteTodo {
uuid: Uuid,
}
fn main() {
route(
"/",
get(|| async { html!(@for todo in Todos::find().all(&*DB).await.unwrap() {(todo)}) })
.put(|Vals(NewTodo { task }): Vals<NewTodo>| async move {
todos::ActiveModel {
uuid: ActiveValue::Set(Uuid::now_v7()),
task: ActiveValue::Set(task),
done: ActiveValue::Set(false),
}
.insert(&*DB)
.await
.unwrap();
Redirect::to("/")
})
.patch(
|Vals(ToggleTodo { uuid, done }): Vals<ToggleTodo>| async move {
todos::ActiveModel {
uuid: ActiveValue::Set(uuid),
done: ActiveValue::Set(!done),
..Default::default()
}
.update(&*DB)
.await
.unwrap();
Redirect::to("/")
},
)
.delete(|Vals(DeleteTodo { uuid }): Vals<DeleteTodo>| async move {
todos::ActiveModel {
uuid: ActiveValue::Set(uuid),
..Default::default()
}
.delete(&*DB)
.await
.unwrap();
Redirect::to("/")
}),
)
.wrap_non_htmx(page)
.run()
}
impl Render for todos::Model {
fn render(&self) -> Markup {
html!(
$"flex items-center" vals=(json!(self)) {
input type="checkbox" patch="/" checked[self.done] {}
label $"ml-4 text-lg" {(self.task)}
button $"ml-auto" detele="/" {"Delete"}
}
)
}
}
async fn page(content: Markup) -> Markup {
html! { html data-theme="dark" {
(Head::with_title("With SeaORM Postgres"))
body."max-w-screen-sm mx-auto mt-12" into="div" {
form $"flex gap-4 justify-center" put="/" into="#list" swap-beforeend after-request="this.reset()" {
input $"border rounded-md" type="text" name="task" {}
button type="submit" {"Add"}
}
div #"list" $"w-full" {(content)}
(Scripts::default())
}
}}
}