030e59e
zhenyi
refactor(lib): migrate storage and security tier to central lib/config (auth, cache, db, email, jwt, payment, queue, rate_limit, storage)
030e59e
use db::project::project_documents as doc_entity;
use db::project::project_documents::Column as Dc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set,
};
use time::OffsetDateTime;
use uuid::Uuid;
use crate::error::{ProjectError, ProjectResult};
use crate::features::ProjectService;
use crate::features::lifecycle::usecase::log_activity;
use crate::metrics;
use crate::permission;
use super::{CreateDocumentCmd, DocumentPage, DocumentView, UpdateDocumentCmd};
pub async fn create_document(
svc: &ProjectService,
actor: Uuid,
space_id: Uuid,
project_id: Uuid,
cmd: CreateDocumentCmd,
) -> ProjectResult<DocumentView> {
let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_CREATE);
let result: ProjectResult<DocumentView> = async {
crate::features::authz::require(
svc,
actor,
space_id,
project_id,
permission::MANAGE_DOCUMENTS,
)
.await?;
let slug_exists = doc_entity::Entity::find()
.filter(Dc::ProjectId.eq(project_id))
.filter(Dc::Slug.eq(&cmd.slug))
.count(&svc.db.reader())
.await?;
if slug_exists > 0 {
return Err(ProjectError::Conflict(format!(
"document slug '{}' already exists in project {project_id}",
cmd.slug
)));
}
let id = Uuid::now_v7();
let now = OffsetDateTime::now_utc();
let row = doc_entity::ActiveModel {
id: Set(id),
project_id: Set(project_id),
parent_id: Set(cmd.parent_id),
creator_id: Set(actor),
last_editor_id: Set(Some(actor)),
title: Set(cmd.title),
slug: Set(cmd.slug),
content: Set(cmd.content),
icon: Set(cmd.icon),
created_at: Set(now),
updated_at: Set(Some(now)),
}
.insert(&svc.db.writer())
.await?;
log_activity(
svc,
project_id,
actor,
"document_created",
"document",
Some(id),
None,
)
.await;
Ok(document_view(&row))
}
.await;
metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_CREATE, &result);
result
}
pub async fn list_documents(
svc: &ProjectService,
actor: Uuid,
space_id: Uuid,
project_id: Uuid,
) -> ProjectResult<DocumentPage> {
let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_LIST);
let result: ProjectResult<DocumentPage> = async {
crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
.await?;
let rows = doc_entity::Entity::find()
.filter(Dc::ProjectId.eq(project_id))
.order_by_asc(Dc::Title)
.all(&svc.db.reader())
.await?;
let items = rows.iter().map(document_view).collect::<Vec<_>>();
let total = items.len() as u64;
Ok(DocumentPage { total, items })
}
.await;
metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_LIST, &result);
result
}
pub async fn get_document(
svc: &ProjectService,
actor: Uuid,
space_id: Uuid,
project_id: Uuid,
doc_id: Uuid,
) -> ProjectResult<DocumentView> {
let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_GET);
let result: ProjectResult<DocumentView> = async {
crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
.await?;
let row = doc_entity::Entity::find_by_id(doc_id)
.filter(Dc::ProjectId.eq(project_id))
.one(&svc.db.reader())
.await?
.ok_or_else(|| {
ProjectError::NotFound(format!("document {doc_id} in project {project_id}"))
})?;
Ok(document_view(&row))
}
.await;
metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_GET, &result);
result
}
pub async fn update_document(
svc: &ProjectService,
actor: Uuid,
space_id: Uuid,
project_id: Uuid,
doc_id: Uuid,
cmd: UpdateDocumentCmd,
) -> ProjectResult<DocumentView> {
let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_UPDATE);
let result: ProjectResult<DocumentView> = async {
crate::features::authz::require(
svc,
actor,
space_id,
project_id,
permission::MANAGE_DOCUMENTS,
)
.await?;
let current = doc_entity::Entity::find_by_id(doc_id)
.filter(Dc::ProjectId.eq(project_id))
.one(&svc.db.reader())
.await?
.ok_or_else(|| {
ProjectError::NotFound(format!("document {doc_id} in project {project_id}"))
})?;
if let Some(slug) = &cmd.slug
&& slug != ¤t.slug
{
let slug_exists = doc_entity::Entity::find()
.filter(Dc::ProjectId.eq(project_id))
.filter(Dc::Slug.eq(slug))
.filter(Dc::Id.ne(doc_id))
.count(&svc.db.reader())
.await?;
if slug_exists > 0 {
return Err(ProjectError::Conflict(format!(
"document slug '{slug}' already exists in project {project_id}"
)));
}
}
let mut active: doc_entity::ActiveModel = current.into();
if cmd.parent_id.is_some() {
active.parent_id = Set(cmd.parent_id);
}
if let Some(title) = cmd.title {
active.title = Set(title);
}
if let Some(slug) = cmd.slug {
active.slug = Set(slug);
}
if let Some(content) = cmd.content {
active.content = Set(content);
}
if cmd.icon.is_some() {
active.icon = Set(cmd.icon);
}
active.last_editor_id = Set(Some(actor));
active.updated_at = Set(Some(OffsetDateTime::now_utc()));
let updated = active.update(&svc.db.writer()).await?;
log_activity(
svc,
project_id,
actor,
"document_updated",
"document",
Some(doc_id),
None,
)
.await;
Ok(document_view(&updated))
}
.await;
metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_UPDATE, &result);
result
}
pub async fn delete_document(
svc: &ProjectService,
actor: Uuid,
space_id: Uuid,
project_id: Uuid,
doc_id: Uuid,
) -> ProjectResult<()> {
let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_DELETE);
let result: ProjectResult<()> = async {
crate::features::authz::require(
svc,
actor,
space_id,
project_id,
permission::MANAGE_DOCUMENTS,
)
.await?;
let deleted = doc_entity::Entity::delete_many()
.filter(Dc::Id.eq(doc_id))
.filter(Dc::ProjectId.eq(project_id))
.exec(&svc.db.writer())
.await?;
if deleted.rows_affected == 0 {
return Err(ProjectError::NotFound(format!(
"document {doc_id} in project {project_id}"
)));
}
log_activity(
svc,
project_id,
actor,
"document_deleted",
"document",
Some(doc_id),
None,
)
.await;
Ok(())
}
.await;
metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_DELETE, &result);
result
}
fn document_view(m: &doc_entity::Model) -> DocumentView {
DocumentView {
id: m.id,
project_id: m.project_id,
parent_id: m.parent_id,
creator_id: m.creator_id,
last_editor_id: m.last_editor_id,
title: m.title.clone(),
slug: m.slug.clone(),
content: m.content.clone(),
icon: m.icon.clone(),
created_at: m.created_at,
updated_at: m.updated_at,
}
}