Add profiles table

This commit is contained in:
2025-02-07 02:32:31 +00:00
parent 124b1cf484
commit cf18044b95
27 changed files with 1194 additions and 150 deletions

5
Cargo.lock generated
View File

@@ -369,8 +369,10 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde", "serde",
"wasm-bindgen",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@@ -379,6 +381,7 @@ name = "cipher_core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono",
] ]
[[package]] [[package]]
@@ -386,6 +389,7 @@ name = "cipher_database"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono",
"cipher_core", "cipher_core",
"diesel", "diesel",
"diesel-async", "diesel-async",
@@ -710,6 +714,7 @@ checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"byteorder", "byteorder",
"chrono",
"diesel_derives", "diesel_derives",
"itoa", "itoa",
"libsqlite3-sys", "libsqlite3-sys",

View File

@@ -5,3 +5,4 @@ edition = "2021"
[dependencies] [dependencies]
async-trait = "0.1.85" async-trait = "0.1.85"
chrono = "0.4.39"

View File

@@ -1,8 +1,10 @@
use std::fmt::Display; use std::fmt::Display;
use profile_repository::ProfileRepository;
use staff_role_repository::StaffRoleRepository; use staff_role_repository::StaffRoleRepository;
use user_repository::UserRepository; use user_repository::UserRepository;
pub mod profile_repository;
pub mod staff_role_repository; pub mod staff_role_repository;
pub mod user_repository; pub mod user_repository;
@@ -19,6 +21,7 @@ pub trait RepositoryProvider {
pub trait Repository pub trait Repository
where where
Self: ProfileRepository<BackendError = <Self as Repository>::BackendError>,
Self: StaffRoleRepository<BackendError = <Self as Repository>::BackendError>, Self: StaffRoleRepository<BackendError = <Self as Repository>::BackendError>,
Self: UserRepository<BackendError = <Self as Repository>::BackendError>, Self: UserRepository<BackendError = <Self as Repository>::BackendError>,
{ {

View File

@@ -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<Self::BackendError>`, 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<Self::BackendError>)` - If the operation fails.
async fn insert_profile(&mut self, new_profile: NewProfile) -> Result<Profile, RepositoryError<Self::BackendError>>;
/// 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<Self::BackendError>)` - If an error occurs.
async fn profile(&mut self, id: i32) -> Result<Option<Profile>, RepositoryError<Self::BackendError>>;
/// 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<Self::BackendError>)` - If an error occurs.
async fn active_profile(&mut self, user_id: i32) -> Result<Option<Profile>, RepositoryError<Self::BackendError>>;
/// 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<Self::BackendError>)` - If an error occurs.
async fn active_profile_by_discord_id(&mut self, discord_user_id: u64) -> Result<Option<Profile>, RepositoryError<Self::BackendError>>;
/// 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<Profile>)` - A list of all past versions of the user's profile.
/// * `Err(RepositoryError<Self::BackendError>)` - If an error occurs.
async fn profiles_by_user_id(&mut self, user_id: i32) -> Result<Vec<Profile>, RepositoryError<Self::BackendError>>;
/// Retrieves the full profile history for a given Discord user.
///
/// # Arguments
/// * `discord_user_id` - The Discord user's unique identifier.
///
/// # Returns
/// * `Ok(Vec<Profile>)` - A list of all past versions of the user's profile.
/// * `Err(RepositoryError<Self::BackendError>)` - If an error occurs.
async fn profiles_by_discord_id(&mut self, discord_user_id: u64) -> Result<Vec<Profile>, RepositoryError<Self::BackendError>>;
/// 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<Self::BackendError>)` - If the operation fails.
async fn set_active_profile(&mut self, user_id: i32, profile_id: i32) -> Result<bool, RepositoryError<Self::BackendError>>;
}
pub struct Profile {
pub id: i32,
pub user_id: i32,
pub thumbnail_url: Option<String>,
pub image_url: Option<String>,
pub trainer_class: Option<String>,
pub nature: Option<String>,
pub partner_pokemon: Option<String>,
pub starting_region: Option<String>,
pub favourite_food: Option<String>,
pub likes: Option<String>,
pub quotes: Option<String>,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
pub created_at: DateTime<Utc>,
pub is_active: bool,
}
pub struct NewProfile {
pub user_id: i32,
pub thumbnail_url: Option<String>,
pub image_url: Option<String>,
pub trainer_class: Option<String>,
pub nature: Option<String>,
pub partner_pokemon: Option<String>,
pub starting_region: Option<String>,
pub favourite_food: Option<String>,
pub likes: Option<String>,
pub quotes: Option<String>,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
}
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,
}
}
}

