//! Message write operations — insert, update body, soft delete. //! //! All mutations use parameterized queries and return the affected row(s). use chrono::Utc; use uuid::Uuid; use crate::ImksResult; use crate::models::message::{Message, new_message_id}; use super::message_repo::MessageRepo; /// Input payload for creating a new message. #[derive(Debug, Clone)] pub struct CreateMessageInput { /// Target channel UUID. pub channel_id: Uuid, /// Author (user) UUID — extracted from JWT `sub` claim. pub author_id: Uuid, /// Thread this message belongs to (`None` = top-level). pub thread_id: Option, /// Direct reply reference (`None` = not a reply). pub reply_to_message_id: Option, /// Discriminator: `"text"`, `"system"`, `"event"`, `"article"`. pub message_type: String, /// Plain text or markdown body. pub body: String, /// Extensible metadata (flags, locale, etc.). pub metadata: Option, /// Whether this is a system/bot-generated message. pub system: bool, } impl MessageRepo { /// Insert a new message row and return it. /// /// The message ID is a fresh UUID v7 (time-ordered). pub async fn create(&self, input: &CreateMessageInput) -> ImksResult { let id = new_message_id(); let now = Utc::now(); let row = sqlx::query_as::<_, Message>( r#" INSERT INTO message ( id, channel_id, author_id, thread_id, reply_to_message_id, message_type, body, metadata, pinned, system, edited_at, deleted_at, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, FALSE, $9, NULL, NULL, $10, $10 ) RETURNING * "#, ) .bind(id) .bind(input.channel_id) .bind(input.author_id) .bind(input.thread_id) .bind(input.reply_to_message_id) .bind(&input.message_type) .bind(&input.body) .bind(&input.metadata) .bind(input.system) .bind(now) .fetch_one(self.pool()) .await?; Ok(row) } /// Update the body of an existing message. Sets `edited_at` and `updated_at`. /// /// Returns the updated row, or an error if the message is not found or deleted. pub async fn update_body(&self, message_id: Uuid, new_body: &str) -> ImksResult { let now = Utc::now(); let row = sqlx::query_as::<_, Message>( r#" UPDATE message SET body = $1, edited_at = $2, updated_at = $2 WHERE id = $3 AND deleted_at IS NULL RETURNING * "#, ) .bind(new_body) .bind(now) .bind(message_id) .fetch_optional(self.pool()) .await? .ok_or_else(|| crate::ImksError::NotFound(format!("message {message_id}")))?; Ok(row) } /// Soft-delete a message by setting `deleted_at`. /// /// Returns `Ok(())` even if the message was already deleted. pub async fn soft_delete(&self, message_id: Uuid) -> ImksResult<()> { let now = Utc::now(); sqlx::query( r#" UPDATE message SET deleted_at = $1, updated_at = $1 WHERE id = $2 AND deleted_at IS NULL "#, ) .bind(now) .bind(message_id) .execute(self.pool()) .await?; Ok(()) } }