use crate::store::stateless::StatelessStore;
use crate::store::{ListOrganizationMembersParams, MemberPageQuery, MemberStore};
use crate::utils::javascript::number_from_usize;
use crate::{AuthError, AuthResult, CreateMember, Member};
use async_trait::async_trait;
use chrono::Utc;
use std::cmp::Ordering;
fn field<'a>(row: &'a Member, name: &str) -> Option<&'a str> {
match name {
"id" => Some(&row.id),
"organizationId" => Some(&row.organization_id),
"userId" => Some(&row.user_id),
"role" => Some(&row.role),
_ => None,
}
}
fn comparison(order: Ordering, operator: &str) -> bool {
match operator {
"ne" => order != Ordering::Equal,
"gt" => order == Ordering::Greater,
"gte" => order != Ordering::Less,
"lt" => order == Ordering::Less,
"lte" => order != Ordering::Greater,
_ => order == Ordering::Equal,
}
}
fn matches(row: &Member, query: &MemberPageQuery) -> bool {
let (Some(name), Some(value)) = (query.filter_field.as_deref(), query.filter_value.as_deref())
else {
return true;
};
let operator = query.filter_operator.as_deref().unwrap_or("eq");
if name == "createdAt" {
return chrono::DateTime::parse_from_rfc3339(value).is_ok_and(|value| {
comparison(row.created_at.cmp(&value.with_timezone(&Utc)), operator)
});
}
field(row, name).is_some_and(|stored| {
if operator == "contains" {
stored.contains(value)
} else {
comparison(stored.encode_utf16().cmp(value.encode_utf16()), operator)
}
})
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "The value is clamped to 0..=length before the conversion"
)]
pub(super) fn slice_index(value: f64, length: usize) -> usize {
if value.is_nan() {
return 0;
}
let value = value.trunc();
let length = number_from_usize(length);
if value < 0.0 {
(length + value).max(0.0) as usize
} else {
value.min(length) as usize
}
}
#[async_trait]
impl MemberStore for StatelessStore {
async fn create_member(&self, data: CreateMember) -> AuthResult<Member> {
let row = Member {
id: uuid::Uuid::new_v4().to_string(),
organization_id: data.organization_id,
user_id: data.user_id,
role: data.role,
created_at: Utc::now(),
};
_ = self
.organization_state()?
.members
.insert(row.id.clone(), row.clone());
Ok(row)
}
async fn get_member(&self, organization_id: &str, user_id: &str) -> AuthResult<Option<Member>> {
Ok(self
.organization_state()?
.members
.values()
.find(|row| row.organization_id == organization_id && row.user_id == user_id)
.cloned())
}
async fn get_member_by_id(&self, id: &str) -> AuthResult<Option<Member>> {
Ok(self.organization_state()?.members.get(id).cloned())
}
async fn update_member_role(&self, id: &str, role: &str) -> AuthResult<Member> {
self.update_member_role_if_present(id, role)
.await?
.ok_or_else(|| AuthError::not_found("Member not found"))
}
async fn update_member_role_if_present(
&self,
id: &str,
role: &str,
) -> AuthResult<Option<Member>> {
let mut state = self.organization_state()?;
let Some(row) = state.members.get_mut(id) else {
return Ok(None);
};
role.clone_into(&mut row.role);
Ok(Some(row.clone()))
}
async fn delete_member(&self, id: &str) -> AuthResult<()> {
_ = self.organization_state()?.members.shift_remove(id);
Ok(())
}
async fn delete_member_with_context(
&self,
id: &str,
organization_id: &str,
user_id: &str,
remove_team_members: bool,
) -> AuthResult<()> {
let mut state = self.organization_state()?;
_ = state.members.shift_remove(id);
if remove_team_members {
let teams: Vec<_> = state
.teams
.values()
.filter(|row| row.organization_id == organization_id)
.take(self.find_many_limit)
.map(|row| row.id.clone())
.collect();
for team in teams {
_ = state.remove_team_members(&team, user_id);
}
}
Ok(())
}
async fn list_organization_members_page(
&self,
organization_id: &str,
limit: usize,
) -> AuthResult<Vec<Member>> {
Ok(self
.organization_state()?
.members
.values()
.filter(|row| row.organization_id == organization_id)
.take(limit)
.cloned()
.collect())
}
async fn list_organization_members(&self, org_id: &str) -> AuthResult<Vec<Member>> {
let mut rows = self
.list_organization_members_page(org_id, usize::MAX)
.await?;
rows.sort_by_key(|row| row.created_at);
Ok(rows)
}
async fn query_organization_members(
&self,
params: &ListOrganizationMembersParams,
) -> AuthResult<(Vec<Member>, usize)> {
self.query_organization_members_page(&MemberPageQuery {
organization_id: params.organization_id.clone(),
limit: params.limit.map(number_from_usize),
offset: params.offset.map(number_from_usize),
sort_by: Some(params.sort_by.clone().unwrap_or_else(|| "createdAt".into())),
sort_direction: Some(
params
.sort_direction
.clone()
.unwrap_or_else(|| "asc".into()),
),
filter_field: params.filter_field.clone(),
filter_value: params.filter_value.clone(),
filter_operator: params.filter_operator.clone(),
})
.await
}
async fn query_organization_members_page(
&self,
query: &MemberPageQuery,
) -> AuthResult<(Vec<Member>, usize)> {
let mut rows: Vec<_> = self
.organization_state()?
.members
.values()
.filter(|row| row.organization_id == query.organization_id && matches(row, query))
.cloned()
.collect();
let total = rows.len();
if let Some(name) = query.sort_by.as_deref() {
rows.sort_by(|a, b| {
let order = if name == "createdAt" {
a.created_at.cmp(&b.created_at)
} else {
field(a, name).cmp(&field(b, name))
};
if query.sort_direction.as_deref() == Some("desc") {
order.reverse()
} else {
order
}
});
}
let start = query
.offset
.map_or(0, |value| slice_index(value, rows.len()));
_ = rows.drain(..start);
rows.truncate(slice_index(
query
.limit
.unwrap_or_else(|| number_from_usize(self.find_many_limit)),
rows.len(),
));
Ok((rows, total))
}
async fn count_organization_members(&self, org_id: &str) -> AuthResult<i64> {
i64::try_from(
self.organization_state()?
.members
.values()
.filter(|row| row.organization_id == org_id)
.count(),
)
.map_err(|_| AuthError::internal("Member count overflow"))
}
async fn count_organization_owners(&self, org_id: &str) -> AuthResult<i64> {
i64::try_from(
self.organization_state()?
.members
.values()
.filter(|row| row.organization_id == org_id && row.role == "owner")
.count(),
)
.map_err(|_| AuthError::internal("Owner count overflow"))
}
}