refactor(models): update data models and remove deprecated IM entities
- Update channel, notification, PR, repo, user, workspace models - Remove deleted IM models: articles, channel follows, message attachments/bookmarks/drafts/edit history/embeds/mentions/pins/ polls/reactions/threads, saved messages, thread read states - Add new PR models: review requests, templates - Add repo release assets model - Add base_info module for API detail responses
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
//! BaseInfo structs — stable, minimal projections of core entities for API/WS responses.
|
||||
//!
|
||||
//! Every raw UUID foreign key in API / WebSocket responses must be expanded
|
||||
//! to its corresponding `*BaseInfo` object so frontend consumers never
|
||||
//! receive bare identifiers.
|
||||
//!
|
||||
//! Batch resolvers are provided to avoid N+1 queries when enriching
|
||||
//! collections of entities.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::models::common::{ChannelType, State, Visibility};
|
||||
use crate::models::db::AppDatabase;
|
||||
|
||||
// Section: BaseInfo structs
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct UserBaseInfo {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub is_bot: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct WorkspaceBaseInfo {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub visibility: Visibility,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct RepoBaseInfo {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub workspace_id: Uuid,
|
||||
pub visibility: Visibility,
|
||||
pub is_fork: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ChannelBaseInfo {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub channel_type: ChannelType,
|
||||
pub workspace_id: Uuid,
|
||||
pub archived: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct IssueBaseInfo {
|
||||
pub id: Uuid,
|
||||
pub number: i64,
|
||||
pub title: String,
|
||||
pub state: State,
|
||||
pub workspace_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct PullRequestBaseInfo {
|
||||
pub id: Uuid,
|
||||
pub number: i64,
|
||||
pub title: String,
|
||||
pub state: State,
|
||||
pub repo_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct WikiPageBaseInfo {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub repo_id: Uuid,
|
||||
}
|
||||
|
||||
// Section: DB row structs for batch queries
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct UserBaseInfoRow {
|
||||
id: Uuid,
|
||||
username: String,
|
||||
display_name: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
is_bot: bool,
|
||||
}
|
||||
|
||||
impl From<UserBaseInfoRow> for UserBaseInfo {
|
||||
fn from(r: UserBaseInfoRow) -> Self {
|
||||
UserBaseInfo {
|
||||
id: r.id,
|
||||
username: r.username,
|
||||
display_name: r.display_name,
|
||||
avatar_url: r.avatar_url,
|
||||
is_bot: r.is_bot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct WorkspaceBaseInfoRow {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
avatar_url: Option<String>,
|
||||
visibility: Visibility,
|
||||
}
|
||||
|
||||
impl From<WorkspaceBaseInfoRow> for WorkspaceBaseInfo {
|
||||
fn from(r: WorkspaceBaseInfoRow) -> Self {
|
||||
WorkspaceBaseInfo {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
avatar_url: r.avatar_url,
|
||||
visibility: r.visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct RepoBaseInfoRow {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
workspace_id: Uuid,
|
||||
visibility: Visibility,
|
||||
is_fork: bool,
|
||||
}
|
||||
|
||||
impl From<RepoBaseInfoRow> for RepoBaseInfo {
|
||||
fn from(r: RepoBaseInfoRow) -> Self {
|
||||
RepoBaseInfo {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
workspace_id: r.workspace_id,
|
||||
visibility: r.visibility,
|
||||
is_fork: r.is_fork,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct ChannelBaseInfoRow {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
channel_type: ChannelType,
|
||||
workspace_id: Uuid,
|
||||
archived: bool,
|
||||
}
|
||||
|
||||
impl From<ChannelBaseInfoRow> for ChannelBaseInfo {
|
||||
fn from(r: ChannelBaseInfoRow) -> Self {
|
||||
ChannelBaseInfo {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
channel_type: r.channel_type,
|
||||
workspace_id: r.workspace_id,
|
||||
archived: r.archived,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct IssueBaseInfoRow {
|
||||
id: Uuid,
|
||||
number: i64,
|
||||
title: String,
|
||||
state: State,
|
||||
workspace_id: Uuid,
|
||||
}
|
||||
|
||||
impl From<IssueBaseInfoRow> for IssueBaseInfo {
|
||||
fn from(r: IssueBaseInfoRow) -> Self {
|
||||
IssueBaseInfo {
|
||||
id: r.id,
|
||||
number: r.number,
|
||||
title: r.title,
|
||||
state: r.state,
|
||||
workspace_id: r.workspace_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct PullRequestBaseInfoRow {
|
||||
id: Uuid,
|
||||
number: i64,
|
||||
title: String,
|
||||
state: State,
|
||||
repo_id: Uuid,
|
||||
}
|
||||
|
||||
impl From<PullRequestBaseInfoRow> for PullRequestBaseInfo {
|
||||
fn from(r: PullRequestBaseInfoRow) -> Self {
|
||||
PullRequestBaseInfo {
|
||||
id: r.id,
|
||||
number: r.number,
|
||||
title: r.title,
|
||||
state: r.state,
|
||||
repo_id: r.repo_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserBaseInfo {
|
||||
pub fn placeholder(id: Uuid) -> Self {
|
||||
Self {
|
||||
id,
|
||||
username: "unknown".into(),
|
||||
display_name: Some("Unknown User".into()),
|
||||
avatar_url: None,
|
||||
is_bot: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Section: Batch resolvers
|
||||
|
||||
/// Resolve multiple users to `UserBaseInfo`. Always do a single
|
||||
/// `SELECT … WHERE id = ANY($1)` to avoid N+1.
|
||||
pub async fn resolve_users(
|
||||
db: &AppDatabase,
|
||||
ids: &[Uuid],
|
||||
) -> Result<HashMap<Uuid, UserBaseInfo>, AppError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
let rows = sqlx::query_as::<_, UserBaseInfoRow>(
|
||||
r#"SELECT id, username, display_name, avatar_url, is_bot
|
||||
FROM "user" WHERE id = ANY($1) AND deleted_at IS NULL"#,
|
||||
)
|
||||
.bind(ids)
|
||||
.fetch_all(db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
Ok(rows.into_iter().map(|r| (r.id, r.into())).collect())
|
||||
}
|
||||
|
||||
/// Resolve multiple workspaces to `WorkspaceBaseInfo`.
|
||||
pub async fn resolve_workspaces(
|
||||
db: &AppDatabase,
|
||||
ids: &[Uuid],
|
||||
) -> Result<HashMap<Uuid, WorkspaceBaseInfo>, AppError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
let rows = sqlx::query_as::<_, WorkspaceBaseInfoRow>(
|
||||
"SELECT id, name, avatar_url, visibility FROM workspace WHERE id = ANY($1) AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(ids)
|
||||
.fetch_all(db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
Ok(rows.into_iter().map(|r| (r.id, r.into())).collect())
|
||||
}
|
||||
|
||||
/// Resolve multiple repos to `RepoBaseInfo`.
|
||||
pub async fn resolve_repos(
|
||||
db: &AppDatabase,
|
||||
ids: &[Uuid],
|
||||
) -> Result<HashMap<Uuid, RepoBaseInfo>, AppError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
let rows = sqlx::query_as::<_, RepoBaseInfoRow>(
|
||||
"SELECT id, name, workspace_id, visibility, is_fork FROM repo WHERE id = ANY($1) AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(ids)
|
||||
.fetch_all(db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
Ok(rows.into_iter().map(|r| (r.id, r.into())).collect())
|
||||
}
|
||||
|
||||
/// Resolve multiple channels to `ChannelBaseInfo`.
|
||||
pub async fn resolve_channels(
|
||||
db: &AppDatabase,
|
||||
ids: &[Uuid],
|
||||
) -> Result<HashMap<Uuid, ChannelBaseInfo>, AppError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
let rows = sqlx::query_as::<_, ChannelBaseInfoRow>(
|
||||
"SELECT id, name, channel_type, workspace_id, archived FROM channel WHERE id = ANY($1) AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(ids)
|
||||
.fetch_all(db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
Ok(rows.into_iter().map(|r| (r.id, r.into())).collect())
|
||||
}
|
||||
|
||||
/// Resolve multiple issues to `IssueBaseInfo`.
|
||||
pub async fn resolve_issues(
|
||||
db: &AppDatabase,
|
||||
ids: &[Uuid],
|
||||
) -> Result<HashMap<Uuid, IssueBaseInfo>, AppError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
let rows = sqlx::query_as::<_, IssueBaseInfoRow>(
|
||||
"SELECT id, number, title, state, workspace_id FROM issue WHERE id = ANY($1) AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(ids)
|
||||
.fetch_all(db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
Ok(rows.into_iter().map(|r| (r.id, r.into())).collect())
|
||||
}
|
||||
|
||||
/// Resolve multiple pull requests to `PullRequestBaseInfo`.
|
||||
pub async fn resolve_pull_requests(
|
||||
db: &AppDatabase,
|
||||
ids: &[Uuid],
|
||||
) -> Result<HashMap<Uuid, PullRequestBaseInfo>, AppError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
let rows = sqlx::query_as::<_, PullRequestBaseInfoRow>(
|
||||
"SELECT id, number, title, state, repo_id FROM pull_request WHERE id = ANY($1) AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(ids)
|
||||
.fetch_all(db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
Ok(rows.into_iter().map(|r| (r.id, r.into())).collect())
|
||||
}
|
||||
Reference in New Issue
Block a user