feat: fully replace database operations with diesel

image-uploads
Jef Roosens 2025-01-16 15:00:26 +01:00
parent d0ecb9357a
commit c7dc68926b
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
16 changed files with 227 additions and 224 deletions

175
Cargo.lock generated
View File

@ -17,18 +17,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -341,10 +329,8 @@ dependencies = [
"chrono",
"clap",
"diesel",
"r2d2",
"r2d2_sqlite",
"diesel_migrations",
"rand",
"rusqlite",
"serde",
"tera",
"tokio",
@ -597,6 +583,17 @@ dependencies = [
"syn",
]
[[package]]
name = "diesel_migrations"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6"
dependencies = [
"diesel",
"migrations_internals",
"migrations_macros",
]
[[package]]
name = "diesel_table_macro_syntax"
version = "0.2.0"
@ -638,16 +635,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "fallible-iterator"
version = "0.3.0"
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "flate2"
@ -766,21 +757,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.14.5"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown",
]
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "heck"
@ -929,6 +908,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "indexmap"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -975,7 +964,6 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
@ -1008,6 +996,27 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "migrations_internals"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff"
dependencies = [
"serde",
"toml",
]
[[package]]
name = "migrations_macros"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd"
dependencies = [
"migrations_internals",
"proc-macro2",
"quote",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -1284,17 +1293,6 @@ dependencies = [
"scheduled-thread-pool",
]
[[package]]
name = "r2d2_sqlite"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb14dba8247a6a15b7fdbc7d389e2e6f03ee9f184f87117706d509c092dfe846"
dependencies = [
"r2d2",
"rusqlite",
"uuid",
]
[[package]]
name = "rand"
version = "0.8.5"
@ -1363,21 +1361,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rusqlite"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
dependencies = [
"bitflags",
"chrono",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -1462,6 +1445,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1695,6 +1687,40 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower"
version = "0.5.2"
@ -1888,16 +1914,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"rand",
]
[[package]]
name = "valuable"
version = "0.1.0"
@ -2108,6 +2124,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.7.35"

View File

@ -14,12 +14,10 @@ axum-extra = { version = "0.10.0", features = ["cookie"] }
chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.26", features = ["derive", "env"] }
diesel = { version = "2.2.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2", "chrono"] }
r2d2 = "0.8.10"
r2d2_sqlite = "0.25.0"
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
rand = "0.8.5"
# this dependency is needed soly because the r2d2_sqlite crate doesn't export
# the 'chrono' feature flag
rusqlite = { version = "0.32.1", features = ["chrono", "bundled"] }
serde = { version = "1.0.217", features = ["derive"] }
tera = "1.20.0"
tokio = { version = "1.42.0", features = ["full"] }

View File

