- 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
255 lines
9.7 KiB
Rust
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())
|
|
}
|
|
}
|