Merge branch 'development_add_incremental_mode_to_heromodels'
This commit is contained in:
		@@ -32,8 +32,16 @@ where
 | 
			
		||||
    /// Get an object from its ID. This does not use an index lookup
 | 
			
		||||
    fn get_by_id(&self, id: u32) -> Result<Option<V>, Error<Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    /// Store an item in the DB.
 | 
			
		||||
    fn set(&self, value: &V) -> Result<(), Error<Self::Error>>;
 | 
			
		||||
    /// Store an item in the DB and return the assigned ID and the updated model.
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Important Notes
 | 
			
		||||
    /// - This method does not modify the original model passed as an argument.
 | 
			
		||||
    /// - For new models (with ID 0), an ID will be auto-generated by OurDB.
 | 
			
		||||
    /// - The returned model will have the correct ID and should be used instead of the original model.
 | 
			
		||||
    /// - The original model should not be used after calling this method, as it may have
 | 
			
		||||
    ///   an inconsistent state compared to what's in the database.
 | 
			
		||||
    /// - ID 0 is reserved for new models and should not be used for existing models.
 | 
			
		||||
    fn set(&self, value: &V) -> Result<(u32, V), Error<Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    /// Delete all items from the db with a given index.
 | 
			
		||||
    fn delete<I, Q>(&self, key: &Q) -> Result<(), Error<Self::Error>>
 | 
			
		||||
@@ -45,8 +53,11 @@ where
 | 
			
		||||
    /// Delete an object with a given ID
 | 
			
		||||
    fn delete_by_id(&self, id: u32) -> Result<(), Error<Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    /// Get all objects from the colelction
 | 
			
		||||
    /// Get all objects from the collection
 | 
			
		||||
    fn get_all(&self) -> Result<Vec<V>, Error<Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    /// Begin a transaction for this collection
 | 
			
		||||
    fn begin_transaction(&self) -> Result<Box<dyn Transaction<Error = Self::Error>>, Error<Self::Error>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Errors returned by the DB implementation
 | 
			
		||||
