ft_sdk/session/
mod.rs

1mod session_data;
2
3pub use ft_sys_shared::TRACKER_KEY;
4pub use session_data::SessionData;
5
6#[derive(Clone, Debug)]
7pub struct SessionID(pub String);
8
9impl SessionID {
10    /// Create a new session entry with the given user ID.
11    /// If the user ID is None, the session will be created without a user ID.
12    pub fn create(
13        conn: &mut ft_sdk::Connection,
14        user_id: Option<ft_sdk::auth::UserId>,
15        data: Option<serde_json::Value>,
16    ) -> Result<Self, ft_sdk::Error> {
17        use diesel::prelude::*;
18        use ft_sdk::schema::fastn_session;
19
20        let session_id = ft_sdk::utils::uuid_v8();
21
22        let data = match data {
23            Some(d) => serde_json::to_string(&d)?,
24            None => "{}".to_string(),
25        };
26
27        let user_id = user_id.map(|u| u.0);
28
29        diesel::insert_into(fastn_session::table)
30            .values((
31                fastn_session::id.eq(&session_id),
32                fastn_session::uid.eq(user_id),
33                fastn_session::created_at.eq(ft_sdk::env::now()),
34                fastn_session::updated_at.eq(ft_sdk::env::now()),
35                fastn_session::data.eq(data),
36            ))
37            .execute(conn)?;
38
39        Ok(Self(session_id))
40    }
41
42    pub fn from_string<S: AsRef<str>>(s: S) -> Self {
43        Self(s.as_ref().to_string())
44    }
45
46    /// Set the user ID for the given session.
47    pub fn set_user_id(
48        &self,
49        conn: &mut ft_sdk::Connection,
50        user_id: ft_sdk::auth::UserId,
51    ) -> Result<SessionID, diesel::result::Error> {
52        use diesel::prelude::*;
53        use ft_sdk::schema::fastn_session;
54
55        let affected =
56            diesel::update(fastn_session::table.filter(fastn_session::id.eq(self.0.as_str())))
57                // None means that the field will not be updated
58                .set(fastn_session::uid.eq(Some(user_id.0)))
59                .execute(conn)?;
60
61        assert_eq!(
62            affected, 1,
63            r#"Expected to update exactly one session. SessionID is unique so
64there can't be more than one. Zero is not possible if the SessionID can only be constructed
65using `SessionID::new`"#
66        );
67
68        Ok(self.clone())
69    }
70
71    /// Get the session data object.
72    /// Useful for fetching the entire session data in a single db call. Use
73    /// [get_key](SessionID::get_key) instead if you only need a single key
74    pub fn data(
75        &self,
76        conn: &mut ft_sdk::Connection,
77    ) -> Result<ft_sdk::SessionData, diesel::result::Error> {
78        use diesel::prelude::*;
79        use ft_sdk::schema::fastn_session;
80
81        let data = fastn_session::table
82            .select(fastn_session::data)
83            .filter(fastn_session::id.eq(self.0.as_str()))
84            .first::<String>(conn)?;
85
86        let data: std::collections::HashMap<String, serde_json::Value> =
87            serde_json::from_str(&data).expect("session data must be serializable json object");
88
89        Ok(SessionData::new(self.0.as_str(), data))
90    }
91
92    /// Directly store a key-value in the session store. This will overwrite the existing value for
93    /// the given key if it exists.
94    /// This is useful for storing a single key-value pair without fetching the entire session data
95    pub fn set_key<S: AsRef<str>, V: serde::Serialize>(
96        &self,
97        conn: &mut ft_sdk::Connection,
98        k: S,
99        v: V,
100    ) -> Result<SessionID, SetKeyError> {
101        use diesel::prelude::*;
102        use diesel::sql_types::Text;
103        use ft_sdk::schema::fastn_session;
104
105        let value = serde_json::to_string(&v).map_err(SetKeyError::SerdeError)?;
106
107        // json_set(data, '.<key>', json('<value>'))
108        let sql_set = diesel::dsl::sql::<Text>("json_set(data, ")
109            .bind::<Text, _>(format!("$.{}", k.as_ref()))
110            .sql(", json(")
111            .bind::<Text, _>(value)
112            .sql("))");
113
114        diesel::update(fastn_session::table.filter(fastn_session::id.eq(self.0.as_str())))
115            .set(fastn_session::data.eq(sql_set))
116            .execute(conn)
117            .map_err(SetKeyError::DatabaseError)?;
118
119        Ok(self.clone())
120    }
121}
122
123#[derive(thiserror::Error, Debug)]
124pub enum GetKeyError {
125    #[error("key `{0}` not found in session data")]
126    KeyNotFound(String),
127    #[error("failed to query db: {0:?}")]
128    DatabaseError(diesel::result::Error),
129    #[error("failed to deserialize session data: {0:?}")]
130    SerdeError(serde_json::Error),
131}
132
133#[derive(thiserror::Error, Debug)]
134pub enum SetKeyError {
135    #[error("db error: {0:?}")]
136    DatabaseError(diesel::result::Error),
137    #[error("failed to serialize value: {0:?}")]
138    SerdeError(serde_json::Error),
139}