Modify todo example to use postgres and diesel_async

This commit is contained in:
ThouCheese 2023-11-21 15:44:31 +01:00
parent 89a2af179b
commit 6a6b53c466
6 changed files with 73 additions and 62 deletions

View File

@ -7,16 +7,16 @@ publish = false
[dependencies]
rocket = { path = "../../core/lib" }
diesel = { version = "2.0.0", features = ["sqlite", "r2d2"] }
diesel = { version = "2.0.0", features = ["postgres", "r2d2"] }
diesel_migrations = "2.0.0"
[dev-dependencies]
parking_lot = "0.12"
rand = "0.8"
[dependencies.rocket_sync_db_pools]
path = "../../contrib/sync_db_pools/lib/"
features = ["diesel_sqlite_pool"]
[dependencies.rocket_db_pools]
path = "../../contrib/db_pools/lib/"
features = ["diesel_postgres"]
[dependencies.rocket_dyn_templates]
path = "../../contrib/dyn_templates"

View File

@ -1,5 +1,7 @@
[default]
template_dir = "static"
[default.databases.sqlite_database]
url = "db/db.sqlite"
[default.databases.epic_todo_database]
url = "postgresql://postgres@localhost:5432/epic_todo_database"
max_connections = 1
connect_timeout = 5

View File

