diff --git a/Cargo.toml b/Cargo.toml index d770cbf..0d0abb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,5 @@ sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "chron validator = { version = "0.20.0", features = ["derive"] } regex = "1.0" -lazy_static = "1.4" \ No newline at end of file +lazy_static = "1.4" +uuid = "1.18.1" diff --git a/src/models/beatmap/beatmap/impl.rs b/src/models/beatmap/beatmap/impl.rs new file mode 100644 index 0000000..cf72fb0 --- /dev/null +++ b/src/models/beatmap/beatmap/impl.rs @@ -0,0 +1,17 @@ +use super::query::{find_by_id, find_by_osu_id, insert}; +use super::BeatmapRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl BeatmapRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } + + pub async fn find_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, SqlxError> { + find_by_osu_id(pool, osu_id).await + } +} diff --git a/src/models/score_rating/mod.rs b/src/models/beatmap/beatmap/mod.rs similarity index 77% rename from src/models/score_rating/mod.rs rename to src/models/beatmap/beatmap/mod.rs index 7134bd1..3e70365 100644 --- a/src/models/score_rating/mod.rs +++ b/src/models/beatmap/beatmap/mod.rs @@ -6,4 +6,4 @@ pub mod validators; #[cfg(test)] mod tests; -pub use types::*; +pub use types::BeatmapRow; diff --git a/src/models/beatmap/beatmap/query/by_id.rs b/src/models/beatmap/beatmap/query/by_id.rs new file mode 100644 index 0000000..fa66e38 --- /dev/null +++ b/src/models/beatmap/beatmap/query/by_id.rs @@ -0,0 +1,30 @@ +use crate::models::beatmap::beatmap::types::BeatmapRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + BeatmapRow, + r#" + SELECT id, osu_id, beatmapset_id, difficulty, count_circles, count_sliders, count_spinners, max_combo, main_pattern, cs, ar, od, hp, mode, status, created_at, updated_at + FROM beatmap + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} + +pub async fn find_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, SqlxError> { + sqlx::query_as!( + BeatmapRow, + r#" + SELECT id, osu_id, beatmapset_id, difficulty, count_circles, count_sliders, count_spinners, max_combo, main_pattern, cs, ar, od, hp, mode, status, created_at, updated_at + FROM beatmap + WHERE osu_id = $1 + "#, + osu_id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/beatmap/query/insert.rs b/src/models/beatmap/beatmap/query/insert.rs similarity index 64% rename from src/models/beatmap/query/insert.rs rename to src/models/beatmap/beatmap/query/insert.rs index 5cac24f..825129d 100644 --- a/src/models/beatmap/query/insert.rs +++ b/src/models/beatmap/beatmap/query/insert.rs @@ -1,5 +1,6 @@ use crate::define_insert_returning_id; -use crate::models::beatmap::types::BeatmapRow; +use crate::models::beatmap::beatmap::types::BeatmapRow; +// no extra imports needed define_insert_returning_id!( insert, @@ -8,20 +9,15 @@ define_insert_returning_id!( osu_id, beatmapset_id, difficulty, - difficulty_rating, count_circles, count_sliders, count_spinners, max_combo, - drain_time, - total_time, - bpm, + main_pattern, cs, ar, od, hp, mode, - status, - file_md5, - file_path + status ); diff --git a/src/models/score_rating/query/mod.rs b/src/models/beatmap/beatmap/query/mod.rs similarity index 60% rename from src/models/score_rating/query/mod.rs rename to src/models/beatmap/beatmap/query/mod.rs index 47b13ed..a9a3d33 100644 --- a/src/models/score_rating/query/mod.rs +++ b/src/models/beatmap/beatmap/query/mod.rs @@ -1,7 +1,5 @@ pub mod by_id; -pub mod by_score_id; pub mod insert; pub use by_id::*; -pub use by_score_id::*; pub use insert::*; diff --git a/src/models/beatmap/beatmap/tests/mod.rs b/src/models/beatmap/beatmap/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/beatmap/beatmap/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/beatmap/beatmap/types.rs b/src/models/beatmap/beatmap/types.rs new file mode 100644 index 0000000..f0fc4f6 --- /dev/null +++ b/src/models/beatmap/beatmap/types.rs @@ -0,0 +1,91 @@ +use super::validators::{validate_ar, validate_od_hp_cs, validate_status}; +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use serde_json::Value; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct BeatmapRow { + /// Unique identifier for the beatmap record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Osu beatmap ID from the official osu! API. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Osu ID must be positive"))] + pub osu_id: Option, + + /// Reference to the beatmapset this beatmap belongs to. + /// Optional field, can be None. + pub beatmapset_id: Option, + + /// Difficulty name of the beatmap. + /// Must be between 1 and 255 characters. + #[validate(length( + min = 1, + max = 255, + message = "Difficulty must be between 1 and 255 characters" + ))] + pub difficulty: String, + + /// Number of circles in the beatmap. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Count circles must be non-negative"))] + pub count_circles: i32, + + /// Number of sliders in the beatmap. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Count sliders must be non-negative"))] + pub count_sliders: i32, + + /// Number of spinners in the beatmap. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Count spinners must be non-negative"))] + pub count_spinners: i32, + + /// Maximum combo possible in the beatmap. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Max combo must be non-negative"))] + pub max_combo: i32, + + /// Main pattern data stored as JSON. + /// Must not be null. + pub main_pattern: Value, + + /// Circle Size (CS) value. + /// Must be between 0.0 and 10.0. + #[validate(custom(function = "validate_od_hp_cs"))] + pub cs: BigDecimal, + + /// Approach Rate (AR) value. + /// Must be between 0.0 and 10.0. + #[validate(custom(function = "validate_ar"))] + pub ar: BigDecimal, + + /// Overall Difficulty (OD) value. + /// Must be between 0.0 and 10.0. + #[validate(custom(function = "validate_od_hp_cs"))] + pub od: BigDecimal, + + /// HP Drain (HP) value. + /// Must be between 0.0 and 10.0. + #[validate(custom(function = "validate_od_hp_cs"))] + pub hp: BigDecimal, + + /// Game mode (0=osu!, 1=Taiko, 2=Catch, 3=Mania). + /// Must be between 0 and 3. + #[validate(range(min = 0, max = 3, message = "Mode must be between 0 and 3"))] + pub mode: i32, + + /// Status of the beatmap. + /// Must be one of: 'pending', 'ranked', 'qualified', 'loved', 'graveyard'. + #[validate(custom(function = "validate_status"))] + pub status: String, + + /// Timestamp when the beatmap was created. + pub created_at: Option, + + /// Timestamp when the beatmap was last updated. + pub updated_at: Option, +} diff --git a/src/models/beatmap/beatmap/validators.rs b/src/models/beatmap/beatmap/validators.rs new file mode 100644 index 0000000..f37a5c9 --- /dev/null +++ b/src/models/beatmap/beatmap/validators.rs @@ -0,0 +1,23 @@ +use bigdecimal::{BigDecimal, FromPrimitive}; +use validator::ValidationError; + +pub fn validate_status(status: &str) -> Result<(), ValidationError> { + match status { + "pending" | "ranked" | "qualified" | "loved" | "graveyard" => Ok(()), + _ => Err(validator::ValidationError::new("invalid_status")), + } +} + +pub fn validate_od_hp_cs(value: &BigDecimal) -> Result<(), ValidationError> { + if value < &BigDecimal::from_f64(0.0).unwrap() || value > &BigDecimal::from_f64(10.0).unwrap() { + return Err(validator::ValidationError::new("invalid_od_hp_cs")); + } + Ok(()) +} + +pub fn validate_ar(value: &BigDecimal) -> Result<(), ValidationError> { + if value < &BigDecimal::from_f64(0.0).unwrap() || value > &BigDecimal::from_f64(11.0).unwrap() { + return Err(validator::ValidationError::new("invalid_ar")); + } + Ok(()) +} diff --git a/src/models/beatmap/beatmapset/impl.rs b/src/models/beatmap/beatmapset/impl.rs new file mode 100644 index 0000000..3a51a76 --- /dev/null +++ b/src/models/beatmap/beatmapset/impl.rs @@ -0,0 +1,17 @@ +use super::query::{find_by_id, find_by_osu_id, insert}; +use super::BeatmapsetRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl BeatmapsetRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } + + pub async fn find_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, SqlxError> { + find_by_osu_id(pool, osu_id).await + } +} diff --git a/src/models/beatmapset/mod.rs b/src/models/beatmap/beatmapset/mod.rs similarity index 81% rename from src/models/beatmapset/mod.rs rename to src/models/beatmap/beatmapset/mod.rs index e576b64..9da66cc 100644 --- a/src/models/beatmapset/mod.rs +++ b/src/models/beatmap/beatmapset/mod.rs @@ -1,7 +1,6 @@ pub mod r#impl; pub mod query; pub mod types; -pub(super) mod validators; #[cfg(test)] mod tests; diff --git a/src/models/beatmap/beatmapset/query/by_id.rs b/src/models/beatmap/beatmapset/query/by_id.rs new file mode 100644 index 0000000..2ff8757 --- /dev/null +++ b/src/models/beatmap/beatmapset/query/by_id.rs @@ -0,0 +1,33 @@ +use crate::models::beatmap::beatmapset::types::BeatmapsetRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + BeatmapsetRow, + r#" + SELECT id, osu_id, artist, artist_unicode, title, title_unicode, creator, source, tags, has_video, has_storyboard, is_explicit, is_featured, cover_url, preview_url, osu_file_url, created_at, updated_at + FROM beatmapset + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} + +pub async fn find_by_osu_id( + pool: &PgPool, + osu_id: i32, +) -> Result, SqlxError> { + sqlx::query_as!( + BeatmapsetRow, + r#" + SELECT id, osu_id, artist, artist_unicode, title, title_unicode, creator, source, tags, has_video, has_storyboard, is_explicit, is_featured, cover_url, preview_url, osu_file_url, created_at, updated_at + FROM beatmapset + WHERE osu_id = $1 + "#, + osu_id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/beatmap/beatmapset/query/insert.rs b/src/models/beatmap/beatmapset/query/insert.rs new file mode 100644 index 0000000..d7b6863 --- /dev/null +++ b/src/models/beatmap/beatmapset/query/insert.rs @@ -0,0 +1,24 @@ +use crate::define_insert_returning_id; +use crate::models::beatmap::beatmapset::types::BeatmapsetRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "beatmapset", + BeatmapsetRow, + osu_id, + artist, + artist_unicode, + title, + title_unicode, + creator, + source, + tags, + has_video, + has_storyboard, + is_explicit, + is_featured, + cover_url, + preview_url, + osu_file_url +); diff --git a/src/models/beatmap/beatmapset/query/mod.rs b/src/models/beatmap/beatmapset/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/beatmap/beatmapset/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/beatmap/beatmapset/tests/mod.rs b/src/models/beatmap/beatmapset/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/beatmap/beatmapset/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/beatmap/beatmapset/types.rs b/src/models/beatmap/beatmapset/types.rs new file mode 100644 index 0000000..01f0b29 --- /dev/null +++ b/src/models/beatmap/beatmapset/types.rs @@ -0,0 +1,88 @@ +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct BeatmapsetRow { + /// Unique identifier for the beatmapset record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Osu beatmapset ID from the official osu! API. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Osu ID must be positive"))] + pub osu_id: Option, + + /// Artist name of the beatmapset. + /// Must be between 1 and 255 characters. + #[validate(length( + min = 1, + max = 255, + message = "Artist must be between 1 and 255 characters" + ))] + pub artist: String, + + /// Unicode artist name of the beatmapset. + /// Optional field, can be None. + pub artist_unicode: Option, + + /// Title of the beatmapset. + /// Must be between 1 and 255 characters. + #[validate(length( + min = 1, + max = 255, + message = "Title must be between 1 and 255 characters" + ))] + pub title: String, + + /// Unicode title of the beatmapset. + /// Optional field, can be None. + pub title_unicode: Option, + + /// Creator/mapper of the beatmapset. + /// Must be between 1 and 255 characters. + #[validate(length( + min = 1, + max = 255, + message = "Creator must be between 1 and 255 characters" + ))] + pub creator: String, + + /// Source of the beatmapset (e.g., anime, game, etc.). + /// Optional field, can be None. + pub source: Option, + + /// Tags associated with the beatmapset. + /// Optional field, can be None. + pub tags: Option>, + + /// Whether the beatmapset has a video. + pub has_video: bool, + + /// Whether the beatmapset has a storyboard. + pub has_storyboard: bool, + + /// Whether the beatmapset contains explicit content. + pub is_explicit: bool, + + /// Whether the beatmapset is featured. + pub is_featured: bool, + + /// URL to the cover image. + /// Optional field, can be None. + pub cover_url: Option, + + /// URL to the preview audio. + /// Optional field, can be None. + pub preview_url: Option, + + /// URL to the osu file. + /// Optional field, can be None. + pub osu_file_url: Option, + + /// Timestamp when the beatmapset was created. + pub created_at: Option, + + /// Timestamp when the beatmapset was last updated. + pub updated_at: Option, +} diff --git a/src/models/beatmap/impl.rs b/src/models/beatmap/impl.rs deleted file mode 100644 index 0dcdae5..0000000 --- a/src/models/beatmap/impl.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::query::{exists_by_checksum, find_by_beatmapset_id, find_by_id, insert::insert}; -use super::types::BeatmapRow; -use sqlx::PgPool; - -impl BeatmapRow { - pub async fn insert_into_db(self, pool: &PgPool) -> Result { - insert(pool, self).await - } - - pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, sqlx::Error> { - find_by_id(pool, id).await - } - - pub async fn exists_by_checksum(pool: &PgPool, checksum: &str) -> Result { - exists_by_checksum(pool, checksum).await - } - - pub async fn find_by_beatmapset_id( - pool: &PgPool, - beatmap_id: i32, - ) -> Result, sqlx::Error> { - find_by_beatmapset_id(pool, beatmap_id).await - } -} diff --git a/src/models/beatmap/mod.rs b/src/models/beatmap/mod.rs index 29d2209..593dbf6 100644 --- a/src/models/beatmap/mod.rs +++ b/src/models/beatmap/mod.rs @@ -1,10 +1,4 @@ -pub mod r#impl; -pub mod query; -pub mod types; -pub(super) mod validators; - -#[cfg(test)] -mod tests; - -pub use query::*; -pub use types::BeatmapRow; +pub mod beatmap; +pub mod beatmapset; +pub mod pending_beatmap; +pub mod rates; diff --git a/src/models/beatmap/pending_beatmap/impl.rs b/src/models/beatmap/pending_beatmap/impl.rs new file mode 100644 index 0000000..5c66128 --- /dev/null +++ b/src/models/beatmap/pending_beatmap/impl.rs @@ -0,0 +1,17 @@ +use super::query::{find_by_hash, find_by_id, insert}; +use super::PendingBeatmapRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl PendingBeatmapRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } + + pub async fn find_by_hash(pool: &PgPool, osu_hash: &str) -> Result, SqlxError> { + find_by_hash(pool, osu_hash).await + } +} diff --git a/src/models/pending_beatmap/mod.rs b/src/models/beatmap/pending_beatmap/mod.rs similarity index 66% rename from src/models/pending_beatmap/mod.rs rename to src/models/beatmap/pending_beatmap/mod.rs index 23aa96a..1aaaaae 100644 --- a/src/models/pending_beatmap/mod.rs +++ b/src/models/beatmap/pending_beatmap/mod.rs @@ -5,5 +5,4 @@ pub mod types; #[cfg(test)] mod tests; -pub use query::*; -pub use types::*; +pub use types::PendingBeatmapRow; diff --git a/src/models/beatmap/pending_beatmap/query/by_id.rs b/src/models/beatmap/pending_beatmap/query/by_id.rs new file mode 100644 index 0000000..895b66e --- /dev/null +++ b/src/models/beatmap/pending_beatmap/query/by_id.rs @@ -0,0 +1,33 @@ +use crate::models::beatmap::pending_beatmap::types::PendingBeatmapRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + PendingBeatmapRow, + r#" + SELECT id, osu_hash, osu_id, created_at + FROM pending_beatmap + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} + +pub async fn find_by_hash( + pool: &PgPool, + osu_hash: &str, +) -> Result, SqlxError> { + sqlx::query_as!( + PendingBeatmapRow, + r#" + SELECT id, osu_hash, osu_id, created_at + FROM pending_beatmap + WHERE osu_hash = $1 + "#, + osu_hash + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/beatmap/pending_beatmap/query/insert.rs b/src/models/beatmap/pending_beatmap/query/insert.rs new file mode 100644 index 0000000..1a8c41d --- /dev/null +++ b/src/models/beatmap/pending_beatmap/query/insert.rs @@ -0,0 +1,11 @@ +use crate::define_insert_returning_id; +use crate::models::beatmap::pending_beatmap::types::PendingBeatmapRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "pending_beatmap", + PendingBeatmapRow, + osu_hash, + osu_id +); diff --git a/src/models/beatmap/pending_beatmap/query/mod.rs b/src/models/beatmap/pending_beatmap/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/beatmap/pending_beatmap/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/beatmap/pending_beatmap/tests/mod.rs b/src/models/beatmap/pending_beatmap/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/beatmap/pending_beatmap/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/pending_beatmap/types.rs b/src/models/beatmap/pending_beatmap/types.rs similarity index 50% rename from src/models/pending_beatmap/types.rs rename to src/models/beatmap/pending_beatmap/types.rs index aff03d4..09a0a96 100644 --- a/src/models/pending_beatmap/types.rs +++ b/src/models/beatmap/pending_beatmap/types.rs @@ -3,24 +3,30 @@ use validator::Validate; use crate::utils::HASH_REGEX; -#[derive(Debug, Clone, Validate)] +#[derive(Debug, Clone, sqlx::FromRow, Validate)] pub struct PendingBeatmapRow { + /// Unique identifier for the pending beatmap record. + /// Must be a positive integer (≥ 1). #[validate(range(min = 1, message = "ID must be positive"))] pub id: i32, + /// Osu hash of the pending beatmap. + /// Must be between 1 and 255 characters. #[validate(length( min = 1, max = 255, - message = "Hash must be between 1 and 255 characters" + message = "Osu hash must be between 1 and 255 characters" ))] #[validate(regex( path = "*HASH_REGEX", message = "Hash must contain only alphanumeric characters" ))] - pub hash: String, + pub osu_hash: String, - #[validate(range(min = 1, message = "Osu ID must be positive"))] + /// Osu ID of the pending beatmap. + /// Optional field, can be None. pub osu_id: Option, + /// Timestamp when the pending beatmap was created. pub created_at: Option, } diff --git a/src/models/beatmap/query/by_id.rs b/src/models/beatmap/query/by_id.rs deleted file mode 100644 index 355dbac..0000000 --- a/src/models/beatmap/query/by_id.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::define_by_id; -use crate::models::beatmap::types::BeatmapRow; -use sqlx::{Error as SqlxError, PgPool}; - -define_by_id!( - find_by_id, - "beatmap", - BeatmapRow, - "id, osu_id, beatmapset_id, difficulty, difficulty_rating, count_circles, - count_sliders, count_spinners, max_combo, drain_time, total_time, bpm, cs, - ar, od, hp, mode, status, file_md5, file_path, created_at" -); - -pub async fn find_by_beatmapset_id(pool: &PgPool, id: i32) -> Result, SqlxError> { - Ok( - sqlx::query!("SELECT beatmapset_id FROM beatmap WHERE id = $1", id) - .fetch_optional(pool) - .await? - .and_then(|r| r.beatmapset_id), - ) -} diff --git a/src/models/beatmap/query/count.rs b/src/models/beatmap/query/count.rs deleted file mode 100644 index ea95ca9..0000000 --- a/src/models/beatmap/query/count.rs +++ /dev/null @@ -1,83 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -/// Compte le nombre total de beatmaps dans la base de données -pub async fn count_beatmaps(pool: &PgPool) -> Result, SqlxError> { - // Utiliser une estimation rapide basée sur les statistiques de la table - let row = sqlx::query!( - r#" - SELECT COALESCE( - (SELECT reltuples::bigint FROM pg_class WHERE relname = 'beatmap'), - (SELECT COUNT(*) FROM beatmap) - ) as count - "# - ) - .fetch_one(pool) - .await?; - Ok(row.count) -} - -/// Récupère toutes les statistiques en une seule requête optimisée -pub async fn get_all_stats( - pool: &PgPool, -) -> Result< - ( - Option, - Option, - std::collections::HashMap, - ), - SqlxError, -> { - use serde_json::Value; - use std::collections::HashMap; - - // Requête unique optimisée qui récupère tout en une fois - let rows = sqlx::query!( - r#" - WITH stats AS ( - SELECT - (SELECT COALESCE(reltuples::bigint, 0) FROM pg_class WHERE relname = 'beatmap') as beatmap_count, - (SELECT COALESCE(reltuples::bigint, 0) FROM pg_class WHERE relname = 'beatmapset') as beatmapset_count - ), - pattern_stats AS ( - SELECT main_pattern, COUNT(*) as count - FROM msd - WHERE rate = 1.0 AND main_pattern IS NOT NULL - GROUP BY main_pattern - ORDER BY count DESC - ) - SELECT - s.beatmap_count, - s.beatmapset_count, - p.main_pattern, - p.count as pattern_count - FROM stats s - CROSS JOIN pattern_stats p - "# - ) - .fetch_all(pool) - .await?; - - let mut beatmap_count = None; - let mut beatmapset_count = None; - let mut patterns: HashMap = HashMap::new(); - - for row in rows { - beatmap_count = Some(row.beatmap_count.unwrap_or(0)); - beatmapset_count = Some(row.beatmapset_count.unwrap_or(0)); - - if let Some(pattern_json) = row.main_pattern { - if let Ok(json_value) = serde_json::from_str::(&pattern_json) { - if let Some(array) = json_value.as_array() { - if let Some(first_pattern) = array.first() { - if let Some(pattern_str) = first_pattern.as_str() { - *patterns.entry(pattern_str.to_string()).or_insert(0) += - row.pattern_count.unwrap_or(0) as u64; - } - } - } - } - } - } - - Ok((beatmap_count, beatmapset_count, patterns)) -} diff --git a/src/models/beatmap/query/exists.rs b/src/models/beatmap/query/exists.rs deleted file mode 100644 index 39e7f34..0000000 --- a/src/models/beatmap/query/exists.rs +++ /dev/null @@ -1,11 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn exists_by_checksum(pool: &PgPool, checksum: &str) -> Result { - let row = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM beatmap WHERE file_md5 = $1) as exists", - checksum - ) - .fetch_one(pool) - .await?; - Ok(row.exists.unwrap_or(false)) -} diff --git a/src/models/beatmap/query/mod.rs b/src/models/beatmap/query/mod.rs deleted file mode 100644 index 0655d97..0000000 --- a/src/models/beatmap/query/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod by_id; -pub mod count; -pub mod exists; -pub mod insert; -pub mod search; - -pub use by_id::*; -pub use count::*; -pub use exists::*; -pub use insert::*; -pub use search::*; diff --git a/src/models/beatmap/query/search.rs b/src/models/beatmap/query/search.rs deleted file mode 100644 index 9f651a8..0000000 --- a/src/models/beatmap/query/search.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::models::beatmap::types::BeatmapRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn find_all( - pool: &PgPool, - limit: i64, - offset: i64, -) -> Result, SqlxError> { - sqlx::query_as!( - BeatmapRow, - "SELECT * FROM beatmap ORDER BY created_at DESC LIMIT $1 OFFSET $2", - limit, - offset - ) - .fetch_all(pool) - .await -} diff --git a/src/models/beatmap/rates/impl.rs b/src/models/beatmap/rates/impl.rs new file mode 100644 index 0000000..661d041 --- /dev/null +++ b/src/models/beatmap/rates/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::RatesRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl RatesRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/score_metadata/mod.rs b/src/models/beatmap/rates/mod.rs similarity index 61% rename from src/models/score_metadata/mod.rs rename to src/models/beatmap/rates/mod.rs index 9f775ac..3ee9a78 100644 --- a/src/models/score_metadata/mod.rs +++ b/src/models/beatmap/rates/mod.rs @@ -1,9 +1,8 @@ pub mod r#impl; pub mod query; pub mod types; -pub(super) mod validators; #[cfg(test)] mod tests; -pub use types::*; +pub use types::RatesRow; diff --git a/src/models/beatmap/rates/query/by_id.rs b/src/models/beatmap/rates/query/by_id.rs new file mode 100644 index 0000000..e327595 --- /dev/null +++ b/src/models/beatmap/rates/query/by_id.rs @@ -0,0 +1,16 @@ +use crate::models::beatmap::rates::types::RatesRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + RatesRow, + r#" + SELECT id, beatmap_id, osu_hash, centirate, drain_time, total_time, bpm, created_at + FROM rates + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/beatmap/rates/query/insert.rs b/src/models/beatmap/rates/query/insert.rs new file mode 100644 index 0000000..b264a93 --- /dev/null +++ b/src/models/beatmap/rates/query/insert.rs @@ -0,0 +1,7 @@ +use crate::define_insert_returning_id; +use crate::models::beatmap::rates::types::RatesRow; +// no extra imports needed + +define_insert_returning_id!( + insert, "rates", RatesRow, beatmap_id, osu_hash, centirate, drain_time, total_time, bpm +); diff --git a/src/models/beatmap/rates/query/mod.rs b/src/models/beatmap/rates/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/beatmap/rates/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/beatmap/rates/tests/mod.rs b/src/models/beatmap/rates/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/beatmap/rates/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/beatmap/rates/types.rs b/src/models/beatmap/rates/types.rs new file mode 100644 index 0000000..e2e800f --- /dev/null +++ b/src/models/beatmap/rates/types.rs @@ -0,0 +1,50 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use validator::Validate; + +use crate::utils::HASH_REGEX; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct RatesRow { + /// Unique identifier for the rates record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Reference to the beatmap this rate applies to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Beatmap ID must be positive"))] + pub beatmap_id: i32, + + /// Osu hash of the beatmap. + /// Must be between 1 and 128 characters. + #[validate(length( + min = 1, + max = 128, + message = "Osu hash must be between 1 and 128 characters" + ))] + #[validate(regex(path = *HASH_REGEX))] + pub osu_hash: String, + + /// Rate value in centi (e.g., 110 for 1.1x rate). + /// Must be between 70 and 200. + #[validate(range(min = 70, max = 200, message = "Centirate must be between 70 and 200"))] + pub centirate: i32, + + /// Drain time of the beatmap in seconds. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Drain time must be non-negative"))] + pub drain_time: i32, + + /// Total time of the beatmap in seconds. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Total time must be non-negative"))] + pub total_time: i32, + + /// Beats per minute of the beatmap. + /// Must be a positive decimal value. + pub bpm: BigDecimal, + + /// Timestamp when the rate was created. + pub created_at: Option, +} diff --git a/src/models/beatmap/tests/mod.rs b/src/models/beatmap/tests/mod.rs deleted file mode 100644 index 8695201..0000000 --- a/src/models/beatmap/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod validation; diff --git a/src/models/beatmap/tests/validation/ar_tests.rs b/src/models/beatmap/tests/validation/ar_tests.rs deleted file mode 100644 index 939ba2e..0000000 --- a/src/models/beatmap/tests/validation/ar_tests.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::models::beatmap::validators::validate_ar; -use bigdecimal::BigDecimal; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_ar_valid_range() { - assert!(validate_ar(&BigDecimal::from(0)).is_ok()); - assert!(validate_ar(&BigDecimal::from(1)).is_ok()); - assert!(validate_ar(&BigDecimal::from(5)).is_ok()); - assert!(validate_ar(&BigDecimal::from(10)).is_ok()); - assert!(validate_ar(&BigDecimal::from(11)).is_ok()); - } - - #[test] - fn test_validate_ar_invalid_negative() { - let result = validate_ar(&BigDecimal::from(-1)); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "ar_out_of_range"); - } - - #[test] - fn test_validate_ar_invalid_too_high() { - let result = validate_ar(&BigDecimal::from(12)); - assert!(result.is_err()); - } - - #[test] - fn test_validate_ar_invalid_very_high() { - let result = validate_ar(&BigDecimal::from(20)); - assert!(result.is_err()); - } -} diff --git a/src/models/beatmap/tests/validation/bpm_tests.rs b/src/models/beatmap/tests/validation/bpm_tests.rs deleted file mode 100644 index c788b56..0000000 --- a/src/models/beatmap/tests/validation/bpm_tests.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::models::beatmap::validators::validate_bpm; -use bigdecimal::BigDecimal; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_bpm_valid_range() { - assert!(validate_bpm(&BigDecimal::from(1)).is_ok()); - assert!(validate_bpm(&BigDecimal::from(120)).is_ok()); - assert!(validate_bpm(&BigDecimal::from(200)).is_ok()); - assert!(validate_bpm(&BigDecimal::from(10000)).is_ok()); - } - - #[test] - fn test_validate_bpm_invalid_zero() { - let result = validate_bpm(&BigDecimal::from(0)); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "bpm_out_of_range"); - } - - #[test] - fn test_validate_bpm_invalid_negative() { - let result = validate_bpm(&BigDecimal::from(-1)); - assert!(result.is_err()); - } - - #[test] - fn test_validate_bpm_invalid_too_high() { - let result = validate_bpm(&BigDecimal::from(10001)); - assert!(result.is_err()); - } -} diff --git a/src/models/beatmap/tests/validation/cs_tests.rs b/src/models/beatmap/tests/validation/cs_tests.rs deleted file mode 100644 index cec2358..0000000 --- a/src/models/beatmap/tests/validation/cs_tests.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::models::beatmap::validators::validate_cs; -use bigdecimal::BigDecimal; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_cs_valid_range() { - assert!(validate_cs(&BigDecimal::from(0)).is_ok()); - assert!(validate_cs(&BigDecimal::from(1)).is_ok()); - assert!(validate_cs(&BigDecimal::from(5)).is_ok()); - assert!(validate_cs(&BigDecimal::from(10)).is_ok()); - } - - #[test] - fn test_validate_cs_invalid_negative() { - let result = validate_cs(&BigDecimal::from(-1)); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "cs_out_of_range"); - } - - #[test] - fn test_validate_cs_invalid_too_high() { - let result = validate_cs(&BigDecimal::from(11)); - assert!(result.is_err()); - } - - #[test] - fn test_validate_cs_invalid_very_high() { - let result = validate_cs(&BigDecimal::from(20)); - assert!(result.is_err()); - } -} diff --git a/src/models/beatmap/tests/validation/difficulty_rating_tests.rs b/src/models/beatmap/tests/validation/difficulty_rating_tests.rs deleted file mode 100644 index a565edc..0000000 --- a/src/models/beatmap/tests/validation/difficulty_rating_tests.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::models::beatmap::validators::validate_difficulty_rating; -use bigdecimal::BigDecimal; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_difficulty_rating_valid_range() { - assert!(validate_difficulty_rating(&BigDecimal::from(0)).is_ok()); - assert!(validate_difficulty_rating(&BigDecimal::from(1)).is_ok()); - assert!(validate_difficulty_rating(&BigDecimal::from(20)).is_ok()); - assert!(validate_difficulty_rating(&BigDecimal::from(40)).is_ok()); - } - - #[test] - fn test_validate_difficulty_rating_invalid_negative() { - let result = validate_difficulty_rating(&BigDecimal::from(-1)); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "difficulty_rating_out_of_range"); - } - - #[test] - fn test_validate_difficulty_rating_invalid_too_high() { - let result = validate_difficulty_rating(&BigDecimal::from(41)); - assert!(result.is_err()); - } - - #[test] - fn test_validate_difficulty_rating_invalid_very_high() { - let result = validate_difficulty_rating(&BigDecimal::from(100)); - assert!(result.is_err()); - } -} diff --git a/src/models/beatmap/tests/validation/hp_tests.rs b/src/models/beatmap/tests/validation/hp_tests.rs deleted file mode 100644 index 82b588b..0000000 --- a/src/models/beatmap/tests/validation/hp_tests.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::models::beatmap::validators::validate_hp; -use bigdecimal::BigDecimal; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_hp_valid_range() { - assert!(validate_hp(&BigDecimal::from(0)).is_ok()); - assert!(validate_hp(&BigDecimal::from(1)).is_ok()); - assert!(validate_hp(&BigDecimal::from(5)).is_ok()); - assert!(validate_hp(&BigDecimal::from(10)).is_ok()); - } - - #[test] - fn test_validate_hp_invalid_negative() { - let result = validate_hp(&BigDecimal::from(-1)); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "hp_out_of_range"); - } - - #[test] - fn test_validate_hp_invalid_too_high() { - let result = validate_hp(&BigDecimal::from(11)); - assert!(result.is_err()); - } - - #[test] - fn test_validate_hp_invalid_very_high() { - let result = validate_hp(&BigDecimal::from(20)); - assert!(result.is_err()); - } -} diff --git a/src/models/beatmap/tests/validation/mod.rs b/src/models/beatmap/tests/validation/mod.rs deleted file mode 100644 index c17d83e..0000000 --- a/src/models/beatmap/tests/validation/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod ar_tests; -pub mod bpm_tests; -pub mod cs_tests; -pub mod difficulty_rating_tests; -pub mod hp_tests; -pub mod mode_tests; -pub mod od_tests; -pub mod status_tests; diff --git a/src/models/beatmap/tests/validation/mode_tests.rs b/src/models/beatmap/tests/validation/mode_tests.rs deleted file mode 100644 index a5f95ce..0000000 --- a/src/models/beatmap/tests/validation/mode_tests.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::models::beatmap::validators::validate_mode; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_mode_valid_values() { - assert!(validate_mode(0).is_ok()); - assert!(validate_mode(1).is_ok()); - assert!(validate_mode(2).is_ok()); - assert!(validate_mode(3).is_ok()); - } - - #[test] - fn test_validate_mode_invalid_negative() { - let result = validate_mode(-1); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "invalid_mode"); - } - - #[test] - fn test_validate_mode_invalid_too_high() { - let result = validate_mode(4); - assert!(result.is_err()); - } - - #[test] - fn test_validate_mode_invalid_very_high() { - let result = validate_mode(10); - assert!(result.is_err()); - } -} diff --git a/src/models/beatmap/tests/validation/od_tests.rs b/src/models/beatmap/tests/validation/od_tests.rs deleted file mode 100644 index 2a28373..0000000 --- a/src/models/beatmap/tests/validation/od_tests.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::models::beatmap::validators::validate_od; -use bigdecimal::BigDecimal; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_od_valid_range() { - assert!(validate_od(&BigDecimal::from(0)).is_ok()); - assert!(validate_od(&BigDecimal::from(1)).is_ok()); - assert!(validate_od(&BigDecimal::from(5)).is_ok()); - assert!(validate_od(&BigDecimal::from(10)).is_ok()); - } - - #[test] - fn test_validate_od_invalid_negative() { - let result = validate_od(&BigDecimal::from(-1)); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "od_out_of_range"); - } - - #[test] - fn test_validate_od_invalid_too_high() { - let result = validate_od(&BigDecimal::from(11)); - assert!(result.is_err()); - } - - #[test] - fn test_validate_od_invalid_very_high() { - let result = validate_od(&BigDecimal::from(20)); - assert!(result.is_err()); - } -} diff --git a/src/models/beatmap/tests/validation/status_tests.rs b/src/models/beatmap/tests/validation/status_tests.rs deleted file mode 100644 index f09d0f3..0000000 --- a/src/models/beatmap/tests/validation/status_tests.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::models::beatmap::validators::validate_status; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_status_valid_values() { - assert!(validate_status("graveyard").is_ok()); - assert!(validate_status("wip").is_ok()); - assert!(validate_status("pending").is_ok()); - assert!(validate_status("ranked").is_ok()); - assert!(validate_status("approved").is_ok()); - assert!(validate_status("qualified").is_ok()); - assert!(validate_status("loved").is_ok()); - } - - #[test] - fn test_validate_status_invalid_empty() { - let result = validate_status(""); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "invalid_status"); - } - - #[test] - fn test_validate_status_invalid_random() { - let result = validate_status("invalid_status"); - assert!(result.is_err()); - } - - #[test] - fn test_validate_status_invalid_case_sensitive() { - let result = validate_status("RANKED"); - assert!(result.is_err()); - } - - #[test] - fn test_validate_status_invalid_with_spaces() { - let result = validate_status(" ranked "); - assert!(result.is_err()); - } -} diff --git a/src/models/beatmap/types.rs b/src/models/beatmap/types.rs deleted file mode 100644 index f8ee5ed..0000000 --- a/src/models/beatmap/types.rs +++ /dev/null @@ -1,54 +0,0 @@ -use bigdecimal::BigDecimal; -use chrono::NaiveDateTime; -use validator::Validate; - -use super::validators::{ - validate_ar, validate_bpm, validate_cs, validate_difficulty_rating, validate_hp, validate_mode, - validate_od, validate_status, -}; - -#[derive(Clone, Debug, sqlx::FromRow, Validate)] -pub struct BeatmapRow { - #[validate(range(min = 1))] - pub id: i32, - #[validate(range(min = 0, message = "ID must be positive"))] - pub osu_id: Option, - #[validate(range(min = 0, message = "ID must be positive"))] - pub beatmapset_id: Option, - #[validate(length(min = 1, max = 100))] - pub difficulty: String, - #[validate(custom(function = "validate_difficulty_rating"))] - pub difficulty_rating: BigDecimal, - #[validate(range(min = 0))] - pub count_circles: i32, - #[validate(range(min = 0))] - pub count_sliders: i32, - #[validate(range(min = 0))] - pub count_spinners: i32, - #[validate(range(min = 1))] - pub max_combo: i32, - #[validate(range(min = 0))] - pub drain_time: i32, - #[validate(range(min = 0))] - pub total_time: i32, - #[validate(custom(function = "validate_bpm"))] - pub bpm: BigDecimal, - #[validate(custom(function = "validate_cs"))] - pub cs: BigDecimal, - #[validate(custom(function = "validate_ar"))] - pub ar: BigDecimal, - #[validate(custom(function = "validate_od"))] - pub od: BigDecimal, - #[validate(custom(function = "validate_hp"))] - pub hp: BigDecimal, - #[validate(custom(function = "validate_mode"))] - pub mode: i32, - #[validate(custom(function = "validate_status"))] - pub status: String, - #[validate(length(min = 32, max = 32))] - pub file_md5: String, - #[validate(length(min = 1, max = 500))] - pub file_path: String, - pub created_at: Option, - pub updated_at: Option, -} diff --git a/src/models/beatmap/validators.rs b/src/models/beatmap/validators.rs deleted file mode 100644 index ad3e9b3..0000000 --- a/src/models/beatmap/validators.rs +++ /dev/null @@ -1,67 +0,0 @@ -use bigdecimal::BigDecimal; -use validator::ValidationError; - -pub(super) fn validate_difficulty_rating(rating: &BigDecimal) -> Result<(), ValidationError> { - if *rating < BigDecimal::from(0) || *rating > BigDecimal::from(40) { - return Err(ValidationError::new("difficulty_rating_out_of_range")); - } - Ok(()) -} - -pub(super) fn validate_bpm(bpm: &BigDecimal) -> Result<(), ValidationError> { - if *bpm <= BigDecimal::from(0) || *bpm > BigDecimal::from(10000) { - return Err(ValidationError::new("bpm_out_of_range")); - } - Ok(()) -} - -pub(super) fn validate_cs(cs: &BigDecimal) -> Result<(), ValidationError> { - if *cs < BigDecimal::from(0) || *cs > BigDecimal::from(10) { - return Err(ValidationError::new("cs_out_of_range")); - } - Ok(()) -} - -pub(super) fn validate_ar(ar: &BigDecimal) -> Result<(), ValidationError> { - if *ar < BigDecimal::from(0) || *ar > BigDecimal::from(11) { - return Err(ValidationError::new("ar_out_of_range")); - } - Ok(()) -} - -pub(super) fn validate_od(od: &BigDecimal) -> Result<(), ValidationError> { - if *od < BigDecimal::from(0) || *od > BigDecimal::from(10) { - return Err(ValidationError::new("od_out_of_range")); - } - Ok(()) -} - -pub(super) fn validate_hp(hp: &BigDecimal) -> Result<(), ValidationError> { - if *hp < BigDecimal::from(0) || *hp > BigDecimal::from(10) { - return Err(ValidationError::new("hp_out_of_range")); - } - Ok(()) -} - -pub(super) fn validate_mode(mode: i32) -> Result<(), ValidationError> { - if ![0, 1, 2, 3].contains(&mode) { - return Err(ValidationError::new("invalid_mode")); - } - Ok(()) -} - -pub(super) fn validate_status(status: &str) -> Result<(), ValidationError> { - let valid_statuses = [ - "graveyard", - "wip", - "pending", - "ranked", - "approved", - "qualified", - "loved", - ]; - if !valid_statuses.contains(&status) { - return Err(ValidationError::new("invalid_status")); - } - Ok(()) -} diff --git a/src/models/beatmapset/impl.rs b/src/models/beatmapset/impl.rs deleted file mode 100644 index d722bc0..0000000 --- a/src/models/beatmapset/impl.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::query::{exists_by_osu_id, find_all, find_by_id, find_by_osu_id, insert, search}; -use super::types::BeatmapsetRow; -use sqlx::PgPool; - -impl BeatmapsetRow { - pub async fn insert(self, pool: &PgPool) -> Result { - insert(pool, self).await - } - - pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, sqlx::Error> { - find_by_id(pool, id).await - } - - pub async fn find_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, sqlx::Error> { - find_by_osu_id(pool, osu_id).await - } - - pub async fn exists_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, sqlx::Error> { - exists_by_osu_id(pool, osu_id).await - } - - pub async fn search( - pool: &PgPool, - term: &str, - limit: Option, - offset: Option, - ) -> Result, sqlx::Error> { - search(pool, term, limit, offset).await - } - - pub async fn find_all( - pool: &PgPool, - limit: Option, - offset: Option, - ) -> Result, sqlx::Error> { - find_all(pool, limit, offset).await - } -} diff --git a/src/models/beatmapset/query/by_id.rs b/src/models/beatmapset/query/by_id.rs deleted file mode 100644 index 1a4feb7..0000000 --- a/src/models/beatmapset/query/by_id.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::models::beatmapset::BeatmapsetRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { - sqlx::query_as!(BeatmapsetRow, "SELECT * FROM beatmapset WHERE id = $1", id) - .fetch_optional(pool) - .await -} diff --git a/src/models/beatmapset/query/by_osu_id.rs b/src/models/beatmapset/query/by_osu_id.rs deleted file mode 100644 index 5b62755..0000000 --- a/src/models/beatmapset/query/by_osu_id.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::models::beatmapset::BeatmapsetRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn find_by_osu_id( - pool: &PgPool, - osu_id: i32, -) -> Result, SqlxError> { - sqlx::query_as!( - BeatmapsetRow, - "SELECT * FROM beatmapset WHERE osu_id = $1", - osu_id - ) - .fetch_optional(pool) - .await -} diff --git a/src/models/beatmapset/query/count.rs b/src/models/beatmapset/query/count.rs deleted file mode 100644 index b73e5f9..0000000 --- a/src/models/beatmapset/query/count.rs +++ /dev/null @@ -1,17 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -/// Compte le nombre total de beatmapsets dans la base de données -pub async fn count_beatmapsets(pool: &PgPool) -> Result, SqlxError> { - // Utiliser une estimation rapide basée sur les statistiques de la table - Ok(sqlx::query!( - r#" - SELECT COALESCE( - (SELECT reltuples::bigint FROM pg_class WHERE relname = 'beatmapset'), - (SELECT COUNT(*) FROM beatmapset) - ) as count - "# - ) - .fetch_one(pool) - .await? - .count) -} diff --git a/src/models/beatmapset/query/exists.rs b/src/models/beatmapset/query/exists.rs deleted file mode 100644 index ac4222f..0000000 --- a/src/models/beatmapset/query/exists.rs +++ /dev/null @@ -1,11 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn exists_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, SqlxError> { - Ok(sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM beatmapset WHERE osu_id = $1) as exists", - osu_id - ) - .fetch_one(pool) - .await? - .exists) -} diff --git a/src/models/beatmapset/query/insert.rs b/src/models/beatmapset/query/insert.rs deleted file mode 100644 index aa890f4..0000000 --- a/src/models/beatmapset/query/insert.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::models::beatmapset::types::BeatmapsetRow; -use sqlx::QueryBuilder; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn insert(pool: &PgPool, beatmapset: BeatmapsetRow) -> Result { - let mut builder = QueryBuilder::::new( - "INSERT INTO beatmapset (osu_id, artist, artist_unicode, title, title_unicode, creator, source, tags, has_video, has_storyboard, is_explicit, is_featured, cover_url, preview_url, osu_file_url) VALUES (" - ); - - let mut separated = builder.separated(", "); - separated.push_bind(beatmapset.osu_id); - separated.push_bind(beatmapset.artist); - separated.push_bind(beatmapset.artist_unicode); - separated.push_bind(beatmapset.title); - separated.push_bind(beatmapset.title_unicode); - separated.push_bind(beatmapset.creator); - separated.push_bind(beatmapset.source); - separated.push_bind(beatmapset.tags); - separated.push_bind(beatmapset.has_video); - separated.push_bind(beatmapset.has_storyboard); - separated.push_bind(beatmapset.is_explicit); - separated.push_bind(beatmapset.is_featured); - separated.push_bind(beatmapset.cover_url); - separated.push_bind(beatmapset.preview_url); - separated.push_bind(beatmapset.osu_file_url); - // separated will go out of scope here - builder.push(") ON CONFLICT (osu_id) DO UPDATE SET "); - builder.push( - "artist = EXCLUDED.artist, artist_unicode = EXCLUDED.artist_unicode, title = EXCLUDED.title, title_unicode = EXCLUDED.title_unicode, creator = EXCLUDED.creator, source = EXCLUDED.source, tags = EXCLUDED.tags, has_video = EXCLUDED.has_video, has_storyboard = EXCLUDED.has_storyboard, is_explicit = EXCLUDED.is_explicit, is_featured = EXCLUDED.is_featured, cover_url = EXCLUDED.cover_url, preview_url = EXCLUDED.preview_url, osu_file_url = EXCLUDED.osu_file_url, updated_at = NOW() RETURNING id", - ); - - let rec = builder.build().fetch_one(pool).await?; - use sqlx::Row; - rec.try_get("id") -} diff --git a/src/models/beatmapset/query/mod.rs b/src/models/beatmapset/query/mod.rs deleted file mode 100644 index a7e1c58..0000000 --- a/src/models/beatmapset/query/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod by_id; -pub mod by_osu_id; -pub mod count; -pub mod exists; -pub mod insert; -pub mod search; - -pub use by_id::*; -pub use by_osu_id::*; -pub use count::*; -pub use exists::*; -pub use insert::*; -pub use search::*; diff --git a/src/models/beatmapset/query/search.rs b/src/models/beatmapset/query/search.rs deleted file mode 100644 index 1050ce3..0000000 --- a/src/models/beatmapset/query/search.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::models::beatmapset::BeatmapsetRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn search( - pool: &PgPool, - term: &str, - limit: Option, - offset: Option, -) -> Result, SqlxError> { - let limit = limit.unwrap_or(50); - let offset = offset.unwrap_or(0); - sqlx::query_as!( - BeatmapsetRow, - r#" - SELECT * FROM beatmapset - WHERE artist ILIKE $1 OR artist_unicode ILIKE $1 OR title ILIKE $1 OR title_unicode ILIKE $1 OR creator ILIKE $1 - ORDER BY created_at DESC - LIMIT $2 OFFSET $3 - "#, - format!("%{}%", term), - limit, - offset - ) - .fetch_all(pool) - .await -} - -pub async fn find_all( - pool: &PgPool, - limit: Option, - offset: Option, -) -> Result, SqlxError> { - let limit = limit.unwrap_or(50); - let offset = offset.unwrap_or(0); - sqlx::query_as!( - BeatmapsetRow, - "SELECT * FROM beatmapset ORDER BY created_at DESC LIMIT $1 OFFSET $2", - limit, - offset - ) - .fetch_all(pool) - .await -} diff --git a/src/models/beatmapset/tests/mod.rs b/src/models/beatmapset/tests/mod.rs deleted file mode 100644 index d8125f0..0000000 --- a/src/models/beatmapset/tests/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod model_tests; -pub mod validation; diff --git a/src/models/beatmapset/tests/model_tests.rs b/src/models/beatmapset/tests/model_tests.rs deleted file mode 100644 index 22c1c83..0000000 --- a/src/models/beatmapset/tests/model_tests.rs +++ /dev/null @@ -1,247 +0,0 @@ -use crate::models::beatmapset::BeatmapsetRow; -use chrono::DateTime; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_beatmapset_valid_model() { - let beatmapset = BeatmapsetRow { - id: 1, - osu_id: Some(12345), - artist: "Test Artist".to_string(), - artist_unicode: Some("テストアーティスト".to_string()), - title: "Test Title".to_string(), - title_unicode: Some("テストタイトル".to_string()), - creator: "Test Creator".to_string(), - source: Some("Test Source".to_string()), - tags: Some(vec!["anime".to_string(), "op".to_string()]), - has_video: true, - has_storyboard: false, - is_explicit: false, - is_featured: true, - cover_url: Some("https://example.com/cover.jpg".to_string()), - preview_url: Some("https://example.com/preview.mp3".to_string()), - osu_file_url: Some("https://example.com/beatmap.osz".to_string()), - created_at: Some(DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc()), - updated_at: Some(DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc()), - }; - - assert!(beatmapset.validate().is_ok()); - assert_eq!(beatmapset.id, 1); - assert_eq!(beatmapset.artist, "Test Artist"); - assert_eq!(beatmapset.title, "Test Title"); - assert!(beatmapset.has_video); - assert!(!beatmapset.has_storyboard); - } - - #[test] - fn test_beatmapset_minimal_valid_model() { - let beatmapset = BeatmapsetRow { - id: 1, - osu_id: None, - artist: "A".to_string(), - artist_unicode: None, - title: "T".to_string(), - title_unicode: None, - creator: "C".to_string(), - source: None, - tags: None, - has_video: false, - has_storyboard: false, - is_explicit: false, - is_featured: false, - cover_url: None, - preview_url: None, - osu_file_url: None, - created_at: None, - updated_at: None, - }; - - assert!(beatmapset.validate().is_ok()); - } - - #[test] - fn test_beatmapset_invalid_empty_artist() { - let beatmapset = BeatmapsetRow { - id: 1, - osu_id: None, - artist: "".to_string(), - artist_unicode: None, - title: "Test Title".to_string(), - title_unicode: None, - creator: "Test Creator".to_string(), - source: None, - tags: None, - has_video: false, - has_storyboard: false, - is_explicit: false, - is_featured: false, - cover_url: None, - preview_url: None, - osu_file_url: None, - created_at: None, - updated_at: None, - }; - - assert!(beatmapset.validate().is_err()); - } - - #[test] - fn test_beatmapset_invalid_empty_title() { - let beatmapset = BeatmapsetRow { - id: 1, - osu_id: None, - artist: "Test Artist".to_string(), - artist_unicode: None, - title: "".to_string(), - title_unicode: None, - creator: "Test Creator".to_string(), - source: None, - tags: None, - has_video: false, - has_storyboard: false, - is_explicit: false, - is_featured: false, - cover_url: None, - preview_url: None, - osu_file_url: None, - created_at: None, - updated_at: None, - }; - - assert!(beatmapset.validate().is_err()); - } - - #[test] - fn test_beatmapset_invalid_empty_creator() { - let beatmapset = BeatmapsetRow { - id: 1, - osu_id: None, - artist: "Test Artist".to_string(), - artist_unicode: None, - title: "Test Title".to_string(), - title_unicode: None, - creator: "".to_string(), - source: None, - tags: None, - has_video: false, - has_storyboard: false, - is_explicit: false, - is_featured: false, - cover_url: None, - preview_url: None, - osu_file_url: None, - created_at: None, - updated_at: None, - }; - - assert!(beatmapset.validate().is_err()); - } - - #[test] - fn test_beatmapset_invalid_negative_osu_id() { - let beatmapset = BeatmapsetRow { - id: 1, - osu_id: Some(-1), - artist: "Test Artist".to_string(), - artist_unicode: None, - title: "Test Title".to_string(), - title_unicode: None, - creator: "Test Creator".to_string(), - source: None, - tags: None, - has_video: false, - has_storyboard: false, - is_explicit: false, - is_featured: false, - cover_url: None, - preview_url: None, - osu_file_url: None, - created_at: None, - updated_at: None, - }; - - assert!(beatmapset.validate().is_err()); - } - - #[test] - fn test_beatmapset_invalid_tags() { - let beatmapset = BeatmapsetRow { - id: 1, - osu_id: None, - artist: "Test Artist".to_string(), - artist_unicode: None, - title: "Test Title".to_string(), - title_unicode: None, - creator: "Test Creator".to_string(), - source: None, - tags: Some(vec!["".to_string()]), // Empty tag - has_video: false, - has_storyboard: false, - is_explicit: false, - is_featured: false, - cover_url: None, - preview_url: None, - osu_file_url: None, - created_at: None, - updated_at: None, - }; - - assert!(beatmapset.validate().is_err()); - } - - #[test] - fn test_beatmapset_invalid_url() { - let beatmapset = BeatmapsetRow { - id: 1, - osu_id: None, - artist: "Test Artist".to_string(), - artist_unicode: None, - title: "Test Title".to_string(), - title_unicode: None, - creator: "Test Creator".to_string(), - source: None, - tags: None, - has_video: false, - has_storyboard: false, - is_explicit: false, - is_featured: false, - cover_url: Some("invalid-url".to_string()), // Invalid URL - preview_url: None, - osu_file_url: None, - created_at: None, - updated_at: None, - }; - - assert!(beatmapset.validate().is_err()); - } - - #[test] - fn test_beatmapset_invalid_osu_id_zero() { - let beatmapset = BeatmapsetRow { - id: 1, - osu_id: Some(0), // Zero is invalid - artist: "Test Artist".to_string(), - artist_unicode: None, - title: "Test Title".to_string(), - title_unicode: None, - creator: "Test Creator".to_string(), - source: None, - tags: None, - has_video: false, - has_storyboard: false, - is_explicit: false, - is_featured: false, - cover_url: None, - preview_url: None, - osu_file_url: None, - created_at: None, - updated_at: None, - }; - - assert!(beatmapset.validate().is_err()); - } -} diff --git a/src/models/beatmapset/tests/validation/mod.rs b/src/models/beatmapset/tests/validation/mod.rs deleted file mode 100644 index 86e5d5c..0000000 --- a/src/models/beatmapset/tests/validation/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod tags_tests; -pub mod url_tests; diff --git a/src/models/beatmapset/tests/validation/tags_tests.rs b/src/models/beatmapset/tests/validation/tags_tests.rs deleted file mode 100644 index 4642bdd..0000000 --- a/src/models/beatmapset/tests/validation/tags_tests.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::models::beatmapset::validators::validate_tags; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_tags_valid_single_tag() { - let tags = vec!["anime".to_string()]; - assert!(validate_tags(&tags).is_ok()); - } - - #[test] - fn test_validate_tags_valid_multiple_tags() { - let tags = vec!["anime".to_string(), "op".to_string(), "tv".to_string()]; - assert!(validate_tags(&tags).is_ok()); - } - - #[test] - fn test_validate_tags_invalid_empty_tag() { - let tags = vec!["".to_string()]; - let result = validate_tags(&tags); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "tag_cannot_be_empty"); - } - - #[test] - fn test_validate_tags_invalid_too_long() { - let long_tag = "a".repeat(51); - let tags = vec![long_tag]; - let result = validate_tags(&tags); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "tag_too_long"); - } - - #[test] - fn test_validate_tags_invalid_with_newline() { - let tags = vec!["anime\n".to_string()]; - let result = validate_tags(&tags); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "tag_contains_invalid_characters"); - } - - #[test] - fn test_validate_tags_invalid_with_tab() { - let tags = vec!["anime\t".to_string()]; - let result = validate_tags(&tags); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "tag_contains_invalid_characters"); - } - - #[test] - fn test_validate_tags_invalid_with_carriage_return() { - let tags = vec!["anime\r".to_string()]; - let result = validate_tags(&tags); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "tag_contains_invalid_characters"); - } -} diff --git a/src/models/beatmapset/tests/validation/url_tests.rs b/src/models/beatmapset/tests/validation/url_tests.rs deleted file mode 100644 index 3b53796..0000000 --- a/src/models/beatmapset/tests/validation/url_tests.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::models::beatmapset::validators::validate_url; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_url_valid_http() { - let url = "http://example.com"; - assert!(validate_url(url).is_ok()); - } - - #[test] - fn test_validate_url_valid_https() { - let url = "https://example.com"; - assert!(validate_url(url).is_ok()); - } - - #[test] - fn test_validate_url_valid_with_path() { - let url = "https://example.com/path/to/resource"; - assert!(validate_url(url).is_ok()); - } - - #[test] - fn test_validate_url_invalid_empty() { - let url = ""; - let result = validate_url(url); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "url_cannot_be_empty"); - } - - #[test] - fn test_validate_url_invalid_too_long() { - let long_url = format!("https://example.com/{}", "a".repeat(500)); - let result = validate_url(&long_url); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "url_too_long"); - } - - #[test] - fn test_validate_url_invalid_no_protocol() { - let url = "example.com"; - let result = validate_url(url); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "url_must_start_with_http"); - } - - #[test] - fn test_validate_url_invalid_ftp() { - let url = "ftp://example.com"; - let result = validate_url(url); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "url_must_start_with_http"); - } -} diff --git a/src/models/beatmapset/types.rs b/src/models/beatmapset/types.rs deleted file mode 100644 index d7303a7..0000000 --- a/src/models/beatmapset/types.rs +++ /dev/null @@ -1,64 +0,0 @@ -use chrono::NaiveDateTime; -use serde::{Deserialize, Serialize}; -use validator::Validate; - -use crate::models::beatmapset::validators::*; - -#[derive(Clone, Debug, Deserialize, Serialize, Validate)] -pub struct BeatmapsetRow { - #[validate(range(min = 1, message = "ID must be positive"))] - pub id: i32, - - #[validate(range(min = 1, message = "ID must be positive"))] - pub osu_id: Option, - - #[validate(length( - min = 1, - max = 255, - message = "Artist must be between 1 and 255 characters" - ))] - pub artist: String, - - #[validate(length(max = 255, message = "Artist unicode must be at most 255 characters"))] - pub artist_unicode: Option, - - #[validate(length( - min = 1, - max = 255, - message = "Title must be between 1 and 255 characters" - ))] - pub title: String, - - #[validate(length(max = 255, message = "Title unicode must be at most 255 characters"))] - pub title_unicode: Option, - - #[validate(length( - min = 1, - max = 255, - message = "Creator must be between 1 and 255 characters" - ))] - pub creator: String, - - #[validate(length(max = 255, message = "Source must be at most 255 characters"))] - pub source: Option, - - #[validate(custom(function = "validate_tags"))] - pub tags: Option>, - - pub has_video: bool, - pub has_storyboard: bool, - pub is_explicit: bool, - pub is_featured: bool, - - #[validate(custom(function = "validate_url"))] - pub cover_url: Option, - - #[validate(custom(function = "validate_url"))] - pub preview_url: Option, - - #[validate(custom(function = "validate_url"))] - pub osu_file_url: Option, - - pub created_at: Option, - pub updated_at: Option, -} diff --git a/src/models/beatmapset/validators.rs b/src/models/beatmapset/validators.rs deleted file mode 100644 index 7d1de4e..0000000 --- a/src/models/beatmapset/validators.rs +++ /dev/null @@ -1,37 +0,0 @@ -use validator::ValidationError; - -/// Validates that tags are properly formatted if provided. -pub fn validate_tags(tags: &Vec) -> Result<(), ValidationError> { - for tag in tags { - if tag.is_empty() { - return Err(ValidationError::new("tag_cannot_be_empty")); - } - if tag.len() > 50 { - return Err(ValidationError::new("tag_too_long")); - } - // Check for invalid characters in tags - if tag - .chars() - .any(|c| c.is_control() || c == '\n' || c == '\r' || c == '\t') - { - return Err(ValidationError::new("tag_contains_invalid_characters")); - } - } - - Ok(()) -} - -/// Validates that a URL is properly formatted if provided. -pub fn validate_url(url: &str) -> Result<(), ValidationError> { - if url.is_empty() { - return Err(ValidationError::new("url_cannot_be_empty")); - } - if url.len() > 500 { - return Err(ValidationError::new("url_too_long")); - } - // Basic URL validation - if !url.starts_with("http://") && !url.starts_with("https://") { - return Err(ValidationError::new("url_must_start_with_http")); - } - Ok(()) -} diff --git a/src/models/failed_query/query/delete_by_hash.rs b/src/models/failed_query/query/delete_by_hash.rs deleted file mode 100644 index 275b796..0000000 --- a/src/models/failed_query/query/delete_by_hash.rs +++ /dev/null @@ -1,15 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn delete_by_hash(pool: &PgPool, hash: &str) -> Result { - let result = sqlx::query!( - r#" - DELETE FROM failed_query - WHERE hash = $1 - "#, - hash - ) - .execute(pool) - .await?; - - Ok(result.rows_affected()) -} diff --git a/src/models/failed_query/query/exists_by_hash.rs b/src/models/failed_query/query/exists_by_hash.rs deleted file mode 100644 index fa98208..0000000 --- a/src/models/failed_query/query/exists_by_hash.rs +++ /dev/null @@ -1,14 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn exists_by_hash(pool: &PgPool, hash: &str) -> Result { - let query = sqlx::query!( - r#" - SELECT EXISTS( - SELECT 1 FROM failed_query WHERE hash = $1 - ) as exists - "#, - hash - ); - let result = query.fetch_one(pool).await?; - Ok(result.exists.unwrap_or(false)) -} diff --git a/src/models/failed_query/query/mod.rs b/src/models/failed_query/query/mod.rs deleted file mode 100644 index 1770cfb..0000000 --- a/src/models/failed_query/query/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod by_id; -pub mod delete_by_hash; -pub mod exists_by_hash; -pub mod insert; - -pub use by_id::find_by_id; -pub use delete_by_hash::delete_by_hash; -pub use exists_by_hash::exists_by_hash; -pub use insert::insert; diff --git a/src/models/failed_query/tests/mod.rs b/src/models/failed_query/tests/mod.rs deleted file mode 100644 index 8d947d7..0000000 --- a/src/models/failed_query/tests/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Test modules for the `FailedQuery` model. -//! -//! This module contains comprehensive tests for the `FailedQuery` model, -//! including validation tests and model behavior tests. -//! -//! # Test Categories -//! -//! ## Validation Tests (`validation/`) -//! Tests that verify the validation rules of the `FailedQuery` model: -//! - Hash format validation (alphanumeric characters only) -//! - Hash length validation (1-255 characters) -//! - Edge cases and error conditions -//! -//! ## Model Tests (`model_tests.rs`) -//! Tests that verify the basic functionality and behavior of the `FailedQuery` struct: -//! - Model creation and field access -//! - Edge cases for different field values -//! - Timestamp handling -//! -//! # Running Tests -//! -//! To run all failed query tests: -//! ```bash -//! cargo test models::failed_query::tests -//! ``` -//! -//! To run only validation tests: -//! ```bash -//! cargo test models::failed_query::tests::validation -//! ``` -//! -//! To run only model tests: -//! ```bash -//! cargo test models::failed_query::tests::model_tests -//! ``` - -pub mod model_tests; -pub mod validation; diff --git a/src/models/failed_query/tests/model_tests.rs b/src/models/failed_query/tests/model_tests.rs deleted file mode 100644 index b1a1c3b..0000000 --- a/src/models/failed_query/tests/model_tests.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::models::failed_query::FailedQueryRow; -use chrono::DateTime; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_failed_query_valid_model() { - let failed_query = FailedQueryRow { - id: 1, - hash: "abc123def456".to_string(), - created_at: Some(DateTime::from_timestamp(1640995200, 0).unwrap().naive_utc()), - }; - - // Test que le modèle peut être créé sans erreur - assert_eq!(failed_query.id, 1); - assert_eq!(failed_query.hash, "abc123def456"); - assert!(failed_query.created_at.is_some()); - } - - #[test] - fn test_failed_query_without_created_at() { - let failed_query = FailedQueryRow { - id: 1, - hash: "abc123def456".to_string(), - created_at: None, - }; - - assert_eq!(failed_query.id, 1); - assert_eq!(failed_query.hash, "abc123def456"); - assert!(failed_query.created_at.is_none()); - } - - #[test] - fn test_failed_query_hash_edge_cases() { - // Test avec hash de longueur minimale - let failed_query_min = FailedQueryRow { - id: 1, - hash: "a".to_string(), - created_at: None, - }; - assert_eq!(failed_query_min.hash, "a"); - - // Test avec hash de longueur maximale (255 caractères) - let long_hash = "a".repeat(255); - let failed_query_max = FailedQueryRow { - id: 1, - hash: long_hash.clone(), - created_at: None, - }; - assert_eq!(failed_query_max.hash, long_hash); - } - - #[test] - fn test_failed_query_id_edge_cases() { - // Test avec ID minimal - let failed_query_min = FailedQueryRow { - id: 1, - hash: "abc123".to_string(), - created_at: None, - }; - assert_eq!(failed_query_min.id, 1); - - // Test avec ID plus grand - let failed_query_large = FailedQueryRow { - id: 999999, - hash: "abc123".to_string(), - created_at: None, - }; - assert_eq!(failed_query_large.id, 999999); - } -} diff --git a/src/models/failed_query/tests/validation/hash_tests.rs b/src/models/failed_query/tests/validation/hash_tests.rs deleted file mode 100644 index 8ea9a92..0000000 --- a/src/models/failed_query/tests/validation/hash_tests.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::models::failed_query::FailedQueryRow; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_failed_query_hash_validation_valid_alphanumeric() { - let failed_query = FailedQueryRow { - id: 1, - hash: "abc123".to_string(), - created_at: None, - }; - assert!(failed_query.validate().is_ok()); - - let failed_query2 = FailedQueryRow { - id: 1, - hash: "ABC123".to_string(), - created_at: None, - }; - assert!(failed_query2.validate().is_ok()); - - let failed_query3 = FailedQueryRow { - id: 1, - hash: "a1b2c3".to_string(), - created_at: None, - }; - assert!(failed_query3.validate().is_ok()); - } - - #[test] - fn test_failed_query_hash_validation_valid_single_character() { - let failed_query = FailedQueryRow { - id: 1, - hash: "a".to_string(), - created_at: None, - }; - assert!(failed_query.validate().is_ok()); - - let failed_query2 = FailedQueryRow { - id: 1, - hash: "1".to_string(), - created_at: None, - }; - assert!(failed_query2.validate().is_ok()); - } - - #[test] - fn test_failed_query_hash_validation_invalid_empty() { - let failed_query = FailedQueryRow { - id: 1, - hash: "".to_string(), - created_at: None, - }; - assert!(failed_query.validate().is_err()); - } - - #[test] - fn test_failed_query_hash_validation_invalid_too_long() { - let long_hash = "a".repeat(256); - let failed_query = FailedQueryRow { - id: 1, - hash: long_hash, - created_at: None, - }; - assert!(failed_query.validate().is_err()); - } - - #[test] - fn test_failed_query_hash_validation_invalid_with_special_characters() { - let failed_query = FailedQueryRow { - id: 1, - hash: "abc-123".to_string(), - created_at: None, - }; - assert!(failed_query.validate().is_err()); - - let failed_query2 = FailedQueryRow { - id: 1, - hash: "abc_123".to_string(), - created_at: None, - }; - assert!(failed_query2.validate().is_err()); - - let failed_query3 = FailedQueryRow { - id: 1, - hash: "abc.123".to_string(), - created_at: None, - }; - assert!(failed_query3.validate().is_err()); - } - - #[test] - fn test_failed_query_hash_validation_invalid_with_spaces() { - let failed_query = FailedQueryRow { - id: 1, - hash: "abc 123".to_string(), - created_at: None, - }; - assert!(failed_query.validate().is_err()); - - let failed_query2 = FailedQueryRow { - id: 1, - hash: " abc123".to_string(), - created_at: None, - }; - assert!(failed_query2.validate().is_err()); - - let failed_query3 = FailedQueryRow { - id: 1, - hash: "abc123 ".to_string(), - created_at: None, - }; - assert!(failed_query3.validate().is_err()); - } - - #[test] - fn test_failed_query_hash_validation_invalid_with_special_chars() { - let failed_query = FailedQueryRow { - id: 1, - hash: "abc@123".to_string(), - created_at: None, - }; - assert!(failed_query.validate().is_err()); - - let failed_query2 = FailedQueryRow { - id: 1, - hash: "abc#123".to_string(), - created_at: None, - }; - assert!(failed_query2.validate().is_err()); - } -} diff --git a/src/models/failed_query/tests/validation/mod.rs b/src/models/failed_query/tests/validation/mod.rs deleted file mode 100644 index 0f350d8..0000000 --- a/src/models/failed_query/tests/validation/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Validation tests for the `FailedQuery` model. -//! -//! This module contains tests that verify the validation rules and constraints -//! of the `FailedQuery` model, ensuring data integrity and proper error handling. - -pub mod hash_tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index e3e93b9..d2086ab 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,9 +1,14 @@ pub mod beatmap; -pub mod beatmapset; -pub mod failed_query; -pub mod msd; -pub mod pending_beatmap; -pub mod replay; +pub mod other; +pub mod rating; pub mod score; -pub mod score_metadata; -pub mod score_rating; +pub mod users; +pub mod weekly; + +// Re-exports for easy access +pub use beatmap::*; +pub use other::*; +pub use rating::*; +pub use score::*; +pub use users::*; +pub use weekly::*; diff --git a/src/models/msd/impl.rs b/src/models/msd/impl.rs deleted file mode 100644 index 39cef57..0000000 --- a/src/models/msd/impl.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::query::{ - find_all_by_beatmap_id, find_by_beatmap_id, find_by_beatmap_id_and_rate, find_by_id, insert, -}; -use super::types::MSDRow; -use sqlx::PgPool; - -impl MSDRow { - pub async fn insert_into_db(self, pool: &PgPool) -> Result { - insert(pool, self).await - } - - pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, sqlx::Error> { - find_by_id(pool, id).await - } - - pub async fn find_by_beatmap_id( - pool: &PgPool, - beatmap_id: i32, - ) -> Result, sqlx::Error> { - find_by_beatmap_id(pool, beatmap_id).await - } - - pub async fn find_by_beatmap_id_and_rate( - pool: &PgPool, - beatmap_id: i32, - rate: f64, - ) -> Result, sqlx::Error> { - find_by_beatmap_id_and_rate(pool, beatmap_id, rate).await - } - - pub async fn find_all_by_beatmap_id( - pool: &PgPool, - beatmap_id: i32, - ) -> Result, sqlx::Error> { - find_all_by_beatmap_id(pool, beatmap_id).await - } -} diff --git a/src/models/msd/mod.rs b/src/models/msd/mod.rs deleted file mode 100644 index e0dd4b3..0000000 --- a/src/models/msd/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod r#impl; -pub mod query; -pub mod types; -pub(super) mod validators; - -#[cfg(test)] -mod tests; - -pub use query::*; -pub use types::MSDRow; diff --git a/src/models/msd/query/by_beatmap_id.rs b/src/models/msd/query/by_beatmap_id.rs deleted file mode 100644 index 67e1856..0000000 --- a/src/models/msd/query/by_beatmap_id.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::models::msd::types::MSDRow; -use bigdecimal::BigDecimal; -use bigdecimal::FromPrimitive; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn find_by_beatmap_id( - pool: &PgPool, - beatmap_id: i32, -) -> Result, SqlxError> { - sqlx::query_as!( - MSDRow, - r#" - SELECT * FROM msd WHERE beatmap_id = $1 ORDER BY created_at DESC LIMIT 1 - "#, - beatmap_id - ) - .fetch_optional(pool) - .await -} - -pub async fn find_by_beatmap_id_and_rate( - pool: &PgPool, - beatmap_id: i32, - rate: f64, -) -> Result, SqlxError> { - sqlx::query_as!( - MSDRow, - r#"SELECT * FROM msd WHERE beatmap_id = $1 AND rate = $2"#, - beatmap_id, - BigDecimal::from_f64(rate).unwrap() - ) - .fetch_optional(pool) - .await -} - -pub async fn find_all_by_beatmap_id( - pool: &PgPool, - beatmap_id: i32, -) -> Result, SqlxError> { - sqlx::query_as!( - MSDRow, - r#"SELECT * FROM msd WHERE beatmap_id = $1 ORDER BY created_at DESC"#, - beatmap_id - ) - .fetch_all(pool) - .await -} diff --git a/src/models/msd/query/count_by_pattern.rs b/src/models/msd/query/count_by_pattern.rs deleted file mode 100644 index 006c898..0000000 --- a/src/models/msd/query/count_by_pattern.rs +++ /dev/null @@ -1,42 +0,0 @@ -use serde_json::Value; -use sqlx::{Error as SqlxError, PgPool}; -use std::collections::HashMap; - -/// Compte le nombre de beatmaps par pattern en utilisant main_patterns avec rate = 1.0 -pub async fn count_beatmaps_by_pattern(pool: &PgPool) -> Result, SqlxError> { - // Requête optimisée avec index sur rate et main_pattern - // Utiliser LIMIT pour éviter de traiter trop de données - let rows = sqlx::query!( - r#" - SELECT main_pattern, COUNT(*) as count - FROM msd - WHERE rate = 1.0 AND main_pattern IS NOT NULL - GROUP BY main_pattern - ORDER BY count DESC - LIMIT 20 - "# - ) - .fetch_all(pool) - .await?; - - let mut pattern_counts: HashMap = HashMap::new(); - - for row in rows { - if let Some(pattern_json) = row.main_pattern { - // Parser le JSON string pour extraire les patterns - if let Ok(json_value) = serde_json::from_str::(&pattern_json) { - if let Some(array) = json_value.as_array() { - // Prendre le premier pattern du tableau (le plus dominant) - if let Some(first_pattern) = array.first() { - if let Some(pattern_str) = first_pattern.as_str() { - *pattern_counts.entry(pattern_str.to_string()).or_insert(0) += - row.count.unwrap_or(0) as u64; - } - } - } - } - } - } - - Ok(pattern_counts) -} diff --git a/src/models/msd/query/insert.rs b/src/models/msd/query/insert.rs deleted file mode 100644 index 9e2308d..0000000 --- a/src/models/msd/query/insert.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::define_insert_returning_id; -use crate::models::msd::types::MSDRow; - -define_insert_returning_id!( - insert, - "msd", - MSDRow, - beatmap_id, - overall, - stream, - jumpstream, - handstream, - stamina, - jackspeed, - chordjack, - technical, - rate, - main_pattern -); diff --git a/src/models/msd/query/mod.rs b/src/models/msd/query/mod.rs deleted file mode 100644 index fa99725..0000000 --- a/src/models/msd/query/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod by_beatmap_id; -pub mod by_id; -pub mod count_by_pattern; -pub mod insert; - -pub use by_beatmap_id::*; -pub use by_id::*; -pub use count_by_pattern::*; -pub use insert::*; diff --git a/src/models/msd/tests/mod.rs b/src/models/msd/tests/mod.rs deleted file mode 100644 index 8695201..0000000 --- a/src/models/msd/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod validation; diff --git a/src/models/msd/tests/validation/main_pattern_tests.rs b/src/models/msd/tests/validation/main_pattern_tests.rs deleted file mode 100644 index 7fbe6da..0000000 --- a/src/models/msd/tests/validation/main_pattern_tests.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::models::msd::validators::validate_main_pattern; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_main_pattern_valid_simple_json() { - let pattern = r#"{"type": "stream"}"#.to_string(); - assert!(validate_main_pattern(&pattern).is_ok()); - } - - #[test] - fn test_validate_main_pattern_valid_complex_json() { - let pattern = r#"{"patterns": ["stream", "jumpstream", "handstream"], "difficulty": 5.5}"# - .to_string(); - assert!(validate_main_pattern(&pattern).is_ok()); - } - - #[test] - fn test_validate_main_pattern_valid_array_json() { - let pattern = r#"["stream", "jumpstream", "handstream"]"#.to_string(); - assert!(validate_main_pattern(&pattern).is_ok()); - } - - #[test] - fn test_validate_main_pattern_valid_nested_json() { - let pattern = - r#"{"stream": {"difficulty": 6.0, "notes": 100}, "jumpstream": {"difficulty": 4.5}}"# - .to_string(); - assert!(validate_main_pattern(&pattern).is_ok()); - } - - #[test] - fn test_validate_main_pattern_invalid_empty() { - let pattern = "".to_string(); - let result = validate_main_pattern(&pattern); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "main_pattern_cannot_be_empty"); - } - - #[test] - fn test_validate_main_pattern_invalid_too_long() { - let long_pattern = "a".repeat(1001); - let pattern = long_pattern; - let result = validate_main_pattern(&pattern); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "main_pattern_too_long"); - } - - #[test] - fn test_validate_main_pattern_invalid_not_json() { - let pattern = "stream+jumpstream+handstream".to_string(); - let result = validate_main_pattern(&pattern); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "main_pattern_invalid_json"); - } - - #[test] - fn test_validate_main_pattern_invalid_malformed_json() { - let pattern = r#"{"type": "stream", "difficulty": 5.5"#.to_string(); // Missing closing brace - let result = validate_main_pattern(&pattern); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "main_pattern_invalid_json"); - } - - #[test] - fn test_validate_main_pattern_invalid_json_with_trailing_comma() { - let pattern = r#"{"type": "stream", "difficulty": 5.5,}"#.to_string(); // Trailing comma - let result = validate_main_pattern(&pattern); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "main_pattern_invalid_json"); - } -} diff --git a/src/models/msd/tests/validation/mod.rs b/src/models/msd/tests/validation/mod.rs deleted file mode 100644 index 96ed619..0000000 --- a/src/models/msd/tests/validation/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod main_pattern_tests; -pub mod msd_value_tests; -pub mod rate_value_tests; diff --git a/src/models/msd/tests/validation/msd_value_tests.rs b/src/models/msd/tests/validation/msd_value_tests.rs deleted file mode 100644 index a97e1c1..0000000 --- a/src/models/msd/tests/validation/msd_value_tests.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::models::msd::validators::validate_msd_value; -use bigdecimal::BigDecimal; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_msd_value_valid_zero() { - let value = BigDecimal::from(0); - assert!(validate_msd_value(&value).is_ok()); - } - - #[test] - fn test_validate_msd_value_valid_positive() { - let value = BigDecimal::from(5); - assert!(validate_msd_value(&value).is_ok()); - } - - #[test] - fn test_validate_msd_value_valid_max() { - let value = BigDecimal::from(100); - assert!(validate_msd_value(&value).is_ok()); - } - - #[test] - fn test_validate_msd_value_invalid_negative() { - let value = BigDecimal::from(-1); - let result = validate_msd_value(&value); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "msd_value_out_of_range"); - } - - #[test] - fn test_validate_msd_value_invalid_too_high() { - let value = BigDecimal::from(101); - let result = validate_msd_value(&value); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "msd_value_out_of_range"); - } -} diff --git a/src/models/msd/tests/validation/rate_value_tests.rs b/src/models/msd/tests/validation/rate_value_tests.rs deleted file mode 100644 index 4451c40..0000000 --- a/src/models/msd/tests/validation/rate_value_tests.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::models::msd::validators::validate_rate_value; -use bigdecimal::{BigDecimal, FromPrimitive}; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_rate_value_valid_none() { - assert!(validate_rate_value(&BigDecimal::from_f64(0.5).unwrap()).is_ok()); - } - - #[test] - fn test_validate_rate_value_valid_min() { - let value = BigDecimal::from_f64(0.5).unwrap(); - assert!(validate_rate_value(&value).is_ok()); - } - - #[test] - fn test_validate_rate_value_valid_normal() { - let value = BigDecimal::from_f64(1.0).unwrap(); - assert!(validate_rate_value(&value).is_ok()); - } - - #[test] - fn test_validate_rate_value_valid_max() { - let value = BigDecimal::from_f64(2.0).unwrap(); - assert!(validate_rate_value(&value).is_ok()); - } - - #[test] - fn test_validate_rate_value_invalid_too_low() { - let value = BigDecimal::from_f64(0.4).unwrap(); - let result = validate_rate_value(&value); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "rate_value_out_of_range"); - } - - #[test] - fn test_validate_rate_value_invalid_too_high() { - let value = BigDecimal::from_f64(2.1).unwrap(); - let result = validate_rate_value(&value); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "rate_value_out_of_range"); - } - - #[test] - fn test_validate_rate_value_invalid_negative() { - let value = BigDecimal::from_f64(-0.5).unwrap(); - let result = validate_rate_value(&value); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "rate_value_out_of_range"); - } -} diff --git a/src/models/msd/types.rs b/src/models/msd/types.rs deleted file mode 100644 index 8c45c45..0000000 --- a/src/models/msd/types.rs +++ /dev/null @@ -1,47 +0,0 @@ -use bigdecimal::BigDecimal; -use chrono::NaiveDateTime; -use validator::Validate; - -use crate::models::msd::validators::*; - -#[derive(Debug, Clone, Validate)] -pub struct MSDRow { - #[validate(range(min = 1, message = "ID must be positive"))] - pub id: Option, - - #[validate(range(min = 1, message = "Beatmap ID must be positive"))] - pub beatmap_id: Option, - - #[validate(custom(function = "validate_msd_value"))] - pub overall: Option, - - #[validate(custom(function = "validate_msd_value"))] - pub stream: Option, - - #[validate(custom(function = "validate_msd_value"))] - pub jumpstream: Option, - - #[validate(custom(function = "validate_msd_value"))] - pub handstream: Option, - - #[validate(custom(function = "validate_msd_value"))] - pub stamina: Option, - - #[validate(custom(function = "validate_msd_value"))] - pub jackspeed: Option, - - #[validate(custom(function = "validate_msd_value"))] - pub chordjack: Option, - - #[validate(custom(function = "validate_msd_value"))] - pub technical: Option, - - #[validate(custom(function = "validate_rate_value"))] - pub rate: Option, - - #[validate(custom(function = "validate_main_pattern"))] - pub main_pattern: Option, - - pub created_at: Option, - pub updated_at: Option, -} diff --git a/src/models/msd/validators.rs b/src/models/msd/validators.rs deleted file mode 100644 index 94be9b5..0000000 --- a/src/models/msd/validators.rs +++ /dev/null @@ -1,36 +0,0 @@ -use bigdecimal::BigDecimal; -use bigdecimal::FromPrimitive; -use validator::ValidationError; - -/// Validates that a MSD value is within valid range (0-20) if provided. -pub fn validate_msd_value(value: &BigDecimal) -> Result<(), ValidationError> { - if *value < BigDecimal::from(0) || *value > BigDecimal::from(100) { - return Err(ValidationError::new("msd_value_out_of_range")); - } - Ok(()) -} - -/// Validates that a rate value is within valid range (0.5-2.0) if provided. -pub fn validate_rate_value(rate: &BigDecimal) -> Result<(), ValidationError> { - if *rate < BigDecimal::from_f64(0.5).unwrap() || *rate > BigDecimal::from_f64(2.0).unwrap() { - return Err(ValidationError::new("rate_value_out_of_range")); - } - Ok(()) -} - -/// Validates that a main pattern is a valid JSON string if provided. -pub fn validate_main_pattern(pattern: &str) -> Result<(), ValidationError> { - if pattern.is_empty() { - return Err(ValidationError::new("main_pattern_cannot_be_empty")); - } - if pattern.len() > 1000 { - return Err(ValidationError::new("main_pattern_too_long")); - } - - // Validate that it's valid JSON - if serde_json::from_str::(pattern).is_err() { - return Err(ValidationError::new("main_pattern_invalid_json")); - } - - Ok(()) -} diff --git a/src/models/failed_query/impl.rs b/src/models/other/failed_query/impl.rs similarity index 51% rename from src/models/failed_query/impl.rs rename to src/models/other/failed_query/impl.rs index 5fc1b94..83f19d5 100644 --- a/src/models/failed_query/impl.rs +++ b/src/models/other/failed_query/impl.rs @@ -1,4 +1,4 @@ -use super::query::{delete_by_hash, exists_by_hash, find_by_id, insert}; +use super::query::{find_by_id, insert}; use super::FailedQueryRow; use sqlx::{Error as SqlxError, PgPool}; @@ -7,15 +7,7 @@ impl FailedQueryRow { insert(pool, self).await } - pub async fn exists_by_hash(pool: &PgPool, hash: &str) -> Result { - exists_by_hash(pool, hash).await - } - pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) } - - pub async fn delete_by_hash(pool: &PgPool, hash: &str) -> Result { - delete_by_hash(pool, hash).await - } } diff --git a/src/models/failed_query/mod.rs b/src/models/other/failed_query/mod.rs similarity index 100% rename from src/models/failed_query/mod.rs rename to src/models/other/failed_query/mod.rs diff --git a/src/models/failed_query/query/by_id.rs b/src/models/other/failed_query/query/by_id.rs similarity index 84% rename from src/models/failed_query/query/by_id.rs rename to src/models/other/failed_query/query/by_id.rs index 1a10e8e..fa9677f 100644 --- a/src/models/failed_query/query/by_id.rs +++ b/src/models/other/failed_query/query/by_id.rs @@ -1,4 +1,4 @@ -use crate::models::failed_query::types::FailedQueryRow; +use crate::models::other::failed_query::types::FailedQueryRow; use sqlx::{Error as SqlxError, PgPool}; pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { diff --git a/src/models/failed_query/query/insert.rs b/src/models/other/failed_query/query/insert.rs similarity index 69% rename from src/models/failed_query/query/insert.rs rename to src/models/other/failed_query/query/insert.rs index cc0e74e..fb56abb 100644 --- a/src/models/failed_query/query/insert.rs +++ b/src/models/other/failed_query/query/insert.rs @@ -1,5 +1,5 @@ use crate::define_insert_returning_id; -use crate::models::failed_query::types::FailedQueryRow; +use crate::models::other::failed_query::types::FailedQueryRow; // no extra imports needed define_insert_returning_id!(insert, "failed_query", FailedQueryRow, hash); diff --git a/src/models/other/failed_query/query/mod.rs b/src/models/other/failed_query/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/other/failed_query/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/other/failed_query/tests/mod.rs b/src/models/other/failed_query/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/other/failed_query/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/failed_query/types.rs b/src/models/other/failed_query/types.rs similarity index 63% rename from src/models/failed_query/types.rs rename to src/models/other/failed_query/types.rs index ae0de6c..bea0261 100644 --- a/src/models/failed_query/types.rs +++ b/src/models/other/failed_query/types.rs @@ -1,7 +1,8 @@ -use crate::utils::HASH_REGEX; use chrono::NaiveDateTime; use validator::Validate; +use crate::utils::HASH_REGEX; + #[derive(Debug, Clone, sqlx::FromRow, Validate)] pub struct FailedQueryRow { /// Unique identifier for the failed query record. @@ -9,17 +10,16 @@ pub struct FailedQueryRow { #[validate(range(min = 1, message = "ID must be positive"))] pub id: i32, - /// Hash identifier of the failed query. - /// Must be between 1 and 255 characters and contain only alphanumeric characters. + /// Hash of the failed query. + /// Must be between 1 and 255 characters. #[validate(length( min = 1, max = 255, message = "Hash must be between 1 and 255 characters" ))] - #[validate(regex(path = "*HASH_REGEX"))] + #[validate(regex(path = *HASH_REGEX))] pub hash: String, - /// Timestamp when the failed query was recorded. - /// This field is optional and can be `None` if the timestamp is not available. + /// Timestamp when the failed query was created. pub created_at: Option, } diff --git a/src/models/other/mod.rs b/src/models/other/mod.rs new file mode 100644 index 0000000..4ec0fde --- /dev/null +++ b/src/models/other/mod.rs @@ -0,0 +1,4 @@ +pub mod failed_query; + +// Re-exports for easy access +pub use failed_query::*; diff --git a/src/models/pending_beatmap/impl.rs b/src/models/pending_beatmap/impl.rs deleted file mode 100644 index ba50f12..0000000 --- a/src/models/pending_beatmap/impl.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::query::*; -use super::PendingBeatmapRow; -use sqlx::PgPool; - -impl PendingBeatmapRow { - pub async fn insert(self, pool: &PgPool) -> Result { - insert(pool, self).await - } - - pub async fn delete_by_id(pool: &PgPool, id: i32) -> Result { - delete_by_id(pool, id).await - } - - pub async fn delete_by_hash(pool: &PgPool, hash: &str) -> Result { - delete_by_hash(pool, hash).await - } - - pub async fn count(pool: &PgPool) -> Result { - count(pool).await - } - - pub async fn oldest(pool: &PgPool) -> Result, sqlx::Error> { - oldest(pool).await - } - - pub async fn bulk_insert(pool: &PgPool, hashes: &[String]) -> Result { - bulk_insert(pool, hashes).await - } - - pub async fn position_by_osu_id( - pool: &PgPool, - osu_id: i32, - ) -> Result, sqlx::Error> { - position_by_osu_id(pool, osu_id).await - } -} diff --git a/src/models/pending_beatmap/query/bulk_insert.rs b/src/models/pending_beatmap/query/bulk_insert.rs deleted file mode 100644 index 8b29176..0000000 --- a/src/models/pending_beatmap/query/bulk_insert.rs +++ /dev/null @@ -1,22 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn bulk_insert(pool: &PgPool, hashes: &[String]) -> Result { - if hashes.is_empty() { - return Ok(0); - } - - let placeholders: Vec = (1..=hashes.len()).map(|i| format!("(${})", i)).collect(); - - let query = format!( - "INSERT INTO pending_beatmap (hash) VALUES {} ON CONFLICT (hash) DO NOTHING", - placeholders.join(", ") - ); - - let mut q = sqlx::query(&query); - for hash in hashes { - q = q.bind(hash); - } - - // Retourne le nombre de lignes affectées - Ok(q.execute(pool).await?.rows_affected() as usize) -} diff --git a/src/models/pending_beatmap/query/count.rs b/src/models/pending_beatmap/query/count.rs deleted file mode 100644 index c5e81db..0000000 --- a/src/models/pending_beatmap/query/count.rs +++ /dev/null @@ -1,7 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn count(pool: &PgPool) -> Result { - sqlx::query_scalar::<_, i64>(r#"SELECT COUNT(*) FROM pending_beatmap"#) - .fetch_one(pool) - .await -} diff --git a/src/models/pending_beatmap/query/delete.rs b/src/models/pending_beatmap/query/delete.rs deleted file mode 100644 index fa9a06c..0000000 --- a/src/models/pending_beatmap/query/delete.rs +++ /dev/null @@ -1,19 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn delete_by_id(pool: &PgPool, id: i32) -> Result { - let result = sqlx::query(r#"DELETE FROM pending_beatmap WHERE id = $1"#) - .bind(id) - .execute(pool) - .await?; - - Ok(result.rows_affected()) -} - -pub async fn delete_by_hash(pool: &PgPool, hash: &str) -> Result { - let result = sqlx::query(r#"DELETE FROM pending_beatmap WHERE hash = $1"#) - .bind(hash) - .execute(pool) - .await?; - - Ok(result.rows_affected()) -} diff --git a/src/models/pending_beatmap/query/insert.rs b/src/models/pending_beatmap/query/insert.rs deleted file mode 100644 index 978b695..0000000 --- a/src/models/pending_beatmap/query/insert.rs +++ /dev/null @@ -1,4 +0,0 @@ -use crate::define_insert_returning_id; -use crate::models::pending_beatmap::types::PendingBeatmapRow; - -define_insert_returning_id!(insert, "pending_beatmap", PendingBeatmapRow, hash, osu_id); diff --git a/src/models/pending_beatmap/query/mod.rs b/src/models/pending_beatmap/query/mod.rs deleted file mode 100644 index f3c8dea..0000000 --- a/src/models/pending_beatmap/query/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub mod bulk_insert; -pub mod count; -pub mod delete; -pub mod insert; -pub mod oldest; -pub mod position_by_osu_id; -pub use bulk_insert::*; -pub use count::*; -pub use delete::*; -pub use insert::*; -pub use oldest::*; -pub use position_by_osu_id::*; diff --git a/src/models/pending_beatmap/query/oldest.rs b/src/models/pending_beatmap/query/oldest.rs deleted file mode 100644 index 70a509d..0000000 --- a/src/models/pending_beatmap/query/oldest.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::models::pending_beatmap::PendingBeatmapRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn oldest(pool: &PgPool) -> Result, SqlxError> { - let row = sqlx::query_as!( - PendingBeatmapRow, - r#" - SELECT id, hash, osu_id, created_at - FROM pending_beatmap - ORDER BY created_at ASC, id ASC - LIMIT 1 - "# - ) - .fetch_optional(pool) - .await?; - - Ok(row) -} diff --git a/src/models/pending_beatmap/query/position_by_osu_id.rs b/src/models/pending_beatmap/query/position_by_osu_id.rs deleted file mode 100644 index 837a869..0000000 --- a/src/models/pending_beatmap/query/position_by_osu_id.rs +++ /dev/null @@ -1,21 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn position_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, SqlxError> { - let position = sqlx::query_scalar::<_, i64>( - r#" - SELECT position - FROM ( - SELECT - osu_id, - ROW_NUMBER() OVER (ORDER BY created_at ASC, id ASC) as position - FROM pending_beatmap - ) ranked - WHERE osu_id = $1 - "#, - ) - .bind(osu_id) - .fetch_optional(pool) - .await?; - - Ok(position) -} diff --git a/src/models/pending_beatmap/tests/mod.rs b/src/models/pending_beatmap/tests/mod.rs deleted file mode 100644 index 8695201..0000000 --- a/src/models/pending_beatmap/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod validation; diff --git a/src/models/pending_beatmap/tests/validation/hash_tests.rs b/src/models/pending_beatmap/tests/validation/hash_tests.rs deleted file mode 100644 index 8d92f93..0000000 --- a/src/models/pending_beatmap/tests/validation/hash_tests.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::models::pending_beatmap::PendingBeatmapRow; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pending_beatmap_hash_validation_valid_alphanumeric() { - let pending_beatmap = PendingBeatmapRow { - id: 1, - hash: "abc123".to_string(), - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap.validate().is_ok()); - - let pending_beatmap2 = PendingBeatmapRow { - id: 1, - hash: "ABC123".to_string(), - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap2.validate().is_ok()); - } - - #[test] - fn test_pending_beatmap_hash_validation_valid_single_character() { - let pending_beatmap = PendingBeatmapRow { - id: 1, - hash: "a".to_string(), - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap.validate().is_ok()); - - let pending_beatmap2 = PendingBeatmapRow { - id: 1, - hash: "1".to_string(), - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap2.validate().is_ok()); - } - - #[test] - fn test_pending_beatmap_hash_validation_invalid_empty() { - let pending_beatmap = PendingBeatmapRow { - id: 1, - hash: "".to_string(), - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap.validate().is_err()); - } - - #[test] - fn test_pending_beatmap_hash_validation_invalid_too_long() { - let long_hash = "a".repeat(256); - let pending_beatmap = PendingBeatmapRow { - id: 1, - hash: long_hash, - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap.validate().is_err()); - } - - #[test] - fn test_pending_beatmap_hash_validation_invalid_with_special_characters() { - let pending_beatmap = PendingBeatmapRow { - id: 1, - hash: "abc-123".to_string(), - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap.validate().is_err()); - - let pending_beatmap2 = PendingBeatmapRow { - id: 1, - hash: "abc_123".to_string(), - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap2.validate().is_err()); - } - - #[test] - fn test_pending_beatmap_hash_validation_invalid_with_spaces() { - let pending_beatmap = PendingBeatmapRow { - id: 1, - hash: "abc 123".to_string(), - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap.validate().is_err()); - } - - #[test] - fn test_pending_beatmap_id_validation_invalid_negative() { - let pending_beatmap = PendingBeatmapRow { - id: -1, - hash: "abc123".to_string(), - osu_id: Some(12345), - created_at: None, - }; - assert!(pending_beatmap.validate().is_err()); - } - - #[test] - fn test_pending_beatmap_osu_id_validation_invalid_negative() { - let pending_beatmap = PendingBeatmapRow { - id: 1, - hash: "abc123".to_string(), - osu_id: Some(-1), - created_at: None, - }; - assert!(pending_beatmap.validate().is_err()); - } -} diff --git a/src/models/pending_beatmap/tests/validation/mod.rs b/src/models/pending_beatmap/tests/validation/mod.rs deleted file mode 100644 index 10ee565..0000000 --- a/src/models/pending_beatmap/tests/validation/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod hash_tests; diff --git a/src/models/rating/beatmap_mania_rating/impl.rs b/src/models/rating/beatmap_mania_rating/impl.rs new file mode 100644 index 0000000..efded22 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::BeatmapManiaRatingRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl BeatmapManiaRatingRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/rating/beatmap_mania_rating/mod.rs b/src/models/rating/beatmap_mania_rating/mod.rs new file mode 100644 index 0000000..9d05f5c --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::BeatmapManiaRatingRow; diff --git a/src/models/rating/beatmap_mania_rating/query/by_id.rs b/src/models/rating/beatmap_mania_rating/query/by_id.rs new file mode 100644 index 0000000..59166f1 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/query/by_id.rs @@ -0,0 +1,19 @@ +use crate::models::rating::beatmap_mania_rating::types::BeatmapManiaRatingRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id( + pool: &PgPool, + id: i32, +) -> Result, SqlxError> { + sqlx::query_as!( + BeatmapManiaRatingRow, + r#" + SELECT id, rating_id, stream, jumpstream, handstream, stamina, jackspeed, chordjack, technical, created_at, updated_at + FROM beatmap_mania_rating + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/rating/beatmap_mania_rating/query/insert.rs b/src/models/rating/beatmap_mania_rating/query/insert.rs new file mode 100644 index 0000000..627f4a5 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/query/insert.rs @@ -0,0 +1,17 @@ +use crate::define_insert_returning_id; +use crate::models::rating::beatmap_mania_rating::types::BeatmapManiaRatingRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "beatmap_mania_rating", + BeatmapManiaRatingRow, + rating_id, + stream, + jumpstream, + handstream, + stamina, + jackspeed, + chordjack, + technical +); diff --git a/src/models/rating/beatmap_mania_rating/query/mod.rs b/src/models/rating/beatmap_mania_rating/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/rating/beatmap_mania_rating/tests/mod.rs b/src/models/rating/beatmap_mania_rating/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/rating/beatmap_mania_rating/types.rs b/src/models/rating/beatmap_mania_rating/types.rs new file mode 100644 index 0000000..b206ab7 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/types.rs @@ -0,0 +1,49 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct BeatmapManiaRatingRow { + /// Unique identifier for the beatmap mania rating record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Reference to the beatmap rating record this mania rating applies to. + /// Optional field, can be None. + pub rating_id: Option, + + /// Stream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub stream: Option, + + /// Jumpstream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub jumpstream: Option, + + /// Handstream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub handstream: Option, + + /// Stamina difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub stamina: Option, + + /// Jackspeed difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub jackspeed: Option, + + /// Chordjack difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub chordjack: Option, + + /// Technical difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub technical: Option, + + /// Timestamp when the mania rating was created. + pub created_at: Option, + + /// Timestamp when the mania rating was last updated. + pub updated_at: Option, +} diff --git a/src/models/rating/beatmap_rating/impl.rs b/src/models/rating/beatmap_rating/impl.rs new file mode 100644 index 0000000..7625b75 --- /dev/null +++ b/src/models/rating/beatmap_rating/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::BeatmapRatingRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl BeatmapRatingRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/rating/beatmap_rating/mod.rs b/src/models/rating/beatmap_rating/mod.rs new file mode 100644 index 0000000..2c3572f --- /dev/null +++ b/src/models/rating/beatmap_rating/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::BeatmapRatingRow; diff --git a/src/models/rating/beatmap_rating/query/by_id.rs b/src/models/rating/beatmap_rating/query/by_id.rs new file mode 100644 index 0000000..148e444 --- /dev/null +++ b/src/models/rating/beatmap_rating/query/by_id.rs @@ -0,0 +1,16 @@ +use crate::models::rating::beatmap_rating::types::BeatmapRatingRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + BeatmapRatingRow, + r#" + SELECT id, rates_id, rating, rating_type, created_at + FROM beatmap_rating + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/rating/beatmap_rating/query/insert.rs b/src/models/rating/beatmap_rating/query/insert.rs new file mode 100644 index 0000000..bc632fa --- /dev/null +++ b/src/models/rating/beatmap_rating/query/insert.rs @@ -0,0 +1,12 @@ +use crate::define_insert_returning_id; +use crate::models::rating::beatmap_rating::types::BeatmapRatingRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "beatmap_rating", + BeatmapRatingRow, + rates_id, + rating, + rating_type +); diff --git a/src/models/rating/beatmap_rating/query/mod.rs b/src/models/rating/beatmap_rating/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/rating/beatmap_rating/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/rating/beatmap_rating/tests/mod.rs b/src/models/rating/beatmap_rating/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/rating/beatmap_rating/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/rating/beatmap_rating/types.rs b/src/models/rating/beatmap_rating/types.rs new file mode 100644 index 0000000..12c2a5e --- /dev/null +++ b/src/models/rating/beatmap_rating/types.rs @@ -0,0 +1,34 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct BeatmapRatingRow { + /// Unique identifier for the beatmap rating record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Reference to the rates record this rating applies to. + /// Optional field, can be None. + pub rates_id: Option, + + /// Rating value for the beatmap. + /// Must be a positive decimal value. + pub rating: BigDecimal, + + /// Type of rating system used. + /// Must be one of: 'osu', 'etterna', 'quaver', 'malody', 'interlude'. + #[validate(custom(function = "validate_rating_type"))] + pub rating_type: String, + + /// Timestamp when the rating was created. + pub created_at: Option, +} + +fn validate_rating_type(rating_type: &str) -> Result<(), validator::ValidationError> { + match rating_type { + "osu" | "etterna" | "quaver" | "malody" | "interlude" => Ok(()), + _ => Err(validator::ValidationError::new("invalid_rating_type")), + } +} diff --git a/src/models/rating/mod.rs b/src/models/rating/mod.rs new file mode 100644 index 0000000..57bd3b2 --- /dev/null +++ b/src/models/rating/mod.rs @@ -0,0 +1,4 @@ +pub mod beatmap_mania_rating; +pub mod beatmap_rating; +pub mod score_mania_rating; +pub mod score_rating; diff --git a/src/models/rating/score_mania_rating/impl.rs b/src/models/rating/score_mania_rating/impl.rs new file mode 100644 index 0000000..b2d495e --- /dev/null +++ b/src/models/rating/score_mania_rating/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::ScoreManiaRatingRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl ScoreManiaRatingRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/rating/score_mania_rating/mod.rs b/src/models/rating/score_mania_rating/mod.rs new file mode 100644 index 0000000..65e20f7 --- /dev/null +++ b/src/models/rating/score_mania_rating/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::ScoreManiaRatingRow; diff --git a/src/models/rating/score_mania_rating/query/by_id.rs b/src/models/rating/score_mania_rating/query/by_id.rs new file mode 100644 index 0000000..50f1874 --- /dev/null +++ b/src/models/rating/score_mania_rating/query/by_id.rs @@ -0,0 +1,16 @@ +use crate::models::rating::score_mania_rating::types::ScoreManiaRatingRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + ScoreManiaRatingRow, + r#" + SELECT id, rating_id, stream, jumpstream, handstream, stamina, jackspeed, chordjack, technical, created_at + FROM score_mania_rating + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/rating/score_mania_rating/query/insert.rs b/src/models/rating/score_mania_rating/query/insert.rs new file mode 100644 index 0000000..04a9b50 --- /dev/null +++ b/src/models/rating/score_mania_rating/query/insert.rs @@ -0,0 +1,17 @@ +use crate::define_insert_returning_id; +use crate::models::rating::score_mania_rating::types::ScoreManiaRatingRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "score_mania_rating", + ScoreManiaRatingRow, + rating_id, + stream, + jumpstream, + handstream, + stamina, + jackspeed, + chordjack, + technical +); diff --git a/src/models/rating/score_mania_rating/query/mod.rs b/src/models/rating/score_mania_rating/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/rating/score_mania_rating/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/rating/score_mania_rating/tests/mod.rs b/src/models/rating/score_mania_rating/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/rating/score_mania_rating/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/rating/score_mania_rating/types.rs b/src/models/rating/score_mania_rating/types.rs new file mode 100644 index 0000000..a50e6a8 --- /dev/null +++ b/src/models/rating/score_mania_rating/types.rs @@ -0,0 +1,46 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct ScoreManiaRatingRow { + /// Unique identifier for the score mania rating record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Reference to the score rating record this mania rating applies to. + /// Optional field, can be None. + pub rating_id: Option, + + /// Stream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub stream: Option, + + /// Jumpstream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub jumpstream: Option, + + /// Handstream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub handstream: Option, + + /// Stamina difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub stamina: Option, + + /// Jackspeed difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub jackspeed: Option, + + /// Chordjack difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub chordjack: Option, + + /// Technical difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + pub technical: Option, + + /// Timestamp when the score mania rating was created. + pub created_at: Option, +} diff --git a/src/models/rating/score_rating/impl.rs b/src/models/rating/score_rating/impl.rs new file mode 100644 index 0000000..58178b0 --- /dev/null +++ b/src/models/rating/score_rating/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::ScoreRatingRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl ScoreRatingRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/rating/score_rating/mod.rs b/src/models/rating/score_rating/mod.rs new file mode 100644 index 0000000..8f8bd0b --- /dev/null +++ b/src/models/rating/score_rating/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::ScoreRatingRow; diff --git a/src/models/score_rating/query/by_id.rs b/src/models/rating/score_rating/query/by_id.rs similarity index 68% rename from src/models/score_rating/query/by_id.rs rename to src/models/rating/score_rating/query/by_id.rs index 52b7880..c5af630 100644 --- a/src/models/score_rating/query/by_id.rs +++ b/src/models/rating/score_rating/query/by_id.rs @@ -1,18 +1,16 @@ -use super::super::types::ScoreRatingRow; +use crate::models::rating::score_rating::types::ScoreRatingRow; use sqlx::{Error as SqlxError, PgPool}; pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { - let score_rating = sqlx::query_as!( + sqlx::query_as!( ScoreRatingRow, r#" SELECT id, score_id, rating, rating_type, created_at - FROM score_rating + FROM score_rating WHERE id = $1 "#, id ) .fetch_optional(pool) - .await?; - - Ok(score_rating) + .await } diff --git a/src/models/rating/score_rating/query/insert.rs b/src/models/rating/score_rating/query/insert.rs new file mode 100644 index 0000000..a2f4d9e --- /dev/null +++ b/src/models/rating/score_rating/query/insert.rs @@ -0,0 +1,12 @@ +use crate::define_insert_returning_id; +use crate::models::rating::score_rating::types::ScoreRatingRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "score_rating", + ScoreRatingRow, + score_id, + rating, + rating_type +); diff --git a/src/models/rating/score_rating/query/mod.rs b/src/models/rating/score_rating/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/rating/score_rating/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/rating/score_rating/tests/mod.rs b/src/models/rating/score_rating/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/rating/score_rating/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/rating/score_rating/types.rs b/src/models/rating/score_rating/types.rs new file mode 100644 index 0000000..206d083 --- /dev/null +++ b/src/models/rating/score_rating/types.rs @@ -0,0 +1,35 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct ScoreRatingRow { + /// Unique identifier for the score rating record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Reference to the score record this rating applies to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Score ID must be positive"))] + pub score_id: i32, + + /// Rating value for the score. + /// Must be a positive decimal value. + pub rating: BigDecimal, + + /// Type of rating system used. + /// Must be one of: 'osu', 'etterna', 'quaver', 'malody', 'interlude'. + #[validate(custom(function = "validate_rating_type"))] + pub rating_type: String, + + /// Timestamp when the score rating was created. + pub created_at: Option, +} + +fn validate_rating_type(rating_type: &str) -> Result<(), validator::ValidationError> { + match rating_type { + "osu" | "etterna" | "quaver" | "malody" | "interlude" => Ok(()), + _ => Err(validator::ValidationError::new("invalid_rating_type")), + } +} diff --git a/src/models/replay/impl.rs b/src/models/replay/impl.rs deleted file mode 100644 index 33dcd77..0000000 --- a/src/models/replay/impl.rs +++ /dev/null @@ -1,9 +0,0 @@ -use super::query::insert::insert; -use super::types::ReplayRow; -use sqlx::PgPool; - -impl ReplayRow { - pub async fn insert(pool: &PgPool, hash: &str, replay_path: &str) -> Result { - insert(pool, hash, replay_path).await - } -} diff --git a/src/models/replay/query/insert.rs b/src/models/replay/query/insert.rs deleted file mode 100644 index 119c6af..0000000 --- a/src/models/replay/query/insert.rs +++ /dev/null @@ -1,15 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool, Row}; - -pub async fn insert(pool: &PgPool, hash: &str, replay_path: &str) -> Result { - let rec = sqlx::QueryBuilder::new( - "INSERT INTO replays (hash, replay_available, replay_path, created_at) VALUES (", - ) - .push_bind(hash) - .push(", true, ") - .push_bind(replay_path) - .push(", NOW()) RETURNING id") - .build() - .fetch_one(pool) - .await?; - rec.try_get("id") -} diff --git a/src/models/replay/query/mod.rs b/src/models/replay/query/mod.rs deleted file mode 100644 index 885e55f..0000000 --- a/src/models/replay/query/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod by_id; -pub mod insert; - -pub use by_id::find_by_id; -pub use insert::insert; diff --git a/src/models/replay/tests/mod.rs b/src/models/replay/tests/mod.rs deleted file mode 100644 index 8695201..0000000 --- a/src/models/replay/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod validation; diff --git a/src/models/replay/tests/validation/hash_tests.rs b/src/models/replay/tests/validation/hash_tests.rs deleted file mode 100644 index 8b7ba4b..0000000 --- a/src/models/replay/tests/validation/hash_tests.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::models::replay::ReplayRow; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_replay_hash_validation_valid_alphanumeric() { - let replay = ReplayRow { - id: 1, - hash: "abc123".to_string(), - replay_available: true, - replay_path: "/path/to/replay.osr".to_string(), - created_at: None, - }; - assert!(replay.validate().is_ok()); - - let replay2 = ReplayRow { - id: 1, - hash: "ABC123".to_string(), - replay_available: true, - replay_path: "/path/to/replay.osr".to_string(), - created_at: None, - }; - assert!(replay2.validate().is_ok()); - } - - #[test] - fn test_replay_hash_validation_valid_single_character() { - let replay = ReplayRow { - id: 1, - hash: "a".to_string(), - replay_available: true, - replay_path: "/path/to/replay.osr".to_string(), - created_at: None, - }; - assert!(replay.validate().is_ok()); - } - - #[test] - fn test_replay_hash_validation_invalid_empty() { - let replay = ReplayRow { - id: 1, - hash: "".to_string(), - replay_available: true, - replay_path: "/path/to/replay.osr".to_string(), - created_at: None, - }; - assert!(replay.validate().is_err()); - } - - #[test] - fn test_replay_hash_validation_invalid_too_long() { - let long_hash = "a".repeat(256); - let replay = ReplayRow { - id: 1, - hash: long_hash, - replay_available: true, - replay_path: "/path/to/replay.osr".to_string(), - created_at: None, - }; - assert!(replay.validate().is_err()); - } - - #[test] - fn test_replay_hash_validation_invalid_with_special_characters() { - let replay = ReplayRow { - id: 1, - hash: "abc-123".to_string(), - replay_available: true, - replay_path: "/path/to/replay.osr".to_string(), - created_at: None, - }; - assert!(replay.validate().is_err()); - } - - #[test] - fn test_replay_id_validation_invalid_negative() { - let replay = ReplayRow { - id: -1, - hash: "abc123".to_string(), - replay_available: true, - replay_path: "/path/to/replay.osr".to_string(), - created_at: None, - }; - assert!(replay.validate().is_err()); - } -} diff --git a/src/models/replay/tests/validation/mod.rs b/src/models/replay/tests/validation/mod.rs deleted file mode 100644 index 5db4071..0000000 --- a/src/models/replay/tests/validation/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod hash_tests; -pub mod replay_path_tests; diff --git a/src/models/replay/tests/validation/replay_path_tests.rs b/src/models/replay/tests/validation/replay_path_tests.rs deleted file mode 100644 index b086c8b..0000000 --- a/src/models/replay/tests/validation/replay_path_tests.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::models::replay::ReplayRow; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_replay_path_validation_valid_normal() { - let replay = ReplayRow { - id: 1, - hash: "abc123".to_string(), - replay_available: true, - replay_path: "/path/to/replay.osr".to_string(), - created_at: None, - }; - assert!(replay.validate().is_ok()); - } - - #[test] - fn test_replay_path_validation_valid_short() { - let replay = ReplayRow { - id: 1, - hash: "abc123".to_string(), - replay_available: true, - replay_path: "a.osr".to_string(), - created_at: None, - }; - assert!(replay.validate().is_ok()); - } - - #[test] - fn test_replay_path_validation_valid_long() { - let long_path = "/very/long/path/to/replay/file/".repeat(10) + "replay.osr"; - let replay = ReplayRow { - id: 1, - hash: "abc123".to_string(), - replay_available: true, - replay_path: long_path, - created_at: None, - }; - assert!(replay.validate().is_ok()); - } - - #[test] - fn test_replay_path_validation_invalid_empty() { - let replay = ReplayRow { - id: 1, - hash: "abc123".to_string(), - replay_available: true, - replay_path: "".to_string(), - created_at: None, - }; - assert!(replay.validate().is_err()); - } - - #[test] - fn test_replay_path_validation_invalid_too_long() { - let too_long_path = "a".repeat(501); - let replay = ReplayRow { - id: 1, - hash: "abc123".to_string(), - replay_available: true, - replay_path: too_long_path, - created_at: None, - }; - assert!(replay.validate().is_err()); - } -} diff --git a/src/models/replay/types.rs b/src/models/replay/types.rs deleted file mode 100644 index 3e0f2c2..0000000 --- a/src/models/replay/types.rs +++ /dev/null @@ -1,33 +0,0 @@ -use chrono::NaiveDateTime; -use sqlx::FromRow; -use validator::Validate; - -use crate::utils::HASH_REGEX; - -#[derive(Debug, Clone, FromRow, Validate)] -pub struct ReplayRow { - #[validate(range(min = 1, message = "ID must be positive"))] - pub id: i32, - - #[validate(length( - min = 1, - max = 255, - message = "Hash must be between 1 and 255 characters" - ))] - #[validate(regex( - path = "*HASH_REGEX", - message = "Hash must contain only alphanumeric characters" - ))] - pub hash: String, - - pub replay_available: bool, - - #[validate(length( - min = 1, - max = 500, - message = "Replay path must be between 1 and 500 characters" - ))] - pub replay_path: String, - - pub created_at: Option, -} diff --git a/src/models/score/impl.rs b/src/models/score/impl.rs deleted file mode 100644 index 4aec8cf..0000000 --- a/src/models/score/impl.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::query::{ - by_beatmap_id::find_by_beatmap_id, - by_id::find_by_id, - by_pending::find_pending_score, - by_user_id::find_by_user_id, - exists::exists_by_hash, - insert::insert, - update_status::{update_status, update_status_by_hash}, -}; -use super::types::ScoreRow; -use sqlx::PgPool; - -impl ScoreRow { - pub async fn insert(self, pool: &PgPool) -> Result { - insert(pool, self).await - } - - pub async fn exists_by_hash(pool: &PgPool, hash: &str) -> Result { - exists_by_hash(pool, hash).await - } - - pub async fn find_pending_score(pool: &PgPool) -> Result, sqlx::Error> { - find_pending_score(pool).await - } - - pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, sqlx::Error> { - find_by_id(pool, id).await - } - - pub async fn find_by_user_id(pool: &PgPool, user_id: i64) -> Result, sqlx::Error> { - find_by_user_id(pool, user_id).await - } - - pub async fn find_by_beatmap_id( - pool: &PgPool, - beatmap_id: i32, - ) -> Result, sqlx::Error> { - find_by_beatmap_id(pool, beatmap_id).await - } - - pub async fn update_status(pool: &PgPool, id: i32, status: &str) -> Result { - update_status(pool, id, status).await - } - - pub async fn update_status_by_hash( - pool: &PgPool, - hash: &str, - status: &str, - ) -> Result { - update_status_by_hash(pool, hash, status).await - } -} diff --git a/src/models/score/mod.rs b/src/models/score/mod.rs index 7134bd1..0b9c27f 100644 --- a/src/models/score/mod.rs +++ b/src/models/score/mod.rs @@ -1,9 +1,3 @@ -pub mod r#impl; -pub mod query; -pub mod types; -pub mod validators; - -#[cfg(test)] -mod tests; - -pub use types::*; +pub mod replay; +pub mod score; +pub mod score_metadata; diff --git a/src/models/score/query/by_beatmap_id.rs b/src/models/score/query/by_beatmap_id.rs deleted file mode 100644 index 3983f9e..0000000 --- a/src/models/score/query/by_beatmap_id.rs +++ /dev/null @@ -1,23 +0,0 @@ -use super::super::types::ScoreRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn find_by_beatmap_id( - pool: &PgPool, - beatmap_id: i32, -) -> Result, SqlxError> { - let scores = sqlx::query_as!( - ScoreRow, - r#" - SELECT id, user_id, beatmap_id, score_metadata_id, replay_id, rate, - hwid, mods, hash, rank, status, created_at - FROM score - WHERE beatmap_id = $1 - ORDER BY created_at DESC - "#, - beatmap_id - ) - .fetch_all(pool) - .await?; - - Ok(scores) -} diff --git a/src/models/score/query/by_id.rs b/src/models/score/query/by_id.rs deleted file mode 100644 index 0143114..0000000 --- a/src/models/score/query/by_id.rs +++ /dev/null @@ -1,19 +0,0 @@ -use super::super::types::ScoreRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { - let score = sqlx::query_as!( - ScoreRow, - r#" - SELECT id, user_id, beatmap_id, score_metadata_id, replay_id, rate, - hwid, mods, hash, rank, status, created_at - FROM score - WHERE id = $1 - "#, - id - ) - .fetch_optional(pool) - .await?; - - Ok(score) -} diff --git a/src/models/score/query/by_pending.rs b/src/models/score/query/by_pending.rs deleted file mode 100644 index 3dbf993..0000000 --- a/src/models/score/query/by_pending.rs +++ /dev/null @@ -1,20 +0,0 @@ -use super::super::types::ScoreRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn find_pending_score(pool: &PgPool) -> Result, SqlxError> { - let score = sqlx::query_as!( - ScoreRow, - r#" - SELECT id, user_id, beatmap_id, score_metadata_id, replay_id, rate, - hwid, mods, hash, rank, status, created_at - FROM score - WHERE status = 'pending' - ORDER BY created_at ASC - LIMIT 1 - "# - ) - .fetch_optional(pool) - .await?; - - Ok(score) -} diff --git a/src/models/score/query/by_user_id.rs b/src/models/score/query/by_user_id.rs deleted file mode 100644 index edce5fb..0000000 --- a/src/models/score/query/by_user_id.rs +++ /dev/null @@ -1,20 +0,0 @@ -use super::super::types::ScoreRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn find_by_user_id(pool: &PgPool, user_id: i64) -> Result, SqlxError> { - let scores = sqlx::query_as!( - ScoreRow, - r#" - SELECT id, user_id, beatmap_id, score_metadata_id, replay_id, rate, - hwid, mods, hash, rank, status, created_at - FROM score - WHERE user_id = $1 - ORDER BY created_at DESC - "#, - user_id - ) - .fetch_all(pool) - .await?; - - Ok(scores) -} diff --git a/src/models/score/query/exists.rs b/src/models/score/query/exists.rs deleted file mode 100644 index 350bd08..0000000 --- a/src/models/score/query/exists.rs +++ /dev/null @@ -1,16 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn exists_by_hash(pool: &PgPool, hash: &str) -> Result { - let exists: Option = sqlx::query_scalar( - r#" - SELECT EXISTS( - SELECT 1 FROM score WHERE hash = $1 - ) - "#, - ) - .bind(hash) - .fetch_optional(pool) - .await?; - - Ok(exists.unwrap_or(false)) -} diff --git a/src/models/score/query/insert.rs b/src/models/score/query/insert.rs deleted file mode 100644 index 966c635..0000000 --- a/src/models/score/query/insert.rs +++ /dev/null @@ -1,6 +0,0 @@ -use super::super::types::ScoreRow; -use crate::define_insert_returning_row; - -define_insert_returning_row!(insert, "score", ScoreRow, - user_id, beatmap_id, score_metadata_id, replay_id, rate, hwid, mods, hash, rank, status; - "id, user_id, beatmap_id, score_metadata_id, replay_id, rate, hwid, mods, hash, rank, status, created_at"); diff --git a/src/models/score/query/mod.rs b/src/models/score/query/mod.rs deleted file mode 100644 index 048460f..0000000 --- a/src/models/score/query/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod by_beatmap_id; -pub mod by_id; -pub mod by_pending; -pub mod by_user_id; -pub mod exists; -pub mod insert; -pub mod update_status; diff --git a/src/models/score/query/update_status.rs b/src/models/score/query/update_status.rs deleted file mode 100644 index 5f183c2..0000000 --- a/src/models/score/query/update_status.rs +++ /dev/null @@ -1,37 +0,0 @@ -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn update_status(pool: &PgPool, id: i32, status: &str) -> Result { - let result = sqlx::query!( - r#" - UPDATE score - SET status = $1 - WHERE id = $2 - "#, - status, - id - ) - .execute(pool) - .await?; - - Ok(result.rows_affected()) -} - -pub async fn update_status_by_hash( - pool: &PgPool, - hash: &str, - status: &str, -) -> Result { - let result = sqlx::query!( - r#" - UPDATE score - SET status = $1 - WHERE hash = $2 - "#, - status, - hash - ) - .execute(pool) - .await?; - - Ok(result.rows_affected()) -} diff --git a/src/models/score/replay/impl.rs b/src/models/score/replay/impl.rs new file mode 100644 index 0000000..d31bc1f --- /dev/null +++ b/src/models/score/replay/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::ReplayRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl ReplayRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/score/replay/mod.rs b/src/models/score/replay/mod.rs new file mode 100644 index 0000000..9690eff --- /dev/null +++ b/src/models/score/replay/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::ReplayRow; diff --git a/src/models/replay/query/by_id.rs b/src/models/score/replay/query/by_id.rs similarity index 65% rename from src/models/replay/query/by_id.rs rename to src/models/score/replay/query/by_id.rs index fa1f5f7..e2be88f 100644 --- a/src/models/replay/query/by_id.rs +++ b/src/models/score/replay/query/by_id.rs @@ -1,12 +1,12 @@ -use super::super::types::ReplayRow; +use crate::models::score::replay::types::ReplayRow; use sqlx::{Error as SqlxError, PgPool}; pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { sqlx::query_as!( ReplayRow, r#" - SELECT id, hash, replay_available, replay_path, created_at - FROM replays + SELECT id, replay_hash, replay_available, replay_path, created_at + FROM replay WHERE id = $1 "#, id diff --git a/src/models/score/replay/query/insert.rs b/src/models/score/replay/query/insert.rs new file mode 100644 index 0000000..9655e2f --- /dev/null +++ b/src/models/score/replay/query/insert.rs @@ -0,0 +1,12 @@ +use crate::define_insert_returning_id; +use crate::models::score::replay::types::ReplayRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "replay", + ReplayRow, + replay_hash, + replay_available, + replay_path +); diff --git a/src/models/score/replay/query/mod.rs b/src/models/score/replay/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/score/replay/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/score/replay/tests/mod.rs b/src/models/score/replay/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/score/replay/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/score/replay/types.rs b/src/models/score/replay/types.rs new file mode 100644 index 0000000..e2cd26a --- /dev/null +++ b/src/models/score/replay/types.rs @@ -0,0 +1,24 @@ +use chrono::NaiveDateTime; +use validator::Validate; + +use crate::utils::HASH_REGEX; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct ReplayRow { + /// Unique identifier for the replay record. + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + #[validate(regex(path = *HASH_REGEX))] + pub replay_hash: String, + + /// Must be a boolean. + pub replay_available: bool, + + /// Must be a string. + #[validate(length(min = 1, message = "Replay path cannot be empty"))] + pub replay_path: String, + + /// Must be a timestamp. + pub created_at: Option, +} diff --git a/src/models/score/score/impl.rs b/src/models/score/score/impl.rs new file mode 100644 index 0000000..56ed460 --- /dev/null +++ b/src/models/score/score/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::ScoreRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl ScoreRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/score/score/mod.rs b/src/models/score/score/mod.rs new file mode 100644 index 0000000..cb2f9cc --- /dev/null +++ b/src/models/score/score/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::ScoreRow; diff --git a/src/models/score/score/query/by_id.rs b/src/models/score/score/query/by_id.rs new file mode 100644 index 0000000..e94775f --- /dev/null +++ b/src/models/score/score/query/by_id.rs @@ -0,0 +1,16 @@ +use crate::models::score::score::types::ScoreRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + ScoreRow, + r#" + SELECT id, user_id, rates_id, score_metadata_id, replay_id, hwid, mods, rank, status, created_at + FROM score + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/score/score/query/insert.rs b/src/models/score/score/query/insert.rs new file mode 100644 index 0000000..6f49bae --- /dev/null +++ b/src/models/score/score/query/insert.rs @@ -0,0 +1,17 @@ +use crate::define_insert_returning_id; +use crate::models::score::score::types::ScoreRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "score", + ScoreRow, + user_id, + rates_id, + score_metadata_id, + replay_id, + hwid, + mods, + rank, + status +); diff --git a/src/models/score/score/query/mod.rs b/src/models/score/score/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/score/score/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/score/score/tests/mod.rs b/src/models/score/score/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/score/score/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/score/score/types.rs b/src/models/score/score/types.rs new file mode 100644 index 0000000..28adb36 --- /dev/null +++ b/src/models/score/score/types.rs @@ -0,0 +1,65 @@ +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct ScoreRow { + /// Unique identifier for the score record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Discord ID of the user who achieved this score. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "User ID must be positive"))] + pub user_id: i64, + + /// Reference to the rates record this score applies to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Rates ID must be positive"))] + pub rates_id: i32, + + /// Reference to the score metadata record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Score metadata ID must be positive"))] + pub score_metadata_id: i32, + + /// Reference to the replay record. + /// Optional field, can be None. + pub replay_id: Option, + + /// Hardware ID of the computer used to play the score. + /// Optional field, can be None. + pub hwid: Option, + + /// Mods used in the play. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Mods must be non-negative"))] + pub mods: i64, + + /// Rank achieved in the play. + /// Must be one of: 'XH', 'X', 'SH', 'SS', 'S', 'A', 'B', 'C', 'D', 'E', 'F', 'G'. + #[validate(custom(function = "validate_rank"))] + pub rank: String, + + /// Status of the score. + /// Must be one of: 'pending', 'processing', 'validated', 'cheated', 'unsubmitted'. + #[validate(custom(function = "validate_status"))] + pub status: String, + + /// Timestamp when the score was created. + pub created_at: Option, +} + +fn validate_rank(rank: &str) -> Result<(), validator::ValidationError> { + match rank { + "XH" | "X" | "SH" | "SS" | "S" | "A" | "B" | "C" | "D" | "E" | "F" | "G" => Ok(()), + _ => Err(validator::ValidationError::new("invalid_rank")), + } +} + +fn validate_status(status: &str) -> Result<(), validator::ValidationError> { + match status { + "pending" | "processing" | "validated" | "cheated" | "unsubmitted" => Ok(()), + _ => Err(validator::ValidationError::new("invalid_status")), + } +} diff --git a/src/models/score/score_metadata/impl.rs b/src/models/score/score_metadata/impl.rs new file mode 100644 index 0000000..604b198 --- /dev/null +++ b/src/models/score/score_metadata/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::ScoreMetadataRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl ScoreMetadataRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/score/score_metadata/mod.rs b/src/models/score/score_metadata/mod.rs new file mode 100644 index 0000000..39a4608 --- /dev/null +++ b/src/models/score/score_metadata/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::ScoreMetadataRow; diff --git a/src/models/score/score_metadata/query/by_id.rs b/src/models/score/score_metadata/query/by_id.rs new file mode 100644 index 0000000..0d8d4f2 --- /dev/null +++ b/src/models/score/score_metadata/query/by_id.rs @@ -0,0 +1,16 @@ +use crate::models::score::score_metadata::types::ScoreMetadataRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + ScoreMetadataRow, + r#" + SELECT id, skin, pause_count, started_at, ended_at, time_paused, score, accuracy, max_combo, perfect, count_300, count_100, count_50, count_miss, count_geki, count_katu, created_at + FROM score_metadata + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/score_metadata/query/insert.rs b/src/models/score/score_metadata/query/insert.rs similarity index 71% rename from src/models/score_metadata/query/insert.rs rename to src/models/score/score_metadata/query/insert.rs index 860ce6b..ce06720 100644 --- a/src/models/score_metadata/query/insert.rs +++ b/src/models/score/score_metadata/query/insert.rs @@ -1,5 +1,6 @@ use crate::define_insert_returning_id; -use crate::models::score_metadata::types::ScoreMetadataRow; +use crate::models::score::score_metadata::types::ScoreMetadataRow; +// no extra imports needed define_insert_returning_id!( insert, @@ -18,6 +19,6 @@ define_insert_returning_id!( count_100, count_50, count_miss, - count_katu, - count_geki + count_geki, + count_katu ); diff --git a/src/models/score/score_metadata/query/mod.rs b/src/models/score/score_metadata/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/score/score_metadata/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/score/score_metadata/tests/mod.rs b/src/models/score/score_metadata/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/score/score_metadata/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/score/score_metadata/types.rs b/src/models/score/score_metadata/types.rs new file mode 100644 index 0000000..3083581 --- /dev/null +++ b/src/models/score/score_metadata/types.rs @@ -0,0 +1,83 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use validator::Validate; +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct ScoreMetadataRow { + /// Unique identifier for the score metadata record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Skin used for the play. + /// Maximum 100 characters. + #[validate(length(max = 100, message = "Skin name must be 100 characters or less"))] + pub skin: Option, + + /// Number of times the game was paused. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Pause count must be non-negative"))] + pub pause_count: i32, + + /// Timestamp when the play started. + /// Used for anti-cheat verification. + pub started_at: NaiveDateTime, + + /// Timestamp when the play ended. + /// Used for anti-cheat verification. + pub ended_at: NaiveDateTime, + + /// Total time paused in seconds. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Time paused must be non-negative"))] + pub time_paused: i32, + + /// Score achieved. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Score must be non-negative"))] + pub score: i32, + + /// Accuracy achieved (as a percentage). + /// Must be between 0.0 and 100.0. + pub accuracy: BigDecimal, + + /// Maximum combo achieved. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Max combo must be non-negative"))] + pub max_combo: i32, + + /// Whether the play was perfect (no misses). + pub perfect: bool, + + /// Number of 300 hits. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Count 300 must be non-negative"))] + pub count_300: i32, + + /// Number of 100 hits. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Count 100 must be non-negative"))] + pub count_100: i32, + + /// Number of 50 hits. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Count 50 must be non-negative"))] + pub count_50: i32, + + /// Number of misses. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Count miss must be non-negative"))] + pub count_miss: i32, + + /// Number of katu hits (perfect 100s). + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Count katu must be non-negative"))] + pub count_katu: i32, + + /// Number of geki hits (perfect 300s). + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Count geki must be non-negative"))] + pub count_geki: i32, + + /// Timestamp when the score metadata was created. + pub created_at: Option, +} diff --git a/src/models/score/tests/mod.rs b/src/models/score/tests/mod.rs deleted file mode 100644 index 8695201..0000000 --- a/src/models/score/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod validation; diff --git a/src/models/score/tests/validation/hash_tests.rs b/src/models/score/tests/validation/hash_tests.rs deleted file mode 100644 index f01eace..0000000 --- a/src/models/score/tests/validation/hash_tests.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::models::score::ScoreRow; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - use bigdecimal::{BigDecimal, FromPrimitive}; - - #[test] - fn test_hash_validation_valid_alphanumeric() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_hash_validation_valid_single_character() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "a".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_hash_validation_invalid_empty() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } - - #[test] - fn test_hash_validation_invalid_too_long() { - let long_hash = "a".repeat(256); - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: long_hash, - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } - - #[test] - fn test_hash_validation_invalid_with_special_characters() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def-456".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } -} diff --git a/src/models/score/tests/validation/hwid_tests.rs b/src/models/score/tests/validation/hwid_tests.rs deleted file mode 100644 index 271fcf4..0000000 --- a/src/models/score/tests/validation/hwid_tests.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::models::score::ScoreRow; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - use bigdecimal::{BigDecimal, FromPrimitive}; - - #[test] - fn test_hwid_validation_valid_alphanumeric() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_hwid_validation_valid_none() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: None, - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_hwid_validation_invalid_too_long() { - let long_hwid = "a".repeat(256); - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some(long_hwid), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } - - #[test] - fn test_hwid_validation_invalid_with_special_characters() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc-123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } -} diff --git a/src/models/score/tests/validation/mod.rs b/src/models/score/tests/validation/mod.rs deleted file mode 100644 index 03c0fb3..0000000 --- a/src/models/score/tests/validation/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod hash_tests; -pub mod hwid_tests; -pub mod rank_tests; -pub mod rate_tests; -pub mod status_tests; diff --git a/src/models/score/tests/validation/rank_tests.rs b/src/models/score/tests/validation/rank_tests.rs deleted file mode 100644 index a0d4cc9..0000000 --- a/src/models/score/tests/validation/rank_tests.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::models::score::ScoreRow; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - use bigdecimal::{BigDecimal, FromPrimitive}; - - #[test] - fn test_rank_validation_valid_ss() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "SS".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_rank_validation_valid_s() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "S".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_rank_validation_valid_a() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_rank_validation_invalid_empty() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } - - #[test] - fn test_rank_validation_invalid_too_long() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "ABC".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } - - #[test] - fn test_rank_validation_invalid_unknown_rank() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "X".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } -} diff --git a/src/models/score/tests/validation/rate_tests.rs b/src/models/score/tests/validation/rate_tests.rs deleted file mode 100644 index dac5ce5..0000000 --- a/src/models/score/tests/validation/rate_tests.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::models::score::validators::validate_rate; - -#[cfg(test)] -mod tests { - use super::*; - use bigdecimal::{BigDecimal, FromPrimitive}; - #[test] - fn test_validate_rate_valid_zero() { - let rate = BigDecimal::from_f64(0.0).unwrap(); - assert!(validate_rate(&rate).is_ok()); - } - - #[test] - fn test_validate_rate_valid_normal() { - let rate = BigDecimal::from_f64(1.5).unwrap(); - assert!(validate_rate(&rate).is_ok()); - } - - #[test] - fn test_validate_rate_valid_max() { - let rate = BigDecimal::from_f64(10.0).unwrap(); - assert!(validate_rate(&rate).is_ok()); - } - - #[test] - fn test_validate_rate_invalid_negative() { - let rate = BigDecimal::from_f64(-1.0).unwrap(); - let result = validate_rate(&rate); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "rate_out_of_range"); - } - - #[test] - fn test_validate_rate_invalid_too_high() { - let rate = BigDecimal::from_f64(11.0).unwrap(); - let result = validate_rate(&rate); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "rate_out_of_range"); - } - - #[test] - fn test_validate_rate_invalid_very_high() { - let rate = BigDecimal::from_f64(100.0).unwrap(); - let result = validate_rate(&rate); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "rate_out_of_range"); - } -} diff --git a/src/models/score/tests/validation/status_tests.rs b/src/models/score/tests/validation/status_tests.rs deleted file mode 100644 index aeb6895..0000000 --- a/src/models/score/tests/validation/status_tests.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::models::score::ScoreRow; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - use bigdecimal::{BigDecimal, FromPrimitive}; - - #[test] - fn test_status_validation_valid_pending() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "pending".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_status_validation_valid_processing() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "processing".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_status_validation_valid_validated() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "validated".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_status_validation_valid_cheated() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "cheated".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_status_validation_valid_unsubmitted() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "unsubmitted".to_string(), - created_at: None, - }; - assert!(score.validate().is_ok()); - } - - #[test] - fn test_status_validation_invalid_empty() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } - - #[test] - fn test_status_validation_invalid_too_long() { - let long_status = "a".repeat(21); - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: long_status, - created_at: None, - }; - assert!(score.validate().is_err()); - } - - #[test] - fn test_status_validation_invalid_unknown_status() { - let score = ScoreRow { - id: 1, - user_id: 12345, - beatmap_id: 67890, - score_metadata_id: 11111, - replay_id: Some(22222), - rate: BigDecimal::from_f64(1.0).unwrap(), - hwid: Some("abc123".to_string()), - mods: 0, - hash: "def456".to_string(), - rank: "A".to_string(), - status: "unknown".to_string(), - created_at: None, - }; - assert!(score.validate().is_err()); - } -} diff --git a/src/models/score/types.rs b/src/models/score/types.rs deleted file mode 100644 index e3e9681..0000000 --- a/src/models/score/types.rs +++ /dev/null @@ -1,68 +0,0 @@ -use bigdecimal::BigDecimal; -use chrono::NaiveDateTime; -use sqlx::FromRow; -use validator::Validate; - -use super::validators::*; -use crate::utils::{HASH_REGEX, RANK_REGEX, SCORE_STATUS_REGEX}; - -#[derive(Debug, Clone, FromRow, Validate)] -pub struct ScoreRow { - #[validate(range(min = 1, message = "ID must be positive"))] - pub id: i32, - - #[validate(range(min = 1, message = "User ID must be positive"))] - pub user_id: i64, - - #[validate(range(min = 1, message = "Beatmap ID must be positive"))] - pub beatmap_id: i32, - - #[validate(range(min = 1, message = "Score metadata ID must be positive"))] - pub score_metadata_id: i32, - - #[validate(range(min = 1, message = "Replay ID must be positive"))] - pub replay_id: Option, - - #[validate(custom(function = "validate_rate"))] - pub rate: BigDecimal, - - #[validate(length(max = 255, message = "HWID must be at most 255 characters"))] - #[validate(regex( - path = "*HASH_REGEX", - message = "HWID must contain only alphanumeric characters" - ))] - pub hwid: Option, - - #[validate(range(min = 0, message = "Mods must be positive"))] - pub mods: i64, - - #[validate(length( - min = 1, - max = 255, - message = "Hash must be between 1 and 255 characters" - ))] - #[validate(regex( - path = "*HASH_REGEX", - message = "Hash must contain only alphanumeric characters" - ))] - pub hash: String, - - #[validate(length(min = 1, max = 2, message = "Rank must be between 1 and 2 characters"))] - #[validate(regex( - path = "*RANK_REGEX", - message = "Rank must contain only alphanumeric characters" - ))] - pub rank: String, - - #[validate(length( - min = 1, - max = 20, - message = "Status must be between 1 and 20 characters" - ))] - #[validate(regex( - path = "*SCORE_STATUS_REGEX", - message = "Status must contain only alphanumeric characters" - ))] - pub status: String, - pub created_at: Option, -} diff --git a/src/models/score/validators.rs b/src/models/score/validators.rs deleted file mode 100644 index 6a4fc98..0000000 --- a/src/models/score/validators.rs +++ /dev/null @@ -1,9 +0,0 @@ -use bigdecimal::BigDecimal; -use validator::ValidationError; - -pub fn validate_rate(rate: &BigDecimal) -> Result<(), ValidationError> { - if *rate < BigDecimal::from(0) || *rate > BigDecimal::from(10) { - return Err(ValidationError::new("rate_out_of_range")); - } - Ok(()) -} diff --git a/src/models/score_metadata/impl.rs b/src/models/score_metadata/impl.rs deleted file mode 100644 index 065ef70..0000000 --- a/src/models/score_metadata/impl.rs +++ /dev/null @@ -1,13 +0,0 @@ -use super::query::{find_by_id, insert}; -use super::types::ScoreMetadataRow; -use sqlx::PgPool; - -impl ScoreMetadataRow { - pub async fn insert(self, pool: &PgPool) -> Result { - insert(pool, self).await - } - - pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, sqlx::Error> { - find_by_id(pool, id).await - } -} diff --git a/src/models/score_metadata/query/by_id.rs b/src/models/score_metadata/query/by_id.rs deleted file mode 100644 index fa8eac5..0000000 --- a/src/models/score_metadata/query/by_id.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::define_by_id; -use crate::models::score_metadata::types::ScoreMetadataRow; - -define_by_id!(find_by_id, "score_metadata", ScoreMetadataRow, - "id, skin, pause_count, started_at, ended_at, time_paused, score, accuracy, max_combo, perfect, count_300, count_100, count_50, count_miss, count_katu, count_geki, created_at"); diff --git a/src/models/score_metadata/query/mod.rs b/src/models/score_metadata/query/mod.rs deleted file mode 100644 index 885e55f..0000000 --- a/src/models/score_metadata/query/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod by_id; -pub mod insert; - -pub use by_id::find_by_id; -pub use insert::insert; diff --git a/src/models/score_metadata/tests/mod.rs b/src/models/score_metadata/tests/mod.rs deleted file mode 100644 index 8695201..0000000 --- a/src/models/score_metadata/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod validation; diff --git a/src/models/score_metadata/tests/validation/accuracy_tests.rs b/src/models/score_metadata/tests/validation/accuracy_tests.rs deleted file mode 100644 index 444f93b..0000000 --- a/src/models/score_metadata/tests/validation/accuracy_tests.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::models::score_metadata::validators::validate_accuracy; -use bigdecimal::BigDecimal; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_accuracy_valid_zero() { - let accuracy = BigDecimal::from(0); - assert!(validate_accuracy(&accuracy).is_ok()); - } - - #[test] - fn test_validate_accuracy_valid_normal() { - let accuracy = BigDecimal::from(85); - assert!(validate_accuracy(&accuracy).is_ok()); - } - - #[test] - fn test_validate_accuracy_valid_max() { - let accuracy = BigDecimal::from(100); - assert!(validate_accuracy(&accuracy).is_ok()); - } - - #[test] - fn test_validate_accuracy_invalid_negative() { - let accuracy = BigDecimal::from(-1); - let result = validate_accuracy(&accuracy); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "accuracy_out_of_range"); - } - - #[test] - fn test_validate_accuracy_invalid_too_high() { - let accuracy = BigDecimal::from(101); - let result = validate_accuracy(&accuracy); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "accuracy_out_of_range"); - } - - #[test] - fn test_validate_accuracy_invalid_very_high() { - let accuracy = BigDecimal::from(150); - let result = validate_accuracy(&accuracy); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "accuracy_out_of_range"); - } -} diff --git a/src/models/score_metadata/tests/validation/mod.rs b/src/models/score_metadata/tests/validation/mod.rs deleted file mode 100644 index a9bd5da..0000000 --- a/src/models/score_metadata/tests/validation/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod accuracy_tests; diff --git a/src/models/score_metadata/types.rs b/src/models/score_metadata/types.rs deleted file mode 100644 index 5a667e0..0000000 --- a/src/models/score_metadata/types.rs +++ /dev/null @@ -1,56 +0,0 @@ -use bigdecimal::BigDecimal; -use chrono::NaiveDateTime; -use sqlx::FromRow; -use validator::Validate; - -use crate::models::score_metadata::validators::*; - -#[derive(Debug, Clone, FromRow, Validate)] -pub struct ScoreMetadataRow { - #[validate(range(min = 1, message = "ID must be positive"))] - pub id: i32, - - #[validate(length(max = 255, message = "Skin name must be at most 255 characters"))] - pub skin: Option, - - #[validate(range(min = 0, message = "Pause count cannot be negative"))] - pub pause_count: i32, - - pub started_at: NaiveDateTime, - - pub ended_at: NaiveDateTime, - - #[validate(range(min = 0, message = "Time paused cannot be negative"))] - pub time_paused: i32, - - #[validate(range(min = 0, message = "Score cannot be negative"))] - pub score: i32, - - #[validate(custom(function = "validate_accuracy"))] - pub accuracy: BigDecimal, - - #[validate(range(min = 0, message = "Max combo cannot be negative"))] - pub max_combo: i32, - - pub perfect: bool, - - #[validate(range(min = 0, message = "Count 300 cannot be negative"))] - pub count_300: i32, - - #[validate(range(min = 0, message = "Count 100 cannot be negative"))] - pub count_100: i32, - - #[validate(range(min = 0, message = "Count 50 cannot be negative"))] - pub count_50: i32, - - #[validate(range(min = 0, message = "Count miss cannot be negative"))] - pub count_miss: i32, - - #[validate(range(min = 0, message = "Count katu cannot be negative"))] - pub count_katu: i32, - - #[validate(range(min = 0, message = "Count geki cannot be negative"))] - pub count_geki: i32, - - pub created_at: Option, -} diff --git a/src/models/score_metadata/validators.rs b/src/models/score_metadata/validators.rs deleted file mode 100644 index d71ef1b..0000000 --- a/src/models/score_metadata/validators.rs +++ /dev/null @@ -1,10 +0,0 @@ -use bigdecimal::BigDecimal; -use validator::ValidationError; - -/// Validates that accuracy is within valid range (0-100). -pub fn validate_accuracy(accuracy: &BigDecimal) -> Result<(), ValidationError> { - if *accuracy < BigDecimal::from(0) || *accuracy > BigDecimal::from(100) { - return Err(ValidationError::new("accuracy_out_of_range")); - } - Ok(()) -} diff --git a/src/models/score_rating/impl.rs b/src/models/score_rating/impl.rs deleted file mode 100644 index 0f2d500..0000000 --- a/src/models/score_rating/impl.rs +++ /dev/null @@ -1,23 +0,0 @@ -use super::query::{by_id::find_by_id, by_score_id::find_by_score_id, insert::insert}; -use super::types::ScoreRatingRow; -use bigdecimal::BigDecimal; -use sqlx::PgPool; - -impl ScoreRatingRow { - pub async fn insert( - pool: &PgPool, - score_id: i32, - rating: BigDecimal, - rating_type: &str, - ) -> Result { - insert(pool, score_id, rating, rating_type).await - } - - pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, sqlx::Error> { - find_by_id(pool, id).await - } - - pub async fn find_by_score_id(pool: &PgPool, score_id: i32) -> Result, sqlx::Error> { - find_by_score_id(pool, score_id).await - } -} diff --git a/src/models/score_rating/query/by_score_id.rs b/src/models/score_rating/query/by_score_id.rs deleted file mode 100644 index df66a0c..0000000 --- a/src/models/score_rating/query/by_score_id.rs +++ /dev/null @@ -1,22 +0,0 @@ -use super::super::types::ScoreRatingRow; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn find_by_score_id( - pool: &PgPool, - score_id: i32, -) -> Result, SqlxError> { - let score_ratings = sqlx::query_as!( - ScoreRatingRow, - r#" - SELECT id, score_id, rating, rating_type, created_at - FROM score_rating - WHERE score_id = $1 - ORDER BY created_at DESC - "#, - score_id - ) - .fetch_all(pool) - .await?; - - Ok(score_ratings) -} diff --git a/src/models/score_rating/query/insert.rs b/src/models/score_rating/query/insert.rs deleted file mode 100644 index b64f716..0000000 --- a/src/models/score_rating/query/insert.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::super::types::ScoreRatingRow; -use bigdecimal::BigDecimal; -use sqlx::{Error as SqlxError, PgPool}; - -pub async fn insert( - pool: &PgPool, - score_id: i32, - rating: BigDecimal, - rating_type: &str, -) -> Result { - let mut builder = sqlx::QueryBuilder::::new( - "INSERT INTO score_rating (score_id, rating, rating_type, created_at) VALUES (", - ); - let mut sep = builder.separated(", "); - sep.push_bind(score_id); - sep.push_bind(rating); - sep.push_bind(rating_type); - sep.push("NOW()"); - // `sep` drops here naturally; no need to call drop explicitly - builder.push(") RETURNING id, score_id, rating, rating_type, created_at"); - - builder - .build_query_as::() - .fetch_one(pool) - .await -} diff --git a/src/models/score_rating/tests/mod.rs b/src/models/score_rating/tests/mod.rs deleted file mode 100644 index 8695201..0000000 --- a/src/models/score_rating/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod validation; diff --git a/src/models/score_rating/tests/validation/mod.rs b/src/models/score_rating/tests/validation/mod.rs deleted file mode 100644 index b41629c..0000000 --- a/src/models/score_rating/tests/validation/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod rating_tests; -pub mod rating_type_tests; diff --git a/src/models/score_rating/tests/validation/rating_tests.rs b/src/models/score_rating/tests/validation/rating_tests.rs deleted file mode 100644 index 400154c..0000000 --- a/src/models/score_rating/tests/validation/rating_tests.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::models::score_rating::validators::validate_rating; -use bigdecimal::BigDecimal; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_rating_valid_zero() { - let rating = BigDecimal::from(0); - assert!(validate_rating(&rating).is_ok()); - } - - #[test] - fn test_validate_rating_valid_normal() { - let rating = BigDecimal::from(75); - assert!(validate_rating(&rating).is_ok()); - } - - #[test] - fn test_validate_rating_valid_max() { - let rating = BigDecimal::from(100); - assert!(validate_rating(&rating).is_ok()); - } - - #[test] - fn test_validate_rating_invalid_negative() { - let rating = BigDecimal::from(-1); - let result = validate_rating(&rating); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "rating_out_of_range"); - } - - #[test] - fn test_validate_rating_invalid_too_high() { - let rating = BigDecimal::from(101); - let result = validate_rating(&rating); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "rating_out_of_range"); - } - - #[test] - fn test_validate_rating_invalid_very_high() { - let rating = BigDecimal::from(150); - let result = validate_rating(&rating); - assert!(result.is_err()); - - let error = result.unwrap_err(); - assert_eq!(error.code, "rating_out_of_range"); - } -} diff --git a/src/models/score_rating/tests/validation/rating_type_tests.rs b/src/models/score_rating/tests/validation/rating_type_tests.rs deleted file mode 100644 index b3b1da2..0000000 --- a/src/models/score_rating/tests/validation/rating_type_tests.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate::models::score_rating::ScoreRatingRow; -use validator::Validate; - -#[cfg(test)] -mod tests { - use super::*; - use bigdecimal::BigDecimal; - - #[test] - fn test_rating_type_validation_valid_etterna() { - let score_rating = ScoreRatingRow { - id: 1, - score_id: 12345, - rating: BigDecimal::from(85), - rating_type: "etterna".to_string(), - created_at: None, - }; - assert!(score_rating.validate().is_ok()); - } - - #[test] - fn test_rating_type_validation_valid_osu() { - let score_rating = ScoreRatingRow { - id: 1, - score_id: 12345, - rating: BigDecimal::from(85), - rating_type: "osu".to_string(), - created_at: None, - }; - assert!(score_rating.validate().is_ok()); - } - - #[test] - fn test_rating_type_validation_valid_quaver() { - let score_rating = ScoreRatingRow { - id: 1, - score_id: 12345, - rating: BigDecimal::from(85), - rating_type: "quaver".to_string(), - created_at: None, - }; - assert!(score_rating.validate().is_ok()); - } - - #[test] - fn test_rating_type_validation_valid_malody() { - let score_rating = ScoreRatingRow { - id: 1, - score_id: 12345, - rating: BigDecimal::from(85), - rating_type: "malody".to_string(), - created_at: None, - }; - assert!(score_rating.validate().is_ok()); - } - - #[test] - fn test_rating_type_validation_invalid_empty() { - let score_rating = ScoreRatingRow { - id: 1, - score_id: 12345, - rating: BigDecimal::from(85), - rating_type: "".to_string(), - created_at: None, - }; - assert!(score_rating.validate().is_err()); - } - - #[test] - fn test_rating_type_validation_invalid_too_long() { - let long_type = "a".repeat(31); - let score_rating = ScoreRatingRow { - id: 1, - score_id: 12345, - rating: BigDecimal::from(85), - rating_type: long_type, - created_at: None, - }; - assert!(score_rating.validate().is_err()); - } - - #[test] - fn test_rating_type_validation_invalid_unknown_type() { - let score_rating = ScoreRatingRow { - id: 1, - score_id: 12345, - rating: BigDecimal::from(85), - rating_type: "unknown".to_string(), - created_at: None, - }; - assert!(score_rating.validate().is_err()); - } - - #[test] - fn test_rating_type_validation_invalid_with_numbers() { - let score_rating = ScoreRatingRow { - id: 1, - score_id: 12345, - rating: BigDecimal::from(85), - rating_type: "etterna2".to_string(), - created_at: None, - }; - assert!(score_rating.validate().is_err()); - } - - #[test] - fn test_score_rating_id_validation_invalid_negative() { - let score_rating = ScoreRatingRow { - id: -1, - score_id: 12345, - rating: BigDecimal::from(85), - rating_type: "etterna".to_string(), - created_at: None, - }; - assert!(score_rating.validate().is_err()); - } - - #[test] - fn test_score_rating_score_id_validation_invalid_negative() { - let score_rating = ScoreRatingRow { - id: 1, - score_id: -1, - rating: BigDecimal::from(85), - rating_type: "etterna".to_string(), - created_at: None, - }; - assert!(score_rating.validate().is_err()); - } -} diff --git a/src/models/score_rating/types.rs b/src/models/score_rating/types.rs deleted file mode 100644 index 4409f39..0000000 --- a/src/models/score_rating/types.rs +++ /dev/null @@ -1,28 +0,0 @@ -use bigdecimal::BigDecimal; -use chrono::NaiveDateTime; -use sqlx::FromRow; -use validator::Validate; - -use crate::models::score_rating::validators::*; -use crate::utils::RATING_TYPE_REGEX; - -#[derive(Debug, Clone, FromRow, Validate)] -pub struct ScoreRatingRow { - #[validate(range(min = 1, message = "ID must be positive"))] - pub id: i32, - - #[validate(range(min = 1, message = "Score ID must be positive"))] - pub score_id: i32, - - #[validate(custom(function = "validate_rating"))] - pub rating: BigDecimal, - - #[validate(length( - min = 1, - max = 30, - message = "Rating type must be between 1 and 30 characters" - ))] - #[validate(regex(path = "*RATING_TYPE_REGEX", message = "Invalid rating type"))] - pub rating_type: String, - pub created_at: Option, -} diff --git a/src/models/score_rating/validators.rs b/src/models/score_rating/validators.rs deleted file mode 100644 index fc02f28..0000000 --- a/src/models/score_rating/validators.rs +++ /dev/null @@ -1,9 +0,0 @@ -use bigdecimal::BigDecimal; -use validator::ValidationError; - -pub fn validate_rating(rating: &BigDecimal) -> Result<(), ValidationError> { - if *rating < BigDecimal::from(0) || *rating > BigDecimal::from(100) { - return Err(ValidationError::new("rating_out_of_range")); - } - Ok(()) -} diff --git a/src/models/users/bans/impl.rs b/src/models/users/bans/impl.rs new file mode 100644 index 0000000..4a6eadb --- /dev/null +++ b/src/models/users/bans/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::BansRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl BansRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/replay/mod.rs b/src/models/users/bans/mod.rs similarity index 75% rename from src/models/replay/mod.rs rename to src/models/users/bans/mod.rs index 6fca91b..0eafee9 100644 --- a/src/models/replay/mod.rs +++ b/src/models/users/bans/mod.rs @@ -5,4 +5,4 @@ pub mod types; #[cfg(test)] mod tests; -pub use types::*; +pub use types::BansRow; diff --git a/src/models/msd/query/by_id.rs b/src/models/users/bans/query/by_id.rs similarity index 53% rename from src/models/msd/query/by_id.rs rename to src/models/users/bans/query/by_id.rs index 6dbaaaa..63343a8 100644 --- a/src/models/msd/query/by_id.rs +++ b/src/models/users/bans/query/by_id.rs @@ -1,11 +1,13 @@ -use crate::models::msd::types::MSDRow; +use crate::models::users::bans::types::BansRow; use sqlx::{Error as SqlxError, PgPool}; -pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { sqlx::query_as!( - MSDRow, + BansRow, r#" - SELECT * FROM msd WHERE id = $1 + SELECT id, discord_id, reason, banned_at + FROM bans + WHERE id = $1 "#, id ) diff --git a/src/models/users/bans/query/insert.rs b/src/models/users/bans/query/insert.rs new file mode 100644 index 0000000..5d5641b --- /dev/null +++ b/src/models/users/bans/query/insert.rs @@ -0,0 +1,5 @@ +use crate::define_insert_returning_id; +use crate::models::users::bans::types::BansRow; +// no extra imports needed + +define_insert_returning_id!(insert, "bans", BansRow, discord_id, reason, banned_at); diff --git a/src/models/users/bans/query/mod.rs b/src/models/users/bans/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/users/bans/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/users/bans/tests/mod.rs b/src/models/users/bans/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/users/bans/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/users/bans/types.rs b/src/models/users/bans/types.rs new file mode 100644 index 0000000..9f573bc --- /dev/null +++ b/src/models/users/bans/types.rs @@ -0,0 +1,21 @@ +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct BansRow { + /// Unique identifier for the ban record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Discord ID of the banned user. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Discord ID must be positive"))] + pub discord_id: Option, + + /// Optional reason for the ban. + pub reason: Option, + + /// Timestamp when the user was banned. + pub banned_at: Option, +} diff --git a/src/models/users/device_tokens/impl.rs b/src/models/users/device_tokens/impl.rs new file mode 100644 index 0000000..b35e7b7 --- /dev/null +++ b/src/models/users/device_tokens/impl.rs @@ -0,0 +1,14 @@ +use super::query::{find_by_token, insert}; +use super::DeviceTokensRow; +use sqlx::{Error as SqlxError, PgPool}; +use uuid::Uuid; + +impl DeviceTokensRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_token(pool: &PgPool, token: Uuid) -> Result, SqlxError> { + find_by_token(pool, token).await + } +} diff --git a/src/models/users/device_tokens/mod.rs b/src/models/users/device_tokens/mod.rs new file mode 100644 index 0000000..d61e15b --- /dev/null +++ b/src/models/users/device_tokens/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::DeviceTokensRow; diff --git a/src/models/users/device_tokens/query/by_id.rs b/src/models/users/device_tokens/query/by_id.rs new file mode 100644 index 0000000..e5a9a87 --- /dev/null +++ b/src/models/users/device_tokens/query/by_id.rs @@ -0,0 +1,20 @@ +use crate::models::users::device_tokens::types::DeviceTokensRow; +use sqlx::{Error as SqlxError, PgPool}; +use uuid::Uuid; + +pub async fn find_by_token( + pool: &PgPool, + token: Uuid, +) -> Result, SqlxError> { + sqlx::query_as!( + DeviceTokensRow, + r#" + SELECT token, discord_id, device_name, hwid, created_at + FROM device_tokens + WHERE token = $1 + "#, + token + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/users/device_tokens/query/insert.rs b/src/models/users/device_tokens/query/insert.rs new file mode 100644 index 0000000..9941217 --- /dev/null +++ b/src/models/users/device_tokens/query/insert.rs @@ -0,0 +1,13 @@ +use crate::define_insert_returning_id; +use crate::models::users::device_tokens::types::DeviceTokensRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "device_tokens", + DeviceTokensRow, + token, + discord_id, + device_name, + hwid +); diff --git a/src/models/users/device_tokens/query/mod.rs b/src/models/users/device_tokens/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/users/device_tokens/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/users/device_tokens/tests/mod.rs b/src/models/users/device_tokens/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/users/device_tokens/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/users/device_tokens/types.rs b/src/models/users/device_tokens/types.rs new file mode 100644 index 0000000..de7ce91 --- /dev/null +++ b/src/models/users/device_tokens/types.rs @@ -0,0 +1,23 @@ +use chrono::NaiveDateTime; +use uuid::Uuid; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct DeviceTokensRow { + /// Unique token identifier (UUID). + pub token: Uuid, + + /// Discord ID of the user who owns this device token. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Discord ID must be positive"))] + pub discord_id: Option, + + /// Optional device name identifier. + pub device_name: Option, + + /// Optional hardware identifier. + pub hwid: Option, + + /// Timestamp when the device token was created. + pub created_at: Option, +} diff --git a/src/models/users/mod.rs b/src/models/users/mod.rs new file mode 100644 index 0000000..c041ce4 --- /dev/null +++ b/src/models/users/mod.rs @@ -0,0 +1,4 @@ +pub mod bans; +pub mod device_tokens; +pub mod new_users; +pub mod users; diff --git a/src/models/users/new_users/impl.rs b/src/models/users/new_users/impl.rs new file mode 100644 index 0000000..1528913 --- /dev/null +++ b/src/models/users/new_users/impl.rs @@ -0,0 +1,21 @@ +use super::query::{find_by_discord_id, find_by_token, insert}; +use super::NewUsersRow; +use sqlx::{Error as SqlxError, PgPool}; +use uuid::Uuid; + +impl NewUsersRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_discord_id( + pool: &PgPool, + discord_id: i64, + ) -> Result, SqlxError> { + find_by_discord_id(pool, discord_id).await + } + + pub async fn find_by_token(pool: &PgPool, token: Uuid) -> Result, SqlxError> { + find_by_token(pool, token).await + } +} diff --git a/src/models/users/new_users/mod.rs b/src/models/users/new_users/mod.rs new file mode 100644 index 0000000..9d34d6c --- /dev/null +++ b/src/models/users/new_users/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::NewUsersRow; diff --git a/src/models/users/new_users/query/by_id.rs b/src/models/users/new_users/query/by_id.rs new file mode 100644 index 0000000..6ebbf25 --- /dev/null +++ b/src/models/users/new_users/query/by_id.rs @@ -0,0 +1,34 @@ +use crate::models::users::new_users::types::NewUsersRow; +use sqlx::{Error as SqlxError, PgPool}; +use uuid::Uuid; + +pub async fn find_by_discord_id( + pool: &PgPool, + discord_id: i64, +) -> Result, SqlxError> { + sqlx::query_as!( + NewUsersRow, + r#" + SELECT discord_id, username, token, created_at + FROM new_users + WHERE discord_id = $1 + "#, + discord_id + ) + .fetch_optional(pool) + .await +} + +pub async fn find_by_token(pool: &PgPool, token: Uuid) -> Result, SqlxError> { + sqlx::query_as!( + NewUsersRow, + r#" + SELECT discord_id, username, token, created_at + FROM new_users + WHERE token = $1 + "#, + token + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/users/new_users/query/insert.rs b/src/models/users/new_users/query/insert.rs new file mode 100644 index 0000000..b79669a --- /dev/null +++ b/src/models/users/new_users/query/insert.rs @@ -0,0 +1,12 @@ +use crate::define_insert_returning_id; +use crate::models::users::new_users::types::NewUsersRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "new_users", + NewUsersRow, + discord_id, + username, + token +); diff --git a/src/models/users/new_users/query/mod.rs b/src/models/users/new_users/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/users/new_users/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/users/new_users/tests/mod.rs b/src/models/users/new_users/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/users/new_users/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/users/new_users/types.rs b/src/models/users/new_users/types.rs new file mode 100644 index 0000000..ebe8229 --- /dev/null +++ b/src/models/users/new_users/types.rs @@ -0,0 +1,20 @@ +use chrono::NaiveDateTime; +use uuid::Uuid; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct NewUsersRow { + /// Discord ID of the new user (primary key). + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Discord ID must be positive"))] + pub discord_id: i64, + + /// Optional username of the new user. + pub username: Option, + + /// Unique validation token sent via Discord DM. + pub token: Uuid, + + /// Timestamp when the new user record was created. + pub created_at: Option, +} diff --git a/src/models/users/users/impl.rs b/src/models/users/users/impl.rs new file mode 100644 index 0000000..9bff74d --- /dev/null +++ b/src/models/users/users/impl.rs @@ -0,0 +1,16 @@ +use super::query::{find_by_discord_id, insert}; +use super::UsersRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl UsersRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_discord_id( + pool: &PgPool, + discord_id: i64, + ) -> Result, SqlxError> { + find_by_discord_id(pool, discord_id).await + } +} diff --git a/src/models/users/users/mod.rs b/src/models/users/users/mod.rs new file mode 100644 index 0000000..b125175 --- /dev/null +++ b/src/models/users/users/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::UsersRow; diff --git a/src/models/users/users/query/by_id.rs b/src/models/users/users/query/by_id.rs new file mode 100644 index 0000000..6b2566c --- /dev/null +++ b/src/models/users/users/query/by_id.rs @@ -0,0 +1,19 @@ +use crate::models::users::users::types::UsersRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_discord_id( + pool: &PgPool, + discord_id: i64, +) -> Result, SqlxError> { + sqlx::query_as!( + UsersRow, + r#" + SELECT discord_id, username, created_at, roles + FROM users + WHERE discord_id = $1 + "#, + discord_id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/users/users/query/insert.rs b/src/models/users/users/query/insert.rs new file mode 100644 index 0000000..a1e08cb --- /dev/null +++ b/src/models/users/users/query/insert.rs @@ -0,0 +1,5 @@ +use crate::define_insert_returning_id; +use crate::models::users::users::types::UsersRow; +// no extra imports needed + +define_insert_returning_id!(insert, "users", UsersRow, discord_id, username, roles); diff --git a/src/models/users/users/query/mod.rs b/src/models/users/users/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/users/users/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/users/users/tests/mod.rs b/src/models/users/users/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/users/users/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/users/users/types.rs b/src/models/users/users/types.rs new file mode 100644 index 0000000..2ad7003 --- /dev/null +++ b/src/models/users/users/types.rs @@ -0,0 +1,22 @@ +use chrono::NaiveDateTime; +use serde_json::Value; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct UsersRow { + /// Discord ID of the user (primary key). + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Discord ID must be positive"))] + pub discord_id: i64, + + /// Username of the user. + /// Optional field, can be None. + pub username: Option, + + /// Timestamp when the user was created. + pub created_at: Option, + + /// Roles assigned to the user stored as JSON array. + /// Defaults to ["user"]. + pub roles: Value, +} diff --git a/src/models/weekly/mod.rs b/src/models/weekly/mod.rs new file mode 100644 index 0000000..abfdc1e --- /dev/null +++ b/src/models/weekly/mod.rs @@ -0,0 +1,12 @@ +pub mod weekly; +pub mod weekly_maps; +pub mod weekly_participants; +pub mod weekly_pool; +pub mod weekly_scores; + +// Re-exports for easy access +pub use weekly::WeeklyRow; +pub use weekly_maps::WeeklyMapsRow; +pub use weekly_participants::WeeklyParticipantsRow; +pub use weekly_pool::WeeklyPoolRow; +pub use weekly_scores::WeeklyScoresRow; diff --git a/src/models/weekly/weekly/impl.rs b/src/models/weekly/weekly/impl.rs new file mode 100644 index 0000000..1e0ea1f --- /dev/null +++ b/src/models/weekly/weekly/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::WeeklyRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl WeeklyRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/weekly/weekly/mod.rs b/src/models/weekly/weekly/mod.rs new file mode 100644 index 0000000..e5ad184 --- /dev/null +++ b/src/models/weekly/weekly/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::WeeklyRow; diff --git a/src/models/weekly/weekly/query/by_id.rs b/src/models/weekly/weekly/query/by_id.rs new file mode 100644 index 0000000..9b38e1f --- /dev/null +++ b/src/models/weekly/weekly/query/by_id.rs @@ -0,0 +1,16 @@ +use crate::models::weekly::weekly::types::WeeklyRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + WeeklyRow, + r#" + SELECT id, name, end_at, start_at, created_at + FROM weekly + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/weekly/weekly/query/insert.rs b/src/models/weekly/weekly/query/insert.rs new file mode 100644 index 0000000..8e64f2f --- /dev/null +++ b/src/models/weekly/weekly/query/insert.rs @@ -0,0 +1,5 @@ +use crate::define_insert_returning_id; +use crate::models::weekly::weekly::types::WeeklyRow; +// no extra imports needed + +define_insert_returning_id!(insert, "weekly", WeeklyRow, name, end_at, start_at); diff --git a/src/models/weekly/weekly/query/mod.rs b/src/models/weekly/weekly/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/weekly/weekly/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/weekly/weekly/tests/mod.rs b/src/models/weekly/weekly/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/weekly/weekly/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/weekly/weekly/types.rs b/src/models/weekly/weekly/types.rs new file mode 100644 index 0000000..d124654 --- /dev/null +++ b/src/models/weekly/weekly/types.rs @@ -0,0 +1,30 @@ +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct WeeklyRow { + /// Unique identifier for the weekly record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Name of the weekly challenge. + /// Must be between 1 and 255 characters. + #[validate(length( + min = 1, + max = 255, + message = "Name must be between 1 and 255 characters" + ))] + pub name: String, + + /// End date of the weekly challenge. + /// Can be null if the weekly is ongoing. + pub end_at: Option, + + /// Start date of the weekly challenge. + /// Can be null if the weekly hasn't started yet. + pub start_at: Option, + + /// Timestamp when the weekly challenge was created. + pub created_at: Option, +} diff --git a/src/models/weekly/weekly_maps/impl.rs b/src/models/weekly/weekly_maps/impl.rs new file mode 100644 index 0000000..ac120b9 --- /dev/null +++ b/src/models/weekly/weekly_maps/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::WeeklyMapsRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl WeeklyMapsRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/weekly/weekly_maps/mod.rs b/src/models/weekly/weekly_maps/mod.rs new file mode 100644 index 0000000..042ac40 --- /dev/null +++ b/src/models/weekly/weekly_maps/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::WeeklyMapsRow; diff --git a/src/models/weekly/weekly_maps/query/by_id.rs b/src/models/weekly/weekly_maps/query/by_id.rs new file mode 100644 index 0000000..627542f --- /dev/null +++ b/src/models/weekly/weekly_maps/query/by_id.rs @@ -0,0 +1,16 @@ +use crate::models::weekly::weekly_maps::types::WeeklyMapsRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + WeeklyMapsRow, + r#" + SELECT id, beatmap_id, weekly_id, max_rate, created_at + FROM weekly_maps + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/weekly/weekly_maps/query/insert.rs b/src/models/weekly/weekly_maps/query/insert.rs new file mode 100644 index 0000000..1c30e1a --- /dev/null +++ b/src/models/weekly/weekly_maps/query/insert.rs @@ -0,0 +1,12 @@ +use crate::define_insert_returning_id; +use crate::models::weekly::weekly_maps::types::WeeklyMapsRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "weekly_maps", + WeeklyMapsRow, + beatmap_id, + weekly_id, + max_rate +); diff --git a/src/models/weekly/weekly_maps/query/mod.rs b/src/models/weekly/weekly_maps/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/weekly/weekly_maps/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/weekly/weekly_maps/tests/mod.rs b/src/models/weekly/weekly_maps/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/weekly/weekly_maps/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/weekly/weekly_maps/types.rs b/src/models/weekly/weekly_maps/types.rs new file mode 100644 index 0000000..c28f5eb --- /dev/null +++ b/src/models/weekly/weekly_maps/types.rs @@ -0,0 +1,28 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct WeeklyMapsRow { + /// Unique identifier for the weekly maps record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Reference to the beatmap this weekly map refers to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Beatmap ID must be positive"))] + pub beatmap_id: i32, + + /// Reference to the weekly challenge this map belongs to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Weekly ID must be positive"))] + pub weekly_id: i32, + + /// Maximum rate multiplier allowed for this map. + /// Must be between 0.5 and 10.0 (decimal(4,2) constraint). + pub max_rate: BigDecimal, + + /// Timestamp when the weekly map was created. + pub created_at: Option, +} diff --git a/src/models/weekly/weekly_participants/impl.rs b/src/models/weekly/weekly_participants/impl.rs new file mode 100644 index 0000000..ac0d408 --- /dev/null +++ b/src/models/weekly/weekly_participants/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::WeeklyParticipantsRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl WeeklyParticipantsRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/weekly/weekly_participants/mod.rs b/src/models/weekly/weekly_participants/mod.rs new file mode 100644 index 0000000..c49f086 --- /dev/null +++ b/src/models/weekly/weekly_participants/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::WeeklyParticipantsRow; diff --git a/src/models/weekly/weekly_participants/query/by_id.rs b/src/models/weekly/weekly_participants/query/by_id.rs new file mode 100644 index 0000000..0f7e75d --- /dev/null +++ b/src/models/weekly/weekly_participants/query/by_id.rs @@ -0,0 +1,19 @@ +use crate::models::weekly::weekly_participants::types::WeeklyParticipantsRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id( + pool: &PgPool, + id: i32, +) -> Result, SqlxError> { + sqlx::query_as!( + WeeklyParticipantsRow, + r#" + SELECT id, user_id, weekly_id, op, final_rank, created_at + FROM weekly_participants + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/weekly/weekly_participants/query/insert.rs b/src/models/weekly/weekly_participants/query/insert.rs new file mode 100644 index 0000000..0238ce6 --- /dev/null +++ b/src/models/weekly/weekly_participants/query/insert.rs @@ -0,0 +1,13 @@ +use crate::define_insert_returning_id; +use crate::models::weekly::weekly_participants::types::WeeklyParticipantsRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "weekly_participants", + WeeklyParticipantsRow, + user_id, + weekly_id, + op, + final_rank +); diff --git a/src/models/weekly/weekly_participants/query/mod.rs b/src/models/weekly/weekly_participants/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/weekly/weekly_participants/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/weekly/weekly_participants/tests/mod.rs b/src/models/weekly/weekly_participants/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/weekly/weekly_participants/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/weekly/weekly_participants/types.rs b/src/models/weekly/weekly_participants/types.rs new file mode 100644 index 0000000..26f4d10 --- /dev/null +++ b/src/models/weekly/weekly_participants/types.rs @@ -0,0 +1,33 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct WeeklyParticipantsRow { + /// Unique identifier for the weekly participants record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Discord ID of the participant. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "User ID must be positive"))] + pub user_id: i64, + + /// Reference to the weekly challenge this participant belongs to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Weekly ID must be positive"))] + pub weekly_id: i32, + + /// Overall Performance (OP) points for this participant. + /// Must be a non-negative decimal (≥ 0). + pub op: BigDecimal, + + /// Final rank of the participant (computed at the end). + /// Must be a positive integer (≥ 1) if set. + #[validate(range(min = 1, message = "Final rank must be positive"))] + pub final_rank: Option, + + /// Timestamp when the participant joined the weekly challenge. + pub created_at: Option, +} diff --git a/src/models/weekly/weekly_pool/impl.rs b/src/models/weekly/weekly_pool/impl.rs new file mode 100644 index 0000000..8687a70 --- /dev/null +++ b/src/models/weekly/weekly_pool/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::WeeklyPoolRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl WeeklyPoolRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/weekly/weekly_pool/mod.rs b/src/models/weekly/weekly_pool/mod.rs new file mode 100644 index 0000000..ab23cd8 --- /dev/null +++ b/src/models/weekly/weekly_pool/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::WeeklyPoolRow; diff --git a/src/models/weekly/weekly_pool/query/by_id.rs b/src/models/weekly/weekly_pool/query/by_id.rs new file mode 100644 index 0000000..b3099b5 --- /dev/null +++ b/src/models/weekly/weekly_pool/query/by_id.rs @@ -0,0 +1,16 @@ +use crate::models::weekly::weekly_pool::types::WeeklyPoolRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + WeeklyPoolRow, + r#" + SELECT id, beatmap_id, weekly_id, vote_count, created_at + FROM weekly_pool + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/weekly/weekly_pool/query/insert.rs b/src/models/weekly/weekly_pool/query/insert.rs new file mode 100644 index 0000000..8f6f6ef --- /dev/null +++ b/src/models/weekly/weekly_pool/query/insert.rs @@ -0,0 +1,12 @@ +use crate::define_insert_returning_id; +use crate::models::weekly::weekly_pool::types::WeeklyPoolRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "weekly_pool", + WeeklyPoolRow, + beatmap_id, + weekly_id, + vote_count +); diff --git a/src/models/weekly/weekly_pool/query/mod.rs b/src/models/weekly/weekly_pool/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/weekly/weekly_pool/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/weekly/weekly_pool/tests/mod.rs b/src/models/weekly/weekly_pool/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/weekly/weekly_pool/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/weekly/weekly_pool/types.rs b/src/models/weekly/weekly_pool/types.rs new file mode 100644 index 0000000..f8e341b --- /dev/null +++ b/src/models/weekly/weekly_pool/types.rs @@ -0,0 +1,28 @@ +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct WeeklyPoolRow { + /// Unique identifier for the weekly pool record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Reference to the beatmap this pool entry refers to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Beatmap ID must be positive"))] + pub beatmap_id: i32, + + /// Reference to the weekly challenge this pool belongs to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Weekly ID must be positive"))] + pub weekly_id: i32, + + /// Number of votes for this beatmap in the pool. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Vote count must be non-negative"))] + pub vote_count: i32, + + /// Timestamp when the weekly pool entry was created. + pub created_at: Option, +} diff --git a/src/models/weekly/weekly_scores/impl.rs b/src/models/weekly/weekly_scores/impl.rs new file mode 100644 index 0000000..30edb2a --- /dev/null +++ b/src/models/weekly/weekly_scores/impl.rs @@ -0,0 +1,13 @@ +use super::query::{find_by_id, insert}; +use super::WeeklyScoresRow; +use sqlx::{Error as SqlxError, PgPool}; + +impl WeeklyScoresRow { + pub async fn insert(self, pool: &PgPool) -> Result { + insert(pool, self).await + } + + pub async fn find_by_id(pool: &PgPool, id: i32) -> Result { + find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) + } +} diff --git a/src/models/weekly/weekly_scores/mod.rs b/src/models/weekly/weekly_scores/mod.rs new file mode 100644 index 0000000..47f7dcd --- /dev/null +++ b/src/models/weekly/weekly_scores/mod.rs @@ -0,0 +1,8 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +pub use types::WeeklyScoresRow; diff --git a/src/models/weekly/weekly_scores/query/by_id.rs b/src/models/weekly/weekly_scores/query/by_id.rs new file mode 100644 index 0000000..fbd57c3 --- /dev/null +++ b/src/models/weekly/weekly_scores/query/by_id.rs @@ -0,0 +1,16 @@ +use crate::models::weekly::weekly_scores::types::WeeklyScoresRow; +use sqlx::{Error as SqlxError, PgPool}; + +pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxError> { + sqlx::query_as!( + WeeklyScoresRow, + r#" + SELECT id, user_id, weekly_id, score_id, op, created_at + FROM weekly_scores + WHERE id = $1 + "#, + id + ) + .fetch_optional(pool) + .await +} diff --git a/src/models/weekly/weekly_scores/query/insert.rs b/src/models/weekly/weekly_scores/query/insert.rs new file mode 100644 index 0000000..bf438f7 --- /dev/null +++ b/src/models/weekly/weekly_scores/query/insert.rs @@ -0,0 +1,13 @@ +use crate::define_insert_returning_id; +use crate::models::weekly::weekly_scores::types::WeeklyScoresRow; +// no extra imports needed + +define_insert_returning_id!( + insert, + "weekly_scores", + WeeklyScoresRow, + user_id, + weekly_id, + score_id, + op +); diff --git a/src/models/weekly/weekly_scores/query/mod.rs b/src/models/weekly/weekly_scores/query/mod.rs new file mode 100644 index 0000000..a9a3d33 --- /dev/null +++ b/src/models/weekly/weekly_scores/query/mod.rs @@ -0,0 +1,5 @@ +pub mod by_id; +pub mod insert; + +pub use by_id::*; +pub use insert::*; diff --git a/src/models/weekly/weekly_scores/tests/mod.rs b/src/models/weekly/weekly_scores/tests/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/models/weekly/weekly_scores/tests/mod.rs @@ -0,0 +1 @@ + diff --git a/src/models/weekly/weekly_scores/types.rs b/src/models/weekly/weekly_scores/types.rs new file mode 100644 index 0000000..fbbd6ce --- /dev/null +++ b/src/models/weekly/weekly_scores/types.rs @@ -0,0 +1,33 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct WeeklyScoresRow { + /// Unique identifier for the weekly scores record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Discord ID of the user who submitted this score. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "User ID must be positive"))] + pub user_id: i64, + + /// Reference to the weekly challenge this score belongs to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Weekly ID must be positive"))] + pub weekly_id: i32, + + /// Reference to the score record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Score ID must be positive"))] + pub score_id: i32, + + /// Overall Performance (OP) points for this score. + /// Must be a non-negative decimal (≥ 0). + pub op: BigDecimal, + + /// Timestamp when the weekly score was created. + pub created_at: Option, +}