Jacob Schmidt 9cd7278746 Add org leave/disband bridge flow across client and server
- Introduce `OrgUIBridge` to centralize UI event/request/response handling
- Add leave and disband org requests with server handlers and client bridge events
- Enforce portal permissions for leaving/disbanding and protect owner/self from member removal
2026-03-09 23:06:26 -05:00

255 lines
9.7 KiB
Rust

//! Organization repository implementation for data persistence operations.
//!
//! This module provides the data access layer for organization (guild/clan) management,
//! implementing the repository pattern to abstract database operations.
//!
//! For full documentation and examples, see the [crate README](../README.md).
use forge_models::{MemberSummary, Org};
use forge_shared::{RedisClient, parse_json_value, parse_redis_value};
/// Repository trait defining the contract for organization data operations.
///
/// This trait abstracts the data persistence layer, allowing different
/// implementations (Redis, PostgreSQL, etc.) while maintaining a consistent
/// interface for the service layer. All implementations must be thread-safe.
pub trait OrgRepository: Send + Sync {
/// Creates a new organization in the repository.
fn create(&self, org: &Org) -> Result<(), String>;
/// Retrieves an organization by its unique identifier.
fn get_by_id(&self, id: &str) -> Result<Option<Org>, String>;
/// Updates an existing organization with new data.
fn update(&self, org: &Org) -> Result<(), String>;
/// Permanently removes an organization from the repository.
fn delete(&self, id: &str) -> Result<(), String>;
/// Checks if an organization exists in the repository.
fn exists(&self, id: &str) -> Result<bool, String>;
/// Adds a new member UID to an organization.
fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String>;
/// Retrieves all members of an organization as a list of MemberSummary objects.
fn get_members(&self, org_id: &str) -> Result<Vec<MemberSummary>, String>;
/// Removes a specific member from an organization.
fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String>;
}
/// Redis-based implementation of the OrgRepository trait.
///
/// Uses Redis hash maps for organization data providing
/// efficient field-level access and atomic operations. Each organization is stored
/// as a seperate hash with the key format `org:{org_id}`.
/// Member lists are stored as sets with the key format `org:{org_id}:members`.
pub struct RedisOrgRepository<C: RedisClient> {
/// The Redis client used for all database operations.
///
/// This client handles the actual communication with Redis, including
/// connection management, command execution, and error handling for
/// both organization and member data operations.
client: C,
}
impl<C: RedisClient> RedisOrgRepository<C> {
/// Creates a new Redis organization repository with the provided client.
pub fn new(client: C) -> Self {
Self { client }
}
}
impl<C: RedisClient> OrgRepository for RedisOrgRepository<C> {
/// Creates a new organization in Redis using hash map storage.
///
/// Stores each organization as a Redis hash map with the key format `{org_id}:org`.
/// Each field of the organization struct becomes a field in the Redis hash.
fn create(&self, org: &Org) -> Result<(), String> {
// Generate Redis key using organization ID
let redis_key = format!("org:{}", org.id());
// Serialize organization to JSON string
let org_json =
serde_json::to_string(org).map_err(|e| format!("Failed to serialize org: {}", e))?;
// Parse JSON string back to Value for field extraction
let json_value: serde_json::Value = serde_json::from_str(&org_json)
.map_err(|e| format!("Failed to parse org JSON: {}", e))?;
// Extract fields from JSON object
if let serde_json::Value::Object(org_map) = json_value {
// Convert each field to Redis-compatible format
let fields: Vec<(String, String)> = org_map
.into_iter()
.map(|(field, value)| (field, parse_json_value(&value)))
.collect();
// Store all fields atomically using Redis HMSET
self.client.hash_mset(redis_key, fields)
} else {
Err("Failed to convert org to object".to_string())
}
}
/// Retrieves an organization from Redis by its unique identifier.
///
/// Uses Redis HGETALL to retrieve all fields of the organization hash map,
/// then reconstructs the Org struct through JSON deserialization.
fn get_by_id(&self, id: &str) -> Result<Option<Org>, String> {
// Generate Redis key using organization ID
let redis_key = format!("org:{}", id);
// Retrieve all hash fields from Redis
let org_string = self.client.hash_get_all(redis_key)?;
// Return None if no data found (organization doesn't exist)
if org_string.is_empty() {
return Ok(None);
}
// Parse comma-separated field-value pairs
let parts: Vec<&str> = org_string.split(", ").collect();
let mut json_map = serde_json::Map::new();
let mut i = 0;
// Process pairs of field names and values
while i + 1 < parts.len() {
let key = parts[i];
let value = parts[i + 1];
// Convert Redis string value back to proper JSON type
let json_value = parse_redis_value(value);
json_map.insert(key.to_string(), json_value);
i += 2; // Move to next field-value pair
}
// Reconstruct Org from JSON object
let json_obj = serde_json::Value::Object(json_map);
match serde_json::from_value::<Org>(json_obj) {
Ok(org) => Ok(Some(org)),
// Return None for any deserialization errors (corrupted data)
Err(_) => Ok(None),
}
}
/// Updates an existing organization with the provided data.
///
/// Uses `HMSET` to atomically update the provided fields. Fields present in Redis
/// but missing from the input are preserved.
fn update(&self, org: &Org) -> Result<(), String> {
// Delegate to create() which handles both creation and updates
// Redis HMSET naturally supports upsert behavior
self.create(org)
}
/// Permanently deletes an organization and all associated data from Redis.
///
/// Removes the organization hash and related subordinate keys.
/// This operation is irreversible.
fn delete(&self, id: &str) -> Result<(), String> {
let redis_keys = [
format!("org:{}", id),
format!("org:{}:members", id),
format!("org:{}:assets", id),
format!("org:{}:fleet", id),
];
for redis_key in redis_keys {
self.client.delete_key(redis_key)?;
}
Ok(())
}
/// Checks if an organization exists in Redis without retrieving the data.
///
/// Uses Redis EXISTS command for a lightweight check.
fn exists(&self, id: &str) -> Result<bool, String> {
// Generate Redis key using organization ID
let redis_key = format!("org:{}", id);
// Check if the key exists in Redis
// This is a lightweight operation that doesn't retrieve data
self.client.key_exists(redis_key)
}
/// Adds a new member to the organization.
///
/// Stores member data in a Redis list associated with the organization.
/// Validates that the organization exists before adding the member.
fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> {
// Check if organization exists
if !self.exists(org_id)? {
return Err(format!("Organization {} does not exist", org_id));
}
// Generate Redis key for organization member set
let redis_key = format!("org:{}:members", org_id);
// Add member UID to set using SADD
self.client.set_add(redis_key, member_uid.to_string())
}
/// Retrieves all members of the organization.
///
/// Uses Redis SMEMBERS to get all member UIDs, then retrieves member details.
/// Returns a list of `MemberSummary` objects.
fn get_members(&self, org_id: &str) -> Result<Vec<MemberSummary>, String> {
// Generate Redis key for organization member set
let redis_key = format!("org:{}:members", org_id);
// Retrieve all member UIDs from the set; fall back to empty on error
let uids: Vec<String> = match self.client.set_members(redis_key) {
Ok(v) => v,
Err(_) => Vec::new(),
};
// Pre-allocate result vector
let mut result: Vec<MemberSummary> = Vec::with_capacity(uids.len());
for uid in uids {
if uid.trim().is_empty() {
continue;
}
// Lookup actor name by UID; fall back to "Unknown" on error/missing
let actor_key = format!("actor:{}", uid);
let raw_name = match self.client.hash_get(actor_key, "name".to_string()) {
Ok(n) => n,
_ => String::new(),
};
let name = match parse_redis_value(&raw_name) {
serde_json::Value::String(s) => s,
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
_ => "Unknown".to_string(),
};
let name = if name.trim().is_empty() {
"Unknown".to_string()
} else {
name
};
result.push(MemberSummary { uid, name });
}
Ok(result)
}
/// Removes a specific member UID from an organization.
///
/// Uses Redis SREM to remove the UID from the organization's member set.
fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> {
// Generate Redis key for organization member set
let redis_key = format!("org:{}:members", org_id);
// Remove the UID from the set using SREM
self.client.set_del(redis_key, member_uid.to_string())
}
}