refactor(issues): refresh all feature usecase implementations
zhenyi committed
Modified lib/issues/features/comment/usecase/create.rs lib/issues/features/comment/usecase/create.rs +40 −33 View lib/issues/features/comment/usecase/create.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::super::issue::usecase::load_issue;
1011use super::super::{CommentView, CreateCommentOption};
@@ -17,40 +18,46 @@
1718 issue_id: Uuid,
1819 input: CreateCommentOption,
1920) -> IssueResult<CommentView> {
20 require_authenticated(actor)?;
21 if input.content.trim().is_empty() || input.content.len() > svc.config.max_comment_len {
22 return Err(IssueError::BadRequest(format!(
23 "comment length must be in 1..={}",
24 svc.config.max_comment_len
25 )));
26 }
27 let _issue = load_issue(svc, channel_id, issue_id).await?;
21 let _timer = metrics::Timer::start(metrics::FEATURE_COMMENTS_CREATE);
22 let result: IssueResult<CommentView> = async {
23 require_authenticated(actor)?;
24 if input.content.trim().is_empty() || input.content.len() > svc.config.max_comment_len {
25 return Err(IssueError::BadRequest(format!(
26 "comment length must be in 1..={}",
27 svc.config.max_comment_len
28 )));
29 }
30 let _issue = load_issue(svc, channel_id, issue_id).await?;
2831
29 if let Some(parent_id) = input.parent_id {
30 let db = svc.db.reader();
31 let parent = comment_entity::Entity::find_by_id(parent_id)
32 .one(&db)
33 .await?
34 .ok_or_else(|| IssueError::NotFound(format!("comment {parent_id}")))?;
35 if parent.issue_id != Some(issue_id) {
36 return Err(IssueError::BadRequest(
37 "parent comment must belong to the same issue".into(),
38 ));
32 if let Some(parent_id) = input.parent_id {
33 let db = svc.db.reader();
34 let parent = comment_entity::Entity::find_by_id(parent_id)
35 .one(&db)
36 .await?
37 .ok_or_else(|| IssueError::NotFound(format!("comment {parent_id}")))?;
38 if parent.issue_id != Some(issue_id) {
39 return Err(IssueError::BadRequest(
40 "parent comment must belong to the same issue".into(),
41 ));
42 }
3943 }
44
45 let db = svc.db.writer();
46 let now = OffsetDateTime::now_utc();
47 let id = Uuid::new_v4();
48 let active: comment_entity::ActiveModel = comment_entity::ActiveModel::builder()
49 .set_id(id)
50 .set_issue_id(issue_id)
51 .set_parent_id(input.parent_id)
52 .set_poster_id(actor)
53 .set_content(input.content)
54 .set_created_at(now)
55 .set_updated_at(None)
56 .into();
57 let saved = active.insert(&db).await?;
58 Ok(CommentView::from(saved))
4059 }
41
42 let db = svc.db.writer();
43 let now = OffsetDateTime::now_utc();
44 let id = Uuid::new_v4();
45 let active: comment_entity::ActiveModel = comment_entity::ActiveModel::builder()
46 .set_id(id)
47 .set_issue_id(issue_id)
48 .set_parent_id(input.parent_id)
49 .set_poster_id(actor)
50 .set_content(input.content)
51 .set_created_at(now)
52 .set_updated_at(None)
53 .into();
54 let saved = active.insert(&db).await?;
55 Ok(CommentView::from(saved))
60 .await;
61 metrics::record_outcome(metrics::FEATURE_COMMENTS_CREATE, &result);
62 result
5663}
No newline at end of file
Modified lib/issues/features/comment/usecase/delete.rs lib/issues/features/comment/usecase/delete.rs +17 −10 View lib/issues/features/comment/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::{IssueError, IssueResult};
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_comment, require_authenticated};
89
@@ -13,15 +14,21 @@
1314 issue_id: Uuid,
1415 comment_id: Uuid,
1516) -> IssueResult<()> {
16 require_authenticated(actor)?;
17 let cur = load_comment(svc, channel_id, issue_id, comment_id).await?;
18 if cur.poster_id != actor {
19 return Err(IssueError::Forbidden(
20 "only the poster can delete the comment".into(),
21 ));
17 let _timer = metrics::Timer::start(metrics::FEATURE_COMMENTS_DELETE);
18 let result: IssueResult<()> = async {
19 require_authenticated(actor)?;
20 let cur = load_comment(svc, channel_id, issue_id, comment_id).await?;
21 if cur.poster_id != actor {
22 return Err(IssueError::Forbidden(
23 "only the poster can delete the comment".into(),
24 ));
25 }
26 let db = svc.db.writer();
27 let active: db::issue::comment::ActiveModel = cur.into();
28 active.delete(&db).await?;
29 Ok(())
2230 }
23 let db = svc.db.writer();
24 let active: db::issue::comment::ActiveModel = cur.into();
25 active.delete(&db).await?;
26 Ok(())
31 .await;
32 metrics::record_outcome(metrics::FEATURE_COMMENTS_DELETE, &result);
33 result
2734}
No newline at end of file
Modified lib/issues/features/comment/usecase/get.rs lib/issues/features/comment/usecase/get.rs +10 −3 View lib/issues/features/comment/usecase/get.rs
@@ -2,6 +2,7 @@
22
33use crate::error::IssueResult;
44use crate::features::IssueService;
5use crate::metrics;
56
67use super::super::CommentView;
78use super::helpers::{load_comment, require_authenticated};
@@ -13,7 +14,13 @@
1314 issue_id: Uuid,
1415 comment_id: Uuid,
1516) -> IssueResult<CommentView> {
16 require_authenticated(actor)?;
17 let c = load_comment(svc, channel_id, issue_id, comment_id).await?;
18 Ok(CommentView::from(c))
17 let _timer = metrics::Timer::start(metrics::FEATURE_COMMENTS_GET);
18 let result: IssueResult<CommentView> = async {
19 require_authenticated(actor)?;
20 let c = load_comment(svc, channel_id, issue_id, comment_id).await?;
21 Ok(CommentView::from(c))
22 }
23 .await;
24 metrics::record_outcome(metrics::FEATURE_COMMENTS_GET, &result);
25 result
1926}
No newline at end of file
Modified lib/issues/features/comment/usecase/list.rs lib/issues/features/comment/usecase/list.rs +30 −23 View lib/issues/features/comment/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::super::issue::usecase::load_issue;
910use super::super::{CommentPage, CommentView, ListCommentQuery};
@@ -16,32 +17,38 @@
1617 issue_id: Uuid,
1718 q: ListCommentQuery,
1819) -> IssueResult<CommentPage> {
19 require_authenticated(actor)?;
20 let _timer = metrics::Timer::start(metrics::FEATURE_COMMENTS_LIST);
21 let result: IssueResult<CommentPage> = async {
22 require_authenticated(actor)?;
2023
21 let limit = q.limit.unwrap_or(svc.config.default_pagination);
22 let offset = q.offset.unwrap_or(0);
23 if limit == 0 || limit > svc.config.max_pagination {
24 return Err(IssueError::BadRequest(format!(
25 "limit must be in 1..={}",
26 svc.config.max_pagination
27 )));
28 }
24 let limit = q.limit.unwrap_or(svc.config.default_pagination);
25 let offset = q.offset.unwrap_or(0);
26 if limit == 0 || limit > svc.config.max_pagination {
27 return Err(IssueError::BadRequest(format!(
28 "limit must be in 1..={}",
29 svc.config.max_pagination
30 )));
31 }
2932
30 let _issue = load_issue(svc, channel_id, issue_id).await?;
31 let db = svc.db.reader();
33 let _issue = load_issue(svc, channel_id, issue_id).await?;
34 let db = svc.db.reader();
3235
33 let finder = comment_entity::Entity::find()
34 .filter(comment_entity::COLUMN.issue_id.eq(issue_id))
35 .apply_if(q.parent_id, |f, pid| f.filter(comment_entity::COLUMN.parent_id.eq(pid)));
36 let finder = comment_entity::Entity::find()
37 .filter(comment_entity::COLUMN.issue_id.eq(issue_id))
38 .apply_if(q.parent_id, |f, pid| f.filter(comment_entity::COLUMN.parent_id.eq(pid)));
3639
37 let total = finder.clone().count(&db).await? as i64;
38 let rows = finder
39 .order_by_asc(comment_entity::COLUMN.created_at)
40 .limit(limit)
41 .offset(offset)
42 .all(&db)
43 .await?;
40 let total = finder.clone().count(&db).await? as i64;
41 let rows = finder
42 .order_by_asc(comment_entity::COLUMN.created_at)
43 .limit(limit)
44 .offset(offset)
45 .all(&db)
46 .await?;
4447
45 let items = rows.into_iter().map(CommentView::from).collect();
46 Ok(CommentPage { items, total })
48 let items = rows.into_iter().map(CommentView::from).collect();
49 Ok(CommentPage { items, total })
50 }
51 .await;
52 metrics::record_outcome(metrics::FEATURE_COMMENTS_LIST, &result);
53 result
4754}
No newline at end of file
Modified lib/issues/features/comment/usecase/update.rs lib/issues/features/comment/usecase/update.rs +25 −18 View lib/issues/features/comment/usecase/update.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{CommentView, UpdateCommentOption};
910use super::helpers::{load_comment, require_authenticated};
@@ -16,23 +17,29 @@
1617 comment_id: Uuid,
1718 input: UpdateCommentOption,
1819) -> IssueResult<CommentView> {
19 require_authenticated(actor)?;
20 let cur = load_comment(svc, channel_id, issue_id, comment_id).await?;
21 if cur.poster_id != actor {
22 return Err(IssueError::Forbidden(
23 "only the poster can edit the comment".into(),
24 ));
25 }
26 if input.content.trim().is_empty() || input.content.len() > svc.config.max_comment_len {
27 return Err(IssueError::BadRequest(format!(
28 "comment length must be in 1..={}",
29 svc.config.max_comment_len
30 )));
20 let _timer = metrics::Timer::start(metrics::FEATURE_COMMENTS_UPDATE);
21 let result: IssueResult<CommentView> = async {
22 require_authenticated(actor)?;
23 let cur = load_comment(svc, channel_id, issue_id, comment_id).await?;
24 if cur.poster_id != actor {
25 return Err(IssueError::Forbidden(
26 "only the poster can edit the comment".into(),
27 ));
28 }
29 if input.content.trim().is_empty() || input.content.len() > svc.config.max_comment_len {
30 return Err(IssueError::BadRequest(format!(
31 "comment length must be in 1..={}",
32 svc.config.max_comment_len
33 )));
34 }
35 let db = svc.db.writer();
36 let mut active: db::issue::comment::ActiveModel = cur.into();
37 active.content = Set(input.content);
38 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
39 let saved = active.update(&db).await?;
40 Ok(CommentView::from(saved))
3141 }
32 let db = svc.db.writer();
33 let mut active: db::issue::comment::ActiveModel = cur.into();
34 active.content = Set(input.content);
35 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
36 let saved = active.update(&db).await?;
37 Ok(CommentView::from(saved))
42 .await;
43 metrics::record_outcome(metrics::FEATURE_COMMENTS_UPDATE, &result);
44 result
3845}
No newline at end of file
Modified lib/issues/features/issue/usecase/close.rs lib/issues/features/issue/usecase/close.rs +19 −12 View lib/issues/features/issue/usecase/close.rs
@@ -4,6 +4,7 @@
44
55use crate::error::IssueResult;
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{CloseIssueOption, IssueView};
910use super::helpers::{load_issue, require_authenticated};
@@ -15,18 +16,24 @@
1516 issue_id: Uuid,
1617 input: CloseIssueOption,
1718) -> IssueResult<IssueView> {
18 require_authenticated(actor)?;
19 let cur = load_issue(svc, channel_id, issue_id).await?;
20 let db = svc.db.writer();
19 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUES_CLOSE);
20 let result: IssueResult<IssueView> = async {
21 require_authenticated(actor)?;
22 let cur = load_issue(svc, channel_id, issue_id).await?;
23 let db = svc.db.writer();
2124
22 let mut active: db::issue::issue::ActiveModel = cur.into();
23 active.closed_at = Set(if input.closed {
24 Some(OffsetDateTime::now_utc())
25 } else {
26 None
27 });
28 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
25 let mut active: db::issue::issue::ActiveModel = cur.into();
26 active.closed_at = Set(if input.closed {
27 Some(OffsetDateTime::now_utc())
28 } else {
29 None
30 });
31 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
2932
30 let saved = active.update(&db).await?;
31 Ok(IssueView::from(saved))
33 let saved = active.update(&db).await?;
34 Ok(IssueView::from(saved))
35 }
36 .await;
37 metrics::record_outcome(metrics::FEATURE_ISSUES_CLOSE, &result);
38 result
3239}
No newline at end of file
Modified lib/issues/features/issue/usecase/create.rs lib/issues/features/issue/usecase/create.rs +76 −69 View lib/issues/features/issue/usecase/create.rs
@@ -6,6 +6,7 @@
66
77use crate::error::{IssueError, IssueResult};
88use crate::features::IssueService;
9use crate::metrics;
910
1011use super::super::{CreateIssueOption, IssueView};
1112use super::helpers::require_authenticated;
@@ -16,79 +17,85 @@
1617 channel_id: Uuid,
1718 input: CreateIssueOption,
1819) -> IssueResult<IssueView> {
19 require_authenticated(actor)?;
20 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUES_CREATE);
21 let result: IssueResult<IssueView> = async {
22 require_authenticated(actor)?;
2023
21 if input.title.trim().is_empty() || input.title.len() > svc.config.max_title_len {
22 return Err(IssueError::BadRequest(format!(
23 "title length must be in 1..={}",
24 svc.config.max_title_len
25 )));
26 }
27 if input.body.len() > svc.config.max_body_len {
28 return Err(IssueError::BadRequest(format!(
29 "body too long (max {})",
30 svc.config.max_body_len
31 )));
32 }
24 if input.title.trim().is_empty() || input.title.len() > svc.config.max_title_len {
25 return Err(IssueError::BadRequest(format!(
26 "title length must be in 1..={}",
27 svc.config.max_title_len
28 )));
29 }
30 if input.body.len() > svc.config.max_body_len {
31 return Err(IssueError::BadRequest(format!(
32 "body too long (max {})",
33 svc.config.max_body_len
34 )));
35 }
3336
34 let db = svc.db.writer();
35 let txn = db.begin().await?;
37 let db = svc.db.writer();
38 let txn = db.begin().await?;
3639
37 // 1) Resolve / create default issues board for this channel (atomic within txn)
38 let collection: issues_entity::Model = match issues_entity::Entity::find()
39 .filter(issues_entity::COLUMN.channel_id.eq(channel_id))
40 .order_by_asc(issues_entity::COLUMN.created_at)
41 .lock_exclusive()
42 .one(&txn)
43 .await?
44 {
45 Some(c) => c,
46 None => {
47 let now = OffsetDateTime::now_utc();
48 let new_collection_id = Uuid::new_v4();
49 let active: issues_entity::ActiveModel = issues_entity::ActiveModel::builder()
50 .set_id(new_collection_id)
51 .set_channel_id(channel_id)
52 .set_name(svc.config.default_collection_name.clone())
53 .set_icon(None)
54 .set_description(None)
55 .set_created_at(now)
56 .set_updated_at(None)
57 .into();
58 active.insert(&txn).await?
59 }
60 };
40 // 1) Resolve / create default issues board for this channel (atomic within txn)
41 let collection: issues_entity::Model = match issues_entity::Entity::find()
42 .filter(issues_entity::COLUMN.channel_id.eq(channel_id))
43 .order_by_asc(issues_entity::COLUMN.created_at)
44 .lock_exclusive()
45 .one(&txn)
46 .await?
47 {
48 Some(c) => c,
49 None => {
50 let now = OffsetDateTime::now_utc();
51 let new_collection_id = Uuid::new_v4();
52 let active: issues_entity::ActiveModel = issues_entity::ActiveModel::builder()
53 .set_id(new_collection_id)
54 .set_channel_id(channel_id)
55 .set_name(svc.config.default_collection_name.clone())
56 .set_icon(None)
57 .set_description(None)
58 .set_created_at(now)
59 .set_updated_at(None)
60 .into();
61 active.insert(&txn).await?
62 }
63 };
6164
62 // 2) SELECT … FOR UPDATE on this collection's issue rows to compute next id
63 let max_id: Option<i32> = issue_entity::Entity::find()
64 .filter(issue_entity::COLUMN.collection_id.eq(collection.id))
65 .lock_exclusive()
66 .select_only()
67 .column_as(issue_entity::COLUMN.autoincrement_id.max(), "max")
68 .into_tuple::<Option<i32>>()
69 .one(&txn)
70 .await?
71 .flatten();
72 let next_id = max_id.map(|n| n + 1).unwrap_or(1);
65 // 2) SELECT … FOR UPDATE on this collection's issue rows to compute next id
66 let max_id: Option<i32> = issue_entity::Entity::find()
67 .filter(issue_entity::COLUMN.collection_id.eq(collection.id))
68 .lock_exclusive()
69 .select_only()
70 .column_as(issue_entity::COLUMN.autoincrement_id.max(), "max")
71 .into_tuple::<Option<i32>>()
72 .one(&txn)
73 .await?
74 .flatten();
75 let next_id = max_id.map(|n| n + 1).unwrap_or(1);
7376
74 // 3) Insert new issue
75 let now = OffsetDateTime::now_utc();
76 let issue_id = Uuid::new_v4();
77 let active: issue_entity::ActiveModel = issue_entity::ActiveModel::builder()
78 .set_id(issue_id)
79 .set_collection_id(collection.id)
80 .set_autoincrement_id(next_id)
81 .set_author_id(actor)
82 .set_title(input.title.trim().to_string())
83 .set_description(input.description)
84 .set_body(input.body)
85 .set_milestone_id(input.milestone_id)
86 .set_created_at(now)
87 .set_updated_at(None)
88 .set_closed_at(None)
89 .into();
90 let saved = active.insert(&txn).await?;
77 // 3) Insert new issue
78 let now = OffsetDateTime::now_utc();
79 let issue_id = Uuid::new_v4();
80 let active: issue_entity::ActiveModel = issue_entity::ActiveModel::builder()
81 .set_id(issue_id)
82 .set_collection_id(collection.id)
83 .set_autoincrement_id(next_id)
84 .set_author_id(actor)
85 .set_title(input.title.trim().to_string())
86 .set_description(input.description)
87 .set_body(input.body)
88 .set_milestone_id(input.milestone_id)
89 .set_created_at(now)
90 .set_updated_at(None)
91 .set_closed_at(None)
92 .into();
93 let saved = active.insert(&txn).await?;
9194
92 txn.commit().await?;
93 Ok(IssueView::from(saved))
95 txn.commit().await?;
96 Ok(IssueView::from(saved))
97 }
98 .await;
99 metrics::record_outcome(metrics::FEATURE_ISSUES_CREATE, &result);
100 result
94101}
No newline at end of file
Modified lib/issues/features/issue/usecase/delete.rs lib/issues/features/issue/usecase/delete.rs +13 −6 View lib/issues/features/issue/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::IssueResult;
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_issue, require_authenticated};
89
@@ -12,10 +13,16 @@
1213 channel_id: Uuid,
1314 issue_id: Uuid,
1415) -> IssueResult<()> {
15 require_authenticated(actor)?;
16 let cur = load_issue(svc, channel_id, issue_id).await?;
17 let db = svc.db.writer();
18 let active: db::issue::issue::ActiveModel = cur.into();
19 active.delete(&db).await?;
20 Ok(())
16 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUES_DELETE);
17 let result: IssueResult<()> = async {
18 require_authenticated(actor)?;
19 let cur = load_issue(svc, channel_id, issue_id).await?;
20 let db = svc.db.writer();
21 let active: db::issue::issue::ActiveModel = cur.into();
22 active.delete(&db).await?;
23 Ok(())
24 }
25 .await;
26 metrics::record_outcome(metrics::FEATURE_ISSUES_DELETE, &result);
27 result
2128}
No newline at end of file
Modified lib/issues/features/issue/usecase/get.rs lib/issues/features/issue/usecase/get.rs +10 −3 View lib/issues/features/issue/usecase/get.rs
@@ -2,6 +2,7 @@
22
33use crate::error::IssueResult;
44use crate::features::IssueService;
5use crate::metrics;
56
67use super::super::IssueView;
78use super::helpers::{load_issue, require_authenticated};
@@ -12,7 +13,13 @@
1213 channel_id: Uuid,
1314 issue_id: Uuid,
1415) -> IssueResult<IssueView> {
15 require_authenticated(actor)?;
16 let it = load_issue(svc, channel_id, issue_id).await?;
17 Ok(IssueView::from(it))
16 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUES_GET);
17 let result: IssueResult<IssueView> = async {
18 require_authenticated(actor)?;
19 let it = load_issue(svc, channel_id, issue_id).await?;
20 Ok(IssueView::from(it))
21 }
22 .await;
23 metrics::record_outcome(metrics::FEATURE_ISSUES_GET, &result);
24 result
1825}
No newline at end of file
Modified lib/issues/features/issue/usecase/list.rs lib/issues/features/issue/usecase/list.rs +40 −33 View lib/issues/features/issue/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{IssuePage, IssueView, IssueState, ListIssueQuery};
910use super::helpers::{load_collection, require_authenticated};
@@ -14,42 +15,48 @@
1415 channel_id: Uuid,
1516 q: ListIssueQuery,
1617) -> IssueResult<IssuePage> {
17 require_authenticated(actor)?;
18 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUES_LIST);
19 let result: IssueResult<IssuePage> = async {
20 require_authenticated(actor)?;
1821
19 let limit = q.limit.unwrap_or(svc.config.default_pagination);
20 let offset = q.offset.unwrap_or(0);
21 if limit == 0 || limit > svc.config.max_pagination {
22 return Err(IssueError::BadRequest(format!(
23 "limit must be in 1..={}",
24 svc.config.max_pagination
25 )));
26 }
22 let limit = q.limit.unwrap_or(svc.config.default_pagination);
23 let offset = q.offset.unwrap_or(0);
24 if limit == 0 || limit > svc.config.max_pagination {
25 return Err(IssueError::BadRequest(format!(
26 "limit must be in 1..={}",
27 svc.config.max_pagination
28 )));
29 }
2730
28 let collection = load_collection(svc, channel_id).await?;
29 let db = svc.db.reader();
31 let collection = load_collection(svc, channel_id).await?;
32 let db = svc.db.reader();
3033
31 let finder = issue_entity::Entity::find()
32 .filter(issue_entity::COLUMN.collection_id.eq(collection.id))
33 .apply_if(q.state, |f, s| match s {
34 IssueState::Open => f.filter(issue_entity::COLUMN.closed_at.is_null()),
35 IssueState::Closed => f.filter(issue_entity::COLUMN.closed_at.is_not_null()),
36 })
37 .apply_if(q.author_id, |f, uid| {
38 f.filter(issue_entity::COLUMN.author_id.eq(uid))
39 })
40 .apply_if(q.milestone_id, |f, mid| {
41 f.filter(issue_entity::COLUMN.milestone_id.eq(mid))
42 })
43 .apply_if(q.q, |f, s| f.filter(issue_entity::COLUMN.title.contains(&s)));
34 let finder = issue_entity::Entity::find()
35 .filter(issue_entity::COLUMN.collection_id.eq(collection.id))
36 .apply_if(q.state, |f, s| match s {
37 IssueState::Open => f.filter(issue_entity::COLUMN.closed_at.is_null()),
38 IssueState::Closed => f.filter(issue_entity::COLUMN.closed_at.is_not_null()),
39 })
40 .apply_if(q.author_id, |f, uid| {
41 f.filter(issue_entity::COLUMN.author_id.eq(uid))
42 })
43 .apply_if(q.milestone_id, |f, mid| {
44 f.filter(issue_entity::COLUMN.milestone_id.eq(mid))
45 })
46 .apply_if(q.q, |f, s| f.filter(issue_entity::COLUMN.title.contains(&s)));
4447
45 let total = finder.clone().count(&db).await? as i64;
46 let rows = finder
47 .order_by_desc(issue_entity::COLUMN.created_at)
48 .limit(limit)
49 .offset(offset)
50 .all(&db)
51 .await?;
48 let total = finder.clone().count(&db).await? as i64;
49 let rows = finder
50 .order_by_desc(issue_entity::COLUMN.created_at)
51 .limit(limit)
52 .offset(offset)
53 .all(&db)
54 .await?;
5255
53 let items = rows.into_iter().map(IssueView::from).collect();
54 Ok(IssuePage { items, total })
56 let items = rows.into_iter().map(IssueView::from).collect();
57 Ok(IssuePage { items, total })
58 }
59 .await;
60 metrics::record_outcome(metrics::FEATURE_ISSUES_LIST, &result);
61 result
5562}
No newline at end of file
Modified lib/issues/features/issue/usecase/update.rs lib/issues/features/issue/usecase/update.rs +42 −35 View lib/issues/features/issue/usecase/update.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{IssueView, UpdateIssueOption};
910use super::helpers::{load_issue, require_authenticated};
@@ -15,43 +16,49 @@
1516 issue_id: Uuid,
1617 input: UpdateIssueOption,
1718) -> IssueResult<IssueView> {
18 require_authenticated(actor)?;
19 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUES_UPDATE);
20 let result: IssueResult<IssueView> = async {
21 require_authenticated(actor)?;
1922
20 let cur = load_issue(svc, channel_id, issue_id).await?;
23 let cur = load_issue(svc, channel_id, issue_id).await?;
2124
22 if let Some(ref t) = input.title
23 && (t.trim().is_empty() || t.len() > svc.config.max_title_len)
24 {
25 return Err(IssueError::BadRequest(format!(
26 "title length must be in 1..={}",
27 svc.config.max_title_len
28 )));
29 }
30 if let Some(ref b) = input.body
31 && b.len() > svc.config.max_body_len
32 {
33 return Err(IssueError::BadRequest(format!(
34 "body too long (max {})",
35 svc.config.max_body_len
36 )));
37 }
25 if let Some(ref t) = input.title
26 && (t.trim().is_empty() || t.len() > svc.config.max_title_len)
27 {
28 return Err(IssueError::BadRequest(format!(
29 "title length must be in 1..={}",
30 svc.config.max_title_len
31 )));
32 }
33 if let Some(ref b) = input.body
34 && b.len() > svc.config.max_body_len
35 {
36 return Err(IssueError::BadRequest(format!(
37 "body too long (max {})",
38 svc.config.max_body_len
39 )));
40 }
3841
39 let db = svc.db.writer();
40 let mut active: db::issue::issue::ActiveModel = cur.into();
41 if let Some(t) = input.title {
42 active.title = Set(t.trim().to_string());
43 }
44 if let Some(opt) = input.description {
45 active.description = Set(opt);
42 let db = svc.db.writer();
43 let mut active: db::issue::issue::ActiveModel = cur.into();
44 if let Some(t) = input.title {
45 active.title = Set(t.trim().to_string());
46 }
47 if let Some(opt) = input.description {
48 active.description = Set(opt);
49 }
50 if let Some(b) = input.body {
51 active.body = Set(b);
52 }
53 if let Some(opt) = input.milestone_id {
54 active.milestone_id = Set(opt);
55 }
56 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
57
58 let saved = active.update(&db).await?;
59 Ok(IssueView::from(saved))
4660 }
47 if let Some(b) = input.body {
48 active.body = Set(b);
49 }
50 if let Some(opt) = input.milestone_id {
51 active.milestone_id = Set(opt);
52 }
53 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
54
55 let saved = active.update(&db).await?;
56 Ok(IssueView::from(saved))
61 .await;
62 metrics::record_outcome(metrics::FEATURE_ISSUES_UPDATE, &result);
63 result
5764}
No newline at end of file
Modified lib/issues/features/issue_attachment/usecase/create.rs lib/issues/features/issue_attachment/usecase/create.rs +43 −36 View lib/issues/features/issue_attachment/usecase/create.rs
@@ -8,6 +8,7 @@
88
99use crate::error::{IssueError, IssueResult};
1010use crate::features::IssueService;
11use crate::metrics;
1112
1213use super::super::{AttachmentView, UploadAttachmentInput};
1314use super::helpers::require_authenticated;
@@ -20,43 +21,49 @@
2021 owner_id: Uuid,
2122 input: UploadAttachmentInput,
2223) -> IssueResult<AttachmentView> {
23 require_authenticated(actor)?;
24 crate::features::issue::usecase::load_issue(svc, channel_id, owner_id).await?;
24 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_ATTACHMENTS_CREATE);
25 let result: IssueResult<AttachmentView> = async {
26 require_authenticated(actor)?;
27 crate::features::issue::usecase::load_issue(svc, channel_id, owner_id).await?;
2528
26 let size = input.data.len() as i64;
27 if size > svc.config.max_attachment_size_bytes {
28 return Err(IssueError::BadRequest(format!(
29 "attachment too large, max {} bytes",
30 svc.config.max_attachment_size_bytes
31 )));
32 }
33 if input.filename.trim().is_empty() {
34 return Err(IssueError::BadRequest("filename is required".into()));
35 }
29 let size = input.data.len() as i64;
30 if size > svc.config.max_attachment_size_bytes {
31 return Err(IssueError::BadRequest(format!(
32 "attachment too large, max {} bytes",
33 svc.config.max_attachment_size_bytes
34 )));
35 }
36 if input.filename.trim().is_empty() {
37 return Err(IssueError::BadRequest("filename is required".into()));
38 }
3639
37 let id = Uuid::new_v4();
38 let path = Path::from(format!(
39 "issues/{channel_id}/{owner_id}/{owner_type:?}/{id}-{}",
40 input.filename
41 ));
42 svc.storage
43 .put(&path, Bytes::from(input.data))
44 .await
45 .map_err(|e| IssueError::Internal(format!("storage put failed: {e}")))?;
40 let id = Uuid::new_v4();
41 let path = Path::from(format!(
42 "issues/{channel_id}/{owner_id}/{owner_type:?}/{id}-{}",
43 input.filename
44 ));
45 svc.storage
46 .put(&path, Bytes::from(input.data))
47 .await
48 .map_err(|e| IssueError::Internal(format!("storage put failed: {e}")))?;
4649
47 let db = svc.db.writer();
48 let now = OffsetDateTime::now_utc();
49 let active: att_entity::ActiveModel = att_entity::ActiveModel::builder()
50 .set_id(id)
51 .set_owner_type(owner_type)
52 .set_owner_id(owner_id)
53 .set_filename(input.filename)
54 .set_content_type(input.content_type)
55 .set_size(size)
56 .set_url(path.to_string())
57 .set_uploaded_by(Some(actor))
58 .set_created_at(now)
59 .into();
60 let saved = active.insert(&db).await?;
61 Ok(AttachmentView::from(saved))
50 let db = svc.db.writer();
51 let now = OffsetDateTime::now_utc();
52 let active: att_entity::ActiveModel = att_entity::ActiveModel::builder()
53 .set_id(id)
54 .set_owner_type(owner_type)
55 .set_owner_id(owner_id)
56 .set_filename(input.filename)
57 .set_content_type(input.content_type)
58 .set_size(size)
59 .set_url(path.to_string())
60 .set_uploaded_by(Some(actor))
61 .set_created_at(now)
62 .into();
63 let saved = active.insert(&db).await?;
64 Ok(AttachmentView::from(saved))
65 }
66 .await;
67 metrics::record_outcome(metrics::FEATURE_ISSUE_ATTACHMENTS_CREATE, &result);
68 result
6269}
Modified lib/issues/features/issue_attachment/usecase/delete.rs lib/issues/features/issue_attachment/usecase/delete.rs +18 −11 View lib/issues/features/issue_attachment/usecase/delete.rs
@@ -5,6 +5,7 @@
55
66use crate::error::IssueResult;
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::helpers::{load_attachment, require_authenticated};
1011
@@ -16,17 +17,23 @@
1617 owner_id: Uuid,
1718 att_id: Uuid,
1819) -> IssueResult<()> {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, owner_id).await?;
21 let cur = load_attachment(svc, owner_type, owner_id, att_id).await?;
20 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_ATTACHMENTS_DELETE);
21 let result: IssueResult<()> = async {
22 require_authenticated(actor)?;
23 crate::features::issue::usecase::load_issue(svc, channel_id, owner_id).await?;
24 let cur = load_attachment(svc, owner_type, owner_id, att_id).await?;
2225
23 let obj_path = Path::parse(cur.url.clone()).map_err(|e| {
24 crate::error::IssueError::Internal(format!("invalid stored path {}: {e}", cur.url))
25 })?;
26 let _ = svc.storage.delete(&obj_path).await;
26 let obj_path = Path::parse(cur.url.clone()).map_err(|e| {
27 crate::error::IssueError::Internal(format!("invalid stored path {}: {e}", cur.url))
28 })?;
29 let _ = svc.storage.delete(&obj_path).await;
2730
28 let db = svc.db.writer();
29 let active: db::attachment::attachments::ActiveModel = cur.into();
30 active.delete(&db).await?;
31 Ok(())
31 let db = svc.db.writer();
32 let active: db::attachment::attachments::ActiveModel = cur.into();
33 active.delete(&db).await?;
34 Ok(())
35 }
36 .await;
37 metrics::record_outcome(metrics::FEATURE_ISSUE_ATTACHMENTS_DELETE, &result);
38 result
3239}
Modified lib/issues/features/issue_attachment/usecase/list.rs lib/issues/features/issue_attachment/usecase/list.rs +29 −22 View lib/issues/features/issue_attachment/usecase/list.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::{AttachmentPage, AttachmentView, ListAttachmentQuery};
1011use super::helpers::require_authenticated;
@@ -17,27 +18,33 @@
1718 owner_id: Uuid,
1819 q: ListAttachmentQuery,
1920) -> IssueResult<AttachmentPage> {
20 require_authenticated(actor)?;
21 let limit = q.limit.unwrap_or(svc.config.default_pagination);
22 let offset = q.offset.unwrap_or(0);
23 if limit == 0 || limit > svc.config.max_pagination {
24 return Err(IssueError::BadRequest(format!(
25 "limit must be in 1..={}",
26 svc.config.max_pagination
27 )));
21 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_ATTACHMENTS_LIST);
22 let result: IssueResult<AttachmentPage> = async {
23 require_authenticated(actor)?;
24 let limit = q.limit.unwrap_or(svc.config.default_pagination);
25 let offset = q.offset.unwrap_or(0);
26 if limit == 0 || limit > svc.config.max_pagination {
27 return Err(IssueError::BadRequest(format!(
28 "limit must be in 1..={}",
29 svc.config.max_pagination
30 )));
31 }
32 crate::features::issue::usecase::load_issue(svc, channel_id, owner_id).await?;
33 let db = svc.db.reader();
34 let finder = att_entity::Entity::find()
35 .filter(att_entity::COLUMN.owner_type.eq(owner_type))
36 .filter(att_entity::COLUMN.owner_id.eq(owner_id));
37 let total = finder.clone().count(&db).await? as i64;
38 let rows = finder
39 .order_by_desc(att_entity::COLUMN.created_at)
40 .limit(limit)
41 .offset(offset)
42 .all(&db)
43 .await?;
44 let items = rows.into_iter().map(AttachmentView::from).collect();
45 Ok(AttachmentPage { items, total })
2846 }
29 crate::features::issue::usecase::load_issue(svc, channel_id, owner_id).await?;
30 let db = svc.db.reader();
31 let finder = att_entity::Entity::find()
32 .filter(att_entity::COLUMN.owner_type.eq(owner_type))
33 .filter(att_entity::COLUMN.owner_id.eq(owner_id));
34 let total = finder.clone().count(&db).await? as i64;
35 let rows = finder
36 .order_by_desc(att_entity::COLUMN.created_at)
37 .limit(limit)
38 .offset(offset)
39 .all(&db)
40 .await?;
41 let items = rows.into_iter().map(AttachmentView::from).collect();
42 Ok(AttachmentPage { items, total })
47 .await;
48 metrics::record_outcome(metrics::FEATURE_ISSUE_ATTACHMENTS_LIST, &result);
49 result
4350}
Modified lib/issues/features/issue_content_history/usecase/list.rs lib/issues/features/issue_content_history/usecase/list.rs +31 −24 View lib/issues/features/issue_content_history/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{ContentHistoryPage, ContentHistoryView, ListContentHistoryQuery};
910use super::helpers::require_authenticated;
@@ -15,29 +16,35 @@
1516 issue_id: Uuid,
1617 q: ListContentHistoryQuery,
1718) -> IssueResult<ContentHistoryPage> {
18 require_authenticated(actor)?;
19 let limit = q.limit.unwrap_or(svc.config.default_pagination);
20 let offset = q.offset.unwrap_or(0);
21 if limit == 0 || limit > svc.config.max_pagination {
22 return Err(IssueError::BadRequest(format!(
23 "limit must be in 1..={}",
24 svc.config.max_pagination
25 )));
19 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_CONTENT_HISTORY_LIST);
20 let result: IssueResult<ContentHistoryPage> = async {
21 require_authenticated(actor)?;
22 let limit = q.limit.unwrap_or(svc.config.default_pagination);
23 let offset = q.offset.unwrap_or(0);
24 if limit == 0 || limit > svc.config.max_pagination {
25 return Err(IssueError::BadRequest(format!(
26 "limit must be in 1..={}",
27 svc.config.max_pagination
28 )));
29 }
30 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
31 let db = svc.db.reader();
32 let finder = hist_entity::Entity::find()
33 .filter(hist_entity::COLUMN.issue_id.eq(issue_id))
34 .apply_if(q.comment_id, |f, cid| {
35 f.filter(hist_entity::COLUMN.comment_id.eq(cid))
36 });
37 let total = finder.clone().count(&db).await? as i64;
38 let rows = finder
39 .order_by_desc(hist_entity::COLUMN.edited_at)
40 .limit(limit)
41 .offset(offset)
42 .all(&db)
43 .await?;
44 let items = rows.into_iter().map(ContentHistoryView::from).collect();
45 Ok(ContentHistoryPage { items, total })
2646 }
27 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
28 let db = svc.db.reader();
29 let finder = hist_entity::Entity::find()
30 .filter(hist_entity::COLUMN.issue_id.eq(issue_id))
31 .apply_if(q.comment_id, |f, cid| {
32 f.filter(hist_entity::COLUMN.comment_id.eq(cid))
33 });
34 let total = finder.clone().count(&db).await? as i64;
35 let rows = finder
36 .order_by_desc(hist_entity::COLUMN.edited_at)
37 .limit(limit)
38 .offset(offset)
39 .all(&db)
40 .await?;
41 let items = rows.into_iter().map(ContentHistoryView::from).collect();
42 Ok(ContentHistoryPage { items, total })
47 .await;
48 metrics::record_outcome(metrics::FEATURE_ISSUE_CONTENT_HISTORY_LIST, &result);
49 result
4350}
Modified lib/issues/features/issue_dependency/usecase/create.rs lib/issues/features/issue_dependency/usecase/create.rs +35 −28 View lib/issues/features/issue_dependency/usecase/create.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::{CreateDependencyOption, DependencyView};
1011use super::helpers::require_authenticated;
@@ -16,34 +17,40 @@
1617 issue_id: Uuid,
1718 input: CreateDependencyOption,
1819) -> IssueResult<DependencyView> {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
21 if input.dependency_id == issue_id {
22 return Err(IssueError::BadRequest(
23 "issue cannot depend on itself".into(),
24 ));
25 }
26 let db = svc.db.writer();
27 let id = Uuid::new_v4();
28 let active: dep_entity::ActiveModel = dep_entity::ActiveModel::builder()
29 .set_id(id)
30 .set_issue_id(issue_id)
31 .set_dependency_id(input.dependency_id)
32 .set_created_at(OffsetDateTime::now_utc())
33 .into();
34 match dep_entity::Entity::insert(active)
35 .on_conflict_do_nothing()
36 .exec(&db)
37 .await?
38 {
39 TryInsertResult::Inserted(_) | TryInsertResult::Empty => {}
40 TryInsertResult::Conflicted => {
41 return Err(IssueError::Conflict("dependency already exists".into()));
20 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_DEPENDENCIES_CREATE);
21 let result: IssueResult<DependencyView> = async {
22 require_authenticated(actor)?;
23 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
24 if input.dependency_id == issue_id {
25 return Err(IssueError::BadRequest(
26 "issue cannot depend on itself".into(),
27 ));
4228 }
29 let db = svc.db.writer();
30 let id = Uuid::new_v4();
31 let active: dep_entity::ActiveModel = dep_entity::ActiveModel::builder()
32 .set_id(id)
33 .set_issue_id(issue_id)
34 .set_dependency_id(input.dependency_id)
35 .set_created_at(OffsetDateTime::now_utc())
36 .into();
37 match dep_entity::Entity::insert(active)
38 .on_conflict_do_nothing()
39 .exec(&db)
40 .await?
41 {
42 TryInsertResult::Inserted(_) | TryInsertResult::Empty => {}
43 TryInsertResult::Conflicted => {
44 return Err(IssueError::Conflict("dependency already exists".into()));
45 }
46 }
47 let saved = dep_entity::Entity::find_by_id(id)
48 .one(&db)
49 .await?
50 .ok_or_else(|| IssueError::Internal("dependency not found after insert".into()))?;
51 Ok(DependencyView::from(saved))
4352 }
44 let saved = dep_entity::Entity::find_by_id(id)
45 .one(&db)
46 .await?
47 .ok_or_else(|| IssueError::Internal("dependency not found after insert".into()))?;
48 Ok(DependencyView::from(saved))
53 .await;
54 metrics::record_outcome(metrics::FEATURE_ISSUE_DEPENDENCIES_CREATE, &result);
55 result
4956}
Modified lib/issues/features/issue_dependency/usecase/delete.rs lib/issues/features/issue_dependency/usecase/delete.rs +16 −9 View lib/issues/features/issue_dependency/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::IssueResult;
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_dependency, require_authenticated};
89
@@ -13,14 +14,20 @@
1314 issue_id: Uuid,
1415 dep_id: Uuid,
1516) -> IssueResult<()> {
16 require_authenticated(actor)?;
17 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
18 let cur = load_dependency(svc, dep_id).await?;
19 if cur.issue_id != issue_id {
20 return Err(crate::error::IssueError::NotFound(format!("dep {dep_id}")));
17 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_DEPENDENCIES_DELETE);
18 let result: IssueResult<()> = async {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
21 let cur = load_dependency(svc, dep_id).await?;
22 if cur.issue_id != issue_id {
23 return Err(crate::error::IssueError::NotFound(format!("dep {dep_id}")));
24 }
25 let db = svc.db.writer();
26 let active: db::issue::issue_dependency::ActiveModel = cur.into();
27 active.delete(&db).await?;
28 Ok(())
2129 }
22 let db = svc.db.writer();
23 let active: db::issue::issue_dependency::ActiveModel = cur.into();
24 active.delete(&db).await?;
25 Ok(())
30 .await;
31 metrics::record_outcome(metrics::FEATURE_ISSUE_DEPENDENCIES_DELETE, &result);
32 result
2633}
Modified lib/issues/features/issue_dependency/usecase/list.rs lib/issues/features/issue_dependency/usecase/list.rs +15 −8 View lib/issues/features/issue_dependency/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::IssueResult;
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::DependencyView;
910use super::helpers::require_authenticated;
@@ -14,12 +15,18 @@
1415 channel_id: Uuid,
1516 issue_id: Uuid,
1617) -> IssueResult<Vec<DependencyView>> {
17 require_authenticated(actor)?;
18 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
19 let db = svc.db.reader();
20 let rows = dep_entity::Entity::find()
21 .filter(dep_entity::COLUMN.issue_id.eq(issue_id))
22 .all(&db)
23 .await?;
24 Ok(rows.into_iter().map(DependencyView::from).collect())
18 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_DEPENDENCIES_LIST);
19 let result: IssueResult<Vec<DependencyView>> = async {
20 require_authenticated(actor)?;
21 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
22 let db = svc.db.reader();
23 let rows = dep_entity::Entity::find()
24 .filter(dep_entity::COLUMN.issue_id.eq(issue_id))
25 .all(&db)
26 .await?;
27 Ok(rows.into_iter().map(DependencyView::from).collect())
28 }
29 .await;
30 metrics::record_outcome(metrics::FEATURE_ISSUE_DEPENDENCIES_LIST, &result);
31 result
2532}
Modified lib/issues/features/issue_event/usecase/list.rs lib/issues/features/issue_event/usecase/list.rs +27 −20 View lib/issues/features/issue_event/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{EventPage, EventView, ListEventQuery};
910use super::helpers::require_authenticated;
@@ -15,25 +16,31 @@
1516 issue_id: Uuid,
1617 q: ListEventQuery,
1718) -> IssueResult<EventPage> {
18 require_authenticated(actor)?;
19 let limit = q.limit.unwrap_or(svc.config.default_pagination);
20 let offset = q.offset.unwrap_or(0);
21 if limit == 0 || limit > svc.config.max_pagination {
22 return Err(IssueError::BadRequest(format!(
23 "limit must be in 1..={}",
24 svc.config.max_pagination
25 )));
19 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_EVENTS_LIST);
20 let result: IssueResult<EventPage> = async {
21 require_authenticated(actor)?;
22 let limit = q.limit.unwrap_or(svc.config.default_pagination);
23 let offset = q.offset.unwrap_or(0);
24 if limit == 0 || limit > svc.config.max_pagination {
25 return Err(IssueError::BadRequest(format!(
26 "limit must be in 1..={}",
27 svc.config.max_pagination
28 )));
29 }
30 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
31 let db = svc.db.reader();
32 let finder = event_entity::Entity::find().filter(event_entity::COLUMN.issue_id.eq(issue_id));
33 let total = finder.clone().count(&db).await? as i64;
34 let rows = finder
35 .order_by_desc(event_entity::COLUMN.created_at)
36 .limit(limit)
37 .offset(offset)
38 .all(&db)
39 .await?;
40 let items = rows.into_iter().map(EventView::from).collect();
41 Ok(EventPage { items, total })
2642 }
27 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
28 let db = svc.db.reader();
29 let finder = event_entity::Entity::find().filter(event_entity::COLUMN.issue_id.eq(issue_id));
30 let total = finder.clone().count(&db).await? as i64;
31 let rows = finder
32 .order_by_desc(event_entity::COLUMN.created_at)
33 .limit(limit)
34 .offset(offset)
35 .all(&db)
36 .await?;
37 let items = rows.into_iter().map(EventView::from).collect();
38 Ok(EventPage { items, total })
43 .await;
44 metrics::record_outcome(metrics::FEATURE_ISSUE_EVENTS_LIST, &result);
45 result
3946}
Modified lib/issues/features/issue_member/usecase/create.rs lib/issues/features/issue_member/usecase/create.rs +23 −16 View lib/issues/features/issue_member/usecase/create.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::{CreateMemberOption, MemberView};
1011use super::helpers::{parse_kind, require_authenticated};
@@ -16,20 +17,26 @@
1617 issue_id: Uuid,
1718 input: CreateMemberOption,
1819) -> IssueResult<MemberView> {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
21 let kind = parse_kind(&input.kind)?;
22 let db = svc.db.writer();
23 let now = OffsetDateTime::now_utc();
24 let id = Uuid::new_v4();
25 let active: member_entity::ActiveModel = member_entity::ActiveModel::builder()
26 .set_id(id)
27 .set_issue_id(issue_id)
28 .set_user_id(input.user_id)
29 .set_kind(kind)
30 .set_created_at(now)
31 .set_updated_at(None)
32 .into();
33 let saved = active.insert(&db).await?;
34 Ok(MemberView::from(saved))
20 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_MEMBERS_CREATE);
21 let result: IssueResult<MemberView> = async {
22 require_authenticated(actor)?;
23 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
24 let kind = parse_kind(&input.kind)?;
25 let db = svc.db.writer();
26 let now = OffsetDateTime::now_utc();
27 let id = Uuid::new_v4();
28 let active: member_entity::ActiveModel = member_entity::ActiveModel::builder()
29 .set_id(id)
30 .set_issue_id(issue_id)
31 .set_user_id(input.user_id)
32 .set_kind(kind)
33 .set_created_at(now)
34 .set_updated_at(None)
35 .into();
36 let saved = active.insert(&db).await?;
37 Ok(MemberView::from(saved))
38 }
39 .await;
40 metrics::record_outcome(metrics::FEATURE_ISSUE_MEMBERS_CREATE, &result);
41 result
3542}
No newline at end of file
Modified lib/issues/features/issue_member/usecase/delete.rs lib/issues/features/issue_member/usecase/delete.rs +18 −11 View lib/issues/features/issue_member/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::IssueResult;
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_member, require_authenticated};
89
@@ -13,16 +14,22 @@
1314 issue_id: Uuid,
1415 member_id: Uuid,
1516) -> IssueResult<()> {
16 require_authenticated(actor)?;
17 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
18 let cur = load_member(svc, member_id).await?;
19 if cur.issue_id != issue_id {
20 return Err(crate::error::IssueError::NotFound(format!(
21 "member {member_id}"
22 )));
17 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_MEMBERS_DELETE);
18 let result: IssueResult<()> = async {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
21 let cur = load_member(svc, member_id).await?;
22 if cur.issue_id != issue_id {
23 return Err(crate::error::IssueError::NotFound(format!(
24 "member {member_id}"
25 )));
26 }
27 let db = svc.db.writer();
28 let active: db::issue::issue_member::ActiveModel = cur.into();
29 active.delete(&db).await?;
30 Ok(())
2331 }
24 let db = svc.db.writer();
25 let active: db::issue::issue_member::ActiveModel = cur.into();
26 active.delete(&db).await?;
27 Ok(())
32 .await;
33 metrics::record_outcome(metrics::FEATURE_ISSUE_MEMBERS_DELETE, &result);
34 result
2835}
No newline at end of file
Modified lib/issues/features/issue_member/usecase/list.rs lib/issues/features/issue_member/usecase/list.rs +32 −25 View lib/issues/features/issue_member/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{ListMemberQuery, MemberPage, MemberView};
910use super::helpers::require_authenticated;
@@ -15,32 +16,38 @@
1516 issue_id: Uuid,
1617 q: ListMemberQuery,
1718) -> IssueResult<MemberPage> {
18 require_authenticated(actor)?;
19 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_MEMBERS_LIST);
20 let result: IssueResult<MemberPage> = async {
21 require_authenticated(actor)?;
1922
20 let limit = q.limit.unwrap_or(svc.config.default_pagination);
21 let offset = q.offset.unwrap_or(0);
22 if limit == 0 || limit > svc.config.max_pagination {
23 return Err(IssueError::BadRequest(format!(
24 "limit must be in 1..={}",
25 svc.config.max_pagination
26 )));
27 }
28 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
29 let db = svc.db.reader();
23 let limit = q.limit.unwrap_or(svc.config.default_pagination);
24 let offset = q.offset.unwrap_or(0);
25 if limit == 0 || limit > svc.config.max_pagination {
26 return Err(IssueError::BadRequest(format!(
27 "limit must be in 1..={}",
28 svc.config.max_pagination
29 )));
30 }
31 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
32 let db = svc.db.reader();
3033
31 let mut finder =
32 member_entity::Entity::find().filter(member_entity::COLUMN.issue_id.eq(issue_id));
33 if let Some(ref k) = q.kind {
34 let kind = super::helpers::parse_kind(k)?;
35 finder = finder.filter(member_entity::COLUMN.kind.eq(kind));
34 let mut finder =
35 member_entity::Entity::find().filter(member_entity::COLUMN.issue_id.eq(issue_id));
36 if let Some(ref k) = q.kind {
37 let kind = super::helpers::parse_kind(k)?;
38 finder = finder.filter(member_entity::COLUMN.kind.eq(kind));
39 }
40 let total = finder.clone().count(&db).await? as i64;
41 let rows = finder
42 .order_by_asc(member_entity::COLUMN.created_at)
43 .limit(limit)
44 .offset(offset)
45 .all(&db)
46 .await?;
47 let items = rows.into_iter().map(MemberView::from).collect();
48 Ok(MemberPage { items, total })
3649 }
37 let total = finder.clone().count(&db).await? as i64;
38 let rows = finder
39 .order_by_asc(member_entity::COLUMN.created_at)
40 .limit(limit)
41 .offset(offset)
42 .all(&db)
43 .await?;
44 let items = rows.into_iter().map(MemberView::from).collect();
45 Ok(MemberPage { items, total })
50 .await;
51 metrics::record_outcome(metrics::FEATURE_ISSUE_MEMBERS_LIST, &result);
52 result
4653}
No newline at end of file
Modified lib/issues/features/issue_pin/usecase/create.rs lib/issues/features/issue_pin/usecase/create.rs +28 −21 View lib/issues/features/issue_pin/usecase/create.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::{CreatePinOption, PinView};
1011use super::helpers::require_authenticated;
@@ -16,26 +17,32 @@
1617 collection_id: Uuid,
1718 input: CreatePinOption,
1819) -> IssueResult<PinView> {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, input.issue_id).await?;
21 let db = svc.db.reader();
22 let dup = pin_entity::Entity::find()
23 .filter(pin_entity::COLUMN.collection_id.eq(collection_id))
24 .filter(pin_entity::COLUMN.issue_id.eq(input.issue_id))
25 .one(&db)
26 .await?;
27 if dup.is_some() {
28 return Err(IssueError::Conflict("issue already pinned".into()));
20 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_PINS_CREATE);
21 let result: IssueResult<PinView> = async {
22 require_authenticated(actor)?;
23 crate::features::issue::usecase::load_issue(svc, channel_id, input.issue_id).await?;
24 let db = svc.db.reader();
25 let dup = pin_entity::Entity::find()
26 .filter(pin_entity::COLUMN.collection_id.eq(collection_id))
27 .filter(pin_entity::COLUMN.issue_id.eq(input.issue_id))
28 .one(&db)
29 .await?;
30 if dup.is_some() {
31 return Err(IssueError::Conflict("issue already pinned".into()));
32 }
33 let db = svc.db.writer();
34 let id = Uuid::new_v4();
35 let active: pin_entity::ActiveModel = pin_entity::ActiveModel::builder()
36 .set_id(id)
37 .set_collection_id(collection_id)
38 .set_issue_id(input.issue_id)
39 .set_pin_order(input.pin_order)
40 .set_created_at(OffsetDateTime::now_utc())
41 .into();
42 let saved = active.insert(&db).await?;
43 Ok(PinView::from(saved))
2944 }
30 let db = svc.db.writer();
31 let id = Uuid::new_v4();
32 let active: pin_entity::ActiveModel = pin_entity::ActiveModel::builder()
33 .set_id(id)
34 .set_collection_id(collection_id)
35 .set_issue_id(input.issue_id)
36 .set_pin_order(input.pin_order)
37 .set_created_at(OffsetDateTime::now_utc())
38 .into();
39 let saved = active.insert(&db).await?;
40 Ok(PinView::from(saved))
45 .await;
46 metrics::record_outcome(metrics::FEATURE_ISSUE_PINS_CREATE, &result);
47 result
4148}
Modified lib/issues/features/issue_pin/usecase/delete.rs lib/issues/features/issue_pin/usecase/delete.rs +15 −8 View lib/issues/features/issue_pin/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::IssueResult;
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_pin, require_authenticated};
89
@@ -13,13 +14,19 @@
1314 collection_id: Uuid,
1415 pin_id: Uuid,
1516) -> IssueResult<()> {
16 require_authenticated(actor)?;
17 let cur = load_pin(svc, pin_id).await?;
18 if cur.collection_id != collection_id {
19 return Err(crate::error::IssueError::NotFound(format!("pin {pin_id}")));
17 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_PINS_DELETE);
18 let result: IssueResult<()> = async {
19 require_authenticated(actor)?;
20 let cur = load_pin(svc, pin_id).await?;
21 if cur.collection_id != collection_id {
22 return Err(crate::error::IssueError::NotFound(format!("pin {pin_id}")));
23 }
24 let db = svc.db.writer();
25 let active: db::issue::issue_pin::ActiveModel = cur.into();
26 active.delete(&db).await?;
27 Ok(())
2028 }
21 let db = svc.db.writer();
22 let active: db::issue::issue_pin::ActiveModel = cur.into();
23 active.delete(&db).await?;
24 Ok(())
29 .await;
30 metrics::record_outcome(metrics::FEATURE_ISSUE_PINS_DELETE, &result);
31 result
2532}
Modified lib/issues/features/issue_pin/usecase/list.rs lib/issues/features/issue_pin/usecase/list.rs +16 −9 View lib/issues/features/issue_pin/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::IssueResult;
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::PinView;
910use super::helpers::require_authenticated;
@@ -14,13 +15,19 @@
1415 channel_id: Uuid,
1516 collection_id: Uuid,
1617) -> IssueResult<Vec<PinView>> {
17 require_authenticated(actor)?;
18 let _ = channel_id;
19 let db = svc.db.reader();
20 let rows = pin_entity::Entity::find()
21 .filter(pin_entity::COLUMN.collection_id.eq(collection_id))
22 .order_by_asc(pin_entity::COLUMN.pin_order)
23 .all(&db)
24 .await?;
25 Ok(rows.into_iter().map(PinView::from).collect())
18 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_PINS_LIST);
19 let result: IssueResult<Vec<PinView>> = async {
20 require_authenticated(actor)?;
21 let _ = channel_id;
22 let db = svc.db.reader();
23 let rows = pin_entity::Entity::find()
24 .filter(pin_entity::COLUMN.collection_id.eq(collection_id))
25 .order_by_asc(pin_entity::COLUMN.pin_order)
26 .all(&db)
27 .await?;
28 Ok(rows.into_iter().map(PinView::from).collect())
29 }
30 .await;
31 metrics::record_outcome(metrics::FEATURE_ISSUE_PINS_LIST, &result);
32 result
2633}
Modified lib/issues/features/issue_reaction/usecase/create.rs lib/issues/features/issue_reaction/usecase/create.rs +72 −59 View lib/issues/features/issue_reaction/usecase/create.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use crate::features::issue::usecase::load_issue;
1011use super::super::{CreateReactionOption, ReactionView};
@@ -17,34 +18,40 @@
1718 issue_id: Uuid,
1819 input: CreateReactionOption,
1920) -> IssueResult<ReactionView> {
20 require_authenticated(actor)?;
21 validate_emoji(svc, &input.emoji)?;
22 ensure_issue_visible(svc, channel_id, issue_id).await?;
23 let db = svc.db.writer();
24 let id = Uuid::new_v4();
25 let active: reaction_entity::ActiveModel = reaction_entity::ActiveModel::builder()
26 .set_id(id)
27 .set_issue_id(Some(issue_id))
28 .set_comment_id(None)
29 .set_user_id(actor)
30 .set_emoji(input.emoji)
31 .set_created_at(OffsetDateTime::now_utc())
32 .into();
33 match reaction_entity::Entity::insert(active)
34 .on_conflict_do_nothing()
35 .exec(&db)
36 .await?
37 {
38 TryInsertResult::Inserted(_) | TryInsertResult::Empty => {}
39 TryInsertResult::Conflicted => {
40 return Err(IssueError::Conflict("reaction already exists".into()));
21 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_REACTIONS_CREATE);
22 let result: IssueResult<ReactionView> = async {
23 require_authenticated(actor)?;
24 validate_emoji(svc, &input.emoji)?;
25 ensure_issue_visible(svc, channel_id, issue_id).await?;
26 let db = svc.db.writer();
27 let id = Uuid::new_v4();
28 let active: reaction_entity::ActiveModel = reaction_entity::ActiveModel::builder()
29 .set_id(id)
30 .set_issue_id(Some(issue_id))
31 .set_comment_id(None)
32 .set_user_id(actor)
33 .set_emoji(input.emoji)
34 .set_created_at(OffsetDateTime::now_utc())
35 .into();
36 match reaction_entity::Entity::insert(active)
37 .on_conflict_do_nothing()
38 .exec(&db)
39 .await?
40 {
41 TryInsertResult::Inserted(_) | TryInsertResult::Empty => {}
42 TryInsertResult::Conflicted => {
43 return Err(IssueError::Conflict("reaction already exists".into()));
44 }
4145 }
46 let saved = reaction_entity::Entity::find_by_id(id)
47 .one(&db)
48 .await?
49 .ok_or_else(|| IssueError::Internal("reaction not found after insert".into()))?;
50 Ok(ReactionView::from(saved))
4251 }
43 let saved = reaction_entity::Entity::find_by_id(id)
44 .one(&db)
45 .await?
46 .ok_or_else(|| IssueError::Internal("reaction not found after insert".into()))?;
47 Ok(ReactionView::from(saved))
52 .await;
53 metrics::record_outcome(metrics::FEATURE_ISSUE_REACTIONS_CREATE, &result);
54 result
4855}
4956
5057pub async fn create_comment_reaction(
@@ -55,41 +62,47 @@
5562 comment_id: Uuid,
5663 input: CreateReactionOption,
5764) -> IssueResult<ReactionView> {
58 require_authenticated(actor)?;
59 validate_emoji(svc, &input.emoji)?;
60 load_issue(svc, channel_id, issue_id).await?;
61 crate::features::comment::usecase::require_authenticated(actor)?;
62 let db = svc.db.reader();
63 let _c = db::issue::comment::Entity::find_by_id(comment_id)
64 .filter(db::issue::comment::COLUMN.issue_id.eq(issue_id))
65 .one(&db)
66 .await?
67 .ok_or_else(|| IssueError::NotFound(format!("comment {comment_id}")))?;
68 let db = svc.db.writer();
69 let id = Uuid::new_v4();
70 let active: reaction_entity::ActiveModel = reaction_entity::ActiveModel::builder()
71 .set_id(id)
72 .set_issue_id(None)
73 .set_comment_id(Some(comment_id))
74 .set_user_id(actor)
75 .set_emoji(input.emoji)
76 .set_created_at(OffsetDateTime::now_utc())
77 .into();
78 match reaction_entity::Entity::insert(active)
79 .on_conflict_do_nothing()
80 .exec(&db)
81 .await?
82 {
83 TryInsertResult::Inserted(_) | TryInsertResult::Empty => {}
84 TryInsertResult::Conflicted => {
85 return Err(IssueError::Conflict("reaction already exists".into()));
65 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_REACTIONS_CREATE);
66 let result: IssueResult<ReactionView> = async {
67 require_authenticated(actor)?;
68 validate_emoji(svc, &input.emoji)?;
69 load_issue(svc, channel_id, issue_id).await?;
70 crate::features::comment::usecase::require_authenticated(actor)?;
71 let db = svc.db.reader();
72 let _c = db::issue::comment::Entity::find_by_id(comment_id)
73 .filter(db::issue::comment::COLUMN.issue_id.eq(issue_id))
74 .one(&db)
75 .await?
76 .ok_or_else(|| IssueError::NotFound(format!("comment {comment_id}")))?;
77 let db = svc.db.writer();
78 let id = Uuid::new_v4();
79 let active: reaction_entity::ActiveModel = reaction_entity::ActiveModel::builder()
80 .set_id(id)
81 .set_issue_id(None)
82 .set_comment_id(Some(comment_id))
83 .set_user_id(actor)
84 .set_emoji(input.emoji)
85 .set_created_at(OffsetDateTime::now_utc())
86 .into();
87 match reaction_entity::Entity::insert(active)
88 .on_conflict_do_nothing()
89 .exec(&db)
90 .await?
91 {
92 TryInsertResult::Inserted(_) | TryInsertResult::Empty => {}
93 TryInsertResult::Conflicted => {
94 return Err(IssueError::Conflict("reaction already exists".into()));
95 }
8696 }
97 let saved = reaction_entity::Entity::find_by_id(id)
98 .one(&db)
99 .await?
100 .ok_or_else(|| IssueError::Internal("reaction not found after insert".into()))?;
101 Ok(ReactionView::from(saved))
87102 }
88 let saved = reaction_entity::Entity::find_by_id(id)
89 .one(&db)
90 .await?
91 .ok_or_else(|| IssueError::Internal("reaction not found after insert".into()))?;
92 Ok(ReactionView::from(saved))
103 .await;
104 metrics::record_outcome(metrics::FEATURE_ISSUE_REACTIONS_CREATE, &result);
105 result
93106}
94107
95108fn validate_emoji(svc: &IssueService, emoji: &str) -> IssueResult<()> {
Modified lib/issues/features/issue_reaction/usecase/delete.rs lib/issues/features/issue_reaction/usecase/delete.rs +17 −10 View lib/issues/features/issue_reaction/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::{IssueError, IssueResult};
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_reaction, require_authenticated};
89
@@ -11,15 +12,21 @@
1112 actor: Uuid,
1213 reaction_id: Uuid,
1314) -> IssueResult<()> {
14 require_authenticated(actor)?;
15 let cur = load_reaction(svc, reaction_id).await?;
16 if cur.user_id != actor {
17 return Err(IssueError::Forbidden(
18 "only the reaction owner can delete".into(),
19 ));
15 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_REACTIONS_DELETE);
16 let result: IssueResult<()> = async {
17 require_authenticated(actor)?;
18 let cur = load_reaction(svc, reaction_id).await?;
19 if cur.user_id != actor {
20 return Err(IssueError::Forbidden(
21 "only the reaction owner can delete".into(),
22 ));
23 }
24 let db = svc.db.writer();
25 let active: db::issue::issue_reactions::ActiveModel = cur.into();
26 active.delete(&db).await?;
27 Ok(())
2028 }
21 let db = svc.db.writer();
22 let active: db::issue::issue_reactions::ActiveModel = cur.into();
23 active.delete(&db).await?;
24 Ok(())
29 .await;
30 metrics::record_outcome(metrics::FEATURE_ISSUE_REACTIONS_DELETE, &result);
31 result
2532}
No newline at end of file
Modified lib/issues/features/issue_reaction/usecase/list.rs lib/issues/features/issue_reaction/usecase/list.rs +29 −16 View lib/issues/features/issue_reaction/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::IssueResult;
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{ReactionAggView, ReactionView};
910use super::helpers::ensure_issue_visible;
@@ -15,14 +16,20 @@
1516 channel_id: Uuid,
1617 issue_id: Uuid,
1718) -> IssueResult<Vec<ReactionAggView>> {
18 require_authenticated(actor)?;
19 ensure_issue_visible(svc, channel_id, issue_id).await?;
20 let db = svc.db.reader();
21 let rows = reaction_entity::Entity::find()
22 .filter(reaction_entity::COLUMN.issue_id.eq(issue_id))
23 .all(&db)
24 .await?;
25 Ok(aggregate(rows))
19 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_REACTIONS_LIST);
20 let result: IssueResult<Vec<ReactionAggView>> = async {
21 require_authenticated(actor)?;
22 ensure_issue_visible(svc, channel_id, issue_id).await?;
23 let db = svc.db.reader();
24 let rows = reaction_entity::Entity::find()
25 .filter(reaction_entity::COLUMN.issue_id.eq(issue_id))
26 .all(&db)
27 .await?;
28 Ok(aggregate(rows))
29 }
30 .await;
31 metrics::record_outcome(metrics::FEATURE_ISSUE_REACTIONS_LIST, &result);
32 result
2633}
2734
2835pub async fn list_reactions_for_comment(
@@ -32,14 +39,20 @@
3239 issue_id: Uuid,
3340 comment_id: Uuid,
3441) -> IssueResult<Vec<ReactionAggView>> {
35 require_authenticated(actor)?;
36 ensure_issue_visible(svc, channel_id, issue_id).await?;
37 let db = svc.db.reader();
38 let rows = reaction_entity::Entity::find()
39 .filter(reaction_entity::COLUMN.comment_id.eq(comment_id))
40 .all(&db)
41 .await?;
42 Ok(aggregate(rows))
42 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_REACTIONS_LIST);
43 let result: IssueResult<Vec<ReactionAggView>> = async {
44 require_authenticated(actor)?;
45 ensure_issue_visible(svc, channel_id, issue_id).await?;
46 let db = svc.db.reader();
47 let rows = reaction_entity::Entity::find()
48 .filter(reaction_entity::COLUMN.comment_id.eq(comment_id))
49 .all(&db)
50 .await?;
51 Ok(aggregate(rows))
52 }
53 .await;
54 metrics::record_outcome(metrics::FEATURE_ISSUE_REACTIONS_LIST, &result);
55 result
4356}
4457
4558fn aggregate(rows: Vec<reaction_entity::Model>) -> Vec<ReactionAggView> {
Modified lib/issues/features/issue_repo_rel/usecase/create.rs lib/issues/features/issue_repo_rel/usecase/create.rs +19 −12 View lib/issues/features/issue_repo_rel/usecase/create.rs
@@ -5,6 +5,7 @@
55
66use crate::error::IssueResult;
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::{CreateRepoRelOption, RepoRelView};
1011use super::helpers::require_authenticated;
@@ -16,16 +17,22 @@
1617 issue_id: Uuid,
1718 input: CreateRepoRelOption,
1819) -> IssueResult<RepoRelView> {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
21 let db = svc.db.writer();
22 let id = Uuid::new_v4();
23 let active: repo_rel_entity::ActiveModel = repo_rel_entity::ActiveModel::builder()
24 .set_id(id)
25 .set_issue_id(issue_id)
26 .set_repo_id(input.repo_id)
27 .set_created_at(OffsetDateTime::now_utc())
28 .into();
29 let saved = active.insert(&db).await?;
30 Ok(RepoRelView::from(saved))
20 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_REPO_REL_CREATE);
21 let result: IssueResult<RepoRelView> = async {
22 require_authenticated(actor)?;
23 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
24 let db = svc.db.writer();
25 let id = Uuid::new_v4();
26 let active: repo_rel_entity::ActiveModel = repo_rel_entity::ActiveModel::builder()
27 .set_id(id)
28 .set_issue_id(issue_id)
29 .set_repo_id(input.repo_id)
30 .set_created_at(OffsetDateTime::now_utc())
31 .into();
32 let saved = active.insert(&db).await?;
33 Ok(RepoRelView::from(saved))
34 }
35 .await;
36 metrics::record_outcome(metrics::FEATURE_ISSUE_REPO_REL_CREATE, &result);
37 result
3138}
Modified lib/issues/features/issue_repo_rel/usecase/delete.rs lib/issues/features/issue_repo_rel/usecase/delete.rs +16 −9 View lib/issues/features/issue_repo_rel/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::IssueResult;
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_repo_rel, require_authenticated};
89
@@ -13,14 +14,20 @@
1314 issue_id: Uuid,
1415 rel_id: Uuid,
1516) -> IssueResult<()> {
16 require_authenticated(actor)?;
17 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
18 let cur = load_repo_rel(svc, rel_id).await?;
19 if cur.issue_id != issue_id {
20 return Err(crate::error::IssueError::NotFound(format!("rel {rel_id}")));
17 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_REPO_REL_DELETE);
18 let result: IssueResult<()> = async {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
21 let cur = load_repo_rel(svc, rel_id).await?;
22 if cur.issue_id != issue_id {
23 return Err(crate::error::IssueError::NotFound(format!("rel {rel_id}")));
24 }
25 let db = svc.db.writer();
26 let active: db::issue::issue_relation_repo::ActiveModel = cur.into();
27 active.delete(&db).await?;
28 Ok(())
2129 }
22 let db = svc.db.writer();
23 let active: db::issue::issue_relation_repo::ActiveModel = cur.into();
24 active.delete(&db).await?;
25 Ok(())
30 .await;
31 metrics::record_outcome(metrics::FEATURE_ISSUE_REPO_REL_DELETE, &result);
32 result
2633}
Modified lib/issues/features/issue_repo_rel/usecase/list.rs lib/issues/features/issue_repo_rel/usecase/list.rs +15 −8 View lib/issues/features/issue_repo_rel/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::IssueResult;
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::RepoRelView;
910use super::helpers::require_authenticated;
@@ -14,12 +15,18 @@
1415 channel_id: Uuid,
1516 issue_id: Uuid,
1617) -> IssueResult<Vec<RepoRelView>> {
17 require_authenticated(actor)?;
18 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
19 let db = svc.db.reader();
20 let rows = repo_rel_entity::Entity::find()
21 .filter(repo_rel_entity::COLUMN.issue_id.eq(issue_id))
22 .all(&db)
23 .await?;
24 Ok(rows.into_iter().map(RepoRelView::from).collect())
18 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_REPO_REL_LIST);
19 let result: IssueResult<Vec<RepoRelView>> = async {
20 require_authenticated(actor)?;
21 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
22 let db = svc.db.reader();
23 let rows = repo_rel_entity::Entity::find()
24 .filter(repo_rel_entity::COLUMN.issue_id.eq(issue_id))
25 .all(&db)
26 .await?;
27 Ok(rows.into_iter().map(RepoRelView::from).collect())
28 }
29 .await;
30 metrics::record_outcome(metrics::FEATURE_ISSUE_REPO_REL_LIST, &result);
31 result
2532}
Modified lib/issues/features/issue_stopwatch/usecase/delete.rs lib/issues/features/issue_stopwatch/usecase/delete.rs +27 −20 View lib/issues/features/issue_stopwatch/usecase/delete.rs
@@ -4,6 +4,7 @@
44
55use crate::error::IssueResult;
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::helpers::{load_my_stopwatch, require_authenticated};
910
@@ -13,25 +14,31 @@
1314 channel_id: Uuid,
1415 issue_id: Uuid,
1516) -> IssueResult<()> {
16 require_authenticated(actor)?;
17 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
18 let cur = load_my_stopwatch(svc, issue_id, actor).await?;
19 let elapsed = (OffsetDateTime::now_utc() - cur.started_at).whole_seconds();
20 let db = svc.db.writer();
17 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_STOPWATCH_DELETE);
18 let result: IssueResult<()> = async {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
21 let cur = load_my_stopwatch(svc, issue_id, actor).await?;
22 let elapsed = (OffsetDateTime::now_utc() - cur.started_at).whole_seconds();
23 let db = svc.db.writer();
2124
22 let txn = db.begin().await?;
23 let active_sw: db::issue::stopwatch::ActiveModel = cur.into();
24 active_sw.delete(&txn).await?;
25 let tt_id = Uuid::new_v4();
26 let now = OffsetDateTime::now_utc();
27 let active_tt: db::issue::tracked_time::ActiveModel = db::issue::tracked_time::ActiveModel {
28 id: Set(tt_id),
29 issue_id: Set(issue_id),
30 user_id: Set(actor),
31 seconds: Set(std::cmp::max(elapsed, 0_i64)),
32 created_at: Set(now),
33 };
34 active_tt.insert(&txn).await?;
35 txn.commit().await?;
36 Ok(())
25 let txn = db.begin().await?;
26 let active_sw: db::issue::stopwatch::ActiveModel = cur.into();
27 active_sw.delete(&txn).await?;
28 let tt_id = Uuid::new_v4();
29 let now = OffsetDateTime::now_utc();
30 let active_tt: db::issue::tracked_time::ActiveModel = db::issue::tracked_time::ActiveModel {
31 id: Set(tt_id),
32 issue_id: Set(issue_id),
33 user_id: Set(actor),
34 seconds: Set(std::cmp::max(elapsed, 0_i64)),
35 created_at: Set(now),
36 };
37 active_tt.insert(&txn).await?;
38 txn.commit().await?;
39 Ok(())
40 }
41 .await;
42 metrics::record_outcome(metrics::FEATURE_ISSUE_STOPWATCH_DELETE, &result);
43 result
3744}
Modified lib/issues/features/issue_stopwatch/usecase/start.rs lib/issues/features/issue_stopwatch/usecase/start.rs +31 −24 View lib/issues/features/issue_stopwatch/usecase/start.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::StopwatchView;
1011use super::helpers::require_authenticated;
@@ -15,29 +16,35 @@
1516 channel_id: Uuid,
1617 issue_id: Uuid,
1718) -> IssueResult<StopwatchView> {
18 require_authenticated(actor)?;
19 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
20 let db = svc.db.reader();
21 let existing = sw_entity::Entity::find()
22 .filter(sw_entity::COLUMN.issue_id.eq(issue_id))
23 .filter(sw_entity::COLUMN.user_id.eq(actor))
24 .one(&db)
25 .await?;
26 if existing.is_some() {
27 return Err(IssueError::Conflict(
28 "stopwatch already running for this issue".into(),
29 ));
19 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_STOPWATCH_START);
20 let result: IssueResult<StopwatchView> = async {
21 require_authenticated(actor)?;
22 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
23 let db = svc.db.reader();
24 let existing = sw_entity::Entity::find()
25 .filter(sw_entity::COLUMN.issue_id.eq(issue_id))
26 .filter(sw_entity::COLUMN.user_id.eq(actor))
27 .one(&db)
28 .await?;
29 if existing.is_some() {
30 return Err(IssueError::Conflict(
31 "stopwatch already running for this issue".into(),
32 ));
33 }
34 let db = svc.db.writer();
35 let now = OffsetDateTime::now_utc();
36 let id = Uuid::new_v4();
37 let active: sw_entity::ActiveModel = sw_entity::ActiveModel::builder()
38 .set_id(id)
39 .set_issue_id(issue_id)
40 .set_user_id(actor)
41 .set_started_at(now)
42 .set_created_at(now)
43 .into();
44 let saved = active.insert(&db).await?;
45 Ok(StopwatchView::from(saved))
3046 }
31 let db = svc.db.writer();
32 let now = OffsetDateTime::now_utc();
33 let id = Uuid::new_v4();
34 let active: sw_entity::ActiveModel = sw_entity::ActiveModel::builder()
35 .set_id(id)
36 .set_issue_id(issue_id)
37 .set_user_id(actor)
38 .set_started_at(now)
39 .set_created_at(now)
40 .into();
41 let saved = active.insert(&db).await?;
42 Ok(StopwatchView::from(saved))
47 .await;
48 metrics::record_outcome(metrics::FEATURE_ISSUE_STOPWATCH_START, &result);
49 result
4350}
Modified lib/issues/features/issue_tracked_time/usecase/list.rs lib/issues/features/issue_tracked_time/usecase/list.rs +29 −22 View lib/issues/features/issue_tracked_time/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{ListTrackedTimeQuery, TrackedTimePage, TrackedTimeView};
910use super::helpers::require_authenticated;
@@ -15,27 +16,33 @@
1516 issue_id: Uuid,
1617 q: ListTrackedTimeQuery,
1718) -> IssueResult<TrackedTimePage> {
18 require_authenticated(actor)?;
19 let limit = q.limit.unwrap_or(svc.config.default_pagination);
20 let offset = q.offset.unwrap_or(0);
21 if limit == 0 || limit > svc.config.max_pagination {
22 return Err(IssueError::BadRequest(format!(
23 "limit must be in 1..={}",
24 svc.config.max_pagination
25 )));
19 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_TRACKED_TIME_LIST);
20 let result: IssueResult<TrackedTimePage> = async {
21 require_authenticated(actor)?;
22 let limit = q.limit.unwrap_or(svc.config.default_pagination);
23 let offset = q.offset.unwrap_or(0);
24 if limit == 0 || limit > svc.config.max_pagination {
25 return Err(IssueError::BadRequest(format!(
26 "limit must be in 1..={}",
27 svc.config.max_pagination
28 )));
29 }
30 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
31 let db = svc.db.reader();
32 let finder = tt_entity::Entity::find()
33 .filter(tt_entity::COLUMN.issue_id.eq(issue_id))
34 .apply_if(q.user_id, |f, uid| f.filter(tt_entity::COLUMN.user_id.eq(uid)));
35 let total = finder.clone().count(&db).await? as i64;
36 let rows = finder
37 .order_by_desc(tt_entity::COLUMN.created_at)
38 .limit(limit)
39 .offset(offset)
40 .all(&db)
41 .await?;
42 let items = rows.into_iter().map(TrackedTimeView::from).collect();
43 Ok(TrackedTimePage { items, total })
2644 }
27 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
28 let db = svc.db.reader();
29 let finder = tt_entity::Entity::find()
30 .filter(tt_entity::COLUMN.issue_id.eq(issue_id))
31 .apply_if(q.user_id, |f, uid| f.filter(tt_entity::COLUMN.user_id.eq(uid)));
32 let total = finder.clone().count(&db).await? as i64;
33 let rows = finder
34 .order_by_desc(tt_entity::COLUMN.created_at)
35 .limit(limit)
36 .offset(offset)
37 .all(&db)
38 .await?;
39 let items = rows.into_iter().map(TrackedTimeView::from).collect();
40 Ok(TrackedTimePage { items, total })
45 .await;
46 metrics::record_outcome(metrics::FEATURE_ISSUE_TRACKED_TIME_LIST, &result);
47 result
4148}
Modified lib/issues/features/issue_user/usecase/list.rs lib/issues/features/issue_user/usecase/list.rs +15 −8 View lib/issues/features/issue_user/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::IssueResult;
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::IssueUserView;
910use super::helpers::require_authenticated;
@@ -14,12 +15,18 @@
1415 channel_id: Uuid,
1516 issue_id: Uuid,
1617) -> IssueResult<Vec<IssueUserView>> {
17 require_authenticated(actor)?;
18 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
19 let db = svc.db.reader();
20 let rows = issue_user_entity::Entity::find()
21 .filter(issue_user_entity::COLUMN.issue_id.eq(issue_id))
22 .all(&db)
23 .await?;
24 Ok(rows.into_iter().map(IssueUserView::from).collect())
18 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_USERS_LIST);
19 let result: IssueResult<Vec<IssueUserView>> = async {
20 require_authenticated(actor)?;
21 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
22 let db = svc.db.reader();
23 let rows = issue_user_entity::Entity::find()
24 .filter(issue_user_entity::COLUMN.issue_id.eq(issue_id))
25 .all(&db)
26 .await?;
27 Ok(rows.into_iter().map(IssueUserView::from).collect())
28 }
29 .await;
30 metrics::record_outcome(metrics::FEATURE_ISSUE_USERS_LIST, &result);
31 result
2532}
Modified lib/issues/features/issue_user/usecase/update.rs lib/issues/features/issue_user/usecase/update.rs +20 −13 View lib/issues/features/issue_user/usecase/update.rs
@@ -4,6 +4,7 @@
44
55use crate::error::IssueResult;
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{IssueUserView, UpdateIssueUserOption};
910use super::helpers::{load_issue_user, require_authenticated};
@@ -16,18 +17,24 @@
1617 uid: Uuid,
1718 input: UpdateIssueUserOption,
1819) -> IssueResult<IssueUserView> {
19 require_authenticated(actor)?;
20 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
21 let cur = load_issue_user(svc, issue_id, uid).await?;
22 let db = svc.db.writer();
23 let mut active: db::issue::issue_user::ActiveModel = cur.into();
24 if let Some(v) = input.is_read {
25 active.is_read = Set(v);
26 }
27 if let Some(v) = input.is_mentioned {
28 active.is_mentioned = Set(v);
20 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_USERS_UPDATE);
21 let result: IssueResult<IssueUserView> = async {
22 require_authenticated(actor)?;
23 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
24 let cur = load_issue_user(svc, issue_id, uid).await?;
25 let db = svc.db.writer();
26 let mut active: db::issue::issue_user::ActiveModel = cur.into();
27 if let Some(v) = input.is_read {
28 active.is_read = Set(v);
29 }
30 if let Some(v) = input.is_mentioned {
31 active.is_mentioned = Set(v);
32 }
33 let _ = OffsetDateTime::now_utc();
34 let saved = active.update(&db).await?;
35 Ok(IssueUserView::from(saved))
2936 }
30 let _ = OffsetDateTime::now_utc();
31 let saved = active.update(&db).await?;
32 Ok(IssueUserView::from(saved))
37 .await;
38 metrics::record_outcome(metrics::FEATURE_ISSUE_USERS_UPDATE, &result);
39 result
3340}
Modified lib/issues/features/issue_watch/usecase/delete.rs lib/issues/features/issue_watch/usecase/delete.rs +14 −7 View lib/issues/features/issue_watch/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::IssueResult;
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_my_watch, require_authenticated};
89
@@ -12,11 +13,17 @@
1213 channel_id: Uuid,
1314 issue_id: Uuid,
1415) -> IssueResult<()> {
15 require_authenticated(actor)?;
16 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
17 let cur = load_my_watch(svc, issue_id, actor).await?;
18 let db = svc.db.writer();
19 let active: db::issue::issue_watch::ActiveModel = cur.into();
20 active.delete(&db).await?;
21 Ok(())
16 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_WATCHES_DELETE);
17 let result: IssueResult<()> = async {
18 require_authenticated(actor)?;
19 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
20 let cur = load_my_watch(svc, issue_id, actor).await?;
21 let db = svc.db.writer();
22 let active: db::issue::issue_watch::ActiveModel = cur.into();
23 active.delete(&db).await?;
24 Ok(())
25 }
26 .await;
27 metrics::record_outcome(metrics::FEATURE_ISSUE_WATCHES_DELETE, &result);
28 result
2229}
No newline at end of file
Modified lib/issues/features/issue_watch/usecase/helpers.rs lib/issues/features/issue_watch/usecase/helpers.rs +30 −23 View lib/issues/features/issue_watch/usecase/helpers.rs
@@ -7,6 +7,7 @@
77
88use crate::error::{IssueError, IssueResult};
99use crate::features::IssueService;
10use crate::metrics;
1011
1112pub fn require_authenticated(actor: Uuid) -> IssueResult<()> {
1213 if actor.is_nil() {
@@ -51,27 +52,33 @@
5152 issue_id: Uuid,
5253 mode_str: &str,
5354) -> IssueResult<crate::features::issue_watch::WatchView> {
54 require_authenticated(actor)?;
55 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
56 let mode = parse_mode(mode_str)?;
57 let db = svc.db.writer();
58 let now = OffsetDateTime::now_utc();
59 let active: watch_entity::ActiveModel = watch_entity::ActiveModel::builder()
60 .set_id(Uuid::new_v4())
61 .set_issue_id(issue_id)
62 .set_user_id(actor)
63 .set_mode(mode)
64 .set_created_at(now)
65 .set_updated_at(Some(now))
66 .into();
67 let saved = watch_entity::Entity::insert(active)
68 .on_conflict(
69 OnConflict::columns([watch_entity::COLUMN.issue_id, watch_entity::COLUMN.user_id])
70 .update_column(watch_entity::COLUMN.mode)
71 .update_column(watch_entity::COLUMN.updated_at)
72 .to_owned(),
73 )
74 .exec_with_returning(&db)
75 .await?;
76 Ok(saved.into())
55 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_WATCHES_UPSERT);
56 let result: IssueResult<crate::features::issue_watch::WatchView> = async {
57 require_authenticated(actor)?;
58 crate::features::issue::usecase::load_issue(svc, channel_id, issue_id).await?;
59 let mode = parse_mode(mode_str)?;
60 let db = svc.db.writer();
61 let now = OffsetDateTime::now_utc();
62 let active: watch_entity::ActiveModel = watch_entity::ActiveModel::builder()
63 .set_id(Uuid::new_v4())
64 .set_issue_id(issue_id)
65 .set_user_id(actor)
66 .set_mode(mode)
67 .set_created_at(now)
68 .set_updated_at(Some(now))
69 .into();
70 let saved = watch_entity::Entity::insert(active)
71 .on_conflict(
72 OnConflict::columns([watch_entity::COLUMN.issue_id, watch_entity::COLUMN.user_id])
73 .update_column(watch_entity::COLUMN.mode)
74 .update_column(watch_entity::COLUMN.updated_at)
75 .to_owned(),
76 )
77 .exec_with_returning(&db)
78 .await?;
79 Ok(saved.into())
80 }
81 .await;
82 metrics::record_outcome(metrics::FEATURE_ISSUE_WATCHES_UPSERT, &result);
83 result
7784}
No newline at end of file
Modified lib/issues/features/issues/usecase/create.rs lib/issues/features/issues/usecase/create.rs +29 −22 View lib/issues/features/issues/usecase/create.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::{CollectionView, CreateCollectionOption};
1011use super::helpers::require_authenticated;
@@ -14,29 +15,35 @@
1415 actor: Uuid,
1516 input: CreateCollectionOption,
1617) -> IssueResult<CollectionView> {
17 require_authenticated(actor)?;
18 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_BOARDS_CREATE);
19 let result: IssueResult<CollectionView> = async {
20 require_authenticated(actor)?;
1821
19 let name = input.name.trim().to_string();
20 if name.is_empty() || name.len() > svc.config.max_collection_name_len {
21 return Err(IssueError::BadRequest(format!(
22 "collection name length must be in 1..={}",
23 svc.config.max_collection_name_len
24 )));
25 }
22 let name = input.name.trim().to_string();
23 if name.is_empty() || name.len() > svc.config.max_collection_name_len {
24 return Err(IssueError::BadRequest(format!(
25 "collection name length must be in 1..={}",
26 svc.config.max_collection_name_len
27 )));
28 }
2629
27 let db = svc.db.writer();
28 let now = OffsetDateTime::now_utc();
29 let collection_id = Uuid::new_v4();
30 let db = svc.db.writer();
31 let now = OffsetDateTime::now_utc();
32 let collection_id = Uuid::new_v4();
3033
31 let active: issues_entity::ActiveModel = issues_entity::ActiveModel::builder()
32 .set_id(collection_id)
33 .set_channel_id(input.channel_id)
34 .set_name(name)
35 .set_icon(input.icon)
36 .set_description(input.description)
37 .set_created_at(now)
38 .set_updated_at(None)
39 .into();
40 let saved = active.insert(&db).await?;
41 Ok(CollectionView::from(saved))
34 let active: issues_entity::ActiveModel = issues_entity::ActiveModel::builder()
35 .set_id(collection_id)
36 .set_channel_id(input.channel_id)
37 .set_name(name)
38 .set_icon(input.icon)
39 .set_description(input.description)
40 .set_created_at(now)
41 .set_updated_at(None)
42 .into();
43 let saved = active.insert(&db).await?;
44 Ok(CollectionView::from(saved))
45 }
46 .await;
47 metrics::record_outcome(metrics::FEATURE_ISSUE_BOARDS_CREATE, &result);
48 result
4249}
No newline at end of file
Modified lib/issues/features/issues/usecase/delete.rs lib/issues/features/issues/usecase/delete.rs +13 −6 View lib/issues/features/issues/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::IssueResult;
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_collection, require_authenticated};
89
@@ -12,10 +13,16 @@
1213 channel_id: Uuid,
1314 collection_id: Uuid,
1415) -> IssueResult<()> {
15 require_authenticated(actor)?;
16 let cur = load_collection(svc, channel_id, collection_id).await?;
17 let db = svc.db.writer();
18 let active: db::issue::issues::ActiveModel = cur.into();
19 active.delete(&db).await?;
20 Ok(())
16 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_BOARDS_DELETE);
17 let result: IssueResult<()> = async {
18 require_authenticated(actor)?;
19 let cur = load_collection(svc, channel_id, collection_id).await?;
20 let db = svc.db.writer();
21 let active: db::issue::issues::ActiveModel = cur.into();
22 active.delete(&db).await?;
23 Ok(())
24 }
25 .await;
26 metrics::record_outcome(metrics::FEATURE_ISSUE_BOARDS_DELETE, &result);
27 result
2128}
No newline at end of file
Modified lib/issues/features/issues/usecase/get.rs lib/issues/features/issues/usecase/get.rs +10 −3 View lib/issues/features/issues/usecase/get.rs
@@ -2,6 +2,7 @@
22
33use crate::error::IssueResult;
44use crate::features::IssueService;
5use crate::metrics;
56
67use super::super::CollectionView;
78use super::helpers::{load_collection, require_authenticated};
@@ -12,7 +13,13 @@
1213 channel_id: Uuid,
1314 collection_id: Uuid,
1415) -> IssueResult<CollectionView> {
15 require_authenticated(actor)?;
16 let c = load_collection(svc, channel_id, collection_id).await?;
17 Ok(CollectionView::from(c))
16 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_BOARDS_GET);
17 let result: IssueResult<CollectionView> = async {
18 require_authenticated(actor)?;
19 let c = load_collection(svc, channel_id, collection_id).await?;
20 Ok(CollectionView::from(c))
21 }
22 .await;
23 metrics::record_outcome(metrics::FEATURE_ISSUE_BOARDS_GET, &result);
24 result
1825}
No newline at end of file
Modified lib/issues/features/issues/usecase/list.rs lib/issues/features/issues/usecase/list.rs +29 −22 View lib/issues/features/issues/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{CollectionPage, CollectionView, ListCollectionQuery};
910use super::helpers::require_authenticated;
@@ -13,30 +14,36 @@
1314 actor: Uuid,
1415 q: ListCollectionQuery,
1516) -> IssueResult<CollectionPage> {
16 require_authenticated(actor)?;
17 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_BOARDS_LIST);
18 let result: IssueResult<CollectionPage> = async {
19 require_authenticated(actor)?;
1720
18 let limit = q.limit.unwrap_or(svc.config.default_pagination);
19 let offset = q.offset.unwrap_or(0);
20 if limit == 0 || limit > svc.config.max_pagination {
21 return Err(IssueError::BadRequest(format!(
22 "limit must be in 1..={}",
23 svc.config.max_pagination
24 )));
25 }
21 let limit = q.limit.unwrap_or(svc.config.default_pagination);
22 let offset = q.offset.unwrap_or(0);
23 if limit == 0 || limit > svc.config.max_pagination {
24 return Err(IssueError::BadRequest(format!(
25 "limit must be in 1..={}",
26 svc.config.max_pagination
27 )));
28 }
2629
27 let db = svc.db.reader();
28 let finder = issues_entity::Entity::find()
29 .apply_if(q.channel_id, |f, cid| f.filter(issues_entity::COLUMN.channel_id.eq(cid)))
30 .apply_if(q.q, |f, s| f.filter(issues_entity::COLUMN.name.contains(&s)));
30 let db = svc.db.reader();
31 let finder = issues_entity::Entity::find()
32 .apply_if(q.channel_id, |f, cid| f.filter(issues_entity::COLUMN.channel_id.eq(cid)))
33 .apply_if(q.q, |f, s| f.filter(issues_entity::COLUMN.name.contains(&s)));
3134
32 let total = finder.clone().count(&db).await? as i64;
33 let rows = finder
34 .order_by_desc(issues_entity::COLUMN.created_at)
35 .limit(limit)
36 .offset(offset)
37 .all(&db)
38 .await?;
35 let total = finder.clone().count(&db).await? as i64;
36 let rows = finder
37 .order_by_desc(issues_entity::COLUMN.created_at)
38 .limit(limit)
39 .offset(offset)
40 .all(&db)
41 .await?;
3942
40 let items = rows.into_iter().map(CollectionView::from).collect();
41 Ok(CollectionPage { items, total })
43 let items = rows.into_iter().map(CollectionView::from).collect();
44 Ok(CollectionPage { items, total })
45 }
46 .await;
47 metrics::record_outcome(metrics::FEATURE_ISSUE_BOARDS_LIST, &result);
48 result
4249}
No newline at end of file
Modified lib/issues/features/issues/usecase/update.rs lib/issues/features/issues/usecase/update.rs +26 −19 View lib/issues/features/issues/usecase/update.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{CollectionView, UpdateCollectionOption};
910use super::helpers::{load_collection, require_authenticated};
@@ -15,27 +16,33 @@
1516 collection_id: Uuid,
1617 input: UpdateCollectionOption,
1718) -> IssueResult<CollectionView> {
18 require_authenticated(actor)?;
19 let _timer = metrics::Timer::start(metrics::FEATURE_ISSUE_BOARDS_UPDATE);
20 let result: IssueResult<CollectionView> = async {
21 require_authenticated(actor)?;
1922
20 let cur = load_collection(svc, channel_id, collection_id).await?;
21 let db = svc.db.writer();
23 let cur = load_collection(svc, channel_id, collection_id).await?;
24 let db = svc.db.writer();
2225
23 let mut active: db::issue::issues::ActiveModel = cur.into();
24 if let Some(new_name) = input.name {
25 let trimmed = new_name.trim().to_string();
26 if trimmed.is_empty() || trimmed.len() > svc.config.max_collection_name_len {
27 return Err(IssueError::BadRequest("collection name length invalid".into()));
26 let mut active: db::issue::issues::ActiveModel = cur.into();
27 if let Some(new_name) = input.name {
28 let trimmed = new_name.trim().to_string();
29 if trimmed.is_empty() || trimmed.len() > svc.config.max_collection_name_len {
30 return Err(IssueError::BadRequest("collection name length invalid".into()));
31 }
32 active.name = Set(trimmed);
33 }
34 if let Some(opt) = input.icon {
35 active.icon = Set(opt);
2836 }
29 active.name = Set(trimmed);
30 }
31 if let Some(opt) = input.icon {
32 active.icon = Set(opt);
33 }
34 if let Some(opt) = input.description {
35 active.description = Set(opt);
37 if let Some(opt) = input.description {
38 active.description = Set(opt);
39 }
40 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
41
42 let saved = active.update(&db).await?;
43 Ok(CollectionView::from(saved))
3644 }
37 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
38
39 let saved = active.update(&db).await?;
40 Ok(CollectionView::from(saved))
45 .await;
46 metrics::record_outcome(metrics::FEATURE_ISSUE_BOARDS_UPDATE, &result);
47 result
4148}
No newline at end of file
Modified lib/issues/features/label/usecase/bind.rs lib/issues/features/label/usecase/bind.rs +31 −24 View lib/issues/features/label/usecase/bind.rs
@@ -6,6 +6,7 @@
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
88use crate::features::issue::usecase::load_issue;
9use crate::metrics;
910
1011use super::helpers::{load_label, require_authenticated};
1112
@@ -16,30 +17,36 @@
1617 issue_id: Uuid,
1718 label_id: Uuid,
1819) -> IssueResult<()> {
19 require_authenticated(actor)?;
20 let _issue = load_issue(svc, channel_id, issue_id).await?;
21 let _label = load_label(svc, channel_id, label_id).await?;
20 let _timer = metrics::Timer::start(metrics::FEATURE_LABELS_BIND);
21 let result: IssueResult<()> = async {
22 require_authenticated(actor)?;
23 let _issue = load_issue(svc, channel_id, issue_id).await?;
24 let _label = load_label(svc, channel_id, label_id).await?;
2225
23 let db = svc.db.reader();
24 let existing = rel_entity::Entity::find()
25 .filter(rel_entity::COLUMN.issue_id.eq(issue_id))
26 .filter(rel_entity::COLUMN.label_id.eq(label_id))
27 .one(&db)
28 .await?;
29 if existing.is_some() {
30 return Err(IssueError::Conflict(format!(
31 "label {label_id} already bound to issue {issue_id}"
32 )));
33 }
26 let db = svc.db.reader();
27 let existing = rel_entity::Entity::find()
28 .filter(rel_entity::COLUMN.issue_id.eq(issue_id))
29 .filter(rel_entity::COLUMN.label_id.eq(label_id))
30 .one(&db)
31 .await?;
32 if existing.is_some() {
33 return Err(IssueError::Conflict(format!(
34 "label {label_id} already bound to issue {issue_id}"
35 )));
36 }
3437
35 let db = svc.db.writer();
36 let now = OffsetDateTime::now_utc();
37 let active: rel_entity::ActiveModel = rel_entity::ActiveModel::builder()
38 .set_id(Uuid::new_v4())
39 .set_issue_id(issue_id)
40 .set_label_id(label_id)
41 .set_created_at(now)
42 .into();
43 active.insert(&db).await?;
44 Ok(())
38 let db = svc.db.writer();
39 let now = OffsetDateTime::now_utc();
40 let active: rel_entity::ActiveModel = rel_entity::ActiveModel::builder()
41 .set_id(Uuid::new_v4())
42 .set_issue_id(issue_id)
43 .set_label_id(label_id)
44 .set_created_at(now)
45 .into();
46 active.insert(&db).await?;
47 Ok(())
48 }
49 .await;
50 metrics::record_outcome(metrics::FEATURE_LABELS_BIND, &result);
51 result
4552}
No newline at end of file
Modified lib/issues/features/label/usecase/create.rs lib/issues/features/label/usecase/create.rs +38 −31 View lib/issues/features/label/usecase/create.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::{CreateLabelOption, LabelView};
1011use super::helpers::require_authenticated;
@@ -15,37 +16,43 @@
1516 channel_id: Uuid,
1617 input: CreateLabelOption,
1718) -> IssueResult<LabelView> {
18 require_authenticated(actor)?;
19 let _timer = metrics::Timer::start(metrics::FEATURE_LABELS_CREATE);
20 let result: IssueResult<LabelView> = async {
21 require_authenticated(actor)?;
1922
20 let name = input.name.trim().to_string();
21 if name.is_empty() || name.len() > svc.config.max_label_name_len {
22 return Err(IssueError::BadRequest(format!(
23 "label name length must be in 1..={}",
24 svc.config.max_label_name_len
25 )));
26 }
27 if input.color.len() > svc.config.max_label_color_len {
28 return Err(IssueError::BadRequest(format!(
29 "label color too long (max {})",
30 svc.config.max_label_color_len
31 )));
32 }
23 let name = input.name.trim().to_string();
24 if name.is_empty() || name.len() > svc.config.max_label_name_len {
25 return Err(IssueError::BadRequest(format!(
26 "label name length must be in 1..={}",
27 svc.config.max_label_name_len
28 )));
29 }
30 if input.color.len() > svc.config.max_label_color_len {
31 return Err(IssueError::BadRequest(format!(
32 "label color too long (max {})",
33 svc.config.max_label_color_len
34 )));
35 }
3336
34 let db = svc.db.writer();
35 let now = OffsetDateTime::now_utc();
36 let id = Uuid::new_v4();
37 let active: label_entity::ActiveModel = label_entity::ActiveModel::builder()
38 .set_id(id)
39 .set_channel_id(channel_id)
40 .set_name(name)
41 .set_exclusive(input.exclusive.unwrap_or(false))
42 .set_exclusive_order(input.exclusive_order.unwrap_or(0))
43 .set_description(input.description)
44 .set_color(input.color)
45 .set_is_archived(false)
46 .set_created_at(now)
47 .set_updated_at(None)
48 .into();
49 let saved = active.insert(&db).await?;
50 Ok(LabelView::from(saved))
37 let db = svc.db.writer();
38 let now = OffsetDateTime::now_utc();
39 let id = Uuid::new_v4();
40 let active: label_entity::ActiveModel = label_entity::ActiveModel::builder()
41 .set_id(id)
42 .set_channel_id(channel_id)
43 .set_name(name)
44 .set_exclusive(input.exclusive.unwrap_or(false))
45 .set_exclusive_order(input.exclusive_order.unwrap_or(0))
46 .set_description(input.description)
47 .set_color(input.color)
48 .set_is_archived(false)
49 .set_created_at(now)
50 .set_updated_at(None)
51 .into();
52 let saved = active.insert(&db).await?;
53 Ok(LabelView::from(saved))
54 }
55 .await;
56 metrics::record_outcome(metrics::FEATURE_LABELS_CREATE, &result);
57 result
5158}
No newline at end of file
Modified lib/issues/features/label/usecase/delete.rs lib/issues/features/label/usecase/delete.rs +13 −6 View lib/issues/features/label/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::IssueResult;
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_label, require_authenticated};
89
@@ -12,10 +13,16 @@
1213 channel_id: Uuid,
1314 label_id: Uuid,
1415) -> IssueResult<()> {
15 require_authenticated(actor)?;
16 let cur = load_label(svc, channel_id, label_id).await?;
17 let db = svc.db.writer();
18 let active: db::issue::issue_label::ActiveModel = cur.into();
19 active.delete(&db).await?;
20 Ok(())
16 let _timer = metrics::Timer::start(metrics::FEATURE_LABELS_DELETE);
17 let result: IssueResult<()> = async {
18 require_authenticated(actor)?;
19 let cur = load_label(svc, channel_id, label_id).await?;
20 let db = svc.db.writer();
21 let active: db::issue::issue_label::ActiveModel = cur.into();
22 active.delete(&db).await?;
23 Ok(())
24 }
25 .await;
26 metrics::record_outcome(metrics::FEATURE_LABELS_DELETE, &result);
27 result
2128}
No newline at end of file
Modified lib/issues/features/label/usecase/get.rs lib/issues/features/label/usecase/get.rs +10 −3 View lib/issues/features/label/usecase/get.rs
@@ -2,6 +2,7 @@
22
33use crate::error::IssueResult;
44use crate::features::IssueService;
5use crate::metrics;
56
67use super::super::LabelView;
78use super::helpers::{load_label, require_authenticated};
@@ -12,7 +13,13 @@
1213 channel_id: Uuid,
1314 label_id: Uuid,
1415) -> IssueResult<LabelView> {
15 require_authenticated(actor)?;
16 let l = load_label(svc, channel_id, label_id).await?;
17 Ok(LabelView::from(l))
16 let _timer = metrics::Timer::start(metrics::FEATURE_LABELS_GET);
17 let result: IssueResult<LabelView> = async {
18 require_authenticated(actor)?;
19 let l = load_label(svc, channel_id, label_id).await?;
20 Ok(LabelView::from(l))
21 }
22 .await;
23 metrics::record_outcome(metrics::FEATURE_LABELS_GET, &result);
24 result
1825}
No newline at end of file
Modified lib/issues/features/label/usecase/list.rs lib/issues/features/label/usecase/list.rs +30 −23 View lib/issues/features/label/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{LabelPage, LabelView, ListLabelQuery};
910use super::helpers::require_authenticated;
@@ -14,31 +15,37 @@
1415 channel_id: Uuid,
1516 q: ListLabelQuery,
1617) -> IssueResult<LabelPage> {
17 require_authenticated(actor)?;
18 let _timer = metrics::Timer::start(metrics::FEATURE_LABELS_LIST);
19 let result: IssueResult<LabelPage> = async {
20 require_authenticated(actor)?;
1821
19 let limit = q.limit.unwrap_or(svc.config.default_pagination);
20 let offset = q.offset.unwrap_or(0);
21 if limit == 0 || limit > svc.config.max_pagination {
22 return Err(IssueError::BadRequest(format!(
23 "limit must be in 1..={}",
24 svc.config.max_pagination
25 )));
26 }
22 let limit = q.limit.unwrap_or(svc.config.default_pagination);
23 let offset = q.offset.unwrap_or(0);
24 if limit == 0 || limit > svc.config.max_pagination {
25 return Err(IssueError::BadRequest(format!(
26 "limit must be in 1..={}",
27 svc.config.max_pagination
28 )));
29 }
2730
28 let db = svc.db.reader();
29 let finder = label_entity::Entity::find()
30 .filter(label_entity::COLUMN.channel_id.eq(channel_id))
31 .apply_if(q.q, |f, s| f.filter(label_entity::COLUMN.name.contains(&s)))
32 .apply_if(q.is_archived, |f, arch| f.filter(label_entity::COLUMN.is_archived.eq(arch)));
31 let db = svc.db.reader();
32 let finder = label_entity::Entity::find()
33 .filter(label_entity::COLUMN.channel_id.eq(channel_id))
34 .apply_if(q.q, |f, s| f.filter(label_entity::COLUMN.name.contains(&s)))
35 .apply_if(q.is_archived, |f, arch| f.filter(label_entity::COLUMN.is_archived.eq(arch)));
3336
34 let total = finder.clone().count(&db).await? as i64;
35 let rows = finder
36 .order_by_asc(label_entity::COLUMN.name)
37 .limit(limit)
38 .offset(offset)
39 .all(&db)
40 .await?;
37 let total = finder.clone().count(&db).await? as i64;
38 let rows = finder
39 .order_by_asc(label_entity::COLUMN.name)
40 .limit(limit)
41 .offset(offset)
42 .all(&db)
43 .await?;
4144
42 let items = rows.into_iter().map(LabelView::from).collect();
43 Ok(LabelPage { items, total })
45 let items = rows.into_iter().map(LabelView::from).collect();
46 Ok(LabelPage { items, total })
47 }
48 .await;
49 metrics::record_outcome(metrics::FEATURE_LABELS_LIST, &result);
50 result
4451}
No newline at end of file
Modified lib/issues/features/label/usecase/list_for_issue.rs lib/issues/features/label/usecase/list_for_issue.rs +22 −15 View lib/issues/features/label/usecase/list_for_issue.rs
@@ -6,6 +6,7 @@
66use crate::error::IssueResult;
77use crate::features::IssueService;
88use crate::features::issue::usecase::load_issue;
9use crate::metrics;
910
1011use super::helpers::require_authenticated;
1112use super::super::LabelView;
@@ -16,21 +17,27 @@
1617 channel_id: Uuid,
1718 issue_id: Uuid,
1819) -> IssueResult<Vec<LabelView>> {
19 require_authenticated(actor)?;
20 let _issue = load_issue(svc, channel_id, issue_id).await?;
20 let _timer = metrics::Timer::start(metrics::FEATURE_LABELS_LIST_FOR_ISSUE);
21 let result: IssueResult<Vec<LabelView>> = async {
22 require_authenticated(actor)?;
23 let _issue = load_issue(svc, channel_id, issue_id).await?;
2124
22 let db = svc.db.reader();
23 let rels = rel_entity::Entity::find()
24 .filter(rel_entity::COLUMN.issue_id.eq(issue_id))
25 .all(&db)
26 .await?;
27 let label_ids: Vec<Uuid> = rels.into_iter().map(|r| r.label_id).collect();
28 if label_ids.is_empty() {
29 return Ok(Vec::new());
25 let db = svc.db.reader();
26 let rels = rel_entity::Entity::find()
27 .filter(rel_entity::COLUMN.issue_id.eq(issue_id))
28 .all(&db)
29 .await?;
30 let label_ids: Vec<Uuid> = rels.into_iter().map(|r| r.label_id).collect();
31 if label_ids.is_empty() {
32 return Ok(Vec::new());
33 }
34 let labels = label_entity::Entity::find()
35 .filter(label_entity::COLUMN.id.is_in(label_ids))
36 .all(&db)
37 .await?;
38 Ok(labels.into_iter().map(LabelView::from).collect())
3039 }
31 let labels = label_entity::Entity::find()
32 .filter(label_entity::COLUMN.id.is_in(label_ids))
33 .all(&db)
34 .await?;
35 Ok(labels.into_iter().map(LabelView::from).collect())
40 .await;
41 metrics::record_outcome(metrics::FEATURE_LABELS_LIST_FOR_ISSUE, &result);
42 result
3643}
No newline at end of file
Modified lib/issues/features/label/usecase/unbind.rs lib/issues/features/label/usecase/unbind.rs +22 −15 View lib/issues/features/label/usecase/unbind.rs
@@ -5,6 +5,7 @@
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
77use crate::features::issue::usecase::load_issue;
8use crate::metrics;
89
910use super::helpers::require_authenticated;
1011
@@ -15,20 +16,26 @@
1516 issue_id: Uuid,
1617 label_id: Uuid,
1718) -> IssueResult<()> {
18 require_authenticated(actor)?;
19 let _issue = load_issue(svc, channel_id, issue_id).await?;
19 let _timer = metrics::Timer::start(metrics::FEATURE_LABELS_UNBIND);
20 let result: IssueResult<()> = async {
21 require_authenticated(actor)?;
22 let _issue = load_issue(svc, channel_id, issue_id).await?;
2023
21 let db = svc.db.writer();
22 let rel = rel_entity::Entity::find()
23 .filter(rel_entity::COLUMN.issue_id.eq(issue_id))
24 .filter(rel_entity::COLUMN.label_id.eq(label_id))
25 .one(&db)
26 .await?
27 .ok_or_else(|| {
28 IssueError::NotFound(format!(
29 "label {label_id} not bound to issue {issue_id}"
30 ))
31 })?;
32 rel.delete(&db).await?;
33 Ok(())
24 let db = svc.db.writer();
25 let rel = rel_entity::Entity::find()
26 .filter(rel_entity::COLUMN.issue_id.eq(issue_id))
27 .filter(rel_entity::COLUMN.label_id.eq(label_id))
28 .one(&db)
29 .await?
30 .ok_or_else(|| {
31 IssueError::NotFound(format!(
32 "label {label_id} not bound to issue {issue_id}"
33 ))
34 })?;
35 rel.delete(&db).await?;
36 Ok(())
37 }
38 .await;
39 metrics::record_outcome(metrics::FEATURE_LABELS_UNBIND, &result);
40 result
3441}
No newline at end of file
Modified lib/issues/features/label/usecase/update.rs lib/issues/features/label/usecase/update.rs +42 −35 View lib/issues/features/label/usecase/update.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::{LabelView, UpdateLabelOption};
910use super::helpers::{load_label, require_authenticated};
@@ -15,43 +16,49 @@
1516 label_id: Uuid,
1617 input: UpdateLabelOption,
1718) -> IssueResult<LabelView> {
18 require_authenticated(actor)?;
19 let cur = load_label(svc, channel_id, label_id).await?;
19 let _timer = metrics::Timer::start(metrics::FEATURE_LABELS_UPDATE);
20 let result: IssueResult<LabelView> = async {
21 require_authenticated(actor)?;
22 let cur = load_label(svc, channel_id, label_id).await?;
2023
21 if let Some(ref n) = input.name {
22 let trimmed = n.trim().to_string();
23 if trimmed.is_empty() || trimmed.len() > svc.config.max_label_name_len {
24 return Err(IssueError::BadRequest("invalid label name".into()));
24 if let Some(ref n) = input.name {
25 let trimmed = n.trim().to_string();
26 if trimmed.is_empty() || trimmed.len() > svc.config.max_label_name_len {
27 return Err(IssueError::BadRequest("invalid label name".into()));
28 }
29 }
30 if let Some(ref c) = input.color
31 && c.len() > svc.config.max_label_color_len
32 {
33 return Err(IssueError::BadRequest("invalid label color".into()));
2534 }
26 }
27 if let Some(ref c) = input.color
28 && c.len() > svc.config.max_label_color_len
29 {
30 return Err(IssueError::BadRequest("invalid label color".into()));
31 }
3235
33 let db = svc.db.writer();
34 let mut active: db::issue::issue_label::ActiveModel = cur.into();
35 if let Some(n) = input.name {
36 active.name = Set(n.trim().to_string());
37 }
38 if let Some(c) = input.color {
39 active.color = Set(c);
40 }
41 if let Some(e) = input.exclusive {
42 active.exclusive = Set(e);
43 }
44 if let Some(o) = input.exclusive_order {
45 active.exclusive_order = Set(o);
46 }
47 if let Some(opt) = input.description {
48 active.description = Set(opt);
49 }
50 if let Some(a) = input.is_archived {
51 active.is_archived = Set(a);
52 }
53 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
36 let db = svc.db.writer();
37 let mut active: db::issue::issue_label::ActiveModel = cur.into();
38 if let Some(n) = input.name {
39 active.name = Set(n.trim().to_string());
40 }
41 if let Some(c) = input.color {
42 active.color = Set(c);
43 }
44 if let Some(e) = input.exclusive {
45 active.exclusive = Set(e);
46 }
47 if let Some(o) = input.exclusive_order {
48 active.exclusive_order = Set(o);
49 }
50 if let Some(opt) = input.description {
51 active.description = Set(opt);
52 }
53 if let Some(a) = input.is_archived {
54 active.is_archived = Set(a);
55 }
56 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
5457
55 let saved = active.update(&db).await?;
56 Ok(LabelView::from(saved))
58 let saved = active.update(&db).await?;
59 Ok(LabelView::from(saved))
60 }
61 .await;
62 metrics::record_outcome(metrics::FEATURE_LABELS_UPDATE, &result);
63 result
5764}
No newline at end of file
Modified lib/issues/features/milestone/usecase/create.rs lib/issues/features/milestone/usecase/create.rs +45 −38 View lib/issues/features/milestone/usecase/create.rs
@@ -6,6 +6,7 @@
66
77use crate::error::{IssueError, IssueResult};
88use crate::features::IssueService;
9use crate::metrics;
910
1011use super::super::super::issue::usecase::load_issue_collection;
1112use super::super::{CreateMilestoneOption, MilestoneView};
@@ -17,44 +18,50 @@
1718 channel_id: Uuid,
1819 input: CreateMilestoneOption,
1920) -> IssueResult<MilestoneView> {
20 require_authenticated(actor)?;
21 let _timer = metrics::Timer::start(metrics::FEATURE_MILESTONES_CREATE);
22 let result: IssueResult<MilestoneView> = async {
23 require_authenticated(actor)?;
2124
22 let name = input.name.trim().to_string();
23 if name.is_empty() || name.len() > svc.config.max_milestone_title_len {
24 return Err(IssueError::BadRequest(format!(
25 "milestone name length must be in 1..={}",
26 svc.config.max_milestone_title_len
27 )));
28 }
29 if let Some(ref d) = input.description
30 && d.len() > svc.config.max_milestone_description_len
31 {
32 return Err(IssueError::BadRequest("milestone description too long".into()));
33 }
34 let due_at = match input.due_at.as_deref() {
35 Some(s) => Some(
36 OffsetDateTime::parse(s, &Rfc3339)
37 .map_err(|e| IssueError::BadRequest(format!("invalid due_at: {e}")))?,
38 ),
39 None => None,
40 };
25 let name = input.name.trim().to_string();
26 if name.is_empty() || name.len() > svc.config.max_milestone_title_len {
27 return Err(IssueError::BadRequest(format!(
28 "milestone name length must be in 1..={}",
29 svc.config.max_milestone_title_len
30 )));
31 }
32 if let Some(ref d) = input.description
33 && d.len() > svc.config.max_milestone_description_len
34 {
35 return Err(IssueError::BadRequest("milestone description too long".into()));
36 }
37 let due_at = match input.due_at.as_deref() {
38 Some(s) => Some(
39 OffsetDateTime::parse(s, &Rfc3339)
40 .map_err(|e| IssueError::BadRequest(format!("invalid due_at: {e}")))?,
41 ),
42 None => None,
43 };
4144
42 let collection = load_issue_collection(svc, channel_id).await?;
43 let db = svc.db.writer();
44 let now = OffsetDateTime::now_utc();
45 let id = Uuid::new_v4();
46 let active: milestone_entity::ActiveModel = milestone_entity::ActiveModel::builder()
47 .set_id(id)
48 .set_collection_id(collection.id)
49 .set_name(name)
50 .set_description(input.description)
51 .set_is_closed(false)
52 .set_due_at(due_at)
53 .set_num_issues(0)
54 .set_num_closed_issues(0)
55 .set_created_at(now)
56 .set_updated_at(None)
57 .into();
58 let saved = active.insert(&db).await?;
59 Ok(MilestoneView::from(saved))
45 let collection = load_issue_collection(svc, channel_id).await?;
46 let db = svc.db.writer();
47 let now = OffsetDateTime::now_utc();
48 let id = Uuid::new_v4();
49 let active: milestone_entity::ActiveModel = milestone_entity::ActiveModel::builder()
50 .set_id(id)
51 .set_collection_id(collection.id)
52 .set_name(name)
53 .set_description(input.description)
54 .set_is_closed(false)
55 .set_due_at(due_at)
56 .set_num_issues(0)
57 .set_num_closed_issues(0)
58 .set_created_at(now)
59 .set_updated_at(None)
60 .into();
61 let saved = active.insert(&db).await?;
62 Ok(MilestoneView::from(saved))
63 }
64 .await;
65 metrics::record_outcome(metrics::FEATURE_MILESTONES_CREATE, &result);
66 result
6067}
No newline at end of file
Modified lib/issues/features/milestone/usecase/delete.rs lib/issues/features/milestone/usecase/delete.rs +13 −6 View lib/issues/features/milestone/usecase/delete.rs
@@ -3,6 +3,7 @@
33
44use crate::error::IssueResult;
55use crate::features::IssueService;
6use crate::metrics;
67
78use super::helpers::{load_milestone, require_authenticated};
89
@@ -12,10 +13,16 @@
1213 channel_id: Uuid,
1314 milestone_id: Uuid,
1415) -> IssueResult<()> {
15 require_authenticated(actor)?;
16 let cur = load_milestone(svc, channel_id, milestone_id).await?;
17 let db = svc.db.writer();
18 let active: db::issue::milestone::ActiveModel = cur.into();
19 active.delete(&db).await?;
20 Ok(())
16 let _timer = metrics::Timer::start(metrics::FEATURE_MILESTONES_DELETE);
17 let result: IssueResult<()> = async {
18 require_authenticated(actor)?;
19 let cur = load_milestone(svc, channel_id, milestone_id).await?;
20 let db = svc.db.writer();
21 let active: db::issue::milestone::ActiveModel = cur.into();
22 active.delete(&db).await?;
23 Ok(())
24 }
25 .await;
26 metrics::record_outcome(metrics::FEATURE_MILESTONES_DELETE, &result);
27 result
2128}
No newline at end of file
Modified lib/issues/features/milestone/usecase/get.rs lib/issues/features/milestone/usecase/get.rs +10 −3 View lib/issues/features/milestone/usecase/get.rs
@@ -2,6 +2,7 @@
22
33use crate::error::IssueResult;
44use crate::features::IssueService;
5use crate::metrics;
56
67use super::super::MilestoneView;
78use super::helpers::{load_milestone, require_authenticated};
@@ -12,7 +13,13 @@
1213 channel_id: Uuid,
1314 milestone_id: Uuid,
1415) -> IssueResult<MilestoneView> {
15 require_authenticated(actor)?;
16 let m = load_milestone(svc, channel_id, milestone_id).await?;
17 Ok(MilestoneView::from(m))
16 let _timer = metrics::Timer::start(metrics::FEATURE_MILESTONES_GET);
17 let result: IssueResult<MilestoneView> = async {
18 require_authenticated(actor)?;
19 let m = load_milestone(svc, channel_id, milestone_id).await?;
20 Ok(MilestoneView::from(m))
21 }
22 .await;
23 metrics::record_outcome(metrics::FEATURE_MILESTONES_GET, &result);
24 result
1825}
No newline at end of file
Modified lib/issues/features/milestone/usecase/list.rs lib/issues/features/milestone/usecase/list.rs +31 −24 View lib/issues/features/milestone/usecase/list.rs
@@ -4,6 +4,7 @@
44
55use crate::error::{IssueError, IssueResult};
66use crate::features::IssueService;
7use crate::metrics;
78
89use super::super::super::issue::usecase::load_issue_collection;
910use super::super::{ListMilestoneQuery, MilestonePage, MilestoneView};
@@ -15,33 +16,39 @@
1516 channel_id: Uuid,
1617 q: ListMilestoneQuery,
1718) -> IssueResult<MilestonePage> {
18 require_authenticated(actor)?;
19 let _timer = metrics::Timer::start(metrics::FEATURE_MILESTONES_LIST);
20 let result: IssueResult<MilestonePage> = async {
21 require_authenticated(actor)?;
1922
20 let limit = q.limit.unwrap_or(svc.config.default_pagination);
21 let offset = q.offset.unwrap_or(0);
22 if limit == 0 || limit > svc.config.max_pagination {
23 return Err(IssueError::BadRequest(format!(
24 "limit must be in 1..={}",
25 svc.config.max_pagination
26 )));
27 }
23 let limit = q.limit.unwrap_or(svc.config.default_pagination);
24 let offset = q.offset.unwrap_or(0);
25 if limit == 0 || limit > svc.config.max_pagination {
26 return Err(IssueError::BadRequest(format!(
27 "limit must be in 1..={}",
28 svc.config.max_pagination
29 )));
30 }
2831
29 let collection = load_issue_collection(svc, channel_id).await?;
30 let db = svc.db.reader();
32 let collection = load_issue_collection(svc, channel_id).await?;
33 let db = svc.db.reader();
3134
32 let finder = milestone_entity::Entity::find()
33 .filter(milestone_entity::COLUMN.collection_id.eq(collection.id))
34 .apply_if(q.is_closed, |f, closed| f.filter(milestone_entity::COLUMN.is_closed.eq(closed)))
35 .apply_if(q.q, |f, s| f.filter(milestone_entity::COLUMN.name.contains(&s)));
35 let finder = milestone_entity::Entity::find()
36 .filter(milestone_entity::COLUMN.collection_id.eq(collection.id))
37 .apply_if(q.is_closed, |f, closed| f.filter(milestone_entity::COLUMN.is_closed.eq(closed)))
38 .apply_if(q.q, |f, s| f.filter(milestone_entity::COLUMN.name.contains(&s)));
3639
37 let total = finder.clone().count(&db).await? as i64;
38 let rows = finder
39 .order_by_desc(milestone_entity::COLUMN.created_at)
40 .limit(limit)
41 .offset(offset)
42 .all(&db)
43 .await?;
40 let total = finder.clone().count(&db).await? as i64;
41 let rows = finder
42 .order_by_desc(milestone_entity::COLUMN.created_at)
43 .limit(limit)
44 .offset(offset)
45 .all(&db)
46 .await?;
4447
45 let items = rows.into_iter().map(MilestoneView::from).collect();
46 Ok(MilestonePage { items, total })
48 let items = rows.into_iter().map(MilestoneView::from).collect();
49 Ok(MilestonePage { items, total })
50 }
51 .await;
52 metrics::record_outcome(metrics::FEATURE_MILESTONES_LIST, &result);
53 result
4754}
No newline at end of file
Modified lib/issues/features/milestone/usecase/update.rs lib/issues/features/milestone/usecase/update.rs +42 −35 View lib/issues/features/milestone/usecase/update.rs
@@ -5,6 +5,7 @@
55
66use crate::error::{IssueError, IssueResult};
77use crate::features::IssueService;
8use crate::metrics;
89
910use super::super::{MilestoneView, UpdateMilestoneOption};
1011use super::helpers::{load_milestone, require_authenticated};
@@ -16,43 +17,49 @@
1617 milestone_id: Uuid,
1718 input: UpdateMilestoneOption,
1819) -> IssueResult<MilestoneView> {
19 require_authenticated(actor)?;
20 let cur = load_milestone(svc, channel_id, milestone_id).await?;
20 let _timer = metrics::Timer::start(metrics::FEATURE_MILESTONES_UPDATE);
21 let result: IssueResult<MilestoneView> = async {
22 require_authenticated(actor)?;
23 let cur = load_milestone(svc, channel_id, milestone_id).await?;
2124
22 if let Some(ref n) = input.name {
23 let trimmed = n.trim().to_string();
24 if trimmed.is_empty() || trimmed.len() > svc.config.max_milestone_title_len {
25 return Err(IssueError::BadRequest("invalid milestone name".into()));
25 if let Some(ref n) = input.name {
26 let trimmed = n.trim().to_string();
27 if trimmed.is_empty() || trimmed.len() > svc.config.max_milestone_title_len {
28 return Err(IssueError::BadRequest("invalid milestone name".into()));
29 }
2630 }
27 }
28 if let Some(Some(d)) = input.description.as_ref()
29 && d.len() > svc.config.max_milestone_description_len
30 {
31 return Err(IssueError::BadRequest("description too long".into()));
32 }
33 let due_at = match input.due_at {
34 Some(Some(s)) => Some(
35 OffsetDateTime::parse(&s, &Rfc3339)
36 .map_err(|e| IssueError::BadRequest(format!("invalid due_at: {e}")))?,
37 ),
38 Some(None) => None,
39 None => cur.due_at,
40 };
31 if let Some(Some(d)) = input.description.as_ref()
32 && d.len() > svc.config.max_milestone_description_len
33 {
34 return Err(IssueError::BadRequest("description too long".into()));
35 }
36 let due_at = match input.due_at {
37 Some(Some(s)) => Some(
38 OffsetDateTime::parse(&s, &Rfc3339)
39 .map_err(|e| IssueError::BadRequest(format!("invalid due_at: {e}")))?,
40 ),
41 Some(None) => None,
42 None => cur.due_at,
43 };
4144
42 let db = svc.db.writer();
43 let mut active: db::issue::milestone::ActiveModel = cur.into();
44 if let Some(n) = input.name {
45 active.name = Set(n.trim().to_string());
46 }
47 if let Some(opt) = input.description {
48 active.description = Set(opt);
49 }
50 active.due_at = Set(due_at);
51 if let Some(c) = input.is_closed {
52 active.is_closed = Set(c);
45 let db = svc.db.writer();
46 let mut active: db::issue::milestone::ActiveModel = cur.into();
47 if let Some(n) = input.name {
48 active.name = Set(n.trim().to_string());
49 }
50 if let Some(opt) = input.description {
51 active.description = Set(opt);
52 }
53 active.due_at = Set(due_at);
54 if let Some(c) = input.is_closed {
55 active.is_closed = Set(c);
56 }
57 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
58
59 let saved = active.update(&db).await?;
60 Ok(MilestoneView::from(saved))
5361 }
54 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
55
56 let saved = active.update(&db).await?;
57 Ok(MilestoneView::from(saved))
62 .await;
63 metrics::record_outcome(metrics::FEATURE_MILESTONES_UPDATE, &result);
64 result
5865}
No newline at end of file