@ -0,0 +1 @@
db does not live here :(

View File

@ -1,6 +1,4 @@
#[macro_use] extern crate rocket;
#[macro_use] extern crate rocket_sync_db_pools;
#[macro_use] extern crate diesel;
#[cfg(test)]
mod tests;
@ -13,13 +11,15 @@ use rocket::response::{Flash, Redirect};
use rocket::serde::Serialize;
use rocket::form::Form;
use rocket::fs::{FileServer, relative};
use rocket_db_pools::{Connection, Database};
use rocket_dyn_templates::Template;
use crate::task::{Task, Todo};
#[database("sqlite_database")]
pub struct DbConn(diesel::SqliteConnection);
#[derive(Database)]
#[database("epic_todo_database")]
pub struct Db(rocket_db_pools::diesel::PgPool);
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
@ -29,14 +29,14 @@ struct Context {
}
impl Context {
pub async fn err<M: std::fmt::Display>(conn: &DbConn, msg: M) -> Context {
pub async fn err<M: std::fmt::Display>(conn: &mut Connection<Db>, msg: M) -> Context {
Context {
flash: Some(("error".into(), msg.to_string())),
tasks: Task::all(conn).await.unwrap_or_default()
}
}
pub async fn raw(conn: &DbConn, flash: Option<(String, String)>) -> Context {
pub async fn raw(conn: &mut Connection<Db>, flash: Option<(String, String)>) -> Context {
match Task::all(conn).await {
Ok(tasks) => Context { flash, tasks },
Err(e) => {
@ -51,11 +51,11 @@ impl Context {
}
#[post("/", data = "<todo_form>")]
async fn new(todo_form: Form<Todo>, conn: DbConn) -> Flash<Redirect> {
async fn new(todo_form: Form<Todo>, mut conn: Connection<Db>) -> Flash<Redirect> {
let todo = todo_form.into_inner();
if todo.description.is_empty() {
Flash::error(Redirect::to("/"), "Description cannot be empty.")
} else if let Err(e) = Task::insert(todo, &conn).await {
} else if let Err(e) = Task::insert(todo, &mut conn).await {
error_!("DB insertion error: {}", e);
Flash::error(Redirect::to("/"), "Todo could not be inserted due an internal error.")
} else {
@ -64,42 +64,50 @@ async fn new(todo_form: Form<Todo>, conn: DbConn) -> Flash<Redirect> {
}
#[put("/<id>")]
async fn toggle(id: i32, conn: DbConn) -> Result<Redirect, Template> {
match Task::toggle_with_id(id, &conn).await {
async fn toggle(id: i32, mut conn: Connection<Db>) -> Result<Redirect, Template> {
match Task::toggle_with_id(id, &mut conn).await {
Ok(_) => Ok(Redirect::to("/")),
Err(e) => {
error_!("DB toggle({}) error: {}", id, e);
Err(Template::render("index", Context::err(&conn, "Failed to toggle task.").await))
Err(Template::render("index", Context::err(&mut conn, "Failed to toggle task.").await))
}
}
}
#[delete("/<id>")]
async fn delete(id: i32, conn: DbConn) -> Result<Flash<Redirect>, Template> {
match Task::delete_with_id(id, &conn).await {
async fn delete(id: i32, mut conn: Connection<Db>) -> Result<Flash<Redirect>, Template> {
match Task::delete_with_id(id, &mut conn).await {
Ok(_) => Ok(Flash::success(Redirect::to("/"), "Todo was deleted.")),
Err(e) => {
error_!("DB deletion({}) error: {}", id, e);
Err(Template::render("index", Context::err(&conn, "Failed to delete task.").await))
Err(Template::render("index", Context::err(&mut conn, "Failed to delete task.").await))
}
}
}
#[get("/")]
async fn index(flash: Option<FlashMessage<'_>>, conn: DbConn) -> Template {
async fn index(flash: Option<FlashMessage<'_>>, mut conn: Connection<Db>) -> Template {
let flash = flash.map(FlashMessage::into_inner);
Template::render("index", Context::raw(&conn, flash).await)
Template::render("index", Context::raw(&mut conn, flash).await)
}
async fn run_migrations(rocket: Rocket<Build>) -> Rocket<Build> {
use diesel::Connection;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
let config: rocket_db_pools::Config = rocket
.figment()
.extract_inner("databases.epic_todo_database")
.expect("Db not configured");
DbConn::get_one(&rocket).await
.expect("database connection")
.run(|conn| { conn.run_pending_migrations(MIGRATIONS).expect("diesel migrations"); })
.await;
rocket::tokio::task::spawn_blocking(move || {
diesel::PgConnection::establish(&config.url)
.expect("No database")
.run_pending_migrations(MIGRATIONS)
.expect("Invalid migrations");
})
.await.expect("tokio doesn't work");
rocket
}
@ -107,7 +115,7 @@ async fn run_migrations(rocket: Rocket<Build>) -> Rocket<Build> {
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(DbConn::fairing())
.attach(Db::init())
.attach(Template::fairing())
.attach(AdHoc::on_ignite("Run Migrations", run_migrations))
.mount("/", FileServer::from(relative!("static")))

View File

@ -1,8 +1,9 @@
use rocket::serde::Serialize;
use diesel::{self, result::QueryResult, prelude::*};
use rocket_db_pools::diesel::RunQueryDsl;
mod schema {
table! {
diesel::table! {
tasks {
id -> Nullable<Integer>,
description -> Text,
@ -13,7 +14,7 @@ mod schema {
use self::schema::tasks;
use crate::DbConn;
type DbConn = rocket_db_pools::diesel::AsyncPgConnection;
#[derive(Serialize, Queryable, Insertable, Debug, Clone)]
#[serde(crate = "rocket::serde")]
@ -31,41 +32,35 @@ pub struct Todo {
}
impl Task {
pub async fn all(conn: &DbConn) -> QueryResult<Vec<Task>> {
conn.run(|c| {
tasks::table.order(tasks::id.desc()).load::<Task>(c)
}).await
pub async fn all(conn: &mut DbConn) -> QueryResult<Vec<Task>> {
tasks::table.order(tasks::id.desc()).load::<Task>(conn).await
}
/// Returns the number of affected rows: 1.
pub async fn insert(todo: Todo, conn: &DbConn) -> QueryResult<usize> {
conn.run(|c| {
let t = Task { id: None, description: todo.description, completed: false };
diesel::insert_into(tasks::table).values(&t).execute(c)
}).await
pub async fn insert(todo: Todo, conn: &mut DbConn) -> QueryResult<usize> {
let t = Task { id: None, description: todo.description, completed: false };
diesel::insert_into(tasks::table).values(&t).execute(conn).await
}
/// Returns the number of affected rows: 1.
pub async fn toggle_with_id(id: i32, conn: &DbConn) -> QueryResult<usize> {
conn.run(move |c| {
let task = tasks::table.filter(tasks::id.eq(id)).get_result::<Task>(c)?;
let new_status = !task.completed;
let updated_task = diesel::update(tasks::table.filter(tasks::id.eq(id)));
updated_task.set(tasks::completed.eq(new_status)).execute(c)
}).await
pub async fn toggle_with_id(id: i32, conn: &mut DbConn) -> QueryResult<usize> {
let task = tasks::table.filter(tasks::id.eq(id)).get_result::<Task>(conn).await?;
let new_status = !task.completed;
let updated_task = diesel::update(tasks::table.filter(tasks::id.eq(id)));
updated_task.set(tasks::completed.eq(new_status)).execute(conn).await
}
/// Returns the number of affected rows: 1.
pub async fn delete_with_id(id: i32, conn: &DbConn) -> QueryResult<usize> {
conn.run(move |c| diesel::delete(tasks::table)
pub async fn delete_with_id(id: i32, conn: &mut DbConn) -> QueryResult<usize> {
diesel::delete(tasks::table)
.filter(tasks::id.eq(id))
.execute(c))
.execute(conn)
.await
}
/// Returns the number of affected rows.
#[cfg(test)]
pub async fn delete_all(conn: &DbConn) -> QueryResult<usize> {
conn.run(|c| diesel::delete(tasks::table).execute(c)).await
pub async fn delete_all(conn: &mut DbConn) -> QueryResult<usize> {
diesel::delete(tasks::table).execute(conn).await
}
}

View File

@ -4,6 +4,7 @@ use rand::{Rng, thread_rng, distributions::Alphanumeric};
use rocket::local::asynchronous::Client;
use rocket::http::{Status, ContentType};
use rocket_db_pools::Database;
// We use a lock to synchronize between tests so DB operations don't collide.
// For now. In the future, we'll have a nice way to run each test in a DB
@ -15,10 +16,14 @@ macro_rules! run_test {
let _lock = DB_LOCK.lock();
rocket::async_test(async move {
let $client = Client::tracked(super::rocket()).await.expect("Rocket client");
let db = super::DbConn::get_one($client.rocket()).await;
let $conn = db.expect("failed to get database connection for testing");
Task::delete_all(&$conn).await.expect("failed to delete all tasks for testing");
let rocket = super::rocket();
let mut $conn = super::Db::fetch(&rocket)
.expect("database")
.get()
.await
.expect("database connection");
let $client = Client::tracked(rocket).await.expect("Rocket client");
Task::delete_all(&mut $conn).await.expect("failed to delete all tasks for testing");
$block
})
@ -39,7 +44,7 @@ fn test_index() {
fn test_insertion_deletion() {
run_test!(|client, conn| {
// Get the tasks before making changes.
let init_tasks = Task::all(&conn).await.unwrap();
let init_tasks = Task::all(&mut conn).await.unwrap();
// Issue a request to insert a new task.
client.post("/todo")
@ -49,7 +54,7 @@ fn test_insertion_deletion() {
.await;
// Ensure we have one more task in the database.
let new_tasks = Task::all(&conn).await.unwrap();
let new_tasks = Task::all(&mut conn).await.unwrap();
assert_eq!(new_tasks.len(), init_tasks.len() + 1);
// Ensure the task is what we expect.
@ -61,7 +66,7 @@ fn test_insertion_deletion() {
client.delete(format!("/todo/{}", id)).dispatch().await;
// Ensure it's gone.
let final_tasks = Task::all(&conn).await.unwrap();
let final_tasks = Task::all(&mut conn).await.unwrap();
assert_eq!(final_tasks.len(), init_tasks.len());
if final_tasks.len() > 0 {
assert_ne!(final_tasks[0].description, "My first task");
@ -79,16 +84,16 @@ fn test_toggle() {
.dispatch()
.await;
let task = Task::all(&conn).await.unwrap()[0].clone();
let task = Task::all(&mut conn).await.unwrap()[0].clone();
assert_eq!(task.completed, false);
// Issue a request to toggle the task; ensure it is completed.
client.put(format!("/todo/{}", task.id.unwrap())).dispatch().await;
assert_eq!(Task::all(&conn).await.unwrap()[0].completed, true);
assert_eq!(Task::all(&mut conn).await.unwrap()[0].completed, true);
// Issue a request to toggle the task; ensure it's not completed again.
client.put(format!("/todo/{}", task.id.unwrap())).dispatch().await;
assert_eq!(Task::all(&conn).await.unwrap()[0].completed, false);
assert_eq!(Task::all(&mut conn).await.unwrap()[0].completed, false);
})
}
@ -98,7 +103,7 @@ fn test_many_insertions() {
run_test!(|client, conn| {
// Get the number of tasks initially.
let init_num = Task::all(&conn).await.unwrap().len();
let init_num = Task::all(&mut conn).await.unwrap().len();
let mut descs = Vec::new();
for i in 0..ITER {
@ -119,7 +124,7 @@ fn test_many_insertions() {
descs.insert(0, desc);
// Ensure the task was inserted properly and all other tasks remain.
let tasks = Task::all(&conn).await.unwrap();
let tasks = Task::all(&mut conn).await.unwrap();
assert_eq!(tasks.len(), init_num + i + 1);
for j in 0..i {