From 59ba8c59d69285684265f41ccee6398b02f09e77 Mon Sep 17 00:00:00 2001 From: Glubus Date: Thu, 18 Sep 2025 21:01:46 +0200 Subject: [PATCH 1/2] Reorganize database models into domain-based structure - Reorganized all models into domain folders: users/, beatmap/, rating/, score/, weekly/, other/ - Each model now has proper structure: types.rs, impl.rs, mod.rs, query/insert.rs, query/by_id.rs - Added proper validation using validator crate - Removed old flat structure and consolidated related models - All models follow consistent patterns with proper error handling --- src/models/beatmap/beatmap/impl.rs | 17 ++ .../beatmap}/mod.rs | 4 +- src/models/beatmap/beatmap/query/by_id.rs | 31 +++ src/models/beatmap/beatmap/query/insert.rs | 6 + src/models/beatmap/beatmap/query/mod.rs | 3 + src/models/beatmap/beatmap/types.rs | 96 +++++++ src/models/beatmap/beatmapset/impl.rs | 18 ++ src/models/{ => beatmap}/beatmapset/mod.rs | 3 +- src/models/beatmap/beatmapset/query/by_id.rs | 31 +++ src/models/beatmap/beatmapset/query/insert.rs | 6 + src/models/beatmap/beatmapset/query/mod.rs | 3 + src/models/beatmap/beatmapset/types.rs | 89 +++++++ src/models/beatmap/impl.rs | 24 -- src/models/beatmap/mod.rs | 18 +- src/models/beatmap/pending_beatmap/impl.rs | 18 ++ .../pending_beatmap}/mod.rs | 4 +- .../beatmap/pending_beatmap/query/by_id.rs | 30 +++ .../beatmap/pending_beatmap/query/insert.rs | 6 + .../beatmap/pending_beatmap/query/mod.rs | 3 + .../{ => beatmap}/pending_beatmap/types.rs | 15 +- src/models/beatmap/query/by_id.rs | 21 -- src/models/beatmap/query/count.rs | 83 ------ src/models/beatmap/query/exists.rs | 11 - src/models/beatmap/query/insert.rs | 27 -- src/models/beatmap/query/mod.rs | 11 - src/models/beatmap/query/search.rs | 17 -- src/models/beatmap/rates/impl.rs | 13 + src/models/beatmap/rates/mod.rs | 9 + src/models/beatmap/rates/query/by_id.rs | 17 ++ src/models/beatmap/rates/query/insert.rs | 6 + src/models/beatmap/rates/query/mod.rs | 3 + src/models/beatmap/rates/types.rs | 48 ++++ src/models/beatmap/tests/mod.rs | 1 - .../beatmap/tests/validation/ar_tests.rs | 37 --- .../beatmap/tests/validation/bpm_tests.rs | 36 --- .../beatmap/tests/validation/cs_tests.rs | 36 --- .../validation/difficulty_rating_tests.rs | 36 --- .../beatmap/tests/validation/hp_tests.rs | 36 --- src/models/beatmap/tests/validation/mod.rs | 8 - .../beatmap/tests/validation/mode_tests.rs | 35 --- .../beatmap/tests/validation/od_tests.rs | 36 --- .../beatmap/tests/validation/status_tests.rs | 44 ---- src/models/beatmap/types.rs | 54 ---- src/models/beatmap/validators.rs | 67 ----- src/models/beatmapset/impl.rs | 38 --- src/models/beatmapset/query/by_id.rs | 8 - src/models/beatmapset/query/by_osu_id.rs | 15 -- src/models/beatmapset/query/count.rs | 17 -- src/models/beatmapset/query/exists.rs | 11 - src/models/beatmapset/query/insert.rs | 35 --- src/models/beatmapset/query/mod.rs | 13 - src/models/beatmapset/query/search.rs | 43 --- src/models/beatmapset/tests/mod.rs | 2 - src/models/beatmapset/tests/model_tests.rs | 247 ------------------ src/models/beatmapset/tests/validation/mod.rs | 2 - .../beatmapset/tests/validation/tags_tests.rs | 69 ----- .../beatmapset/tests/validation/url_tests.rs | 64 ----- src/models/beatmapset/types.rs | 64 ----- src/models/beatmapset/validators.rs | 37 --- .../failed_query/query/delete_by_hash.rs | 15 -- .../failed_query/query/exists_by_hash.rs | 14 - src/models/failed_query/query/mod.rs | 9 - src/models/failed_query/tests/mod.rs | 38 --- src/models/failed_query/tests/model_tests.rs | 73 ------ .../tests/validation/hash_tests.rs | 134 ---------- .../failed_query/tests/validation/mod.rs | 6 - src/models/failed_query/types.rs | 25 -- src/models/mod.rs | 19 +- src/models/msd/impl.rs | 37 --- src/models/msd/mod.rs | 10 - src/models/msd/query/by_beatmap_id.rs | 47 ---- src/models/msd/query/count_by_pattern.rs | 42 --- src/models/msd/query/insert.rs | 19 -- src/models/msd/query/mod.rs | 9 - src/models/msd/tests/mod.rs | 1 - .../tests/validation/main_pattern_tests.rs | 84 ------ src/models/msd/tests/validation/mod.rs | 3 - .../msd/tests/validation/msd_value_tests.rs | 45 ---- .../msd/tests/validation/rate_value_tests.rs | 60 ----- src/models/msd/types.rs | 47 ---- src/models/msd/validators.rs | 36 --- src/models/{ => other}/failed_query/impl.rs | 11 +- src/models/{ => other}/failed_query/mod.rs | 1 + .../{ => other}/failed_query/query/by_id.rs | 3 +- .../{ => other}/failed_query/query/insert.rs | 3 +- src/models/other/failed_query/query/mod.rs | 3 + src/models/other/failed_query/types.rs | 19 ++ src/models/other/mod.rs | 4 + src/models/pending_beatmap/impl.rs | 36 --- .../pending_beatmap/query/bulk_insert.rs | 22 -- src/models/pending_beatmap/query/count.rs | 7 - src/models/pending_beatmap/query/delete.rs | 19 -- src/models/pending_beatmap/query/insert.rs | 4 - src/models/pending_beatmap/query/mod.rs | 12 - src/models/pending_beatmap/query/oldest.rs | 18 -- .../query/position_by_osu_id.rs | 21 -- src/models/pending_beatmap/tests/mod.rs | 1 - .../tests/validation/hash_tests.rs | 120 --------- .../pending_beatmap/tests/validation/mod.rs | 1 - .../rating/beatmap_mania_rating/impl.rs | 14 + src/models/rating/beatmap_mania_rating/mod.rs | 9 + .../beatmap_mania_rating/query/by_id.rs | 17 ++ .../beatmap_mania_rating/query/insert.rs | 6 + .../rating/beatmap_mania_rating/query/mod.rs | 3 + .../rating/beatmap_mania_rating/types.rs | 56 ++++ src/models/rating/beatmap_rating/impl.rs | 14 + src/models/rating/beatmap_rating/mod.rs | 9 + .../rating/beatmap_rating/query/by_id.rs | 17 ++ .../rating/beatmap_rating/query/insert.rs | 5 + src/models/rating/beatmap_rating/query/mod.rs | 3 + src/models/rating/beatmap_rating/types.rs | 34 +++ src/models/rating/mod.rs | 10 + src/models/rating/score_mania_rating/impl.rs | 14 + src/models/rating/score_mania_rating/mod.rs | 9 + .../rating/score_mania_rating/query/by_id.rs | 17 ++ .../rating/score_mania_rating/query/insert.rs | 6 + .../rating/score_mania_rating/query/mod.rs | 3 + src/models/rating/score_mania_rating/types.rs | 53 ++++ src/models/rating/score_rating/impl.rs | 14 + src/models/rating/score_rating/mod.rs | 9 + .../{ => rating}/score_rating/query/by_id.rs | 11 +- .../rating/score_rating/query/insert.rs | 6 + src/models/rating/score_rating/query/mod.rs | 3 + src/models/rating/score_rating/types.rs | 36 +++ src/models/replay/impl.rs | 9 - src/models/replay/query/insert.rs | 15 -- src/models/replay/query/mod.rs | 5 - src/models/replay/tests/mod.rs | 1 - .../replay/tests/validation/hash_tests.rs | 89 ------- src/models/replay/tests/validation/mod.rs | 2 - .../tests/validation/replay_path_tests.rs | 69 ----- src/models/replay/types.rs | 33 --- src/models/score/impl.rs | 52 ---- src/models/score/mod.rs | 15 +- src/models/score/query/by_beatmap_id.rs | 23 -- src/models/score/query/by_id.rs | 19 -- src/models/score/query/by_pending.rs | 20 -- src/models/score/query/by_user_id.rs | 20 -- src/models/score/query/exists.rs | 16 -- src/models/score/query/insert.rs | 6 - src/models/score/query/mod.rs | 7 - src/models/score/query/update_status.rs | 37 --- src/models/score/replay/impl.rs | 14 + src/models/score/replay/mod.rs | 9 + src/models/{ => score}/replay/query/by_id.rs | 7 +- src/models/score/replay/query/insert.rs | 6 + src/models/score/replay/query/mod.rs | 3 + src/models/score/replay/types.rs | 19 ++ src/models/score/score/impl.rs | 14 + src/models/score/score/mod.rs | 9 + src/models/score/score/query/by_id.rs | 17 ++ src/models/score/score/query/insert.rs | 6 + src/models/score/score/query/mod.rs | 3 + src/models/score/score/types.rs | 66 +++++ src/models/score/score_metadata/impl.rs | 14 + src/models/score/score_metadata/mod.rs | 9 + .../score/score_metadata/query/by_id.rs | 17 ++ .../score/score_metadata/query/insert.rs | 6 + src/models/score/score_metadata/query/mod.rs | 3 + src/models/score/score_metadata/types.rs | 58 ++++ src/models/score/tests/mod.rs | 1 - .../score/tests/validation/hash_tests.rs | 104 -------- .../score/tests/validation/hwid_tests.rs | 85 ------ src/models/score/tests/validation/mod.rs | 5 - .../score/tests/validation/rank_tests.rs | 122 --------- .../score/tests/validation/rate_tests.rs | 54 ---- .../score/tests/validation/status_tests.rs | 161 ------------ src/models/score/types.rs | 68 ----- src/models/score/validators.rs | 9 - src/models/score_metadata/impl.rs | 13 - src/models/score_metadata/mod.rs | 9 - src/models/score_metadata/query/by_id.rs | 5 - src/models/score_metadata/query/insert.rs | 23 -- src/models/score_metadata/query/mod.rs | 5 - src/models/score_metadata/tests/mod.rs | 1 - .../tests/validation/accuracy_tests.rs | 55 ---- .../score_metadata/tests/validation/mod.rs | 1 - src/models/score_metadata/types.rs | 56 ---- src/models/score_metadata/validators.rs | 10 - src/models/score_rating/impl.rs | 23 -- src/models/score_rating/query/by_score_id.rs | 22 -- src/models/score_rating/query/insert.rs | 26 -- src/models/score_rating/query/mod.rs | 7 - src/models/score_rating/tests/mod.rs | 1 - .../score_rating/tests/validation/mod.rs | 2 - .../tests/validation/rating_tests.rs | 55 ---- .../tests/validation/rating_type_tests.rs | 129 --------- src/models/score_rating/types.rs | 28 -- src/models/score_rating/validators.rs | 9 - src/models/users/bans/impl.rs | 14 + src/models/users/bans/mod.rs | 9 + src/models/{msd => users/bans}/query/by_id.rs | 11 +- src/models/users/bans/query/insert.rs | 6 + src/models/users/bans/query/mod.rs | 3 + src/models/users/bans/types.rs | 21 ++ src/models/users/device_tokens/impl.rs | 15 ++ src/models/users/device_tokens/mod.rs | 9 + src/models/users/device_tokens/query/by_id.rs | 18 ++ .../users/device_tokens/query/insert.rs | 6 + src/models/users/device_tokens/query/mod.rs | 3 + src/models/users/device_tokens/types.rs | 24 ++ src/models/users/mod.rs | 10 + src/models/users/new_users/impl.rs | 19 ++ src/models/users/new_users/mod.rs | 9 + src/models/users/new_users/query/by_id.rs | 32 +++ src/models/users/new_users/query/insert.rs | 6 + src/models/users/new_users/query/mod.rs | 3 + src/models/users/new_users/types.rs | 21 ++ src/models/users/users/impl.rs | 13 + src/models/{replay => users/users}/mod.rs | 2 +- src/models/users/users/query/by_id.rs | 16 ++ src/models/users/users/query/insert.rs | 5 + src/models/users/users/query/mod.rs | 2 + src/models/users/users/types.rs | 22 ++ src/models/weekly/mod.rs | 12 + src/models/weekly/weekly/impl.rs | 14 + src/models/weekly/weekly/mod.rs | 9 + src/models/weekly/weekly/query/by_id.rs | 17 ++ src/models/weekly/weekly/query/insert.rs | 6 + src/models/weekly/weekly/query/mod.rs | 3 + src/models/weekly/weekly/types.rs | 32 +++ src/models/weekly/weekly_maps/impl.rs | 14 + src/models/weekly/weekly_maps/mod.rs | 9 + src/models/weekly/weekly_maps/query/by_id.rs | 17 ++ src/models/weekly/weekly_maps/query/insert.rs | 6 + src/models/weekly/weekly_maps/query/mod.rs | 3 + src/models/weekly/weekly_maps/types.rs | 24 ++ src/models/weekly/weekly_participants/impl.rs | 14 + src/models/weekly/weekly_participants/mod.rs | 9 + .../weekly/weekly_participants/query/by_id.rs | 17 ++ .../weekly_participants/query/insert.rs | 6 + .../weekly/weekly_participants/query/mod.rs | 3 + .../weekly/weekly_participants/types.rs | 24 ++ src/models/weekly/weekly_pool/impl.rs | 14 + src/models/weekly/weekly_pool/mod.rs | 9 + src/models/weekly/weekly_pool/query/by_id.rs | 17 ++ src/models/weekly/weekly_pool/query/insert.rs | 6 + src/models/weekly/weekly_pool/query/mod.rs | 3 + src/models/weekly/weekly_pool/types.rs | 28 ++ src/models/weekly/weekly_scores/impl.rs | 14 + src/models/weekly/weekly_scores/mod.rs | 9 + .../weekly/weekly_scores/query/by_id.rs | 17 ++ .../weekly/weekly_scores/query/insert.rs | 6 + src/models/weekly/weekly_scores/query/mod.rs | 3 + src/models/weekly/weekly_scores/types.rs | 24 ++ 245 files changed, 1862 insertions(+), 3871 deletions(-) create mode 100644 src/models/beatmap/beatmap/impl.rs rename src/models/{pending_beatmap => beatmap/beatmap}/mod.rs (66%) create mode 100644 src/models/beatmap/beatmap/query/by_id.rs create mode 100644 src/models/beatmap/beatmap/query/insert.rs create mode 100644 src/models/beatmap/beatmap/query/mod.rs create mode 100644 src/models/beatmap/beatmap/types.rs create mode 100644 src/models/beatmap/beatmapset/impl.rs rename src/models/{ => beatmap}/beatmapset/mod.rs (69%) create mode 100644 src/models/beatmap/beatmapset/query/by_id.rs create mode 100644 src/models/beatmap/beatmapset/query/insert.rs create mode 100644 src/models/beatmap/beatmapset/query/mod.rs create mode 100644 src/models/beatmap/beatmapset/types.rs delete mode 100644 src/models/beatmap/impl.rs create mode 100644 src/models/beatmap/pending_beatmap/impl.rs rename src/models/{score_rating => beatmap/pending_beatmap}/mod.rs (65%) create mode 100644 src/models/beatmap/pending_beatmap/query/by_id.rs create mode 100644 src/models/beatmap/pending_beatmap/query/insert.rs create mode 100644 src/models/beatmap/pending_beatmap/query/mod.rs rename src/models/{ => beatmap}/pending_beatmap/types.rs (50%) delete mode 100644 src/models/beatmap/query/by_id.rs delete mode 100644 src/models/beatmap/query/count.rs delete mode 100644 src/models/beatmap/query/exists.rs delete mode 100644 src/models/beatmap/query/insert.rs delete mode 100644 src/models/beatmap/query/mod.rs delete mode 100644 src/models/beatmap/query/search.rs create mode 100644 src/models/beatmap/rates/impl.rs create mode 100644 src/models/beatmap/rates/mod.rs create mode 100644 src/models/beatmap/rates/query/by_id.rs create mode 100644 src/models/beatmap/rates/query/insert.rs create mode 100644 src/models/beatmap/rates/query/mod.rs create mode 100644 src/models/beatmap/rates/types.rs delete mode 100644 src/models/beatmap/tests/mod.rs delete mode 100644 src/models/beatmap/tests/validation/ar_tests.rs delete mode 100644 src/models/beatmap/tests/validation/bpm_tests.rs delete mode 100644 src/models/beatmap/tests/validation/cs_tests.rs delete mode 100644 src/models/beatmap/tests/validation/difficulty_rating_tests.rs delete mode 100644 src/models/beatmap/tests/validation/hp_tests.rs delete mode 100644 src/models/beatmap/tests/validation/mod.rs delete mode 100644 src/models/beatmap/tests/validation/mode_tests.rs delete mode 100644 src/models/beatmap/tests/validation/od_tests.rs delete mode 100644 src/models/beatmap/tests/validation/status_tests.rs delete mode 100644 src/models/beatmap/types.rs delete mode 100644 src/models/beatmap/validators.rs delete mode 100644 src/models/beatmapset/impl.rs delete mode 100644 src/models/beatmapset/query/by_id.rs delete mode 100644 src/models/beatmapset/query/by_osu_id.rs delete mode 100644 src/models/beatmapset/query/count.rs delete mode 100644 src/models/beatmapset/query/exists.rs delete mode 100644 src/models/beatmapset/query/insert.rs delete mode 100644 src/models/beatmapset/query/mod.rs delete mode 100644 src/models/beatmapset/query/search.rs delete mode 100644 src/models/beatmapset/tests/mod.rs delete mode 100644 src/models/beatmapset/tests/model_tests.rs delete mode 100644 src/models/beatmapset/tests/validation/mod.rs delete mode 100644 src/models/beatmapset/tests/validation/tags_tests.rs delete mode 100644 src/models/beatmapset/tests/validation/url_tests.rs delete mode 100644 src/models/beatmapset/types.rs delete mode 100644 src/models/beatmapset/validators.rs delete mode 100644 src/models/failed_query/query/delete_by_hash.rs delete mode 100644 src/models/failed_query/query/exists_by_hash.rs delete mode 100644 src/models/failed_query/query/mod.rs delete mode 100644 src/models/failed_query/tests/mod.rs delete mode 100644 src/models/failed_query/tests/model_tests.rs delete mode 100644 src/models/failed_query/tests/validation/hash_tests.rs delete mode 100644 src/models/failed_query/tests/validation/mod.rs delete mode 100644 src/models/failed_query/types.rs delete mode 100644 src/models/msd/impl.rs delete mode 100644 src/models/msd/mod.rs delete mode 100644 src/models/msd/query/by_beatmap_id.rs delete mode 100644 src/models/msd/query/count_by_pattern.rs delete mode 100644 src/models/msd/query/insert.rs delete mode 100644 src/models/msd/query/mod.rs delete mode 100644 src/models/msd/tests/mod.rs delete mode 100644 src/models/msd/tests/validation/main_pattern_tests.rs delete mode 100644 src/models/msd/tests/validation/mod.rs delete mode 100644 src/models/msd/tests/validation/msd_value_tests.rs delete mode 100644 src/models/msd/tests/validation/rate_value_tests.rs delete mode 100644 src/models/msd/types.rs delete mode 100644 src/models/msd/validators.rs rename src/models/{ => other}/failed_query/impl.rs (51%) rename src/models/{ => other}/failed_query/mod.rs (99%) rename src/models/{ => other}/failed_query/query/by_id.rs (84%) rename src/models/{ => other}/failed_query/query/insert.rs (68%) create mode 100644 src/models/other/failed_query/query/mod.rs create mode 100644 src/models/other/failed_query/types.rs create mode 100644 src/models/other/mod.rs delete mode 100644 src/models/pending_beatmap/impl.rs delete mode 100644 src/models/pending_beatmap/query/bulk_insert.rs delete mode 100644 src/models/pending_beatmap/query/count.rs delete mode 100644 src/models/pending_beatmap/query/delete.rs delete mode 100644 src/models/pending_beatmap/query/insert.rs delete mode 100644 src/models/pending_beatmap/query/mod.rs delete mode 100644 src/models/pending_beatmap/query/oldest.rs delete mode 100644 src/models/pending_beatmap/query/position_by_osu_id.rs delete mode 100644 src/models/pending_beatmap/tests/mod.rs delete mode 100644 src/models/pending_beatmap/tests/validation/hash_tests.rs delete mode 100644 src/models/pending_beatmap/tests/validation/mod.rs create mode 100644 src/models/rating/beatmap_mania_rating/impl.rs create mode 100644 src/models/rating/beatmap_mania_rating/mod.rs create mode 100644 src/models/rating/beatmap_mania_rating/query/by_id.rs create mode 100644 src/models/rating/beatmap_mania_rating/query/insert.rs create mode 100644 src/models/rating/beatmap_mania_rating/query/mod.rs create mode 100644 src/models/rating/beatmap_mania_rating/types.rs create mode 100644 src/models/rating/beatmap_rating/impl.rs create mode 100644 src/models/rating/beatmap_rating/mod.rs create mode 100644 src/models/rating/beatmap_rating/query/by_id.rs create mode 100644 src/models/rating/beatmap_rating/query/insert.rs create mode 100644 src/models/rating/beatmap_rating/query/mod.rs create mode 100644 src/models/rating/beatmap_rating/types.rs create mode 100644 src/models/rating/mod.rs create mode 100644 src/models/rating/score_mania_rating/impl.rs create mode 100644 src/models/rating/score_mania_rating/mod.rs create mode 100644 src/models/rating/score_mania_rating/query/by_id.rs create mode 100644 src/models/rating/score_mania_rating/query/insert.rs create mode 100644 src/models/rating/score_mania_rating/query/mod.rs create mode 100644 src/models/rating/score_mania_rating/types.rs create mode 100644 src/models/rating/score_rating/impl.rs create mode 100644 src/models/rating/score_rating/mod.rs rename src/models/{ => rating}/score_rating/query/by_id.rs (68%) create mode 100644 src/models/rating/score_rating/query/insert.rs create mode 100644 src/models/rating/score_rating/query/mod.rs create mode 100644 src/models/rating/score_rating/types.rs delete mode 100644 src/models/replay/impl.rs delete mode 100644 src/models/replay/query/insert.rs delete mode 100644 src/models/replay/query/mod.rs delete mode 100644 src/models/replay/tests/mod.rs delete mode 100644 src/models/replay/tests/validation/hash_tests.rs delete mode 100644 src/models/replay/tests/validation/mod.rs delete mode 100644 src/models/replay/tests/validation/replay_path_tests.rs delete mode 100644 src/models/replay/types.rs delete mode 100644 src/models/score/impl.rs delete mode 100644 src/models/score/query/by_beatmap_id.rs delete mode 100644 src/models/score/query/by_id.rs delete mode 100644 src/models/score/query/by_pending.rs delete mode 100644 src/models/score/query/by_user_id.rs delete mode 100644 src/models/score/query/exists.rs delete mode 100644 src/models/score/query/insert.rs delete mode 100644 src/models/score/query/mod.rs delete mode 100644 src/models/score/query/update_status.rs create mode 100644 src/models/score/replay/impl.rs create mode 100644 src/models/score/replay/mod.rs rename src/models/{ => score}/replay/query/by_id.rs (68%) create mode 100644 src/models/score/replay/query/insert.rs create mode 100644 src/models/score/replay/query/mod.rs create mode 100644 src/models/score/replay/types.rs create mode 100644 src/models/score/score/impl.rs create mode 100644 src/models/score/score/mod.rs create mode 100644 src/models/score/score/query/by_id.rs create mode 100644 src/models/score/score/query/insert.rs create mode 100644 src/models/score/score/query/mod.rs create mode 100644 src/models/score/score/types.rs create mode 100644 src/models/score/score_metadata/impl.rs create mode 100644 src/models/score/score_metadata/mod.rs create mode 100644 src/models/score/score_metadata/query/by_id.rs create mode 100644 src/models/score/score_metadata/query/insert.rs create mode 100644 src/models/score/score_metadata/query/mod.rs create mode 100644 src/models/score/score_metadata/types.rs delete mode 100644 src/models/score/tests/mod.rs delete mode 100644 src/models/score/tests/validation/hash_tests.rs delete mode 100644 src/models/score/tests/validation/hwid_tests.rs delete mode 100644 src/models/score/tests/validation/mod.rs delete mode 100644 src/models/score/tests/validation/rank_tests.rs delete mode 100644 src/models/score/tests/validation/rate_tests.rs delete mode 100644 src/models/score/tests/validation/status_tests.rs delete mode 100644 src/models/score/types.rs delete mode 100644 src/models/score/validators.rs delete mode 100644 src/models/score_metadata/impl.rs delete mode 100644 src/models/score_metadata/mod.rs delete mode 100644 src/models/score_metadata/query/by_id.rs delete mode 100644 src/models/score_metadata/query/insert.rs delete mode 100644 src/models/score_metadata/query/mod.rs delete mode 100644 src/models/score_metadata/tests/mod.rs delete mode 100644 src/models/score_metadata/tests/validation/accuracy_tests.rs delete mode 100644 src/models/score_metadata/tests/validation/mod.rs delete mode 100644 src/models/score_metadata/types.rs delete mode 100644 src/models/score_metadata/validators.rs delete mode 100644 src/models/score_rating/impl.rs delete mode 100644 src/models/score_rating/query/by_score_id.rs delete mode 100644 src/models/score_rating/query/insert.rs delete mode 100644 src/models/score_rating/query/mod.rs delete mode 100644 src/models/score_rating/tests/mod.rs delete mode 100644 src/models/score_rating/tests/validation/mod.rs delete mode 100644 src/models/score_rating/tests/validation/rating_tests.rs delete mode 100644 src/models/score_rating/tests/validation/rating_type_tests.rs delete mode 100644 src/models/score_rating/types.rs delete mode 100644 src/models/score_rating/validators.rs create mode 100644 src/models/users/bans/impl.rs create mode 100644 src/models/users/bans/mod.rs rename src/models/{msd => users/bans}/query/by_id.rs (53%) create mode 100644 src/models/users/bans/query/insert.rs create mode 100644 src/models/users/bans/query/mod.rs create mode 100644 src/models/users/bans/types.rs create mode 100644 src/models/users/device_tokens/impl.rs create mode 100644 src/models/users/device_tokens/mod.rs create mode 100644 src/models/users/device_tokens/query/by_id.rs create mode 100644 src/models/users/device_tokens/query/insert.rs create mode 100644 src/models/users/device_tokens/query/mod.rs create mode 100644 src/models/users/device_tokens/types.rs create mode 100644 src/models/users/mod.rs create mode 100644 src/models/users/new_users/impl.rs create mode 100644 src/models/users/new_users/mod.rs create mode 100644 src/models/users/new_users/query/by_id.rs create mode 100644 src/models/users/new_users/query/insert.rs create mode 100644 src/models/users/new_users/query/mod.rs create mode 100644 src/models/users/new_users/types.rs create mode 100644 src/models/users/users/impl.rs rename src/models/{replay => users/users}/mod.rs (75%) create mode 100644 src/models/users/users/query/by_id.rs create mode 100644 src/models/users/users/query/insert.rs create mode 100644 src/models/users/users/query/mod.rs create mode 100644 src/models/users/users/types.rs create mode 100644 src/models/weekly/mod.rs create mode 100644 src/models/weekly/weekly/impl.rs create mode 100644 src/models/weekly/weekly/mod.rs create mode 100644 src/models/weekly/weekly/query/by_id.rs create mode 100644 src/models/weekly/weekly/query/insert.rs create mode 100644 src/models/weekly/weekly/query/mod.rs create mode 100644 src/models/weekly/weekly/types.rs create mode 100644 src/models/weekly/weekly_maps/impl.rs create mode 100644 src/models/weekly/weekly_maps/mod.rs create mode 100644 src/models/weekly/weekly_maps/query/by_id.rs create mode 100644 src/models/weekly/weekly_maps/query/insert.rs create mode 100644 src/models/weekly/weekly_maps/query/mod.rs create mode 100644 src/models/weekly/weekly_maps/types.rs create mode 100644 src/models/weekly/weekly_participants/impl.rs create mode 100644 src/models/weekly/weekly_participants/mod.rs create mode 100644 src/models/weekly/weekly_participants/query/by_id.rs create mode 100644 src/models/weekly/weekly_participants/query/insert.rs create mode 100644 src/models/weekly/weekly_participants/query/mod.rs create mode 100644 src/models/weekly/weekly_participants/types.rs create mode 100644 src/models/weekly/weekly_pool/impl.rs create mode 100644 src/models/weekly/weekly_pool/mod.rs create mode 100644 src/models/weekly/weekly_pool/query/by_id.rs create mode 100644 src/models/weekly/weekly_pool/query/insert.rs create mode 100644 src/models/weekly/weekly_pool/query/mod.rs create mode 100644 src/models/weekly/weekly_pool/types.rs create mode 100644 src/models/weekly/weekly_scores/impl.rs create mode 100644 src/models/weekly/weekly_scores/mod.rs create mode 100644 src/models/weekly/weekly_scores/query/by_id.rs create mode 100644 src/models/weekly/weekly_scores/query/insert.rs create mode 100644 src/models/weekly/weekly_scores/query/mod.rs create mode 100644 src/models/weekly/weekly_scores/types.rs 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/pending_beatmap/mod.rs b/src/models/beatmap/beatmap/mod.rs similarity index 66% rename from src/models/pending_beatmap/mod.rs rename to src/models/beatmap/beatmap/mod.rs index 23aa96a..ffbd47c 100644 --- a/src/models/pending_beatmap/mod.rs +++ b/src/models/beatmap/beatmap/mod.rs @@ -5,5 +5,5 @@ pub mod types; #[cfg(test)] mod tests; -pub use query::*; -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..174c53b --- /dev/null +++ b/src/models/beatmap/beatmap/query/by_id.rs @@ -0,0 +1,31 @@ +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/beatmap/query/insert.rs b/src/models/beatmap/beatmap/query/insert.rs new file mode 100644 index 0000000..3f6473b --- /dev/null +++ b/src/models/beatmap/beatmap/query/insert.rs @@ -0,0 +1,6 @@ +use crate::define_insert_returning_id; +use crate::models::beatmap::beatmap::types::BeatmapRow; +// no extra imports needed + +define_insert_returning_id!(insert, "beatmap", BeatmapRow, osu_id, beatmapset_id, difficulty, count_circles, count_sliders, count_spinners, max_combo, main_pattern, cs, ar, od, hp, mode, status); + diff --git a/src/models/beatmap/beatmap/query/mod.rs b/src/models/beatmap/beatmap/query/mod.rs new file mode 100644 index 0000000..4ee94be --- /dev/null +++ b/src/models/beatmap/beatmap/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/beatmap/beatmap/types.rs b/src/models/beatmap/beatmap/types.rs new file mode 100644 index 0000000..a639ed4 --- /dev/null +++ b/src/models/beatmap/beatmap/types.rs @@ -0,0 +1,96 @@ +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: i32, + + /// 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(range(min = 0.0, max = 10.0, message = "CS must be between 0.0 and 10.0"))] + pub cs: f64, + + /// Approach Rate (AR) value. + /// Must be between 0.0 and 10.0. + #[validate(range(min = 0.0, max = 10.0, message = "AR must be between 0.0 and 10.0"))] + pub ar: f64, + + /// Overall Difficulty (OD) value. + /// Must be between 0.0 and 10.0. + #[validate(range(min = 0.0, max = 10.0, message = "OD must be between 0.0 and 10.0"))] + pub od: f64, + + /// HP Drain (HP) value. + /// Must be between 0.0 and 10.0. + #[validate(range(min = 0.0, max = 10.0, message = "HP must be between 0.0 and 10.0"))] + pub hp: f64, + + /// 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 = "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, +} + +fn validate_status(status: &str) -> Result<(), validator::ValidationError> { + match status { + "pending" | "ranked" | "qualified" | "loved" | "graveyard" => Ok(()), + _ => Err(validator::ValidationError::new("invalid_status")), + } +} diff --git a/src/models/beatmap/beatmapset/impl.rs b/src/models/beatmap/beatmapset/impl.rs new file mode 100644 index 0000000..940b476 --- /dev/null +++ b/src/models/beatmap/beatmapset/impl.rs @@ -0,0 +1,18 @@ +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 69% rename from src/models/beatmapset/mod.rs rename to src/models/beatmap/beatmapset/mod.rs index e576b64..69e3992 100644 --- a/src/models/beatmapset/mod.rs +++ b/src/models/beatmap/beatmapset/mod.rs @@ -1,10 +1,9 @@ pub mod r#impl; pub mod query; pub mod types; -pub(super) mod validators; #[cfg(test)] mod tests; -pub use query::*; pub use types::BeatmapsetRow; + 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..82c798e --- /dev/null +++ b/src/models/beatmap/beatmapset/query/by_id.rs @@ -0,0 +1,31 @@ +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..a0f543b --- /dev/null +++ b/src/models/beatmap/beatmapset/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/beatmap/beatmapset/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/beatmap/beatmapset/types.rs b/src/models/beatmap/beatmapset/types.rs new file mode 100644 index 0000000..bf0c215 --- /dev/null +++ b/src/models/beatmap/beatmapset/types.rs @@ -0,0 +1,89 @@ +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: i32, + + /// 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..8c274ab 100644 --- a/src/models/beatmap/mod.rs +++ b/src/models/beatmap/mod.rs @@ -1,10 +1,10 @@ -pub mod r#impl; -pub mod query; -pub mod types; -pub(super) mod validators; +pub mod beatmap; +pub mod beatmapset; +pub mod pending_beatmap; +pub mod rates; -#[cfg(test)] -mod tests; - -pub use query::*; -pub use types::BeatmapRow; +// Re-exports for easy access +pub use beatmap::*; +pub use beatmapset::*; +pub use pending_beatmap::*; +pub use rates::*; \ No newline at end of file diff --git a/src/models/beatmap/pending_beatmap/impl.rs b/src/models/beatmap/pending_beatmap/impl.rs new file mode 100644 index 0000000..d050390 --- /dev/null +++ b/src/models/beatmap/pending_beatmap/impl.rs @@ -0,0 +1,18 @@ +use super::query::{find_by_id, find_by_hash, 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/score_rating/mod.rs b/src/models/beatmap/pending_beatmap/mod.rs similarity index 65% rename from src/models/score_rating/mod.rs rename to src/models/beatmap/pending_beatmap/mod.rs index 7134bd1..7baee86 100644 --- a/src/models/score_rating/mod.rs +++ b/src/models/beatmap/pending_beatmap/mod.rs @@ -1,9 +1,9 @@ pub mod r#impl; pub mod query; pub mod types; -pub mod validators; #[cfg(test)] mod tests; -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..68ed2cc --- /dev/null +++ b/src/models/beatmap/pending_beatmap/query/by_id.rs @@ -0,0 +1,30 @@ +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..1c23434 --- /dev/null +++ b/src/models/beatmap/pending_beatmap/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/beatmap/pending_beatmap/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + 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..20d9ad5 100644 --- a/src/models/pending_beatmap/types.rs +++ b/src/models/beatmap/pending_beatmap/types.rs @@ -3,24 +3,31 @@ 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/insert.rs b/src/models/beatmap/query/insert.rs deleted file mode 100644 index 5cac24f..0000000 --- a/src/models/beatmap/query/insert.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::define_insert_returning_id; -use crate::models::beatmap::types::BeatmapRow; - -define_insert_returning_id!( - insert, - "beatmap", - BeatmapRow, - 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 -); 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/beatmap/rates/mod.rs b/src/models/beatmap/rates/mod.rs new file mode 100644 index 0000000..4bc0a40 --- /dev/null +++ b/src/models/beatmap/rates/mod.rs @@ -0,0 +1,9 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +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..7033361 --- /dev/null +++ b/src/models/beatmap/rates/query/by_id.rs @@ -0,0 +1,17 @@ +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..723dc8d --- /dev/null +++ b/src/models/beatmap/rates/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/beatmap/rates/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/beatmap/rates/types.rs b/src/models/beatmap/rates/types.rs new file mode 100644 index 0000000..dabb517 --- /dev/null +++ b/src/models/beatmap/rates/types.rs @@ -0,0 +1,48 @@ +use chrono::NaiveDateTime; +use validator::Validate; + +#[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" + ))] + 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. + #[validate(range(min = 0.01, message = "BPM must be positive"))] + pub bpm: f64, + + /// 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/failed_query/types.rs b/src/models/failed_query/types.rs deleted file mode 100644 index ae0de6c..0000000 --- a/src/models/failed_query/types.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::utils::HASH_REGEX; -use chrono::NaiveDateTime; -use validator::Validate; - -#[derive(Debug, Clone, sqlx::FromRow, Validate)] -pub struct FailedQueryRow { - /// Unique identifier for the failed query record. - /// Must be a positive integer (≥ 1). - #[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. - #[validate(length( - min = 1, - max = 255, - message = "Hash must be between 1 and 255 characters" - ))] - #[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. - pub created_at: Option, -} diff --git a/src/models/mod.rs b/src/models/mod.rs index e3e93b9..6a82e13 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::*; \ No newline at end of file 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..c6db18f 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,8 @@ 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 99% rename from src/models/failed_query/mod.rs rename to src/models/other/failed_query/mod.rs index 451b0e7..ef5eccd 100644 --- a/src/models/failed_query/mod.rs +++ b/src/models/other/failed_query/mod.rs @@ -6,3 +6,4 @@ pub mod types; mod tests; pub use types::FailedQueryRow; + 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..3f775dc 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> { @@ -14,3 +14,4 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result .fetch_optional(pool) .await } + diff --git a/src/models/failed_query/query/insert.rs b/src/models/other/failed_query/query/insert.rs similarity index 68% rename from src/models/failed_query/query/insert.rs rename to src/models/other/failed_query/query/insert.rs index cc0e74e..1d564c2 100644 --- a/src/models/failed_query/query/insert.rs +++ b/src/models/other/failed_query/query/insert.rs @@ -1,5 +1,6 @@ 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..4ee94be --- /dev/null +++ b/src/models/other/failed_query/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/other/failed_query/types.rs b/src/models/other/failed_query/types.rs new file mode 100644 index 0000000..a512def --- /dev/null +++ b/src/models/other/failed_query/types.rs @@ -0,0 +1,19 @@ +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct FailedQueryRow { + /// Unique identifier for the failed query record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// 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"))] + pub hash: String, + + /// 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..80cea53 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/impl.rs @@ -0,0 +1,14 @@ +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..f706b20 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/mod.rs @@ -0,0 +1,9 @@ +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..37a2ae0 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/query/by_id.rs @@ -0,0 +1,17 @@ +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..6d10537 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + 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..be7ea82 --- /dev/null +++ b/src/models/rating/beatmap_mania_rating/types.rs @@ -0,0 +1,56 @@ +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). + #[validate(range(min = 0.0, message = "Stream rating must be non-negative"))] + pub stream: Option, + + /// Jumpstream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Jumpstream rating must be non-negative"))] + pub jumpstream: Option, + + /// Handstream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Handstream rating must be non-negative"))] + pub handstream: Option, + + /// Stamina difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Stamina rating must be non-negative"))] + pub stamina: Option, + + /// Jackspeed difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Jackspeed rating must be non-negative"))] + pub jackspeed: Option, + + /// Chordjack difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Chordjack rating must be non-negative"))] + pub chordjack: Option, + + /// Technical difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Technical rating must be non-negative"))] + 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..9497fc3 --- /dev/null +++ b/src/models/rating/beatmap_rating/impl.rs @@ -0,0 +1,14 @@ +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..148c423 --- /dev/null +++ b/src/models/rating/beatmap_rating/mod.rs @@ -0,0 +1,9 @@ +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..7aaefcd --- /dev/null +++ b/src/models/rating/beatmap_rating/query/by_id.rs @@ -0,0 +1,17 @@ +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..aa02331 --- /dev/null +++ b/src/models/rating/beatmap_rating/query/insert.rs @@ -0,0 +1,5 @@ +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..4ee94be --- /dev/null +++ b/src/models/rating/beatmap_rating/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/rating/beatmap_rating/types.rs b/src/models/rating/beatmap_rating/types.rs new file mode 100644 index 0000000..751d2c2 --- /dev/null +++ b/src/models/rating/beatmap_rating/types.rs @@ -0,0 +1,34 @@ +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. + #[validate(range(min = 0.01, message = "Rating must be positive"))] + pub rating: f64, + + /// Type of rating system used. + /// Must be one of: 'osu', 'etterna', 'quaver', 'malody', 'interlude'. + #[validate(custom = "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..c4e85ef --- /dev/null +++ b/src/models/rating/mod.rs @@ -0,0 +1,10 @@ +pub mod beatmap_mania_rating; +pub mod beatmap_rating; +pub mod score_mania_rating; +pub mod score_rating; + +// Re-exports for easy access +pub use beatmap_mania_rating::*; +pub use beatmap_rating::*; +pub use score_mania_rating::*; +pub use score_rating::*; \ No newline at end of file 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..98bcdcb --- /dev/null +++ b/src/models/rating/score_mania_rating/impl.rs @@ -0,0 +1,14 @@ +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..a7a96ed --- /dev/null +++ b/src/models/rating/score_mania_rating/mod.rs @@ -0,0 +1,9 @@ +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..d34f4c7 --- /dev/null +++ b/src/models/rating/score_mania_rating/query/by_id.rs @@ -0,0 +1,17 @@ +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..6b83a88 --- /dev/null +++ b/src/models/rating/score_mania_rating/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/rating/score_mania_rating/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + 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..00bafb4 --- /dev/null +++ b/src/models/rating/score_mania_rating/types.rs @@ -0,0 +1,53 @@ +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). + #[validate(range(min = 0.0, message = "Stream rating must be non-negative"))] + pub stream: Option, + + /// Jumpstream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Jumpstream rating must be non-negative"))] + pub jumpstream: Option, + + /// Handstream difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Handstream rating must be non-negative"))] + pub handstream: Option, + + /// Stamina difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Stamina rating must be non-negative"))] + pub stamina: Option, + + /// Jackspeed difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Jackspeed rating must be non-negative"))] + pub jackspeed: Option, + + /// Chordjack difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Chordjack rating must be non-negative"))] + pub chordjack: Option, + + /// Technical difficulty rating. + /// Must be a non-negative decimal value (≥ 0). + #[validate(range(min = 0.0, message = "Technical rating must be non-negative"))] + 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..4c91f23 --- /dev/null +++ b/src/models/rating/score_rating/impl.rs @@ -0,0 +1,14 @@ +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..87a0b9e --- /dev/null +++ b/src/models/rating/score_rating/mod.rs @@ -0,0 +1,9 @@ +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..103b6c6 100644 --- a/src/models/score_rating/query/by_id.rs +++ b/src/models/rating/score_rating/query/by_id.rs @@ -1,18 +1,17 @@ -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..936c13c --- /dev/null +++ b/src/models/rating/score_rating/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/rating/score_rating/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/rating/score_rating/types.rs b/src/models/rating/score_rating/types.rs new file mode 100644 index 0000000..f87dc5a --- /dev/null +++ b/src/models/rating/score_rating/types.rs @@ -0,0 +1,36 @@ +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. + #[validate(range(min = 0.01, message = "Rating must be positive"))] + pub rating: f64, + + /// Type of rating system used. + /// Must be one of: 'osu', 'etterna', 'quaver', 'malody', 'interlude'. + #[validate(custom = "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..bf917e6 100644 --- a/src/models/score/mod.rs +++ b/src/models/score/mod.rs @@ -1,9 +1,8 @@ -pub mod r#impl; -pub mod query; -pub mod types; -pub mod validators; +pub mod replay; +pub mod score; +pub mod score_metadata; -#[cfg(test)] -mod tests; - -pub use types::*; +// Re-exports for easy access +pub use replay::*; +pub use score::*; +pub use score_metadata::*; \ No newline at end of file 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..890af1e --- /dev/null +++ b/src/models/score/replay/impl.rs @@ -0,0 +1,14 @@ +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..0df70e3 --- /dev/null +++ b/src/models/score/replay/mod.rs @@ -0,0 +1,9 @@ +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 68% rename from src/models/replay/query/by_id.rs rename to src/models/score/replay/query/by_id.rs index fa1f5f7..7092571 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_data, created_at + FROM replay WHERE id = $1 "#, id @@ -14,3 +14,4 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, Sql .fetch_optional(pool) .await } + diff --git a/src/models/score/replay/query/insert.rs b/src/models/score/replay/query/insert.rs new file mode 100644 index 0000000..9c9dd87 --- /dev/null +++ b/src/models/score/replay/query/insert.rs @@ -0,0 +1,6 @@ +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_data); + diff --git a/src/models/score/replay/query/mod.rs b/src/models/score/replay/query/mod.rs new file mode 100644 index 0000000..4ee94be --- /dev/null +++ b/src/models/score/replay/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/score/replay/types.rs b/src/models/score/replay/types.rs new file mode 100644 index 0000000..07ecd64 --- /dev/null +++ b/src/models/score/replay/types.rs @@ -0,0 +1,19 @@ +use chrono::NaiveDateTime; +use validator::Validate; + +#[derive(Debug, Clone, sqlx::FromRow, Validate)] +pub struct ReplayRow { + /// Unique identifier for the replay record. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "ID must be positive"))] + pub id: i32, + + /// Replay data as binary content. + /// Must not be empty. + #[validate(length(min = 1, message = "Replay data cannot be empty"))] + pub replay_data: Vec, + + /// Timestamp when the replay was created. + 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..7b90f03 --- /dev/null +++ b/src/models/score/score/impl.rs @@ -0,0 +1,14 @@ +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..9a99b9f --- /dev/null +++ b/src/models/score/score/mod.rs @@ -0,0 +1,9 @@ +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..667bcac --- /dev/null +++ b/src/models/score/score/query/by_id.rs @@ -0,0 +1,17 @@ +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..abacf7b --- /dev/null +++ b/src/models/score/score/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/score/score/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/score/score/types.rs b/src/models/score/score/types.rs new file mode 100644 index 0000000..f887835 --- /dev/null +++ b/src/models/score/score/types.rs @@ -0,0 +1,66 @@ +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 = "validate_rank")] + pub rank: String, + + /// Status of the score. + /// Must be one of: 'pending', 'processing', 'validated', 'cheated', 'unsubmitted'. + #[validate(custom = "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..6e60263 --- /dev/null +++ b/src/models/score/score_metadata/impl.rs @@ -0,0 +1,14 @@ +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..f0f34b5 --- /dev/null +++ b/src/models/score/score_metadata/mod.rs @@ -0,0 +1,9 @@ +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..17ae926 --- /dev/null +++ b/src/models/score/score_metadata/query/by_id.rs @@ -0,0 +1,17 @@ +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, total_score, accuracy, max_combo, 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/score_metadata/query/insert.rs b/src/models/score/score_metadata/query/insert.rs new file mode 100644 index 0000000..43a5969 --- /dev/null +++ b/src/models/score/score_metadata/query/insert.rs @@ -0,0 +1,6 @@ +use crate::define_insert_returning_id; +use crate::models::score::score_metadata::types::ScoreMetadataRow; +// no extra imports needed + +define_insert_returning_id!(insert, "score_metadata", ScoreMetadataRow, total_score, accuracy, max_combo, count_300, count_100, count_50, count_miss, 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..4ee94be --- /dev/null +++ b/src/models/score/score_metadata/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/score/score_metadata/types.rs b/src/models/score/score_metadata/types.rs new file mode 100644 index 0000000..9cab56a --- /dev/null +++ b/src/models/score/score_metadata/types.rs @@ -0,0 +1,58 @@ +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, + + /// Total score achieved. + /// Must be a non-negative integer (≥ 0). + #[validate(range(min = 0, message = "Total score must be non-negative"))] + pub total_score: i64, + + /// Accuracy achieved (as a percentage). + /// Must be between 0.0 and 100.0. + #[validate(range(min = 0.0, max = 100.0, message = "Accuracy must be between 0.0 and 100.0"))] + pub accuracy: f64, + + /// 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, + + /// 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 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, + + /// 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, + + /// 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/mod.rs b/src/models/score_metadata/mod.rs deleted file mode 100644 index 9f775ac..0000000 --- a/src/models/score_metadata/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod r#impl; -pub mod query; -pub mod types; -pub(super) mod validators; - -#[cfg(test)] -mod tests; - -pub use types::*; 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/insert.rs b/src/models/score_metadata/query/insert.rs deleted file mode 100644 index 860ce6b..0000000 --- a/src/models/score_metadata/query/insert.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::define_insert_returning_id; -use crate::models::score_metadata::types::ScoreMetadataRow; - -define_insert_returning_id!( - insert, - "score_metadata", - ScoreMetadataRow, - 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 -); 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/query/mod.rs b/src/models/score_rating/query/mod.rs deleted file mode 100644 index 47b13ed..0000000 --- a/src/models/score_rating/query/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -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/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..0926de3 --- /dev/null +++ b/src/models/users/bans/impl.rs @@ -0,0 +1,14 @@ +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/users/bans/mod.rs b/src/models/users/bans/mod.rs new file mode 100644 index 0000000..949f066 --- /dev/null +++ b/src/models/users/bans/mod.rs @@ -0,0 +1,9 @@ +pub mod r#impl; +pub mod query; +pub mod types; + +#[cfg(test)] +mod tests; + +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..da7bb68 100644 --- a/src/models/msd/query/by_id.rs +++ b/src/models/users/bans/query/by_id.rs @@ -1,14 +1,17 @@ -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 ) .fetch_optional(pool) .await } + diff --git a/src/models/users/bans/query/insert.rs b/src/models/users/bans/query/insert.rs new file mode 100644 index 0000000..1e8957f --- /dev/null +++ b/src/models/users/bans/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/users/bans/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/users/bans/types.rs b/src/models/users/bans/types.rs new file mode 100644 index 0000000..28379e6 --- /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: i64, + + /// 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..44ab9eb --- /dev/null +++ b/src/models/users/device_tokens/impl.rs @@ -0,0 +1,15 @@ +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..72b0e6c --- /dev/null +++ b/src/models/users/device_tokens/mod.rs @@ -0,0 +1,9 @@ +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..012ca50 --- /dev/null +++ b/src/models/users/device_tokens/query/by_id.rs @@ -0,0 +1,18 @@ +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..7b9c0a3 --- /dev/null +++ b/src/models/users/device_tokens/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/users/device_tokens/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/users/device_tokens/types.rs b/src/models/users/device_tokens/types.rs new file mode 100644 index 0000000..6db53c1 --- /dev/null +++ b/src/models/users/device_tokens/types.rs @@ -0,0 +1,24 @@ +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: i64, + + /// 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..8d54eb5 --- /dev/null +++ b/src/models/users/mod.rs @@ -0,0 +1,10 @@ +pub mod bans; +pub mod device_tokens; +pub mod new_users; +pub mod users; + +// Re-exports for easy access +pub use bans::*; +pub use device_tokens::*; +pub use new_users::*; +pub use users::*; \ No newline at end of file diff --git a/src/models/users/new_users/impl.rs b/src/models/users/new_users/impl.rs new file mode 100644 index 0000000..bbcd17b --- /dev/null +++ b/src/models/users/new_users/impl.rs @@ -0,0 +1,19 @@ +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..840e3b1 --- /dev/null +++ b/src/models/users/new_users/mod.rs @@ -0,0 +1,9 @@ +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..5167aa3 --- /dev/null +++ b/src/models/users/new_users/query/by_id.rs @@ -0,0 +1,32 @@ +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..bb7b9b5 --- /dev/null +++ b/src/models/users/new_users/query/insert.rs @@ -0,0 +1,6 @@ +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..4ee94be --- /dev/null +++ b/src/models/users/new_users/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/users/new_users/types.rs b/src/models/users/new_users/types.rs new file mode 100644 index 0000000..f238f17 --- /dev/null +++ b/src/models/users/new_users/types.rs @@ -0,0 +1,21 @@ +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..dc5c2ef --- /dev/null +++ b/src/models/users/users/impl.rs @@ -0,0 +1,13 @@ +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 + } +} \ No newline at end of file diff --git a/src/models/replay/mod.rs b/src/models/users/users/mod.rs similarity index 75% rename from src/models/replay/mod.rs rename to src/models/users/users/mod.rs index 6fca91b..e9a8ef9 100644 --- a/src/models/replay/mod.rs +++ b/src/models/users/users/mod.rs @@ -5,4 +5,4 @@ pub mod types; #[cfg(test)] mod tests; -pub use types::*; +pub use types::UsersRow; \ No newline at end of file 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..caad51e --- /dev/null +++ b/src/models/users/users/query/by_id.rs @@ -0,0 +1,16 @@ +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 +} \ No newline at end of file diff --git a/src/models/users/users/query/insert.rs b/src/models/users/users/query/insert.rs new file mode 100644 index 0000000..3ca7ac0 --- /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); \ No newline at end of file diff --git a/src/models/users/users/query/mod.rs b/src/models/users/users/query/mod.rs new file mode 100644 index 0000000..e09092c --- /dev/null +++ b/src/models/users/users/query/mod.rs @@ -0,0 +1,2 @@ +pub mod by_id; +pub mod insert; \ No newline at end of file diff --git a/src/models/users/users/types.rs b/src/models/users/users/types.rs new file mode 100644 index 0000000..c0ee681 --- /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, +} \ No newline at end of file diff --git a/src/models/weekly/mod.rs b/src/models/weekly/mod.rs new file mode 100644 index 0000000..f0c9e6b --- /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::*; +pub use weekly_maps::*; +pub use weekly_participants::*; +pub use weekly_pool::*; +pub use weekly_scores::*; \ No newline at end of file diff --git a/src/models/weekly/weekly/impl.rs b/src/models/weekly/weekly/impl.rs new file mode 100644 index 0000000..dd05cf4 --- /dev/null +++ b/src/models/weekly/weekly/impl.rs @@ -0,0 +1,14 @@ +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..7034d29 --- /dev/null +++ b/src/models/weekly/weekly/mod.rs @@ -0,0 +1,9 @@ +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..5f5e2f2 --- /dev/null +++ b/src/models/weekly/weekly/query/by_id.rs @@ -0,0 +1,17 @@ +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, description, start_date, end_date, is_active, 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..c018266 --- /dev/null +++ b/src/models/weekly/weekly/query/insert.rs @@ -0,0 +1,6 @@ +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, description, start_date, end_date, is_active); + diff --git a/src/models/weekly/weekly/query/mod.rs b/src/models/weekly/weekly/query/mod.rs new file mode 100644 index 0000000..4ee94be --- /dev/null +++ b/src/models/weekly/weekly/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/weekly/weekly/types.rs b/src/models/weekly/weekly/types.rs new file mode 100644 index 0000000..48759d9 --- /dev/null +++ b/src/models/weekly/weekly/types.rs @@ -0,0 +1,32 @@ +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, + + /// Description of the weekly challenge. + /// Optional field, can be None. + pub description: Option, + + /// Start date of the weekly challenge. + pub start_date: NaiveDateTime, + + /// End date of the weekly challenge. + pub end_date: NaiveDateTime, + + /// Whether the weekly challenge is currently active. + pub is_active: bool, + + /// 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..da2f88a --- /dev/null +++ b/src/models/weekly/weekly_maps/impl.rs @@ -0,0 +1,14 @@ +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..e0c2d46 --- /dev/null +++ b/src/models/weekly/weekly_maps/mod.rs @@ -0,0 +1,9 @@ +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..df5de8e --- /dev/null +++ b/src/models/weekly/weekly_maps/query/by_id.rs @@ -0,0 +1,17 @@ +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, weekly_pool_id, beatmap_id, 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..7770a4a --- /dev/null +++ b/src/models/weekly/weekly_maps/query/insert.rs @@ -0,0 +1,6 @@ +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, weekly_pool_id, beatmap_id); + 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..4ee94be --- /dev/null +++ b/src/models/weekly/weekly_maps/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/weekly/weekly_maps/types.rs b/src/models/weekly/weekly_maps/types.rs new file mode 100644 index 0000000..3d6bc13 --- /dev/null +++ b/src/models/weekly/weekly_maps/types.rs @@ -0,0 +1,24 @@ +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 weekly pool this map belongs to. + /// Must be a positive integer (≥ 1). + #[validate(range(min = 1, message = "Weekly pool ID must be positive"))] + pub weekly_pool_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, + + /// 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..f3c9299 --- /dev/null +++ b/src/models/weekly/weekly_participants/impl.rs @@ -0,0 +1,14 @@ +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..375f658 --- /dev/null +++ b/src/models/weekly/weekly_participants/mod.rs @@ -0,0 +1,9 @@ +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..dd50e4d --- /dev/null +++ b/src/models/weekly/weekly_participants/query/by_id.rs @@ -0,0 +1,17 @@ +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, weekly_id, user_id, 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..ca7f2e4 --- /dev/null +++ b/src/models/weekly/weekly_participants/query/insert.rs @@ -0,0 +1,6 @@ +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, weekly_id, user_id); + 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..4ee94be --- /dev/null +++ b/src/models/weekly/weekly_participants/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/weekly/weekly_participants/types.rs b/src/models/weekly/weekly_participants/types.rs new file mode 100644 index 0000000..7c8f839 --- /dev/null +++ b/src/models/weekly/weekly_participants/types.rs @@ -0,0 +1,24 @@ +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, + + /// 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, + + /// 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, + + /// 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..487bc03 --- /dev/null +++ b/src/models/weekly/weekly_pool/impl.rs @@ -0,0 +1,14 @@ +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..8e1f4d8 --- /dev/null +++ b/src/models/weekly/weekly_pool/mod.rs @@ -0,0 +1,9 @@ +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..1973ebc --- /dev/null +++ b/src/models/weekly/weekly_pool/query/by_id.rs @@ -0,0 +1,17 @@ +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, weekly_id, name, description, 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..7a76417 --- /dev/null +++ b/src/models/weekly/weekly_pool/query/insert.rs @@ -0,0 +1,6 @@ +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, weekly_id, name, description); + 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..4ee94be --- /dev/null +++ b/src/models/weekly/weekly_pool/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/weekly/weekly_pool/types.rs b/src/models/weekly/weekly_pool/types.rs new file mode 100644 index 0000000..b21ff3d --- /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 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, + + /// Name of the pool. + /// 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, + + /// Description of the pool. + /// Optional field, can be None. + pub description: Option, + + /// Timestamp when the weekly pool 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..006305e --- /dev/null +++ b/src/models/weekly/weekly_scores/impl.rs @@ -0,0 +1,14 @@ +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..7f1d5b0 --- /dev/null +++ b/src/models/weekly/weekly_scores/mod.rs @@ -0,0 +1,9 @@ +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..9867b6b --- /dev/null +++ b/src/models/weekly/weekly_scores/query/by_id.rs @@ -0,0 +1,17 @@ +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, weekly_id, score_id, 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..d078310 --- /dev/null +++ b/src/models/weekly/weekly_scores/query/insert.rs @@ -0,0 +1,6 @@ +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, weekly_id, score_id); + 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..4ee94be --- /dev/null +++ b/src/models/weekly/weekly_scores/query/mod.rs @@ -0,0 +1,3 @@ +pub mod by_id; +pub mod insert; + diff --git a/src/models/weekly/weekly_scores/types.rs b/src/models/weekly/weekly_scores/types.rs new file mode 100644 index 0000000..af5cd5d --- /dev/null +++ b/src/models/weekly/weekly_scores/types.rs @@ -0,0 +1,24 @@ +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, + + /// 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, + + /// Timestamp when the weekly score was created. + pub created_at: Option, +} + From b9fc01357193ef9b0bd0828fec0549bee3ba0e98 Mon Sep 17 00:00:00 2001 From: Glubus Date: Thu, 18 Sep 2025 22:51:57 +0200 Subject: [PATCH 2/2] feat: remade the whole db lib to be on pair with migrations next is DTO --- Cargo.toml | 3 +- src/models/beatmap/beatmap/mod.rs | 2 +- src/models/beatmap/beatmap/query/by_id.rs | 1 - src/models/beatmap/beatmap/query/insert.rs | 21 ++++++++- src/models/beatmap/beatmap/query/mod.rs | 2 + src/models/beatmap/beatmap/tests/mod.rs | 1 + src/models/beatmap/beatmap/types.rs | 29 +++++------- src/models/beatmap/beatmap/validators.rs | 23 +++++++++ src/models/beatmap/beatmapset/impl.rs | 1 - src/models/beatmap/beatmapset/mod.rs | 2 +- src/models/beatmap/beatmapset/query/by_id.rs | 6 ++- src/models/beatmap/beatmapset/query/insert.rs | 22 ++++++++- src/models/beatmap/beatmapset/query/mod.rs | 2 + src/models/beatmap/beatmapset/tests/mod.rs | 1 + src/models/beatmap/beatmapset/types.rs | 3 +- src/models/beatmap/mod.rs | 6 --- src/models/beatmap/pending_beatmap/impl.rs | 3 +- src/models/beatmap/pending_beatmap/mod.rs | 1 - .../beatmap/pending_beatmap/query/by_id.rs | 5 +- .../beatmap/pending_beatmap/query/insert.rs | 9 +++- .../beatmap/pending_beatmap/query/mod.rs | 2 + .../beatmap/pending_beatmap/tests/mod.rs | 1 + src/models/beatmap/pending_beatmap/types.rs | 1 - src/models/beatmap/rates/mod.rs | 1 - src/models/beatmap/rates/query/by_id.rs | 1 - src/models/beatmap/rates/query/insert.rs | 5 +- src/models/beatmap/rates/query/mod.rs | 2 + src/models/beatmap/rates/tests/mod.rs | 1 + src/models/beatmap/rates/types.rs | 8 ++-- src/models/mod.rs | 2 +- src/models/other/failed_query/impl.rs | 1 - src/models/other/failed_query/mod.rs | 1 - src/models/other/failed_query/query/by_id.rs | 1 - src/models/other/failed_query/query/insert.rs | 1 - src/models/other/failed_query/query/mod.rs | 2 + src/models/other/failed_query/tests/mod.rs | 1 + src/models/other/failed_query/types.rs | 10 +++- .../rating/beatmap_mania_rating/impl.rs | 1 - src/models/rating/beatmap_mania_rating/mod.rs | 1 - .../beatmap_mania_rating/query/by_id.rs | 6 ++- .../beatmap_mania_rating/query/insert.rs | 15 +++++- .../rating/beatmap_mania_rating/query/mod.rs | 2 + .../rating/beatmap_mania_rating/tests/mod.rs | 1 + .../rating/beatmap_mania_rating/types.rs | 23 ++++----- src/models/rating/beatmap_rating/impl.rs | 1 - src/models/rating/beatmap_rating/mod.rs | 1 - .../rating/beatmap_rating/query/by_id.rs | 1 - .../rating/beatmap_rating/query/insert.rs | 9 +++- src/models/rating/beatmap_rating/query/mod.rs | 2 + src/models/rating/beatmap_rating/tests/mod.rs | 1 + src/models/rating/beatmap_rating/types.rs | 6 +-- src/models/rating/mod.rs | 6 --- src/models/rating/score_mania_rating/impl.rs | 1 - src/models/rating/score_mania_rating/mod.rs | 1 - .../rating/score_mania_rating/query/by_id.rs | 1 - .../rating/score_mania_rating/query/insert.rs | 15 +++++- .../rating/score_mania_rating/query/mod.rs | 2 + .../rating/score_mania_rating/tests/mod.rs | 1 + src/models/rating/score_mania_rating/types.rs | 23 ++++----- src/models/rating/score_rating/impl.rs | 1 - src/models/rating/score_rating/mod.rs | 1 - src/models/rating/score_rating/query/by_id.rs | 1 - .../rating/score_rating/query/insert.rs | 10 +++- src/models/rating/score_rating/query/mod.rs | 2 + src/models/rating/score_rating/tests/mod.rs | 1 + src/models/rating/score_rating/types.rs | 7 ++- src/models/score/mod.rs | 5 -- src/models/score/replay/impl.rs | 1 - src/models/score/replay/mod.rs | 1 - src/models/score/replay/query/by_id.rs | 3 +- src/models/score/replay/query/insert.rs | 10 +++- src/models/score/replay/query/mod.rs | 2 + src/models/score/replay/tests/mod.rs | 1 + src/models/score/replay/types.rs | 19 +++++--- src/models/score/score/impl.rs | 1 - src/models/score/score/mod.rs | 1 - src/models/score/score/query/by_id.rs | 1 - src/models/score/score/query/insert.rs | 15 +++++- src/models/score/score/query/mod.rs | 2 + src/models/score/score/tests/mod.rs | 1 + src/models/score/score/types.rs | 5 +- src/models/score/score_metadata/impl.rs | 1 - src/models/score/score_metadata/mod.rs | 1 - .../score/score_metadata/query/by_id.rs | 3 +- .../score/score_metadata/query/insert.rs | 22 ++++++++- src/models/score/score_metadata/query/mod.rs | 2 + src/models/score/score_metadata/tests/mod.rs | 1 + src/models/score/score_metadata/types.rs | 47 ++++++++++++++----- src/models/users/bans/impl.rs | 1 - src/models/users/bans/mod.rs | 1 - src/models/users/bans/query/by_id.rs | 1 - src/models/users/bans/query/insert.rs | 1 - src/models/users/bans/query/mod.rs | 2 + src/models/users/bans/tests/mod.rs | 1 + src/models/users/bans/types.rs | 2 +- src/models/users/device_tokens/impl.rs | 3 +- src/models/users/device_tokens/mod.rs | 1 - src/models/users/device_tokens/query/by_id.rs | 6 ++- .../users/device_tokens/query/insert.rs | 11 ++++- src/models/users/device_tokens/query/mod.rs | 2 + src/models/users/device_tokens/tests/mod.rs | 1 + src/models/users/device_tokens/types.rs | 3 +- src/models/users/mod.rs | 6 --- src/models/users/new_users/impl.rs | 8 ++-- src/models/users/new_users/mod.rs | 1 - src/models/users/new_users/query/by_id.rs | 6 ++- src/models/users/new_users/query/insert.rs | 10 +++- src/models/users/new_users/query/mod.rs | 2 + src/models/users/new_users/tests/mod.rs | 1 + src/models/users/new_users/types.rs | 1 - src/models/users/users/impl.rs | 9 ++-- src/models/users/users/mod.rs | 2 +- src/models/users/users/query/by_id.rs | 7 ++- src/models/users/users/query/insert.rs | 2 +- src/models/users/users/query/mod.rs | 5 +- src/models/users/users/tests/mod.rs | 1 + src/models/users/users/types.rs | 2 +- src/models/weekly/mod.rs | 10 ++-- src/models/weekly/weekly/impl.rs | 1 - src/models/weekly/weekly/mod.rs | 1 - src/models/weekly/weekly/query/by_id.rs | 3 +- src/models/weekly/weekly/query/insert.rs | 3 +- src/models/weekly/weekly/query/mod.rs | 2 + src/models/weekly/weekly/tests/mod.rs | 1 + src/models/weekly/weekly/types.rs | 22 ++++----- src/models/weekly/weekly_maps/impl.rs | 1 - src/models/weekly/weekly_maps/mod.rs | 1 - src/models/weekly/weekly_maps/query/by_id.rs | 3 +- src/models/weekly/weekly_maps/query/insert.rs | 10 +++- src/models/weekly/weekly_maps/query/mod.rs | 2 + src/models/weekly/weekly_maps/tests/mod.rs | 1 + src/models/weekly/weekly_maps/types.rs | 16 ++++--- src/models/weekly/weekly_participants/impl.rs | 1 - src/models/weekly/weekly_participants/mod.rs | 1 - .../weekly/weekly_participants/query/by_id.rs | 8 ++-- .../weekly_participants/query/insert.rs | 11 ++++- .../weekly/weekly_participants/query/mod.rs | 2 + .../weekly/weekly_participants/tests/mod.rs | 1 + .../weekly/weekly_participants/types.rs | 19 ++++++-- src/models/weekly/weekly_pool/impl.rs | 1 - src/models/weekly/weekly_pool/mod.rs | 1 - src/models/weekly/weekly_pool/query/by_id.rs | 3 +- src/models/weekly/weekly_pool/query/insert.rs | 10 +++- src/models/weekly/weekly_pool/query/mod.rs | 2 + src/models/weekly/weekly_pool/tests/mod.rs | 1 + src/models/weekly/weekly_pool/types.rs | 20 ++++---- src/models/weekly/weekly_scores/impl.rs | 1 - src/models/weekly/weekly_scores/mod.rs | 1 - .../weekly/weekly_scores/query/by_id.rs | 3 +- .../weekly/weekly_scores/query/insert.rs | 11 ++++- src/models/weekly/weekly_scores/query/mod.rs | 2 + src/models/weekly/weekly_scores/tests/mod.rs | 1 + src/models/weekly/weekly_scores/types.rs | 11 ++++- 153 files changed, 488 insertions(+), 271 deletions(-) create mode 100644 src/models/beatmap/beatmap/tests/mod.rs create mode 100644 src/models/beatmap/beatmap/validators.rs create mode 100644 src/models/beatmap/beatmapset/tests/mod.rs create mode 100644 src/models/beatmap/pending_beatmap/tests/mod.rs create mode 100644 src/models/beatmap/rates/tests/mod.rs create mode 100644 src/models/other/failed_query/tests/mod.rs create mode 100644 src/models/rating/beatmap_mania_rating/tests/mod.rs create mode 100644 src/models/rating/beatmap_rating/tests/mod.rs create mode 100644 src/models/rating/score_mania_rating/tests/mod.rs create mode 100644 src/models/rating/score_rating/tests/mod.rs create mode 100644 src/models/score/replay/tests/mod.rs create mode 100644 src/models/score/score/tests/mod.rs create mode 100644 src/models/score/score_metadata/tests/mod.rs create mode 100644 src/models/users/bans/tests/mod.rs create mode 100644 src/models/users/device_tokens/tests/mod.rs create mode 100644 src/models/users/new_users/tests/mod.rs create mode 100644 src/models/users/users/tests/mod.rs create mode 100644 src/models/weekly/weekly/tests/mod.rs create mode 100644 src/models/weekly/weekly_maps/tests/mod.rs create mode 100644 src/models/weekly/weekly_participants/tests/mod.rs create mode 100644 src/models/weekly/weekly_pool/tests/mod.rs create mode 100644 src/models/weekly/weekly_scores/tests/mod.rs 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/mod.rs b/src/models/beatmap/beatmap/mod.rs index ffbd47c..3e70365 100644 --- a/src/models/beatmap/beatmap/mod.rs +++ b/src/models/beatmap/beatmap/mod.rs @@ -1,9 +1,9 @@ pub mod r#impl; pub mod query; pub mod types; +pub mod validators; #[cfg(test)] mod tests; pub use types::BeatmapRow; - diff --git a/src/models/beatmap/beatmap/query/by_id.rs b/src/models/beatmap/beatmap/query/by_id.rs index 174c53b..fa66e38 100644 --- a/src/models/beatmap/beatmap/query/by_id.rs +++ b/src/models/beatmap/beatmap/query/by_id.rs @@ -28,4 +28,3 @@ pub async fn find_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, /// Reference to the beatmapset this beatmap belongs to. /// Optional field, can be None. @@ -53,23 +55,23 @@ pub struct BeatmapRow { /// Circle Size (CS) value. /// Must be between 0.0 and 10.0. - #[validate(range(min = 0.0, max = 10.0, message = "CS must be between 0.0 and 10.0"))] - pub cs: f64, + #[validate(custom(function = "validate_od_hp_cs"))] + pub cs: BigDecimal, /// Approach Rate (AR) value. /// Must be between 0.0 and 10.0. - #[validate(range(min = 0.0, max = 10.0, message = "AR must be between 0.0 and 10.0"))] - pub ar: f64, + #[validate(custom(function = "validate_ar"))] + pub ar: BigDecimal, /// Overall Difficulty (OD) value. /// Must be between 0.0 and 10.0. - #[validate(range(min = 0.0, max = 10.0, message = "OD must be between 0.0 and 10.0"))] - pub od: f64, + #[validate(custom(function = "validate_od_hp_cs"))] + pub od: BigDecimal, /// HP Drain (HP) value. /// Must be between 0.0 and 10.0. - #[validate(range(min = 0.0, max = 10.0, message = "HP must be between 0.0 and 10.0"))] - pub hp: f64, + #[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. @@ -78,7 +80,7 @@ pub struct BeatmapRow { /// Status of the beatmap. /// Must be one of: 'pending', 'ranked', 'qualified', 'loved', 'graveyard'. - #[validate(custom = "validate_status")] + #[validate(custom(function = "validate_status"))] pub status: String, /// Timestamp when the beatmap was created. @@ -87,10 +89,3 @@ pub struct BeatmapRow { /// Timestamp when the beatmap was last updated. pub updated_at: Option, } - -fn validate_status(status: &str) -> Result<(), validator::ValidationError> { - match status { - "pending" | "ranked" | "qualified" | "loved" | "graveyard" => Ok(()), - _ => Err(validator::ValidationError::new("invalid_status")), - } -} 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 index 940b476..3a51a76 100644 --- a/src/models/beatmap/beatmapset/impl.rs +++ b/src/models/beatmap/beatmapset/impl.rs @@ -15,4 +15,3 @@ impl BeatmapsetRow { find_by_osu_id(pool, osu_id).await } } - diff --git a/src/models/beatmap/beatmapset/mod.rs b/src/models/beatmap/beatmapset/mod.rs index 69e3992..9da66cc 100644 --- a/src/models/beatmap/beatmapset/mod.rs +++ b/src/models/beatmap/beatmapset/mod.rs @@ -5,5 +5,5 @@ pub mod types; #[cfg(test)] mod tests; +pub use query::*; pub use types::BeatmapsetRow; - diff --git a/src/models/beatmap/beatmapset/query/by_id.rs b/src/models/beatmap/beatmapset/query/by_id.rs index 82c798e..2ff8757 100644 --- a/src/models/beatmap/beatmapset/query/by_id.rs +++ b/src/models/beatmap/beatmapset/query/by_id.rs @@ -15,7 +15,10 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, .await } -pub async fn find_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, SqlxError> { +pub async fn find_by_osu_id( + pool: &PgPool, + osu_id: i32, +) -> Result, SqlxError> { sqlx::query_as!( BeatmapsetRow, r#" @@ -28,4 +31,3 @@ pub async fn find_by_osu_id(pool: &PgPool, osu_id: i32) -> Result, /// Artist name of the beatmapset. /// Must be between 1 and 255 characters. @@ -86,4 +86,3 @@ pub struct BeatmapsetRow { /// Timestamp when the beatmapset was last updated. pub updated_at: Option, } - diff --git a/src/models/beatmap/mod.rs b/src/models/beatmap/mod.rs index 8c274ab..593dbf6 100644 --- a/src/models/beatmap/mod.rs +++ b/src/models/beatmap/mod.rs @@ -2,9 +2,3 @@ pub mod beatmap; pub mod beatmapset; pub mod pending_beatmap; pub mod rates; - -// Re-exports for easy access -pub use beatmap::*; -pub use beatmapset::*; -pub use pending_beatmap::*; -pub use rates::*; \ No newline at end of file diff --git a/src/models/beatmap/pending_beatmap/impl.rs b/src/models/beatmap/pending_beatmap/impl.rs index d050390..5c66128 100644 --- a/src/models/beatmap/pending_beatmap/impl.rs +++ b/src/models/beatmap/pending_beatmap/impl.rs @@ -1,4 +1,4 @@ -use super::query::{find_by_id, find_by_hash, insert}; +use super::query::{find_by_hash, find_by_id, insert}; use super::PendingBeatmapRow; use sqlx::{Error as SqlxError, PgPool}; @@ -15,4 +15,3 @@ impl PendingBeatmapRow { find_by_hash(pool, osu_hash).await } } - diff --git a/src/models/beatmap/pending_beatmap/mod.rs b/src/models/beatmap/pending_beatmap/mod.rs index 7baee86..1aaaaae 100644 --- a/src/models/beatmap/pending_beatmap/mod.rs +++ b/src/models/beatmap/pending_beatmap/mod.rs @@ -6,4 +6,3 @@ pub mod types; mod tests; 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 index 68ed2cc..895b66e 100644 --- a/src/models/beatmap/pending_beatmap/query/by_id.rs +++ b/src/models/beatmap/pending_beatmap/query/by_id.rs @@ -15,7 +15,10 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result Result, SqlxError> { +pub async fn find_by_hash( + pool: &PgPool, + osu_hash: &str, +) -> Result, SqlxError> { sqlx::query_as!( PendingBeatmapRow, r#" diff --git a/src/models/beatmap/pending_beatmap/query/insert.rs b/src/models/beatmap/pending_beatmap/query/insert.rs index 1c23434..1a8c41d 100644 --- a/src/models/beatmap/pending_beatmap/query/insert.rs +++ b/src/models/beatmap/pending_beatmap/query/insert.rs @@ -2,5 +2,10 @@ 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); - +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 index 4ee94be..a9a3d33 100644 --- a/src/models/beatmap/pending_beatmap/query/mod.rs +++ b/src/models/beatmap/pending_beatmap/query/mod.rs @@ -1,3 +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/beatmap/pending_beatmap/types.rs b/src/models/beatmap/pending_beatmap/types.rs index 20d9ad5..09a0a96 100644 --- a/src/models/beatmap/pending_beatmap/types.rs +++ b/src/models/beatmap/pending_beatmap/types.rs @@ -30,4 +30,3 @@ pub struct PendingBeatmapRow { /// Timestamp when the pending beatmap was created. pub created_at: Option, } - diff --git a/src/models/beatmap/rates/mod.rs b/src/models/beatmap/rates/mod.rs index 4bc0a40..3ee9a78 100644 --- a/src/models/beatmap/rates/mod.rs +++ b/src/models/beatmap/rates/mod.rs @@ -6,4 +6,3 @@ pub mod types; mod tests; pub use types::RatesRow; - diff --git a/src/models/beatmap/rates/query/by_id.rs b/src/models/beatmap/rates/query/by_id.rs index 7033361..e327595 100644 --- a/src/models/beatmap/rates/query/by_id.rs +++ b/src/models/beatmap/rates/query/by_id.rs @@ -14,4 +14,3 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, Sqlx .fetch_optional(pool) .await } - diff --git a/src/models/beatmap/rates/query/insert.rs b/src/models/beatmap/rates/query/insert.rs index 723dc8d..b264a93 100644 --- a/src/models/beatmap/rates/query/insert.rs +++ b/src/models/beatmap/rates/query/insert.rs @@ -2,5 +2,6 @@ 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); - +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 index 4ee94be..a9a3d33 100644 --- a/src/models/beatmap/rates/query/mod.rs +++ b/src/models/beatmap/rates/query/mod.rs @@ -1,3 +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 index dabb517..e2e800f 100644 --- a/src/models/beatmap/rates/types.rs +++ b/src/models/beatmap/rates/types.rs @@ -1,6 +1,9 @@ +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. @@ -20,6 +23,7 @@ pub struct RatesRow { 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). @@ -39,10 +43,8 @@ pub struct RatesRow { /// Beats per minute of the beatmap. /// Must be a positive decimal value. - #[validate(range(min = 0.01, message = "BPM must be positive"))] - pub bpm: f64, + pub bpm: BigDecimal, /// Timestamp when the rate was created. pub created_at: Option, } - diff --git a/src/models/mod.rs b/src/models/mod.rs index 6a82e13..d2086ab 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -11,4 +11,4 @@ pub use other::*; pub use rating::*; pub use score::*; pub use users::*; -pub use weekly::*; \ No newline at end of file +pub use weekly::*; diff --git a/src/models/other/failed_query/impl.rs b/src/models/other/failed_query/impl.rs index c6db18f..83f19d5 100644 --- a/src/models/other/failed_query/impl.rs +++ b/src/models/other/failed_query/impl.rs @@ -11,4 +11,3 @@ impl FailedQueryRow { find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) } } - diff --git a/src/models/other/failed_query/mod.rs b/src/models/other/failed_query/mod.rs index ef5eccd..451b0e7 100644 --- a/src/models/other/failed_query/mod.rs +++ b/src/models/other/failed_query/mod.rs @@ -6,4 +6,3 @@ pub mod types; mod tests; pub use types::FailedQueryRow; - diff --git a/src/models/other/failed_query/query/by_id.rs b/src/models/other/failed_query/query/by_id.rs index 3f775dc..fa9677f 100644 --- a/src/models/other/failed_query/query/by_id.rs +++ b/src/models/other/failed_query/query/by_id.rs @@ -14,4 +14,3 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result .fetch_optional(pool) .await } - diff --git a/src/models/other/failed_query/query/insert.rs b/src/models/other/failed_query/query/insert.rs index 1d564c2..fb56abb 100644 --- a/src/models/other/failed_query/query/insert.rs +++ b/src/models/other/failed_query/query/insert.rs @@ -3,4 +3,3 @@ 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 index 4ee94be..a9a3d33 100644 --- a/src/models/other/failed_query/query/mod.rs +++ b/src/models/other/failed_query/query/mod.rs @@ -1,3 +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/other/failed_query/types.rs b/src/models/other/failed_query/types.rs index a512def..bea0261 100644 --- a/src/models/other/failed_query/types.rs +++ b/src/models/other/failed_query/types.rs @@ -1,6 +1,8 @@ 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. @@ -10,10 +12,14 @@ pub struct FailedQueryRow { /// 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(length( + min = 1, + max = 255, + message = "Hash must be between 1 and 255 characters" + ))] + #[validate(regex(path = *HASH_REGEX))] pub hash: String, /// Timestamp when the failed query was created. pub created_at: Option, } - diff --git a/src/models/rating/beatmap_mania_rating/impl.rs b/src/models/rating/beatmap_mania_rating/impl.rs index 80cea53..efded22 100644 --- a/src/models/rating/beatmap_mania_rating/impl.rs +++ b/src/models/rating/beatmap_mania_rating/impl.rs @@ -11,4 +11,3 @@ impl BeatmapManiaRatingRow { 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 index f706b20..9d05f5c 100644 --- a/src/models/rating/beatmap_mania_rating/mod.rs +++ b/src/models/rating/beatmap_mania_rating/mod.rs @@ -6,4 +6,3 @@ pub mod types; 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 index 37a2ae0..59166f1 100644 --- a/src/models/rating/beatmap_mania_rating/query/by_id.rs +++ b/src/models/rating/beatmap_mania_rating/query/by_id.rs @@ -1,7 +1,10 @@ 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> { +pub async fn find_by_id( + pool: &PgPool, + id: i32, +) -> Result, SqlxError> { sqlx::query_as!( BeatmapManiaRatingRow, r#" @@ -14,4 +17,3 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, + pub stream: Option, /// Jumpstream difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Jumpstream rating must be non-negative"))] - pub jumpstream: Option, + pub jumpstream: Option, /// Handstream difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Handstream rating must be non-negative"))] - pub handstream: Option, + pub handstream: Option, /// Stamina difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Stamina rating must be non-negative"))] - pub stamina: Option, + pub stamina: Option, /// Jackspeed difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Jackspeed rating must be non-negative"))] - pub jackspeed: Option, + pub jackspeed: Option, /// Chordjack difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Chordjack rating must be non-negative"))] - pub chordjack: Option, + pub chordjack: Option, /// Technical difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Technical rating must be non-negative"))] - pub technical: Option, + pub technical: Option, /// Timestamp when the mania rating was created. pub created_at: Option, @@ -53,4 +47,3 @@ pub struct BeatmapManiaRatingRow { /// 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 index 9497fc3..7625b75 100644 --- a/src/models/rating/beatmap_rating/impl.rs +++ b/src/models/rating/beatmap_rating/impl.rs @@ -11,4 +11,3 @@ impl BeatmapRatingRow { 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 index 148c423..2c3572f 100644 --- a/src/models/rating/beatmap_rating/mod.rs +++ b/src/models/rating/beatmap_rating/mod.rs @@ -6,4 +6,3 @@ pub mod types; 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 index 7aaefcd..148e444 100644 --- a/src/models/rating/beatmap_rating/query/by_id.rs +++ b/src/models/rating/beatmap_rating/query/by_id.rs @@ -14,4 +14,3 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result Result, + pub stream: Option, /// Jumpstream difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Jumpstream rating must be non-negative"))] - pub jumpstream: Option, + pub jumpstream: Option, /// Handstream difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Handstream rating must be non-negative"))] - pub handstream: Option, + pub handstream: Option, /// Stamina difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Stamina rating must be non-negative"))] - pub stamina: Option, + pub stamina: Option, /// Jackspeed difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Jackspeed rating must be non-negative"))] - pub jackspeed: Option, + pub jackspeed: Option, /// Chordjack difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Chordjack rating must be non-negative"))] - pub chordjack: Option, + pub chordjack: Option, /// Technical difficulty rating. /// Must be a non-negative decimal value (≥ 0). - #[validate(range(min = 0.0, message = "Technical rating must be non-negative"))] - pub technical: Option, + 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 index 4c91f23..58178b0 100644 --- a/src/models/rating/score_rating/impl.rs +++ b/src/models/rating/score_rating/impl.rs @@ -11,4 +11,3 @@ impl ScoreRatingRow { 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 index 87a0b9e..8f8bd0b 100644 --- a/src/models/rating/score_rating/mod.rs +++ b/src/models/rating/score_rating/mod.rs @@ -6,4 +6,3 @@ pub mod types; mod tests; pub use types::ScoreRatingRow; - diff --git a/src/models/rating/score_rating/query/by_id.rs b/src/models/rating/score_rating/query/by_id.rs index 103b6c6..c5af630 100644 --- a/src/models/rating/score_rating/query/by_id.rs +++ b/src/models/rating/score_rating/query/by_id.rs @@ -14,4 +14,3 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result .fetch_optional(pool) .await } - diff --git a/src/models/rating/score_rating/query/insert.rs b/src/models/rating/score_rating/query/insert.rs index 936c13c..a2f4d9e 100644 --- a/src/models/rating/score_rating/query/insert.rs +++ b/src/models/rating/score_rating/query/insert.rs @@ -2,5 +2,11 @@ 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); - +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 index 4ee94be..a9a3d33 100644 --- a/src/models/rating/score_rating/query/mod.rs +++ b/src/models/rating/score_rating/query/mod.rs @@ -1,3 +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 index f87dc5a..206d083 100644 --- a/src/models/rating/score_rating/types.rs +++ b/src/models/rating/score_rating/types.rs @@ -1,3 +1,4 @@ +use bigdecimal::BigDecimal; use chrono::NaiveDateTime; use validator::Validate; @@ -15,12 +16,11 @@ pub struct ScoreRatingRow { /// Rating value for the score. /// Must be a positive decimal value. - #[validate(range(min = 0.01, message = "Rating must be positive"))] - pub rating: f64, + pub rating: BigDecimal, /// Type of rating system used. /// Must be one of: 'osu', 'etterna', 'quaver', 'malody', 'interlude'. - #[validate(custom = "validate_rating_type")] + #[validate(custom(function = "validate_rating_type"))] pub rating_type: String, /// Timestamp when the score rating was created. @@ -33,4 +33,3 @@ fn validate_rating_type(rating_type: &str) -> Result<(), validator::ValidationEr _ => Err(validator::ValidationError::new("invalid_rating_type")), } } - diff --git a/src/models/score/mod.rs b/src/models/score/mod.rs index bf917e6..0b9c27f 100644 --- a/src/models/score/mod.rs +++ b/src/models/score/mod.rs @@ -1,8 +1,3 @@ pub mod replay; pub mod score; pub mod score_metadata; - -// Re-exports for easy access -pub use replay::*; -pub use score::*; -pub use score_metadata::*; \ No newline at end of file diff --git a/src/models/score/replay/impl.rs b/src/models/score/replay/impl.rs index 890af1e..d31bc1f 100644 --- a/src/models/score/replay/impl.rs +++ b/src/models/score/replay/impl.rs @@ -11,4 +11,3 @@ impl ReplayRow { 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 index 0df70e3..9690eff 100644 --- a/src/models/score/replay/mod.rs +++ b/src/models/score/replay/mod.rs @@ -6,4 +6,3 @@ pub mod types; mod tests; pub use types::ReplayRow; - diff --git a/src/models/score/replay/query/by_id.rs b/src/models/score/replay/query/by_id.rs index 7092571..e2be88f 100644 --- a/src/models/score/replay/query/by_id.rs +++ b/src/models/score/replay/query/by_id.rs @@ -5,7 +5,7 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, Sql sqlx::query_as!( ReplayRow, r#" - SELECT id, replay_data, created_at + SELECT id, replay_hash, replay_available, replay_path, created_at FROM replay WHERE id = $1 "#, @@ -14,4 +14,3 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, Sql .fetch_optional(pool) .await } - diff --git a/src/models/score/replay/query/insert.rs b/src/models/score/replay/query/insert.rs index 9c9dd87..9655e2f 100644 --- a/src/models/score/replay/query/insert.rs +++ b/src/models/score/replay/query/insert.rs @@ -2,5 +2,11 @@ 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_data); - +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 index 4ee94be..a9a3d33 100644 --- a/src/models/score/replay/query/mod.rs +++ b/src/models/score/replay/query/mod.rs @@ -1,3 +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 index 07ecd64..e2cd26a 100644 --- a/src/models/score/replay/types.rs +++ b/src/models/score/replay/types.rs @@ -1,19 +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. - /// Must be a positive integer (≥ 1). #[validate(range(min = 1, message = "ID must be positive"))] pub id: i32, - /// Replay data as binary content. - /// Must not be empty. - #[validate(length(min = 1, message = "Replay data cannot be empty"))] - pub replay_data: Vec, + #[validate(regex(path = *HASH_REGEX))] + pub replay_hash: String, + + /// Must be a boolean. + pub replay_available: bool, - /// Timestamp when the replay was created. + /// 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 index 7b90f03..56ed460 100644 --- a/src/models/score/score/impl.rs +++ b/src/models/score/score/impl.rs @@ -11,4 +11,3 @@ impl ScoreRow { 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 index 9a99b9f..cb2f9cc 100644 --- a/src/models/score/score/mod.rs +++ b/src/models/score/score/mod.rs @@ -6,4 +6,3 @@ pub mod types; 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 index 667bcac..e94775f 100644 --- a/src/models/score/score/query/by_id.rs +++ b/src/models/score/score/query/by_id.rs @@ -14,4 +14,3 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, Sqlx .fetch_optional(pool) .await } - diff --git a/src/models/score/score/query/insert.rs b/src/models/score/score/query/insert.rs index abacf7b..6f49bae 100644 --- a/src/models/score/score/query/insert.rs +++ b/src/models/score/score/query/insert.rs @@ -2,5 +2,16 @@ 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); - +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 index 4ee94be..a9a3d33 100644 --- a/src/models/score/score/query/mod.rs +++ b/src/models/score/score/query/mod.rs @@ -1,3 +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 index f887835..28adb36 100644 --- a/src/models/score/score/types.rs +++ b/src/models/score/score/types.rs @@ -38,12 +38,12 @@ pub struct ScoreRow { /// Rank achieved in the play. /// Must be one of: 'XH', 'X', 'SH', 'SS', 'S', 'A', 'B', 'C', 'D', 'E', 'F', 'G'. - #[validate(custom = "validate_rank")] + #[validate(custom(function = "validate_rank"))] pub rank: String, /// Status of the score. /// Must be one of: 'pending', 'processing', 'validated', 'cheated', 'unsubmitted'. - #[validate(custom = "validate_status")] + #[validate(custom(function = "validate_status"))] pub status: String, /// Timestamp when the score was created. @@ -63,4 +63,3 @@ fn validate_status(status: &str) -> Result<(), validator::ValidationError> { _ => Err(validator::ValidationError::new("invalid_status")), } } - diff --git a/src/models/score/score_metadata/impl.rs b/src/models/score/score_metadata/impl.rs index 6e60263..604b198 100644 --- a/src/models/score/score_metadata/impl.rs +++ b/src/models/score/score_metadata/impl.rs @@ -11,4 +11,3 @@ impl ScoreMetadataRow { 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 index f0f34b5..39a4608 100644 --- a/src/models/score/score_metadata/mod.rs +++ b/src/models/score/score_metadata/mod.rs @@ -6,4 +6,3 @@ pub mod types; 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 index 17ae926..0d8d4f2 100644 --- a/src/models/score/score_metadata/query/by_id.rs +++ b/src/models/score/score_metadata/query/by_id.rs @@ -5,7 +5,7 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result Result, + + /// 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 = "Total score must be non-negative"))] - pub total_score: i64, + #[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. - #[validate(range(min = 0.0, max = 100.0, message = "Accuracy must be between 0.0 and 100.0"))] - pub accuracy: f64, + 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"))] @@ -43,16 +68,16 @@ pub struct ScoreMetadataRow { #[validate(range(min = 0, message = "Count miss must be non-negative"))] pub count_miss: 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, - /// 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/users/bans/impl.rs b/src/models/users/bans/impl.rs index 0926de3..4a6eadb 100644 --- a/src/models/users/bans/impl.rs +++ b/src/models/users/bans/impl.rs @@ -11,4 +11,3 @@ impl BansRow { find_by_id(pool, id).await?.ok_or(SqlxError::RowNotFound) } } - diff --git a/src/models/users/bans/mod.rs b/src/models/users/bans/mod.rs index 949f066..0eafee9 100644 --- a/src/models/users/bans/mod.rs +++ b/src/models/users/bans/mod.rs @@ -6,4 +6,3 @@ pub mod types; mod tests; pub use types::BansRow; - diff --git a/src/models/users/bans/query/by_id.rs b/src/models/users/bans/query/by_id.rs index da7bb68..63343a8 100644 --- a/src/models/users/bans/query/by_id.rs +++ b/src/models/users/bans/query/by_id.rs @@ -14,4 +14,3 @@ pub async fn find_by_id(pool: &PgPool, id: i32) -> Result, SqlxE .fetch_optional(pool) .await } - diff --git a/src/models/users/bans/query/insert.rs b/src/models/users/bans/query/insert.rs index 1e8957f..5d5641b 100644 --- a/src/models/users/bans/query/insert.rs +++ b/src/models/users/bans/query/insert.rs @@ -3,4 +3,3 @@ 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 index 4ee94be..a9a3d33 100644 --- a/src/models/users/bans/query/mod.rs +++ b/src/models/users/bans/query/mod.rs @@ -1,3 +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 index 28379e6..9f573bc 100644 --- a/src/models/users/bans/types.rs +++ b/src/models/users/bans/types.rs @@ -11,7 +11,7 @@ pub struct BansRow { /// 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: i64, + pub discord_id: Option, /// Optional reason for the ban. pub reason: Option, diff --git a/src/models/users/device_tokens/impl.rs b/src/models/users/device_tokens/impl.rs index 44ab9eb..b35e7b7 100644 --- a/src/models/users/device_tokens/impl.rs +++ b/src/models/users/device_tokens/impl.rs @@ -4,7 +4,7 @@ use sqlx::{Error as SqlxError, PgPool}; use uuid::Uuid; impl DeviceTokensRow { - pub async fn insert(self, pool: &PgPool) -> Result { + pub async fn insert(self, pool: &PgPool) -> Result { insert(pool, self).await } @@ -12,4 +12,3 @@ impl DeviceTokensRow { find_by_token(pool, token).await } } - diff --git a/src/models/users/device_tokens/mod.rs b/src/models/users/device_tokens/mod.rs index 72b0e6c..d61e15b 100644 --- a/src/models/users/device_tokens/mod.rs +++ b/src/models/users/device_tokens/mod.rs @@ -6,4 +6,3 @@ pub mod types; 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 index 012ca50..e5a9a87 100644 --- a/src/models/users/device_tokens/query/by_id.rs +++ b/src/models/users/device_tokens/query/by_id.rs @@ -2,7 +2,10 @@ 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> { +pub async fn find_by_token( + pool: &PgPool, + token: Uuid, +) -> Result, SqlxError> { sqlx::query_as!( DeviceTokensRow, r#" @@ -15,4 +18,3 @@ pub async fn find_by_token(pool: &PgPool, token: Uuid) -> Result, /// Optional device name identifier. pub device_name: Option, @@ -21,4 +21,3 @@ pub struct DeviceTokensRow { /// 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 index 8d54eb5..c041ce4 100644 --- a/src/models/users/mod.rs +++ b/src/models/users/mod.rs @@ -2,9 +2,3 @@ pub mod bans; pub mod device_tokens; pub mod new_users; pub mod users; - -// Re-exports for easy access -pub use bans::*; -pub use device_tokens::*; -pub use new_users::*; -pub use users::*; \ No newline at end of file diff --git a/src/models/users/new_users/impl.rs b/src/models/users/new_users/impl.rs index bbcd17b..1528913 100644 --- a/src/models/users/new_users/impl.rs +++ b/src/models/users/new_users/impl.rs @@ -4,11 +4,14 @@ use sqlx::{Error as SqlxError, PgPool}; use uuid::Uuid; impl NewUsersRow { - pub async fn insert(self, pool: &PgPool) -> Result { + 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> { + pub async fn find_by_discord_id( + pool: &PgPool, + discord_id: i64, + ) -> Result, SqlxError> { find_by_discord_id(pool, discord_id).await } @@ -16,4 +19,3 @@ impl NewUsersRow { find_by_token(pool, token).await } } - diff --git a/src/models/users/new_users/mod.rs b/src/models/users/new_users/mod.rs index 840e3b1..9d34d6c 100644 --- a/src/models/users/new_users/mod.rs +++ b/src/models/users/new_users/mod.rs @@ -6,4 +6,3 @@ pub mod types; 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 index 5167aa3..6ebbf25 100644 --- a/src/models/users/new_users/query/by_id.rs +++ b/src/models/users/new_users/query/by_id.rs @@ -2,7 +2,10 @@ 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> { +pub async fn find_by_discord_id( + pool: &PgPool, + discord_id: i64, +) -> Result, SqlxError> { sqlx::query_as!( NewUsersRow, r#" @@ -29,4 +32,3 @@ pub async fn find_by_token(pool: &PgPool, token: Uuid) -> Result, } - diff --git a/src/models/users/users/impl.rs b/src/models/users/users/impl.rs index dc5c2ef..9bff74d 100644 --- a/src/models/users/users/impl.rs +++ b/src/models/users/users/impl.rs @@ -3,11 +3,14 @@ use super::UsersRow; use sqlx::{Error as SqlxError, PgPool}; impl UsersRow { - pub async fn insert(self, pool: &PgPool) -> Result { + 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> { + pub async fn find_by_discord_id( + pool: &PgPool, + discord_id: i64, + ) -> Result, SqlxError> { find_by_discord_id(pool, discord_id).await } -} \ No newline at end of file +} diff --git a/src/models/users/users/mod.rs b/src/models/users/users/mod.rs index e9a8ef9..b125175 100644 --- a/src/models/users/users/mod.rs +++ b/src/models/users/users/mod.rs @@ -5,4 +5,4 @@ pub mod types; #[cfg(test)] mod tests; -pub use types::UsersRow; \ No newline at end of file +pub use types::UsersRow; diff --git a/src/models/users/users/query/by_id.rs b/src/models/users/users/query/by_id.rs index caad51e..6b2566c 100644 --- a/src/models/users/users/query/by_id.rs +++ b/src/models/users/users/query/by_id.rs @@ -1,7 +1,10 @@ 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> { +pub async fn find_by_discord_id( + pool: &PgPool, + discord_id: i64, +) -> Result, SqlxError> { sqlx::query_as!( UsersRow, r#" @@ -13,4 +16,4 @@ pub async fn find_by_discord_id(pool: &PgPool, discord_id: i64) -> Result