9eb77ab98b
- 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
331 lines
8.5 KiB
Rust
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())
|
|
}
|