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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -341,10 +329,8 @@ dependencies = [
"chrono", "chrono",
"clap", "clap",
"diesel", "diesel",
"r2d2", "diesel_migrations",
"r2d2_sqlite",
"rand", "rand",
"rusqlite",
"serde", "serde",
"tera", "tera",
"tokio", "tokio",
@ -597,6 +583,17 @@ dependencies = [
"syn", "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]] [[package]]
name = "diesel_table_macro_syntax" name = "diesel_table_macro_syntax"
version = "0.2.0" version = "0.2.0"
@ -638,16 +635,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]] [[package]]
name = "fallible-iterator" name = "equivalent"
version = "0.3.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]] [[package]]
name = "flate2" name = "flate2"
@ -766,21 +757,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown",
]
[[package]] [[package]]
name = "heck" name = "heck"
@ -929,6 +908,16 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -975,7 +964,6 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [ dependencies = [
"cc",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
@ -1008,6 +996,27 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 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]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -1284,17 +1293,6 @@ dependencies = [
"scheduled-thread-pool", "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]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1363,21 +1361,6 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 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]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"
@ -1462,6 +1445,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -1695,6 +1687,40 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@ -1888,16 +1914,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 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]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"
@ -2108,6 +2124,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" 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"] } chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.26", features = ["derive", "env"] } clap = { version = "4.5.26", features = ["derive", "env"] }
diesel = { version = "2.2.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2", "chrono"] } diesel = { version = "2.2.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2", "chrono"] }
r2d2 = "0.8.10" diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
r2d2_sqlite = "0.25.0"
rand = "0.8.5" rand = "0.8.5"
# this dependency is needed soly because the r2d2_sqlite crate doesn't export # this dependency is needed soly because the r2d2_sqlite crate doesn't export
# the 'chrono' feature flag # the 'chrono' feature flag
rusqlite = { version = "0.32.1", features = ["chrono", "bundled"] }
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
tera = "1.20.0" tera = "1.20.0"
tokio = { version = "1.42.0", features = ["full"] } tokio = { version = "1.42.0", features = ["full"] }

View File

