tower_sessions_firestore_store/
lib.rs

1use std::{collections::HashMap, str::FromStr, sync::Arc};
2
3use async_trait::async_trait;
4use chrono::{DateTime, TimeZone, Utc};
5use firestore::FirestoreDb;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use time::OffsetDateTime;
9use tower_sessions_core::{
10    session::{Id, Record},
11    session_store, SessionStore,
12};
13
14/// An error type for `FirestoreStore`.
15#[derive(thiserror::Error, Debug)]
16pub enum FirestoreStoreError {
17    /// A variant to map to `firestore::errors::FirestoreError` errors.
18    #[error(transparent)]
19    Firestore(#[from] firestore::errors::FirestoreError),
20}
21
22impl From<FirestoreStoreError> for session_store::Error {
23    fn from(err: FirestoreStoreError) -> Self {
24        match err {
25            FirestoreStoreError::Firestore(inner) => {
26                session_store::Error::Backend(inner.to_string())
27            }
28        }
29    }
30}
31
32#[derive(Debug, Clone)]
33pub struct FirestoreStore {
34    pub db: Arc<FirestoreDb>,
35    pub collection_id: String,
36}
37
38impl FirestoreStore {
39    pub fn new(db: FirestoreDb, collection_id: String) -> Self {
40        Self {
41            db: Arc::new(db),
42            collection_id,
43        }
44    }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48struct FirestoreRecord {
49    pub id: String,
50    pub data: HashMap<String, Value>,
51    pub expiry_date: OffsetDateTime,
52}
53
54impl From<Record> for FirestoreRecord {
55    fn from(record: Record) -> Self {
56        Self {
57            id: record.id.to_string(),
58            data: record.data,
59            expiry_date: record.expiry_date,
60        }
61    }
62}
63
64impl From<FirestoreRecord> for Record {
65    fn from(record: FirestoreRecord) -> Self {
66        Self {
67            id: Id::from_str(&record.id).unwrap_or_default(),
68            data: record.data,
69            expiry_date: record.expiry_date,
70        }
71    }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75struct FirestoreDocument {
76    record: FirestoreRecord,
77    #[serde(
78        rename = "expireAt",
79        with = "firestore::serialize_as_optional_timestamp"
80    )]
81    expire_at: Option<DateTime<Utc>>,
82}
83
84impl From<Record> for FirestoreDocument {
85    fn from(record: Record) -> Self {
86        let expire_at = match Utc.timestamp_opt(
87            record.expiry_date.unix_timestamp(),
88            record.expiry_date.nanosecond(),
89        ) {
90            chrono::offset::LocalResult::Single(expire_at) => Some(expire_at),
91            _ => None,
92        };
93        Self {
94            record: record.into(),
95            expire_at,
96        }
97    }
98}
99
100#[async_trait]
101impl SessionStore for FirestoreStore {
102    async fn save(&self, record: &Record) -> session_store::Result<()> {
103        let doc = FirestoreDocument::from(record.clone());
104        self.db
105            .fluent()
106            .update() // Update will create the document if it doesn't exist
107            .in_col(self.collection_id.as_ref())
108            .document_id(&record.id.to_string())
109            .object(&doc)
110            .execute()
111            .await
112            .map_err(FirestoreStoreError::Firestore)?;
113        Ok(())
114    }
115
116    async fn load(&self, session_id: &Id) -> session_store::Result<Option<Record>> {
117        let doc: Option<FirestoreDocument> = self
118            .db
119            .fluent()
120            .select()
121            .by_id_in(self.collection_id.as_ref())
122            .obj()
123            .one(session_id.to_string())
124            .await
125            .map_err(FirestoreStoreError::Firestore)?;
126
127        if let Some(doc) = doc {
128            Ok(Some(doc.record.into()))
129        } else {
130            Ok(None)
131        }
132    }
133
134    async fn delete(&self, session_id: &Id) -> session_store::Result<()> {
135        self.db
136            .fluent()
137            .delete()
138            .from(self.collection_id.as_ref())
139            .document_id(session_id.to_string())
140            .execute()
141            .await
142            .map_err(FirestoreStoreError::Firestore)?;
143        Ok(())
144    }
145}