systemprompt_users/repository/user/
list.rs1use systemprompt_identifiers::UserId;
7
8use crate::error::{Result, UserError};
9use crate::models::{User, UserActivity, UserRole, UserStatus, UserWithSessions};
10use crate::repository::{MAX_PAGE_SIZE, UserRepository};
11
12impl UserRepository {
13 pub async fn find_with_sessions(&self, user_id: &UserId) -> Result<Option<UserWithSessions>> {
14 let deleted_status = UserStatus::Deleted.as_str();
15 let row = sqlx::query_as!(
16 UserWithSessions,
17 r#"
18 SELECT
19 u.id, u.name, u.email, u.full_name, u.status, u.roles, u.created_at,
20 COUNT(s.session_id) FILTER (WHERE s.ended_at IS NULL) as "active_sessions!",
21 MAX(s.last_activity_at) as last_session_at
22 FROM users u
23 LEFT JOIN user_sessions s ON s.user_id = u.id
24 WHERE u.id = $1 AND u.status != $2
25 GROUP BY u.id
26 "#,
27 user_id.as_str(),
28 deleted_status
29 )
30 .fetch_optional(&*self.pool)
31 .await?;
32
33 Ok(row)
34 }
35
36 pub async fn get_activity(&self, user_id: &UserId) -> Result<UserActivity> {
37 let row = sqlx::query_as!(
38 UserActivity,
39 r#"
40 SELECT
41 u.id as user_id,
42 MAX(s.last_activity_at) as last_active,
43 COUNT(DISTINCT s.session_id) as "session_count!",
44 COUNT(DISTINCT t.task_id) as "task_count!",
45 0::bigint as "message_count!"
46 FROM users u
47 LEFT JOIN user_sessions s ON s.user_id = u.id
48 LEFT JOIN agent_tasks t ON t.user_id = u.id
49 WHERE u.id = $1
50 GROUP BY u.id
51 "#,
52 user_id.as_str()
53 )
54 .fetch_one(&*self.pool)
55 .await?;
56
57 Ok(row)
58 }
59
60 pub async fn list(&self, limit: i64, offset: i64) -> Result<Vec<User>> {
61 self.list_filtered(limit, offset, false).await
62 }
63
64 pub async fn list_including_anonymous(&self, limit: i64, offset: i64) -> Result<Vec<User>> {
65 self.list_filtered(limit, offset, true).await
66 }
67
68 async fn list_filtered(
69 &self,
70 limit: i64,
71 offset: i64,
72 include_anonymous: bool,
73 ) -> Result<Vec<User>> {
74 let safe_limit = limit.min(MAX_PAGE_SIZE);
75 let deleted_status = UserStatus::Deleted.as_str();
76 let anonymous_role = UserRole::Anonymous.as_str();
77 let rows = sqlx::query_as!(
78 User,
79 r#"
80 SELECT id, name, email, full_name, display_name, status, email_verified,
81 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
82 FROM users
83 WHERE status != $1
84 AND ($4 OR NOT ($5 = ANY(roles)))
85 ORDER BY created_at DESC
86 LIMIT $2 OFFSET $3
87 "#,
88 deleted_status,
89 safe_limit,
90 offset,
91 include_anonymous,
92 anonymous_role
93 )
94 .fetch_all(&*self.pool)
95 .await?;
96
97 Ok(rows)
98 }
99
100 pub async fn list_all(&self) -> Result<Vec<User>> {
101 let deleted_status = UserStatus::Deleted.as_str();
102 let anonymous_role = UserRole::Anonymous.as_str();
103 let rows = sqlx::query_as!(
104 User,
105 r#"
106 SELECT id, name, email, full_name, display_name, status, email_verified,
107 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
108 FROM users
109 WHERE status != $1
110 AND NOT ($2 = ANY(roles))
111 ORDER BY created_at DESC
112 "#,
113 deleted_status,
114 anonymous_role
115 )
116 .fetch_all(&*self.pool)
117 .await?;
118
119 Ok(rows)
120 }
121
122 pub async fn search(&self, query: &str, limit: i64) -> Result<Vec<User>> {
123 self.search_filtered(query, limit, false).await
124 }
125
126 pub async fn search_including_anonymous(&self, query: &str, limit: i64) -> Result<Vec<User>> {
127 self.search_filtered(query, limit, true).await
128 }
129
130 async fn search_filtered(
131 &self,
132 query: &str,
133 limit: i64,
134 include_anonymous: bool,
135 ) -> Result<Vec<User>> {
136 let safe_limit = limit.min(MAX_PAGE_SIZE);
137 let pattern = format!("%{query}%");
138 let deleted_status = UserStatus::Deleted.as_str();
139 let anonymous_role = UserRole::Anonymous.as_str();
140 let rows = sqlx::query_as!(
141 User,
142 r#"
143 SELECT id, name, email, full_name, display_name, status, email_verified,
144 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
145 FROM users
146 WHERE status != $1
147 AND ($4 OR NOT ($5 = ANY(roles)))
148 AND (name ILIKE $2 OR email ILIKE $2 OR full_name ILIKE $2)
149 ORDER BY
150 CASE WHEN name ILIKE $2 THEN 0 ELSE 1 END,
151 created_at DESC
152 LIMIT $3
153 "#,
154 deleted_status,
155 pattern,
156 safe_limit,
157 include_anonymous,
158 anonymous_role
159 )
160 .fetch_all(&*self.pool)
161 .await?;
162
163 Ok(rows)
164 }
165
166 pub async fn count(&self) -> Result<i64> {
167 self.count_filtered(false).await
168 }
169
170 pub async fn count_including_anonymous(&self) -> Result<i64> {
171 self.count_filtered(true).await
172 }
173
174 async fn count_filtered(&self, include_anonymous: bool) -> Result<i64> {
175 let deleted_status = UserStatus::Deleted.as_str();
176 let anonymous_role = UserRole::Anonymous.as_str();
177 let result = sqlx::query_scalar!(
178 r#"SELECT COUNT(*) as "count!" FROM users
179 WHERE status != $1
180 AND ($2 OR NOT ($3 = ANY(roles)))"#,
181 deleted_status,
182 include_anonymous,
183 anonymous_role
184 )
185 .fetch_one(&*self.pool)
186 .await?;
187
188 Ok(result)
189 }
190
191 pub async fn list_by_filter(
192 &self,
193 status: Option<&str>,
194 role: Option<&str>,
195 older_than_days: Option<i64>,
196 limit: i64,
197 ) -> Result<Vec<User>> {
198 let safe_limit = limit.min(MAX_PAGE_SIZE);
199 let deleted_status = UserStatus::Deleted.as_str();
200
201 let rows = sqlx::query_as!(
202 User,
203 r#"
204 SELECT id, name, email, full_name, display_name, status, email_verified,
205 roles, avatar_url, is_bot, is_scanner, created_at, updated_at
206 FROM users
207 WHERE status != $1
208 AND ($2::text IS NULL OR status = $2)
209 AND ($3::text IS NULL OR $3 = ANY(roles))
210 AND ($4::bigint IS NULL OR created_at < NOW() - make_interval(days => $4::int))
211 ORDER BY created_at DESC
212 LIMIT $5
213 "#,
214 deleted_status,
215 status,
216 role,
217 older_than_days,
218 safe_limit
219 )
220 .fetch_all(&*self.pool)
221 .await?;
222
223 Ok(rows)
224 }
225
226 pub async fn is_temporary_anonymous(&self, id: &UserId) -> Result<bool> {
227 let anonymous_role = UserRole::Anonymous.as_str();
228 let result = sqlx::query_scalar!(
229 r#"
230 SELECT $1 = ANY(roles) as "is_anonymous!"
231 FROM users
232 WHERE id = $2
233 "#,
234 anonymous_role,
235 id.as_str()
236 )
237 .fetch_optional(&*self.pool)
238 .await?;
239
240 result.ok_or(UserError::NotFound(id.clone()))
241 }
242
243 pub async fn list_non_anonymous_with_sessions(
244 &self,
245 limit: i64,
246 ) -> Result<Vec<UserWithSessions>> {
247 let safe_limit = limit.min(MAX_PAGE_SIZE);
248 let deleted_status = UserStatus::Deleted.as_str();
249 let anonymous_role = UserRole::Anonymous.as_str();
250 let rows = sqlx::query_as!(
251 UserWithSessions,
252 r#"
253 SELECT
254 u.id, u.name, u.email, u.full_name, u.status, u.roles, u.created_at,
255 COUNT(s.session_id) FILTER (WHERE s.ended_at IS NULL) as "active_sessions!",
256 MAX(s.last_activity_at) as last_session_at
257 FROM users u
258 LEFT JOIN user_sessions s ON s.user_id = u.id
259 WHERE u.status != $1
260 AND NOT ($2 = ANY(u.roles))
261 GROUP BY u.id
262 ORDER BY last_session_at DESC NULLS LAST
263 LIMIT $3
264 "#,
265 deleted_status,
266 anonymous_role,
267 safe_limit
268 )
269 .fetch_all(&*self.pool)
270 .await?;
271
272 Ok(rows)
273 }
274}