Skip to main content

alibi_core/store/stateless/
members.rs

1use 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// Array.slice applies ToIntegerOrInfinity, supports negative indices, and treats NaN as zero.
48#[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}