Skip to main content

systemprompt_users/repository/user/
find.rs

1//! User lookup queries.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use systemprompt_identifiers::UserId;
7
8use crate::error::Result;
9use crate::models::{User, UserRole, UserRow, UserStatus};
10use crate::repository::UserRepository;
11
12impl UserRepository {
13    pub async fn find_by_id(&self, id: &UserId) -> Result<Option<User>> {
14        let deleted_status = UserStatus::Deleted.as_str();
15        let row = sqlx::query_as!(
16            UserRow,
17            r#"
18            SELECT id, name, email, full_name, display_name, status, email_verified,
19                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
20            FROM users
21            WHERE id = $1 AND status != $2
22            "#,
23            id.as_str(),
24            deleted_status
25        )
26        .fetch_optional(&*self.pool)
27        .await?
28        .map(User::try_from)
29        .transpose()?;
30
31        Ok(row)
32    }
33
34    pub async fn find_by_email(&self, email: &str) -> Result<Option<User>> {
35        let email = crate::models::normalise_email(email);
36        let deleted_status = UserStatus::Deleted.as_str();
37        let row = sqlx::query_as!(
38            UserRow,
39            r#"
40            SELECT id, name, email, full_name, display_name, status, email_verified,
41                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
42            FROM users
43            WHERE email = $1 AND status != $2
44            "#,
45            email,
46            deleted_status
47        )
48        .fetch_optional(&*self.pool)
49        .await?
50        .map(User::try_from)
51        .transpose()?;
52
53        Ok(row)
54    }
55
56    pub async fn find_by_name(&self, name: &str) -> Result<Option<User>> {
57        let deleted_status = UserStatus::Deleted.as_str();
58        let row = sqlx::query_as!(
59            UserRow,
60            r#"
61            SELECT id, name, email, full_name, display_name, status, email_verified,
62                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
63            FROM users
64            WHERE name = $1 AND status != $2
65            ORDER BY created_at ASC
66            LIMIT 1
67            "#,
68            name,
69            deleted_status
70        )
71        .fetch_optional(&*self.pool)
72        .await?
73        .map(User::try_from)
74        .transpose()?;
75
76        Ok(row)
77    }
78
79    pub async fn find_by_reference_any_status(&self, reference: &str) -> Result<Option<User>> {
80        let row = sqlx::query_as!(
81            UserRow,
82            r#"
83            SELECT id, name, email, full_name, display_name, status, email_verified,
84                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
85            FROM users
86            WHERE id = $1 OR email = $1 OR name = $1
87            ORDER BY (id = $1) DESC, (email = $1) DESC, created_at ASC
88            LIMIT 1
89            "#,
90            reference
91        )
92        .fetch_optional(&*self.pool)
93        .await?
94        .map(User::try_from)
95        .transpose()?;
96
97        Ok(row)
98    }
99
100    pub async fn list_by_role(&self, role: UserRole) -> Result<Vec<User>> {
101        let deleted_status = UserStatus::Deleted.as_str();
102        let rows = sqlx::query_as!(
103            UserRow,
104            r#"
105            SELECT id, name, email, full_name, display_name, status, email_verified,
106                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
107            FROM users
108            WHERE $1 = ANY(roles) AND status != $2
109            ORDER BY created_at DESC
110            "#,
111            role.as_str(),
112            deleted_status
113        )
114        .fetch_all(&*self.pool)
115        .await?
116        .into_iter()
117        .map(User::try_from)
118        .collect::<Result<Vec<_>>>()?;
119
120        Ok(rows)
121    }
122
123    pub async fn find_first_user(&self) -> Result<Option<User>> {
124        let deleted_status = UserStatus::Deleted.as_str();
125        let row = sqlx::query_as!(
126            UserRow,
127            r#"
128            SELECT id, name, email, full_name, display_name, status, email_verified,
129                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
130            FROM users
131            WHERE status != $1
132            ORDER BY created_at ASC
133            LIMIT 1
134            "#,
135            deleted_status
136        )
137        .fetch_optional(&*self.pool)
138        .await?
139        .map(User::try_from)
140        .transpose()?;
141
142        Ok(row)
143    }
144
145    pub async fn find_first_admin(&self) -> Result<Option<User>> {
146        let deleted_status = UserStatus::Deleted.as_str();
147        let admin_role = UserRole::Admin.as_str();
148        let row = sqlx::query_as!(
149            UserRow,
150            r#"
151            SELECT id, name, email, full_name, display_name, status, email_verified,
152                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
153            FROM users
154            WHERE $1 = ANY(roles) AND status != $2
155            ORDER BY created_at ASC
156            LIMIT 1
157            "#,
158            admin_role,
159            deleted_status
160        )
161        .fetch_optional(&*self.pool)
162        .await?
163        .map(User::try_from)
164        .transpose()?;
165
166        Ok(row)
167    }
168
169    pub async fn find_authenticated_user(&self, user_id: &UserId) -> Result<Option<User>> {
170        let active_status = UserStatus::Active.as_str();
171        let row = sqlx::query_as!(
172            UserRow,
173            r#"
174            SELECT id, name, email, full_name, display_name, status, email_verified,
175                   roles, avatar_url, is_bot, is_scanner, created_at, updated_at, account_kind
176            FROM users
177            WHERE id = $1 AND status = $2
178            "#,
179            user_id.as_str(),
180            active_status
181        )
182        .fetch_optional(&*self.pool)
183        .await?
184        .map(User::try_from)
185        .transpose()?;
186
187        Ok(row)
188    }
189}
190
191impl UserRepository {
192    pub async fn missing_ids(&self, candidates: &[UserId]) -> Result<Vec<UserId>> {
193        let ids: Vec<String> = candidates.iter().map(ToString::to_string).collect();
194        let rows = sqlx::query_scalar!(
195            r#"
196            SELECT candidate AS "candidate!"
197            FROM UNNEST($1::text[]) AS candidate
198            WHERE NOT EXISTS (SELECT 1 FROM users WHERE users.id = candidate)
199            "#,
200            &ids[..]
201        )
202        .fetch_all(&*self.pool)
203        .await?;
204        Ok(rows.into_iter().map(UserId::new).collect())
205    }
206}