Skip to main content

cot/session/store/
db.rs

1//! Database-backed session store.
2//!
3//! This module provides a session store implementation that persists session
4//! records in a database using the Cot ORM. The database connection is
5//! typically set via the [`cot::config::DatabaseConfig`] in the project
6//! configuration and then passed to the `DbStore` constructor.
7//!
8//! # Examples
9//!
10//! ```
11//! use std::sync::Arc;
12//!
13//! use cot::db::Database;
14//! use cot::session::store::db::DbStore;
15//!
16//! # #[tokio::main]
17//! # async fn main() -> cot::Result<()> {
18//! let db = Database::new("sqlite://:memory:").await?;
19//! let store = DbStore::new(db);
20//! # Ok(())
21//! # }
22//! ```
23
24use std::collections::HashMap;
25use std::error::Error;
26
27use async_trait::async_trait;
28use thiserror::Error;
29use tower_sessions::session::{Id, Record};
30use tower_sessions::{SessionStore, session_store};
31
32use crate::db::{Auto, Database, DatabaseError, Model, query};
33use crate::session::db::Session;
34use crate::session::store::{ERROR_PREFIX, MAX_COLLISION_RETRIES};
35use crate::utils::chrono::DateTimeWithOffsetAdapter;
36
37/// Errors that can occur while interacting with the database session store.
38#[derive(Debug, Error)]
39#[non_exhaustive]
40pub enum DbStoreError {
41    /// An error occurred while interacting with the database.
42    #[error("{ERROR_PREFIX} {0} ")]
43    DatabaseError(#[from] DatabaseError),
44    /// The record ID collided too many times while saving in the database.
45    #[error("{ERROR_PREFIX} session‐id collision retried too many times ({0})")]
46    TooManyIdCollisions(u32),
47    /// An error occurred during JSON serialization.
48    #[error("{ERROR_PREFIX} JSON serialization error: {0}")]
49    Serialize(Box<dyn Error + Send + Sync>),
50    /// An error occurred during JSON deserialization.
51    #[error("{ERROR_PREFIX} JSON serialization error: {0}")]
52    Deserialize(Box<dyn Error + Send + Sync>),
53}
54
55impl From<DbStoreError> for session_store::Error {
56    fn from(err: DbStoreError) -> Self {
57        match err {
58            DbStoreError::DatabaseError(db_err) => {
59                session_store::Error::Backend(db_err.to_string())
60            }
61            DbStoreError::Serialize(ser_err) => session_store::Error::Encode(ser_err.to_string()),
62            DbStoreError::Deserialize(de_err) => session_store::Error::Decode(de_err.to_string()),
63            other => session_store::Error::Backend(other.to_string()),
64        }
65    }
66}
67
68/// A database-backed session store.
69///
70/// This store uses a database to persist session records, allowing for
71/// session data to be stored across application restarts.
72///
73/// # Examples
74///
75/// ```
76/// use std::sync::Arc;
77///
78/// use cot::db::Database;
79/// use cot::session::store::db::DbStore;
80///
81/// # #[tokio::main]
82/// # async fn main() -> Result<(), cot::session::store::db::DbStoreError> {
83/// let db = Database::new("sqlite://:memory:").await?;
84/// let store = DbStore::new(db);
85/// # Ok(())
86/// # }
87/// ```
88#[derive(Clone, Debug)]
89pub struct DbStore {
90    connection: Database,
91}
92
93impl DbStore {
94    /// Creates a new `DbStore` instance with the provided database connection.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// use std::sync::Arc;
100    ///
101    /// use cot::db::Database;
102    /// use cot::session::store::db::DbStore;
103    ///
104    /// # #[tokio::main]
105    /// # async fn main() -> Result<(), cot::session::store::db::DbStoreError> {
106    /// let db = Database::new("sqlite://:memory:").await?;
107    /// let store = DbStore::new(db);
108    /// # Ok(())
109    /// # }
110    /// ```
111    #[must_use]
112    pub fn new(connection: Database) -> DbStore {
113        DbStore { connection }
114    }
115}
116
117#[async_trait]
118impl SessionStore for DbStore {
119    async fn create(&self, record: &mut Record) -> session_store::Result<()> {
120        for _ in 0..=MAX_COLLISION_RETRIES {
121            let key = record.id.to_string();
122
123            let data = serde_json::to_string(&record.data).unwrap();
124            let expiry = DateTimeWithOffsetAdapter::try_from(record.expiry_date)
125                .expect("Failed to convert expiry date to a valid datetime")
126                .into_chrono_db_safe();
127
128            let mut model = Session {
129                id: Auto::auto(),
130                key,
131                data,
132                expiry,
133            };
134
135            let res = self.connection.insert(&mut model).await;
136            match res {
137                Ok(()) => {
138                    return Ok(());
139                }
140                Err(DatabaseError::UniqueViolation) => {
141                    // If a unique constraint violation occurs, we need to generate a new ID
142                    record.id = Id::default();
143                }
144                Err(err) => return Err(DbStoreError::DatabaseError(err))?,
145            }
146        }
147        Err(DbStoreError::TooManyIdCollisions(MAX_COLLISION_RETRIES))?
148    }
149
150    async fn save(&self, record: &Record) -> session_store::Result<()> {
151        // TODO: use transactions when implemented
152        let key = record.id.to_string();
153        let data = serde_json::to_string(&record.data)
154            .map_err(|err| DbStoreError::Serialize(Box::new(err)))?;
155
156        let query = query!(Session, $key == key)
157            .get(&self.connection)
158            .await
159            .map_err(DbStoreError::DatabaseError)?;
160        if let Some(mut model) = query {
161            model.data = data;
162            model
163                .update(&self.connection)
164                .await
165                .map_err(DbStoreError::DatabaseError)?;
166        } else {
167            let mut record = record.clone();
168            self.create(&mut record).await?;
169        }
170        Ok(())
171    }
172
173    async fn load(&self, session_id: &Id) -> session_store::Result<Option<Record>> {
174        let key = session_id.to_string();
175        let query = query!(Session, $key == key)
176            .get(&self.connection)
177            .await
178            .map_err(DbStoreError::DatabaseError)?;
179        if let Some(session) = query {
180            let data = serde_json::from_str::<HashMap<String, serde_json::Value>>(&session.data)
181                .map_err(|err| DbStoreError::Serialize(Box::new(err)))?;
182
183            let id = session
184                .key
185                .parse::<Id>()
186                .map_err(|err| DbStoreError::Deserialize(Box::new(err)))?;
187
188            let expiry_date = DateTimeWithOffsetAdapter::new(session.expiry).into_offsetdatetime();
189
190            let rec = Record {
191                id,
192                data,
193                expiry_date,
194            };
195
196            Ok(Some(rec))
197        } else {
198            Ok(None)
199        }
200    }
201
202    async fn delete(&self, session_id: &Id) -> session_store::Result<()> {
203        let key = session_id.to_string();
204        query!(Session, $key == key)
205            .delete(&self.connection)
206            .await
207            .map_err(DbStoreError::DatabaseError)?;
208        Ok(())
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use std::io;
215
216    use tower_sessions::session::Record;
217
218    use super::*;
219    use crate::db::DatabaseError;
220
221    #[cot::test]
222    async fn test_from_db_store_error_to_session_store_error() {
223        let sqlx_err = sqlx::Error::Protocol("protocol error".into());
224        let db_err = DatabaseError::DatabaseEngineError(sqlx_err);
225        let sess_err: session_store::Error = DbStoreError::DatabaseError(db_err).into();
226        assert!(matches!(sess_err, session_store::Error::Backend(_)));
227
228        let io_err = io::Error::other("oops");
229        let serialize_err: session_store::Error = DbStoreError::Serialize(Box::new(io_err)).into();
230
231        assert!(matches!(serialize_err, session_store::Error::Encode(_)));
232
233        let parse_err = serde_json::from_str::<Record>("not a json").unwrap_err();
234        let deserialize_err: session_store::Error =
235            DbStoreError::Deserialize(Box::new(parse_err)).into();
236        assert!(matches!(deserialize_err, session_store::Error::Decode(_)));
237
238        let sess_err: session_store::Error = DbStoreError::TooManyIdCollisions(99).into();
239        assert!(matches!(sess_err, session_store::Error::Backend(_)));
240    }
241}