feat: fully replace database operations with diesel
This commit is contained in:
parent
d0ecb9357a
commit
c7dc68926b
16 changed files with 227 additions and 224 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()?)?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
mod event;
|
||||
mod plant;
|
||||
mod session;
|
||||
mod user;
|
||||
pub mod event;
|
||||
pub mod plant;
|
||||
pub mod session;
|
||||
pub mod user;
|
||||
|
|
|
|||
|
|
@ -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()?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,);
|
||||
|
|
|
|||
27
src/main.rs
27
src/main.rs
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue