feat(project): add documents usecase
zhenyi committed
Added lib/project/features/documents/usecase.rs lib/project/features/documents/usecase.rs +215 −0 View lib/project/features/documents/usecase.rs
@@ -0,0 +1,215 @@
1use db::project::project_documents as doc_entity;
2use db::project::project_documents::Column as Dc;
3use sea_orm::{
4 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set,
5};
6use time::OffsetDateTime;
7use uuid::Uuid;
8
9use crate::error::{ProjectError, ProjectResult};
10use crate::features::lifecycle::usecase::log_activity;
11use crate::features::ProjectService;
12use crate::permission;
13
14use super::{CreateDocumentCmd, DocumentPage, DocumentView, UpdateDocumentCmd};
15
16pub async fn create_document(
17 svc: &ProjectService,
18 actor: Uuid,
19 space_id: Uuid,
20 project_id: Uuid,
21 cmd: CreateDocumentCmd,
22) -> ProjectResult<DocumentView> {
23 crate::features::authz::require(
24 svc,
25 actor,
26 space_id,
27 project_id,
28 permission::MANAGE_DOCUMENTS,
29 )
30 .await?;
31
32 let slug_exists = doc_entity::Entity::find()
33 .filter(Dc::ProjectId.eq(project_id))
34 .filter(Dc::Slug.eq(&cmd.slug))
35 .count(&svc.db.reader())
36 .await?;
37 if slug_exists > 0 {
38 return Err(ProjectError::Conflict(format!(
39 "document slug '{}' already exists in project {project_id}",
40 cmd.slug
41 )));
42 }
43
44 let id = Uuid::new_v4();
45 let now = OffsetDateTime::now_utc();
46 let row = doc_entity::ActiveModel {
47 id: Set(id),
48 project_id: Set(project_id),
49 parent_id: Set(cmd.parent_id),
50 creator_id: Set(actor),
51 last_editor_id: Set(Some(actor)),
52 title: Set(cmd.title),
53 slug: Set(cmd.slug),
54 content: Set(cmd.content),
55 icon: Set(cmd.icon),
56 created_at: Set(now),
57 updated_at: Set(Some(now)),
58 }
59 .insert(&svc.db.writer())
60 .await?;
61
62 log_activity(svc, project_id, actor, "document_created", "document", Some(id), None).await;
63 Ok(document_view(&row))
64}
65
66pub async fn list_documents(
67 svc: &ProjectService,
68 actor: Uuid,
69 space_id: Uuid,
70 project_id: Uuid,
71) -> ProjectResult<DocumentPage> {
72 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
73 .await?;
74 let rows = doc_entity::Entity::find()
75 .filter(Dc::ProjectId.eq(project_id))
76 .order_by_asc(Dc::Title)
77 .all(&svc.db.reader())
78 .await?;
79 let items = rows.iter().map(document_view).collect::<Vec<_>>();
80 let total = items.len() as u64;
81 Ok(DocumentPage { total, items })
82}
83
84pub async fn get_document(
85 svc: &ProjectService,
86 actor: Uuid,
87 space_id: Uuid,
88 project_id: Uuid,
89 doc_id: Uuid,
90) -> ProjectResult<DocumentView> {
91 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
92 .await?;
93 let row = doc_entity::Entity::find_by_id(doc_id)
94 .filter(Dc::ProjectId.eq(project_id))
95 .one(&svc.db.reader())
96 .await?
97 .ok_or_else(|| {
98 ProjectError::NotFound(format!("document {doc_id} in project {project_id}"))
99 })?;
100 Ok(document_view(&row))
101}
102
103pub async fn update_document(
104 svc: &ProjectService,
105 actor: Uuid,
106 space_id: Uuid,
107 project_id: Uuid,
108 doc_id: Uuid,
109 cmd: UpdateDocumentCmd,
110) -> ProjectResult<DocumentView> {
111 crate::features::authz::require(
112 svc,
113 actor,
114 space_id,
115 project_id,
116 permission::MANAGE_DOCUMENTS,
117 )
118 .await?;
119
120 let current = doc_entity::Entity::find_by_id(doc_id)
121 .filter(Dc::ProjectId.eq(project_id))
122 .one(&svc.db.reader())
123 .await?
124 .ok_or_else(|| {
125 ProjectError::NotFound(format!("document {doc_id} in project {project_id}"))
126 })?;
127
128 if let Some(slug) = &cmd.slug
129 && slug != ¤t.slug
130 {
131 let slug_exists = doc_entity::Entity::find()
132 .filter(Dc::ProjectId.eq(project_id))
133 .filter(Dc::Slug.eq(slug))
134 .filter(Dc::Id.ne(doc_id))
135 .count(&svc.db.reader())
136 .await?;
137 if slug_exists > 0 {
138 return Err(ProjectError::Conflict(format!(
139 "document slug '{slug}' already exists in project {project_id}"
140 )));
141 }
142 }
143
144 let mut active: doc_entity::ActiveModel = current.into();
145 if cmd.parent_id.is_some() {
146 active.parent_id = Set(cmd.parent_id);
147 }
148 if let Some(title) = cmd.title {
149 active.title = Set(title);
150 }
151 if let Some(slug) = cmd.slug {
152 active.slug = Set(slug);
153 }
154 if let Some(content) = cmd.content {
155 active.content = Set(content);
156 }
157 if cmd.icon.is_some() {
158 active.icon = Set(cmd.icon);
159 }
160 active.last_editor_id = Set(Some(actor));
161 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
162 let updated = active.update(&svc.db.writer()).await?;
163
164 log_activity(svc, project_id, actor, "document_updated", "document", Some(doc_id), None)
165 .await;
166 Ok(document_view(&updated))
167}
168
169pub async fn delete_document(
170 svc: &ProjectService,
171 actor: Uuid,
172 space_id: Uuid,
173 project_id: Uuid,
174 doc_id: Uuid,
175) -> ProjectResult<()> {
176 crate::features::authz::require(
177 svc,
178 actor,
179 space_id,
180 project_id,
181 permission::MANAGE_DOCUMENTS,
182 )
183 .await?;
184
185 let deleted = doc_entity::Entity::delete_many()
186 .filter(Dc::Id.eq(doc_id))
187 .filter(Dc::ProjectId.eq(project_id))
188 .exec(&svc.db.writer())
189 .await?;
190 if deleted.rows_affected == 0 {
191 return Err(ProjectError::NotFound(format!(
192 "document {doc_id} in project {project_id}"
193 )));
194 }
195
196 log_activity(svc, project_id, actor, "document_deleted", "document", Some(doc_id), None)
197 .await;
198 Ok(())
199}
200
201fn document_view(m: &doc_entity::Model) -> DocumentView {
202 DocumentView {
203 id: m.id,
204 project_id: m.project_id,
205 parent_id: m.parent_id,
206 creator_id: m.creator_id,
207 last_editor_id: m.last_editor_id,
208 title: m.title.clone(),
209 slug: m.slug.clone(),
210 content: m.content.clone(),
211 icon: m.icon.clone(),
212 created_at: m.created_at,
213 updated_at: m.updated_at,
214 }
215}
No newline at end of file