Skip to content
Clone
a06673b
202 lines 6.4 KB Rust
use db::conn::sea_orm::*;
use db::repo::repo_topic as topic_entity;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::error::{RepoError, RepoResult};
use crate::features::RepoService;
use crate::path::{normalize_topic, sanitize_topics};
use super::types::{RepoTopicOptions, TopicList, TopicSearchQuery, TopicSearchResult, TopicView};
fn require_authenticated(actor: Uuid) -> RepoResult<()> {
if actor.is_nil() {
return Err(RepoError::Unauthorized("authentication required".into()));
}
Ok(())
}
async fn ensure_topic(svc: &RepoService, topic_name: &str) -> RepoResult<topic_entity::Model> {
let db = svc.db.writer();
if let Some(t) = topic_entity::Entity::find()
.filter(topic_entity::Column::Name.eq(topic_name))
.one(&db)
.await?
{
return Ok(t);
}
let now = OffsetDateTime::now_utc();
let active: topic_entity::ActiveModel = topic_entity::ActiveModel {
id: Set(Uuid::new_v4()),
name: Set(topic_name.to_string()),
created_at: Set(now),
updated_at: Set(now),
};
Ok(active.insert(&db).await?)
}
pub async fn list_repo_topics(
svc: &RepoService,
actor: Uuid,
repo_id: Uuid,
) -> RepoResult<TopicList> {
let _ = actor;
let db = svc.db.reader();
let links = db::repo::repo_relation_topic::Entity::find()
.filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
.all(&db)
.await?;
let mut names: Vec<String> = Vec::with_capacity(links.len());
for link in links {
if let Some(t) = topic_entity::Entity::find_by_id(link.topic_id)
.one(&db)
.await?
{
names.push(t.name);
}
}
names.sort();
names.dedup();
Ok(TopicList { topics: names })
}
pub async fn update_repo_topics(
svc: &RepoService,
actor: Uuid,
repo_id: Uuid,
input: RepoTopicOptions,
) -> RepoResult<()> {
require_authenticated(actor)?;
let normalized = sanitize_topics(&input.topics, svc.config.max_topics_per_repo)?;
let db = svc.db.writer();
let existing = db::repo::repo_relation_topic::Entity::find()
.filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
.all(&db)
.await?;
for r in existing {
if let Some(t) = topic_entity::Entity::find_by_id(r.topic_id)
.one(&db)
.await?
{
if !normalized.contains(&t.name) {
db::repo::repo_relation_topic::Entity::delete_by_id(r.id)
.exec(&db)
.await?;
}
}
}
for name in &normalized {
let topic = ensure_topic(svc, name).await?;
let exists = db::repo::repo_relation_topic::Entity::find()
.filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
.filter(db::repo::repo_relation_topic::Column::TopicId.eq(topic.id))
.one(&db)
.await?;
if exists.is_none() {
let active: db::repo::repo_relation_topic::ActiveModel =
db::repo::repo_relation_topic::ActiveModel {
id: Set(Uuid::new_v4()),
repo_id: Set(repo_id),
topic_id: Set(topic.id),
created_at: Set(OffsetDateTime::now_utc()),
};
active.insert(&db).await?;
}
}
Ok(())
}
pub async fn add_repo_topic(
svc: &RepoService,
actor: Uuid,
repo_id: Uuid,
topic: &str,
) -> RepoResult<()> {
require_authenticated(actor)?;
let name = normalize_topic(topic)?;
let current = db::repo::repo_relation_topic::Entity::find()
.filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
.count(&svc.db.reader())
.await?;
if current >= svc.config.max_topics_per_repo as u64 {
return Err(RepoError::BadRequest(format!(
"too many topics (max {})",
svc.config.max_topics_per_repo
)));
}
let topic_model = ensure_topic(svc, &name).await?;
let db = svc.db.writer();
let exists = db::repo::repo_relation_topic::Entity::find()
.filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
.filter(db::repo::repo_relation_topic::Column::TopicId.eq(topic_model.id))
.one(&db)
.await?;
if exists.is_some() {
return Ok(());
}
let active: db::repo::repo_relation_topic::ActiveModel =
db::repo::repo_relation_topic::ActiveModel {
id: Set(Uuid::new_v4()),
repo_id: Set(repo_id),
topic_id: Set(topic_model.id),
created_at: Set(OffsetDateTime::now_utc()),
};
active.insert(&db).await?;
Ok(())
}
pub async fn delete_repo_topic(
svc: &RepoService,
actor: Uuid,
repo_id: Uuid,
topic: &str,
) -> RepoResult<()> {
require_authenticated(actor)?;
let name = normalize_topic(topic)?;
let db = svc.db.writer();
let topic_model = topic_entity::Entity::find()
.filter(topic_entity::Column::Name.eq(&name))
.one(&db)
.await?
.ok_or_else(|| RepoError::NotFound(format!("topic {name}")))?;
db::repo::repo_relation_topic::Entity::delete_many()
.filter(db::repo::repo_relation_topic::Column::RepoId.eq(repo_id))
.filter(db::repo::repo_relation_topic::Column::TopicId.eq(topic_model.id))
.exec(&db)
.await?;
Ok(())
}
pub async fn search_topics(
svc: &RepoService,
actor: Uuid,
q: TopicSearchQuery,
) -> RepoResult<TopicSearchResult> {
let _ = actor;
let limit = q.limit.unwrap_or(svc.config.default_pagination);
let offset = q.page.unwrap_or(0).saturating_mul(limit);
let limit = std::cmp::min(limit, svc.config.max_pagination);
let db = svc.db.reader();
let mut query = topic_entity::Entity::find();
if let Some(s) = q.q.as_deref() {
if !s.is_empty() {
query = query.filter(topic_entity::Column::Name.contains(s));
}
}
let items = query
.order_by_asc(topic_entity::Column::Name)
.limit(Some(limit))
.offset(Some(offset))
.all(&db)
.await?
.into_iter()
.map(|t| TopicView {
id: t.id,
name: t.name.clone(),
repo_count: 0,
created_at: super::types::fmt_ts_pub(t.created_at),
updated_at: super::types::fmt_ts_pub(t.updated_at),
})
.collect();
Ok(TopicSearchResult { topics: items })
}