Packages

Session management library for Wisp. Manage user sessions in a simple key-value store.

Current section

Files

Jump to
kv_sessions src kv_sessions postgres_adapter.gleam
Raw

src/kv_sessions/postgres_adapter.gleam

import gleam/io
import gleam/json
import gleam/list
import gleam/option
import gleam/result
import kv_sessions/session
import kv_sessions/session_config
import pog
import wisp
pub fn new(db: pog.Connection) {
session_config.SessionStore(
get_session: get_session(db),
save_session: save_session(db),
delete_session: delete_session(db),
)
}
pub fn migrate_up(db: pog.Connection) {
let sql =
"
CREATE TABLE IF NOT EXISTS kv_sessions (
session_id VARCHAR PRIMARY KEY,
expires_at TIMESTAMP NOT NULL,
data JSON
);
"
pog.query(sql)
|> pog.execute(db)
}
pub fn migrate_down(db: pog.Connection) {
let sql = "DROP TABLE IF EXISTS kv_sessions"
pog.query(sql)
|> pog.execute(db)
}
fn get_session(db: pog.Connection) {
fn(session_id: session.SessionId) {
let sql =
"
SELECT session_id, expires_at, data from kv_sessions
WHERE session_id = $1;
"
use returned <- result.try(
pog.query(sql)
|> pog.parameter(pog.text(session.id_to_string(session_id)))
|> pog.returning(decode_session_row)
|> pog.execute(db)
|> result.map_error(fn(err) {
io.debug(err)
wisp.log_error(
"Could not get session" <> session.id_to_string(session_id),
)
session.DbErrorGetError(
"Could not get session with id"
<> session.id_to_string(session_id)
<> " from database",
)
}),
)
returned.rows
|> list.first
|> option.from_result
|> option.map(fn(row) {
use data <- result.map(
decode_data_from_string(row.2)
|> result.map_error(fn(err) {
io.debug(err)
session.DeserializeError("Could not deserialize data")
}),
)
session.Session(
id: session.id_from_string(row.0),
expires_at: row.1,
data: data,
)
})
|> fn(r) {
case r {
option.Some(Ok(v)) -> Ok(option.Some(v))
option.Some(Error(e)) -> Error(e)
option.None -> Ok(option.None)
}
}
}
}
fn save_session(db: pog.Connection) {
fn(new_session: session.Session) {
let sql =
"
INSERT INTO kv_sessions (session_id, expires_at, data)
VALUES ($1, $2, $3)
ON CONFLICT (session_id)
DO UPDATE SET
session_id = EXCLUDED.session_id,
data = EXCLUDED.data,
expires_at = EXCLUDED.expires_at;
"
// io.debug(birl.to_erlang_datetime(new_session.expires_at))
let #(#(year, month, day), #(hour, minute, seconds)) =
new_session.expires_at
let insert =
pog.query(sql)
|> pog.parameter(pog.text(session.id_to_string(new_session.id)))
|> pog.parameter(
pog.timestamp(pog.Timestamp(
pog.Date(year, month, day),
pog.Time(hour, minute, seconds, 0),
)),
)
|> pog.parameter(pog.text(json.to_string(encode_data(new_session.data))))
|> pog.execute(db)
// let insert =
// pog.execute(
// sql,
// db,
// [
// pog.text(session.id_to_string(new_session.id)),
// pog.timestamp(new_session.expires_at),
// pog.text(json.to_string(internal.encode_data(new_session.data))),
// ],
// fn(_) { Ok(Nil) },
// )
case insert {
Ok(_) -> Ok(new_session)
Error(err) -> {
io.debug(err)
wisp.log_error("Could not insert new session")
Error(session.DbErrorInsertError("Could not insert new session"))
}
}
}
}
fn delete_session(db: pog.Connection) {
fn(session_id: session.SessionId) {
let sql =
"
DELETE FROM kv_sessions
WHERE session_id = $1
"
case
pog.query(sql)
|> pog.parameter(pog.text(session.id_to_string(session_id)))
|> pog.execute(db)
{
Ok(_) -> Ok(Nil)
Error(err) -> {
io.debug(err)
wisp.log_error(
"Could not delete session with id" <> session.id_to_string(session_id),
)
Error(session.DbErrorDeleteError(
"Could not delete session with id" <> session.id_to_string(session_id),
))
}
}
}
}
// Internal
//---------------
import gleam/dict
import gleam/dynamic
// Encode/Decode rows
//--------------------
@internal
pub fn decode_session_row(data: dynamic.Dynamic) {
data
|> dynamic.from
|> dynamic.tuple3(
dynamic.string,
dynamic.tuple2(
dynamic.tuple3(dynamic.int, dynamic.int, dynamic.int),
dynamic.tuple3(dynamic.int, dynamic.int, dynamic.int),
),
dynamic.string,
)
}
@internal
pub fn encode_data(data: dict.Dict(String, String)) {
data
|> dict.fold([], fn(acc, key, val) {
acc |> list.append([#(key, json.string(val))])
})
|> json.object
}
fn decode_data(data: dynamic.Dynamic) {
dynamic.from(data) |> dynamic.dict(dynamic.string, dynamic.string)
}
@internal
pub fn decode_data_from_string(str: String) {
json.decode(from: str, using: decode_data)
}