1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
mod session_data;

pub use ft_sys_shared::TRACKER_KEY;
pub use session_data::SessionData;

#[derive(Clone, Debug)]
pub struct SessionID(pub String);

impl SessionID {
    /// Create a new session entry with the given user ID.
    /// If the user ID is None, the session will be created without a user ID.
    pub fn create(
        conn: &mut ft_sdk::Connection,
        user_id: Option<ft_sdk::auth::UserId>,
        data: Option<serde_json::Value>,
    ) -> Result<Self, ft_sdk::Error> {
        use diesel::prelude::*;
        use ft_sdk::schema::fastn_session;

        let session_id = ft_sdk::utils::uuid_v8();

        let data = match data {
            Some(d) => serde_json::to_string(&d)?,
            None => "{}".to_string(),
        };

        let user_id = user_id.map(|u| u.0);

        diesel::insert_into(fastn_session::table)
            .values((
                fastn_session::id.eq(&session_id),
                fastn_session::uid.eq(user_id),
                fastn_session::created_at.eq(ft_sdk::env::now()),
                fastn_session::updated_at.eq(ft_sdk::env::now()),
                fastn_session::data.eq(data),
            ))
            .execute(conn)?;

        Ok(Self(session_id))
    }

    pub fn from_string<S: AsRef<str>>(s: S) -> Self {
        Self(s.as_ref().to_string())
    }

    /// Set the user ID for the given session.
    pub fn set_user_id(
        &self,
        conn: &mut ft_sdk::Connection,
        user_id: ft_sdk::auth::UserId,
    ) -> Result<SessionID, diesel::result::Error> {
        use diesel::prelude::*;
        use ft_sdk::schema::fastn_session;

        let affected =
            diesel::update(fastn_session::table.filter(fastn_session::id.eq(self.0.as_str())))
                // None means that the field will not be updated
                .set(fastn_session::uid.eq(Some(user_id.0)))
                .execute(conn)?;

        assert_eq!(
            affected, 1,
            r#"Expected to update exactly one session. SessionID is unique so
there can't be more than one. Zero is not possible if the SessionID can only be constructed
using `SessionID::new`"#
        );

        Ok(self.clone())
    }

    /// Get the session data object.
    /// Useful for fetching the entire session data in a single db call. Use
    /// [get_key](SessionID::get_key) instead if you only need a single key
    pub fn data(
        &self,
        conn: &mut ft_sdk::Connection,
    ) -> Result<ft_sdk::SessionData, diesel::result::Error> {
        use diesel::prelude::*;
        use ft_sdk::schema::fastn_session;

        let data = fastn_session::table
            .select(fastn_session::data)
            .filter(fastn_session::id.eq(self.0.as_str()))
            .first::<String>(conn)?;

        let data: std::collections::HashMap<String, serde_json::Value> =
            serde_json::from_str(&data).expect("session data must be serializable json object");

        Ok(SessionData::new(self.0.as_str(), data))
    }

    /// Directly store a key-value in the session store. This will overwrite the existing value for
    /// the given key if it exists.
    /// This is useful for storing a single key-value pair without fetching the entire session data
    pub fn set_key<S: AsRef<str>, V: serde::Serialize>(
        &self,
        conn: &mut ft_sdk::Connection,
        k: S,
        v: V,
    ) -> Result<SessionID, SetKeyError> {
        use diesel::prelude::*;
        use diesel::sql_types::Text;
        use ft_sdk::schema::fastn_session;

        let value = serde_json::to_string(&v).map_err(SetKeyError::SerdeError)?;

        // json_set(data, '.<key>', json('<value>'))
        let sql_set = diesel::dsl::sql::<Text>("json_set(data, ")
            .bind::<Text, _>(format!("$.{}", k.as_ref()))
            .sql(", json(")
            .bind::<Text, _>(value)
            .sql("))");

        diesel::update(fastn_session::table.filter(fastn_session::id.eq(self.0.as_str())))
            .set(fastn_session::data.eq(sql_set))
            .execute(conn)
            .map_err(SetKeyError::DatabaseError)?;

        Ok(self.clone())
    }
}

#[derive(thiserror::Error, Debug)]
pub enum GetKeyError {
    #[error("key `{0}` not found in session data")]
    KeyNotFound(String),
    #[error("failed to query db: {0:?}")]
    DatabaseError(diesel::result::Error),
    #[error("failed to deserialize session data: {0:?}")]
    SerdeError(serde_json::Error),
}

#[derive(thiserror::Error, Debug)]
pub enum SetKeyError {
    #[error("db error: {0:?}")]
    DatabaseError(diesel::result::Error),
    #[error("failed to serialize value: {0:?}")]
    SerdeError(serde_json::Error),
}