mahjong_service/
db_storage.rs

1use crate::{
2    auth::{AuthInfo, AuthInfoData, GetAuthInfo},
3    common::Storage,
4    db_storage::models::{
5        DieselAuthInfo, DieselAuthInfoEmail, DieselAuthInfoGithub, DieselGame, DieselGamePlayer,
6        DieselGameScore, DieselPlayer,
7    },
8    env::{ENV_PG_URL, ENV_REDIS_URL},
9};
10use async_trait::async_trait;
11use diesel::pg::PgConnection;
12use diesel::prelude::*;
13use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
14use mahjong_core::{GameId, PlayerId, Players};
15use redis::Commands;
16use service_contracts::{ServiceGame, ServicePlayer, ServicePlayerGame};
17use tracing::debug;
18
19pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
20
21use self::{
22    models::{
23        DieselAuthInfoAnonymous, DieselGameBoard, DieselGameDrawWall, DieselGameHand,
24        DieselGameSettings,
25    },
26    models_translation::DieselGameExtra,
27};
28
29mod models;
30mod models_translation;
31mod schema;
32
33pub struct DBStorage {
34    db_path: String,
35    redis_path: String,
36}
37
38#[async_trait]
39impl Storage for DBStorage {
40    async fn get_auth_info(&self, get_auth_info: GetAuthInfo) -> Result<Option<AuthInfo>, String> {
41        let mut connection = PgConnection::establish(&self.db_path).unwrap();
42
43        match get_auth_info {
44            GetAuthInfo::EmailUsername(username) => {
45                DieselAuthInfoEmail::get_info_by_username(&mut connection, &username)
46            }
47            GetAuthInfo::GithubUsername(username) => {
48                DieselAuthInfoGithub::get_info_by_username(&mut connection, &username)
49            }
50            GetAuthInfo::PlayerId(player_id) => {
51                DieselAuthInfo::get_info_by_id(&mut connection, &player_id)
52            }
53            GetAuthInfo::AnonymousToken(id_token) => {
54                DieselAuthInfoAnonymous::get_info_by_hashed_token(&mut connection, &id_token)
55            }
56        }
57    }
58
59    async fn get_player_total_score(&self, player_id: &PlayerId) -> Result<i32, String> {
60        let mut connection = PgConnection::establish(&self.db_path).unwrap();
61
62        let total_score = DieselGameScore::read_total_from_player(&mut connection, player_id);
63
64        Ok(total_score)
65    }
66
67    async fn save_auth_info(&self, auth_info: &AuthInfo) -> Result<(), String> {
68        use schema::auth_info::table;
69        use schema::auth_info_anonymous::table as anonymous_table;
70        use schema::auth_info_email::table as email_table;
71        use schema::auth_info_github::table as github_table;
72
73        let mut connection = PgConnection::establish(&self.db_path).unwrap();
74        let diesel_auth_info = DieselAuthInfo::from_raw(auth_info);
75
76        diesel::insert_into(table)
77            .values(&diesel_auth_info)
78            .execute(&mut connection)
79            .unwrap();
80
81        match auth_info.data {
82            AuthInfoData::Email(ref email) => {
83                let diesel_auth_info_email = DieselAuthInfoEmail::from_raw(email);
84
85                diesel::insert_into(email_table)
86                    .values(&diesel_auth_info_email)
87                    .execute(&mut connection)
88                    .unwrap();
89            }
90            AuthInfoData::Github(ref github) => {
91                let diesel_auth_info_github = DieselAuthInfoGithub::from_raw(github);
92
93                diesel::insert_into(github_table)
94                    .values(&diesel_auth_info_github)
95                    .execute(&mut connection)
96                    .unwrap();
97            }
98            AuthInfoData::Anonymous(ref anonymous) => {
99                let diesel_auth_info_anonymous = DieselAuthInfoAnonymous::from_raw(anonymous);
100
101                diesel::insert_into(anonymous_table)
102                    .values(&diesel_auth_info_anonymous)
103                    .execute(&mut connection)
104                    .unwrap();
105            }
106        }
107
108        Ok(())
109    }
110
111    async fn save_game(&self, service_game: &ServiceGame) -> Result<(), String> {
112        let mut connection = PgConnection::establish(&self.db_path).unwrap();
113        let redis_client = redis::Client::open(self.redis_path.clone()).unwrap();
114        let mut redis_connection = redis_client.get_connection().unwrap();
115
116        let game_str = serde_json::to_string(&service_game).unwrap();
117        let redis_key = format!("game:{}", service_game.game.id);
118
119        let _: () = redis_connection.set(redis_key.clone(), game_str).unwrap();
120        let _: () = redis_connection.expire(redis_key, 60 * 60).unwrap();
121
122        DieselPlayer::update_from_game(&mut connection, service_game);
123
124        let diesel_game_extra = DieselGameExtra {
125            created_at: chrono::DateTime::from_timestamp_millis(service_game.created_at)
126                .unwrap()
127                .naive_utc(),
128            game: service_game.game.clone(),
129            updated_at: chrono::DateTime::from_timestamp_millis(service_game.updated_at)
130                .unwrap()
131                .naive_utc(),
132        };
133
134        let diesel_game = DieselGame::from_raw(&diesel_game_extra);
135
136        diesel_game.update(&mut connection);
137
138        let diesel_game_players = DieselGamePlayer::from_game(&service_game.game);
139        DieselGamePlayer::update(&mut connection, &diesel_game_players, &service_game.game);
140
141        DieselGameScore::update_from_game(&mut connection, service_game);
142        DieselGameBoard::update_from_game(&mut connection, service_game);
143        DieselGameDrawWall::update_from_game(&mut connection, service_game);
144        DieselGameHand::update_from_game(&mut connection, service_game);
145        DieselGameSettings::update_from_game(&mut connection, service_game);
146
147        Ok(())
148    }
149
150    async fn get_game(&self, id: &GameId, use_cache: bool) -> Result<Option<ServiceGame>, String> {
151        let redis_client = redis::Client::open(self.redis_path.clone()).unwrap();
152        let mut redis_connection = redis_client.get_connection().unwrap();
153
154        let redis_key = format!("game:{}", id);
155
156        if use_cache {
157            let game_str: Option<String> = redis_connection.get(redis_key).unwrap();
158
159            if let Some(game_str) = game_str {
160                let game: ServiceGame = serde_json::from_str(&game_str).unwrap();
161
162                return Ok(Some(game));
163            }
164        } else {
165            redis_connection.del::<String, bool>(redis_key).unwrap();
166        }
167
168        let mut connection = PgConnection::establish(&self.db_path).unwrap();
169
170        let result = DieselGame::read_from_id(&mut connection, id);
171
172        if result.is_none() {
173            return Ok(None);
174        }
175
176        let game_players = DieselGamePlayer::read_from_game(&mut connection, id);
177        let players = DieselPlayer::read_from_ids(&mut connection, &game_players);
178
179        let score = DieselGameScore::read_from_game(&mut connection, id);
180        let board = DieselGameBoard::read_from_game(&mut connection, id);
181        let draw_wall = DieselGameDrawWall::read_from_game(&mut connection, id);
182        let (hands, bonus_tiles) = DieselGameHand::read_from_game(&mut connection, id);
183        let settings = DieselGameSettings::read_from_game(&mut connection, id);
184
185        if settings.is_none() {
186            return Ok(None);
187        }
188
189        let game_extra = result.unwrap();
190        let mut game = game_extra.game;
191        game.players = Players(game_players);
192        game.score = score;
193        game.table.hands = hands;
194        game.table.bonus_tiles = bonus_tiles;
195        game.table.board = board;
196        game.table.draw_wall = draw_wall;
197
198        let service_game = ServiceGame {
199            created_at: game_extra.created_at.and_utc().timestamp_millis(),
200            game,
201            players,
202            settings: settings.unwrap(),
203            updated_at: game_extra.updated_at.and_utc().timestamp_millis(),
204        };
205
206        Ok(Some(service_game))
207    }
208
209    async fn get_player_games(
210        &self,
211        player_id: &Option<PlayerId>,
212    ) -> Result<Vec<ServicePlayerGame>, String> {
213        let mut connection = PgConnection::establish(&self.db_path).unwrap();
214
215        if player_id.is_some() {
216            let result =
217                DieselGamePlayer::read_from_player(&mut connection, &player_id.clone().unwrap());
218
219            return Ok(result);
220        }
221
222        let all = DieselGame::read_player_games(&mut connection);
223
224        Ok(all)
225    }
226
227    async fn get_player(&self, player_id: &PlayerId) -> Result<Option<ServicePlayer>, String> {
228        let mut connection = PgConnection::establish(&self.db_path).unwrap();
229
230        let player = DieselPlayer::read_from_id(&mut connection, player_id);
231
232        Ok(player)
233    }
234
235    async fn save_player(&self, player: &ServicePlayer) -> Result<(), String> {
236        let mut connection = PgConnection::establish(&self.db_path).unwrap();
237
238        DieselPlayer::save(&mut connection, player);
239
240        Ok(())
241    }
242
243    async fn delete_games(&self, ids: &[GameId]) -> Result<(), String> {
244        let mut connection = PgConnection::establish(&self.db_path).unwrap();
245
246        DieselGamePlayer::delete_games(&mut connection, ids);
247        DieselGameScore::delete_games(&mut connection, ids);
248        DieselGameBoard::delete_games(&mut connection, ids);
249        DieselGameDrawWall::delete_games(&mut connection, ids);
250        DieselGameHand::delete_games(&mut connection, ids);
251        DieselGameSettings::delete_games(&mut connection, ids);
252        DieselGame::delete_games(&mut connection, ids);
253
254        Ok(())
255    }
256}
257
258impl DBStorage {
259    #[allow(dead_code)]
260    pub fn new_dyn() -> Box<dyn Storage> {
261        let db_path = std::env::var(ENV_PG_URL)
262            .unwrap_or("postgres://postgres:postgres@localhost/mahjong".to_string());
263        let redis_path = std::env::var(ENV_REDIS_URL).unwrap_or("redis://localhost".to_string());
264
265        debug!("DBStorage: {} {}", db_path, redis_path);
266
267        let file_storage = Self {
268            db_path: db_path.clone(),
269            redis_path,
270        };
271
272        let mut connection = PgConnection::establish(&db_path).unwrap();
273
274        connection.run_pending_migrations(MIGRATIONS).unwrap();
275
276        Box::new(file_storage)
277    }
278}