@@ -58,6 +69,14 @@ pub enum Error<E> {
 | 
			
		||||
    Decode(bincode::error::DecodeError),
 | 
			
		||||
    /// Error encoding a model for storage
 | 
			
		||||
    Encode(bincode::error::EncodeError),
 | 
			
		||||
    /// Invalid ID used (e.g., using ID 0 for an existing model)
 | 
			
		||||
    InvalidId(String),
 | 
			
		||||
    /// ID mismatch (e.g., expected ID 5 but got ID 6)
 | 
			
		||||
    IdMismatch(String),
 | 
			
		||||
    /// ID collision (e.g., trying to create a model with an ID that already exists)
 | 
			
		||||
    IdCollision(String),
 | 
			
		||||
    /// Type error (e.g., trying to get a model of the wrong type)
 | 
			
		||||
    TypeError,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<E> From<bincode::error::DecodeError> for Error<E> {
 | 
			
		||||
@@ -80,4 +99,20 @@ impl<E: std::fmt::Debug + std::fmt::Display> std::fmt::Display for Error<E> {
 | 
			
		||||
            Error::Encode(e) => write!(f, "Failed to encode model: {}", e),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
/// A transaction that can be committed or rolled back
 | 
			
		||||
pub trait Transaction {
 | 
			
		||||
    /// Error type for transaction operations
 | 
			
		||||
    type Error: std::fmt::Debug;
 | 
			
		||||
 | 
			
		||||
    /// Begin a transaction
 | 
			
		||||
    fn begin(&self) -> Result<(), Error<Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    /// Commit a transaction
 | 
			
		||||
    fn commit(&self) -> Result<(), Error<Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    /// Roll back a transaction
 | 
			
		||||
    fn rollback(&self) -> Result<(), Error<Self::Error>>;
 | 
			
		||||
 | 
			
		||||
    /// Check if a transaction is active
 | 
			
		||||
    fn is_active(&self) -> bool;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
use crate::db::Transaction;
 | 
			
		||||
use heromodels_core::{Index, Model};
 | 
			
		||||
use ourdb::OurDBSetArgs;
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
@@ -6,36 +7,123 @@ use std::{
 | 
			
		||||
    borrow::Borrow,
 | 
			
		||||
    collections::HashSet,
 | 
			
		||||
    path::PathBuf,
 | 
			
		||||
    sync::{Arc, Mutex},
 | 
			
		||||
    sync::{
 | 
			
		||||
        Arc, Mutex,
 | 
			
		||||
        atomic::{AtomicU32, Ordering},
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Configuration for custom ID sequences
 | 
			
		||||
pub struct IdSequence {
 | 
			
		||||
    /// The starting ID for the sequence
 | 
			
		||||
    start: u32,
 | 
			
		||||
    /// The increment for the sequence
 | 
			
		||||
    increment: u32,
 | 
			
		||||
    /// The current ID in the sequence
 | 
			
		||||
    current: AtomicU32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Implement Clone manually since AtomicU32 doesn't implement Clone
 | 
			
		||||
impl Clone for IdSequence {
 | 
			
		||||
    fn clone(&self) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            start: self.start,
 | 
			
		||||
            increment: self.increment,
 | 
			
		||||
            current: AtomicU32::new(self.current.load(Ordering::SeqCst)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl IdSequence {
 | 
			
		||||
    /// Create a new ID sequence with default values (start=1, increment=1)
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            start: 1,
 | 
			
		||||
            increment: 1,
 | 
			
		||||
            current: AtomicU32::new(1),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Create a new ID sequence with custom values
 | 
			
		||||
    pub fn with_config(start: u32, increment: u32) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            start,
 | 
			
		||||
            increment,
 | 
			
		||||
            current: AtomicU32::new(start),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the next ID in the sequence
 | 
			
		||||
    pub fn next_id(&self) -> u32 {
 | 
			
		||||
        self.current.fetch_add(self.increment, Ordering::SeqCst)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Reset the sequence to its starting value
 | 
			
		||||
    pub fn reset(&self) {
 | 
			
		||||
        self.current.store(self.start, Ordering::SeqCst);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Set the current ID in the sequence
 | 
			
		||||
    pub fn set_current(&self, id: u32) {
 | 
			
		||||
        self.current.store(id, Ordering::SeqCst);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct OurDB {
 | 
			
		||||
    index: Arc<Mutex<tst::TST>>,
 | 
			
		||||
    data: Arc<Mutex<ourdb::OurDB>>,
 | 
			
		||||
    // Mutex for ID generation to prevent race conditions
 | 
			
		||||
    id_lock: Arc<Mutex<()>>,
 | 
			
		||||
    // Custom ID sequence configuration
 | 
			
		||||
    id_sequence: Arc<IdSequence>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl OurDB {
 | 
			
		||||
    /// Create a new instance of ourdb
 | 
			
		||||
    /// Create a new instance of ourdb with default ID sequence (start=1, increment=1)
 | 
			
		||||
    pub fn new(path: impl Into<PathBuf>, reset: bool) -> Result<Self, tst::Error> {
 | 
			
		||||
        Self::with_id_sequence(path, reset, IdSequence::new())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Create a new instance of ourdb with a custom ID sequence
 | 
			
		||||
    pub fn with_id_sequence(
 | 
			
		||||
        path: impl Into<PathBuf>,
 | 
			
		||||
        reset: bool,
 | 
			
		||||
        id_sequence: IdSequence,
 | 
			
		||||
    ) -> Result<Self, tst::Error> {
 | 
			
		||||
        let mut base_path = path.into();
 | 
			
		||||
        let mut data_path = base_path.clone();
 | 
			
		||||
        base_path.push("index");
 | 
			
		||||
        data_path.push("data");
 | 
			
		||||
 | 
			
		||||
        let data_db = ourdb::OurDB::new(ourdb::OurDBConfig {
 | 
			
		||||
            incremental_mode: false,
 | 
			
		||||
        let mut data_db = ourdb::OurDB::new(ourdb::OurDBConfig {
 | 
			
		||||
            incremental_mode: true,
 | 
			
		||||
            path: data_path,
 | 
			
		||||
            file_size: None,
 | 
			
		||||
            keysize: None,
 | 
			
		||||
            reset: Some(reset),
 | 
			
		||||
        })?;
 | 
			
		||||
        let index_db = tst::TST::new(base_path.to_str().expect("Path is valid UTF-8"), reset)?;
 | 
			
		||||
        // If we're resetting the database, also reset the ID sequence
 | 
			
		||||
        if reset {
 | 
			
		||||
            id_sequence.reset();
 | 
			
		||||
        } else {
 | 
			
		||||
            // Otherwise, try to find the highest ID in the database and update the sequence
 | 
			
		||||
            // Since OurDB doesn't have a get_highest_id method, we'll use get_next_id instead
 | 
			
		||||
            // This is not ideal, but it's the best we can do with the current API
 | 
			
		||||
            let highest_id = data_db.get_next_id().unwrap_or(id_sequence.start);
 | 
			
		||||
            if highest_id >= id_sequence.start {
 | 
			
		||||
                id_sequence.set_current(highest_id + id_sequence.increment);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(OurDB {
 | 
			
		||||
            index: Arc::new(Mutex::new(index_db)),
 | 
			
		||||
            data: Arc::new(Mutex::new(data_db)),
 | 
			
		||||
            id_lock: Arc::new(Mutex::new(())),
 | 
			
		||||
            id_sequence: Arc::new(id_sequence),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -56,6 +144,20 @@ where
 | 
			
		||||
{
 | 
			
		||||
    type Error = tst::Error;
 | 
			
		||||
 | 
			
		||||
    /// Begin a transaction for this collection
 | 
			
		||||
    fn begin_transaction(
 | 
			
		||||
        &self,
 | 
			
		||||
    ) -> Result<Box<dyn super::Transaction<Error = Self::Error>>, super::Error<Self::Error>> {
 | 
			
		||||
        // Create a new transaction
 | 
			
		||||
        let transaction = OurDBTransaction::new();
 | 
			
		||||
 | 
			
		||||
        // Begin the transaction
 | 
			
		||||
        transaction.begin()?;
 | 
			
		||||
 | 
			
		||||
        // Return the transaction
 | 
			
		||||
        Ok(Box::new(transaction))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get<I, Q>(&self, key: &Q) -> Result<Vec<M>, super::Error<Self::Error>>
 | 
			
		||||
    where
 | 
			
		||||
        I: Index<Model = M>,
 | 
			
		||||
@@ -87,73 +189,168 @@ where
 | 
			
		||||
        Self::get_ourdb_value(&mut data_db, id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn set(&self, value: &M) -> Result<(), super::Error<Self::Error>> {
 | 
			
		||||
        // Before inserting the new object, check if an object with this ID already exists. If it does, we potentially need to update indices.
 | 
			
		||||
        let mut data_db = self.data.lock().expect("can lock data DB");
 | 
			
		||||
        let old_obj: Option<M> = Self::get_ourdb_value(&mut data_db, value.get_id())?;
 | 
			
		||||
        let (indices_to_delete, indices_to_add) = if let Some(old_obj) = old_obj {
 | 
			
		||||
            let mut indices_to_delete = vec![];
 | 
			
		||||
            let mut indices_to_add = vec![];
 | 
			
		||||
            let old_indices = old_obj.db_keys();
 | 
			
		||||
            let new_indices = value.db_keys();
 | 
			
		||||
            for old_index in old_indices {
 | 
			
		||||
                for new_index in &new_indices {
 | 
			
		||||
                    if old_index.name == new_index.name {
 | 
			
		||||
                        if old_index.value != new_index.value {
 | 
			
		||||
                            // different value now, remove index
 | 
			
		||||
                            indices_to_delete.push(old_index);
 | 
			
		||||
                            // and later add the new one
 | 
			
		||||
                            indices_to_add.push(new_index.clone());
 | 
			
		||||
                            break;
 | 
			
		||||
    fn set(&self, value: &M) -> Result<(u32, M), super::Error<Self::Error>> {
 | 
			
		||||
        // For now, we'll skip using transactions to avoid type inference issues
 | 
			
		||||
        // In a real implementation, you would use a proper transaction mechanism
 | 
			
		||||
 | 
			
		||||
        // Use a result variable to track success/failure
 | 
			
		||||
        let result = (|| {
 | 
			
		||||
            // Before inserting the new object, check if an object with this ID already exists. If it does, we potentially need to update indices.
 | 
			
		||||
            let mut data_db = self.data.lock().expect("can lock data DB");
 | 
			
		||||
            let old_obj: Option<M> = Self::get_ourdb_value(&mut data_db, value.get_id())?;
 | 
			
		||||
            let (indices_to_delete, indices_to_add) = if let Some(ref old_obj) = old_obj {
 | 
			
		||||
                let mut indices_to_delete = vec![];
 | 
			
		||||
                let mut indices_to_add = vec![];
 | 
			
		||||
                let old_indices = old_obj.db_keys();
 | 
			
		||||
                let new_indices = value.db_keys();
 | 
			
		||||
                for old_index in old_indices {
 | 
			
		||||
                    for new_index in &new_indices {
 | 
			
		||||
                        if old_index.name == new_index.name {
 | 
			
		||||
                            if old_index.value != new_index.value {
 | 
			
		||||
                                // different value now, remove index
 | 
			
		||||
                                indices_to_delete.push(old_index);
 | 
			
		||||
                                // and later add the new one
 | 
			
		||||
                                indices_to_add.push(new_index.clone());
 | 
			
		||||
                                break;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            // NOTE: we assume here that the index keys are stable, i.e. new index fields don't appear
 | 
			
		||||
            // and existing ones don't dissapear
 | 
			
		||||
            (indices_to_delete, indices_to_add)
 | 
			
		||||
        } else {
 | 
			
		||||
            (vec![], value.db_keys())
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let mut index_db = self.index.lock().expect("can lock index db");
 | 
			
		||||
        // First delete old indices which need to change
 | 
			
		||||
        for old_index in indices_to_delete {
 | 
			
		||||
            let key = Self::index_key(M::db_prefix(), old_index.name, &old_index.value);
 | 
			
		||||
            let raw_ids = index_db.get(&key)?;
 | 
			
		||||
            let (mut ids, _): (HashSet<u32>, _) =
 | 
			
		||||
                bincode::serde::decode_from_slice(&raw_ids, BINCODE_CONFIG)?;
 | 
			
		||||
            ids.remove(&value.get_id());
 | 
			
		||||
            if ids.is_empty() {
 | 
			
		||||
                // This was the last ID with this index value, remove index entirely
 | 
			
		||||
                index_db.delete(&key)?;
 | 
			
		||||
                // NOTE: we assume here that the index keys are stable, i.e. new index fields don't appear
 | 
			
		||||
                // and existing ones don't dissapear
 | 
			
		||||
                (indices_to_delete, indices_to_add)
 | 
			
		||||
            } else {
 | 
			
		||||
                // There are still objects left with this index value, write back updated set
 | 
			
		||||
                let raw_ids = bincode::serde::encode_to_vec(ids, BINCODE_CONFIG)?;
 | 
			
		||||
                index_db.set(&key, raw_ids)?;
 | 
			
		||||
                (vec![], value.db_keys())
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let mut index_db = self.index.lock().expect("can lock index db");
 | 
			
		||||
            // First delete old indices which need to change
 | 
			
		||||
            for old_index in indices_to_delete {
 | 
			
		||||
                let key = Self::index_key(M::db_prefix(), old_index.name, &old_index.value);
 | 
			
		||||
                let raw_ids = index_db.get(&key)?;
 | 
			
		||||
                let (mut ids, _): (HashSet<u32>, _) =
 | 
			
		||||
                    bincode::serde::decode_from_slice(&raw_ids, BINCODE_CONFIG)?;
 | 
			
		||||
                ids.remove(&value.get_id());
 | 
			
		||||
                if ids.is_empty() {
 | 
			
		||||
                    // This was the last ID with this index value, remove index entirely
 | 
			
		||||
                    index_db.delete(&key)?;
 | 
			
		||||
                } else {
 | 
			
		||||
                    // There are still objects left with this index value, write back updated set
 | 
			
		||||
                    let raw_ids = bincode::serde::encode_to_vec(ids, BINCODE_CONFIG)?;
 | 
			
		||||
                    index_db.set(&key, raw_ids)?;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // set or update the object
 | 
			
		||||
        let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?;
 | 
			
		||||
        let id = value.get_id();
 | 
			
		||||
        data_db.set(OurDBSetArgs {
 | 
			
		||||
            id: Some(id),
 | 
			
		||||
            data: &v,
 | 
			
		||||
        })?;
 | 
			
		||||
            // Get the current ID
 | 
			
		||||
            let id = value.get_id();
 | 
			
		||||
 | 
			
		||||
        // Now add the new indices
 | 
			
		||||
        for index_key in indices_to_add {
 | 
			
		||||
            let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value);
 | 
			
		||||
            // Load the existing id set for the index or create a new set
 | 
			
		||||
            let mut existing_ids =
 | 
			
		||||
                Self::get_tst_value::<HashSet<u32>>(&mut index_db, &key)?.unwrap_or_default();
 | 
			
		||||
            existing_ids.insert(id);
 | 
			
		||||
            let encoded_ids = bincode::serde::encode_to_vec(existing_ids, BINCODE_CONFIG)?;
 | 
			
		||||
            index_db.set(&key, encoded_ids)?;
 | 
			
		||||
        }
 | 
			
		||||
            // Validate that ID 0 is only used for new models
 | 
			
		||||
            if id == 0 {
 | 
			
		||||
                // Check if this model already exists in the database
 | 
			
		||||
                // If it does, it's an error to use ID 0 for an existing model
 | 
			
		||||
                if let Some(existing) = Self::get_ourdb_value::<M>(&mut data_db, id)? {
 | 
			
		||||
                    return Err(super::Error::InvalidId(format!(
 | 
			
		||||
                        "ID 0 is reserved for new models. Found existing model with ID 0: {:?}",
 | 
			
		||||
                        existing
 | 
			
		||||
                    )));
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // Validate that IDs > 0 are only used for existing models
 | 
			
		||||
                // If the model doesn't exist, it's an error to use a specific ID
 | 
			
		||||
                if id > 0 && Self::get_ourdb_value::<M>(&mut data_db, id)?.is_none() {
 | 
			
		||||
                    return Err(super::Error::InvalidId(format!(
 | 
			
		||||
                        "ID {} does not exist in the database. Use ID 0 for new models.",
 | 
			
		||||
                        id
 | 
			
		||||
                    )));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
                // Check for ID collisions when manually setting an ID
 | 
			
		||||
                if id > 0 && Self::get_ourdb_value::<M>(&mut data_db, id)?.is_some() {
 | 
			
		||||
                    // This is only an error if we're trying to create a new model with this ID
 | 
			
		||||
                    // If we're updating an existing model, this is fine
 | 
			
		||||
                    if old_obj.is_none() {
 | 
			
		||||
                        return Err(super::Error::IdCollision(format!(
 | 
			
		||||
                            "ID {} already exists in the database",
 | 
			
		||||
                            id
 | 
			
		||||
                        )));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // If id is 0, it's a new object, so let OurDB auto-generate an ID
 | 
			
		||||
            // Otherwise, it's an update to an existing object
 | 
			
		||||
            let id_param = if id == 0 { None } else { Some(id) };
 | 
			
		||||
 | 
			
		||||
            // Thread-safe approach for handling ID assignment
 | 
			
		||||
            let assigned_id = if id == 0 {
 | 
			
		||||
                // For new objects, serialize with ID 0
 | 
			
		||||
                let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?;
 | 
			
		||||
 | 
			
		||||
                // Save to OurDB with id_param = None to let OurDB auto-generate the ID
 | 
			
		||||
                let assigned_id = data_db.set(OurDBSetArgs {
 | 
			
		||||
                    id: id_param,
 | 
			
		||||
                    data: &v,
 | 
			
		||||
                })?;
 | 
			
		||||
 | 
			
		||||
                // Now that we have the actual assigned ID, create a new model with the correct ID
 | 
			
		||||
                // and save it again to ensure the serialized data contains the correct ID
 | 
			
		||||
                let mut value_clone = value.clone();
 | 
			
		||||
                let base_data = value_clone.base_data_mut();
 | 
			
		||||
                base_data.id = assigned_id;
 | 
			
		||||
 | 
			
		||||
                // Serialize the updated model
 | 
			
		||||
                let v = bincode::serde::encode_to_vec(&value_clone, BINCODE_CONFIG)?;
 | 
			
		||||
 | 
			
		||||
                // Save again with the explicit ID
 | 
			
		||||
                data_db.set(OurDBSetArgs {
 | 
			
		||||
                    id: Some(assigned_id),
 | 
			
		||||
                    data: &v,
 | 
			
		||||
                })?;
 | 
			
		||||
 | 
			
		||||
                // Return the assigned ID
 | 
			
		||||
                assigned_id
 | 
			
		||||
            } else {
 | 
			
		||||
                // For existing objects, just serialize and save
 | 
			
		||||
                let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?;
 | 
			
		||||
 | 
			
		||||
                // Save to OurDB with the existing ID
 | 
			
		||||
                let assigned_id = data_db.set(OurDBSetArgs {
 | 
			
		||||
                    id: id_param,
 | 
			
		||||
                    data: &v,
 | 
			
		||||
                })?;
 | 
			
		||||
 | 
			
		||||
                // Return the existing ID
 | 
			
		||||
                assigned_id
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // Now add the new indices
 | 
			
		||||
            for index_key in indices_to_add {
 | 
			
		||||
                let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value);
 | 
			
		||||
                // Load the existing id set for the index or create a new set
 | 
			
		||||
                let mut existing_ids =
 | 
			
		||||
                    Self::get_tst_value::<HashSet<u32>>(&mut index_db, &key)?.unwrap_or_default();
 | 
			
		||||
                // Use the assigned ID for new objects
 | 
			
		||||
                existing_ids.insert(assigned_id);
 | 
			
		||||
                let encoded_ids = bincode::serde::encode_to_vec(existing_ids, BINCODE_CONFIG)?;
 | 
			
		||||
                index_db.set(&key, encoded_ids)?;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get the updated model from the database
 | 
			
		||||
            let updated_model =
 | 
			
		||||
                Self::get_ourdb_value::<M>(&mut data_db, assigned_id)?.ok_or_else(|| {
 | 
			
		||||
                    super::Error::InvalidId(format!(
 | 
			
		||||
                        "Failed to retrieve model with ID {} after saving",
 | 
			
		||||
                        assigned_id
 | 
			
		||||
                    ))
 | 
			
		||||
                })?;
 | 
			
		||||
 | 
			
		||||
            // Return the assigned ID and the updated model
 | 
			
		||||
            Ok((assigned_id, updated_model))
 | 
			
		||||
        })();
 | 
			
		||||
 | 
			
		||||
        // Return the result directly
 | 
			
		||||
        // In a real implementation, you would commit or rollback the transaction here
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn delete<I, Q>(&self, key: &Q) -> Result<(), super::Error<Self::Error>>
 | 
			
		||||
@@ -295,6 +492,29 @@ impl OurDB {
 | 
			
		||||
        format!("{collection}::{index}::{value}")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Reserve an ID for future use
 | 
			
		||||
    pub fn reserve_id(&self) -> u32 {
 | 
			
		||||
        // Acquire the ID lock to prevent race conditions
 | 
			
		||||
        let _id_lock = self.id_lock.lock().expect("Failed to acquire ID lock");
 | 
			
		||||
 | 
			
		||||
        // Get the next ID from our custom sequence
 | 
			
		||||
        self.id_sequence.next_id()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Reserve multiple IDs for future use
 | 
			
		||||
    pub fn reserve_ids(&self, count: u32) -> Vec<u32> {
 | 
			
		||||
        // Acquire the ID lock to prevent race conditions
 | 
			
		||||
        let _id_lock = self.id_lock.lock().expect("Failed to acquire ID lock");
 | 
			
		||||
 | 
			
		||||
        // Get the next IDs from our custom sequence
 | 
			
		||||
        let mut ids = Vec::with_capacity(count as usize);
 | 
			
		||||
        for _ in 0..count {
 | 
			
		||||
            ids.push(self.id_sequence.next_id());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ids
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Wrapper to load values from ourdb and transform a not found error in to Ok(None)
 | 
			
		||||
    fn get_ourdb_value<V>(
 | 
			
		||||
        data: &mut ourdb::OurDB,
 | 
			
		||||
@@ -342,3 +562,86 @@ impl From<ourdb::Error> for super::Error<tst::Error> {
 | 
			
		||||
        super::Error::DB(tst::Error::OurDB(value))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A transaction for OurDB
 | 
			
		||||
///
 | 
			
		||||
/// Note: This is a simplified implementation that doesn't actually provide
 | 
			
		||||
/// ACID guarantees. In a real implementation, you would need to use a proper
 | 
			
		||||
/// transaction mechanism provided by the underlying database.
 | 
			
		||||
///
 | 
			
		||||
/// This struct implements Drop to ensure that transactions are properly closed.
 | 
			
		||||
/// If a transaction is not explicitly committed or rolled back, it will be
 | 
			
		||||
/// rolled back when the transaction is dropped.
 | 
			
		||||
struct OurDBTransaction {
 | 
			
		||||
    active: std::sync::atomic::AtomicBool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl OurDBTransaction {
 | 
			
		||||
    /// Create a new transaction
 | 
			
		||||
    fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            active: std::sync::atomic::AtomicBool::new(false),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Drop for OurDBTransaction {
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        // If the transaction is still active when dropped, roll it back
 | 
			
		||||
        if self.active.load(std::sync::atomic::Ordering::SeqCst) {
 | 
			
		||||
            // We can't return an error from drop, so we just log it
 | 
			
		||||
            eprintln!(
 | 
			
		||||
                "Warning: Transaction was dropped without being committed or rolled back. Rolling back automatically."
 | 
			
		||||
            );
 | 
			
		||||
            self.active
 | 
			
		||||
                .store(false, std::sync::atomic::Ordering::SeqCst);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl super::Transaction for OurDBTransaction {
 | 
			
		||||
    type Error = tst::Error;
 | 
			
		||||
 | 
			
		||||
    /// Begin the transaction
 | 
			
		||||
    fn begin(&self) -> Result<(), super::Error<Self::Error>> {
 | 
			
		||||
        // In a real implementation, you would start a transaction in the underlying database
 | 
			
		||||
        // For now, we just set the active flag
 | 
			
		||||
        self.active.store(true, std::sync::atomic::Ordering::SeqCst);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Commit the transaction
 | 
			
		||||
    fn commit(&self) -> Result<(), super::Error<Self::Error>> {
 | 
			
		||||
        // In a real implementation, you would commit the transaction in the underlying database
 | 
			
		||||
        // For now, we just check if the transaction is active
 | 
			
		||||
        if !self.active.load(std::sync::atomic::Ordering::SeqCst) {
 | 
			
		||||
            return Err(super::Error::InvalidId(
 | 
			
		||||
                "Cannot commit an inactive transaction".to_string(),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.active
 | 
			
		||||
            .store(false, std::sync::atomic::Ordering::SeqCst);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Roll back the transaction
 | 
			
		||||
    fn rollback(&self) -> Result<(), super::Error<Self::Error>> {
 | 
			
		||||
        // In a real implementation, you would roll back the transaction in the underlying database
 | 
			
		||||
        // For now, we just check if the transaction is active
 | 
			
		||||
        if !self.active.load(std::sync::atomic::Ordering::SeqCst) {
 | 
			
		||||
            return Err(super::Error::InvalidId(
 | 
			
		||||
                "Cannot roll back an inactive transaction".to_string(),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.active
 | 
			
		||||
            .store(false, std::sync::atomic::Ordering::SeqCst);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Check if the transaction is active
 | 
			
		||||
    fn is_active(&self) -> bool {
 | 
			
		||||
        self.active.load(std::sync::atomic::Ordering::SeqCst)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -60,8 +60,12 @@ pub struct Event {
 | 
			
		||||
 | 
			
		||||
impl Event {
 | 
			
		||||
    /// Creates a new event
 | 
			
		||||
    pub fn new(id: i64) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        id: i64,
 | 
			
		||||
        title: impl ToString,
 | 
			
		||||
        start_time: DateTime<Utc>,
 | 
			
		||||
        end_time: DateTime<Utc>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
            base_data: BaseModelData::new(id as u32),
 | 
			
		||||
            title: String::new(),
 | 
			
		||||
            description: None,
 | 
			
		||||
@@ -114,7 +118,11 @@ impl Event {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Reschedules the event to new start and end times
 | 
			
		||||
    pub fn reschedule(mut self, new_start_time: DateTime<Utc>, new_end_time: DateTime<Utc>) -> Self {
 | 
			
		||||
    pub fn reschedule(
 | 
			
		||||
        mut self,
 | 
			
		||||
        new_start_time: DateTime<Utc>,
 | 
			
		||||
        new_end_time: DateTime<Utc>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        // Basic validation: end_time should be after start_time
 | 
			
		||||
        if new_end_time > new_start_time {
 | 
			
		||||
            self.start_time = new_start_time;
 | 
			
		||||
@@ -148,11 +156,20 @@ pub struct Calendar {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Calendar {
 | 
			
		||||
    /// Creates a new calendar
 | 
			
		||||
    pub fn new(id: u32) -> Self {
 | 
			
		||||
    /// Creates a new calendar with auto-generated ID
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    /// * `id` - Optional ID for the calendar (use None for auto-generated ID)
 | 
			
		||||
    /// * `name` - Name of the calendar
 | 
			
		||||
    pub fn new(id: Option<u32>, name: impl ToString) -> Self {
 | 
			
		||||
        let mut base_data = BaseModelData::new();
 | 
			
		||||
        if let Some(id) = id {
 | 
			
		||||
            base_data.update_id(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            base_data: BaseModelData::new(id as u32),
 | 
			
		||||
            name: String::new(),
 | 
			
		||||
            base_data,
 | 
			
		||||
            name: name.to_string(),
 | 
			
		||||
            description: None,
 | 
			
		||||
            events: Vec::new(),
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,10 @@ pub struct Comment {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Comment {
 | 
			
		||||
    /// Create a new comment
 | 
			
		||||
    pub fn new(id: u32) -> Self {
 | 
			
		||||
    /// Create a new comment with auto-generated ID
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            base_data: BaseModelData::new(id),
 | 
			
		||||
            base_data: BaseModelData::new(),
 | 
			
		||||
            user_id: 0,
 | 
			
		||||
            content: String::new(),
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
 | 
			
		||||
use rhai::{CustomType, TypeBuilder};
 | 
			
		||||
use heromodels_derive::model;
 | 
			
		||||
use heromodels_core::BaseModelData;
 | 
			
		||||
use heromodels_derive::model;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use super::asset::Asset;
 | 
			
		||||
 | 
			
		||||
@@ -22,18 +24,32 @@ pub struct Account {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Account {
 | 
			
		||||
    /// Create a new account
 | 
			
		||||
    /// Create a new account with auto-generated ID
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    /// * `id` - Optional ID for the account (use None for auto-generated ID)
 | 
			
		||||
    /// * `name` - Name of the account
 | 
			
		||||
    /// * `user_id` - ID of the user who owns the account
 | 
			
		||||
    /// * `description` - Description of the account
 | 
			
		||||
    /// * `ledger` - Ledger/blockchain where the account is located
 | 
			
		||||
    /// * `address` - Address of the account on the blockchain
 | 
			
		||||
    /// * `pubkey` - Public key
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        id: u32, 
 | 
			
		||||
        name: impl ToString, 
 | 
			
		||||
        user_id: u32, 
 | 
			
		||||
        description: impl ToString, 
 | 
			
		||||
        ledger: impl ToString, 
 | 
			
		||||
        address: impl ToString, 
 | 
			
		||||
        pubkey: impl ToString
 | 
			
		||||
        id: Option<u32>,
 | 
			
		||||
        name: impl ToString,
 | 
			
		||||
        user_id: u32,
 | 
			
		||||
        description: impl ToString,
 | 
			
		||||
        ledger: impl ToString,
 | 
			
		||||
        address: impl ToString,
 | 
			
		||||
        pubkey: impl ToString,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let mut base_data = BaseModelData::new();
 | 
			
		||||
        if let Some(id) = id {
 | 
			
		||||
            base_data.update_id(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            base_data: BaseModelData::new(id),
 | 
			
		||||
            base_data,
 | 
			
		||||
            name: name.to_string(),
 | 
			
		||||
            user_id,
 | 
			
		||||
            description: description.to_string(),
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
 | 
			
		||||
use rhai::{CustomType, TypeBuilder};
 | 
			
		||||
use heromodels_derive::model;
 | 
			
		||||
use heromodels_core::BaseModelData;
 | 
			
		||||
use heromodels_derive::model;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
/// AssetType defines the type of blockchain asset
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
@@ -25,18 +27,27 @@ impl Default for AssetType {
 | 
			
		||||
#[model] // Has base.Base in V spec
 | 
			
		||||
pub struct Asset {
 | 
			
		||||
    pub base_data: BaseModelData,
 | 
			
		||||
    pub name: String,           // Name of the asset
 | 
			
		||||
    pub description: String,    // Description of the asset
 | 
			
		||||
    pub amount: f64,            // Amount of the asset
 | 
			
		||||
    pub address: String,        // Address of the asset on the blockchain or bank
 | 
			
		||||
    pub asset_type: AssetType,  // Type of the asset
 | 
			
		||||
    pub decimals: u8,           // Number of decimals of the asset
 | 
			
		||||
    pub name: String,          // Name of the asset
 | 
			
		||||
    pub description: String,   // Description of the asset
 | 
			
		||||
    pub amount: f64,           // Amount of the asset
 | 
			
		||||
    pub address: String,       // Address of the asset on the blockchain or bank
 | 
			
		||||
    pub asset_type: AssetType, // Type of the asset
 | 
			
		||||
    pub decimals: u8,          // Number of decimals of the asset
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Asset {
 | 
			
		||||
    /// Create a new asset
 | 
			
		||||
    /// Create a new asset with auto-generated ID
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    /// * `id` - Optional ID for the asset (use None for auto-generated ID)
 | 
			
		||||
    /// * `name` - Name of the asset
 | 
			
		||||
    /// * `description` - Description of the asset
 | 
			
		||||
    /// * `amount` - Amount of the asset
 | 
			
		||||
    /// * `address` - Address of the asset on the blockchain or bank
 | 
			
		||||
    /// * `asset_type` - Type of the asset
 | 
			
		||||
    /// * `decimals` - Number of decimals of the asset
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        id: u32,
 | 
			
		||||
        id: Option<u32>,
 | 
			
		||||
        name: impl ToString,
 | 
			
		||||
        description: impl ToString,
 | 
			
		||||
        amount: f64,
 | 
			
		||||
@@ -44,8 +55,13 @@ impl Asset {
 | 
			
		||||
        asset_type: AssetType,
 | 
			
		||||
        decimals: u8,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let mut base_data = BaseModelData::new();
 | 
			
		||||
        if let Some(id) = id {
 | 
			
		||||
            base_data.update_id(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            base_data: BaseModelData::new(id),
 | 
			
		||||
            base_data,
 | 
			
		||||
            name: name.to_string(),
 | 
			
		||||
            description: description.to_string(),
 | 
			
		||||
            amount,
 | 
			
		||||
@@ -73,14 +89,14 @@ impl Asset {
 | 
			
		||||
        if amount <= 0.0 {
 | 
			
		||||
            return Err("Transfer amount must be positive");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if self.amount < amount {
 | 
			
		||||
            return Err("Insufficient balance for transfer");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        self.amount -= amount;
 | 
			
		||||
        target.amount += amount;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ use rhai::{CustomType, TypeBuilder};
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use heromodels_core::BaseModelData;
 | 
			
		||||
use heromodels_derive::model;
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use super::asset::AssetType;
 | 
			
		||||
 | 
			
		||||
@@ -55,11 +57,11 @@ impl Default for BidStatus {
 | 
			
		||||
/// Bid represents a bid on an auction listing
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
 | 
			
		||||
pub struct Bid {
 | 
			
		||||
    pub listing_id: String,  // ID of the listing this bid belongs to
 | 
			
		||||
    pub bidder_id: u32,      // ID of the user who placed the bid
 | 
			
		||||
    pub amount: f64,         // Bid amount
 | 
			
		||||
    pub currency: String,    // Currency of the bid
 | 
			
		||||
    pub status: BidStatus,   // Status of the bid
 | 
			
		||||
    pub listing_id: String,        // ID of the listing this bid belongs to
 | 
			
		||||
    pub bidder_id: u32,            // ID of the user who placed the bid
 | 
			
		||||
    pub amount: f64,               // Bid amount
 | 
			
		||||
    pub currency: String,          // Currency of the bid
 | 
			
		||||
    pub status: BidStatus,         // Status of the bid
 | 
			
		||||
    pub created_at: DateTime<Utc>, // When the bid was created
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -98,7 +100,7 @@ pub struct Listing {
 | 
			
		||||
    pub asset_id: String,
 | 
			
		||||
    pub asset_type: AssetType,
 | 
			
		||||
    pub seller_id: String,
 | 
			
		||||
    pub price: f64,             // Initial price for fixed price, or starting price for auction
 | 
			
		||||
    pub price: f64, // Initial price for fixed price, or starting price for auction
 | 
			
		||||
    pub currency: String,
 | 
			
		||||
    pub listing_type: ListingType,
 | 
			
		||||
    pub status: ListingStatus,
 | 
			
		||||
@@ -112,9 +114,23 @@ pub struct Listing {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Listing {
 | 
			
		||||
    /// Create a new listing
 | 
			
		||||
    /// Create a new listing with auto-generated ID
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    /// * `id` - Optional ID for the listing (use None for auto-generated ID)
 | 
			
		||||
    /// * `title` - Title of the listing
 | 
			
		||||
    /// * `description` - Description of the listing
 | 
			
		||||
    /// * `asset_id` - ID of the asset being listed
 | 
			
		||||
    /// * `asset_type` - Type of the asset
 | 
			
		||||
    /// * `seller_id` - ID of the seller
 | 
			
		||||
    /// * `price` - Initial price for fixed price, or starting price for auction
 | 
			
		||||
    /// * `currency` - Currency of the price
 | 
			
		||||
    /// * `listing_type` - Type of the listing
 | 
			
		||||
    /// * `expires_at` - Optional expiration date
 | 
			
		||||
    /// * `tags` - Tags for the listing
 | 
			
		||||
    /// * `image_url` - Optional image URL
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        id: u32,
 | 
			
		||||
        id: Option<u32>,
 | 
			
		||||
        title: impl ToString,
 | 
			
		||||
        description: impl ToString,
 | 
			
		||||
        asset_id: impl ToString,
 | 
			
		||||
@@ -127,8 +143,13 @@ impl Listing {
 | 
			
		||||
        tags: Vec<String>,
 | 
			
		||||
        image_url: Option<impl ToString>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let mut base_data = BaseModelData::new();
 | 
			
		||||
        if let Some(id) = id {
 | 
			
		||||
            base_data.update_id(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            base_data: BaseModelData::new(id),
 | 
			
		||||
            base_data,
 | 
			
		||||
            title: title.to_string(),
 | 
			
		||||
            description: description.to_string(),
 | 
			
		||||
            asset_id: asset_id.to_string(),
 | 
			
		||||
@@ -154,32 +175,32 @@ impl Listing {
 | 
			
		||||
        if self.listing_type != ListingType::Auction {
 | 
			
		||||
            return Err("Bids can only be placed on auction listings");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // Check if listing is active
 | 
			
		||||
        if self.status != ListingStatus::Active {
 | 
			
		||||
            return Err("Cannot place bid on inactive listing");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // Check if bid amount is higher than current price
 | 
			
		||||
        if bid.amount <= self.price {
 | 
			
		||||
            return Err("Bid amount must be higher than current price");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // Check if there are existing bids and if the new bid is higher
 | 
			
		||||
        if let Some(highest_bid) = self.highest_bid() {
 | 
			
		||||
            if bid.amount <= highest_bid.amount {
 | 
			
		||||
                return Err("Bid amount must be higher than current highest bid");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // Add the bid
 | 
			
		||||
        self.bids.push(bid);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // Update the current price to the new highest bid
 | 
			
		||||
        if let Some(highest_bid) = self.highest_bid() {
 | 
			
		||||
            self.price = highest_bid.amount;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        Ok(self)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -192,27 +213,33 @@ impl Listing {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Complete a sale (fixed price or auction)
 | 
			
		||||
    pub fn complete_sale(mut self, buyer_id: impl ToString, sale_price: f64) -> Result<Self, &'static str> {
 | 
			
		||||
    pub fn complete_sale(
 | 
			
		||||
        mut self,
 | 
			
		||||
        buyer_id: impl ToString,
 | 
			
		||||
        sale_price: f64,
 | 
			
		||||
    ) -> Result<Self, &'static str> {
 | 
			
		||||
        if self.status != ListingStatus::Active {
 | 
			
		||||
            return Err("Cannot complete sale for inactive listing");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        self.status = ListingStatus::Sold;
 | 
			
		||||
        self.buyer_id = Some(buyer_id.to_string());
 | 
			
		||||
        self.sale_price = Some(sale_price);
 | 
			
		||||
        self.sold_at = Some(Utc::now());
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // If this was an auction, accept the winning bid and reject others
 | 
			
		||||
        if self.listing_type == ListingType::Auction {
 | 
			
		||||
            for bid in &mut self.bids {
 | 
			
		||||
                if bid.bidder_id.to_string() == self.buyer_id.as_ref().unwrap().to_string() && bid.amount == sale_price {
 | 
			
		||||
                if bid.bidder_id.to_string() == self.buyer_id.as_ref().unwrap().to_string()
 | 
			
		||||
                    && bid.amount == sale_price
 | 
			
		||||
                {
 | 
			
		||||
                    bid.status = BidStatus::Accepted;
 | 
			
		||||
                } else {
 | 
			
		||||
                    bid.status = BidStatus::Rejected;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        Ok(self)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -221,16 +248,16 @@ impl Listing {
 | 
			
		||||
        if self.status != ListingStatus::Active {
 | 
			
		||||
            return Err("Cannot cancel inactive listing");
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        self.status = ListingStatus::Cancelled;
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // Cancel all active bids
 | 
			
		||||
        for bid in &mut self.bids {
 | 
			
		||||
            if bid.status == BidStatus::Active {
 | 
			
		||||
                bid.status = BidStatus::Cancelled;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        Ok(self)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -240,7 +267,7 @@ impl Listing {
 | 
			
		||||
            if let Some(expires_at) = self.expires_at {
 | 
			
		||||
                if Utc::now() > expires_at {
 | 
			
		||||
                    self.status = ListingStatus::Expired;
 | 
			
		||||
                    
 | 
			
		||||
 | 
			
		||||
                    // Cancel all active bids
 | 
			
		||||
                    for bid in &mut self.bids {
 | 
			
		||||
                        if bid.status == BidStatus::Active {
 | 
			
		||||
@@ -250,7 +277,7 @@ impl Listing {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
// heromodels/src/models/governance/proposal.rs
 | 
			
		||||
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use heromodels_derive::model; // For #[model]
 | 
			
		||||
use rhai_autobind_macros::rhai_model_export;
 | 
			
		||||
use rhai::{CustomType, TypeBuilder};
 | 
			
		||||
use rhai_autobind_macros::rhai_model_export;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use heromodels_core::BaseModelData;
 | 
			
		||||
 | 
			
		||||
@@ -13,11 +13,11 @@ use heromodels_core::BaseModelData;
 | 
			
		||||
/// ProposalStatus defines the lifecycle status of a governance proposal itself
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum ProposalStatus {
 | 
			
		||||
    Draft,    // Proposal is being prepared
 | 
			
		||||
    Active,   // Proposal is active
 | 
			
		||||
    Approved, // Proposal has been formally approved
 | 
			
		||||
    Rejected, // Proposal has been formally rejected
 | 
			
		||||
    Cancelled,// Proposal was cancelled
 | 
			
		||||
    Draft,     // Proposal is being prepared
 | 
			
		||||
    Active,    // Proposal is active
 | 
			
		||||
    Approved,  // Proposal has been formally approved
 | 
			
		||||
    Rejected,  // Proposal has been formally rejected
 | 
			
		||||
    Cancelled, // Proposal was cancelled
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for ProposalStatus {
 | 
			
		||||
@@ -26,7 +26,6 @@ impl Default for ProposalStatus {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// VoteEventStatus represents the status of the voting process for a proposal
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 | 
			
		||||
pub enum VoteEventStatus {
 | 
			
		||||
@@ -46,19 +45,21 @@ impl Default for VoteEventStatus {
 | 
			
		||||
/// VoteOption represents a specific choice that can be voted on
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
 | 
			
		||||
pub struct VoteOption {
 | 
			
		||||
    pub id: u8,          // Simple identifier for this option
 | 
			
		||||
    pub text: String,    // Descriptive text of the option
 | 
			
		||||
    pub count: i64,      // How many votes this option has received
 | 
			
		||||
    pub min_valid: Option<i64>, // Optional: minimum votes needed
 | 
			
		||||
    pub id: u8,                  // Simple identifier for this option
 | 
			
		||||
    pub text: String,            // Descriptive text of the option
 | 
			
		||||
    pub count: i64,              // How many votes this option has received
 | 
			
		||||
    pub min_valid: Option<i64>,  // Optional: minimum votes needed,
 | 
			
		||||
    pub comment: Option<String>, // Optional: comment
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl VoteOption {
 | 
			
		||||
    pub fn new(id: u8, text: impl ToString) -> Self {
 | 
			
		||||
    pub fn new(id: u8, text: impl ToString, comment: Option<impl ToString>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            id,
 | 
			
		||||
            text: text.to_string(),
 | 
			
		||||
            count: 0,
 | 
			
		||||
            min_valid: None,
 | 
			
		||||
            comment: comment.map(|c| c.to_string()),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -69,34 +70,52 @@ impl VoteOption {
 | 
			
		||||
#[model] // Has base.Base in V spec
 | 
			
		||||
pub struct Ballot {
 | 
			
		||||
    pub base_data: BaseModelData,
 | 
			
		||||
    pub user_id: u32,          // The ID of the user who cast this ballot
 | 
			
		||||
    pub vote_option_id: u8,    // The 'id' of the VoteOption chosen
 | 
			
		||||
    pub shares_count: i64,     // Number of shares/tokens/voting power
 | 
			
		||||
    pub user_id: u32,       // The ID of the user who cast this ballot
 | 
			
		||||
    pub vote_option_id: u8, // The 'id' of the VoteOption chosen
 | 
			
		||||
    pub shares_count: i64,  // Number of shares/tokens/voting power
 | 
			
		||||
    pub comment: Option<String>, // Optional comment from the voter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Ballot {
 | 
			
		||||
    pub fn new(id: u32, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self {
 | 
			
		||||
    /// Create a new ballot with auto-generated ID
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    /// * `id` - Optional ID for the ballot (use None for auto-generated ID)
 | 
			
		||||
    /// * `user_id` - ID of the user who cast this ballot
 | 
			
		||||
    /// * `vote_option_id` - ID of the vote option chosen
 | 
			
		||||
    /// * `shares_count` - Number of shares/tokens/voting power
 | 
			
		||||
    pub fn new(id: Option<u32>, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self {
 | 
			
		||||
        let mut base_data = BaseModelData::new();
 | 
			
		||||
        if let Some(id) = id {
 | 
			
		||||
            base_data.update_id(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            base_data: BaseModelData::new(id),
 | 
			
		||||
            base_data,
 | 
			
		||||
            user_id,
 | 
			
		||||
            vote_option_id,
 | 
			
		||||
            shares_count,
 | 
			
		||||
            comment: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Proposal represents a governance proposal that can be voted upon.
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
 | 
			
		||||
#[rhai_model_export(db_type = "std::sync::Arc<crate::db::hero::OurDB>")]
 | 
			
		||||
#[model] // Has base.Base in V spec
 | 
			
		||||
pub struct Proposal {
 | 
			
		||||
    pub base_data: BaseModelData,
 | 
			
		||||
    pub creator_id: String, // User ID of the proposal creator
 | 
			
		||||
    pub creator_id: String,   // User ID of the proposal creator
 | 
			
		||||
    pub creator_name: String, // User name of the proposal creator
 | 
			
		||||
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub status: ProposalStatus,
 | 
			
		||||
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
 | 
			
		||||
    // Voting event aspects
 | 
			
		||||
    pub vote_start_date: DateTime<Utc>,
 | 
			
		||||
    pub vote_end_date: DateTime<Utc>,
 | 
			
		||||
@@ -107,13 +126,41 @@ pub struct Proposal {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Proposal {
 | 
			
		||||
    pub fn new(id: u32, creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime<Utc>, vote_end_date: DateTime<Utc>) -> Self {
 | 
			
		||||
    /// Create a new proposal with auto-generated ID
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    /// * `id` - Optional ID for the proposal (use None for auto-generated ID)
 | 
			
		||||
    /// * `creator_id` - ID of the user who created the proposal
 | 
			
		||||
    /// * `title` - Title of the proposal
 | 
			
		||||
    /// * `description` - Description of the proposal
 | 
			
		||||
    /// * `vote_start_date` - Date when voting starts
 | 
			
		||||
    /// * `vote_end_date` - Date when voting ends
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        id: Option<u32>,
 | 
			
		||||
        creator_id: impl ToString,
 | 
			
		||||
        creator_name: impl ToString,
 | 
			
		||||
        title: impl ToString,
 | 
			
		||||
        description: impl ToString,
 | 
			
		||||
        status: ProposalStatus,
 | 
			
		||||
        created_at: DateTime<Utc>,
 | 
			
		||||
        updated_at: DateTime<Utc>,
 | 
			
		||||
        vote_start_date: DateTime<Utc>,
 | 
			
		||||
        vote_end_date: DateTime<Utc>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let mut base_data = BaseModelData::new();
 | 
			
		||||
        if let Some(id) = id {
 | 
			
		||||
            base_data.update_id(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            base_data: BaseModelData::new(id),
 | 
			
		||||
            base_data,
 | 
			
		||||
            creator_id: creator_id.to_string(),
 | 
			
		||||
            creator_name: creator_name.to_string(),
 | 
			
		||||
            title: title.to_string(),
 | 
			
		||||
            description: description.to_string(),
 | 
			
		||||
            status: ProposalStatus::Draft,
 | 
			
		||||
            status,
 | 
			
		||||
            created_at,
 | 
			
		||||
            updated_at,
 | 
			
		||||
            vote_start_date,
 | 
			
		||||
            vote_end_date,
 | 
			
		||||
            vote_status: VoteEventStatus::Open, // Default to open when created
 | 
			
		||||
@@ -123,24 +170,41 @@ impl Proposal {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_option(mut self, option_id: u8, option_text: impl ToString) -> Self {
 | 
			
		||||
        let new_option = VoteOption::new(option_id, option_text);
 | 
			
		||||
    pub fn add_option(
 | 
			
		||||
        mut self,
 | 
			
		||||
        option_id: u8,
 | 
			
		||||
        option_text: impl ToString,
 | 
			
		||||
        comment: Option<impl ToString>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let new_option = VoteOption::new(option_id, option_text, comment);
 | 
			
		||||
        self.options.push(new_option);
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    pub fn cast_vote(mut self, ballot_id: u32, user_id: u32, chosen_option_id: u8, shares: i64) -> Self {
 | 
			
		||||
 | 
			
		||||
    pub fn cast_vote(
 | 
			
		||||
        mut self,
 | 
			
		||||
        ballot_id: Option<u32>,
 | 
			
		||||
        user_id: u32,
 | 
			
		||||
        chosen_option_id: u8,
 | 
			
		||||
        shares: i64,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        if self.vote_status != VoteEventStatus::Open {
 | 
			
		||||
            eprintln!("Voting is not open for proposal '{}'", self.title);
 | 
			
		||||
            return self;
 | 
			
		||||
        }
 | 
			
		||||
        if !self.options.iter().any(|opt| opt.id == chosen_option_id) {
 | 
			
		||||
            eprintln!("Chosen option ID {} does not exist for proposal '{}'", chosen_option_id, self.title);
 | 
			
		||||
            eprintln!(
 | 
			
		||||
                "Chosen option ID {} does not exist for proposal '{}'",
 | 
			
		||||
                chosen_option_id, self.title
 | 
			
		||||
            );
 | 
			
		||||
            return self;
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(group) = &self.private_group {
 | 
			
		||||
            if !group.contains(&user_id) {
 | 
			
		||||
                eprintln!("User {} is not eligible to vote on proposal '{}'", user_id, self.title);
 | 
			
		||||
                eprintln!(
 | 
			
		||||
                    "User {} is not eligible to vote on proposal '{}'",
 | 
			
		||||
                    user_id, self.title
 | 
			
		||||
                );
 | 
			
		||||
                return self;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -148,7 +212,11 @@ impl Proposal {
 | 
			
		||||
        let new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares);
 | 
			
		||||
        self.ballots.push(new_ballot);
 | 
			
		||||
 | 
			
		||||
        if let Some(option) = self.options.iter_mut().find(|opt| opt.id == chosen_option_id) {
 | 
			
		||||
        if let Some(option) = self
 | 
			
		||||
            .options
 | 
			
		||||
            .iter_mut()
 | 
			
		||||
            .find(|opt| opt.id == chosen_option_id)
 | 
			
		||||
        {
 | 
			
		||||
            option.count += shares;
 | 
			
		||||
        }
 | 
			
		||||
        self
 | 
			
		||||
@@ -163,4 +231,65 @@ impl Proposal {
 | 
			
		||||
        self.vote_status = new_status;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Cast a vote with a comment
 | 
			
		||||
    ///
 | 
			
		||||
    /// # Arguments
 | 
			
		||||
    /// * `ballot_id` - Optional ID for the ballot (use None for auto-generated ID)
 | 
			
		||||
    /// * `user_id` - ID of the user who is casting the vote
 | 
			
		||||
    /// * `chosen_option_id` - ID of the vote option chosen
 | 
			
		||||
    /// * `shares` - Number of shares/tokens/voting power
 | 
			
		||||
    /// * `comment` - Comment from the voter explaining their vote
 | 
			
		||||
    pub fn cast_vote_with_comment(
 | 
			
		||||
        mut self,
 | 
			
		||||
        ballot_id: Option<u32>,
 | 
			
		||||
        user_id: u32,
 | 
			
		||||
        chosen_option_id: u8,
 | 
			
		||||
        shares: i64,
 | 
			
		||||
        comment: impl ToString,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        // First check if voting is open
 | 
			
		||||
        if self.vote_status != VoteEventStatus::Open {
 | 
			
		||||
            eprintln!("Voting is not open for proposal '{}'", self.title);
 | 
			
		||||
            return self;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Check if the option exists
 | 
			
		||||
        if !self.options.iter().any(|opt| opt.id == chosen_option_id) {
 | 
			
		||||
            eprintln!(
 | 
			
		||||
                "Chosen option ID {} does not exist for proposal '{}'",
 | 
			
		||||
                chosen_option_id, self.title
 | 
			
		||||
            );
 | 
			
		||||
            return self;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Check eligibility for private proposals
 | 
			
		||||
        if let Some(group) = &self.private_group {
 | 
			
		||||
            if !group.contains(&user_id) {
 | 
			
		||||
                eprintln!(
 | 
			
		||||
                    "User {} is not eligible to vote on proposal '{}'",
 | 
			
		||||
                    user_id, self.title
 | 
			
		||||
                );
 | 
			
		||||
                return self;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Create a new ballot with the comment
 | 
			
		||||
        let mut new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares);
 | 
			
		||||
        new_ballot.comment = Some(comment.to_string());
 | 
			
		||||
        
 | 
			
		||||
        // Add the ballot to the proposal
 | 
			
		||||
        self.ballots.push(new_ballot);
 | 
			
		||||
        
 | 
			
		||||
        // Update the vote count for the chosen option
 | 
			
		||||
        if let Some(option) = self
 | 
			
		||||
            .options
 | 
			
		||||
            .iter_mut()
 | 
			
		||||
            .find(|opt| opt.id == chosen_option_id)
 | 
			
		||||
        {
 | 
			
		||||
            option.count += shares;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,10 +26,10 @@ pub struct User {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl User {
 | 
			
		||||
    /// Create a new user
 | 
			
		||||
    pub fn new(id: u32) -> Self {
 | 
			
		||||
    /// Create a new user with auto-generated ID
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            base_data: BaseModelData::new(id),
 | 
			
		||||
            base_data: BaseModelData::new(),
 | 
			
		||||
            username: String::new(),
 | 
			
		||||
            email: String::new(),
 | 
			
		||||
            full_name: String::new(),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user