Files
oxspeak_server/src/services/server_order.rs
T
2026-08-02 10:13:49 +02:00

319 lines
9.4 KiB
Rust

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<ServicesContext>,
}
impl ServerOrderService {
pub fn new(service_context: Arc<ServicesContext>) -> 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<Uuid>,
) -> Result<server_item_order::Model, anyhow::Error> {
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<Uuid>,
parent_category_id: Option<Uuid>,
) -> Result<Option<server_item_order::Model>, 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<Uuid>,
) -> Result<i64, anyhow::Error> {
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::<Option<i64>>()
.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()
);
}
}