use std::sync::Arc; use chrono::Utc; use sqlx::AnyPool; use crate::{ config::AppConfig, models::{EmoteRow, new_uuid}, }; /// Thin database abstraction that works with both SQLite and PostgreSQL /// through `sqlx::AnyPool`. #[derive(Clone)] pub struct Database { pub pool: AnyPool, pub config: Arc, } impl Database { pub async fn connect(config: Arc) -> Result { let pool = AnyPool::connect(&config.database.url).await?; Ok(Self { pool, config }) } /// Run pending migrations. pub async fn migrate(&self) -> Result<(), sqlx::migrate::MigrateError> { sqlx::migrate!("./migrations").run(&self.pool).await } pub async fn list_emotes(&self) -> Result, sqlx::Error> { let rows = sqlx::query_as::<_, EmoteRow>( "SELECT uuid, name, alias, image_key, created, modified FROM emotes ORDER BY created DESC", ) .fetch_all(&self.pool) .await?; Ok(rows) } pub async fn get_emote_by_id(&self, uuid: &str) -> Result, sqlx::Error> { let row = sqlx::query_as::<_, EmoteRow>( "SELECT uuid, name, alias, image_key, created, modified FROM emotes WHERE uuid = $1", ) .bind(uuid) .fetch_optional(&self.pool) .await?; Ok(row) } pub async fn create_emote( &self, name: &str, alias: Option<&str>, image_key: &str, ) -> Result { let id = new_uuid(); let now = Utc::now().to_rfc3339(); sqlx::query( "INSERT INTO emotes (uuid, name, alias, image_key, created, modified) VALUES ($1, $2, $3, $4, $5, $6)", ) .bind(&id) .bind(name) .bind(alias) .bind(image_key) .bind(&now) .bind(&now) .execute(&self.pool) .await?; Ok(EmoteRow { uuid: id, name: name.to_string(), alias: alias.map(|s| s.to_string()), image_key: image_key.to_string(), modified: now.clone(), created: now, }) } pub async fn update_emote( &self, uuid: &str, name: Option<&str>, alias: Option>, image_key: Option<&str>, ) -> Result, sqlx::Error> { let existing = match self.get_emote_by_id(uuid).await? { Some(e) => e, None => return Ok(None), }; let new_name = name.unwrap_or(&existing.name); let new_image_key = image_key.unwrap_or(&existing.image_key); let new_alias: Option = match alias { Some(Some(a)) => Some(a.to_string()), Some(None) => None, None => existing.alias.clone(), }; let now = Utc::now().to_rfc3339(); sqlx::query( "UPDATE emotes SET name = $1, alias = $2, image_key = $3, modified = $4 WHERE uuid = $5", ) .bind(new_name) .bind(new_alias.as_deref()) .bind(new_image_key) .bind(&now) .bind(uuid) .execute(&self.pool) .await?; Ok(Some(EmoteRow { uuid: uuid.to_string(), name: new_name.to_string(), alias: new_alias, image_key: new_image_key.to_string(), created: existing.created, modified: now, })) } pub async fn delete_emote(&self, uuid: &str) -> Result { let result = sqlx::query("DELETE FROM emotes WHERE uuid = $1") .bind(uuid) .execute(&self.pool) .await?; Ok(result.rows_affected() > 0) } }