// // Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) // // This file is part of the Bichon Email Archiving Project // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use crate::common::paginated::Paginated; use crate::error::code::ErrorCode; use crate::error::BichonResult; use crate::raise_error; use bichon_memdb::{MemDb, Transaction}; use serde::de::DeserializeOwned; use serde::Serialize; pub mod manager; /// Trait for models that can be stored in MemDb collections. pub trait MemDbModel: Serialize + DeserializeOwned + Clone + Send + 'static { /// The collection name this model is stored under. fn collection() -> &'static str; /// The primary key as a string for MemDb storage. fn key(&self) -> String; } // ─── Insert ─────────────────────────────────────────────────────────────── pub fn insert_impl(db: &MemDb, item: M) -> BichonResult<()> { let coll = db.collection(M::collection()); let key = item.key(); coll.insert(key, &item) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) } pub fn batch_insert_impl(db: &MemDb, items: Vec) -> BichonResult<()> { let txn = db.transaction(); let mut txn = txn; for item in &items { txn = txn .insert(M::collection(), item.key(), item) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; } txn.commit() .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) } // ─── Upsert ──────────────────────────────────────────────────────────────── pub fn upsert_impl(db: &MemDb, item: M) -> BichonResult<()> { let coll = db.collection(M::collection()); coll.upsert(item.key(), &item) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) } pub fn batch_upsert_impl(db: &MemDb, items: Vec) -> BichonResult<()> { let txn = db.transaction(); let mut txn = txn; for item in &items { txn = txn .upsert(M::collection(), item.key(), item) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; } txn.commit() .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) } // ─── Find ────────────────────────────────────────────────────────────────── pub fn find_impl(db: &MemDb, key: &str) -> BichonResult> { let coll = db.collection(M::collection()); coll.get(key) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) } // ─── Filter (replaces secondary key queries) ────────────────────────────── pub fn filter_impl(db: &MemDb, predicate: F) -> BichonResult> where M: MemDbModel, F: Fn(&M) -> bool + Send + 'static, { let coll = db.collection(M::collection()); coll.filter(predicate) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) } // ─── Update (read-modify-write under a single spawn_blocking) ───────────── pub fn update_impl( db: &MemDb, key: &str, update_fn: impl FnOnce(M) -> BichonResult + Send + 'static, ) -> BichonResult { let coll = db.collection(M::collection()); let current: M = coll .get_required(key) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; let updated = update_fn(current)?; coll.upsert(key, &updated) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; Ok(updated) } // ─── Delete ──────────────────────────────────────────────────────────────── pub fn delete_impl(db: &MemDb, key: &str) -> BichonResult<()> { let coll = db.collection(M::collection()); let existed = coll .delete(key) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; if !existed { return Err(raise_error!( format!("{} '{}' not found for deletion", M::collection(), key), ErrorCode::ResourceNotFound )); } Ok(()) } pub fn batch_delete_impl(db: &MemDb, keys: Vec) -> BichonResult { let txn = db.transaction(); let mut txn = txn; let mut count = 0usize; for key in &keys { txn = txn.delete(M::collection(), key.clone()); count += 1; } txn.commit() .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; Ok(count) } // ─── List / Count ────────────────────────────────────────────────────────── pub fn list_all_impl(db: &MemDb) -> BichonResult> { let coll = db.collection(M::collection()); coll.list_all() .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) } pub fn count_impl(db: &MemDb) -> BichonResult { let coll = db.collection(M::collection()); Ok(coll.count()) } // ─── Paginate ────────────────────────────────────────────────────────────── pub fn paginate_impl( db: &MemDb, page: Option, page_size: Option, desc: Option, ) -> BichonResult> { let coll = db.collection(M::collection()); let total_items = coll.count() as u64; let (offset, total_pages) = match (page, page_size) { (Some(p), Some(s)) if p > 0 && s > 0 => { let offset = (p - 1) * s; let total_pages = if total_items > 0 { (total_items as f64 / s as f64).ceil() as u64 } else { 0 }; (Some(offset), Some(total_pages)) } (Some(0), _) | (_, Some(0)) => { return Err(raise_error!( "'page' and 'page_size' must be greater than 0.".into(), ErrorCode::InvalidParameter )); } _ => (None, None), }; let all: Vec = coll .list_all() .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; let items: Vec = match desc { Some(true) => { let iter: Vec = all.into_iter().rev().collect(); let skip = offset.unwrap_or(0) as usize; let take = page_size.unwrap_or(total_items) as usize; iter.into_iter().skip(skip).take(take).collect() } _ => { let skip = offset.unwrap_or(0) as usize; let take = page_size.unwrap_or(total_items) as usize; all.into_iter().skip(skip).take(take).collect() } }; Ok(Paginated::new( page, page_size, total_items, total_pages, items, )) } // ─── Transaction ─────────────────────────────────────────────────────────── /// Execute operations within a single atomic transaction (one WAL entry). pub fn with_transaction( db: &MemDb, f: impl FnOnce(Transaction) -> BichonResult + Send + 'static, ) -> BichonResult<()> { let txn = db.transaction(); let txn = f(txn)?; txn.commit() .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError)) }