forge/lib/repositories
Jacob Schmidt b8dd3ef651 Add task and request payload plumbing to CAD dispatcher
- Thread request data through UI bridge and dispatcher events
- Add task models, repositories, services, and extension wiring
- Include submitted request fields in converted order notes
2026-04-02 15:35:39 -05:00
..
2025-11-26 18:33:09 -06:00

Forge Repositories

This crate provides the data access layer for the Forge application, implementing the repository pattern to abstract database operations from business logic.

Architecture

The repository layer sits between the service layer and the database, providing a clean abstraction for data persistence.

graph TD
    Services[Services Layer]
    Repositories[Repositories Layer<br/>#40;This Module#41;]
    Database[Database]

    Services --> Repositories
    Repositories --> Database

Dual Storage Strategy

The implementation uses a dual storage strategy in Redis to optimize for different access patterns:

  • Hash Maps (HMSET/HGETALL): Used for entity data (Actors, Organizations) to allow O(1) access to specific fields and efficient partial updates.
  • Sets (SADD/SMEMBERS): Used for relationships (e.g., Organization Members) to ensure uniqueness and provide efficient membership testing.

Key Features

  • Redis Integration: Efficient hash-based storage for data
  • JSON Serialization: Automatic conversion between Rust structs and Redis
  • Type Safety: Strong typing with error handling
  • Performance Optimized: Hash operations for fast field-level access
  • Flexible Client: Generic over Redis client implementations
  • Atomic Operations: Uses Redis atomicity guarantees for data integrity

Actor Repository

The ActorRepository handles persistence for player data.

Storage Format

Actors are stored in Redis as hash maps:

actor:{uid} -> Hash {
  "uid": "76561198123456789",
  "name": "PlayerName",
  "bank": "1500.0",
  ...
}

Usage Example

use forge_repositories::ActorRepository;
use forge_models::Actor;

async fn example_usage<R: ActorRepository>(repo: &R) -> Result<(), String> {
    // 1. Create
    let actor = Actor::new("76561198123456789".to_string())?;
    repo.create(&actor)?;

    // 2. Retrieve
    if let Some(retrieved) = repo.get_by_id("76561198123456789")? {
        println!("Found actor: {}", retrieved.name());
    }

    // 3. Update
    // Updates are atomic and preserve fields not present in the update
    let mut actor_to_update = repo.get_by_id("76561198123456789")?.unwrap();
    // ... modify actor ...
    repo.update(&actor_to_update)?;

    // 4. Check Existence
    if repo.exists("76561198123456789")? {
        println!("Actor exists");
    }

    // 5. Delete
    repo.delete("76561198123456789")?;

    Ok(())
}

Organization Repository

The OrgRepository handles persistence for organizations (guilds/clans) and their members.

Storage Format

  • Org Data: org:{org_id} (Hash)
  • Members: org:{org_id}:members (Set)

Usage Example

use forge_repositories::OrgRepository;
use forge_models::{Org, MemberSummary};

async fn example_usage<R: OrgRepository>(repo: &R) -> Result<(), String> {
    // 1. Create Organization
    let org = Org::new("elite_squad".to_string(), "leader_uid".to_string(), "Elite Squad".to_string())?;
    repo.create(&org)?;

    // 2. Manage Members
    // Add a member (idempotent, handles duplicates)
    repo.add_member("elite_squad", "member_uid_1")?;

    // Get all members
    let members = repo.get_members("elite_squad")?;
    for member in members {
        println!("Member: {} ({})", member.name, member.uid);
    }

    // Remove a member
    repo.remove_member("elite_squad", "member_uid_1")?;

    // 3. Update Organization
    let mut org_update = repo.get_by_id("elite_squad")?.unwrap();
    // ... modify org ...
    repo.update(&org_update)?;

    // 4. Delete Organization
    // Note: This removes the org data but may require separate cleanup for members depending on implementation
    repo.delete("elite_squad")?;

    Ok(())
}

Performance & Implementation Details

Atomicity

  • Upserts: create and update operations use HMSET which is atomic. This means either all fields are updated or none are.
  • Schema Evolution: New fields added to the Rust structs are automatically persisted to Redis. Old fields in Redis that are no longer in the struct are preserved (not deleted) during updates, allowing for safe backward compatibility.

Thread Safety

All repository implementations are Send + Sync, allowing them to be safely shared across threads. The underlying RedisClient handles connection pooling and concurrent access.

Error Handling

Repositories return Result<T, String> (or custom error types) to propagate database failures, serialization errors, or validation issues up to the service layer.

Contributing

We welcome contributions to the Forge Repository Layer! This guide will help you understand how to add new repositories and maintain the existing codebase.

Adding a New Repository

To add a new repository (e.g., ItemRepository), follow these steps:

  1. Create the Module: Create a new file in src/ (e.g., src/item.rs).

  2. Define the Trait: Define a trait that specifies the data access operations. Ensure it requires Send + Sync.

    pub trait ItemRepository: Send + Sync {
        fn create(&self, item: &Item) -> Result<(), String>;
        fn get_by_id(&self, id: &str) -> Result<Option<Item>, String>;
        // ... other methods
    }
    
  3. Implement for Redis: Implement the trait for a generic RedisClient. Use forge_shared helpers for value conversion if needed.

    use forge_shared::{RedisClient, parse_json_value, parse_redis_value};
    
    pub struct RedisItemRepository<C: RedisClient> {
        client: C,
    }
    
    impl<C: RedisClient> RedisItemRepository<C> {
        pub fn new(client: C) -> Self {
            Self { client }
        }
    }
    
    impl<C: RedisClient> ItemRepository for RedisItemRepository<C> {
        fn create(&self, item: &Item) -> Result<(), String> {
            let redis_key = format!("item:{}", item.id);
            // ... serialization logic ...
            // Use self.client to interact with Redis
            self.client.hash_mset(redis_key, fields)
        }
    
        // ... other methods
    }
    
  4. Register the Module: Add your new module to src/lib.rs and export the trait and implementation.

    pub mod item;
    pub use item::{ItemRepository, RedisItemRepository};
    

Testing

  • Integration Tests: Write integration tests that use a real Redis instance (if available) or a mock.
  • Mocking: For unit testing services, you don't need to test the repository implementation itself, but you should ensure the repository trait is easy to mock.

Best Practices

  • Abstraction: Keep the repository trait agnostic of the underlying database technology (e.g., don't expose Redis-specific types in the trait signature).
  • Serialization: Handle serialization/deserialization within the repository implementation. The service layer should work with domain models, not raw JSON or database rows.
  • Keyspace: Use a consistent naming convention for Redis keys (e.g., entity:id).
  • Atomicity: Use Redis transactions or atomic commands where possible to ensure data consistency.