@ -1,5 +1,5 @@
create table plants ( create table plants (
id bigint primary key not null, id integer primary key not null,
name text not null, name text not null,
species text not null, species text not null,
description 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 ( create table events (
id bigint primary key not null, id integer primary key not null,
plant_id bigint not null plant_id integer not null
references plants (id) references plants (id)
on delete cascade, on delete cascade,
event_type text not null, event_type text not null,

View File

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

View File

@ -1,32 +1,37 @@
mod event;
mod models; mod models;
mod plant;
mod schema; 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 models::event::{Event, EventType, NewEvent, EVENT_TYPES};
pub use plant::{NewPlant, Plant}; pub use models::plant::{NewPlant, Plant};
pub use session::Session; pub use models::session::Session;
pub use user::{NewUser, User}; 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)] #[derive(Debug)]
pub enum DbError { pub enum DbError {
Pool(r2d2::Error), Pool(diesel::r2d2::PoolError),
Db(rusqlite::Error), Db(diesel::result::Error),
} }
impl fmt::Display for DbError { impl fmt::Display for DbError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Pool(_) => write!(f, "failed to acquire connection from pool"), 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 { impl From<diesel::r2d2::PoolError> for DbError {
fn from(value: r2d2::Error) -> Self { fn from(value: diesel::r2d2::PoolError) -> Self {
Self::Pool(value) Self::Pool(value)
} }
} }
impl From<rusqlite::Error> for DbError { impl From<diesel::result::Error> for DbError {
fn from(value: rusqlite::Error) -> Self { fn from(value: diesel::result::Error) -> Self {
Self::Db(value) Self::Db(value)
} }
} }
pub fn run_migrations(pool: &DbPool, migrations: &[&str]) -> rusqlite::Result<()> { pub fn initialize_db(path: impl AsRef<Path>, run_migrations: bool) -> Result<DbPool, DbError> {
let mut conn = pool.get().unwrap(); 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 if run_migrations {
// try to run the first migration pool.get()?.run_pending_migrations(MIGRATIONS).unwrap();
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;
} }
Ok(()) Ok(pool)
} }

View File

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

View File

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

View File

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

View File

@ -5,13 +5,13 @@ use argon2::{
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::db::schema::*; use crate::db::{schema::*, DbConn, DbPool, DbResult};
#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] #[derive(Serialize, Deserialize, Clone, Queryable, Selectable)]
#[diesel(table_name = users)] #[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct User { pub struct User {
pub id: i64, pub id: i32,
pub username: String, pub username: String,
pub password_hash: String, pub password_hash: String,
pub admin: bool, pub admin: bool,
@ -45,24 +45,27 @@ impl NewUser {
} }
} }
pub fn insert(self, conn: &mut SqliteConnection) -> QueryResult<User> { pub fn insert(self, pool: &DbPool) -> DbResult<User> {
diesel::insert_into(users::table) Ok(diesel::insert_into(users::table)
.values(self) .values(self)
.returning(User::as_returning()) .returning(User::as_returning())
.get_result(conn) .get_result(&mut pool.get()?)?)
} }
} }
impl User { impl User {
pub fn by_username( pub fn by_username(pool: &DbPool, username: impl AsRef<str>) -> DbResult<Option<Self>> {
conn: &mut SqliteConnection, Ok(users::dsl::users
username: impl AsRef<str>,
) -> QueryResult<Option<Self>> {
users::dsl::users
.select(User::as_select()) .select(User::as_select())
.filter(users::username.eq(username.as_ref())) .filter(users::username.eq(username.as_ref()))
.first(conn) .first(&mut pool.get()?)
.optional() .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 { 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) .verify_password(password.as_ref().as_bytes(), &password_hash)
.is_ok() .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. // @generated automatically by Diesel CLI.
diesel::table! {
comments (id) {
id -> BigInt,
plant_id -> BigInt,
comment -> Text,
}
}
diesel::table! { diesel::table! {
events (id) { events (id) {
id -> BigInt, id -> Integer,
plant_id -> BigInt, plant_id -> Integer,
event_type -> Text, event_type -> Text,
date -> Date, date -> Date,
description -> Text, description -> Text,
@ -20,7 +12,7 @@ diesel::table! {
diesel::table! { diesel::table! {
plants (id) { plants (id) {
id -> BigInt, id -> Integer,
name -> Text, name -> Text,
species -> Text, species -> Text,
description -> Text, description -> Text,
@ -30,21 +22,20 @@ diesel::table! {
diesel::table! { diesel::table! {
sessions (id) { sessions (id) {
id -> BigInt, id -> BigInt,
user_id -> BigInt, user_id -> Integer,
} }
} }
diesel::table! { diesel::table! {
users (id) { users (id) {
id -> BigInt, id -> Integer,
username -> Text, username -> Text,
password_hash -> Text, password_hash -> Text,
admin -> Bool, admin -> Bool,
} }
} }
diesel::joinable!(comments -> plants (plant_id));
diesel::joinable!(events -> plants (plant_id)); diesel::joinable!(events -> plants (plant_id));
diesel::joinable!(sessions -> users (user_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 std::{fs, path::Path, sync::Arc};
use clap::Parser; use clap::Parser;
use cli::UserCmd; use diesel_migrations::{embed_migrations, EmbeddedMigrations};
use db::DbError;
use r2d2_sqlite::SqliteConnectionManager;
use tera::Tera; use tera::Tera;
use tower_http::compression::CompressionLayer; use tower_http::compression::CompressionLayer;
const MIGRATIONS: [&str; 4] = [ use cli::UserCmd;
include_str!("migrations/000_initial.sql"), use db::DbError;
include_str!("migrations/001_plants.sql"),
include_str!("migrations/003_events.sql"),
include_str!("migrations/004_auth.sql"),
];
const DB_FILENAME: &str = "db.sqlite3"; const DB_FILENAME: &str = "db.sqlite3";
#[derive(Clone)] #[derive(Clone)]
@ -26,8 +21,7 @@ pub struct Context {
} }
fn run_user_cli(data_dir: impl AsRef<Path>, cmd: UserCmd) -> Result<(), DbError> { 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 = db::initialize_db(data_dir.as_ref().join(DB_FILENAME), false)?;
let pool = r2d2::Pool::new(manager)?;
match cmd.cmd { match cmd.cmd {
cli::UserSubCmd::List => { cli::UserSubCmd::List => {
@ -44,12 +38,7 @@ fn run_user_cli(data_dir: impl AsRef<Path>, cmd: UserCmd) -> Result<(), DbError>
password, password,
admin, admin,
} => { } => {
db::NewUser { db::NewUser::new(username, password, admin).insert(&pool)?;
username,
password,
admin,
}
.insert(&pool)?;
} }
cli::UserSubCmd::Remove { username } => { cli::UserSubCmd::Remove { username } => {
db::User::remove_by_username(&pool, &username)?; db::User::remove_by_username(&pool, &username)?;
@ -71,9 +60,7 @@ async fn main() {
fs::create_dir_all(&args.data_dir).unwrap(); fs::create_dir_all(&args.data_dir).unwrap();
} }
let manager = SqliteConnectionManager::file(args.data_dir.join(DB_FILENAME)); let pool = db::initialize_db(args.data_dir.join(DB_FILENAME), true).unwrap();
let pool = r2d2::Pool::new(manager).unwrap();
db::run_migrations(&pool, &MIGRATIONS).unwrap();
let tera = let tera =
Tera::new(&format!("{}/**/*", args.templates_dir.to_string_lossy())).unwrap(); Tera::new(&format!("{}/**/*", args.templates_dir.to_string_lossy())).unwrap();

View File

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