Skip to main content

tablo_core/auth/
columns.rs

1//! Each signed-in user's choice of the toggleable columns a resource's list shows: the
2//! [`ColumnPreference`] rows.
3
4use topcoat::context::Cx;
5
6use super::signed;
7use crate::{table::ColumnChoice, topcoat_compat::href};
8
9/// Shipped record of the toggleable columns one user shows or hides in one resource's list, on
10/// one panel.
11///
12/// The panel writes a row when the user applies a choice that departs from the columns' defaults,
13/// and deletes it when the user resets to them. Register it with the app's models when a table
14/// declares a [`toggleable`](crate::TextColumn::toggleable) or
15/// [`hidden_by_default`](crate::TextColumn::hidden_by_default) column and auth is on.
16#[derive(Debug, Clone, toasty::Model)]
17pub struct ColumnPreference {
18    /// The panel, the user and the resource, query-encoded: one row per user per list.
19    #[key]
20    pub key: String,
21    /// [`PanelUser::user_id`](crate::PanelUser::user_id) of the user who chose; indexed so an app
22    /// deleting a user can delete their rows, which the panel leaves behind.
23    #[index]
24    pub user_id: String,
25    /// The prefix of the panel the list is on (e.g. `/admin`).
26    pub panel: String,
27    /// The slug of the resource whose list it is.
28    pub resource: String,
29    /// The columns the user hides or shows against their defaults, query-encoded. Text, since a
30    /// few names outgrow the `VARCHAR(191)` MySQL gives a `String`.
31    #[column(type = text)]
32    pub columns: String,
33}
34
35/// The row key of `user_id`'s choice for the resource at `slug` on the panel at `panel`.
36fn preference_key(panel: &str, user_id: &str, slug: &str) -> String {
37    href::encode_query(&[
38        ("panel".to_string(), panel),
39        ("user".to_string(), user_id),
40        ("resource".to_string(), slug),
41    ])
42}
43
44/// The request's user's choice for the resource at `slug`: the defaults without a signed-in
45/// user, and when the database cannot answer, since a list renders without the choice.
46pub(crate) async fn load_choice(cx: &Cx, slug: &str) -> ColumnChoice {
47    let Some(signed) = signed(cx) else {
48        return ColumnChoice::default();
49    };
50    let key = preference_key(&signed.realm.prefix, &signed.user.user_id(), slug);
51    let Ok(mut db) = crate::db::db(cx) else {
52        return ColumnChoice::default();
53    };
54    match ColumnPreference::filter(ColumnPreference::fields().key().eq(key))
55        .first()
56        .exec(&mut db)
57        .await
58    {
59        Ok(row) => row.map_or_else(ColumnChoice::default, |row| {
60            ColumnChoice::decode(&row.columns)
61        }),
62        Err(error) => {
63            tracing::warn!(resource = slug, error = %error, "column preference load failed");
64            ColumnChoice::default()
65        }
66    }
67}
68
69/// Stores `choice` as the request's user's for the resource at `slug`, replacing any earlier one;
70/// a choice departing from no default leaves no row.
71///
72/// # Errors
73///
74/// Forbidden without a signed-in user, and the infrastructure failure when the database cannot
75/// answer.
76pub(crate) async fn save_choice(cx: &Cx, slug: &str, choice: &ColumnChoice) -> topcoat::Result<()> {
77    let Some(signed) = signed(cx) else {
78        return Err(topcoat::router::error::forbidden().into());
79    };
80    let user_id = signed.user.user_id();
81    let panel = signed.realm.prefix.clone();
82    let key = preference_key(&panel, &user_id, slug);
83    let mut db = crate::db::db(cx)?;
84    let mut tx = db.transaction().await.map_err(crate::error::unavailable)?;
85    ColumnPreference::filter(ColumnPreference::fields().key().eq(key.clone()))
86        .delete()
87        .exec(&mut tx)
88        .await
89        .map_err(crate::error::unavailable)?;
90    if !choice.is_default() {
91        toasty::create!(ColumnPreference {
92            key,
93            user_id,
94            panel,
95            resource: slug.to_string(),
96            columns: choice.encode(),
97        })
98        .exec(&mut tx)
99        .await
100        .map_err(crate::error::unavailable)?;
101    }
102    tx.commit().await.map_err(crate::error::unavailable)
103}