use chrono::Utc; use uuid::Uuid; use crate::event::{UserInfo, WorkspaceInfo, category}; use crate::{ChannelBus, ChannelError, ChannelResult}; use super::MAX_CATEGORY_NAME_LEN; use super::WsHandler; use super::WsOutEvent; impl WsHandler { pub(super) async fn category_create( bus: &ChannelBus, user_id: Uuid, workspace: Uuid, name: String, position: Option, ) -> ChannelResult> { if name.is_empty() || name.len() > MAX_CATEGORY_NAME_LEN { return Err(ChannelError::Validation( "invalid category name".into(), )); } Self::ensure_workspace_member(bus, user_id, workspace).await?; let row = db::sqlx::query_as::<_, model::channel::RoomCategoryModel>( "INSERT INTO room_category (wk, name, position, created_at, updated_at) \ VALUES ($1, $2, $3, now(), now()) \ RETURNING id, wk, name, position, collapsed, created_at, updated_at", ) .bind(workspace) .bind(&name) .bind(position.unwrap_or(0)) .fetch_one(bus.inner.db.writer()) .await?; let cc_workspace = bus .lookup_workspace(workspace) .await .unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)); let cc_user = bus .lookup_user(user_id) .await .unwrap_or_else(|_| UserInfo::unknown(user_id)); let data = category::CategoryCreatedService { id: row.id, project: cc_workspace, name: row.name, position: row.position, created_by: cc_user, created_at: row.created_at, }; bus.workspace_changed(workspace).await?; Ok(Some(WsOutEvent::CategoryCreated { workspace: data.project.clone(), data, })) } pub(super) async fn category_update( bus: &ChannelBus, user_id: Uuid, id: Uuid, name: Option, position: Option, ) -> ChannelResult> { let old = db::sqlx::query_as::<_, model::channel::RoomCategoryModel>( "SELECT id, wk, name, position, collapsed, created_at, updated_at \ FROM room_category WHERE id = $1", ) .bind(id) .fetch_one(bus.inner.db.reader()) .await?; Self::ensure_workspace_member(bus, user_id, old.wk).await?; let new_name = name.unwrap_or(old.name.clone()); let new_position = position.unwrap_or(old.position); db::sqlx::query( "UPDATE room_category SET name = $2, position = $3, updated_at = now() WHERE id = $1", ) .bind(id) .bind(&new_name) .bind(new_position) .execute(bus.inner.db.writer()) .await?; let cu_workspace = bus .lookup_workspace(old.wk) .await .unwrap_or_else(|_| WorkspaceInfo::unknown(old.wk)); let cu_user = bus .lookup_user(user_id) .await .unwrap_or_else(|_| UserInfo::unknown(user_id)); let data = category::CategoryUpdatedService { id, project: cu_workspace, name: Some(new_name), position: Some(new_position), updated_by: cu_user, updated_at: Utc::now(), }; bus.workspace_changed(old.wk).await?; Ok(Some(WsOutEvent::CategoryUpdated { workspace: data.project.clone(), data, })) } pub(super) async fn category_delete( bus: &ChannelBus, _user_id: Uuid, id: Uuid, ) -> ChannelResult> { let existing = db::sqlx::query_as::<_, model::channel::RoomCategoryModel>( "SELECT id, wk, name, position, collapsed, created_at, updated_at \ FROM room_category WHERE id = $1", ) .bind(id) .fetch_one(bus.inner.db.reader()) .await?; Self::ensure_workspace_member(bus, _user_id, existing.wk).await?; let row = db::sqlx::query_as::<_, model::channel::RoomCategoryModel>( "DELETE FROM room_category WHERE id = $1 \ RETURNING id, wk, name, position, collapsed, created_at, updated_at", ) .bind(id) .fetch_one(bus.inner.db.writer()) .await?; let cd_workspace = bus .lookup_workspace(row.wk) .await .unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)); let cd_user = bus .lookup_user(_user_id) .await .unwrap_or_else(|_| UserInfo::unknown(_user_id)); let data = category::CategoryDeletedService { id: row.id, project: cd_workspace.clone(), deleted_by: cd_user, deleted_at: Utc::now(), }; bus.workspace_changed(row.wk).await?; Ok(Some(WsOutEvent::CategoryDeleted { workspace: cd_workspace, data, })) } }