This repository has been archived on 2025-11-14. You can view files and clone it, but cannot push or open issues or pull requests.
Files
osiris/docs/ARCHITECTURE.md
Timur Gordon ae846ea734 Start CQRS refactoring: Create Osiris client crate
- Added workspace structure to Osiris Cargo.toml
- Created osiris-client crate for query operations (GET requests)
- Implemented generic get(), list(), query() methods
- Added KYC, payment, and communication query modules
- Created comprehensive refactoring plan document

CQRS Pattern:
- Commands (writes) → Supervisor client → Rhai scripts
- Queries (reads) → Osiris client → REST API

Next steps:
- Implement Osiris server with Axum
- Restructure SDK client by category (kyc/, payment/, etc.)
- Update FreezoneClient to use both supervisor and osiris clients
2025-11-04 10:26:33 +01:00

9.7 KiB

OSIRIS Architecture

Overview

OSIRIS uses a trait-based architecture for storing and retrieving typed objects with automatic indexing, Rhai scripting support, and signatory-based access control.

Core Concepts

1. BaseData

Every OSIRIS object must include BaseData, which provides:

  • id: Unique identifier (UUID or user-assigned)
  • ns: Namespace the object belongs to
  • created_at: Creation timestamp
  • modified_at: Last modification timestamp
  • mime: Optional MIME type
  • size: Optional content size
pub struct BaseData {
    pub id: String,
    pub ns: String,
    pub created_at: OffsetDateTime,
    pub modified_at: OffsetDateTime,
    pub mime: Option<String>,
    pub size: Option<u64>,
}

2. Object Trait

The Object trait is the core abstraction for all OSIRIS objects:

pub trait Object: Debug + Clone + Serialize + Deserialize + Send + Sync {
    /// Get the object type name
    fn object_type() -> &'static str where Self: Sized;
    
    /// Get base data reference
    fn base_data(&self) -> &BaseData;
    
    /// Get mutable base data reference
    fn base_data_mut(&mut self) -> &mut BaseData;
    
    /// Get index keys for this object (auto-generated from #[index] fields)
    fn index_keys(&self) -> Vec<IndexKey>;
    
    /// Get list of indexed field names
    fn indexed_fields() -> Vec<&'static str> where Self: Sized;
    
    /// Get searchable text content
    fn searchable_text(&self) -> Option<String>;
    
    /// Serialize to JSON
    fn to_json(&self) -> Result<String>;
    
    /// Deserialize from JSON
    fn from_json(json: &str) -> Result<Self> where Self: Sized;
}

3. IndexKey

Represents an index entry for a field:

pub struct IndexKey {
    pub name: &'static str,  // Field name
    pub value: String,        // Field value
}

Example: Note Object

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Note {
    pub base_data: BaseData,
    
    // Indexed field - marked with #[index]
    #[index]
    pub title: Option<String>,
    
    // Searchable content (not indexed)
    pub content: Option<String>,
    
    // Indexed tags - marked with #[index]
    #[index]
    pub tags: BTreeMap<String, String>,
}

impl Object for Note {
    fn object_type() -> &'static str {
        "note"
    }
    
    fn base_data(&self) -> &BaseData {
        &self.base_data
    }
    
    fn base_data_mut(&mut self) -> &mut BaseData {
        &mut self.base_data
    }
    
    fn index_keys(&self) -> Vec<IndexKey> {
        let mut keys = Vec::new();
        
        // Index title
        if let Some(title) = &self.title {
            keys.push(IndexKey::new("title", title));
        }
        
        // Index tags
        for (key, value) in &self.tags {
            keys.push(IndexKey::new(&format!("tag:{}", key), value));
        }
        
        keys
    }
    
    fn indexed_fields() -> Vec<&'static str> {
        vec!["title", "tags"]
    }
    
    fn searchable_text(&self) -> Option<String> {
        let mut text = String::new();
        if let Some(title) = &self.title {
            text.push_str(title);
            text.push(' ');
        }
        if let Some(content) = &self.content {
            text.push_str(content);
        }
        if text.is_empty() { None } else { Some(text) }
    }
}

Example: Event Object

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
    pub base_data: BaseData,
    
    #[index]
    pub title: String,
    
    pub description: Option<String>,
    
    #[index]
    pub start_time: OffsetDateTime,
    
    pub end_time: OffsetDateTime,
    
    #[index]
    pub location: Option<String>,
    
    #[index]
    pub status: EventStatus,
    
    pub all_day: bool,
    
    #[index]
    pub category: Option<String>,
}

