//! Draft CRUD operations on `MessageRepo`. //! //! One draft per (channel, user, thread). Upserted on every keystroke debounce, //! deleted on send. Thread_id=NULL uses a dedicated conflict target. use chrono::Utc; use uuid::Uuid; use crate::ImksResult; use crate::models::message_draft::MessageDraft; use super::message_repo::MessageRepo; impl MessageRepo { /// Upsert a draft for the given (channel, user, thread) key. /// Uses NULL-safe conflict handling via COALESCE. pub async fn upsert_draft( &self, channel_id: Uuid, user_id: Uuid, thread_id: Option, body: &str, reply_to_message_id: Option, metadata: Option, ) -> ImksResult { let id = Uuid::now_v7(); let now = Utc::now(); let query = if thread_id.is_some() { r#" INSERT INTO message_draft ( id, channel_id, user_id, thread_id, reply_to_message_id, body, metadata, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8) ON CONFLICT (channel_id, user_id, thread_id) DO UPDATE SET body = EXCLUDED.body, reply_to_message_id = EXCLUDED.reply_to_message_id, metadata = EXCLUDED.metadata, updated_at = EXCLUDED.updated_at RETURNING * "# } else { r#" INSERT INTO message_draft ( id, channel_id, user_id, thread_id, reply_to_message_id, body, metadata, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8) ON CONFLICT (channel_id, user_id) WHERE thread_id IS NULL DO UPDATE SET body = EXCLUDED.body, reply_to_message_id = EXCLUDED.reply_to_message_id, metadata = EXCLUDED.metadata, updated_at = EXCLUDED.updated_at RETURNING * "# }; sqlx::query_as::<_, MessageDraft>(query) .bind(id) .bind(channel_id) .bind(user_id) .bind(thread_id) .bind(reply_to_message_id) .bind(body) .bind(metadata) .bind(now) .fetch_one(self.pool()) .await .map_err(Into::into) } /// Get a user's draft for a channel (optionally scoped to a thread). pub async fn get_draft( &self, channel_id: Uuid, user_id: Uuid, thread_id: Option, ) -> ImksResult> { sqlx::query_as::<_, MessageDraft>( r#" SELECT * FROM message_draft WHERE channel_id = $1 AND user_id = $2 AND thread_id IS NOT DISTINCT FROM $3 "#, ) .bind(channel_id) .bind(user_id) .bind(thread_id) .fetch_optional(self.pool()) .await .map_err(Into::into) } /// Delete a draft after the message is sent. pub async fn delete_draft( &self, channel_id: Uuid, user_id: Uuid, thread_id: Option, ) -> ImksResult { let result = sqlx::query( r#" DELETE FROM message_draft WHERE channel_id = $1 AND user_id = $2 AND thread_id IS NOT DISTINCT FROM $3 "#, ) .bind(channel_id) .bind(user_id) .bind(thread_id) .execute(self.pool()) .await?; Ok(result.rows_affected() > 0) } }