alibi_core/store/stateless/
members.rs1use crate::store::stateless::StatelessStore;
2use crate::store::{ListOrganizationMembersParams, MemberPageQuery, MemberStore};
3use crate::utils::javascript::number_from_usize;
4use crate::{AuthError, AuthResult, CreateMember, Member};
5use async_trait::async_trait;
6use chrono::Utc;
7use std::cmp::Ordering;
8
9fn field<'a>(row: &'a Member, name: &str) -> Option<&'a str> {
10 match name {
11 "id" => Some(&row.id),
12 "organizationId" => Some(&row.organization_id),
13 "userId" => Some(&row.user_id),
14 "role" => Some(&row.role),
15 _ => None,
16 }
17}
18fn comparison(order: Ordering, operator: &str) -> bool {
19 match operator {
20 "ne" => order != Ordering::Equal,
21 "gt" => order == Ordering::Greater,
22 "gte" => order != Ordering::Less,
23 "lt" => order == Ordering::Less,
24 "lte" => order != Ordering::Greater,
25 _ => order == Ordering::Equal,
26 }
27}
28fn matches(row: &Member, query: &MemberPageQuery) -> bool {
29 let (Some(name), Some(value)) = (query.filter_field.as_deref(), query.filter_value.as_deref())
30 else {
31 return true;
32 };
33 let operator = query.filter_operator.as_deref().unwrap_or("eq");
34 if name == "createdAt" {
35 return chrono::DateTime::parse_from_rfc3339(value).is_ok_and(|value| {
36 comparison(row.created_at.cmp(&value.with_timezone(&Utc)), operator)
37 });
38 }
39 field(row, name).is_some_and(|stored| {
40 if operator == "contains" {
41 stored.contains(value)
42 } else {
43 comparison(stored.encode_utf16().cmp(value.encode_utf16()), operator)
44 }
45 })
46}
47#[expect(
49 clippy::cast_possible_truncation,
50 clippy::cast_sign_loss,
51 reason = "The value is clamped to 0..=length before the conversion"
52)]
53pub(super) fn slice_index(value: f64, length: usize) -> usize {
54 if value.is_nan() {
55 return 0;
56 }
57 let value = value.trunc();
58 let length = number_from_usize(length);
59 if value < 0.0 {
60 (length + value).max(0.0) as usize
61 } else {
62 value.min(length) as usize
63 }
64}
65
66#[async_trait]
67impl MemberStore for StatelessStore {
68 async fn create_member(&self, data: CreateMember) -> AuthResult<Member> {
69 let row = Member {
70 id: uuid::Uuid::new_v4().to_string(),
71 organization_id: data.organization_id,
72 user_id: data.user_id,
73 role: data.role,
74 created_at: Utc::now(),
75 };
76 _ = self
77 .organization_state()?
78 .members
79 .insert(row.id.clone(), row.clone());
80 Ok(row)
81 }
82 async fn get_member(&self, organization_id: &str, user_id: &str) -> AuthResult<Option<Member>> {
83 Ok(self
84 .organization_state()?
85 .members
86 .values()
87 .find(|row| row.organization_id == organization_id && row.user_id == user_id)
88 .cloned())
89 }
90 async fn get_member_by_id(&self, id: &str) -> AuthResult<Option<Member>> {
91 Ok(self.organization_state()?.members.get(id).cloned())
92 }
93 async fn update_member_role(&self, id: &str, role: &str) -> AuthResult<Member> {
94 self.update_member_role_if_present(id, role)
95 .await?
96 .ok_or_else(|| AuthError::not_found("Member not found"))
97 }
98 async fn update_member_role_if_present(
99 &self,
100 id: &str,
101 role: &str,
102 ) -> AuthResult<Option<Member>> {
103 let mut state = self.organization_state()?;
104 let Some(row) = state.members.get_mut(id) else {
105 return Ok(None);
106 };
107 role.clone_into(&mut row.role);
108 Ok(Some(row.clone()))
109 }
110 async fn delete_member(&self, id: &str) -> AuthResult<()> {
111 _ = self.organization_state()?.members.shift_remove(id);
112 Ok(())
113 }
114 async fn delete_member_with_context(
115 &self,
116 id: &str,
117 organization_id: &str,
118 user_id: &str,
119 remove_team_members: bool,
120 ) -> AuthResult<()> {
121 let mut state = self.organization_state()?;
122 _ = state.members.shift_remove(id);
123 if remove_team_members {
124 let teams: Vec<_> = state
125 .teams
126 .values()
127 .filter(|row| row.organization_id == organization_id)
128 .take(self.find_many_limit)
129 .map(|row| row.id.clone())
130 .collect();
131 for team in teams {
132 _ = state.remove_team_members(&team, user_id);
133 }
134 }
135 Ok(())
136 }
137 async fn list_organization_members_page(
138 &self,
139 organization_id: &str,
140 limit: usize,
141 ) -> AuthResult<Vec<Member>> {
142 Ok(self
143 .organization_state()?
144 .members
145 .values()
146 .filter(|row| row.organization_id == organization_id)
147 .take(limit)
148 .cloned()
149 .collect())
150 }
151 async fn list_organization_members(&self, org_id: &str) -> AuthResult<Vec<Member>> {
152 let mut rows = self
153 .list_organization_members_page(org_id, usize::MAX)
154 .await?;
155 rows.sort_by_key(|row| row.created_at);
156 Ok(rows)
157 }
158 async fn query_organization_members(
159 &self,
160 params: &ListOrganizationMembersParams,
161 ) -> AuthResult<(Vec<Member>, usize)> {
162 self.query_organization_members_page(&MemberPageQuery {
163 organization_id: params.organization_id.clone(),
164 limit: params.limit.map(number_from_usize),
165 offset: params.offset.map(number_from_usize),
166 sort_by: Some(params.sort_by.clone().unwrap_or_else(|| "createdAt".into())),
167 sort_direction: Some(
168 params
169 .sort_direction
170 .clone()
171 .unwrap_or_else(|| "asc".into()),
172 ),
173 filter_field: params.filter_field.clone(),
174 filter_value: params.filter_value.clone(),
175 filter_operator: params.filter_operator.clone(),
176 })
177 .await
178 }
179 async fn query_organization_members_page(
180 &self,
181 query: &MemberPageQuery,
182 ) -> AuthResult<(Vec<Member>, usize)> {
183 let mut rows: Vec<_> = self
184 .organization_state()?
185 .members
186 .values()
187 .filter(|row| row.organization_id == query.organization_id && matches(row, query))
188 .cloned()
189 .collect();
190 let total = rows.len();
191 if let Some(name) = query.sort_by.as_deref() {
192 rows.sort_by(|a, b| {
193 let order = if name == "createdAt" {
194 a.created_at.cmp(&b.created_at)
195 } else {
196 field(a, name).cmp(&field(b, name))
197 };
198 if query.sort_direction.as_deref() == Some("desc") {
199 order.reverse()
200 } else {
201 order
202 }
203 });
204 }
205 let start = query
206 .offset
207 .map_or(0, |value| slice_index(value, rows.len()));
208 _ = rows.drain(..start);
209 rows.truncate(slice_index(
210 query
211 .limit
212 .unwrap_or_else(|| number_from_usize(self.find_many_limit)),
213 rows.len(),
214 ));
215 Ok((rows, total))
216 }
217 async fn count_organization_members(&self, org_id: &str) -> AuthResult<i64> {
218 i64::try_from(
219 self.organization_state()?
220 .members
221 .values()
222 .filter(|row| row.organization_id == org_id)
223 .count(),
224 )
225 .map_err(|_| AuthError::internal("Member count overflow"))
226 }
227 async fn count_organization_owners(&self, org_id: &str) -> AuthResult<i64> {
228 i64::try_from(
229 self.organization_state()?
230 .members
231 .values()
232 .filter(|row| row.organization_id == org_id && row.role == "owner")
233 .count(),
234 )
235 .map_err(|_| AuthError::internal("Owner count overflow"))
236 }
237}