diff --git a/migrations/2025-02-23-165014_devices/down.sql b/migrations/2025-02-23-165014_devices/down.sql new file mode 100644 index 0000000..3960a97 --- /dev/null +++ b/migrations/2025-02-23-165014_devices/down.sql @@ -0,0 +1 @@ +drop table devices; diff --git a/migrations/2025-02-23-165014_devices/up.sql b/migrations/2025-02-23-165014_devices/up.sql new file mode 100644 index 0000000..2e77c08 --- /dev/null +++ b/migrations/2025-02-23-165014_devices/up.sql @@ -0,0 +1,13 @@ +create table devices ( + id integer primary key not null, + + device_id text not null, + user_id bigint not null + references users (id) + on delete cascade, + + caption text not null, + type text not null, + + unique (user_id, device_id) +); diff --git a/src/db/mod.rs b/src/db/mod.rs index 63d6ef0..d435e9e 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,6 +1,7 @@ pub mod models; mod schema; +pub use models::device::{Device, NewDevice}; pub use models::session::Session; pub use models::user::{NewUser, User}; diff --git a/src/db/models/device.rs b/src/db/models/device.rs new file mode 100644 index 0000000..d5dbb28 --- /dev/null +++ b/src/db/models/device.rs @@ -0,0 +1,115 @@ +use std::{fmt, str::FromStr}; + +use diesel::{ + deserialize::{FromSql, FromSqlRow}, + expression::AsExpression, + prelude::*, + serialize::ToSql, + sql_types::Text, + sqlite::{Sqlite, SqliteValue}, +}; +use serde::{Deserialize, Serialize}; + +use crate::db::{schema::*, DbPool, DbResult}; + +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] +#[diesel(table_name = devices)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct Device { + id: i64, + device_id: String, + user_id: i64, + caption: String, + type_: DeviceType, +} + +#[derive(Deserialize, Insertable)] +#[diesel(table_name = devices)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct NewDevice { + device_id: String, + user_id: i64, + caption: String, + type_: DeviceType, +} + +#[derive(Serialize, Deserialize, FromSqlRow, Debug, AsExpression, Clone)] +#[diesel(sql_type = Text)] +#[serde(rename_all = "lowercase")] +pub enum DeviceType { + Desktop, + Laptop, + Mobile, + Server, + Other, +} + +impl Device { + pub fn for_user(pool: &DbPool, user_id: i64) -> DbResult> { + Ok(devices::dsl::devices + .select(Self::as_select()) + .filter(devices::user_id.eq(user_id)) + .get_results(&mut pool.get()?)?) + } +} + +impl fmt::Display for DeviceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Desktop => "desktop", + Self::Laptop => "laptop", + Self::Mobile => "mobile", + Self::Server => "server", + Self::Other => "other", + } + ) + } +} + +#[derive(Debug)] +pub struct DeviceTypeParseErr(String); + +impl fmt::Display for DeviceTypeParseErr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid device type '{}'", self.0) + } +} + +impl std::error::Error for DeviceTypeParseErr {} + +impl FromStr for DeviceType { + type Err = DeviceTypeParseErr; + + fn from_str(s: &str) -> Result { + match s { + "desktop" => Ok(Self::Desktop), + "laptop" => Ok(Self::Laptop), + "mobile" => Ok(Self::Mobile), + "server" => Ok(Self::Server), + "other" => Ok(Self::Other), + _ => Err(DeviceTypeParseErr(s.to_string())), + } + } +} + +impl FromSql for DeviceType { + fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result { + let s = >::from_sql(bytes)?; + + Ok(s.as_str().parse()?) + } +} + +impl ToSql for DeviceType { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, Sqlite>, + ) -> diesel::serialize::Result { + out.set_value(self.to_string()); + + Ok(diesel::serialize::IsNull::No) + } +} diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index f950c0f..538594b 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -1,2 +1,3 @@ +pub mod device; pub mod session; pub mod user; diff --git a/src/db/models/session.rs b/src/db/models/session.rs index 8534410..10ee4a1 100644 --- a/src/db/models/session.rs +++ b/src/db/models/session.rs @@ -23,15 +23,16 @@ impl Session { .get_result(&mut pool.get()?)?) } - pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult { + pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult> { Ok(sessions::dsl::sessions .inner_join(users::table) .filter(sessions::id.eq(id)) .select(User::as_select()) - .get_result(&mut pool.get()?)?) + .get_result(&mut pool.get()?) + .optional()?) } - pub fn user(&self, pool: &DbPool) -> DbResult { + pub fn user(&self, pool: &DbPool) -> DbResult> { Self::user_from_id(pool, self.id) } diff --git a/src/db/schema.rs b/src/db/schema.rs index d9b70af..50ba51b 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -1,5 +1,16 @@ // @generated automatically by Diesel CLI. +diesel::table! { + devices (id) { + id -> BigInt, + device_id -> Text, + user_id -> BigInt, + caption -> Text, + #[sql_name = "type"] + type_ -> Text, + } +} + diesel::table! { sessions (id) { id -> BigInt, @@ -15,9 +26,11 @@ diesel::table! { } } +diesel::joinable!(devices -> users (user_id)); diesel::joinable!(sessions -> users (user_id)); diesel::allow_tables_to_appear_in_same_query!( + devices, sessions, users, );