821537186e
- Reorganized import statements in adapter tests for better readability - Replaced or_insert_with(Vec::new) with or_default() in test closures - Updated Cargo.lock with new dependency versions and checksums - Added TLS features to tonic dependency configuration - Included sqlx, chrono, and uuid dependencies with specific features - Added jsonwebtoken and arc-swap as project dependencies - Reformatted assertion statements to comply with line length limits - Adjusted base64 import order in engine codec module - Updated protobuf include statement formatting
117 lines
3.5 KiB
Rust
117 lines
3.5 KiB
Rust
//! 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<Uuid>,
|
|
/// Direct reply reference (`None` = not a reply).
|
|
pub reply_to_message_id: Option<Uuid>,
|
|
/// 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<serde_json::Value>,
|
|
/// 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<Message> {
|
|
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<Message> {
|
|
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(())
|
|
}
|
|
}
|