impl Object for Event {
    fn object_type() -> &'static str {
        "event"
    }
    
    fn base_data(&self) -> &BaseData {
        &self.base_data
    }
    
    fn base_data_mut(&mut self) -> &mut BaseData {
        &mut self.base_data
    }
    
    fn index_keys(&self) -> Vec<IndexKey> {
        let mut keys = Vec::new();
        
        keys.push(IndexKey::new("title", &self.title));
        
        if let Some(location) = &self.location {
            keys.push(IndexKey::new("location", location));
        }
        
        let status_str = match self.status {
            EventStatus::Draft => "draft",
            EventStatus::Published => "published",
            EventStatus::Cancelled => "cancelled",
        };
        keys.push(IndexKey::new("status", status_str));
        
        if let Some(category) = &self.category {
            keys.push(IndexKey::new("category", category));
        }
        
        // Index by date for day-based queries
        let date_str = self.start_time.date().to_string();
        keys.push(IndexKey::new("date", date_str));
        
        keys
    }
    
    fn indexed_fields() -> Vec<&'static str> {
        vec!["title", "location", "status", "category", "start_time"]
    }
    
    fn searchable_text(&self) -> Option<String> {
        let mut text = String::new();
        text.push_str(&self.title);
        text.push(' ');
        if let Some(description) = &self.description {
            text.push_str(description);
        }
        Some(text)
    }
}

Storage Layer

GenericStore

The GenericStore provides a type-safe storage layer for any object implementing Object:

pub struct GenericStore {
    client: HeroDbClient,
    index: FieldIndex,
}

impl GenericStore {
    /// Store an object
    pub async fn put<T: Object>(&self, obj: &T) -> Result<()>;
    
    /// Get an object by ID
    pub async fn get<T: Object>(&self, ns: &str, id: &str) -> Result<T>;
    
    /// Delete an object
    pub async fn delete<T: Object>(&self, obj: &T) -> Result<bool>;
    
    /// Get IDs matching an index key
    pub async fn get_ids_by_index(&self, ns: &str, field: &str, value: &str) -> Result<Vec<String>>;
}

Usage Example

use osiris::objects::Note;
use osiris::store::{GenericStore, HeroDbClient};

// Create store
let client = HeroDbClient::new("redis://localhost:6379", 1)?;
let store = GenericStore::new(client);

// Create and store a note
let note = Note::new("notes".to_string())
    .set_title("My Note")
    .set_content("This is the content")
    .add_tag("topic", "rust")
    .add_tag("priority", "high");

store.put(&note).await?;

// Retrieve the note
let retrieved: Note = store.get("notes", &note.id()).await?;

// Search by index
let ids = store.get_ids_by_index("notes", "tag:topic", "rust").await?;

Index Storage

Keyspace Design

obj:<ns>:<id>              → JSON serialized object
idx:<ns>:<field>:<value>   → Set of object IDs
scan:<ns>                  → Set of all object IDs in namespace

Examples

obj:notes:abc123           → {"base_data":{...},"title":"My Note",...}
idx:notes:title:My Note    → {abc123, def456}
idx:notes:tag:topic:rust   → {abc123, xyz789}
idx:notes:mime:text/plain  → {abc123}
scan:notes                 → {abc123, def456, xyz789}

Automatic Indexing

When an object is stored:

  1. Serialize the object to JSON
  2. Store at obj:<ns>:<id>
  3. Generate index keys by calling obj.index_keys()
  4. Create indexes for each key at idx:<ns>:<field>:<value>
  5. Add to scan index at scan:<ns>

When an object is deleted:

  1. Retrieve the object
  2. Generate index keys
  3. Remove from all indexes
  4. Delete the object

Rhai Integration

OSIRIS provides full Rhai scripting support through the rhai module:

OsirisContext

Multi-tenant context with signatory-based access control:

pub struct OsirisContext {
    context_id: String,
    participants: Vec<String>,  // Public keys
    members: HashMap<String, Vec<Privilege>>,
    store: Arc<GenericStore>,
}

Key Features:

  • Signatory-based access: All participants must be signatories
  • Member management: Add/remove members with privileges
  • Generic CRUD: save(), get(), delete(), list(), query()

Rhai API

Each object type provides Rhai bindings in its rhai.rs file:

#[export_module]
mod rhai_note_module {
    #[rhai_fn(name = "note", return_raw)]
    pub fn new_note(ns: String) -> Result<Note, Box<EvalAltResult>> {
        Ok(Note::new(ns))
    }
    
    #[rhai_fn(name = "title", return_raw)]
    pub fn set_title(note: Note, title: String) -> Result<Note, Box<EvalAltResult>> {
        Ok(note.title(title))
    }
}

Engine Configuration

The runner binary supports multiple configurations:

# Single instance
cargo run --bin runner -- runner1 --redis-url redis://localhost:6379 --db-id 1

# Multiple predefined instances
cargo run --bin runner -- runner1 \
  --instance freezone:redis://localhost:6379:1 \
  --instance my:redis://localhost:6379:2

Benefits of Trait-Based Architecture

  1. Type Safety: Compile-time guarantees for object types
  2. Extensibility: Easy to add new object types
  3. Automatic Indexing: Index keys generated from object structure
  4. Consistency: Same pattern as heromodels
  5. Flexibility: Each object type controls its own indexing logic
  6. Testability: Easy to mock and test individual object types

Summary

OSIRIS provides:

  • Type-safe storage: Any type implementing Object can be stored
  • Automatic indexing: Fields marked with #[index] are automatically indexed
  • Rhai scripting: Full scripting support with builder patterns
  • Multi-tenant contexts: Signatory-based access control
  • HeroDB backend: Redis-compatible storage with encryption
  • Extensibility: Easy to add new object types and features

See CREATING_NEW_OBJECTS.md for a guide on creating custom objects.