Files
appks/models/base_info.rs
T
zhenyi 9eb77ab98b 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
2026-06-10 18:49:37 +08:00

331 lines
8.5 KiB
Rust

//! 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())
}