use crate::models::server_item_order::{self, OrderedResourceType}; use crate::services::ServicesContext; use sea_orm::{ ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter, QuerySelect, Set, }; use std::sync::Arc; use uuid::Uuid; #[derive(Debug, Clone)] pub struct ServerOrderService { service_context: Arc, } impl ServerOrderService { pub fn new(service_context: Arc) -> Self { Self { service_context } } pub(crate) async fn append( &self, txn: &DatabaseTransaction, server_id: Uuid, resource_id: Uuid, resource_type: OrderedResourceType, parent_category_id: Option, ) -> Result { self.sync( txn, resource_id, resource_type, Some(server_id), parent_category_id, ) .await? .ok_or_else(|| anyhow::anyhow!("Unable to create server order")) } pub(crate) async fn sync( &self, txn: &DatabaseTransaction, resource_id: Uuid, resource_type: OrderedResourceType, server_id: Option, parent_category_id: Option, ) -> Result, anyhow::Error> { let existing = server_item_order::Entity::find() .filter(server_item_order::Column::ResourceId.eq(resource_id)) .filter(server_item_order::Column::ResourceType.eq(resource_type)) .one(txn) .await?; let Some(server_id) = server_id else { if let Some(order) = existing { server_item_order::Entity::delete_by_id(order.id) .exec(txn) .await?; } return Ok(None); }; let order_key = match existing.as_ref() { Some(order) if order.server_id == server_id && order.parent_category_id == parent_category_id => { order.order_key } _ => { self.next_order_key(txn, server_id, parent_category_id) .await? } }; let order = if let Some(order) = existing { let mut active: server_item_order::ActiveModel = order.into(); active.server_id = Set(server_id); active.parent_category_id = Set(parent_category_id); active.order_key = Set(order_key); active.update(txn).await? } else { server_item_order::ActiveModel { server_id: Set(server_id), resource_id: Set(resource_id), resource_type: Set(resource_type), parent_category_id: Set(parent_category_id), order_key: Set(order_key), ..Default::default() } .insert(txn) .await? }; Ok(Some(order)) } pub(crate) async fn remove( &self, txn: &DatabaseTransaction, resource_id: Uuid, resource_type: OrderedResourceType, ) -> Result<(), anyhow::Error> { server_item_order::Entity::delete_many() .filter(server_item_order::Column::ResourceId.eq(resource_id)) .filter(server_item_order::Column::ResourceType.eq(resource_type)) .exec(txn) .await?; Ok(()) } async fn next_order_key( &self, txn: &DatabaseTransaction, server_id: Uuid, parent_category_id: Option, ) -> Result { let mut query = server_item_order::Entity::find() .filter(server_item_order::Column::ServerId.eq(server_id)); query = match parent_category_id { Some(category_id) => { query.filter(server_item_order::Column::ParentCategoryId.eq(category_id)) } None => query.filter(server_item_order::Column::ParentCategoryId.is_null()), }; let max_order = query .select_only() .column_as(server_item_order::Column::OrderKey.max(), "max_key") .into_tuple::>() .one(txn) .await? .flatten() .unwrap_or(0); Ok(max_order + 1) } pub async fn exemple_call_other_service(&self) { if let Some(services) = self.service_context.services.get() { services.permission_sync.start_listen_event().await; } } } #[cfg(test)] mod tests { use super::*; use crate::models::{category, server}; use crate::repositories::Repositories; use event_bus::EventBus; use migration::{Migrator, MigratorTrait}; use sea_orm::{ ActiveModelTrait, ColumnTrait, Database, DatabaseConnection, EntityTrait, Set, TransactionTrait, }; async fn service() -> ServerOrderService { let db = Database::connect("sqlite::memory:").await.unwrap(); Migrator::up(&db, None).await.unwrap(); let context = Arc::new(ServicesContext { repositories: Arc::new(Repositories::new(db)), event_bus: Arc::new(EventBus::new()), services: std::sync::OnceLock::new(), }); ServerOrderService::new(context) } async fn seed_server(db: &DatabaseConnection, server_id: Uuid) { server::ActiveModel { id: Set(server_id), name: Set("test".to_owned()), is_default: Set(false), ..Default::default() } .insert(db) .await .unwrap(); } #[tokio::test] async fn sync_uses_scoped_keys_and_preserves_position() { let service = service().await; let db = &service.service_context.repositories.server.context.db; let server_id = Uuid::new_v4(); let category_id = Uuid::new_v4(); let category_resource_id = Uuid::new_v4(); let root_channel_id = Uuid::new_v4(); let nested_channel_id = Uuid::new_v4(); seed_server(db, server_id).await; category::ActiveModel { id: Set(category_id), server_id: Set(server_id), name: Set("category".to_owned()), ..Default::default() } .insert(db) .await .unwrap(); let txn = db.begin().await.unwrap(); service .append( &txn, server_id, category_resource_id, OrderedResourceType::Category, None, ) .await .unwrap(); service .append( &txn, server_id, root_channel_id, OrderedResourceType::Channel, None, ) .await .unwrap(); service .append( &txn, server_id, nested_channel_id, OrderedResourceType::Channel, Some(category_id), ) .await .unwrap(); txn.commit().await.unwrap(); let txn = db.begin().await.unwrap(); let unchanged = service .sync( &txn, nested_channel_id, OrderedResourceType::Channel, Some(server_id), Some(category_id), ) .await .unwrap() .unwrap(); assert_eq!(unchanged.order_key, 1); let moved = service .sync( &txn, nested_channel_id, OrderedResourceType::Channel, Some(server_id), None, ) .await .unwrap() .unwrap(); assert_eq!(moved.order_key, 3); txn.commit().await.unwrap(); } #[tokio::test] async fn sync_none_removes_order_and_rollback_is_atomic() { let service = service().await; let db = &service.service_context.repositories.server.context.db; let server_id = Uuid::new_v4(); let resource_id = Uuid::new_v4(); seed_server(db, server_id).await; let txn = db.begin().await.unwrap(); service .append( &txn, server_id, resource_id, OrderedResourceType::Channel, None, ) .await .unwrap(); txn.rollback().await.unwrap(); assert!( server_item_order::Entity::find() .filter(server_item_order::Column::ResourceId.eq(resource_id)) .one(db) .await .unwrap() .is_none() ); let txn = db.begin().await.unwrap(); service .append( &txn, server_id, resource_id, OrderedResourceType::Channel, None, ) .await .unwrap(); service .sync(&txn, resource_id, OrderedResourceType::Channel, None, None) .await .unwrap(); txn.commit().await.unwrap(); assert!( server_item_order::Entity::find() .filter(server_item_order::Column::ResourceId.eq(resource_id)) .one(db) .await .unwrap() .is_none() ); } }