Skip to content
Clone

feat(project): add members usecase

zhenyi committed
parent 26ee5da commit 46bc7df Browse files

1 file changed

+177 −0

Added lib/project/features/members/usecase.rs View lib/project/features/members/usecase.rs
@@ -0,0 +1,177 @@
1use db::project::project_members::{ProjectMemberRole as DbRole, Column as Mc};
2use sea_orm::{
3 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set,
4};
5use time::OffsetDateTime;
6use uuid::Uuid;
7
8use crate::error::{ProjectError, ProjectResult};
9use crate::features::lifecycle::usecase::log_activity;
10use crate::features::ProjectService;
11use crate::permission;
12
13use super::types::{AddMemberCmd, MemberPage, MemberView, UpdateMemberCmd};
14
15pub async fn add_member(
16 svc: &ProjectService,
17 actor: Uuid,
18 space_id: Uuid,
19 project_id: Uuid,
20 cmd: AddMemberCmd,
21) -> ProjectResult<MemberView> {
22 crate::features::authz::require(
23 svc,
24 actor,
25 space_id,
26 project_id,
27 permission::MANAGE_MEMBERS,
28 )
29 .await?;
30
31 let role: DbRole = cmd.role.try_into()?;
32 let now = OffsetDateTime::now_utc();
33 let existing = db::project::project_members::Entity::find()
34 .filter(Mc::ProjectId.eq(project_id))
35 .filter(Mc::UserId.eq(cmd.user_id))
36 .one(&svc.db.reader())
37 .await?;
38
39 let row = if let Some(m) = existing {
40 let mut active: db::project::project_members::ActiveModel = m.into();
41 active.role = Set(role);
42 active.updated_at = Set(Some(now));
43 active.update(&svc.db.writer()).await?
44 } else {
45 db::project::project_members::ActiveModel {
46 id: Set(Uuid::new_v4()),
47 project_id: Set(project_id),
48 user_id: Set(cmd.user_id),
49 role: Set(role),
50 joined_at: Set(now),
51 updated_at: Set(None),
52 }
53 .insert(&svc.db.writer())
54 .await?
55 };
56
57 let _ = svc.cache.invalidate_member_role(project_id, cmd.user_id).await;
58 log_activity(svc, project_id, actor, "member_added", "member", Some(cmd.user_id), None)
59 .await;
60 Ok(member_view(&row))
61}
62
63pub async fn list_members(
64 svc: &ProjectService,
65 actor: Uuid,
66 space_id: Uuid,
67 project_id: Uuid,
68) -> ProjectResult<MemberPage> {
69 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
70 .await?;
71 let rows = db::project::project_members::Entity::find()
72 .filter(Mc::ProjectId.eq(project_id))
73 .order_by_desc(Mc::JoinedAt)
74 .all(&svc.db.reader())
75 .await?;
76 let items = rows.iter().map(member_view).collect::<Vec<_>>();
77 let total = items.len() as u64;
78 Ok(MemberPage { total, items })
79}
80
81pub async fn get_member(
82 svc: &ProjectService,
83 actor: Uuid,
84 space_id: Uuid,
85 project_id: Uuid,
86 user_id: Uuid,
87) -> ProjectResult<MemberView> {
88 crate::features::authz::require(svc, actor, space_id, project_id, permission::VIEW_PROJECT)
89 .await?;
90 let row = db::project::project_members::Entity::find()
91 .filter(Mc::ProjectId.eq(project_id))
92 .filter(Mc::UserId.eq(user_id))
93 .one(&svc.db.reader())
94 .await?
95 .ok_or_else(|| ProjectError::NotFound(format!("member {user_id} in project {project_id}")))?;
96 Ok(member_view(&row))
97}
98
99pub async fn update_member(
100 svc: &ProjectService,
101 actor: Uuid,
102 space_id: Uuid,
103 project_id: Uuid,
104 user_id: Uuid,
105 cmd: UpdateMemberCmd,
106) -> ProjectResult<MemberView> {
107 crate::features::authz::require(
108 svc,
109 actor,
110 space_id,
111 project_id,
112 permission::MANAGE_MEMBERS,
113 )
114 .await?;
115
116 let row = db::project::project_members::Entity::find()
117 .filter(Mc::ProjectId.eq(project_id))
118 .filter(Mc::UserId.eq(user_id))
119 .one(&svc.db.reader())
120 .await?
121 .ok_or_else(|| ProjectError::NotFound(format!("member {user_id} in project {project_id}")))?;
122
123 let role: DbRole = cmd.role.try_into()?;
124 let mut active: db::project::project_members::ActiveModel = row.into();
125 active.role = Set(role);
126 active.updated_at = Set(Some(OffsetDateTime::now_utc()));
127 let updated = active.update(&svc.db.writer()).await?;
128
129 let _ = svc.cache.invalidate_member_role(project_id, user_id).await;
130 log_activity(svc, project_id, actor, "member_updated", "member", Some(user_id), None)
131 .await;
132 Ok(member_view(&updated))
133}
134
135pub async fn remove_member(
136 svc: &ProjectService,
137 actor: Uuid,
138 space_id: Uuid,
139 project_id: Uuid,
140 user_id: Uuid,
141) -> ProjectResult<()> {
142 crate::features::authz::require(
143 svc,
144 actor,
145 space_id,
146 project_id,
147 permission::MANAGE_MEMBERS,
148 )
149 .await?;
150
151 let deleted = db::project::project_members::Entity::delete_many()
152 .filter(Mc::ProjectId.eq(project_id))
153 .filter(Mc::UserId.eq(user_id))
154 .exec(&svc.db.writer())
155 .await?;
156 if deleted.rows_affected == 0 {
157 return Err(ProjectError::NotFound(format!(
158 "member {user_id} in project {project_id}"
159 )));
160 }
161
162 let _ = svc.cache.invalidate_member_role(project_id, user_id).await;
163 log_activity(svc, project_id, actor, "member_removed", "member", Some(user_id), None)
164 .await;
165 Ok(())
166}
167
168fn member_view(m: &db::project::project_members::Model) -> MemberView {
169 MemberView {
170 id: m.id,
171 project_id: m.project_id,
172 user_id: m.user_id,
173 role: m.role.into(),
174 joined_at: m.joined_at,
175 updated_at: m.updated_at,
176 }
177}
No newline at end of file