tower_sessions_firestore_store/
lib.rs1use 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#[derive(thiserror::Error, Debug)]
16pub enum FirestoreStoreError {
17 #[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() .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}