- 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
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(¬e).await?;
// Retrieve the note
let retrieved: Note = store.get("notes", ¬e.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:
- Serialize the object to JSON
- Store at
obj:<ns>:<id> - Generate index keys by calling
obj.index_keys() - Create indexes for each key at
idx:<ns>:<field>:<value> - Add to scan index at
scan:<ns>
When an object is deleted:
- Retrieve the object
- Generate index keys
- Remove from all indexes
- 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
- Type Safety: Compile-time guarantees for object types
- Extensibility: Easy to add new object types
- Automatic Indexing: Index keys generated from object structure
- Consistency: Same pattern as heromodels
- Flexibility: Each object type controls its own indexing logic
- Testability: Easy to mock and test individual object types
Summary
OSIRIS provides:
- Type-safe storage: Any type implementing
Objectcan 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.