View File

@@ -11,21 +11,13 @@ pub trait UserRepository {
async fn insert_user(&mut self, new_user: NewUser) -> Result<User, RepositoryError<Self::BackendError>>; async fn insert_user(&mut self, new_user: NewUser) -> Result<User, RepositoryError<Self::BackendError>>;
async fn update_user(&mut self, user: User) -> Result<Option<User>, RepositoryError<Self::BackendError>>; async fn update_user(&mut self, user: User) -> Result<Option<User>, RepositoryError<Self::BackendError>>;
async fn remove_user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>>;
} }
pub struct User { pub struct User {
pub id: i32, pub id: i32,
pub discord_user_id: u64, pub discord_user_id: u64,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
} }
pub struct NewUser { pub struct NewUser {
pub discord_user_id: u64, pub discord_user_id: u64,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
} }

View File

@@ -5,11 +5,12 @@ edition = "2021"
[dependencies] [dependencies]
async-trait = "0.1.85" 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-async = { version = "0.5.2", features = ["bb8"] }
diesel_migrations = "2.2.0" diesel_migrations = "2.2.0"
cipher_core = { path = "../cipher_core" } cipher_core = { path = "../cipher_core" }
thiserror = "2.0.11" thiserror = "2.0.11"
chrono = "0.4.39"
[features] [features]
default = ["mysql", "postgres", "sqlite"] default = ["mysql", "postgres", "sqlite"]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -7,6 +7,7 @@ use cipher_core::repository::RepositoryProvider;
use crate::BackendError; use crate::BackendError;
mod profile_repository;
mod staff_role_repository; mod staff_role_repository;
mod user_repository; mod user_repository;

View File

