From cf18044b956574bd0e5ee61f1c410932fbfc8f60 Mon Sep 17 00:00:00 2001 From: Kappeh Date: Fri, 7 Feb 2025 02:32:31 +0000 Subject: [PATCH] Add profiles table --- Cargo.lock | 5 + cipher_core/Cargo.toml | 1 + cipher_core/src/repository/mod.rs | 3 + .../src/repository/profile_repository.rs | 165 ++++++++++++ cipher_core/src/repository/user_repository.rs | 8 - cipher_database/Cargo.toml | 3 +- .../down.sql | 20 ++ .../2025-02-05-033255_create_profiles/up.sql | 36 +++ .../down.sql | 21 ++ .../2025-02-05-033227_create_profiles/up.sql | 36 +++ .../down.sql | 20 ++ .../2025-02-05-032343_create_profiles/up.sql | 36 +++ cipher_database/src/mysql/repository/mod.rs | 1 + .../mysql/repository/profile_repository.rs | 246 ++++++++++++++++++ .../mysql/repository/staff_role_repository.rs | 4 +- .../src/mysql/repository/user_repository.rs | 45 +--- cipher_database/src/mysql/schema.rs | 33 ++- .../src/postgres/repository/mod.rs | 1 + .../postgres/repository/profile_repository.rs | 241 +++++++++++++++++ .../postgres/repository/user_repository.rs | 29 --- cipher_database/src/postgres/schema.rs | 33 ++- cipher_database/src/sqlite/repository/mod.rs | 1 + .../sqlite/repository/profile_repository.rs | 241 +++++++++++++++++ .../src/sqlite/repository/user_repository.rs | 29 --- cipher_database/src/sqlite/schema.rs | 27 +- .../src/commands/profile/codes.rs | 46 ++-- .../src/commands/profile/mod.rs | 13 +- 27 files changed, 1194 insertions(+), 150 deletions(-) create mode 100644 cipher_core/src/repository/profile_repository.rs create mode 100644 cipher_database/migrations/mysql/2025-02-05-033255_create_profiles/down.sql create mode 100644 cipher_database/migrations/mysql/2025-02-05-033255_create_profiles/up.sql create mode 100644 cipher_database/migrations/postgres/2025-02-05-033227_create_profiles/down.sql create mode 100644 cipher_database/migrations/postgres/2025-02-05-033227_create_profiles/up.sql create mode 100644 cipher_database/migrations/sqlite/2025-02-05-032343_create_profiles/down.sql create mode 100644 cipher_database/migrations/sqlite/2025-02-05-032343_create_profiles/up.sql create mode 100644 cipher_database/src/mysql/repository/profile_repository.rs create mode 100644 cipher_database/src/postgres/repository/profile_repository.rs create mode 100644 cipher_database/src/sqlite/repository/profile_repository.rs diff --git a/Cargo.lock b/Cargo.lock index 941efde..327ad10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,8 +369,10 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -379,6 +381,7 @@ name = "cipher_core" version = "0.1.0" dependencies = [ "async-trait", + "chrono", ] [[package]] @@ -386,6 +389,7 @@ name = "cipher_database" version = "0.1.0" dependencies = [ "async-trait", + "chrono", "cipher_core", "diesel", "diesel-async", @@ -710,6 +714,7 @@ checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12" dependencies = [ "bitflags 2.8.0", "byteorder", + "chrono", "diesel_derives", "itoa", "libsqlite3-sys", diff --git a/cipher_core/Cargo.toml b/cipher_core/Cargo.toml index 5a778dc..0ce9b88 100644 --- a/cipher_core/Cargo.toml +++ b/cipher_core/Cargo.toml @@ -5,3 +5,4 @@ edition = "2021" [dependencies] async-trait = "0.1.85" +chrono = "0.4.39" diff --git a/cipher_core/src/repository/mod.rs b/cipher_core/src/repository/mod.rs index a596b33..04aad8a 100644 --- a/cipher_core/src/repository/mod.rs +++ b/cipher_core/src/repository/mod.rs @@ -1,8 +1,10 @@ use std::fmt::Display; +use profile_repository::ProfileRepository; use staff_role_repository::StaffRoleRepository; use user_repository::UserRepository; +pub mod profile_repository; pub mod staff_role_repository; pub mod user_repository; @@ -19,6 +21,7 @@ pub trait RepositoryProvider { pub trait Repository where + Self: ProfileRepository::BackendError>, Self: StaffRoleRepository::BackendError>, Self: UserRepository::BackendError>, { diff --git a/cipher_core/src/repository/profile_repository.rs b/cipher_core/src/repository/profile_repository.rs new file mode 100644 index 0000000..1d9f478 --- /dev/null +++ b/cipher_core/src/repository/profile_repository.rs @@ -0,0 +1,165 @@ +use chrono::DateTime; +use chrono::Utc; + +use super::RepositoryError; + +/// A repository trait for managing user profiles, supporting asynchronous operations. +/// +/// This trait defines the required operations for interacting with user profiles, +/// including creation, retrieval, history management, and version rollback. It is +/// designed to be implemented for various backends, such as databases, using Diesel or Diesel Async. +/// +/// # Type Parameters +/// - `BackendError`: Represents the error type returned by the underlying backend implementation. +/// +/// # Errors +/// All methods return a `RepositoryError`, which encapsulates +/// errors specific to the backend implementation. +#[async_trait::async_trait] +pub trait ProfileRepository { + /// The associated error type returned by backend operations. + type BackendError: std::error::Error; + + /// Inserts a new profile into the repository and marks it as the active profile for the user it belongs to. + /// + /// # Arguments + /// * `new_profile` - The profile data to insert. + /// + /// # Returns + /// * `Ok(Profile)` - The inserted profile with its assigned ID. + /// * `Err(RepositoryError)` - If the operation fails. + async fn insert_profile(&mut self, new_profile: NewProfile) -> Result>; + + /// Retrieves a profile by its unique ID. + /// + /// # Arguments + /// * `id` - The unique profile ID. + /// + /// # Returns + /// * `Ok(Some(Profile))` - If a profile with the given ID exists. + /// * `Ok(None)` - If no profile is found. + /// * `Err(RepositoryError)` - If an error occurs. + async fn profile(&mut self, id: i32) -> Result, RepositoryError>; + + /// Retrieves the active profile associated with a given user ID. + /// + /// # Arguments + /// * `user_id` - The ID of the user whose profile is being retrieved. + /// + /// # Returns + /// * `Ok(Some(Profile))` - The users active profile for the user if it exists. + /// * `Ok(None)` - If no active profile is found for the user. + /// * `Err(RepositoryError)` - If an error occurs. + async fn active_profile(&mut self, user_id: i32) -> Result, RepositoryError>; + + /// Retrieves the active profile associated with a given Discord user ID. + /// + /// # Arguments + /// * `discord_user_id` - The Discord user's unique identifier. + /// + /// # Returns + /// * `Ok(Some(Profile))` - The users active profile for the user if it exists. + /// * `Ok(None)` - If no active profile is found for the user. + /// * `Err(RepositoryError)` - If an error occurs. + async fn active_profile_by_discord_id(&mut self, discord_user_id: u64) -> Result, RepositoryError>; + + /// Retrieves the full profile history for a given user. + /// + /// # Arguments + /// * `user_id` - The ID of the user whose profile history is being retrieved. + /// + /// # Returns + /// * `Ok(Vec)` - A list of all past versions of the user's profile. + /// * `Err(RepositoryError)` - If an error occurs. + async fn profiles_by_user_id(&mut self, user_id: i32) -> Result, RepositoryError>; + + /// Retrieves the full profile history for a given Discord user. + /// + /// # Arguments + /// * `discord_user_id` - The Discord user's unique identifier. + /// + /// # Returns + /// * `Ok(Vec)` - A list of all past versions of the user's profile. + /// * `Err(RepositoryError)` - If an error occurs. + async fn profiles_by_discord_id(&mut self, discord_user_id: u64) -> Result, RepositoryError>; + + /// Sets an profile version as active. + /// + /// This function marks `profile_id` as the active profile + /// and deactivates all other profiles for the user. + /// + /// # Arguments + /// * `user_id` - The ID of the user. + /// * `profile_id` - The profile ID to mark as active. + /// + /// # Returns + /// * `Ok(false)` - If the specified profile does not exist. + /// * `Ok(true)` - If the operation was successful. + /// * `Err(RepositoryError)` - If the operation fails. + async fn set_active_profile(&mut self, user_id: i32, profile_id: i32) -> Result>; +} + +pub struct Profile { + pub id: i32, + pub user_id: i32, + + pub thumbnail_url: Option, + pub image_url: Option, + + pub trainer_class: Option, + pub nature: Option, + pub partner_pokemon: Option, + pub starting_region: Option, + pub favourite_food: Option, + pub likes: Option, + pub quotes: Option, + + pub pokemon_go_code: Option, + pub pokemon_pocket_code: Option, + pub switch_code: Option, + + pub created_at: DateTime, + pub is_active: bool, +} + +pub struct NewProfile { + pub user_id: i32, + + pub thumbnail_url: Option, + pub image_url: Option, + + pub trainer_class: Option, + pub nature: Option, + pub partner_pokemon: Option, + pub starting_region: Option, + pub favourite_food: Option, + pub likes: Option, + pub quotes: Option, + + pub pokemon_go_code: Option, + pub pokemon_pocket_code: Option, + pub switch_code: Option, +} + +impl Profile { + pub fn into_new(self) -> NewProfile { + NewProfile { + user_id: self.user_id, + + thumbnail_url: self.thumbnail_url, + image_url: self.image_url, + + trainer_class: self.trainer_class, + nature: self.nature, + partner_pokemon: self.partner_pokemon, + starting_region: self.starting_region, + favourite_food: self.favourite_food, + likes: self.likes, + quotes: self.quotes, + + pokemon_go_code: self.pokemon_go_code, + pokemon_pocket_code: self.pokemon_pocket_code, + switch_code: self.switch_code, + } + } +} diff --git a/cipher_core/src/repository/user_repository.rs b/cipher_core/src/repository/user_repository.rs index f018445..c319995 100644 --- a/cipher_core/src/repository/user_repository.rs +++ b/cipher_core/src/repository/user_repository.rs @@ -11,21 +11,13 @@ pub trait UserRepository { async fn insert_user(&mut self, new_user: NewUser) -> Result>; async fn update_user(&mut self, user: User) -> Result, RepositoryError>; - - async fn remove_user(&mut self, id: i32) -> Result, RepositoryError>; } pub struct User { pub id: i32, pub discord_user_id: u64, - pub pokemon_go_code: Option, - pub pokemon_pocket_code: Option, - pub switch_code: Option, } pub struct NewUser { pub discord_user_id: u64, - pub pokemon_go_code: Option, - pub pokemon_pocket_code: Option, - pub switch_code: Option, } diff --git a/cipher_database/Cargo.toml b/cipher_database/Cargo.toml index 793d38f..48d83a1 100644 --- a/cipher_database/Cargo.toml +++ b/cipher_database/Cargo.toml @@ -5,11 +5,12 @@ edition = "2021" [dependencies] async-trait = "0.1.85" -diesel = { version = "2.2.6", default-features = false } +diesel = { version = "2.2.6", default-features = false, features = ["chrono"] } diesel-async = { version = "0.5.2", features = ["bb8"] } diesel_migrations = "2.2.0" cipher_core = { path = "../cipher_core" } thiserror = "2.0.11" +chrono = "0.4.39" [features] default = ["mysql", "postgres", "sqlite"] diff --git a/cipher_database/migrations/mysql/2025-02-05-033255_create_profiles/down.sql b/cipher_database/migrations/mysql/2025-02-05-033255_create_profiles/down.sql new file mode 100644 index 0000000..4b6e9e8 --- /dev/null +++ b/cipher_database/migrations/mysql/2025-02-05-033255_create_profiles/down.sql @@ -0,0 +1,20 @@ +ALTER TABLE users ADD COLUMN pokemon_go_code VARCHAR(32); +ALTER TABLE users ADD COLUMN pokemon_pocket_code VARCHAR(32); +ALTER TABLE users ADD COLUMN switch_code VARCHAR(32); + +UPDATE users +INNER JOIN ( + SELECT user_id, pokemon_go_code, pokemon_pocket_code, switch_code + FROM profiles + WHERE is_active = true +) AS subquery +ON users.id = subquery.user_id +SET + users.pokemon_go_code = subquery.pokemon_go_code, + users.pokemon_pocket_code = subquery.pokemon_pocket_code, + users.switch_code = subquery.switch_code; + +DROP INDEX profiles_created_at ON profiles; +DROP INDEX profiles_is_active ON profiles; + +DROP TABLE profiles; diff --git a/cipher_database/migrations/mysql/2025-02-05-033255_create_profiles/up.sql b/cipher_database/migrations/mysql/2025-02-05-033255_create_profiles/up.sql new file mode 100644 index 0000000..e6eaf98 --- /dev/null +++ b/cipher_database/migrations/mysql/2025-02-05-033255_create_profiles/up.sql @@ -0,0 +1,36 @@ +CREATE TABLE profiles ( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + user_id INTEGER NOT NULL, + + thumbnail_url TEXT, + image_url TEXT, + + trainer_class TEXT, + nature TEXT, + partner_pokemon TEXT, + starting_region TEXT, + favourite_food TEXT, + likes TEXT, + quotes TEXT, + + pokemon_go_code VARCHAR(32), + pokemon_pocket_code VARCHAR(32), + switch_code VARCHAR(32), + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN NOT NULL DEFAULT true, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE +); + +CREATE INDEX profiles_user_id ON profiles(user_id); +CREATE INDEX profiles_created_at ON profiles(created_at); +CREATE INDEX profiles_is_active ON profiles(is_active); + +INSERT INTO profiles (user_id, pokemon_go_code, pokemon_pocket_code, switch_code) +SELECT id, pokemon_go_code, pokemon_pocket_code, switch_code +FROM users; + +ALTER TABLE users DROP COLUMN pokemon_go_code; +ALTER TABLE users DROP COLUMN pokemon_pocket_code; +ALTER TABLE users DROP COLUMN switch_code; diff --git a/cipher_database/migrations/postgres/2025-02-05-033227_create_profiles/down.sql b/cipher_database/migrations/postgres/2025-02-05-033227_create_profiles/down.sql new file mode 100644 index 0000000..ecdaf2f --- /dev/null +++ b/cipher_database/migrations/postgres/2025-02-05-033227_create_profiles/down.sql @@ -0,0 +1,21 @@ +ALTER TABLE users ADD COLUMN pokemon_go_code VARCHAR(32); +ALTER TABLE users ADD COLUMN pokemon_pocket_code VARCHAR(32); +ALTER TABLE users ADD COLUMN switch_code VARCHAR(32); + +UPDATE users +SET + pokemon_go_code = subquery.pokemon_go_code, + pokemon_pocket_code = subquery.pokemon_pocket_code, + switch_code = subquery.switch_code +FROM ( + SELECT user_id, pokemon_go_code, pokemon_pocket_code, switch_code + FROM profiles + WHERE is_active = true +) AS subquery +WHERE id = subquery.user_id; + +DROP INDEX profiles_user_id; +DROP INDEX profiles_created_at; +DROP INDEX profiles_is_active; + +DROP TABLE profiles; diff --git a/cipher_database/migrations/postgres/2025-02-05-033227_create_profiles/up.sql b/cipher_database/migrations/postgres/2025-02-05-033227_create_profiles/up.sql new file mode 100644 index 0000000..e8717cf --- /dev/null +++ b/cipher_database/migrations/postgres/2025-02-05-033227_create_profiles/up.sql @@ -0,0 +1,36 @@ +CREATE TABLE profiles ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + + thumbnail_url TEXT, + image_url TEXT, + + trainer_class TEXT, + nature TEXT, + partner_pokemon TEXT, + starting_region TEXT, + favourite_food TEXT, + likes TEXT, + quotes TEXT, + + pokemon_go_code VARCHAR(32), + pokemon_pocket_code VARCHAR(32), + switch_code VARCHAR(32), + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN NOT NULL DEFAULT true, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE +); + +CREATE INDEX profiles_user_id ON profiles(user_id); +CREATE INDEX profiles_created_at ON profiles(created_at); +CREATE INDEX profiles_is_active ON profiles(is_active); + +INSERT INTO profiles (user_id, pokemon_go_code, pokemon_pocket_code, switch_code) +SELECT id, pokemon_go_code, pokemon_pocket_code, switch_code +FROM users; + +ALTER TABLE users DROP COLUMN pokemon_go_code; +ALTER TABLE users DROP COLUMN pokemon_pocket_code; +ALTER TABLE users DROP COLUMN switch_code; diff --git a/cipher_database/migrations/sqlite/2025-02-05-032343_create_profiles/down.sql b/cipher_database/migrations/sqlite/2025-02-05-032343_create_profiles/down.sql new file mode 100644 index 0000000..889bcbd --- /dev/null +++ b/cipher_database/migrations/sqlite/2025-02-05-032343_create_profiles/down.sql @@ -0,0 +1,20 @@ +ALTER TABLE users ADD COLUMN pokemon_go_code VARCHAR(32); +ALTER TABLE users ADD COLUMN pokemon_pocket_code VARCHAR(32); +ALTER TABLE users ADD COLUMN switch_code VARCHAR(32); + +UPDATE users SET + pokemon_go_code = subquery.pokemon_go_code, + pokemon_pocket_code = subquery.pokemon_pocket_code, + switch_code = subquery.switch_code +FROM ( + SELECT user_id, pokemon_go_code, pokemon_pocket_code, switch_code + FROM profiles + WHERE is_active = true +) AS subquery +WHERE id = subquery.user_id; + +DROP INDEX profiles_user_id; +DROP INDEX profiles_created_at; +DROP INDEX profiles_is_active; + +DROP TABLE profiles; diff --git a/cipher_database/migrations/sqlite/2025-02-05-032343_create_profiles/up.sql b/cipher_database/migrations/sqlite/2025-02-05-032343_create_profiles/up.sql new file mode 100644 index 0000000..d751aab --- /dev/null +++ b/cipher_database/migrations/sqlite/2025-02-05-032343_create_profiles/up.sql @@ -0,0 +1,36 @@ +CREATE TABLE profiles ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + + thumbnail_url TEXT, + image_url TEXT, + + trainer_class TEXT, + nature TEXT, + partner_pokemon TEXT, + starting_region TEXT, + favourite_food TEXT, + likes TEXT, + quotes TEXT, + + pokemon_go_code VARCHAR(32), + pokemon_pocket_code VARCHAR(32), + switch_code VARCHAR(32), + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN NOT NULL DEFAULT true, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE +); + +CREATE INDEX profiles_user_id ON profiles(user_id); +CREATE INDEX profiles_created_at ON profiles(created_at); +CREATE INDEX profiles_is_active ON profiles(is_active); + +INSERT INTO profiles (user_id, pokemon_go_code, pokemon_pocket_code, switch_code) +SELECT id, pokemon_go_code, pokemon_pocket_code, switch_code +FROM users; + +ALTER TABLE users DROP COLUMN pokemon_go_code; +ALTER TABLE users DROP COLUMN pokemon_pocket_code; +ALTER TABLE users DROP COLUMN switch_code; diff --git a/cipher_database/src/mysql/repository/mod.rs b/cipher_database/src/mysql/repository/mod.rs index a6770f3..6b50530 100644 --- a/cipher_database/src/mysql/repository/mod.rs +++ b/cipher_database/src/mysql/repository/mod.rs @@ -7,6 +7,7 @@ use cipher_core::repository::RepositoryProvider; use crate::BackendError; +mod profile_repository; mod staff_role_repository; mod user_repository; diff --git a/cipher_database/src/mysql/repository/profile_repository.rs b/cipher_database/src/mysql/repository/profile_repository.rs new file mode 100644 index 0000000..af55f32 --- /dev/null +++ b/cipher_database/src/mysql/repository/profile_repository.rs @@ -0,0 +1,246 @@ +use chrono::DateTime; +use chrono::NaiveDateTime; +use chrono::Utc; +use cipher_core::repository::profile_repository::NewProfile; +use diesel_async::scoped_futures::ScopedFutureExt; +use diesel_async::AsyncConnection; +use diesel_async::RunQueryDsl; +use cipher_core::repository::profile_repository::Profile; +use cipher_core::repository::profile_repository::ProfileRepository; +use cipher_core::repository::RepositoryError; +use diesel::prelude::*; + +use crate::mysql::schema::profiles; +use crate::mysql::schema::users; +use crate::BackendError; + +use super::MysqlRepository; + +#[async_trait::async_trait] +impl ProfileRepository for MysqlRepository<'_> { + type BackendError = BackendError; + + async fn insert_profile(&mut self, new_profile: NewProfile) -> Result> { + let model_new_profile = ModelNewProfile::from(new_profile); + self.conn + .transaction::<_, diesel::result::Error, _>(|conn| async move { + diesel::update(profiles::table) + .filter(profiles::user_id.eq(model_new_profile.user_id)) + .set(profiles::is_active.eq(false)) + .execute(conn) + .await?; + + diesel::insert_into(profiles::table) + .values(&model_new_profile) + .execute(conn) + .await?; + + profiles::table + .filter(profiles::is_active.eq(true)) + .select(ModelProfile::as_select()) + .first(conn) + .await + }.scope_boxed()) + .await + .map(Profile::from) + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn profile(&mut self, id: i32) -> Result, RepositoryError> { + profiles::dsl::profiles.find(id) + .first::(&mut self.conn) + .await + .map(Profile::from) + .optional() + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn active_profile(&mut self, user_id: i32) -> Result, RepositoryError> { + profiles::dsl::profiles + .filter(profiles::user_id.eq(user_id)) + .filter(profiles::is_active.eq(true)) + .first::(&mut self.conn) + .await + .map(Profile::from) + .optional() + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn active_profile_by_discord_id(&mut self, discord_user_id: u64) -> Result, RepositoryError> { + let model_discord_user_id = discord_user_id as i64; + profiles::table + .inner_join(users::table) + .filter(users::discord_user_id.eq(model_discord_user_id)) + .filter(profiles::is_active.eq(true)) + .select(ModelProfile::as_select()) + .first(&mut self.conn) + .await + .map(Profile::from) + .optional() + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn profiles_by_user_id(&mut self, user_id: i32) -> Result, RepositoryError> { + let results: Vec<_> = profiles::dsl::profiles + .filter(profiles::user_id.eq(user_id)) + .order(profiles::created_at.desc()) + .get_results::(&mut self.conn) + .await + .map_err(|err| RepositoryError(BackendError::from(err)))? + .into_iter() + .map(Profile::from) + .collect(); + + Ok(results) + } + + async fn profiles_by_discord_id(&mut self, discord_user_id: u64) -> Result, RepositoryError> { + let model_discord_user_id = discord_user_id as i64; + let results = profiles::table + .inner_join(users::table) + .filter(users::discord_user_id.eq(model_discord_user_id)) + .order(profiles::created_at.desc()) + .select(ModelProfile::as_select()) + .get_results(&mut self.conn) + .await + .map_err(|err| RepositoryError(BackendError::from(err)))? + .into_iter() + .map(Profile::from) + .collect(); + + Ok(results) + } + + async fn set_active_profile(&mut self, user_id: i32, profile_id: i32) -> Result> { + self.conn + .transaction::<_, diesel::result::Error, _>(move |conn| async move { + let num_affected = diesel::update(profiles::table.find(profile_id)) + .set(profiles::is_active.eq(true)) + .execute(conn) + .await?; + + if num_affected == 0 { + return Ok(false); + } + + diesel::update(profiles::table) + .filter(profiles::user_id.eq(user_id)) + .filter(profiles::id.ne(profile_id)) + .filter(profiles::is_active.eq(true)) + .set(profiles::is_active.eq(false)) + .execute(conn) + .await?; + + Ok(true) + }.scope_boxed()) + .await + .map_err(|err| RepositoryError(BackendError::from(err))) + } +} + +#[derive(Queryable, Selectable, AsChangeset)] +#[diesel(table_name = profiles)] +#[diesel(belongs_to(ModelUser))] +#[diesel(treat_none_as_null = true)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct ModelProfile { + pub id: i32, + pub user_id: i32, + + pub thumbnail_url: Option, + pub image_url: Option, + + pub trainer_class: Option, + pub nature: Option, + pub partner_pokemon: Option, + pub starting_region: Option, + pub favourite_food: Option, + pub likes: Option, + pub quotes: Option, + + pub pokemon_go_code: Option, + pub pokemon_pocket_code: Option, + pub switch_code: Option, + + pub created_at: NaiveDateTime, + pub is_active: bool, +} + +impl From for Profile { + fn from(value: ModelProfile) -> Self { + Self { + id: value.id, + user_id: value.user_id, + + thumbnail_url: value.thumbnail_url, + image_url: value.image_url, + + trainer_class: value.trainer_class, + nature: value.nature, + partner_pokemon: value.partner_pokemon, + starting_region: value.starting_region, + favourite_food: value.favourite_food, + likes: value.likes, + quotes: value.quotes, + + pokemon_go_code: value.pokemon_go_code, + pokemon_pocket_code: value.pokemon_pocket_code, + switch_code: value.switch_code, + + created_at: DateTime::from_naive_utc_and_offset(value.created_at, Utc), + is_active: value.is_active, + } + } +} + +#[derive(Insertable)] +#[diesel(table_name = profiles)] +#[diesel(treat_none_as_null = true)] +#[diesel(check_for_backend(diesel::mysql::Mysql))] +pub struct ModelNewProfile { + pub user_id: i32, + + pub thumbnail_url: Option, + pub image_url: Option, + + pub trainer_class: Option, + pub nature: Option, + pub partner_pokemon: Option, + pub starting_region: Option, + pub favourite_food: Option, + pub likes: Option, + pub quotes: Option, + + pub pokemon_go_code: Option, + pub pokemon_pocket_code: Option, + pub switch_code: Option, + + pub created_at: NaiveDateTime, + pub is_active: bool, +} + +impl From for ModelNewProfile { + fn from(value: NewProfile) -> Self { + Self { + user_id: value.user_id, + + thumbnail_url: value.thumbnail_url, + image_url: value.image_url, + + trainer_class: value.trainer_class, + nature: value.nature, + partner_pokemon: value.partner_pokemon, + starting_region: value.starting_region, + favourite_food: value.favourite_food, + likes: value.likes, + quotes: value.quotes, + + pokemon_go_code: value.pokemon_go_code, + pokemon_pocket_code: value.pokemon_pocket_code, + switch_code: value.switch_code, + + created_at: Utc::now().naive_utc(), + is_active: true, + } + } +} diff --git a/cipher_database/src/mysql/repository/staff_role_repository.rs b/cipher_database/src/mysql/repository/staff_role_repository.rs index 818d904..2b14fd7 100644 --- a/cipher_database/src/mysql/repository/staff_role_repository.rs +++ b/cipher_database/src/mysql/repository/staff_role_repository.rs @@ -91,7 +91,7 @@ impl StaffRoleRepository for MysqlRepository<'_> { #[diesel(table_name = staff_roles)] #[diesel(treat_none_as_null = true)] #[diesel(check_for_backend(diesel::mysql::Mysql))] -struct ModelStaffRole { +pub struct ModelStaffRole { #[allow(unused)] id: i32, discord_role_id: i64, @@ -101,6 +101,6 @@ struct ModelStaffRole { #[diesel(table_name = staff_roles)] #[diesel(treat_none_as_null = true)] #[diesel(check_for_backend(diesel::mysql::Mysql))] -struct ModelNewStaffRole { +pub struct ModelNewStaffRole { discord_role_id: i64, } diff --git a/cipher_database/src/mysql/repository/user_repository.rs b/cipher_database/src/mysql/repository/user_repository.rs index e09458d..eb746a5 100644 --- a/cipher_database/src/mysql/repository/user_repository.rs +++ b/cipher_database/src/mysql/repository/user_repository.rs @@ -86,44 +86,15 @@ impl UserRepository for MysqlRepository<'_> { .map(|option| option.map(User::from)) .map_err(|err| RepositoryError(BackendError::from(err))) } - - async fn remove_user(&mut self, id: i32) -> Result, RepositoryError> { - self.conn - .transaction::<_, diesel::result::Error, _>(move |conn| async move { - let option_removed = users::dsl::users.find(id) - .select(ModelUser::as_select()) - .first(conn) - .await - .optional()?; - - let removed = match option_removed { - Some(previous) => previous, - None => return Ok(None), - }; - - diesel::delete(users::dsl::users.find(id)) - .execute(conn) - .await?; - - Ok(Some(removed)) - - }.scope_boxed()) - .await - .map(|option| option.map(User::from)) - .map_err(|err| RepositoryError(BackendError::from(err))) - } } #[derive(Queryable, Selectable, AsChangeset)] #[diesel(table_name = users)] #[diesel(treat_none_as_null = true)] #[diesel(check_for_backend(diesel::mysql::Mysql))] -struct ModelUser { +pub struct ModelUser { id: i32, discord_user_id: i64, - pokemon_go_code: Option, - pokemon_pocket_code: Option, - switch_code: Option, } impl From for User { @@ -131,9 +102,6 @@ impl From for User { Self { id: value.id, discord_user_id: value.discord_user_id as u64, - pokemon_go_code: value.pokemon_go_code, - pokemon_pocket_code: value.pokemon_pocket_code, - switch_code: value.switch_code, } } } @@ -143,9 +111,6 @@ impl From for ModelUser { Self { id: value.id, discord_user_id: value.discord_user_id as i64, - pokemon_go_code: value.pokemon_go_code, - pokemon_pocket_code: value.pokemon_pocket_code, - switch_code: value.switch_code, } } } @@ -154,20 +119,14 @@ impl From for ModelUser { #[diesel(table_name = users)] #[diesel(treat_none_as_null = true)] #[diesel(check_for_backend(diesel::mysql::Mysql))] -struct ModelNewUser { +pub struct ModelNewUser { discord_user_id: i64, - pokemon_go_code: Option, - pokemon_pocket_code: Option, - switch_code: Option, } impl From for ModelNewUser { fn from(value: NewUser) -> Self { Self { discord_user_id: value.discord_user_id as i64, - pokemon_go_code: value.pokemon_go_code, - pokemon_pocket_code: value.pokemon_pocket_code, - switch_code: value.switch_code, } } } diff --git a/cipher_database/src/mysql/schema.rs b/cipher_database/src/mysql/schema.rs index afd70e4..8514801 100644 --- a/cipher_database/src/mysql/schema.rs +++ b/cipher_database/src/mysql/schema.rs @@ -1,5 +1,29 @@ // @generated automatically by Diesel CLI. +diesel::table! { + profiles (id) { + id -> Integer, + user_id -> Integer, + thumbnail_url -> Nullable, + image_url -> Nullable, + trainer_class -> Nullable, + nature -> Nullable, + partner_pokemon -> Nullable, + starting_region -> Nullable, + favourite_food -> Nullable, + likes -> Nullable, + quotes -> Nullable, + #[max_length = 32] + pokemon_go_code -> Nullable, + #[max_length = 32] + pokemon_pocket_code -> Nullable, + #[max_length = 32] + switch_code -> Nullable, + created_at -> Timestamp, + is_active -> Bool, + } +} + diesel::table! { staff_roles (id) { id -> Integer, @@ -11,16 +35,13 @@ diesel::table! { users (id) { id -> Integer, discord_user_id -> Bigint, - #[max_length = 32] - pokemon_go_code -> Nullable, - #[max_length = 32] - pokemon_pocket_code -> Nullable, - #[max_length = 32] - switch_code -> Nullable, } } +diesel::joinable!(profiles -> users (user_id)); + diesel::allow_tables_to_appear_in_same_query!( + profiles, staff_roles, users, ); diff --git a/cipher_database/src/postgres/repository/mod.rs b/cipher_database/src/postgres/repository/mod.rs index 2740376..5540c6b 100644 --- a/cipher_database/src/postgres/repository/mod.rs +++ b/cipher_database/src/postgres/repository/mod.rs @@ -7,6 +7,7 @@ use cipher_core::repository::RepositoryProvider; use crate::BackendError; +mod profile_repository; mod staff_role_repository; mod user_repository; diff --git a/cipher_database/src/postgres/repository/profile_repository.rs b/cipher_database/src/postgres/repository/profile_repository.rs new file mode 100644 index 0000000..39068a1 --- /dev/null +++ b/cipher_database/src/postgres/repository/profile_repository.rs @@ -0,0 +1,241 @@ +use chrono::DateTime; +use chrono::NaiveDateTime; +use chrono::Utc; +use cipher_core::repository::profile_repository::NewProfile; +use diesel_async::scoped_futures::ScopedFutureExt; +use diesel_async::AsyncConnection; +use diesel_async::RunQueryDsl; +use cipher_core::repository::profile_repository::Profile; +use cipher_core::repository::profile_repository::ProfileRepository; +use cipher_core::repository::RepositoryError; +use diesel::prelude::*; + +use crate::postgres::schema::profiles; +use crate::postgres::schema::users; +use crate::BackendError; + +use super::PostgresRepository; + +#[async_trait::async_trait] +impl ProfileRepository for PostgresRepository<'_> { + type BackendError = BackendError; + + async fn insert_profile(&mut self, new_profile: NewProfile) -> Result> { + let model_new_profile = ModelNewProfile::from(new_profile); + self.conn + .transaction::<_, diesel::result::Error, _>(|conn| async move { + diesel::update(profiles::table) + .filter(profiles::user_id.eq(model_new_profile.user_id)) + .set(profiles::is_active.eq(false)) + .execute(conn) + .await?; + + diesel::insert_into(profiles::table) + .values(&model_new_profile) + .returning(ModelProfile::as_returning()) + .get_result(conn) + .await + }.scope_boxed()) + .await + .map(Profile::from) + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn profile(&mut self, id: i32) -> Result, RepositoryError> { + profiles::dsl::profiles.find(id) + .first::(&mut self.conn) + .await + .map(Profile::from) + .optional() + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn active_profile(&mut self, user_id: i32) -> Result, RepositoryError> { + profiles::dsl::profiles + .filter(profiles::user_id.eq(user_id)) + .filter(profiles::is_active.eq(true)) + .first::(&mut self.conn) + .await + .map(Profile::from) + .optional() + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn active_profile_by_discord_id(&mut self, discord_user_id: u64) -> Result, RepositoryError> { + let model_discord_user_id = discord_user_id as i64; + profiles::table + .inner_join(users::table) + .filter(users::discord_user_id.eq(model_discord_user_id)) + .filter(profiles::is_active.eq(true)) + .select(ModelProfile::as_select()) + .first(&mut self.conn) + .await + .map(Profile::from) + .optional() + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn profiles_by_user_id(&mut self, user_id: i32) -> Result, RepositoryError> { + let results: Vec<_> = profiles::dsl::profiles + .filter(profiles::user_id.eq(user_id)) + .order(profiles::created_at.desc()) + .get_results::(&mut self.conn) + .await + .map_err(|err| RepositoryError(BackendError::from(err)))? + .into_iter() + .map(Profile::from) + .collect(); + + Ok(results) + } + + async fn profiles_by_discord_id(&mut self, discord_user_id: u64) -> Result, RepositoryError> { + let model_discord_user_id = discord_user_id as i64; + let results = profiles::table + .inner_join(users::table) + .filter(users::discord_user_id.eq(model_discord_user_id)) + .order(profiles::created_at.desc()) + .select(ModelProfile::as_select()) + .get_results(&mut self.conn) + .await + .map_err(|err| RepositoryError(BackendError::from(err)))? + .into_iter() + .map(Profile::from) + .collect(); + + Ok(results) + } + + async fn set_active_profile(&mut self, user_id: i32, profile_id: i32) -> Result> { + self.conn + .transaction::<_, diesel::result::Error, _>(move |conn| async move { + let num_affected = diesel::update(profiles::table.find(profile_id)) + .set(profiles::is_active.eq(true)) + .execute(conn) + .await?; + + if num_affected == 0 { + return Ok(false); + } + + diesel::update(profiles::table) + .filter(profiles::user_id.eq(user_id)) + .filter(profiles::id.ne(profile_id)) + .filter(profiles::is_active.eq(true)) + .set(profiles::is_active.eq(false)) + .execute(conn) + .await?; + + Ok(true) + }.scope_boxed()) + .await + .map_err(|err| RepositoryError(BackendError::from(err))) + } +} + +#[derive(Queryable, Selectable, AsChangeset)] +#[diesel(table_name = profiles)] +#[diesel(belongs_to(ModelUser))] +#[diesel(treat_none_as_null = true)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct ModelProfile { + pub id: i32, + pub user_id: i32, + + pub thumbnail_url: Option, + pub image_url: Option, + + pub trainer_class: Option, + pub nature: Option, + pub partner_pokemon: Option, + pub starting_region: Option, + pub favourite_food: Option, + pub likes: Option, + pub quotes: Option, + + pub pokemon_go_code: Option, + pub pokemon_pocket_code: Option, + pub switch_code: Option, + + pub created_at: NaiveDateTime, + pub is_active: bool, +} + +impl From for Profile { + fn from(value: ModelProfile) -> Self { + Self { + id: value.id, + user_id: value.user_id, + + thumbnail_url: value.thumbnail_url, + image_url: value.image_url, + + trainer_class: value.trainer_class, + nature: value.nature, + partner_pokemon: value.partner_pokemon, + starting_region: value.starting_region, + favourite_food: value.favourite_food, + likes: value.likes, + quotes: value.quotes, + + pokemon_go_code: value.pokemon_go_code, + pokemon_pocket_code: value.pokemon_pocket_code, + switch_code: value.switch_code, + + created_at: DateTime::from_naive_utc_and_offset(value.created_at, Utc), + is_active: value.is_active, + } + } +} + +#[derive(Insertable)] +#[diesel(table_name = profiles)] +#[diesel(treat_none_as_null = true)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct ModelNewProfile { + pub user_id: i32, + + pub thumbnail_url: Option, + pub image_url: Option, + + pub trainer_class: Option, + pub nature: Option, + pub partner_pokemon: Option, + pub starting_region: Option, + pub favourite_food: Option, + pub likes: Option, + pub quotes: Option, + + pub pokemon_go_code: Option, + pub pokemon_pocket_code: Option, + pub switch_code: Option, + + pub created_at: NaiveDateTime, + pub is_active: bool, +} + +impl From for ModelNewProfile { + fn from(value: NewProfile) -> Self { + Self { + user_id: value.user_id, + + thumbnail_url: value.thumbnail_url, + image_url: value.image_url, + + trainer_class: value.trainer_class, + nature: value.nature, + partner_pokemon: value.partner_pokemon, + starting_region: value.starting_region, + favourite_food: value.favourite_food, + likes: value.likes, + quotes: value.quotes, + + pokemon_go_code: value.pokemon_go_code, + pokemon_pocket_code: value.pokemon_pocket_code, + switch_code: value.switch_code, + + created_at: Utc::now().naive_utc(), + is_active: true, + } + } +} diff --git a/cipher_database/src/postgres/repository/user_repository.rs b/cipher_database/src/postgres/repository/user_repository.rs index b99735d..182a2a0 100644 --- a/cipher_database/src/postgres/repository/user_repository.rs +++ b/cipher_database/src/postgres/repository/user_repository.rs @@ -81,20 +81,6 @@ impl UserRepository for PostgresRepository<'_> { .map(|option| option.map(User::from)) .map_err(|err| RepositoryError(BackendError::from(err))) } - - async fn remove_user(&mut self, id: i32) -> Result, RepositoryError> { - self.conn - .transaction::<_, diesel::result::Error, _>(move |conn| async move { - diesel::delete(users::dsl::users.find(id)) - .returning(ModelUser::as_returning()) - .get_result(conn) - .await - .optional() - }.scope_boxed()) - .await - .map(|option| option.map(User::from)) - .map_err(|err| RepositoryError(BackendError::from(err))) - } } #[derive(Queryable, Selectable, AsChangeset)] @@ -104,9 +90,6 @@ impl UserRepository for PostgresRepository<'_> { struct ModelUser { id: i32, discord_user_id: i64, - pokemon_go_code: Option, - pokemon_pocket_code: Option, - switch_code: Option, } impl From for User { @@ -114,9 +97,6 @@ impl From for User { Self { id: value.id, discord_user_id: value.discord_user_id as u64, - pokemon_go_code: value.pokemon_go_code, - pokemon_pocket_code: value.pokemon_pocket_code, - switch_code: value.switch_code, } } } @@ -126,9 +106,6 @@ impl From for ModelUser { Self { id: value.id, discord_user_id: value.discord_user_id as i64, - pokemon_go_code: value.pokemon_go_code, - pokemon_pocket_code: value.pokemon_pocket_code, - switch_code: value.switch_code, } } } @@ -139,18 +116,12 @@ impl From for ModelUser { #[diesel(check_for_backend(diesel::pg::Pg))] struct ModelNewUser { discord_user_id: i64, - pokemon_go_code: Option, - pokemon_pocket_code: Option, - switch_code: Option, } impl From for ModelNewUser { fn from(value: NewUser) -> Self { Self { discord_user_id: value.discord_user_id as i64, - pokemon_go_code: value.pokemon_go_code, - pokemon_pocket_code: value.pokemon_pocket_code, - switch_code: value.switch_code, } } } diff --git a/cipher_database/src/postgres/schema.rs b/cipher_database/src/postgres/schema.rs index bfbcb2f..d9e327b 100644 --- a/cipher_database/src/postgres/schema.rs +++ b/cipher_database/src/postgres/schema.rs @@ -1,5 +1,29 @@ // @generated automatically by Diesel CLI. +diesel::table! { + profiles (id) { + id -> Int4, + user_id -> Int4, + thumbnail_url -> Nullable, + image_url -> Nullable, + trainer_class -> Nullable, + nature -> Nullable, + partner_pokemon -> Nullable, + starting_region -> Nullable, + favourite_food -> Nullable, + likes -> Nullable, + quotes -> Nullable, + #[max_length = 32] + pokemon_go_code -> Nullable, + #[max_length = 32] + pokemon_pocket_code -> Nullable, + #[max_length = 32] + switch_code -> Nullable, + created_at -> Timestamp, + is_active -> Bool, + } +} + diesel::table! { staff_roles (id) { id -> Int4, @@ -11,16 +35,13 @@ diesel::table! { users (id) { id -> Int4, discord_user_id -> Int8, - #[max_length = 32] - pokemon_go_code -> Nullable, - #[max_length = 32] - pokemon_pocket_code -> Nullable, - #[max_length = 32] - switch_code -> Nullable, } } +diesel::joinable!(profiles -> users (user_id)); + diesel::allow_tables_to_appear_in_same_query!( + profiles, staff_roles, users, ); diff --git a/cipher_database/src/sqlite/repository/mod.rs b/cipher_database/src/sqlite/repository/mod.rs index cf398d6..5310783 100644 --- a/cipher_database/src/sqlite/repository/mod.rs +++ b/cipher_database/src/sqlite/repository/mod.rs @@ -8,6 +8,7 @@ use cipher_core::repository::RepositoryProvider; use crate::BackendError; +mod profile_repository; mod staff_role_repository; mod user_repository; diff --git a/cipher_database/src/sqlite/repository/profile_repository.rs b/cipher_database/src/sqlite/repository/profile_repository.rs new file mode 100644 index 0000000..9c3aea6 --- /dev/null +++ b/cipher_database/src/sqlite/repository/profile_repository.rs @@ -0,0 +1,241 @@ +use chrono::DateTime; +use chrono::NaiveDateTime; +use chrono::Utc; +use cipher_core::repository::profile_repository::NewProfile; +use diesel_async::scoped_futures::ScopedFutureExt; +use diesel_async::AsyncConnection; +use diesel_async::RunQueryDsl; +use cipher_core::repository::profile_repository::Profile; +use cipher_core::repository::profile_repository::ProfileRepository; +use cipher_core::repository::RepositoryError; +use diesel::prelude::*; + +use crate::sqlite::schema::profiles; +use crate::sqlite::schema::users; +use crate::BackendError; + +use super::SqliteRepository; + +#[async_trait::async_trait] +impl ProfileRepository for SqliteRepository<'_> { + type BackendError = BackendError; + + async fn insert_profile(&mut self, new_profile: NewProfile) -> Result> { + let model_new_profile = ModelNewProfile::from(new_profile); + self.conn + .transaction::<_, diesel::result::Error, _>(|conn| async move { + diesel::update(profiles::table) + .filter(profiles::user_id.eq(model_new_profile.user_id)) + .set(profiles::is_active.eq(false)) + .execute(conn) + .await?; + + diesel::insert_into(profiles::table) + .values(&model_new_profile) + .returning(ModelProfile::as_returning()) + .get_result(conn) + .await + }.scope_boxed()) + .await + .map(Profile::from) + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn profile(&mut self, id: i32) -> Result, RepositoryError> { + profiles::dsl::profiles.find(id) + .first::(&mut self.conn) + .await + .map(Profile::from) + .optional() + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn active_profile(&mut self, user_id: i32) -> Result, RepositoryError> { + profiles::dsl::profiles + .filter(profiles::user_id.eq(user_id)) + .filter(profiles::is_active.eq(true)) + .first::(&mut self.conn) + .await + .map(Profile::from) + .optional() + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn active_profile_by_discord_id(&mut self, discord_user_id: u64) -> Result, RepositoryError> { + let model_discord_user_id = discord_user_id as i64; + profiles::table + .inner_join(users::table) + .filter(users::discord_user_id.eq(model_discord_user_id)) + .filter(profiles::is_active.eq(true)) + .select(ModelProfile::as_select()) + .first(&mut self.conn) + .await + .map(Profile::from) + .optional() + .map_err(|err| RepositoryError(BackendError::from(err))) + } + + async fn profiles_by_user_id(&mut self, user_id: i32) -> Result, RepositoryError> { + let results: Vec<_> = profiles::dsl::profiles + .filter(profiles::user_id.eq(user_id)) + .order(profiles::created_at.desc()) + .get_results::(&mut self.conn) + .await + .map_err(|err| RepositoryError(BackendError::from(err)))? + .into_iter() + .map(Profile::from) + .collect(); + + Ok(results) + } + + async fn profiles_by_discord_id(&mut self, discord_user_id: u64) -> Result, RepositoryError> { + let model_discord_user_id = discord_user_id as i64; + let results = profiles::table + .inner_join(users::table) + .filter(users::discord_user_id.eq(model_discord_user_id)) + .order(profiles::created_at.desc()) + .select(ModelProfile::as_select()) + .get_results(&mut self.conn) + .await + .map_err(|err| RepositoryError(BackendError::from(err)))? + .into_iter() + .map(Profile::from) + .collect(); + + Ok(results) + } + + async fn set_active_profile(&mut self, user_id: i32, profile_id: i32) -> Result> { + self.conn + .transaction::<_, diesel::result::Error, _>(move |conn| async move { + let num_affected = diesel::update(profiles::table.find(profile_id)) + .set(profiles::is_active.eq(true)) + .execute(conn) + .await?; + + if num_affected == 0 { + return Ok(false); + } + + diesel::update(profiles::table) + .filter(profiles::user_id.eq(user_id)) + .filter(profiles::id.ne(profile_id)) + .filter(profiles::is_active.eq(true)) + .set(profiles::is_active.eq(false)) + .execute(conn) + .await?; + + Ok(true) + }.scope_boxed()) + .await + .map_err(|err| RepositoryError(BackendError::from(err))) + } +} + +#[derive(Queryable, Selectable, AsChangeset)] +#[diesel(table_name = profiles)] +#[diesel(belongs_to(ModelUser))] +#[diesel(treat_none_as_null = true)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct ModelProfile { + pub id: i32, + pub user_id: i32, + + pub thumbnail_url: Option, + pub image_url: Option, + + pub trainer_class: Option, + pub nature: Option, + pub partner_pokemon: Option, + pub starting_region: Option, + pub favourite_food: Option, + pub likes: Option, + pub quotes: Option, + + pub pokemon_go_code: Option, + pub pokemon_pocket_code: Option, + pub switch_code: Option, + + pub created_at: NaiveDateTime, + pub is_active: bool, +} + +impl From for Profile { + fn from(value: ModelProfile) -> Self { + Self { + id: value.id, + user_id: value.user_id, + + thumbnail_url: value.thumbnail_url, + image_url: value.image_url, + + trainer_class: value.trainer_class, + nature: value.nature, + partner_pokemon: value.partner_pokemon, + starting_region: value.starting_region, + favourite_food: value.favourite_food, + likes: value.likes, + quotes: value.quotes, + + pokemon_go_code: value.pokemon_go_code, + pokemon_pocket_code: value.pokemon_pocket_code, + switch_code: value.switch_code, + + created_at: DateTime::from_naive_utc_and_offset(value.created_at, Utc), + is_active: value.is_active, + } + } +} + +#[derive(Insertable)] +#[diesel(table_name = profiles)] +#[diesel(treat_none_as_null = true)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct ModelNewProfile { + pub user_id: i32, + + pub thumbnail_url: Option, + pub image_url: Option, + + pub trainer_class: Option, + pub nature: Option, + pub partner_pokemon: Option, + pub starting_region: Option, + pub favourite_food: Option, + pub likes: Option, + pub quotes: Option, + + pub pokemon_go_code: Option, + pub pokemon_pocket_code: Option, + pub switch_code: Option, + + pub created_at: NaiveDateTime, + pub is_active: bool, +} + +impl From for ModelNewProfile { + fn from(value: NewProfile) -> Self { + Self { + user_id: value.user_id, + + thumbnail_url: value.thumbnail_url, + image_url: value.image_url, + + trainer_class: value.trainer_class, + nature: value.nature, + partner_pokemon: value.partner_pokemon, + starting_region: value.starting_region, + favourite_food: value.favourite_food, + likes: value.likes, + quotes: value.quotes, + + pokemon_go_code: value.pokemon_go_code, + pokemon_pocket_code: value.pokemon_pocket_code, + switch_code: value.switch_code, + + created_at: Utc::now().naive_utc(), + is_active: true, + } + } +} diff --git a/cipher_database/src/sqlite/repository/user_repository.rs b/cipher_database/src/sqlite/repository/user_repository.rs index f244d51..0c729c4 100644 --- a/cipher_database/src/sqlite/repository/user_repository.rs +++ b/cipher_database/src/sqlite/repository/user_repository.rs @@ -81,20 +81,6 @@ impl UserRepository for SqliteRepository<'_> { .map(|option| option.map(User::from)) .map_err(|err| RepositoryError(BackendError::from(err))) } - - async fn remove_user(&mut self, id: i32) -> Result, RepositoryError> { - self.conn - .transaction::<_, diesel::result::Error, _>(move |conn| async move { - diesel::delete(users::dsl::users.find(id)) - .returning(ModelUser::as_returning()) - .get_result(conn) - .await - .optional() - }.scope_boxed()) - .await - .map(|option| option.map(User::from)) - .map_err(|err| RepositoryError(BackendError::from(err))) - } } #[derive(Queryable, Selectable, AsChangeset)] @@ -104,9 +90,6 @@ impl UserRepository for SqliteRepository<'_> { struct ModelUser { id: i32, discord_user_id: i64, - pokemon_go_code: Option, - pokemon_pocket_code: Option, - switch_code: Option, } impl From for User { @@ -114,9 +97,6 @@ impl From for User { Self { id: value.id, discord_user_id: value.discord_user_id as u64, - pokemon_go_code: value.pokemon_go_code, - pokemon_pocket_code: value.pokemon_pocket_code, - switch_code: value.switch_code, } } } @@ -126,9 +106,6 @@ impl From for ModelUser { Self { id: value.id, discord_user_id: value.discord_user_id as i64, - pokemon_go_code: value.pokemon_go_code, - pokemon_pocket_code: value.pokemon_pocket_code, - switch_code: value.switch_code, } } } @@ -139,18 +116,12 @@ impl From for ModelUser { #[diesel(check_for_backend(diesel::sqlite::Sqlite))] struct ModelNewUser { discord_user_id: i64, - pokemon_go_code: Option, - pokemon_pocket_code: Option, - switch_code: Option, } impl From for ModelNewUser { fn from(value: NewUser) -> Self { Self { discord_user_id: value.discord_user_id as i64, - pokemon_go_code: value.pokemon_go_code, - pokemon_pocket_code: value.pokemon_pocket_code, - switch_code: value.switch_code, } } } diff --git a/cipher_database/src/sqlite/schema.rs b/cipher_database/src/sqlite/schema.rs index a5911e5..c0111bd 100644 --- a/cipher_database/src/sqlite/schema.rs +++ b/cipher_database/src/sqlite/schema.rs @@ -1,5 +1,26 @@ // @generated automatically by Diesel CLI. +diesel::table! { + profiles (id) { + id -> Integer, + user_id -> Integer, + thumbnail_url -> Nullable, + image_url -> Nullable, + trainer_class -> Nullable, + nature -> Nullable, + partner_pokemon -> Nullable, + starting_region -> Nullable, + favourite_food -> Nullable, + likes -> Nullable, + quotes -> Nullable, + pokemon_go_code -> Nullable, + pokemon_pocket_code -> Nullable, + switch_code -> Nullable, + created_at -> Timestamp, + is_active -> Bool, + } +} + diesel::table! { staff_roles (id) { id -> Integer, @@ -11,13 +32,13 @@ diesel::table! { users (id) { id -> Integer, discord_user_id -> BigInt, - pokemon_go_code -> Nullable, - pokemon_pocket_code -> Nullable, - switch_code -> Nullable, } } +diesel::joinable!(profiles -> users (user_id)); + diesel::allow_tables_to_appear_in_same_query!( + profiles, staff_roles, users, ); diff --git a/cipher_discord_bot/src/commands/profile/codes.rs b/cipher_discord_bot/src/commands/profile/codes.rs index 4c2aa7a..a6721f1 100644 --- a/cipher_discord_bot/src/commands/profile/codes.rs +++ b/cipher_discord_bot/src/commands/profile/codes.rs @@ -1,5 +1,6 @@ +use cipher_core::repository::profile_repository::NewProfile; +use cipher_core::repository::profile_repository::ProfileRepository; use cipher_core::repository::user_repository::NewUser; -use cipher_core::repository::user_repository::User; use cipher_core::repository::user_repository::UserRepository; use cipher_core::repository::RepositoryError; use cipher_core::repository::RepositoryProvider; @@ -176,11 +177,16 @@ where { let mut repo = ctx.data.repository().await?; - if let Some(mut user) = repo.user_by_discord_user_id(discord_user_id).await? { + let user = match repo.user_by_discord_user_id(discord_user_id).await? { + Some(user) => user, + None => repo.insert_user(NewUser { discord_user_id }).await?, + }; + + if let Some(mut profile) = repo.active_profile_by_discord_id(discord_user_id).await? { let defaults = EditCodesModal { - pokemon_go_code: user.pokemon_go_code.clone(), - pokemon_pocket_code: user.pokemon_pocket_code.clone(), - switch_code: user.switch_code.clone(), + pokemon_go_code: profile.pokemon_go_code.clone(), + pokemon_pocket_code: profile.pokemon_pocket_code.clone(), + switch_code: profile.switch_code.clone(), }; let mut data = match EditCodesModal::execute_with_defaults(ctx, defaults).await? { @@ -190,15 +196,11 @@ where data.validate().map_err(EditError::ValidationError)?; - user = User { - id: user.id, - discord_user_id: user.discord_user_id, - pokemon_go_code: data.pokemon_go_code, - pokemon_pocket_code: data.pokemon_pocket_code, - switch_code: data.switch_code, - }; + profile.pokemon_go_code = data.pokemon_go_code; + profile.pokemon_pocket_code = data.pokemon_pocket_code; + profile.switch_code = data.switch_code; - repo.update_user(user).await?; + repo.insert_profile(profile.into_new()).await?; } else { let mut data = match EditCodesModal::execute(ctx).await? { Some(data) => data, @@ -207,14 +209,26 @@ where data.validate().map_err(EditError::ValidationError)?; - let new_user = NewUser { - discord_user_id, + let new_profile = NewProfile { + user_id: user.id, + + thumbnail_url: None, + image_url: None, + + trainer_class: None, + nature: None, + partner_pokemon: None, + starting_region: None, + favourite_food: None, + likes: None, + quotes: None, + pokemon_go_code: data.pokemon_go_code, pokemon_pocket_code: data.pokemon_pocket_code, switch_code: data.switch_code, }; - repo.insert_user(new_user).await?; + repo.insert_profile(new_profile).await?; } Ok(()) diff --git a/cipher_discord_bot/src/commands/profile/mod.rs b/cipher_discord_bot/src/commands/profile/mod.rs index 551fa38..c5c427e 100644 --- a/cipher_discord_bot/src/commands/profile/mod.rs +++ b/cipher_discord_bot/src/commands/profile/mod.rs @@ -1,4 +1,4 @@ -use cipher_core::repository::user_repository::UserRepository; +use cipher_core::repository::profile_repository::ProfileRepository; use cipher_core::repository::RepositoryProvider; use poise::CreateReply; use serenity::all::CreateEmbed; @@ -66,8 +66,6 @@ async fn show_inner( member: Member, ephemeral: bool, ) -> Result<(), AppError> { - let mut repo = ctx.data.repository().await?; - let avatar_url = crate::utils::member_avatar_url(&member); let embed_color = match member.colour(ctx) { @@ -82,18 +80,19 @@ async fn show_inner( let mut is_profile_empty = true; - if let Some(user_info) = repo.user_by_discord_user_id(member.user.id.get()).await? { - if let Some(code) = user_info.pokemon_go_code { + let mut repo = ctx.data.repository().await?; + if let Some(profile) = repo.active_profile_by_discord_id(member.user.id.get()).await? { + if let Some(code) = profile.pokemon_go_code { embed = embed.field("Pokémon Go Friend Code", code, false); is_profile_empty = false; } - if let Some(code) = user_info.pokemon_pocket_code { + if let Some(code) = profile.pokemon_pocket_code { embed = embed.field("Pokémon TCG Pocket Friend Code", code, false); is_profile_empty = false; } - if let Some(code) = user_info.switch_code { + if let Some(code) = profile.switch_code { embed = embed.field("Nintendo Switch Friend Code", code, false); is_profile_empty = false; }