1use super::entities::member::Model;
2use super::entities::team;
3use super::{SqlxStore, lock_exclusive};
4use crate::model::{self, ActiveRow, SqlxModel};
5use crate::pool::Exec;
6use crate::schema::AuthSchema;
7use crate::sql::Sql;
8use crate::value::SqlValue;
9use alibi_core::error::{AuthError, AuthResult};
10use alibi_core::store::{ListOrganizationMembersParams, MemberPageQuery, MemberStore};
11use alibi_core::{CreateMember, Member};
12use async_trait::async_trait;
13use chrono::Utc;
14
15impl<S: AuthSchema> SqlxStore<S> {
16 pub(super) async fn create_member_with_connection(
17 &self,
18 exec: Exec<'_>,
19 member: CreateMember,
20 ) -> AuthResult<Member> {
21 let mut active = ActiveRow::new();
22 active.set("id", self.organization_models.member.new_id());
23 active.set("organization_id", member.organization_id);
24 active.set("user_id", member.user_id);
25 active.set("role", member.role);
26 active.set("created_at", Utc::now());
27 self.organization_models
28 .member
29 .insert(exec, &active)
30 .await
31 .map(|model| Member::from(&model))
32 }
33
34 async fn find_member_by_id(
35 &self,
36 id: &str,
37 ) -> AuthResult<Option<crate::organization_models::Row<Model>>> {
38 let mut sql = self.organization_models.member.by_id(self.exec(), id)?;
39 model::limit_one(&mut sql);
40 self.organization_models
41 .member
42 .fetch_optional(self.exec(), sql)
43 .await
44 }
45}
46
47struct Filter {
49 column: &'static str,
50 operator: &'static str,
51 value: SqlValue,
52}
53
54fn member_column(field: &str) -> Option<&'static str> {
55 match field {
56 "id" => Some("id"),
57 "organizationId" => Some("organization_id"),
58 "userId" => Some("user_id"),
59 "role" => Some("role"),
60 "createdAt" => Some("created_at"),
61 _ => None,
62 }
63}
64
65fn member_filter(params: &ListOrganizationMembersParams) -> Option<Filter> {
66 let field = params.filter_field.as_deref()?;
67 let value = params.filter_value.as_deref()?;
68 let operator = params.filter_operator.as_deref().unwrap_or("eq");
69 match member_column(field)? {
70 "created_at" => {
71 let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(value) else {
72 return Some(Filter {
73 column: "id",
74 operator: " = ",
75 value: "__better_auth_never_matches__".into(),
76 });
77 };
78 let parsed = parsed.with_timezone(&Utc);
79 let operator = match operator {
80 "eq" => " = ",
81 "ne" => " <> ",
82 "gt" => " > ",
83 "gte" => " >= ",
84 "lt" => " < ",
85 "lte" => " <= ",
86 _ => return None,
87 };
88 Some(Filter {
89 column: "created_at",
90 operator,
91 value: parsed.into(),
92 })
93 }
94 column => {
95 let (operator, value) = match operator {
96 "eq" => (" = ", value.to_owned()),
97 "ne" => (" <> ", value.to_owned()),
98 "contains" => (" LIKE ", format!("%{value}%")),
99 "gt" => (" > ", value.to_owned()),
100 "gte" => (" >= ", value.to_owned()),
101 "lt" => (" < ", value.to_owned()),
102 "lte" => (" <= ", value.to_owned()),
103 _ => return None,
104 };
105 Some(Filter {
106 column,
107 operator,
108 value: value.into(),
109 })
110 }
111 }
112}
113
114fn member_where(
115 binding: &crate::organization_models::Binding<Model>,
116 sql: &mut Sql,
117 organization_id: &str,
118 filter: Option<&Filter>,
119) -> AuthResult<()> {
120 sql.push(" WHERE ");
121 binding.compare(sql, "organization_id", " = ", organization_id)?;
122 if let Some(filter) = filter {
123 sql.push(" AND ");
124 binding.compare(sql, filter.column, filter.operator, filter.value.clone())?;
125 }
126 Ok(())
127}
128
129fn member_sort(
130 binding: &crate::organization_models::Binding<Model>,
131 sql: &mut Sql,
132 params: &ListOrganizationMembersParams,
133) -> AuthResult<()> {
134 let column = params
135 .sort_by
136 .as_deref()
137 .and_then(member_column)
138 .unwrap_or("created_at");
139 let descending = params.sort_by.as_deref().and_then(member_column).is_some()
140 && matches!(params.sort_direction.as_deref(), Some("desc"));
141 sql.push(" ORDER BY ");
142 binding.column(sql, column)?;
143 sql.push(if descending { " DESC" } else { " ASC" });
144 Ok(())
145}
146
147impl<S: AuthSchema> SqlxStore<S> {
148 async fn count_members(
149 &self,
150 organization_id: &str,
151 filter: Option<&Filter>,
152 ) -> AuthResult<i64> {
153 let mut sql = Sql::with(self.exec().engine(), "SELECT COUNT(*) FROM ");
154 sql.ident(self.organization_models.member.table());
155 member_where(
156 &self.organization_models.member,
157 &mut sql,
158 organization_id,
159 filter,
160 )?;
161 Ok(self
162 .exec()
163 .fetch_scalar::<i64>(sql)
164 .await?
165 .unwrap_or_default())
166 }
167}
168
169#[async_trait]
170impl<S> MemberStore for SqlxStore<S>
171where
172 S: AuthSchema + Send + Sync,
173{
174 async fn create_member(&self, member: CreateMember) -> AuthResult<Member> {
175 self.create_member_with_connection(self.exec(), member)
176 .await
177 }
178
179 async fn get_member(&self, organization_id: &str, user_id: &str) -> AuthResult<Option<Member>> {
180 let mut sql = self.organization_models.member.select(self.exec());
181 sql.push(" WHERE ");
182 self.organization_models.member.compare(
183 &mut sql,
184 "organization_id",
185 " = ",
186 organization_id,
187 )?;
188 sql.push(" AND ");
189 self.organization_models
190 .member
191 .compare(&mut sql, "user_id", " = ", user_id)?;
192 model::limit_one(&mut sql);
193 Ok(self
194 .organization_models
195 .member
196 .fetch_optional(self.exec(), sql)
197 .await?
198 .map(|model| Member::from(&model)))
199 }
200
201 async fn get_member_by_id(&self, id: &str) -> AuthResult<Option<Member>> {
202 Ok(self
203 .find_member_by_id(id)
204 .await?
205 .map(|model| Member::from(&model)))
206 }
207
208 async fn update_member_role(&self, member_id: &str, role: &str) -> AuthResult<Member> {
209 let Some(model) = self.find_member_by_id(member_id).await? else {
210 return Err(AuthError::not_found("Member not found"));
211 };
212 let mut active = model.into_active();
213 active.set("role", role);
214 self.organization_models
215 .member
216 .update(self.exec(), &active)
217 .await?
218 .map(|model_2| Member::from(&model_2))
219 .ok_or_else(crate::error::record_not_updated)
220 }
221
222 async fn update_member_role_if_present(
223 &self,
224 member_id: &str,
225 role: &str,
226 ) -> AuthResult<Option<Member>> {
227 let Some(model) = self.find_member_by_id(member_id).await? else {
228 return Ok(None);
229 };
230 let mut active = model.into_active();
231 active.set("role", role);
232 Ok(self
233 .organization_models
234 .member
235 .update(self.exec(), &active)
236 .await?
237 .map(|model_2| Member::from(&model_2)))
238 }
239
240 async fn delete_member(&self, member_id: &str) -> AuthResult<()> {
241 let member_id = member_id.to_owned();
242 self.in_transaction(true, async move |tx| {
243 let exec = Exec::Tx(tx);
244 let mut select = self
245 .organization_models
246 .member
247 .by_id(exec, member_id.as_str())?;
248 model::limit_one(&mut select);
249 lock_exclusive(&mut select);
250 if let Some(member) = self
251 .organization_models
252 .member
253 .fetch_optional(exec, select)
254 .await?
255 {
256 super::teams::remove_owned_team_members(
257 tx,
258 &member.user_id,
259 Some(&member.organization_id),
260 )
261 .await?;
262 _ = exec
263 .execute(
264 self.organization_models
265 .member
266 .delete_by_id(exec, member_id)?,
267 )
268 .await?;
269 }
270 Ok(())
271 })
272 .await
273 }
274
275 async fn list_organization_members(&self, org_id: &str) -> AuthResult<Vec<Member>> {
276 let mut sql = self.organization_models.member.select(self.exec());
277 member_where(&self.organization_models.member, &mut sql, org_id, None)?;
278 sql.push(" ORDER BY ");
279 self.organization_models
280 .member
281 .column(&mut sql, "created_at")?;
282 sql.push(" ASC");
283 Ok(self
284 .organization_models
285 .member
286 .fetch_all(self.exec(), sql)
287 .await?
288 .iter()
289 .map(Member::from)
290 .collect())
291 }
292
293 async fn delete_member_with_context(
294 &self,
295 member_id: &str,
296 organization_id: &str,
297 user_id: &str,
298 remove_team_members: bool,
299 ) -> AuthResult<()> {
300 let limit = i64::try_from(self.config().advanced.database.default_find_many_limit)
301 .map_err(|_error| AuthError::internal("Member page parameter exceeds u64"))?;
302 let (member_id, organization_id, user_id) = (
303 member_id.to_owned(),
304 organization_id.to_owned(),
305 user_id.to_owned(),
306 );
307 self.in_transaction(true, async move |tx| {
308 let exec = Exec::Tx(tx);
309 _ = exec
310 .execute(
311 self.organization_models
312 .member
313 .delete_by_id(exec, member_id)?,
314 )
315 .await?;
316 if remove_team_members {
317 let mut rooms = model::select_model::<team::Model>(exec);
318 rooms.push(" WHERE ");
319 rooms.compare(
320 team::Model::TABLE,
321 "organization_id",
322 " = ",
323 organization_id,
324 );
325 rooms.push(" LIMIT ");
326 rooms.bind(limit);
327 lock_exclusive(&mut rooms);
328 let rooms = exec.fetch_all::<team::Model>(rooms).await?;
329 super::teams::release_owned_team_members(tx, &user_id, rooms).await?;
330 }
331 Ok(())
332 })
333 .await
334 }
335
336 async fn list_organization_members_page(
337 &self,
338 organization_id: &str,
339 limit: usize,
340 ) -> AuthResult<Vec<Member>> {
341 let limit = i64::try_from(limit)
342 .map_err(|_error| AuthError::internal("Member page parameter exceeds u64"))?;
343 let mut sql = self.organization_models.member.select(self.exec());
344 member_where(
345 &self.organization_models.member,
346 &mut sql,
347 organization_id,
348 None,
349 )?;
350 sql.push(" LIMIT ");
351 sql.bind(limit);
352 Ok(self
353 .organization_models
354 .member
355 .fetch_all(self.exec(), sql)
356 .await?
357 .iter()
358 .map(Member::from)
359 .collect())
360 }
361
362 async fn query_organization_members(
363 &self,
364 params: &ListOrganizationMembersParams,
365 ) -> AuthResult<(Vec<Member>, usize)> {
366 let filter = member_filter(params);
367 let total = usize::try_from(
368 self.count_members(¶ms.organization_id, filter.as_ref())
369 .await?,
370 )
371 .map_err(|_error| AuthError::internal("Member count exceeds usize"))?;
372
373 let mut sql = self.organization_models.member.select(self.exec());
374 member_where(
375 &self.organization_models.member,
376 &mut sql,
377 ¶ms.organization_id,
378 filter.as_ref(),
379 )?;
380 member_sort(&self.organization_models.member, &mut sql, params)?;
381 if let Some(limit) = params.limit {
382 sql.push(" LIMIT ");
383 sql.bind(
384 i64::try_from(limit)
385 .map_err(|_error| AuthError::internal("Member page parameter exceeds u64"))?,
386 );
387 }
388 if let Some(offset) = params.offset {
389 sql.push(" OFFSET ");
390 sql.bind(
391 i64::try_from(offset)
392 .map_err(|_error| AuthError::internal("Member page parameter exceeds u64"))?,
393 );
394 }
395 Ok((
396 self.organization_models
397 .member
398 .fetch_all(self.exec(), sql)
399 .await?
400 .iter()
401 .map(Member::from)
402 .collect(),
403 total,
404 ))
405 }
406
407 async fn query_organization_members_page(
408 &self,
409 params: &MemberPageQuery,
410 ) -> AuthResult<(Vec<Member>, usize)> {
411 let legacy_filter = ListOrganizationMembersParams {
412 organization_id: params.organization_id.clone(),
413 sort_by: params.sort_by.clone(),
414 sort_direction: params.sort_direction.clone(),
415 filter_field: params.filter_field.clone(),
416 filter_value: params.filter_value.clone(),
417 filter_operator: params.filter_operator.clone(),
418 ..Default::default()
419 };
420 let filter = member_filter(&legacy_filter);
421 let total = usize::try_from(
422 self.count_members(¶ms.organization_id, filter.as_ref())
423 .await?,
424 )
425 .map_err(|error| AuthError::Internal(error.to_string()))?;
426 let mut sql = self.organization_models.member.select(self.exec());
427 member_where(
428 &self.organization_models.member,
429 &mut sql,
430 ¶ms.organization_id,
431 filter.as_ref(),
432 )?;
433 if params.sort_by.as_deref().and_then(member_column).is_some() {
434 member_sort(&self.organization_models.member, &mut sql, &legacy_filter)?;
435 }
436 super::bind_page(&mut sql, params.limit, params.offset);
437 Ok((
438 self.organization_models
439 .member
440 .fetch_all(self.exec(), sql)
441 .await?
442 .iter()
443 .map(Member::from)
444 .collect(),
445 total,
446 ))
447 }
448
449 async fn count_organization_members(&self, org_id: &str) -> AuthResult<i64> {
450 self.count_members(org_id, None).await
451 }
452
453 async fn count_organization_owners(&self, org_id: &str) -> AuthResult<i64> {
454 let filter = Filter {
455 column: "role",
456 operator: " = ",
457 value: "owner".into(),
458 };
459 self.count_members(org_id, Some(&filter)).await
460 }
461}