This commit is contained in:
2026-08-08 14:17:24 +02:00
parent e9fe51363f
commit 0d8c86af16
12 changed files with 835 additions and 151 deletions
+12 -12
View File
@@ -14,6 +14,15 @@ pub struct MessageResponse {
pub reply_to_id: Option<Uuid>,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct MessagePageResponse {
pub messages: Vec<MessageResponse>,
pub oldest_id: Option<Uuid>,
pub newest_id: Option<Uuid>,
pub has_more_before: bool,
pub has_more_after: bool,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct CreateMessageRequest {
pub channel_id: Uuid,
@@ -26,19 +35,10 @@ pub struct UpdateMessageRequest {
pub content: String,
}
#[derive(serde::Deserialize, utoipa::IntoParams)]
#[derive(Debug, serde::Deserialize, utoipa::IntoParams)]
pub struct MessageQueryParams {
pub channel_id: Option<uuid::Uuid>,
pub channel_id: Uuid,
pub before_id: Option<Uuid>,
pub after_id: Option<Uuid>,
pub limit: Option<u64>,
}
impl Default for MessageQueryParams {
fn default() -> Self {
Self {
channel_id: None,
before_id: None,
limit: Some(50),
}
}
}
+20 -20
View File
@@ -1,20 +1,20 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub mod prelude;
pub mod attachment;
pub mod category;
pub mod channel;
pub mod channel_role_permission;
pub mod channel_user;
pub mod channel_user_permission;
pub mod computed_permission;
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;
pub mod user;
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub mod prelude;
pub mod attachment;
pub mod category;
pub mod channel;
pub mod channel_role_permission;
pub mod channel_user;
pub mod channel_user_permission;
pub mod computed_permission;
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;
pub mod user;
+16 -16
View File
@@ -1,16 +1,16 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub use super::attachment::Entity as Attachment;
pub use super::category::Entity as Category;
pub use super::channel::Entity as Channel;
pub use super::channel_user::Entity as ChannelUser;
pub use super::computed_permission::Entity as ComputedPermission;
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;
pub use super::user::Entity as User;
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19
pub use super::attachment::Entity as Attachment;
pub use super::category::Entity as Category;
pub use super::channel::Entity as Channel;
pub use super::channel_user::Entity as ChannelUser;
pub use super::computed_permission::Entity as ComputedPermission;
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;
pub use super::user::Entity as User;
+88 -6
View File
@@ -1,9 +1,18 @@
use super::types::MessageFilter;
use crate::models::{channel, message};
use crate::models::message;
use crate::repositories::{AnyResult, RepositoryContext};
use event_bus::Scope;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
use std::sync::Arc;
use uuid::Uuid;
const DEFAULT_MESSAGE_LIMIT: u64 = 20;
pub const MAX_MESSAGE_LIMIT: u64 = 100;
pub struct MessagePage {
pub messages: Vec<message::Model>,
pub has_more_before: bool,
pub has_more_after: bool,
}
#[derive(Clone, Debug)]
pub struct MessageRepository {
@@ -21,7 +30,11 @@ impl MessageRepository {
.await?)
}
pub async fn filter(&self, filter: MessageFilter) -> AnyResult<Vec<message::Model>> {
pub async fn filter(&self, filter: MessageFilter) -> AnyResult<MessagePage> {
let limit = filter
.limit
.unwrap_or(DEFAULT_MESSAGE_LIMIT)
.clamp(1, MAX_MESSAGE_LIMIT);
let mut query = message::Entity::find();
if let Some(channel_id) = filter.channel_id {
@@ -32,11 +45,80 @@ impl MessageRepository {
query = query.filter(message::Column::Id.lt(before_id));
}
if let Some(limit) = filter.limit {
query = query.order_by_desc(message::Column::Id).limit(limit);
if let Some(after_id) = filter.after_id {
query = query
.filter(message::Column::Id.gt(after_id))
.order_by_asc(message::Column::Id);
} else {
query = query.order_by_desc(message::Column::Id);
}
Ok(query.all(&self.context.db).await?)
let mut messages = query.limit(limit + 1).all(&self.context.db).await?;
let has_more_in_direction = messages.len() > limit as usize;
messages.truncate(limit as usize);
// Queries that walk backwards are executed in descending order so the
// database can stop as soon as it has found the requested rows. The UI
// always receives chronological order.
if filter.after_id.is_none() {
messages.reverse();
}
let (has_more_before, has_more_after) = if filter.after_id.is_some() {
let has_messages_before = self
.exists_on_or_before(filter.channel_id, filter.after_id.unwrap())
.await?;
(has_messages_before, has_more_in_direction)
} else if filter.before_id.is_some() {
let has_messages_after = self
.exists_on_or_after(filter.channel_id, filter.before_id.unwrap())
.await?;
(has_more_in_direction, has_messages_after)
} else {
(has_more_in_direction, false)
};
Ok(MessagePage {
messages,
has_more_before,
has_more_after,
})
}
async fn exists_on_or_before(&self, channel_id: Option<Uuid>, id: Uuid) -> AnyResult<bool> {
let mut query = message::Entity::find()
.select_only()
.column(message::Column::Id)
.filter(message::Column::Id.lte(id));
if let Some(channel_id) = channel_id {
query = query.filter(message::Column::ChannelId.eq(channel_id));
}
Ok(query
.limit(1)
.into_tuple::<Uuid>()
.one(&self.context.db)
.await?
.is_some())
}
async fn exists_on_or_after(&self, channel_id: Option<Uuid>, id: Uuid) -> AnyResult<bool> {
let mut query = message::Entity::find()
.select_only()
.column(message::Column::Id)
.filter(message::Column::Id.gte(id));
if let Some(channel_id) = channel_id {
query = query.filter(message::Column::ChannelId.eq(channel_id));
}
Ok(query
.limit(1)
.into_tuple::<Uuid>()
.one(&self.context.db)
.await?
.is_some())
}
pub async fn get_by_channel(&self, channel_id: uuid::Uuid) -> AnyResult<Vec<message::Model>> {
+1
View File
@@ -24,6 +24,7 @@ pub struct ServerTree {
pub struct MessageFilter {
pub channel_id: Option<uuid::Uuid>,
pub before_id: Option<uuid::Uuid>,
pub after_id: Option<uuid::Uuid>,
pub limit: Option<u64>,
}
+37 -11
View File
@@ -1,21 +1,25 @@
use crate::domain::dto::message::{CreateMessageRequest, MessageQueryParams, MessageResponse, UpdateMessageRequest};
use crate::core::state::AppState;
use crate::domain::dto::message::{
CreateMessageRequest, MessagePageResponse, MessageQueryParams, MessageResponse,
UpdateMessageRequest,
};
use crate::http::context::CurrentUser;
use crate::http::error::HTTPError;
use crate::routes::message::mapper;
use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
Json,
};
use uuid::Uuid;
/// Liste tous les messages
/// Liste une fenêtre paginée de messages
#[utoipa::path(
get,
path = "/messages",
responses(
(status = 200, description = "Liste des messages récupérée avec succès", body = [MessageResponse]),
(status = 200, description = "Fenêtre de messages récupérée avec succès", body = MessagePageResponse),
(status = 400, description = "Curseurs incompatibles ou canal manquant"),
(status = 500, description = "Erreur interne du serveur")
),
params(
@@ -26,15 +30,29 @@ use uuid::Uuid;
pub async fn get_all(
State(state): State<AppState>,
Query(filters): Query<MessageQueryParams>,
) -> Result<Json<Vec<MessageResponse>>, HTTPError> {
) -> Result<Json<MessagePageResponse>, HTTPError> {
if filters.before_id.is_some() && filters.after_id.is_some() {
return Err(HTTPError::BadRequest(
"before_id and after_id cannot be used together".to_string(),
));
}
let params = mapper::query_params_to_message_filter(filters);
let messages = state.repositories.message.filter(params).await?;
Ok(Json(
messages
let page = state.repositories.message.filter(params).await?;
let oldest_id = page.messages.first().map(|message| message.id);
let newest_id = page.messages.last().map(|message| message.id);
Ok(Json(MessagePageResponse {
messages: page
.messages
.into_iter()
.map(mapper::message_model_to_message_response)
.collect(),
))
oldest_id,
newest_id,
has_more_before: page.has_more_before,
has_more_after: page.has_more_after,
}))
}
/// Récupère un message par son ID
@@ -105,7 +123,11 @@ pub async fn create(
))?;
}
let message = state.services.message.create_message(payload.channel_id, user.id, payload.content).await?;
let message = state
.services
.message
.create_message(payload.channel_id, user.id, payload.content)
.await?;
Ok((
StatusCode::CREATED,
Json(mapper::message_model_to_message_response(message)),
@@ -150,7 +172,11 @@ pub async fn update(
return Err(HTTPError::Forbidden);
}
let message = state.services.message.update_message(id, payload.content).await?;
let message = state
.services
.message
.update_message(id, payload.content)
.await?;
Ok(Json(mapper::message_model_to_message_response(message)))
}
+4 -3
View File
@@ -1,8 +1,8 @@
use crate::models::message;
use crate::repositories::types::MessageFilter;
use crate::domain::dto::message::{
CreateMessageRequest, MessageQueryParams, MessageResponse, UpdateMessageRequest,
};
use crate::models::message;
use crate::repositories::types::MessageFilter;
use chrono::Utc;
use sea_orm::Set;
use uuid::Uuid;
@@ -48,8 +48,9 @@ pub fn update_request_to_am(
pub fn query_params_to_message_filter(params: MessageQueryParams) -> MessageFilter {
MessageFilter {
channel_id: params.channel_id,
channel_id: Some(params.channel_id),
before_id: params.before_id,
after_id: params.after_id,
limit: params.limit,
}
}