Skip to main content

hypercall_db_diesel/
usernames.rs

1//! UsernameReader + UsernameWriter trait implementations for DieselDb.
2//!
3//! Manages username CRUD with case-insensitive uniqueness enforcement.
4//! Redis caching and validation stay in the service layer.
5
6use anyhow::Result;
7use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use diesel::prelude::*;
10use diesel_async::{AsyncConnection, RunQueryDsl};
11
12use crate::diesel_db::DieselDb;
13use crate::schema::usernames;
14use hypercall_db::{UsernameReader, UsernameRecord, UsernameWriteError, UsernameWriter};
15
16// -- Diesel helpers --
17
18diesel::define_sql_function! {
19    /// Postgres `LOWER(text) -> text`.
20    fn lower(x: diesel::sql_types::Text) -> diesel::sql_types::Text;
21}
22
23/// Diesel model matching the `usernames` table.
24#[derive(Debug, Clone, Queryable, Selectable, Insertable, AsChangeset)]
25#[diesel(table_name = usernames)]
26#[diesel(check_for_backend(diesel::pg::Pg))]
27struct UsernameRow {
28    pub wallet_address: String,
29    pub username: String,
30    pub created_at: DateTime<Utc>,
31    pub updated_at: DateTime<Utc>,
32}
33
34impl From<UsernameRow> for UsernameRecord {
35    fn from(row: UsernameRow) -> Self {
36        Self {
37            wallet_address: row.wallet_address,
38            username: row.username,
39            created_at: row.created_at,
40            updated_at: row.updated_at,
41        }
42    }
43}
44
45// =============================================================================
46// UsernameReader
47// =============================================================================
48
49#[async_trait]
50impl UsernameReader for DieselDb {
51    async fn get_username_by_wallet(&self, wallet: &str) -> Result<Option<UsernameRecord>> {
52        let mut conn = self.get_conn().await?;
53        let wallet_lower = wallet.to_lowercase();
54
55        let row = usernames::table
56            .filter(lower(usernames::wallet_address).eq(&wallet_lower))
57            .first::<UsernameRow>(&mut conn)
58            .await
59            .optional()?;
60
61        Ok(row.map(Into::into))
62    }
63
64    async fn get_username_by_name(&self, username: &str) -> Result<Option<UsernameRecord>> {
65        let mut conn = self.get_conn().await?;
66        let name_lower = username.to_lowercase();
67
68        let row = usernames::table
69            .filter(lower(usernames::username).eq(&name_lower))
70            .first::<UsernameRow>(&mut conn)
71            .await
72            .optional()?;
73
74        Ok(row.map(Into::into))
75    }
76}
77
78// =============================================================================
79// UsernameWriter
80// =============================================================================
81
82#[async_trait]
83impl UsernameWriter for DieselDb {
84    async fn set_username(
85        &self,
86        wallet: &str,
87        username: &str,
88        now: DateTime<Utc>,
89    ) -> std::result::Result<(UsernameRecord, Option<String>), UsernameWriteError> {
90        let mut conn = self
91            .get_conn()
92            .await
93            .map_err(UsernameWriteError::Internal)?;
94        let wallet_lower = wallet.to_lowercase();
95        let wallet_owned = wallet.to_string();
96        let username_owned = username.to_string();
97
98        let (row, old_username) = conn
99            .transaction::<(UsernameRow, Option<String>), diesel::result::Error, _>(async |conn| {
100                // Capture the previous username inside the transaction
101                let prev = usernames::table
102                    .filter(lower(usernames::wallet_address).eq(&wallet_lower))
103                    .select(usernames::username)
104                    .first::<String>(&mut *conn)
105                    .await
106                    .optional()?;
107
108                // Delete existing row for this wallet (if any)
109                diesel::delete(
110                    usernames::table.filter(lower(usernames::wallet_address).eq(&wallet_lower)),
111                )
112                .execute(&mut *conn)
113                .await?;
114
115                // Insert the new row
116                let new_row = UsernameRow {
117                    wallet_address: wallet_owned.clone(),
118                    username: username_owned.clone(),
119                    created_at: now,
120                    updated_at: now,
121                };
122
123                let inserted = diesel::insert_into(usernames::table)
124                    .values(&new_row)
125                    .get_result::<UsernameRow>(&mut *conn)
126                    .await?;
127
128                Ok((inserted, prev))
129            })
130            .await
131            .map_err(|e| {
132                if let diesel::result::Error::DatabaseError(
133                    diesel::result::DatabaseErrorKind::UniqueViolation,
134                    _,
135                ) = &e
136                {
137                    return UsernameWriteError::UniqueViolation;
138                }
139                UsernameWriteError::Internal(
140                    anyhow::Error::from(e).context("Failed to upsert username"),
141                )
142            })?;
143
144        Ok((row.into(), old_username))
145    }
146
147    async fn delete_username(&self, wallet: &str) -> Result<Option<UsernameRecord>> {
148        let mut conn = self.get_conn().await?;
149        let wallet_lower = wallet.to_lowercase();
150
151        let deleted_row: Option<UsernameRow> = conn
152            .transaction::<Option<UsernameRow>, diesel::result::Error, _>(async |conn| {
153                let existing = usernames::table
154                    .filter(lower(usernames::wallet_address).eq(&wallet_lower))
155                    .first::<UsernameRow>(&mut *conn)
156                    .await
157                    .optional()?;
158
159                if existing.is_some() {
160                    diesel::delete(
161                        usernames::table.filter(lower(usernames::wallet_address).eq(&wallet_lower)),
162                    )
163                    .execute(&mut *conn)
164                    .await?;
165                }
166
167                Ok(existing)
168            })
169            .await?;
170
171        Ok(deleted_row.map(Into::into))
172    }
173}