refactor(project): refresh all feature usecase implementations
zhenyi committed
Modified lib/project/features/activities/usecase.rs lib/project/features/activities/usecase.rs +24 −17 View lib/project/features/activities/usecase.rs
@@ -5,6 +5,7 @@
55
66use crate::error::ProjectResult;
77use crate::features::ProjectService;
8use crate::metrics;
89use crate::permission;
910
1011use super::{ActivityPage, ActivityView, ListActivityQuery};
@@ -16,27 +17,33 @@
1617 project_id: Uuid,
1718 q: ListActivityQuery,
1819) -> ProjectResult<ActivityPage> {
19 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
20 .await?;
20 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_ACTIVITIES_LIST);
21 let result: ProjectResult<ActivityPage> = async {
22 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
23 .await?;
2124
22 let page = q.page.max(1);
23 let per_page = q.per_page.clamp(1, svc.config.max_page_size);
24 let offset = (page - 1) * per_page;
25 let page = q.page.max(1);
26 let per_page = q.per_page.clamp(1, svc.config.max_page_size);
27 let offset = (page - 1) * per_page;
2528
26 let mut query = activity_entity::Entity::find()
27 .filter(Ac::ProjectId.eq(project_id))
28 .order_by_desc(Ac::CreatedAt);
29 let mut query = activity_entity::Entity::find()
30 .filter(Ac::ProjectId.eq(project_id))
31 .order_by_desc(Ac::CreatedAt);
2932
30 if let Some(verb) = q.verb {
31 query = query.filter(Ac::Verb.eq(verb));
33 if let Some(verb) = q.verb {
34 query = query.filter(Ac::Verb.eq(verb));
35 }
36
37 let reader = svc.db.reader();
38 let paginator = query.paginate(&reader, per_page);
39 let total = paginator.num_items().await?;
40 let rows = paginator.fetch_page(offset / per_page).await?;
41 let items = rows.iter().map(activity_view).collect();
42 Ok(ActivityPage { total, items })
3243 }
33
34 let reader = svc.db.reader();
35 let paginator = query.paginate(&reader, per_page);
36 let total = paginator.num_items().await?;
37 let rows = paginator.fetch_page(offset / per_page).await?;
38 let items = rows.iter().map(activity_view).collect();
39 Ok(ActivityPage { total, items })
44 .await;
45 metrics::record_outcome(metrics::FEATURE_PROJECT_ACTIVITIES_LIST, &result);
46 result
4047}
4148
4249fn activity_view(m: &activity_entity::Model) -> ActivityView {
Modified lib/project/features/documents/usecase.rs lib/project/features/documents/usecase.rs +160 −129 View lib/project/features/documents/usecase.rs
@@ -9,6 +9,7 @@
99use crate::error::{ProjectError, ProjectResult};
1010use crate::features::lifecycle::usecase::log_activity;
1111use crate::features::ProjectService;
12use crate::metrics;
1213use crate::permission;
1314
1415use super::{CreateDocumentCmd, DocumentPage, DocumentView, UpdateDocumentCmd};
@@ -20,47 +21,53 @@
2021 project_id: Uuid,
2122 cmd: CreateDocumentCmd,
2223) -> ProjectResult<DocumentView> {
23 crate::features::authz::require(
24 svc,
25 actor,
26 space_id,
27 project_id,
28 permission::MANAGE_DOCUMENTS,
29 )
30 .await?;
24 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_CREATE);
25 let result: ProjectResult<DocumentView> = async {
26 crate::features::authz::require(
27 svc,
28 actor,
29 space_id,
30 project_id,
31 permission::MANAGE_DOCUMENTS,
32 )
33 .await?;
3134
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())
35 let slug_exists = doc_entity::Entity::find()
36 .filter(Dc::ProjectId.eq(project_id))
37 .filter(Dc::Slug.eq(&cmd.slug))
38 .count(&svc.db.reader())
39 .await?;
40 if slug_exists > 0 {
41 return Err(ProjectError::Conflict(format!(
42 "document slug '{}' already exists in project {project_id}",
43 cmd.slug
44 )));
45 }
46
47 let id = Uuid::new_v4();
48 let now = OffsetDateTime::now_utc();
49 let row = doc_entity::ActiveModel {
50 id: Set(id),
51 project_id: Set(project_id),
52 parent_id: Set(cmd.parent_id),
53 creator_id: Set(actor),
54 last_editor_id: Set(Some(actor)),
55 title: Set(cmd.title),
56 slug: Set(cmd.slug),
57 content: Set(cmd.content),
58 icon: Set(cmd.icon),
59 created_at: Set(now),
60 updated_at: Set(Some(now)),
61 }
62 .insert(&svc.db.writer())
3663 .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 }
4364
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)),
65 log_activity(svc, project_id, actor, "document_created", "document", Some(id), None).await;
66 Ok(document_view(&row))
5867 }
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))
68 .await;
69 metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_CREATE, &result);
70 result
6471}
6572
6673pub async fn list_documents(
@@ -69,16 +76,22 @@
6976 space_id: Uuid,
7077 project_id: Uuid,
7178) -> 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 })
79 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_LIST);
80 let result: ProjectResult<DocumentPage> = async {
81 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
82 .await?;
83 let rows = doc_entity::Entity::find()
84 .filter(Dc::ProjectId.eq(project_id))
85 .order_by_asc(Dc::Title)
86 .all(&svc.db.reader())
87 .await?;
88 let items = rows.iter().map(document_view).collect::<Vec<_>>();
89 let total = items.len() as u64;
90 Ok(DocumentPage { total, items })
91 }
92 .await;
93 metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_LIST, &result);
94 result
8295}
8396
8497pub async fn get_document(
@@ -88,16 +101,22 @@
88101 project_id: Uuid,
89102 doc_id: Uuid,
90103) -> 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))
104 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_GET);
105 let result: ProjectResult<DocumentView> = async {
106 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
107 .await?;
108 let row = doc_entity::Entity::find_by_id(doc_id)
109 .filter(Dc::ProjectId.eq(project_id))
110 .one(&svc.db.reader())
111 .await?
112 .ok_or_else(|| {
113 ProjectError::NotFound(format!("document {doc_id} in project {project_id}"))
114 })?;
115 Ok(document_view(&row))
116 }
117 .await;
118 metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_GET, &result);
119 result
101120}
102121
103122pub async fn update_document(
@@ -108,62 +127,68 @@
108127 doc_id: Uuid,
109128 cmd: UpdateDocumentCmd,
110129) -> 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 })?;
130 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_UPDATE);
131 let result: ProjectResult<DocumentView> = async {
132 crate::features::authz::require(
133 svc,
134 actor,
135 space_id,
136 project_id,
137 permission::MANAGE_DOCUMENTS,
138 )
139 .await?;
127140
128 if let Some(slug) = &cmd.slug
129 && slug != ¤t.slug
130 {
131 let slug_exists = doc_entity::Entity::find()
141 let current = doc_entity::Entity::find_by_id(doc_id)
132142 .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 )));
143 .one(&svc.db.reader())
144 .await?
145 .ok_or_else(|| {
146 ProjectError::NotFound(format!("document {doc_id} in project {project_id}"))
147 })?;
148
149 if let Some(slug) = &cmd.slug
150 && slug != ¤t.slug
151 {
152 let slug_exists = doc_entity::Entity::find()
153 .filter(Dc::ProjectId.eq(project_id))
154 .filter(Dc::Slug.eq(slug))
155 .filter(Dc::Id.ne(doc_id))
156 .count(&svc.db.reader())
157 .await?;
158 if slug_exists > 0 {
159 return Err(ProjectError::Conflict(format!(
160 "document slug '{slug}' already exists in project {project_id}"
161 )));
162 }
163 }
164
165 let mut active: doc_entity::ActiveModel = current.into();
166 if cmd.parent_id.is_some() {
167 active.parent_id = Set(cmd.parent_id);
168 }
169 if let Some(title) = cmd.title {
170 active.title = Set(title);
171 }
172 if let Some(slug) = cmd.slug {
173 active.slug = Set(slug);
174 }
175 if let Some(content) = cmd.content {
176 active.content = Set(content);
177 }
178 if cmd.icon.is_some() {
179 active.icon = Set(cmd.icon);
141180 }
142 }
181 active.last_editor_id = Set(Some(actor));
182 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
183 let updated = active.update(&svc.db.writer()).await?;
143184
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);
185 log_activity(svc, project_id, actor, "document_updated", "document", Some(doc_id), None)
186 .await;
187 Ok(document_view(&updated))
150188 }
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))
189 .await;
190 metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_UPDATE, &result);
191 result
167192}
168193
169194pub async fn delete_document(
@@ -173,29 +198,35 @@
173198 project_id: Uuid,
174199 doc_id: Uuid,
175200) -> ProjectResult<()> {
176 crate::features::authz::require(
177 svc,
178 actor,
179 space_id,
180 project_id,
181 permission::MANAGE_DOCUMENTS,
182 )
183 .await?;
201 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_DOCS_DELETE);
202 let result: ProjectResult<()> = async {
203 crate::features::authz::require(
204 svc,
205 actor,
206 space_id,
207 project_id,
208 permission::MANAGE_DOCUMENTS,
209 )
210 .await?;
211
212 let deleted = doc_entity::Entity::delete_many()
213 .filter(Dc::Id.eq(doc_id))
214 .filter(Dc::ProjectId.eq(project_id))
215 .exec(&svc.db.writer())
216 .await?;
217 if deleted.rows_affected == 0 {
218 return Err(ProjectError::NotFound(format!(
219 "document {doc_id} in project {project_id}"
220 )));
221 }
184222
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 )));
223 log_activity(svc, project_id, actor, "document_deleted", "document", Some(doc_id), None)
224 .await;
225 Ok(())
194226 }
195
196 log_activity(svc, project_id, actor, "document_deleted", "document", Some(doc_id), None)
197 .await;
198 Ok(())
227 .await;
228 metrics::record_outcome(metrics::FEATURE_PROJECT_DOCS_DELETE, &result);
229 result
199230}
200231
201232fn document_view(m: &doc_entity::Model) -> DocumentView {
Modified lib/project/features/favorites/usecase.rs lib/project/features/favorites/usecase.rs +68 −49 View lib/project/features/favorites/usecase.rs
@@ -7,6 +7,7 @@
77use crate::error::{ProjectError, ProjectResult};
88use crate::features::lifecycle::usecase::log_activity;
99use crate::features::ProjectService;
10use crate::metrics;
1011use crate::permission;
1112
1213use super::{AddFavoriteCmd, FavoritePage, FavoriteView};
@@ -18,33 +19,39 @@
1819 project_id: Uuid,
1920 cmd: AddFavoriteCmd,
2021) -> ProjectResult<FavoriteView> {
21 crate::features::authz::require(svc, actor, space_id, project_id, permission::STAR).await?;
22 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_FAVORITES_ADD);
23 let result: ProjectResult<FavoriteView> = async {
24 crate::features::authz::require(svc, actor, space_id, project_id, permission::STAR).await?;
2225
23 let existing = fav_entity::Entity::find()
24 .filter(Fc::ProjectId.eq(project_id))
25 .filter(Fc::UserId.eq(actor))
26 .one(&svc.db.reader())
27 .await?;
26 let existing = fav_entity::Entity::find()
27 .filter(Fc::ProjectId.eq(project_id))
28 .filter(Fc::UserId.eq(actor))
29 .one(&svc.db.reader())
30 .await?;
2831
29 let row = if let Some(m) = existing {
30 let mut active: fav_entity::ActiveModel = m.into();
31 active.folder = Set(cmd.folder);
32 active.update(&svc.db.writer()).await?
33 } else {
34 fav_entity::ActiveModel {
35 id: Set(Uuid::new_v4()),
36 project_id: Set(project_id),
37 user_id: Set(actor),
38 folder: Set(cmd.folder),
39 created_at: Set(OffsetDateTime::now_utc()),
40 }
41 .insert(&svc.db.writer())
42 .await?
43 };
32 let row = if let Some(m) = existing {
33 let mut active: fav_entity::ActiveModel = m.into();
34 active.folder = Set(cmd.folder);
35 active.update(&svc.db.writer()).await?
36 } else {
37 fav_entity::ActiveModel {
38 id: Set(Uuid::new_v4()),
39 project_id: Set(project_id),
40 user_id: Set(actor),
41 folder: Set(cmd.folder),
42 created_at: Set(OffsetDateTime::now_utc()),
43 }
44 .insert(&svc.db.writer())
45 .await?
46 };
4447
45 let _ = svc.cache.invalidate_favorites(actor).await;
46 log_activity(svc, project_id, actor, "favorited", "favorite", None, None).await;
47 Ok(favorite_view(&row))
48 let _ = svc.cache.invalidate_favorites(actor).await;
49 log_activity(svc, project_id, actor, "favorited", "favorite", None, None).await;
50 Ok(favorite_view(&row))
51 }
52 .await;
53 metrics::record_outcome(metrics::FEATURE_PROJECT_FAVORITES_ADD, &result);
54 result
4855}
4956
5057pub async fn list_favorites(
@@ -53,16 +60,22 @@
5360 space_id: Uuid,
5461 project_id: Uuid,
5562) -> ProjectResult<FavoritePage> {
56 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
57 .await?;
58 let rows = fav_entity::Entity::find()
59 .filter(Fc::ProjectId.eq(project_id))
60 .order_by_desc(Fc::CreatedAt)
61 .all(&svc.db.reader())
62 .await?;
63 let items = rows.iter().map(favorite_view).collect::<Vec<_>>();
64 let total = items.len() as u64;
65 Ok(FavoritePage { total, items })
63 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_FAVORITES_LIST);
64 let result: ProjectResult<FavoritePage> = async {
65 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
66 .await?;
67 let rows = fav_entity::Entity::find()
68 .filter(Fc::ProjectId.eq(project_id))
69 .order_by_desc(Fc::CreatedAt)
70 .all(&svc.db.reader())
71 .await?;
72 let items = rows.iter().map(favorite_view).collect::<Vec<_>>();
73 let total = items.len() as u64;
74 Ok(FavoritePage { total, items })
75 }
76 .await;
77 metrics::record_outcome(metrics::FEATURE_PROJECT_FAVORITES_LIST, &result);
78 result
6679}
6780
6881pub async fn remove_favorite(
@@ -72,23 +85,29 @@
7285 project_id: Uuid,
7386 favorite_id: Uuid,
7487) -> ProjectResult<()> {
75 crate::features::authz::require(svc, actor, space_id, project_id, permission::STAR).await?;
88 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_FAVORITES_REMOVE);
89 let result: ProjectResult<()> = async {
90 crate::features::authz::require(svc, actor, space_id, project_id, permission::STAR).await?;
91
92 let deleted = fav_entity::Entity::delete_many()
93 .filter(Fc::Id.eq(favorite_id))
94 .filter(Fc::ProjectId.eq(project_id))
95 .filter(Fc::UserId.eq(actor))
96 .exec(&svc.db.writer())
97 .await?;
98 if deleted.rows_affected == 0 {
99 return Err(ProjectError::NotFound(format!(
100 "favorite {favorite_id} in project {project_id}"
101 )));
102 }
76103
77 let deleted = fav_entity::Entity::delete_many()
78 .filter(Fc::Id.eq(favorite_id))
79 .filter(Fc::ProjectId.eq(project_id))
80 .filter(Fc::UserId.eq(actor))
81 .exec(&svc.db.writer())
82 .await?;
83 if deleted.rows_affected == 0 {
84 return Err(ProjectError::NotFound(format!(
85 "favorite {favorite_id} in project {project_id}"
86 )));
104 let _ = svc.cache.invalidate_favorites(actor).await;
105 log_activity(svc, project_id, actor, "unfavorited", "favorite", None, None).await;
106 Ok(())
87107 }
88
89 let _ = svc.cache.invalidate_favorites(actor).await;
90 log_activity(svc, project_id, actor, "unfavorited", "favorite", None, None).await;
91 Ok(())
108 .await;
109 metrics::record_outcome(metrics::FEATURE_PROJECT_FAVORITES_REMOVE, &result);
110 result
92111}
93112
94113fn favorite_view(m: &fav_entity::Model) -> FavoriteView {
Modified lib/project/features/lifecycle/usecase.rs lib/project/features/lifecycle/usecase.rs +167 −136 View lib/project/features/lifecycle/usecase.rs
@@ -10,6 +10,7 @@
1010
1111use crate::error::{ProjectError, ProjectResult};
1212use crate::features::ProjectService;
13use crate::metrics;
1314use crate::permission;
1415
1516use super::{
@@ -21,50 +22,56 @@
2122 actor: Uuid,
2223 cmd: CreateProjectCmd,
2324) -> ProjectResult<ProjectView> {
24 let perms =
25 crate::features::authz::resolve(svc, actor, cmd.space_id, Uuid::nil()).await?;
26 perms.require(permission::VIEW_PROJECT).map_err(|a| {
27 ProjectError::Forbidden(format!("caller not authorized: missing {a}"))
28 })?;
25 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECTS_CREATE);
26 let result: ProjectResult<ProjectView> = async {
27 let perms =
28 crate::features::authz::resolve(svc, actor, cmd.space_id, Uuid::nil()).await?;
29 perms.require(permission::VIEW_PROJECT).map_err(|a| {
30 ProjectError::Forbidden(format!("caller not authorized: missing {a}"))
31 })?;
2932
30 let id = Uuid::new_v4();
31 let now = OffsetDateTime::now_utc();
32 let model = project_entity::ActiveModel {
33 id: Set(id),
34 space_id: Set(cmd.space_id),
35 creator_id: Set(actor),
36 name: Set(cmd.name),
37 description: Set(cmd.description),
38 lead_id: Set(cmd.lead_id),
39 icon: Set(cmd.icon),
40 color: Set(cmd.color),
41 state: Set(DbState::Backlog),
42 target_date: Set(cmd.target_date),
43 started_at: Set(None),
44 completed_at: Set(None),
45 cancelled_at: Set(None),
46 health: Set(None),
47 sort_order: Set(0),
48 starred: Set(0),
49 created_at: Set(now),
50 updated_at: Set(None),
51 };
52 let inserted = model.insert(&svc.db.writer()).await?;
33 let id = Uuid::new_v4();
34 let now = OffsetDateTime::now_utc();
35 let model = project_entity::ActiveModel {
36 id: Set(id),
37 space_id: Set(cmd.space_id),
38 creator_id: Set(actor),
39 name: Set(cmd.name),
40 description: Set(cmd.description),
41 lead_id: Set(cmd.lead_id),
42 icon: Set(cmd.icon),
43 color: Set(cmd.color),
44 state: Set(DbState::Backlog),
45 target_date: Set(cmd.target_date),
46 started_at: Set(None),
47 completed_at: Set(None),
48 cancelled_at: Set(None),
49 health: Set(None),
50 sort_order: Set(0),
51 starred: Set(0),
52 created_at: Set(now),
53 updated_at: Set(None),
54 };
55 let inserted = model.insert(&svc.db.writer()).await?;
5356
54 let _ = member_entity::ActiveModel {
55 id: Set(Uuid::new_v4()),
56 project_id: Set(id),
57 user_id: Set(actor),
58 role: Set(ProjectMemberRole::Owner),
59 joined_at: Set(now),
60 updated_at: Set(None),
61 }
62 .insert(&svc.db.writer())
63 .await?;
57 let _ = member_entity::ActiveModel {
58 id: Set(Uuid::new_v4()),
59 project_id: Set(id),
60 user_id: Set(actor),
61 role: Set(ProjectMemberRole::Owner),
62 joined_at: Set(now),
63 updated_at: Set(None),
64 }
65 .insert(&svc.db.writer())
66 .await?;
6467
65 log_activity(svc, id, actor, "created", "self", None, None).await;
68 log_activity(svc, id, actor, "created", "self", None, None).await;
6669
67 Ok(view_from(&inserted))
70 Ok(view_from(&inserted))
71 }
72 .await;
73 metrics::record_outcome(metrics::FEATURE_PROJECTS_CREATE, &result);
74 result
6875}
6976
7077pub async fn get_project(
@@ -73,14 +80,20 @@
7380 space_id: Uuid,
7481 project_id: Uuid,
7582) -> ProjectResult<ProjectView> {
76 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
77 .await?;
78 use db::conn::sea_orm::EntityTrait;
79 let row = project_entity::Entity::find_by_id(project_id)
80 .one(&svc.db.reader())
81 .await?
82 .ok_or_else(|| ProjectError::NotFound(format!("project {project_id}")))?;
83 Ok(view_from(&row))
83 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECTS_GET);
84 let result: ProjectResult<ProjectView> = async {
85 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
86 .await?;
87 use db::conn::sea_orm::EntityTrait;
88 let row = project_entity::Entity::find_by_id(project_id)
89 .one(&svc.db.reader())
90 .await?
91 .ok_or_else(|| ProjectError::NotFound(format!("project {project_id}")))?;
92 Ok(view_from(&row))
93 }
94 .await;
95 metrics::record_outcome(metrics::FEATURE_PROJECTS_GET, &result);
96 result
8497}
8598
8699pub async fn list_projects(
@@ -88,30 +101,36 @@
88101 actor: Uuid,
89102 q: ListProjectQuery,
90103) -> ProjectResult<ProjectPage> {
91 crate::features::authz::resolve(svc, actor, q.space_id, Uuid::nil()).await?;
92 let page = q.page.max(1);
93 let per_page = q.per_page.clamp(1, svc.config.max_page_size);
94 let offset = (page - 1) * per_page;
104 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECTS_LIST);
105 let result: ProjectResult<ProjectPage> = async {
106 crate::features::authz::resolve(svc, actor, q.space_id, Uuid::nil()).await?;
107 let page = q.page.max(1);
108 let per_page = q.per_page.clamp(1, svc.config.max_page_size);
109 let offset = (page - 1) * per_page;
95110
96 use db::conn::sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, PaginatorTrait};
97 let mut query = project_entity::Entity::find()
98 .filter(project_entity::Column::SpaceId.eq(q.space_id))
99 .order_by_asc(project_entity::Column::SortOrder)
100 .order_by_desc(project_entity::Column::CreatedAt);
111 use db::conn::sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, PaginatorTrait};
112 let mut query = project_entity::Entity::find()
113 .filter(project_entity::Column::SpaceId.eq(q.space_id))
114 .order_by_asc(project_entity::Column::SortOrder)
115 .order_by_desc(project_entity::Column::CreatedAt);
101116
102 if let Some(state) = q.state {
103 query = query.filter(project_entity::Column::State.eq(state as i32));
104 }
105 if let Some(lead) = q.lead_id {
106 query = query.filter(project_entity::Column::LeadId.eq(lead));
107 }
117 if let Some(state) = q.state {
118 query = query.filter(project_entity::Column::State.eq(state as i32));
119 }
120 if let Some(lead) = q.lead_id {
121 query = query.filter(project_entity::Column::LeadId.eq(lead));
122 }
108123
109 let reader = svc.db.reader();
110 let paginator = query.paginate(&reader, per_page);
111 let total = paginator.num_items().await?;
112 let rows = paginator.fetch_page(offset / per_page).await?;
113 let items = rows.iter().map(view_from).collect();
114 Ok(ProjectPage { total, items })
124 let reader = svc.db.reader();
125 let paginator = query.paginate(&reader, per_page);
126 let total = paginator.num_items().await?;
127 let rows = paginator.fetch_page(offset / per_page).await?;
128 let items = rows.iter().map(view_from).collect();
129 Ok(ProjectPage { total, items })
130 }
131 .await;
132 metrics::record_outcome(metrics::FEATURE_PROJECTS_LIST, &result);
133 result
115134}
116135
117136pub async fn update_project(
@@ -121,64 +140,70 @@
121140 project_id: Uuid,
122141 cmd: UpdateProjectCmd,
123142) -> ProjectResult<ProjectView> {
124 crate::features::authz::require(svc, actor, space_id, project_id, permission::EDIT_PROJECT)
125 .await?;
143 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECTS_UPDATE);
144 let result: ProjectResult<ProjectView> = async {
145 crate::features::authz::require(svc, actor, space_id, project_id, permission::EDIT_PROJECT)
146 .await?;
126147
127 let current = {
128 use db::conn::sea_orm::EntityTrait;
129 project_entity::Entity::find_by_id(project_id)
130 .one(&svc.db.reader())
131 .await?
132 .ok_or_else(|| ProjectError::NotFound(format!("project {project_id}")))?
133 };
134
135 let mut active: project_entity::ActiveModel = current.into_active_model();
136 if let Some(name) = cmd.name {
137 active.name = Set(name);
138 }
139 if let Some(desc) = cmd.description {
140 active.description = Set(Some(desc));
141 }
142 if cmd.lead_id.is_some() {
143 active.lead_id = Set(cmd.lead_id);
144 }
145 if cmd.icon.is_some() {
146 active.icon = Set(cmd.icon);
147 }
148 if cmd.color.is_some() {
149 active.color = Set(cmd.color);
150 }
151 if let Some(state) = cmd.state {
152 apply_state_transition(&mut active, state);
153 }
154 if cmd.target_date.is_some() {
155 active.target_date = Set(cmd.target_date);
156 }
157 if let Some(h) = cmd.health {
158 let db_h = match h {
159 crate::features::lifecycle::ProjectHealth::OnTrack => {
160 Some(db::project::project_health::ProjectHealth::OnTrack)
161 }
162 crate::features::lifecycle::ProjectHealth::AtRisk => {
163 Some(db::project::project_health::ProjectHealth::AtRisk)
164 }
165 crate::features::lifecycle::ProjectHealth::OffTrack => {
166 Some(db::project::project_health::ProjectHealth::OffTrack)
167 }
168 crate::features::lifecycle::ProjectHealth::None => None,
148 let current = {
149 use db::conn::sea_orm::EntityTrait;
150 project_entity::Entity::find_by_id(project_id)
151 .one(&svc.db.reader())
152 .await?
153 .ok_or_else(|| ProjectError::NotFound(format!("project {project_id}")))?
169154 };
170 active.health = Set(db_h);
171 }
172 if let Some(so) = cmd.sort_order {
173 active.sort_order = Set(so);
174 }
175 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
176155
177 let updated = active.update(&svc.db.writer()).await?;
178 let _ = svc.cache.invalidate_project(project_id).await;
156 let mut active: project_entity::ActiveModel = current.into_active_model();
157 if let Some(name) = cmd.name {
158 active.name = Set(name);
159 }
160 if let Some(desc) = cmd.description {
161 active.description = Set(Some(desc));
162 }
163 if cmd.lead_id.is_some() {
164 active.lead_id = Set(cmd.lead_id);
165 }
166 if cmd.icon.is_some() {
167 active.icon = Set(cmd.icon);
168 }
169 if cmd.color.is_some() {
170 active.color = Set(cmd.color);
171 }
172 if let Some(state) = cmd.state {
173 apply_state_transition(&mut active, state);
174 }
175 if cmd.target_date.is_some() {
176 active.target_date = Set(cmd.target_date);
177 }
178 if let Some(h) = cmd.health {
179 let db_h = match h {
180 crate::features::lifecycle::ProjectHealth::OnTrack => {
181 Some(db::project::project_health::ProjectHealth::OnTrack)
182 }
183 crate::features::lifecycle::ProjectHealth::AtRisk => {
184 Some(db::project::project_health::ProjectHealth::AtRisk)
185 }
186 crate::features::lifecycle::ProjectHealth::OffTrack => {
187 Some(db::project::project_health::ProjectHealth::OffTrack)
188 }
189 crate::features::lifecycle::ProjectHealth::None => None,
190 };
191 active.health = Set(db_h);
192 }
193 if let Some(so) = cmd.sort_order {
194 active.sort_order = Set(so);
195 }
196 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
179197
180 log_activity(svc, project_id, actor, "updated", "self", None, None).await;
181 Ok(view_from(&updated))
198 let updated = active.update(&svc.db.writer()).await?;
199 let _ = svc.cache.invalidate_project(project_id).await;
200
201 log_activity(svc, project_id, actor, "updated", "self", None, None).await;
202 Ok(view_from(&updated))
203 }
204 .await;
205 metrics::record_outcome(metrics::FEATURE_PROJECTS_UPDATE, &result);
206 result
182207}
183208
184209pub async fn delete_project(
@@ -187,20 +212,26 @@
187212 space_id: Uuid,
188213 project_id: Uuid,
189214) -> ProjectResult<()> {
190 crate::features::authz::require(
191 svc,
192 actor,
193 space_id,
194 project_id,
195 permission::DELETE_PROJECT,
196 )
197 .await?;
198 use db::conn::sea_orm::EntityTrait;
199 project_entity::Entity::delete_by_id(project_id)
200 .exec(&svc.db.writer())
215 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECTS_DELETE);
216 let result: ProjectResult<()> = async {
217 crate::features::authz::require(
218 svc,
219 actor,
220 space_id,
221 project_id,
222 permission::DELETE_PROJECT,
223 )
201224 .await?;
202 let _ = svc.cache.invalidate_project(project_id).await;
203 Ok(())
225 use db::conn::sea_orm::EntityTrait;
226 project_entity::Entity::delete_by_id(project_id)
227 .exec(&svc.db.writer())
228 .await?;
229 let _ = svc.cache.invalidate_project(project_id).await;
230 Ok(())
231 }
232 .await;
233 metrics::record_outcome(metrics::FEATURE_PROJECTS_DELETE, &result);
234 result
204235}
205236
206237fn apply_state_transition(
Modified lib/project/features/links/usecase.rs lib/project/features/links/usecase.rs +79 −60 View lib/project/features/links/usecase.rs
@@ -7,6 +7,7 @@
77use crate::error::{ProjectError, ProjectResult};
88use crate::features::lifecycle::usecase::log_activity;
99use crate::features::ProjectService;
10use crate::metrics;
1011use crate::permission;
1112
1213use super::types::{AddLinkCmd, LinkKind, LinkPage, LinkView};
@@ -18,38 +19,44 @@
1819 project_id: Uuid,
1920 cmd: AddLinkCmd,
2021) -> ProjectResult<LinkView> {
21 crate::features::authz::require(
22 svc,
23 actor,
24 space_id,
25 project_id,
26 permission::MANAGE_LINKS,
27 )
28 .await?;
22 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_LINKS_ADD);
23 let result: ProjectResult<LinkView> = async {
24 crate::features::authz::require(
25 svc,
26 actor,
27 space_id,
28 project_id,
29 permission::MANAGE_LINKS,
30 )
31 .await?;
2932
30 if cmd.related_project_id == project_id {
31 return Err(ProjectError::BadRequest(
32 "a project cannot link to itself".into(),
33 ));
34 }
33 if cmd.related_project_id == project_id {
34 return Err(ProjectError::BadRequest(
35 "a project cannot link to itself".into(),
36 ));
37 }
38
39 let kind: db::project::project_links::ProjectLinkKind = cmd.kind.try_into()?;
40 let id = Uuid::new_v4();
41 let now = OffsetDateTime::now_utc();
42 let row = link_entity::ActiveModel {
43 id: Set(id),
44 project_id: Set(project_id),
45 related_project_id: Set(cmd.related_project_id),
46 kind: Set(kind),
47 created_by: Set(actor),
48 created_at: Set(now),
49 }
50 .insert(&svc.db.writer())
51 .await?;
3552
36 let kind: db::project::project_links::ProjectLinkKind = cmd.kind.try_into()?;
37 let id = Uuid::new_v4();
38 let now = OffsetDateTime::now_utc();
39 let row = link_entity::ActiveModel {
40 id: Set(id),
41 project_id: Set(project_id),
42 related_project_id: Set(cmd.related_project_id),
43 kind: Set(kind),
44 created_by: Set(actor),
45 created_at: Set(now),
53 log_activity(svc, project_id, actor, "link_added", "link", Some(id), None).await;
54 let _ = svc.cache.invalidate_project(project_id).await;
55 Ok(link_view(&row))
4656 }
47 .insert(&svc.db.writer())
48 .await?;
49
50 log_activity(svc, project_id, actor, "link_added", "link", Some(id), None).await;
51 let _ = svc.cache.invalidate_project(project_id).await;
52 Ok(link_view(&row))
57 .await;
58 metrics::record_outcome(metrics::FEATURE_PROJECT_LINKS_ADD, &result);
59 result
5360}
5461
5562pub async fn list_links(
@@ -58,16 +65,22 @@
5865 space_id: Uuid,
5966 project_id: Uuid,
6067) -> ProjectResult<LinkPage> {
61 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
62 .await?;
63 let rows = link_entity::Entity::find()
64 .filter(Lc::ProjectId.eq(project_id))
65 .order_by_desc(Lc::CreatedAt)
66 .all(&svc.db.reader())
67 .await?;
68 let items = rows.iter().map(link_view).collect::<Vec<_>>();
69 let total = items.len() as u64;
70 Ok(LinkPage { total, items })
68 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_LINKS_LIST);
69 let result: ProjectResult<LinkPage> = async {
70 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
71 .await?;
72 let rows = link_entity::Entity::find()
73 .filter(Lc::ProjectId.eq(project_id))
74 .order_by_desc(Lc::CreatedAt)
75 .all(&svc.db.reader())
76 .await?;
77 let items = rows.iter().map(link_view).collect::<Vec<_>>();
78 let total = items.len() as u64;
79 Ok(LinkPage { total, items })
80 }
81 .await;
82 metrics::record_outcome(metrics::FEATURE_PROJECT_LINKS_LIST, &result);
83 result
7184}
7285
7386pub async fn remove_link(
@@ -77,29 +90,35 @@
7790 project_id: Uuid,
7891 link_id: Uuid,
7992) -> ProjectResult<()> {
80 crate::features::authz::require(
81 svc,
82 actor,
83 space_id,
84 project_id,
85 permission::MANAGE_LINKS,
86 )
87 .await?;
88
89 let deleted = link_entity::Entity::delete_many()
90 .filter(Lc::Id.eq(link_id))
91 .filter(Lc::ProjectId.eq(project_id))
92 .exec(&svc.db.writer())
93 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_LINKS_REMOVE);
94 let result: ProjectResult<()> = async {
95 crate::features::authz::require(
96 svc,
97 actor,
98 space_id,
99 project_id,
100 permission::MANAGE_LINKS,
101 )
93102 .await?;
94 if deleted.rows_affected == 0 {
95 return Err(ProjectError::NotFound(format!(
96 "link {link_id} in project {project_id}"
97 )));
98 }
99103
100 log_activity(svc, project_id, actor, "link_removed", "link", Some(link_id), None).await;
101 let _ = svc.cache.invalidate_project(project_id).await;
102 Ok(())
104 let deleted = link_entity::Entity::delete_many()
105 .filter(Lc::Id.eq(link_id))
106 .filter(Lc::ProjectId.eq(project_id))
107 .exec(&svc.db.writer())
108 .await?;
109 if deleted.rows_affected == 0 {
110 return Err(ProjectError::NotFound(format!(
111 "link {link_id} in project {project_id}"
112 )));
113 }
114
115 log_activity(svc, project_id, actor, "link_removed", "link", Some(link_id), None).await;
116 let _ = svc.cache.invalidate_project(project_id).await;
117 Ok(())
118 }
119 .await;
120 metrics::record_outcome(metrics::FEATURE_PROJECT_LINKS_REMOVE, &result);
121 result
103122}
104123
105124fn link_view(m: &link_entity::Model) -> LinkView {
Modified lib/project/features/members/usecase.rs lib/project/features/members/usecase.rs +131 −100 View lib/project/features/members/usecase.rs
@@ -8,6 +8,7 @@
88use crate::error::{ProjectError, ProjectResult};
99use crate::features::lifecycle::usecase::log_activity;
1010use crate::features::ProjectService;
11use crate::metrics;
1112use crate::permission;
1213
1314use super::types::{AddMemberCmd, MemberPage, MemberView, UpdateMemberCmd};
@@ -19,45 +20,51 @@
1920 project_id: Uuid,
2021 cmd: AddMemberCmd,
2122) -> ProjectResult<MemberView> {
22 crate::features::authz::require(
23 svc,
24 actor,
25 space_id,
26 project_id,
27 permission::MANAGE_MEMBERS,
28 )
29 .await?;
23 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_MEMBERS_ADD);
24 let result: ProjectResult<MemberView> = async {
25 crate::features::authz::require(
26 svc,
27 actor,
28 space_id,
29 project_id,
30 permission::MANAGE_MEMBERS,
31 )
32 .await?;
3033
31 let role: DbRole = cmd.role.try_into()?;
32 let now = OffsetDateTime::now_utc();
33 let existing = db::project::project_members::Entity::find()
34 .filter(Mc::ProjectId.eq(project_id))
35 .filter(Mc::UserId.eq(cmd.user_id))
36 .one(&svc.db.reader())
37 .await?;
34 let role: DbRole = cmd.role.try_into()?;
35 let now = OffsetDateTime::now_utc();
36 let existing = db::project::project_members::Entity::find()
37 .filter(Mc::ProjectId.eq(project_id))
38 .filter(Mc::UserId.eq(cmd.user_id))
39 .one(&svc.db.reader())
40 .await?;
3841
39 let row = if let Some(m) = existing {
40 let mut active: db::project::project_members::ActiveModel = m.into();
41 active.role = Set(role);
42 active.updated_at = Set(Some(now));
43 active.update(&svc.db.writer()).await?
44 } else {
45 db::project::project_members::ActiveModel {
46 id: Set(Uuid::new_v4()),
47 project_id: Set(project_id),
48 user_id: Set(cmd.user_id),
49 role: Set(role),
50 joined_at: Set(now),
51 updated_at: Set(None),
52 }
53 .insert(&svc.db.writer())
54 .await?
55 };
42 let row = if let Some(m) = existing {
43 let mut active: db::project::project_members::ActiveModel = m.into();
44 active.role = Set(role);
45 active.updated_at = Set(Some(now));
46 active.update(&svc.db.writer()).await?
47 } else {
48 db::project::project_members::ActiveModel {
49 id: Set(Uuid::new_v4()),
50 project_id: Set(project_id),
51 user_id: Set(cmd.user_id),
52 role: Set(role),
53 joined_at: Set(now),
54 updated_at: Set(None),
55 }
56 .insert(&svc.db.writer())
57 .await?
58 };
5659
57 let _ = svc.cache.invalidate_member_role(project_id, cmd.user_id).await;
58 log_activity(svc, project_id, actor, "member_added", "member", Some(cmd.user_id), None)
59 .await;
60 Ok(member_view(&row))
60 let _ = svc.cache.invalidate_member_role(project_id, cmd.user_id).await;
61 log_activity(svc, project_id, actor, "member_added", "member", Some(cmd.user_id), None)
62 .await;
63 Ok(member_view(&row))
64 }
65 .await;
66 metrics::record_outcome(metrics::FEATURE_PROJECT_MEMBERS_ADD, &result);
67 result
6168}
6269
6370pub async fn list_members(
@@ -66,16 +73,22 @@
6673 space_id: Uuid,
6774 project_id: Uuid,
6875) -> ProjectResult<MemberPage> {
69 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
70 .await?;
71 let rows = db::project::project_members::Entity::find()
72 .filter(Mc::ProjectId.eq(project_id))
73 .order_by_desc(Mc::JoinedAt)
74 .all(&svc.db.reader())
75 .await?;
76 let items = rows.iter().map(member_view).collect::<Vec<_>>();
77 let total = items.len() as u64;
78 Ok(MemberPage { total, items })
76 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_MEMBERS_LIST);
77 let result: ProjectResult<MemberPage> = async {
78 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
79 .await?;
80 let rows = db::project::project_members::Entity::find()
81 .filter(Mc::ProjectId.eq(project_id))
82 .order_by_desc(Mc::JoinedAt)
83 .all(&svc.db.reader())
84 .await?;
85 let items = rows.iter().map(member_view).collect::<Vec<_>>();
86 let total = items.len() as u64;
87 Ok(MemberPage { total, items })
88 }
89 .await;
90 metrics::record_outcome(metrics::FEATURE_PROJECT_MEMBERS_LIST, &result);
91 result
7992}
8093
8194pub async fn get_member(
@@ -85,15 +98,21 @@
8598 project_id: Uuid,
8699 user_id: Uuid,
87100) -> ProjectResult<MemberView> {
88 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
89 .await?;
90 let row = db::project::project_members::Entity::find()
91 .filter(Mc::ProjectId.eq(project_id))
92 .filter(Mc::UserId.eq(user_id))
93 .one(&svc.db.reader())
94 .await?
95 .ok_or_else(|| ProjectError::NotFound(format!("member {user_id} in project {project_id}")))?;
96 Ok(member_view(&row))
101 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_MEMBERS_GET);
102 let result: ProjectResult<MemberView> = async {
103 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
104 .await?;
105 let row = db::project::project_members::Entity::find()
106 .filter(Mc::ProjectId.eq(project_id))
107 .filter(Mc::UserId.eq(user_id))
108 .one(&svc.db.reader())
109 .await?
110 .ok_or_else(|| ProjectError::NotFound(format!("member {user_id} in project {project_id}")))?;
111 Ok(member_view(&row))
112 }
113 .await;
114 metrics::record_outcome(metrics::FEATURE_PROJECT_MEMBERS_GET, &result);
115 result
97116}
98117
99118pub async fn update_member(
@@ -104,32 +123,38 @@
104123 user_id: Uuid,
105124 cmd: UpdateMemberCmd,
106125) -> ProjectResult<MemberView> {
107 crate::features::authz::require(
108 svc,
109 actor,
110 space_id,
111 project_id,
112 permission::MANAGE_MEMBERS,
113 )
114 .await?;
126 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_MEMBERS_UPDATE);
127 let result: ProjectResult<MemberView> = async {
128 crate::features::authz::require(
129 svc,
130 actor,
131 space_id,
132 project_id,
133 permission::MANAGE_MEMBERS,
134 )
135 .await?;
115136
116 let row = db::project::project_members::Entity::find()
117 .filter(Mc::ProjectId.eq(project_id))
118 .filter(Mc::UserId.eq(user_id))
119 .one(&svc.db.reader())
120 .await?
121 .ok_or_else(|| ProjectError::NotFound(format!("member {user_id} in project {project_id}")))?;
137 let row = db::project::project_members::Entity::find()
138 .filter(Mc::ProjectId.eq(project_id))
139 .filter(Mc::UserId.eq(user_id))
140 .one(&svc.db.reader())
141 .await?
142 .ok_or_else(|| ProjectError::NotFound(format!("member {user_id} in project {project_id}")))?;
122143
123 let role: DbRole = cmd.role.try_into()?;
124 let mut active: db::project::project_members::ActiveModel = row.into();
125 active.role = Set(role);
126 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
127 let updated = active.update(&svc.db.writer()).await?;
144 let role: DbRole = cmd.role.try_into()?;
145 let mut active: db::project::project_members::ActiveModel = row.into();
146 active.role = Set(role);
147 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
148 let updated = active.update(&svc.db.writer()).await?;
128149
129 let _ = svc.cache.invalidate_member_role(project_id, user_id).await;
130 log_activity(svc, project_id, actor, "member_updated", "member", Some(user_id), None)
131 .await;
132 Ok(member_view(&updated))
150 let _ = svc.cache.invalidate_member_role(project_id, user_id).await;
151 log_activity(svc, project_id, actor, "member_updated", "member", Some(user_id), None)
152 .await;
153 Ok(member_view(&updated))
154 }
155 .await;
156 metrics::record_outcome(metrics::FEATURE_PROJECT_MEMBERS_UPDATE, &result);
157 result
133158}
134159
135160pub async fn remove_member(
@@ -139,30 +164,36 @@
139164 project_id: Uuid,
140165 user_id: Uuid,
141166) -> ProjectResult<()> {
142 crate::features::authz::require(
143 svc,
144 actor,
145 space_id,
146 project_id,
147 permission::MANAGE_MEMBERS,
148 )
149 .await?;
150
151 let deleted = db::project::project_members::Entity::delete_many()
152 .filter(Mc::ProjectId.eq(project_id))
153 .filter(Mc::UserId.eq(user_id))
154 .exec(&svc.db.writer())
167 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_MEMBERS_REMOVE);
168 let result: ProjectResult<()> = async {
169 crate::features::authz::require(
170 svc,
171 actor,
172 space_id,
173 project_id,
174 permission::MANAGE_MEMBERS,
175 )
155176 .await?;
156 if deleted.rows_affected == 0 {
157 return Err(ProjectError::NotFound(format!(
158 "member {user_id} in project {project_id}"
159 )));
177
178 let deleted = db::project::project_members::Entity::delete_many()
179 .filter(Mc::ProjectId.eq(project_id))
180 .filter(Mc::UserId.eq(user_id))
181 .exec(&svc.db.writer())
182 .await?;
183 if deleted.rows_affected == 0 {
184 return Err(ProjectError::NotFound(format!(
185 "member {user_id} in project {project_id}"
186 )));
187 }
188
189 let _ = svc.cache.invalidate_member_role(project_id, user_id).await;
190 log_activity(svc, project_id, actor, "member_removed", "member", Some(user_id), None)
191 .await;
192 Ok(())
160193 }
161
162 let _ = svc.cache.invalidate_member_role(project_id, user_id).await;
163 log_activity(svc, project_id, actor, "member_removed", "member", Some(user_id), None)
164 .await;
165 Ok(())
194 .await;
195 metrics::record_outcome(metrics::FEATURE_PROJECT_MEMBERS_REMOVE, &result);
196 result
166197}
167198
168199fn member_view(m: &db::project::project_members::Model) -> MemberView {
Modified lib/project/features/views/usecase.rs lib/project/features/views/usecase.rs +138 −107 View lib/project/features/views/usecase.rs
@@ -9,6 +9,7 @@
99use crate::error::{ProjectError, ProjectResult};
1010use crate::features::lifecycle::usecase::log_activity;
1111use crate::features::ProjectService;
12use crate::metrics;
1213use crate::permission;
1314
1415use super::{CreateViewCmd, UpdateViewCmd, ViewPage, ViewView};
@@ -20,37 +21,43 @@
2021 project_id: Uuid,
2122 cmd: CreateViewCmd,
2223) -> ProjectResult<ViewView> {
23 crate::features::authz::require(
24 svc,
25 actor,
26 space_id,
27 project_id,
28 permission::MANAGE_VIEWS,
29 )
30 .await?;
24 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_VIEWS_CREATE);
25 let result: ProjectResult<ViewView> = async {
26 crate::features::authz::require(
27 svc,
28 actor,
29 space_id,
30 project_id,
31 permission::MANAGE_VIEWS,
32 )
33 .await?;
34
35 let id = Uuid::new_v4();
36 let now = OffsetDateTime::now_utc();
37 let row = view_entity::ActiveModel {
38 id: Set(id),
39 project_id: Set(project_id),
40 creator_id: Set(actor),
41 name: Set(cmd.name),
42 description: Set(cmd.description),
43 filter: Set(cmd.filter),
44 sort_by: Set(cmd.sort_by),
45 group_by: Set(cmd.group_by),
46 layout: Set(cmd.layout),
47 is_shared: Set(cmd.is_shared),
48 sort_order: Set(0),
49 created_at: Set(now),
50 updated_at: Set(None),
51 }
52 .insert(&svc.db.writer())
53 .await?;
3154
32 let id = Uuid::new_v4();
33 let now = OffsetDateTime::now_utc();
34 let row = view_entity::ActiveModel {
35 id: Set(id),
36 project_id: Set(project_id),
37 creator_id: Set(actor),
38 name: Set(cmd.name),
39 description: Set(cmd.description),
40 filter: Set(cmd.filter),
41 sort_by: Set(cmd.sort_by),
42 group_by: Set(cmd.group_by),
43 layout: Set(cmd.layout),
44 is_shared: Set(cmd.is_shared),
45 sort_order: Set(0),
46 created_at: Set(now),
47 updated_at: Set(None),
55 log_activity(svc, project_id, actor, "view_created", "view", Some(id), None).await;
56 Ok(view_view(&row))
4857 }
49 .insert(&svc.db.writer())
50 .await?;
51
52 log_activity(svc, project_id, actor, "view_created", "view", Some(id), None).await;
53 Ok(view_view(&row))
58 .await;
59 metrics::record_outcome(metrics::FEATURE_PROJECT_VIEWS_CREATE, &result);
60 result
5461}
5562
5663pub async fn list_views(
@@ -59,17 +66,23 @@
5966 space_id: Uuid,
6067 project_id: Uuid,
6168) -> ProjectResult<ViewPage> {
62 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
63 .await?;
64 let rows = view_entity::Entity::find()
65 .filter(Vc::ProjectId.eq(project_id))
66 .order_by_asc(Vc::SortOrder)
67 .order_by_desc(Vc::CreatedAt)
68 .all(&svc.db.reader())
69 .await?;
70 let items = rows.iter().map(view_view).collect::<Vec<_>>();
71 let total = items.len() as u64;
72 Ok(ViewPage { total, items })
69 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_VIEWS_LIST);
70 let result: ProjectResult<ViewPage> = async {
71 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
72 .await?;
73 let rows = view_entity::Entity::find()
74 .filter(Vc::ProjectId.eq(project_id))
75 .order_by_asc(Vc::SortOrder)
76 .order_by_desc(Vc::CreatedAt)
77 .all(&svc.db.reader())
78 .await?;
79 let items = rows.iter().map(view_view).collect::<Vec<_>>();
80 let total = items.len() as u64;
81 Ok(ViewPage { total, items })
82 }
83 .await;
84 metrics::record_outcome(metrics::FEATURE_PROJECT_VIEWS_LIST, &result);
85 result
7386}
7487
7588pub async fn get_view(
@@ -79,14 +92,20 @@
7992 project_id: Uuid,
8093 view_id: Uuid,
8194) -> ProjectResult<ViewView> {
82 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
83 .await?;
84 let row = view_entity::Entity::find_by_id(view_id)
85 .filter(Vc::ProjectId.eq(project_id))
86 .one(&svc.db.reader())
87 .await?
88 .ok_or_else(|| ProjectError::NotFound(format!("view {view_id} in project {project_id}")))?;
89 Ok(view_view(&row))
95 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_VIEWS_GET);
96 let result: ProjectResult<ViewView> = async {
97 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
98 .await?;
99 let row = view_entity::Entity::find_by_id(view_id)
100 .filter(Vc::ProjectId.eq(project_id))
101 .one(&svc.db.reader())
102 .await?
103 .ok_or_else(|| ProjectError::NotFound(format!("view {view_id} in project {project_id}")))?;
104 Ok(view_view(&row))
105 }
106 .await;
107 metrics::record_outcome(metrics::FEATURE_PROJECT_VIEWS_GET, &result);
108 result
90109}
91110
92111pub async fn update_view(
@@ -97,48 +116,54 @@
97116 view_id: Uuid,
98117 cmd: UpdateViewCmd,
99118) -> ProjectResult<ViewView> {
100 crate::features::authz::require(
101 svc,
102 actor,
103 space_id,
104 project_id,
105 permission::MANAGE_VIEWS,
106 )
107 .await?;
119 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_VIEWS_UPDATE);
120 let result: ProjectResult<ViewView> = async {
121 crate::features::authz::require(
122 svc,
123 actor,
124 space_id,
125 project_id,
126 permission::MANAGE_VIEWS,
127 )
128 .await?;
108129
109 let current = view_entity::Entity::find_by_id(view_id)
110 .filter(Vc::ProjectId.eq(project_id))
111 .one(&svc.db.reader())
112 .await?
113 .ok_or_else(|| ProjectError::NotFound(format!("view {view_id} in project {project_id}")))?;
130 let current = view_entity::Entity::find_by_id(view_id)
131 .filter(Vc::ProjectId.eq(project_id))
132 .one(&svc.db.reader())
133 .await?
134 .ok_or_else(|| ProjectError::NotFound(format!("view {view_id} in project {project_id}")))?;
114135
115 let mut active: view_entity::ActiveModel = current.into();
116 if let Some(name) = cmd.name {
117 active.name = Set(name);
118 }
119 if cmd.description.is_some() {
120 active.description = Set(cmd.description);
121 }
122 if let Some(filter) = cmd.filter {
123 active.filter = Set(filter);
136 let mut active: view_entity::ActiveModel = current.into();
137 if let Some(name) = cmd.name {
138 active.name = Set(name);
139 }
140 if cmd.description.is_some() {
141 active.description = Set(cmd.description);
142 }
143 if let Some(filter) = cmd.filter {
144 active.filter = Set(filter);
145 }
146 if let Some(sort_by) = cmd.sort_by {
147 active.sort_by = Set(sort_by);
148 }
149 if cmd.group_by.is_some() {
150 active.group_by = Set(cmd.group_by);
151 }
152 if let Some(layout) = cmd.layout {
153 active.layout = Set(layout);
154 }
155 if let Some(shared) = cmd.is_shared {
156 active.is_shared = Set(shared);
157 }
158 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
159 let updated = active.update(&svc.db.writer()).await?;
160
161 log_activity(svc, project_id, actor, "view_updated", "view", Some(view_id), None).await;
162 Ok(view_view(&updated))
124163 }
125 if let Some(sort_by) = cmd.sort_by {
126 active.sort_by = Set(sort_by);
127 }
128 if cmd.group_by.is_some() {
129 active.group_by = Set(cmd.group_by);
130 }
131 if let Some(layout) = cmd.layout {
132 active.layout = Set(layout);
133 }
134 if let Some(shared) = cmd.is_shared {
135 active.is_shared = Set(shared);
136 }
137 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
138 let updated = active.update(&svc.db.writer()).await?;
139
140 log_activity(svc, project_id, actor, "view_updated", "view", Some(view_id), None).await;
141 Ok(view_view(&updated))
164 .await;
165 metrics::record_outcome(metrics::FEATURE_PROJECT_VIEWS_UPDATE, &result);
166 result
142167}
143168
144169pub async fn delete_view(
@@ -148,28 +173,34 @@
148173 project_id: Uuid,
149174 view_id: Uuid,
150175) -> ProjectResult<()> {
151 crate::features::authz::require(
152 svc,
153 actor,
154 space_id,
155 project_id,
156 permission::MANAGE_VIEWS,
157 )
158 .await?;
159
160 let deleted = view_entity::Entity::delete_many()
161 .filter(Vc::Id.eq(view_id))
162 .filter(Vc::ProjectId.eq(project_id))
163 .exec(&svc.db.writer())
176 let _timer = metrics::Timer::start(metrics::FEATURE_PROJECT_VIEWS_DELETE);
177 let result: ProjectResult<()> = async {
178 crate::features::authz::require(
179 svc,
180 actor,
181 space_id,
182 project_id,
183 permission::MANAGE_VIEWS,
184 )
164185 .await?;
165 if deleted.rows_affected == 0 {
166 return Err(ProjectError::NotFound(format!(
167 "view {view_id} in project {project_id}"
168 )));
169 }
170186
171 log_activity(svc, project_id, actor, "view_deleted", "view", Some(view_id), None).await;
172 Ok(())
187 let deleted = view_entity::Entity::delete_many()
188 .filter(Vc::Id.eq(view_id))
189 .filter(Vc::ProjectId.eq(project_id))
190 .exec(&svc.db.writer())
191 .await?;
192 if deleted.rows_affected == 0 {
193 return Err(ProjectError::NotFound(format!(
194 "view {view_id} in project {project_id}"
195 )));
196 }
197
198 log_activity(svc, project_id, actor, "view_deleted", "view", Some(view_id), None).await;
199 Ok(())
200 }
201 .await;
202 metrics::record_outcome(metrics::FEATURE_PROJECT_VIEWS_DELETE, &result);
203 result
173204}
174205
175206fn view_view(m: &view_entity::Model) -> ViewView {