This commit is contained in:
2026-07-25 01:17:19 +02:00
parent 62f7c6edba
commit b54b60c988
12 changed files with 224 additions and 98 deletions
+81 -13
View File
@@ -243,12 +243,6 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(Alias::new("server_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("name")).string().not_null())
.col(
ColumnDef::new(Alias::new("position"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
@@ -285,12 +279,6 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(Alias::new("server_id")).uuid().null())
.col(ColumnDef::new(Alias::new("category_id")).uuid().null())
.col(
ColumnDef::new(Alias::new("position"))
.integer()
.not_null()
.default(0),
)
.col(
ColumnDef::new(Alias::new("channel_type"))
.integer()
@@ -327,6 +315,86 @@ impl MigrationTrait for Migration {
)
.await?;
// ---------------------------------------------------------------------
// Ordre des catégories et des canaux (Polymorphique)
// ---------------------------------------------------------------------
manager
.create_table(
Table::create()
.table(Alias::new("server_item_order"))
.if_not_exists()
.col(
ColumnDef::new(Alias::new("id"))
.uuid()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Alias::new("server_id")).uuid().not_null())
.col(ColumnDef::new(Alias::new("resource_id")).uuid().not_null())
.col(
ColumnDef::new(Alias::new("resource_type"))
.integer()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("parent_category_id"))
.uuid()
.null(),
)
.col(
ColumnDef::new(Alias::new("order_key"))
.big_integer()
.not_null(),
)
.col(
ColumnDef::new(Alias::new("created_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new(Alias::new("updated_at"))
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.foreign_key(
ForeignKey::create()
.name("fk_server_item_order_server")
.from(Alias::new("server_item_order"), Alias::new("server_id"))
.to(Alias::new("server"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.name("fk_server_item_order_parent_category")
.from(
Alias::new("server_item_order"),
Alias::new("parent_category_id"),
)
.to(Alias::new("category"), Alias::new("id"))
.on_delete(ForeignKeyAction::Cascade),
)
.index(
Index::create()
.name("uq_server_item_order_resource")
.col(Alias::new("server_id"))
.col(Alias::new("resource_type"))
.col(Alias::new("resource_id"))
.unique(),
)
.index(
Index::create()
.name("idx_server_item_order_scope")
.col(Alias::new("server_id"))
.col(Alias::new("parent_category_id"))
.col(Alias::new("order_key")),
)
.to_owned(),
)
.await?;
// ---------------------------------------------------------------------
// Membres des canaux
// ---------------------------------------------------------------------
@@ -716,7 +784,6 @@ impl MigrationTrait for Migration {
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Les tables dépendantes doivent être supprimées avant leurs parents.
let tables = [
"computed_permission",
"channel_user_permission",
@@ -726,6 +793,7 @@ impl MigrationTrait for Migration {
"attachment",
"message",
"channel_user",
"server_item_order",
"channel",
"category",
"role_user",
+1 -2
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
@@ -12,7 +12,6 @@ pub struct Model {
pub id: Uuid,
pub server_id: Uuid,
pub name: String,
pub position: i32,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(has_many)]
+1 -2
View File
@@ -1,8 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
use sea_orm::Set;
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::Set;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
@@ -28,7 +28,6 @@ pub struct Model {
pub id: Uuid,
pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>,
pub position: i32,
pub channel_type: ChannelType,
pub name: Option<String>,
pub created_at: DateTimeUtc,
+1
View File
@@ -13,6 +13,7 @@ pub mod message;
pub mod role;
pub mod role_user;
pub mod server;
pub mod server_item_order;
pub mod server_role_permission;
pub mod server_user;
pub mod server_user_permission;
+1
View File
@@ -9,6 +9,7 @@ pub use super::message::Entity as Message;
pub use super::role::Entity as Group;
pub use super::role_user::Entity as GroupMember;
pub use super::server::Entity as Server;
pub use super::server_item_order::Entity as ServerItemOrder;
pub use super::server_role_permission::Entity as ServerRolePermission;
pub use super::server_user::Entity as ServerUser;
pub use super::server_user_permission::Entity as ServerUserPermission;
+74
View File
@@ -0,0 +1,74 @@
use sea_orm::Set;
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
)]
#[sea_orm(rs_type = "i32", db_type = "Integer")]
#[serde(rename_all = "snake_case")]
pub enum OrderedResourceType {
#[sea_orm(num_value = 0)]
Channel,
#[sea_orm(num_value = 1)]
Category,
}
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "server_item_order")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub server_id: Uuid,
/// UUID du channel ou de la catégorie ordonné(e).
pub resource_id: Uuid,
/// Type de la ressource référencée par resource_id.
pub resource_type: OrderedResourceType,
/// NULL pour la liste racine du serveur.
/// Renseigné uniquement pour un channel placé dans une catégorie.
pub parent_category_id: Option<Uuid>,
/// Clé de tri relative à (server_id, parent_category_id).
pub order_key: i64,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
#[sea_orm(
belongs_to,
from = "server_id",
to = "id",
on_update = "Cascade",
on_delete = "Cascade"
)]
pub server: HasOne<super::server::Entity>,
#[sea_orm(
belongs_to,
from = "parent_category_id",
to = "id",
on_update = "Cascade",
on_delete = "Cascade"
)]
pub parent_category: HasOne<super::category::Entity>,
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
}
+57 -59
View File
@@ -1,7 +1,5 @@
use super::types::{ServerExplorerItem, ServerTree};
use super::{AnyResult, RepositoryContext};
use crate::models::{
category, channel, role, server, server_role_permission, server_user, server_user_permission,
use crate::models::{role, server, server_role_permission, server_user, server_user_permission,
};
use sea_orm::prelude::*;
use sea_orm::{ActiveModelTrait, QuerySelect, Set};
@@ -204,59 +202,59 @@ impl ServerRepository {
}
// Helpers
impl ServerRepository {
pub async fn get_tree(&self, server_id: Uuid) -> AnyResult<ServerTree> {
// 1. Récupération des catégories avec leurs channels
let categories_with_channels = category::Entity::find()
.filter(category::Column::ServerId.eq(server_id))
.find_with_related(channel::Entity)
.all(&self.context.db)
.await?;
// 2. Récupération des channels orphelins (sans catégorie)
let orphan_channels = channel::Entity::find()
.filter(channel::Column::ServerId.eq(server_id))
.filter(channel::Column::CategoryId.is_null())
.all(&self.context.db)
.await?;
// 3. Transformation et tri des enfants
let mut items: Vec<ServerExplorerItem> = Vec::new();
for (cat, mut channels) in categories_with_channels {
// On trie les channels internes (obligatoire car SQL ne garantit aucun ordre ici)
channels.sort_by(|a, b| {
a.position
.cmp(&b.position)
.then(a.created_at.cmp(&b.created_at))
});
items.push(ServerExplorerItem::Category(cat, channels));
}
for chan in orphan_channels {
items.push(ServerExplorerItem::Channel(chan));
}
// 4. Tri final de la liste globale (Mélange catégories et orphelins)
items.sort_by(|a, b| {
let pos_cmp = a.position().cmp(&b.position());
if pos_cmp == std::cmp::Ordering::Equal {
// Départage par date si position identique
let date_a = match a {
ServerExplorerItem::Category(c, _) => c.created_at,
ServerExplorerItem::Channel(c) => c.created_at,
};
let date_b = match b {
ServerExplorerItem::Category(c, _) => c.created_at,
ServerExplorerItem::Channel(c) => c.created_at,
};
date_a.cmp(&date_b)
} else {
pos_cmp
}
});
Ok(ServerTree { items })
}
}
// impl ServerRepository {
// pub async fn get_tree(&self, server_id: Uuid) -> AnyResult<ServerTree> {
// // 1. Récupération des catégories avec leurs channels
// let categories_with_channels = category::Entity::find()
// .filter(category::Column::ServerId.eq(server_id))
// .find_with_related(channel::Entity)
// .all(&self.context.db)
// .await?;
//
// // 2. Récupération des channels orphelins (sans catégorie)
// let orphan_channels = channel::Entity::find()
// .filter(channel::Column::ServerId.eq(server_id))
// .filter(channel::Column::CategoryId.is_null())
// .all(&self.context.db)
// .await?;
//
// // 3. Transformation et tri des enfants
// let mut items: Vec<ServerExplorerItem> = Vec::new();
//
// for (cat, mut channels) in categories_with_channels {
// // On trie les channels internes (obligatoire car SQL ne garantit aucun ordre ici)
// channels.sort_by(|a, b| {
// a.position
// .cmp(&b.position)
// .then(a.created_at.cmp(&b.created_at))
// });
// items.push(ServerExplorerItem::Category(cat, channels));
// }
//
// for chan in orphan_channels {
// items.push(ServerExplorerItem::Channel(chan));
// }
//
// // 4. Tri final de la liste globale (Mélange catégories et orphelins)
// items.sort_by(|a, b| {
// let pos_cmp = a.position().cmp(&b.position());
//
// if pos_cmp == std::cmp::Ordering::Equal {
// // Départage par date si position identique
// let date_a = match a {
// ServerExplorerItem::Category(c, _) => c.created_at,
// ServerExplorerItem::Channel(c) => c.created_at,
// };
// let date_b = match b {
// ServerExplorerItem::Category(c, _) => c.created_at,
// ServerExplorerItem::Channel(c) => c.created_at,
// };
// date_a.cmp(&date_b)
// } else {
// pos_cmp
// }
// });
//
// Ok(ServerTree { items })
// }
// }
+8 -8
View File
@@ -6,14 +6,14 @@ pub enum ServerExplorerItem {
}
// Pour pouvoir trier facilement
impl ServerExplorerItem {
pub fn position(&self) -> i32 {
match self {
ServerExplorerItem::Category(cat, _) => cat.position,
ServerExplorerItem::Channel(chan) => chan.position,
}
}
}
// impl ServerExplorerItem {
// pub fn position(&self) -> i32 {
// match self {
// ServerExplorerItem::Category(cat, _) => cat.position,
// ServerExplorerItem::Channel(chan) => chan.position,
// }
// }
// }
pub struct ServerTree {
pub items: Vec<ServerExplorerItem>,
-4
View File
@@ -8,15 +8,12 @@ pub struct CreateCategoryRequest {
pub server_id: Uuid,
#[schema(example = "Discussion")]
pub name: String,
#[serde(default)]
pub position: i32,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UpdateCategoryRequest {
#[schema(example = "Discussion (Maj)")]
pub name: String,
pub position: i32,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
@@ -24,7 +21,6 @@ pub struct CategoryResponse {
pub id: Uuid,
pub server_id: Uuid,
pub name: String,
pub position: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
-3
View File
@@ -10,7 +10,6 @@ pub fn category_model_to_category_response(model: category::Model) -> CategoryRe
id: model.id,
server_id: model.server_id,
name: model.name,
position: model.position,
created_at: model.created_at,
updated_at: model.updated_at,
}
@@ -21,7 +20,6 @@ pub fn create_request_to_am(req: CreateCategoryRequest) -> category::ActiveModel
id: Set(Uuid::new_v4()),
server_id: Set(req.server_id),
name: Set(req.name),
position: Set(req.position),
..Default::default()
}
}
@@ -35,7 +33,6 @@ pub fn update_request_to_am(
id: Set(id),
server_id: Set(server_id),
name: Set(req.name),
position: Set(req.position),
..Default::default()
}
}
-4
View File
@@ -8,8 +8,6 @@ use uuid::Uuid;
pub struct CreateChannelRequest {
pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>,
#[serde(default)]
pub position: i32,
pub channel_type: ChannelType,
#[schema(example = "général")]
pub name: Option<String>,
@@ -19,7 +17,6 @@ pub struct CreateChannelRequest {
pub struct UpdateChannelRequest {
pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>,
pub position: i32,
pub channel_type: ChannelType,
pub name: Option<String>,
}
@@ -29,7 +26,6 @@ pub struct ChannelResponse {
pub id: Uuid,
pub server_id: Option<Uuid>,
pub category_id: Option<Uuid>,
pub position: i32,
pub channel_type: ChannelType,
pub name: Option<String>,
pub created_at: DateTime<Utc>,
-3
View File
@@ -11,7 +11,6 @@ pub fn channel_model_to_channel_response(model: channel::Model) -> ChannelRespon
id: model.id,
server_id: model.server_id,
category_id: model.category_id,
position: model.position,
channel_type: model.channel_type,
name: model.name,
created_at: model.created_at,
@@ -24,7 +23,6 @@ pub fn create_request_to_am(req: CreateChannelRequest) -> channel::ActiveModel {
id: Set(Uuid::new_v4()),
server_id: Set(req.server_id),
category_id: Set(req.category_id),
position: Set(req.position),
channel_type: Set(req.channel_type),
name: Set(req.name),
..Default::default()
@@ -36,7 +34,6 @@ pub fn update_request_to_am(id: Uuid, req: UpdateChannelRequest) -> channel::Act
id: Set(id),
server_id: Set(req.server_id),
category_id: Set(req.category_id),
position: Set(req.position),
channel_type: Set(req.channel_type),
name: Set(req.name),
..Default::default()