@@ -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<Profile, RepositoryError<Self::BackendError>> {
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<Option<Profile>, RepositoryError<Self::BackendError>> {
profiles::dsl::profiles.find(id)
.first::<ModelProfile>(&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<Option<Profile>, RepositoryError<Self::BackendError>> {
profiles::dsl::profiles
.filter(profiles::user_id.eq(user_id))
.filter(profiles::is_active.eq(true))
.first::<ModelProfile>(&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<Option<Profile>, RepositoryError<Self::BackendError>> {
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<Vec<Profile>, RepositoryError<Self::BackendError>> {
let results: Vec<_> = profiles::dsl::profiles
.filter(profiles::user_id.eq(user_id))
.order(profiles::created_at.desc())
.get_results::<ModelProfile>(&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<Vec<Profile>, RepositoryError<Self::BackendError>> {
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<bool, RepositoryError<Self::BackendError>> {
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<String>,
pub image_url: Option<String>,
pub trainer_class: Option<String>,
pub nature: Option<String>,
pub partner_pokemon: Option<String>,
pub starting_region: Option<String>,
pub favourite_food: Option<String>,
pub likes: Option<String>,
pub quotes: Option<String>,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
pub created_at: NaiveDateTime,
pub is_active: bool,
}
impl From<ModelProfile> 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<String>,
pub image_url: Option<String>,
pub trainer_class: Option<String>,
pub nature: Option<String>,
pub partner_pokemon: Option<String>,
pub starting_region: Option<String>,
pub favourite_food: Option<String>,
pub likes: Option<String>,
pub quotes: Option<String>,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
pub created_at: NaiveDateTime,
pub is_active: bool,
}
impl From<NewProfile> 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,
}
}
}

View File

@@ -91,7 +91,7 @@ impl StaffRoleRepository for MysqlRepository<'_> {
#[diesel(table_name = staff_roles)] #[diesel(table_name = staff_roles)]
#[diesel(treat_none_as_null = true)] #[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::mysql::Mysql))] #[diesel(check_for_backend(diesel::mysql::Mysql))]
struct ModelStaffRole { pub struct ModelStaffRole {
#[allow(unused)] #[allow(unused)]
id: i32, id: i32,
discord_role_id: i64, discord_role_id: i64,
@@ -101,6 +101,6 @@ struct ModelStaffRole {
#[diesel(table_name = staff_roles)] #[diesel(table_name = staff_roles)]
#[diesel(treat_none_as_null = true)] #[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::mysql::Mysql))] #[diesel(check_for_backend(diesel::mysql::Mysql))]
struct ModelNewStaffRole { pub struct ModelNewStaffRole {
discord_role_id: i64, discord_role_id: i64,
} }

View File

@@ -86,44 +86,15 @@ impl UserRepository for MysqlRepository<'_> {
.map(|option| option.map(User::from)) .map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err))) .map_err(|err| RepositoryError(BackendError::from(err)))
} }
async fn remove_user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
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)] #[derive(Queryable, Selectable, AsChangeset)]
#[diesel(table_name = users)] #[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)] #[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::mysql::Mysql))] #[diesel(check_for_backend(diesel::mysql::Mysql))]
struct ModelUser { pub struct ModelUser {
id: i32, id: i32,
discord_user_id: i64, discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
} }
impl From<ModelUser> for User { impl From<ModelUser> for User {
@@ -131,9 +102,6 @@ impl From<ModelUser> for User {
Self { Self {
id: value.id, id: value.id,
discord_user_id: value.discord_user_id as u64, 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<User> for ModelUser {
Self { Self {
id: value.id, id: value.id,
discord_user_id: value.discord_user_id as i64, 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<User> for ModelUser {
#[diesel(table_name = users)] #[diesel(table_name = users)]
#[diesel(treat_none_as_null = true)] #[diesel(treat_none_as_null = true)]
#[diesel(check_for_backend(diesel::mysql::Mysql))] #[diesel(check_for_backend(diesel::mysql::Mysql))]
struct ModelNewUser { pub struct ModelNewUser {
discord_user_id: i64, discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
} }
impl From<NewUser> for ModelNewUser { impl From<NewUser> for ModelNewUser {
fn from(value: NewUser) -> Self { fn from(value: NewUser) -> Self {
Self { Self {
discord_user_id: value.discord_user_id as i64, 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,
} }
} }
} }

View File

@@ -1,5 +1,29 @@
// @generated automatically by Diesel CLI. // @generated automatically by Diesel CLI.
diesel::table! {
profiles (id) {
id -> Integer,
user_id -> Integer,
thumbnail_url -> Nullable<Text>,
image_url -> Nullable<Text>,
trainer_class -> Nullable<Text>,
nature -> Nullable<Text>,
partner_pokemon -> Nullable<Text>,
starting_region -> Nullable<Text>,
favourite_food -> Nullable<Text>,
likes -> Nullable<Text>,
quotes -> Nullable<Text>,
#[max_length = 32]
pokemon_go_code -> Nullable<Varchar>,
#[max_length = 32]
pokemon_pocket_code -> Nullable<Varchar>,
#[max_length = 32]
switch_code -> Nullable<Varchar>,
created_at -> Timestamp,
is_active -> Bool,
}
}
diesel::table! { diesel::table! {
staff_roles (id) { staff_roles (id) {
id -> Integer, id -> Integer,
@@ -11,16 +35,13 @@ diesel::table! {
users (id) { users (id) {
id -> Integer, id -> Integer,
discord_user_id -> Bigint, discord_user_id -> Bigint,
#[max_length = 32]
pokemon_go_code -> Nullable<Varchar>,
#[max_length = 32]
pokemon_pocket_code -> Nullable<Varchar>,
#[max_length = 32]
switch_code -> Nullable<Varchar>,
} }
} }
diesel::joinable!(profiles -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
profiles,
staff_roles, staff_roles,
users, users,
); );

View File

@@ -7,6 +7,7 @@ use cipher_core::repository::RepositoryProvider;
use crate::BackendError; use crate::BackendError;
mod profile_repository;
mod staff_role_repository; mod staff_role_repository;
mod user_repository; mod user_repository;

View File

@@ -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<Profile, RepositoryError<Self::BackendError>> {
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<Option<Profile>, RepositoryError<Self::BackendError>> {
profiles::dsl::profiles.find(id)
.first::<ModelProfile>(&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<Option<Profile>, RepositoryError<Self::BackendError>> {
profiles::dsl::profiles
.filter(profiles::user_id.eq(user_id))
.filter(profiles::is_active.eq(true))
.first::<ModelProfile>(&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<Option<Profile>, RepositoryError<Self::BackendError>> {
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<Vec<Profile>, RepositoryError<Self::BackendError>> {
let results: Vec<_> = profiles::dsl::profiles
.filter(profiles::user_id.eq(user_id))
.order(profiles::created_at.desc())
.get_results::<ModelProfile>(&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<Vec<Profile>, RepositoryError<Self::BackendError>> {
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<bool, RepositoryError<Self::BackendError>> {
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<String>,
pub image_url: Option<String>,
pub trainer_class: Option<String>,
pub nature: Option<String>,
pub partner_pokemon: Option<String>,
pub starting_region: Option<String>,
pub favourite_food: Option<String>,
pub likes: Option<String>,
pub quotes: Option<String>,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
pub created_at: NaiveDateTime,
pub is_active: bool,
}
impl From<ModelProfile> 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<String>,
pub image_url: Option<String>,
pub trainer_class: Option<String>,
pub nature: Option<String>,
pub partner_pokemon: Option<String>,
pub starting_region: Option<String>,
pub favourite_food: Option<String>,
pub likes: Option<String>,
pub quotes: Option<String>,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
pub created_at: NaiveDateTime,
pub is_active: bool,
}
impl From<NewProfile> 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,
}
}
}

View File

@@ -81,20 +81,6 @@ impl UserRepository for PostgresRepository<'_> {
.map(|option| option.map(User::from)) .map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err))) .map_err(|err| RepositoryError(BackendError::from(err)))
} }
async fn remove_user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
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)] #[derive(Queryable, Selectable, AsChangeset)]
@@ -104,9 +90,6 @@ impl UserRepository for PostgresRepository<'_> {
struct ModelUser { struct ModelUser {
id: i32, id: i32,
discord_user_id: i64, discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
} }
impl From<ModelUser> for User { impl From<ModelUser> for User {
@@ -114,9 +97,6 @@ impl From<ModelUser> for User {
Self { Self {
id: value.id, id: value.id,
discord_user_id: value.discord_user_id as u64, 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<User> for ModelUser {
Self { Self {
id: value.id, id: value.id,
discord_user_id: value.discord_user_id as i64, 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<User> for ModelUser {
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
struct ModelNewUser { struct ModelNewUser {
discord_user_id: i64, discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
} }
impl From<NewUser> for ModelNewUser { impl From<NewUser> for ModelNewUser {
fn from(value: NewUser) -> Self { fn from(value: NewUser) -> Self {
Self { Self {
discord_user_id: value.discord_user_id as i64, 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,
} }
} }
} }

View File

@@ -1,5 +1,29 @@
// @generated automatically by Diesel CLI. // @generated automatically by Diesel CLI.
diesel::table! {
profiles (id) {
id -> Int4,
user_id -> Int4,
thumbnail_url -> Nullable<Text>,
image_url -> Nullable<Text>,
trainer_class -> Nullable<Text>,
nature -> Nullable<Text>,
partner_pokemon -> Nullable<Text>,
starting_region -> Nullable<Text>,
favourite_food -> Nullable<Text>,
likes -> Nullable<Text>,
quotes -> Nullable<Text>,
#[max_length = 32]
pokemon_go_code -> Nullable<Varchar>,
#[max_length = 32]
pokemon_pocket_code -> Nullable<Varchar>,
#[max_length = 32]
switch_code -> Nullable<Varchar>,
created_at -> Timestamp,
is_active -> Bool,
}
}
diesel::table! { diesel::table! {
staff_roles (id) { staff_roles (id) {
id -> Int4, id -> Int4,
@@ -11,16 +35,13 @@ diesel::table! {
users (id) { users (id) {
id -> Int4, id -> Int4,
discord_user_id -> Int8, discord_user_id -> Int8,
#[max_length = 32]
pokemon_go_code -> Nullable<Varchar>,
#[max_length = 32]
pokemon_pocket_code -> Nullable<Varchar>,
#[max_length = 32]
switch_code -> Nullable<Varchar>,
} }
} }
diesel::joinable!(profiles -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
profiles,
staff_roles, staff_roles,
users, users,
); );

View File

@@ -8,6 +8,7 @@ use cipher_core::repository::RepositoryProvider;
use crate::BackendError; use crate::BackendError;
mod profile_repository;
mod staff_role_repository; mod staff_role_repository;
mod user_repository; mod user_repository;

View File

@@ -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<Profile, RepositoryError<Self::BackendError>> {
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<Option<Profile>, RepositoryError<Self::BackendError>> {
profiles::dsl::profiles.find(id)
.first::<ModelProfile>(&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<Option<Profile>, RepositoryError<Self::BackendError>> {
profiles::dsl::profiles
.filter(profiles::user_id.eq(user_id))
.filter(profiles::is_active.eq(true))
.first::<ModelProfile>(&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<Option<Profile>, RepositoryError<Self::BackendError>> {
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<Vec<Profile>, RepositoryError<Self::BackendError>> {
let results: Vec<_> = profiles::dsl::profiles
.filter(profiles::user_id.eq(user_id))
.order(profiles::created_at.desc())
.get_results::<ModelProfile>(&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<Vec<Profile>, RepositoryError<Self::BackendError>> {
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<bool, RepositoryError<Self::BackendError>> {
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<String>,
pub image_url: Option<String>,
pub trainer_class: Option<String>,
pub nature: Option<String>,
pub partner_pokemon: Option<String>,
pub starting_region: Option<String>,
pub favourite_food: Option<String>,
pub likes: Option<String>,
pub quotes: Option<String>,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
pub created_at: NaiveDateTime,
pub is_active: bool,
}
impl From<ModelProfile> 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<String>,
pub image_url: Option<String>,
pub trainer_class: Option<String>,
pub nature: Option<String>,
pub partner_pokemon: Option<String>,
pub starting_region: Option<String>,
pub favourite_food: Option<String>,
pub likes: Option<String>,
pub quotes: Option<String>,
pub pokemon_go_code: Option<String>,
pub pokemon_pocket_code: Option<String>,
pub switch_code: Option<String>,
pub created_at: NaiveDateTime,
pub is_active: bool,
}
impl From<NewProfile> 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,
}
}
}

View File

@@ -81,20 +81,6 @@ impl UserRepository for SqliteRepository<'_> {
.map(|option| option.map(User::from)) .map(|option| option.map(User::from))
.map_err(|err| RepositoryError(BackendError::from(err))) .map_err(|err| RepositoryError(BackendError::from(err)))
} }
async fn remove_user(&mut self, id: i32) -> Result<Option<User>, RepositoryError<Self::BackendError>> {
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)] #[derive(Queryable, Selectable, AsChangeset)]
@@ -104,9 +90,6 @@ impl UserRepository for SqliteRepository<'_> {
struct ModelUser { struct ModelUser {
id: i32, id: i32,
discord_user_id: i64, discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
} }
impl From<ModelUser> for User { impl From<ModelUser> for User {
@@ -114,9 +97,6 @@ impl From<ModelUser> for User {
Self { Self {
id: value.id, id: value.id,
discord_user_id: value.discord_user_id as u64, 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<User> for ModelUser {
Self { Self {
id: value.id, id: value.id,
discord_user_id: value.discord_user_id as i64, 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<User> for ModelUser {
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
struct ModelNewUser { struct ModelNewUser {
discord_user_id: i64, discord_user_id: i64,
pokemon_go_code: Option<String>,
pokemon_pocket_code: Option<String>,
switch_code: Option<String>,
} }
impl From<NewUser> for ModelNewUser { impl From<NewUser> for ModelNewUser {
fn from(value: NewUser) -> Self { fn from(value: NewUser) -> Self {
Self { Self {
discord_user_id: value.discord_user_id as i64, 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,
} }
} }
} }

View File

@@ -1,5 +1,26 @@
// @generated automatically by Diesel CLI. // @generated automatically by Diesel CLI.
diesel::table! {
profiles (id) {
id -> Integer,
user_id -> Integer,
thumbnail_url -> Nullable<Text>,
image_url -> Nullable<Text>,
trainer_class -> Nullable<Text>,
nature -> Nullable<Text>,
partner_pokemon -> Nullable<Text>,
starting_region -> Nullable<Text>,
favourite_food -> Nullable<Text>,
likes -> Nullable<Text>,
quotes -> Nullable<Text>,
pokemon_go_code -> Nullable<Text>,
pokemon_pocket_code -> Nullable<Text>,
switch_code -> Nullable<Text>,
created_at -> Timestamp,
is_active -> Bool,
}
}
diesel::table! { diesel::table! {
staff_roles (id) { staff_roles (id) {
id -> Integer, id -> Integer,
@@ -11,13 +32,13 @@ diesel::table! {
users (id) { users (id) {
id -> Integer, id -> Integer,
discord_user_id -> BigInt, discord_user_id -> BigInt,
pokemon_go_code -> Nullable<Text>,
pokemon_pocket_code -> Nullable<Text>,
switch_code -> Nullable<Text>,
} }
} }
diesel::joinable!(profiles -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
profiles,
staff_roles, staff_roles,
users, users,
); );

View File

@@ -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::NewUser;
use cipher_core::repository::user_repository::User;
use cipher_core::repository::user_repository::UserRepository; use cipher_core::repository::user_repository::UserRepository;
use cipher_core::repository::RepositoryError; use cipher_core::repository::RepositoryError;
use cipher_core::repository::RepositoryProvider; use cipher_core::repository::RepositoryProvider;
@@ -176,11 +177,16 @@ where
{ {
let mut repo = ctx.data.repository().await?; 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 { let defaults = EditCodesModal {
pokemon_go_code: user.pokemon_go_code.clone(), pokemon_go_code: profile.pokemon_go_code.clone(),
pokemon_pocket_code: user.pokemon_pocket_code.clone(), pokemon_pocket_code: profile.pokemon_pocket_code.clone(),
switch_code: user.switch_code.clone(), switch_code: profile.switch_code.clone(),
}; };
let mut data = match EditCodesModal::execute_with_defaults(ctx, defaults).await? { let mut data = match EditCodesModal::execute_with_defaults(ctx, defaults).await? {
@@ -190,15 +196,11 @@ where
data.validate().map_err(EditError::ValidationError)?; data.validate().map_err(EditError::ValidationError)?;
user = User { profile.pokemon_go_code = data.pokemon_go_code;
id: user.id, profile.pokemon_pocket_code = data.pokemon_pocket_code;
discord_user_id: user.discord_user_id, profile.switch_code = data.switch_code;
pokemon_go_code: data.pokemon_go_code,
pokemon_pocket_code: data.pokemon_pocket_code,
switch_code: data.switch_code,
};
repo.update_user(user).await?; repo.insert_profile(profile.into_new()).await?;
} else { } else {
let mut data = match EditCodesModal::execute(ctx).await? { let mut data = match EditCodesModal::execute(ctx).await? {
Some(data) => data, Some(data) => data,
@@ -207,14 +209,26 @@ where
data.validate().map_err(EditError::ValidationError)?; data.validate().map_err(EditError::ValidationError)?;
let new_user = NewUser { let new_profile = NewProfile {
discord_user_id, 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_go_code: data.pokemon_go_code,
pokemon_pocket_code: data.pokemon_pocket_code, pokemon_pocket_code: data.pokemon_pocket_code,
switch_code: data.switch_code, switch_code: data.switch_code,
}; };
repo.insert_user(new_user).await?; repo.insert_profile(new_profile).await?;
} }
Ok(()) Ok(())

View File

@@ -1,4 +1,4 @@
use cipher_core::repository::user_repository::UserRepository; use cipher_core::repository::profile_repository::ProfileRepository;
use cipher_core::repository::RepositoryProvider; use cipher_core::repository::RepositoryProvider;
use poise::CreateReply; use poise::CreateReply;
use serenity::all::CreateEmbed; use serenity::all::CreateEmbed;
@@ -66,8 +66,6 @@ async fn show_inner<R: RepositoryProvider + Send + Sync>(
member: Member, member: Member,
ephemeral: bool, ephemeral: bool,
) -> Result<(), AppError<R::BackendError>> { ) -> Result<(), AppError<R::BackendError>> {
let mut repo = ctx.data.repository().await?;
let avatar_url = crate::utils::member_avatar_url(&member); let avatar_url = crate::utils::member_avatar_url(&member);
let embed_color = match member.colour(ctx) { let embed_color = match member.colour(ctx) {
@@ -82,18 +80,19 @@ async fn show_inner<R: RepositoryProvider + Send + Sync>(
let mut is_profile_empty = true; let mut is_profile_empty = true;
if let Some(user_info) = repo.user_by_discord_user_id(member.user.id.get()).await? { let mut repo = ctx.data.repository().await?;
if let Some(code) = user_info.pokemon_go_code { 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); embed = embed.field("Pokémon Go Friend Code", code, false);
is_profile_empty = 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); embed = embed.field("Pokémon TCG Pocket Friend Code", code, false);
is_profile_empty = 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); embed = embed.field("Nintendo Switch Friend Code", code, false);
is_profile_empty = false; is_profile_empty = false;
} }