@ -1,5 +1,5 @@
create table plants (
id bigint primary key not null,
id integer primary key not null,
name text not null,
species text not null,
description text not null

View File

@ -1 +0,0 @@
drop table comments;

View File

@ -1,7 +0,0 @@
create table comments (
id bigint primary key not null,
plant_id bigint not null
references plants (id)
on delete cascade,
comment text not null
);

View File

@ -1,6 +1,6 @@
create table events (
id bigint primary key not null,
plant_id bigint not null
id integer primary key not null,
plant_id integer not null
references plants (id)
on delete cascade,
event_type text not null,

View File

@ -1,5 +1,5 @@
create table users (
id bigint primary key not null,
id integer primary key not null,
username text unique not null,
password_hash text not null,
admin boolean not null
@ -7,7 +7,7 @@ create table users (
create table sessions (
id bigint primary key not null,
user_id bigint not null
user_id integer not null
references users (id)
on delete cascade,
unique (id, user_id)

View File

@ -1,32 +1,37 @@
mod event;
mod models;
mod plant;
mod schema;
mod session;
mod user;
use r2d2_sqlite::{rusqlite, SqliteConnectionManager};
use diesel::{
prelude::*,
r2d2::{ConnectionManager, Pool, PooledConnection},
SqliteConnection,
};
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use std::{error::Error, fmt};
use std::{error::Error, fmt, path::Path};
pub use event::{Event, EventType, NewEvent, EVENT_TYPES};
pub use plant::{NewPlant, Plant};
pub use session::Session;
pub use user::{NewUser, User};
pub use models::event::{Event, EventType, NewEvent, EVENT_TYPES};
pub use models::plant::{NewPlant, Plant};
pub use models::session::Session;
pub use models::user::{NewUser, User};
pub type DbPool = r2d2::Pool<SqliteConnectionManager>;
pub type DbPool = Pool<ConnectionManager<SqliteConnection>>;
pub type DbConn = PooledConnection<ConnectionManager<SqliteConnection>>;
pub type DbResult<T> = Result<T, DbError>;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
#[derive(Debug)]
pub enum DbError {
Pool(r2d2::Error),
Db(rusqlite::Error),
Pool(diesel::r2d2::PoolError),
Db(diesel::result::Error),
}
impl fmt::Display for DbError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Pool(_) => write!(f, "failed to acquire connection from pool"),
Self::Db(_) => write!(f, "error while accessing the database"),
Self::Db(_) => write!(f, "error while executing query"),
}
}
}
@ -40,46 +45,25 @@ impl Error for DbError {
}
}
impl From<r2d2::Error> for DbError {
fn from(value: r2d2::Error) -> Self {
impl From<diesel::r2d2::PoolError> for DbError {
fn from(value: diesel::r2d2::PoolError) -> Self {
Self::Pool(value)
}
}
impl From<rusqlite::Error> for DbError {
fn from(value: rusqlite::Error) -> Self {
impl From<diesel::result::Error> for DbError {
fn from(value: diesel::result::Error) -> Self {
Self::Db(value)
}
}
pub fn run_migrations(pool: &DbPool, migrations: &[&str]) -> rusqlite::Result<()> {
let mut conn = pool.get().unwrap();
pub fn initialize_db(path: impl AsRef<Path>, run_migrations: bool) -> Result<DbPool, DbError> {
let manager = ConnectionManager::<SqliteConnection>::new(path.as_ref().to_string_lossy());
let pool = Pool::new(manager)?;
// If the migration version query fails, we assume it's because the table isn't there yet so we
// try to run the first migration
let mut next_version = conn
.query_row("select max(version) from migration_version", (), |row| {
row.get::<_, usize>(0).map(|n| n + 1)
})
.unwrap_or(0);
while next_version < migrations.len() {
let tx = conn.transaction()?;
tx.execute_batch(migrations[next_version])?;
let cur_time = chrono::Local::now().timestamp();
tx.execute(
"insert into migration_version values ($1, $2)",
(next_version, cur_time),
)?;
tx.commit()?;
tracing::info!("Applied migration {next_version}");
next_version += 1;
if run_migrations {
pool.get()?.run_pending_migrations(MIGRATIONS).unwrap();
}
Ok(())
Ok(pool)
}

View File

@ -11,7 +11,9 @@ use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};
use crate::db::schema::*;
pub const EVENT_TYPES: [&str; 1] = ["Watering"];
use crate::db::{schema::*, DbPool, DbResult};
#[derive(FromSqlRow, Debug, AsExpression, Serialize, Deserialize)]
#[diesel(sql_type = Text)]
@ -72,10 +74,11 @@ impl ToSql<Text, Sqlite> for EventType {
#[derive(Serialize, Queryable, Selectable)]
#[diesel(table_name = events)]
#[diesel(belongs_to(super::plant::Plant))]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Event {
id: i64,
plant_id: i64,
id: i32,
plant_id: i32,
event_type: EventType,
date: NaiveDate,
description: String,
@ -85,17 +88,26 @@ pub struct Event {
#[diesel(table_name = events)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NewEvent {
plant_id: i64,
plant_id: i32,
event_type: EventType,
date: NaiveDate,
description: String,
}
impl NewEvent {
pub fn insert(self, conn: &mut SqliteConnection) -> QueryResult<Event> {
diesel::insert_into(events::table)
.values(self)
.returning(Event::as_returning())
.get_result(conn)
impl Event {
pub fn for_plant(pool: &DbPool, plant_id: i32) -> DbResult<Vec<Self>> {
Ok(events::dsl::events
.select(Self::as_select())
.filter(events::dsl::plant_id.eq(plant_id))
.load(&mut pool.get()?)?)
}
}
impl NewEvent {
pub fn insert(self, pool: &DbPool) -> DbResult<Event> {
Ok(diesel::insert_into(events::table)
.values(self)
.returning(Event::as_returning())
.get_result(&mut pool.get()?)?)
}
}

View File

@ -1,4 +1,4 @@
mod event;
mod plant;
mod session;
mod user;
pub mod event;
pub mod plant;
pub mod session;
pub mod user;

View File

@ -1,16 +1,16 @@
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use crate::db::schema::*;
use crate::db::{schema::*, DbPool, DbResult};
#[derive(Serialize, Queryable, Selectable)]
#[diesel(table_name = plants)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Plant {
id: i64,
name: String,
species: String,
description: String,
pub id: i32,
pub name: String,
pub species: String,
pub description: String,
}
#[derive(Deserialize, Insertable)]
@ -23,19 +23,26 @@ pub struct NewPlant {
}
impl NewPlant {
pub fn insert(self, conn: &mut SqliteConnection) -> QueryResult<Plant> {
self.insert_into(plants::table)
pub fn insert(self, pool: &DbPool) -> DbResult<Plant> {
Ok(self
.insert_into(plants::table)
.returning(Plant::as_returning())
.get_result(conn)
.get_result(&mut pool.get()?)?)
}
}
impl Plant {
pub fn by_id(conn: &mut SqliteConnection, id: i64) -> QueryResult<Option<Self>> {
plants::table
pub fn all(pool: &DbPool) -> DbResult<Vec<Self>> {
Ok(plants::dsl::plants
.select(Self::as_select())
.load(&mut pool.get()?)?)
}
pub fn by_id(pool: &DbPool, id: i32) -> DbResult<Option<Self>> {
Ok(plants::table
.find(id)
.select(Self::as_select())
.first(conn)
.optional()
.first(&mut pool.get()?)
.optional()?)
}
}

View File

@ -2,7 +2,7 @@ use diesel::prelude::*;
use rand::Rng;
use super::user::User;
use crate::db::schema::*;
use crate::db::{schema::*, DbPool, DbResult};
#[derive(Clone, Queryable, Selectable, Insertable, Associations)]
#[diesel(belongs_to(super::user::User))]
@ -10,28 +10,25 @@ use crate::db::schema::*;
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Session {
pub id: i64,
pub user_id: i64,
pub user_id: i32,
}
impl Session {
pub fn new_for_user(conn: &mut SqliteConnection, user_id: i64) -> QueryResult<Self> {
pub fn new_for_user(pool: &DbPool, user_id: i32) -> DbResult<Self> {
let id: i64 = rand::thread_rng().gen();
Self { id, user_id }
Ok(Self { id, user_id }
.insert_into(sessions::table)
.returning(Self::as_returning())
.get_result(conn)
.get_result(&mut pool.get()?)?)
}
pub fn user_from_id(
conn: &mut SqliteConnection,
id: i64,
) -> QueryResult<Option<super::user::User>> {
sessions::dsl::sessions
pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult<Option<super::user::User>> {
Ok(sessions::dsl::sessions
.inner_join(users::table)
.filter(sessions::id.eq(id))
.select(User::as_select())
.get_result(conn)
.optional()
.get_result(&mut pool.get()?)
.optional()?)
}
}

View File

@ -5,13 +5,13 @@ use argon2::{
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use crate::db::schema::*;
use crate::db::{schema::*, DbConn, DbPool, DbResult};
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct User {
pub id: i64,
pub id: i32,
pub username: String,
pub password_hash: String,
pub admin: bool,
@ -45,24 +45,27 @@ impl NewUser {
}
}
pub fn insert(self, conn: &mut SqliteConnection) -> QueryResult<User> {
diesel::insert_into(users::table)
pub fn insert(self, pool: &DbPool) -> DbResult<User> {
Ok(diesel::insert_into(users::table)
.values(self)
.returning(User::as_returning())
.get_result(conn)
.get_result(&mut pool.get()?)?)
}
}
impl User {
pub fn by_username(
conn: &mut SqliteConnection,
username: impl AsRef<str>,
) -> QueryResult<Option<Self>> {
users::dsl::users
pub fn by_username(pool: &DbPool, username: impl AsRef<str>) -> DbResult<Option<Self>> {
Ok(users::dsl::users
.select(User::as_select())
.filter(users::username.eq(username.as_ref()))
.first(conn)
.optional()
.first(&mut pool.get()?)
.optional()?)
}
pub fn all(pool: &DbPool) -> DbResult<Vec<Self>> {
Ok(users::dsl::users
.select(User::as_select())
.load(&mut pool.get()?)?)
}
pub fn verify_password(&self, password: impl AsRef<str>) -> bool {
@ -72,4 +75,11 @@ impl User {
.verify_password(password.as_ref().as_bytes(), &password_hash)
.is_ok()
}
pub fn remove_by_username(pool: &DbPool, username: impl AsRef<str>) -> DbResult<bool> {
Ok(diesel::delete(users::table)
.filter(users::username.eq(username.as_ref()))
.execute(&mut pool.get()?)
.map(|n| n > 0)?)
}
}

View File

@ -1,17 +1,9 @@
// @generated automatically by Diesel CLI.
diesel::table! {
comments (id) {
id -> BigInt,
plant_id -> BigInt,
comment -> Text,
}
}
diesel::table! {
events (id) {
id -> BigInt,
plant_id -> BigInt,
id -> Integer,
plant_id -> Integer,
event_type -> Text,
date -> Date,
description -> Text,
@ -20,7 +12,7 @@ diesel::table! {
diesel::table! {
plants (id) {
id -> BigInt,
id -> Integer,
name -> Text,
species -> Text,
description -> Text,
@ -30,21 +22,20 @@ diesel::table! {
diesel::table! {
sessions (id) {
id -> BigInt,
user_id -> BigInt,
user_id -> Integer,
}
}
diesel::table! {
users (id) {
id -> BigInt,
id -> Integer,
username -> Text,
password_hash -> Text,
admin -> Bool,
}
}
diesel::joinable!(comments -> plants (plant_id));
diesel::joinable!(events -> plants (plant_id));
diesel::joinable!(sessions -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(comments, events, plants, sessions, users,);
diesel::allow_tables_to_appear_in_same_query!(events, plants, sessions, users,);

View File

@ -5,18 +5,13 @@ mod server;
use std::{fs, path::Path, sync::Arc};
use clap::Parser;
use cli::UserCmd;
use db::DbError;
use r2d2_sqlite::SqliteConnectionManager;
use diesel_migrations::{embed_migrations, EmbeddedMigrations};
use tera::Tera;
use tower_http::compression::CompressionLayer;
const MIGRATIONS: [&str; 4] = [
include_str!("migrations/000_initial.sql"),
include_str!("migrations/001_plants.sql"),
include_str!("migrations/003_events.sql"),
include_str!("migrations/004_auth.sql"),
];
use cli::UserCmd;
use db::DbError;
const DB_FILENAME: &str = "db.sqlite3";
#[derive(Clone)]
@ -26,8 +21,7 @@ pub struct Context {
}
fn run_user_cli(data_dir: impl AsRef<Path>, cmd: UserCmd) -> Result<(), DbError> {
let manager = SqliteConnectionManager::file(data_dir.as_ref().join(DB_FILENAME));
let pool = r2d2::Pool::new(manager)?;
let pool = db::initialize_db(data_dir.as_ref().join(DB_FILENAME), false)?;
match cmd.cmd {
cli::UserSubCmd::List => {
@ -44,12 +38,7 @@ fn run_user_cli(data_dir: impl AsRef<Path>, cmd: UserCmd) -> Result<(), DbError>
password,
admin,
} => {
db::NewUser {
username,
password,
admin,
}
.insert(&pool)?;
db::NewUser::new(username, password, admin).insert(&pool)?;
}
cli::UserSubCmd::Remove { username } => {
db::User::remove_by_username(&pool, &username)?;
@ -71,9 +60,7 @@ async fn main() {
fs::create_dir_all(&args.data_dir).unwrap();
}
let manager = SqliteConnectionManager::file(args.data_dir.join(DB_FILENAME));
let pool = r2d2::Pool::new(manager).unwrap();
db::run_migrations(&pool, &MIGRATIONS).unwrap();
let pool = db::initialize_db(args.data_dir.join(DB_FILENAME), true).unwrap();
let tera =
Tera::new(&format!("{}/**/*", args.templates_dir.to_string_lossy())).unwrap();

View File

@ -7,7 +7,7 @@ use axum::{
};
use tera::Context;
use crate::db::{self, DbError, Plant};
use crate::db::{self, DbError, Event, Plant};
use super::error::AppError;
@ -20,13 +20,13 @@ pub fn app() -> axum::Router<crate::Context> {
async fn get_plant_page(
State(ctx): State<crate::Context>,
headers: HeaderMap,
Path(plant_id): Path<i64>,
Path(plant_id): Path<i32>,
) -> super::Result<Html<String>> {
let res = tokio::task::spawn_blocking(move || {
let plant = Plant::by_id(&ctx.pool, plant_id)?;
if let Some(plant) = plant {
let events = plant.events(&ctx.pool)?;
let events = Event::for_plant(&ctx.pool, plant.id)?;
Ok::<_, DbError>(Some((plant, events)))
} else {