Skip to content
Clone

feat(models): phase2 schema changes (room roles, issue priority/due/assignees, access key expiry, watch 3-state, event enum, pipeline runs)

ZhenYi committed
parent 2891708 commit f0baf26 Browse files

28 files changed

+595 −145

File tree 28
Modified lib/auth/src/services/access_keys.rs View lib/auth/src/services/access_keys.rs
@@ -18,6 +18,8 @@
1818pub struct CreateAccessKeyCmd {
1919 pub name: String,
2020 pub scope: AccessKeyScope,
21 #[serde(default)]
22 pub expires_at: Option<OffsetDateTime>,
2123}
2224
2325#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -26,6 +28,8 @@
2628 pub name: String,
2729 pub scope: AccessKeyScope,
2830 pub created_at: OffsetDateTime,
31 pub expires_at: Option<OffsetDateTime>,
32 pub last_used_at: Option<OffsetDateTime>,
2933}
3034
3135#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -69,6 +73,8 @@
6973 version: Set(1),
7074 created_at: Set(now),
7175 updated_at: Set(now),
76 expires_at: Set(cmd.expires_at),
77 last_used_at: Set(None),
7278 };
7379 let inserted = row.insert(db.write()).await?;
7480 Ok(CreateAccessKeyResp {
@@ -77,6 +83,8 @@
7783 name,
7884 scope: inserted.scope,
7985 created_at: inserted.created_at,
86 expires_at: inserted.expires_at,
87 last_used_at: inserted.last_used_at,
8088 },
8189 token,
8290 })
@@ -102,6 +110,8 @@
102110 name: row.name,
103111 scope: row.scope,
104112 created_at: row.created_at,
113 expires_at: row.expires_at,
114 last_used_at: row.last_used_at,
105115 })
106116 .collect())
107117 }
Modified lib/chat/src/services/dms.rs View lib/chat/src/services/dms.rs
@@ -126,6 +126,7 @@
126126 room_members::ActiveModel {
127127 room_id: Set(room.id),
128128 user_id: Set(uid),
129 role: Set(models::room::room_members::RoomMemberRole::Member),
129130 nick: Set(None),
130131 joined_at: Set(now),
131132 left_at: Set(None),
Modified lib/chat/src/services/reminders.rs View lib/chat/src/services/reminders.rs
@@ -21,6 +21,7 @@
2121 pub room_id: Uuid,
2222 pub message_id: Uuid,
2323 pub remind_at: OffsetDateTime,
24 pub note: Option<String>,
2425 pub status: ReminderStatus,
2526 pub created_at: OffsetDateTime,
2627}
@@ -28,6 +29,8 @@
2829#[derive(Debug, Clone, Deserialize, ToSchema)]
2930pub struct CreateReminderCmd {
3031 pub remind_at: OffsetDateTime,
32 #[serde(default)]
33 pub note: Option<String>,
3134}
3235
3336impl From<message_reminders::Model> for ReminderDto {
@@ -37,6 +40,7 @@
3740 room_id: r.room_id,
3841 message_id: r.message_id,
3942 remind_at: r.remind_at,
43 note: r.note,
4044 status: r.status,
4145 created_at: r.created_at,
4246 }
@@ -69,6 +73,7 @@
6973 room_id: Set(room_id),
7074 message_id: Set(message_id),
7175 remind_at: Set(cmd.remind_at),
76 note: Set(cmd.note.clone().filter(|n| !n.trim().is_empty())),
7277 status: Set(ReminderStatus::Pending),
7378 created_at: Set(OffsetDateTime::now_utc()),
7479 }
Modified lib/chat/src/services/rooms.rs View lib/chat/src/services/rooms.rs
@@ -3,7 +3,7 @@
33use models::chat::room_seq;
44use models::chat::room_user_settings;
55use models::room::room_invites;
6use models::room::room_members;
6use models::room::room_members::{self, RoomMemberRole};
77use models::room::rooms::{self, RoomKind};
88use models::users::user_notifications::NotificationType;
99use rand::Rng;
@@ -79,6 +79,7 @@
7979#[derive(Debug, Clone, Serialize, ToSchema)]
8080pub struct RoomMemberDto {
8181 pub user: space::services::spaces::UserBriefDto,
82 pub role: RoomMemberRole,
8283 pub nick: Option<String>,
8384 pub joined_at: OffsetDateTime,
8485}
@@ -467,6 +468,7 @@
467468 room_members::ActiveModel {
468469 room_id: Set(room_id),
469470 user_id: Set(caller),
471 role: Set(RoomMemberRole::Member),
470472 nick: Set(None),
471473 joined_at: Set(now),
472474 left_at: Set(None),
@@ -582,6 +584,7 @@
582584 room_members::ActiveModel {
583585 room_id: Set(room_id),
584586 user_id: Set(uid),
587 role: Set(RoomMemberRole::Member),
585588 nick: Set(None),
586589 joined_at: Set(now),
587590 left_at: Set(None),
@@ -635,6 +638,7 @@
635638 .into_iter()
636639 .filter_map(|r| {
637640 user_map.get(&r.user_id).map(|u| RoomMemberDto {
641 role: r.role,
638642 user: u.clone(),
639643 nick: r.nick,
640644 joined_at: r.joined_at,
Modified lib/git/src/handler/html/watch_button.rs View lib/git/src/handler/html/watch_button.rs
@@ -33,11 +33,11 @@
3333 .get_watch_mode(&ctx.ctx, uid)
3434 .await
3535 .unwrap_or_default()
36 .unwrap_or(WatchMode::Normal),
37 None => WatchMode::Normal,
36 .unwrap_or(WatchMode::Unwatched),
37 None => WatchMode::Unwatched,
3838 };
3939
40 let is_watching = matches!(watch_mode, WatchMode::Watch | WatchMode::Normal);
40 let is_watching = watch_mode == WatchMode::Watching;
4141
4242 let base = "git-watch-button inline-flex items-center rounded-md border border-zinc-200 bg-white text-xs text-zinc-700 shadow-sm";
4343 let cls = archived_class(&ctx.dto, base);
@@ -46,9 +46,8 @@
4646 let watch_count = ctx.dto.watchers_count;
4747
4848 let mode_text = match watch_mode {
49 WatchMode::Watch => "Watching",
50 WatchMode::Normal => "Normal",
51 WatchMode::NoWatch => "Not watching",
49 WatchMode::Watching => "Watching",
50 WatchMode::Unwatched => "Not watching",
5251 WatchMode::Ignore => "Ignoring",
5352 };
5453
Modified lib/git/src/handler/watches.rs View lib/git/src/handler/watches.rs
@@ -76,7 +76,7 @@
7676 Ok(AppResponseData::ok(RepoWatchDto {
7777 user_id: uid,
7878 repo_id: ctx.model.id,
79 mode: WatchMode::NoWatch,
79 mode: WatchMode::Unwatched,
8080 created_at: time::OffsetDateTime::now_utc(),
8181 updated_at: time::OffsetDateTime::now_utc(),
8282 }))
Modified lib/git/src/share/access.rs View lib/git/src/share/access.rs
@@ -70,6 +70,34 @@
7070 if !constant_time_eq(computed.as_bytes(), row.token_hash.as_bytes()) {
7171 return Ok(None);
7272 }
73 let now = time::OffsetDateTime::now_utc();
74 if let Some(expires_at) = row.expires_at
75 && expires_at <= now
76 {
77 return Ok(None);
78 }
79 let stale = row
80 .last_used_at
81 .map(|t| (now - t).whole_minutes() >= 1)
82 .unwrap_or(true);
83 if stale {
84 let update = user_access_keys::Entity::update_many()
85 .col_expr(
86 user_access_keys::Column::LastUsedAt,
87 sea_orm::sea_query::Expr::value(now),
88 )
89 .filter(user_access_keys::Column::Id.eq(row.id))
90 .filter(
91 sea_orm::Condition::any()
92 .add(user_access_keys::Column::LastUsedAt.is_null())
93 .add(user_access_keys::Column::LastUsedAt.lt(
94 now - time::Duration::minutes(1),
95 )),
96 )
97 .exec(db.write())
98 .await;
99 let _ = update;
100 }
73101 Ok(Some((row.user_id, row.scope)))
74102}
75103
Modified lib/issues/src/services/comments.rs View lib/issues/src/services/comments.rs
@@ -1,3 +1,4 @@
1use models::issue::issue_event_line::IssueEventType;
12use models::issue::comment;
23use models::issue::issue_content_history;
34use models::issue::issue_member::{self, IssueMemberKind};
@@ -151,7 +152,7 @@
151152 txn,
152153 issue_id,
153154 caller,
154 "comment_created",
155 IssueEventType::CommentCreated,
155156 Some(comment_id),
156157 None,
157158 None,
@@ -441,7 +442,7 @@
441442 txn,
442443 issue_id,
443444 caller,
444 "comment_updated",
445 IssueEventType::CommentUpdated,
445446 Some(comment_id),
446447 None,
447448 None,
@@ -507,7 +508,7 @@
507508 txn,
508509 issue_id,
509510 caller,
510 "comment_deleted",
511 IssueEventType::CommentDeleted,
511512 Some(comment_id),
512513 None,
513514 None,
Modified lib/issues/src/services/dependencies.rs View lib/issues/src/services/dependencies.rs
@@ -1,3 +1,4 @@
1use models::issue::issue_event_line::IssueEventType;
12use models::issue::issue_dependency;
23use sea_orm::{
34 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, TransactionTrait,
@@ -101,7 +102,7 @@
101102 txn,
102103 issue_id,
103104 caller,
104 "dependency_added",
105 IssueEventType::DependencyAdded,
105106 None,
106107 None,
107108 None,
@@ -173,7 +174,7 @@
173174 txn,
174175 issue_id,
175176 caller,
176 "dependency_removed",
177 IssueEventType::DependencyRemoved,
177178 None,
178179 None,
179180 None,
Modified lib/issues/src/services/events.rs View lib/issues/src/services/events.rs
@@ -1,4 +1,4 @@
1use models::issue::issue_event_line;
1use models::issue::issue_event_line::{self, IssueEventType};
22use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
33use serde::{Deserialize, Serialize};
44use space::services::spaces::UserBriefDto;
@@ -15,7 +15,7 @@
1515 pub id: Uuid,
1616 pub issue_id: Option<Uuid>,
1717 pub user: Option<UserBriefDto>,
18 pub event_type: String,
18 pub event_type: IssueEventType,
1919 pub comment_id: Option<Uuid>,
2020 pub commit_sha: Option<String>,
2121 pub old_title: Option<String>,
Modified lib/issues/src/services/issues.rs View lib/issues/src/services/issues.rs
@@ -1,3 +1,7 @@
1use models::issue::issue_item::IssuePriority;
2use models::issue::issue_label;
3use models::space::space_members::{self, MemberStatus};
4use models::issue::issue_event_line::IssueEventType;
15use models::issue::comment;
26use models::issue::issue_content_history;
37use models::issue::issue_dependency;
@@ -48,6 +52,9 @@
4852 pub description: Option<String>,
4953 pub content: String,
5054 pub milestone_id: Option<Uuid>,
55 pub priority: IssuePriority,
56 pub due_at: Option<OffsetDateTime>,
57 pub assignees: Vec<UserBriefDto>,
5158 pub state: IssueState,
5259 pub created_at: OffsetDateTime,
5360 pub updated_at: Option<OffsetDateTime>,
@@ -62,6 +69,9 @@
6269 pub author: Option<UserBriefDto>,
6370 pub title: String,
6471 pub milestone_id: Option<Uuid>,
72 pub priority: IssuePriority,
73 pub due_at: Option<OffsetDateTime>,
74 pub assignees: Vec<UserBriefDto>,
6575 pub state: IssueState,
6676 pub created_at: OffsetDateTime,
6777 pub updated_at: Option<OffsetDateTime>,
@@ -79,6 +89,14 @@
7989 pub content: String,
8090 #[serde(default)]
8191 pub milestone_id: Option<Uuid>,
92 #[serde(default)]
93 pub priority: IssuePriority,
94 #[serde(default)]
95 pub due_at: Option<OffsetDateTime>,
96 #[serde(default)]
97 pub assignee_ids: Vec<Uuid>,
98 #[serde(default)]
99 pub label_ids: Vec<Uuid>,
82100}
83101
84102#[derive(Debug, Clone, Deserialize, ToSchema)]
@@ -91,6 +109,14 @@
91109 pub content: Option<String>,
92110 #[serde(default)]
93111 pub milestone_id: Option<Option<Uuid>>,
112 #[serde(default)]
113 pub priority: Option<IssuePriority>,
114 #[serde(default)]
115 pub due_at: Option<Option<OffsetDateTime>>,
116 #[serde(default)]
117 pub assignee_ids: Option<Vec<Uuid>>,
118 #[serde(default)]
119 pub label_ids: Option<Vec<Uuid>>,
94120}
95121
96122#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
@@ -98,6 +124,10 @@
98124 pub state: Option<IssueState>,
99125 pub author_id: Option<Uuid>,
100126 pub milestone_id: Option<Uuid>,
127 pub label_id: Option<Uuid>,
128 pub assignee_id: Option<Uuid>,
129 pub priority: Option<IssuePriority>,
130 pub sort: Option<String>,
101131 pub q: Option<String>,
102132 pub limit: Option<u64>,
103133 pub offset: Option<u64>,
@@ -207,6 +237,64 @@
207237}
208238
209239impl IssueService {
240 pub(crate) async fn load_assignee_map(
241 &self,
242 issue_ids: &[Uuid],
243 ) -> Result<std::collections::HashMap<Uuid, Vec<UserBriefDto>>, IssueError> {
244 let mut map = std::collections::HashMap::new();
245 if issue_ids.is_empty() {
246 return Ok(map);
247 }
248 let rows = issue_member::Entity::find()
249 .filter(issue_member::Column::IssueId.is_in(issue_ids.to_vec()))
250 .filter(issue_member::Column::Kind.eq(issue_member::IssueMemberKind::Assignee))
251 .all(self.db().read())
252 .await?;
253 let uids: Vec<Uuid> = rows.iter().map(|r| r.user_id).collect();
254 let user_map = self.load_user_briefs(&uids).await?;
255 for r in rows {
256 if let Some(brief) = user_map.get(&r.user_id) {
257 map.entry(r.issue_id).or_insert_with(Vec::new).push(brief.clone());
258 }
259 }
260 Ok(map)
261 }
262
263 pub(crate) async fn build_issue_dto(
264 &self,
265 item: issue_item::Model,
266 ) -> Result<IssueDto, IssueError> {
267 let user_map = self.load_user_briefs(&[item.author_id]).await?;
268 let author = user_map.get(&item.author_id).cloned();
269 let assignees = self
270 .load_assignee_map(&[item.id])
271 .await?
272 .remove(&item.id)
273 .unwrap_or_default();
274 let state = if item.closed_at.is_some() {
275 IssueState::Closed
276 } else {
277 IssueState::Open
278 };
279 Ok(IssueDto {
280 id: item.id,
281 board_id: item.collection_id,
282 number: item.autoincrement_id,
283 author,
284 title: item.title,
285 description: item.description,
286 content: item.body,
287 milestone_id: item.milestone_id,
288 priority: item.priority,
289 due_at: item.due_at,
290 assignees,
291 state,
292 created_at: item.created_at,
293 updated_at: item.updated_at,
294 closed_at: item.closed_at,
295 })
296 }
297
210298 pub async fn create_issue(
211299 &self,
212300 caller: Uuid,
@@ -252,6 +340,31 @@
252340 })?;
253341 }
254342
343 for a_uid in &cmd.assignee_ids {
344 let is_member = *a_uid == space.owner_id
345 || space_members::Entity::find_by_id((space.id, *a_uid))
346 .one(self.db().read())
347 .await?
348 .is_some_and(|m| m.status == MemberStatus::Active);
349 if !is_member {
350 return Err(IssueError::BadRequest(format!(
351 "assignee {a_uid} is not a member of this space"
352 )));
353 }
354 }
355 if !cmd.label_ids.is_empty() {
356 let count = issue_label::Entity::find()
357 .filter(issue_label::Column::Id.is_in(cmd.label_ids.clone()))
358 .filter(issue_label::Column::DirId.eq(board.dir_id))
359 .count(self.db().read())
360 .await?;
361 if count != cmd.label_ids.len() as u64 {
362 return Err(IssueError::BadRequest(
363 "one or more labels do not belong to this board".to_string(),
364 ));
365 }
366 }
367
255368 let full_text = format!("{} {}", title, cmd.content);
256369 let mentioned_uids = self
257370 .collect_mentioned_uids(&space, &full_text, caller)
@@ -263,6 +376,10 @@
263376 let desc_opt = cmd.description.clone();
264377 let body_str = cmd.content.clone();
265378 let milestone_opt = cmd.milestone_id;
379 let priority_val = cmd.priority;
380 let due_val = cmd.due_at;
381 let assignee_ids = cmd.assignee_ids.clone();
382 let label_ids = cmd.label_ids.clone();
266383 let space_lower_for_txn = space.lower_name.clone();
267384 let mentioned_for_txn = mentioned_uids.clone();
268385 let this = self.clone();
@@ -296,17 +413,43 @@
296413 description: Set(desc_opt),
297414 body: Set(body_str),
298415 milestone_id: Set(milestone_opt),
416 priority: Set(priority_val),
417 due_at: Set(due_val),
299418 created_at: Set(now),
300419 updated_at: Set(None),
301420 closed_at: Set(None),
302421 };
303422 let ins = row.insert(txn).await?;
304423
424 for a_uid in assignee_ids.clone() {
425 if a_uid == caller {
426 continue;
427 }
428 let member_row = issue_member::ActiveModel {
429 id: Set(Uuid::now_v7()),
430 issue_id: Set(issue_id),
431 user_id: Set(a_uid),
432 kind: Set(issue_member::IssueMemberKind::Assignee),
433 created_at: Set(now),
434 updated_at: Set(Some(now)),
435 };
436 member_row.insert(txn).await?;
437 }
438 for l_id in label_ids.clone() {
439 let link = issue_relation_label::ActiveModel {
440 id: Set(Uuid::now_v7()),
441 issue_id: Set(issue_id),
442 label_id: Set(l_id),
443 created_at: Set(now),
444 };
445 link.insert(txn).await?;
446 }
447
305448 this.write_event(
306449 txn,
307450 issue_id,
308451 caller,
309 "created",
452 IssueEventType::Created,
310453 None,
311454 None,
312455 Some(title_clone.clone()),
@@ -384,23 +527,7 @@
384527 }
385528 }
386529
387 let user_map = self.load_user_briefs(&[inserted.author_id]).await?;
388 let author = user_map.get(&inserted.author_id).cloned();
389
390 Ok(IssueDto {
391 id: inserted.id,
392 board_id: inserted.collection_id,
393 number: inserted.autoincrement_id,
394 author,
395 title: inserted.title,
396 description: inserted.description,
397 content: inserted.body,
398 milestone_id: inserted.milestone_id,
399 state: IssueState::Open,
400 created_at: inserted.created_at,
401 updated_at: inserted.updated_at,
402 closed_at: inserted.closed_at,
403 })
530 self.build_issue_dto(inserted).await
404531 }
405532
406533 pub async fn list_issues(
@@ -451,7 +578,38 @@
451578 }
452579 }
453580
454 select = select.order_by_desc(issue_item::Column::CreatedAt);
581 if let Some(label_id) = q.label_id {
582 let issue_ids: Vec<Uuid> = issue_relation_label::Entity::find()
583 .filter(issue_relation_label::Column::LabelId.eq(label_id))
584 .all(self.db().read())
585 .await?
586 .into_iter()
587 .map(|r| r.issue_id)
588 .collect();
589 select = select.filter(issue_item::Column::Id.is_in(issue_ids));
590 }
591 if let Some(assignee_id) = q.assignee_id {
592 let issue_ids: Vec<Uuid> = issue_member::Entity::find()
593 .filter(issue_member::Column::UserId.eq(assignee_id))
594 .filter(issue_member::Column::Kind.eq(issue_member::IssueMemberKind::Assignee))
595 .all(self.db().read())
596 .await?
597 .into_iter()
598 .map(|r| r.issue_id)
599 .collect();
600 select = select.filter(issue_item::Column::Id.is_in(issue_ids));
601 }
602 if let Some(priority) = q.priority {
603 select = select.filter(issue_item::Column::Priority.eq(priority));
604 }
605
606 match q.sort.as_deref() {
607 Some("updated_at") => select = select.order_by_desc(issue_item::Column::UpdatedAt),
608 Some("due_at") => select = select.order_by_asc(issue_item::Column::DueAt),
609 Some("priority") => select = select.order_by_desc(issue_item::Column::Priority),
610 Some("number") => select = select.order_by_asc(issue_item::Column::AutoincrementId),
611 _ => select = select.order_by_desc(issue_item::Column::CreatedAt),
612 }
455613
456614 let total = select.clone().count(self.db().read()).await? as i64;
457615 let rows = select
@@ -462,11 +620,14 @@
462620
463621 let author_ids: Vec<Uuid> = rows.iter().map(|i| i.author_id).collect();
464622 let user_map = self.load_user_briefs(&author_ids).await?;
623 let issue_ids: Vec<Uuid> = rows.iter().map(|i| i.id).collect();
624 let assignee_map = self.load_assignee_map(&issue_ids).await?;
465625
466626 let items = rows
467627 .into_iter()
468628 .map(|i| {
469629 let author = user_map.get(&i.author_id).cloned();
630 let assignees = assignee_map.get(&i.id).cloned().unwrap_or_default();
470631 let state = if i.closed_at.is_some() {
471632 IssueState::Closed
472633 } else {
@@ -479,6 +640,9 @@
479640 author,
480641 title: i.title,
481642 milestone_id: i.milestone_id,
643 priority: i.priority,
644 due_at: i.due_at,
645 assignees,
482646 state,
483647 created_at: i.created_at,
484648 updated_at: i.updated_at,
@@ -507,28 +671,7 @@
507671 let board = lookup_board(self, &space, board_id).await?;
508672 let item = lookup_issue(self, &board, issue_id).await?;
509673
510 let user_map = self.load_user_briefs(&[item.author_id]).await?;
511 let author = user_map.get(&item.author_id).cloned();
512 let state = if item.closed_at.is_some() {
513 IssueState::Closed
514 } else {
515 IssueState::Open
516 };
517
518 Ok(IssueDto {
519 id: item.id,
520 board_id: item.collection_id,
521 number: item.autoincrement_id,
522 author,
523 title: item.title,
524 description: item.description,
525 content: item.body,
526 milestone_id: item.milestone_id,
527 state,
528 created_at: item.created_at,
529 updated_at: item.updated_at,
530 closed_at: item.closed_at,
531 })
674 self.build_issue_dto(item).await
532675 }
533676
534677 pub async fn update_issue(
@@ -594,10 +737,43 @@
594737 }
595738 }
596739
740 for a_uid in cmd.assignee_ids.iter().flatten() {
741 let is_member = *a_uid == space.owner_id
742 || space_members::Entity::find_by_id((space.id, *a_uid))
743 .one(self.db().read())
744 .await?
745 .is_some_and(|m| m.status == MemberStatus::Active);
746 if !is_member {
747 return Err(IssueError::BadRequest(format!(
748 "assignee {a_uid} is not a member of this space"
749 )));
750 }
751 }
752 if let Some(label_ids) = &cmd.label_ids
753 && !label_ids.is_empty()
754 {
755 let count = issue_label::Entity::find()
756 .filter(issue_label::Column::Id.is_in(label_ids.clone()))
757 .filter(issue_label::Column::DirId.eq(board.dir_id))
758 .count(self.db().read())
759 .await?;
760 if count != label_ids.len() as u64 {
761 return Err(IssueError::BadRequest(
762 "one or more labels do not belong to this board".to_string(),
763 ));
764 }
765 }
766
597767 let now = OffsetDateTime::now_utc();
598768 let mut description_changed: Option<Option<String>> = None;
599769 let mut body_changed: Option<String> = None;
600770 let mut am: issue_item::ActiveModel = item.into();
771 if let Some(priority) = cmd.priority {
772 am.priority = Set(priority);
773 }
774 if let Some(due) = cmd.due_at {
775 am.due_at = Set(due);
776 }
601777 if let Some(ref t) = title_changed {
602778 am.title = Set(t.clone());
603779 }
@@ -617,6 +793,8 @@
617793 am.milestone_id = Set(new_milestone_val);
618794 }
619795 am.updated_at = Set(Some(now));
796 let assignee_ids_val = cmd.assignee_ids.clone();
797 let label_ids_val = cmd.label_ids.clone();
620798 let this = self.clone();
621799
622800 let updated = self
@@ -626,15 +804,69 @@
626804 let title_change_clone = title_changed.clone();
627805 let desc_change_clone = description_changed.clone();
628806 let body_change_clone = body_changed.clone();
807 let assignee_replace = assignee_ids_val.clone();
808 let label_replace = label_ids_val.clone();
629809 Box::pin(async move {
630810 let u = am.update(txn).await?;
631811
812 if let Some(new_assignees) = assignee_replace {
813 issue_member::Entity::delete_many()
814 .filter(issue_member::Column::IssueId.eq(u.id))
815 .filter(issue_member::Column::Kind.eq(issue_member::IssueMemberKind::Assignee))
816 .exec(txn)
817 .await?;
818 for a_uid in new_assignees {
819 if a_uid == u.author_id {
820 continue;
821 }
822 let member_row = issue_member::ActiveModel {
823 id: Set(Uuid::now_v7()),
824 issue_id: Set(u.id),
825 user_id: Set(a_uid),
826 kind: Set(issue_member::IssueMemberKind::Assignee),
827 created_at: Set(OffsetDateTime::now_utc()),
828 updated_at: Set(Some(OffsetDateTime::now_utc())),
829 };
830 member_row.insert(txn).await?;
831 }
832 }
833
834 if let Some(new_labels) = label_replace {
835 let old_links = issue_relation_label::Entity::find()
836 .filter(issue_relation_label::Column::IssueId.eq(u.id))
837 .all(txn)
838 .await?;
839 let old_set: std::collections::HashSet<Uuid> =
840 old_links.iter().map(|l| l.label_id).collect();
841 let new_set: std::collections::HashSet<Uuid> =
842 new_labels.iter().cloned().collect();
843 for l in old_links {
844 if !new_set.contains(&l.label_id) {
845 issue_relation_label::Entity::delete_by_id(l.id)
846 .exec(txn)
847 .await?;
848 }
849 }
850 for lid in &new_labels {
851 if old_set.contains(lid) {
852 continue;
853 }
854 let link = issue_relation_label::ActiveModel {
855 id: Set(Uuid::now_v7()),
856 issue_id: Set(u.id),
857 label_id: Set(*lid),
858 created_at: Set(OffsetDateTime::now_utc()),
859 };
860 link.insert(txn).await?;
861 }
862 }
863
632864 if let Some(new_t) = title_change_clone {
633865 this.write_event(
634866 txn,
635867 issue_id,
636868 caller,
637 "title_updated",
869 IssueEventType::TitleUpdated,
638870 None,
639871 Some(old_title.clone()),
640872 Some(new_t.clone()),
@@ -704,7 +936,7 @@
704936 txn,
705937 issue_id,
706938 caller,
707 "milestone_updated",
939 IssueEventType::MilestoneUpdated,
708940 None,
709941 None,
710942 None,
@@ -718,28 +950,7 @@
718950 })
719951 .await?;
720952
721 let user_map = self.load_user_briefs(&[updated.author_id]).await?;
722 let author = user_map.get(&updated.author_id).cloned();
723 let state = if updated.closed_at.is_some() {
724 IssueState::Closed
725 } else {
726 IssueState::Open
727 };
728
729 Ok(IssueDto {
730 id: updated.id,
731 board_id: updated.collection_id,
732 number: updated.autoincrement_id,
733 author,
734 title: updated.title,
735 description: updated.description,
736 content: updated.body,
737 milestone_id: updated.milestone_id,
738 state,
739 created_at: updated.created_at,
740 updated_at: updated.updated_at,
741 closed_at: updated.closed_at,
742 })
953 self.build_issue_dto(updated).await
743954 }
744955
745956 pub async fn delete_issue(
@@ -790,27 +1001,7 @@
7901001
7911002 let is_currently_closed = item.closed_at.is_some();
7921003 if is_currently_closed == closed {
793 let user_map = self.load_user_briefs(&[item.author_id]).await?;
794 let author = user_map.get(&item.author_id).cloned();
795 let state = if is_currently_closed {
796 IssueState::Closed
797 } else {
798 IssueState::Open
799 };
800 return Ok(IssueDto {
801 id: item.id,
802 board_id: item.collection_id,
803 number: item.autoincrement_id,
804 author,
805 title: item.title,
806 description: item.description,
807 content: item.body,
808 milestone_id: item.milestone_id,
809 state,
810 created_at: item.created_at,
811 updated_at: item.updated_at,
812 closed_at: item.closed_at,
813 });
1004 return self.build_issue_dto(item).await;
8141005 }
8151006
8161007 let now = OffsetDateTime::now_utc();
@@ -818,7 +1009,7 @@
8181009 am.closed_at = Set(closed.then_some(now));
8191010 am.updated_at = Set(Some(now));
8201011
821 let event_type = if closed { "closed" } else { "reopened" };
1012 let event_type = if closed { IssueEventType::Closed } else { IssueEventType::Reopened };
8221013 let this = self.clone();
8231014
8241015 let updated = self
@@ -834,27 +1025,6 @@
8341025 })
8351026 .await?;
8361027
837 let user_map = self.load_user_briefs(&[updated.author_id]).await?;
838 let author = user_map.get(&updated.author_id).cloned();
839 let state = if updated.closed_at.is_some() {
840 IssueState::Closed
841 } else {
842 IssueState::Open
843 };
844
845 Ok(IssueDto {
846 id: updated.id,
847 board_id: updated.collection_id,
848 number: updated.autoincrement_id,
849 author,
850 title: updated.title,
851 description: updated.description,
852 content: updated.body,
853 milestone_id: updated.milestone_id,
854 state,
855 created_at: updated.created_at,
856 updated_at: updated.updated_at,
857 closed_at: updated.closed_at,
858 })
1028 self.build_issue_dto(updated).await
8591029 }
8601030}
Modified lib/issues/src/services/labels.rs View lib/issues/src/services/labels.rs
@@ -1,3 +1,4 @@
1use models::issue::issue_event_line::IssueEventType;
12use models::issue::issue_label;
23use models::issue::issue_relation_label;
34use sea_orm::{
@@ -413,7 +414,7 @@
413414 txn,
414415 issue_id,
415416 caller,
416 "label_added",
417 IssueEventType::LabelAdded,
417418 None,
418419 None,
419420 None,
@@ -472,7 +473,7 @@
472473 txn,
473474 issue_id,
474475 caller,
475 "label_removed",
476 IssueEventType::LabelRemoved,
476477 None,
477478 None,
478479 None,
Modified lib/issues/src/services/members.rs View lib/issues/src/services/members.rs
@@ -1,3 +1,4 @@
1use models::issue::issue_event_line::IssueEventType;
12use models::issue::issue_member::{self, IssueMemberKind};
23use models::space::space_members::{self, MemberStatus};
34use models::users::user_notifications::NotificationType;
@@ -123,7 +124,7 @@
123124 txn,
124125 issue_id,
125126 caller,
126 "member_added",
127 IssueEventType::MemberAdded,
127128 None,
128129 None,
129130 None,
@@ -286,7 +287,7 @@
286287 txn,
287288 issue_id,
288289 caller,
289 "member_removed",
290 IssueEventType::MemberRemoved,
290291 None,
291292 None,
292293 None,
Modified lib/issues/src/services/service.rs View lib/issues/src/services/service.rs
@@ -6,7 +6,7 @@
66use email::EmailService;
77use export::AppState;
88use export::database::AppDatabase;
9use models::issue::issue_event_line;
9use models::issue::issue_event_line::{self, IssueEventType};
1010use models::space::space_members::{self, MemberStatus};
1111use models::space::spaces;
1212use models::users::user;
@@ -71,7 +71,7 @@
7171 txn: &impl ConnectionTrait,
7272 issue_id: Uuid,
7373 user_id: Uuid,
74 event_type: &str,
74 event_type: IssueEventType,
7575 comment_id: Option<Uuid>,
7676 old_title: Option<String>,
7777 new_title: Option<String>,
@@ -82,7 +82,7 @@
8282 issue_id: Set(Some(issue_id)),
8383 project_id: Set(None),
8484 user_id: Set(user_id),
85 event_type: Set(event_type.to_string()),
85 event_type: Set(event_type),
8686 comment_id: Set(comment_id),
8787 commit_sha: Set(None),
8888 old_title: Set(old_title),
Modified lib/models/src/chat/message_reminders.rs View lib/models/src/chat/message_reminders.rs
@@ -40,6 +40,7 @@
4040 pub room_id: Uuid,
4141 pub message_id: Uuid,
4242 pub remind_at: OffsetDateTime,
43 pub note: Option<String>,
4344 #[sea_orm(default_value = 0)]
4445 pub status: ReminderStatus,
4546 pub created_at: OffsetDateTime,
Modified lib/models/src/common/watch_mode.rs View lib/models/src/common/watch_mode.rs
@@ -19,11 +19,9 @@
1919pub enum WatchMode {
2020 #[default]
2121 #[sea_orm(num_value = 0)]
22 Normal,
23 #[sea_orm(num_value = 1)]
24 NoWatch,
22 Unwatched,
2523 #[sea_orm(num_value = 2)]
26 Watch,
24 Watching,
2725 #[sea_orm(num_value = 3)]
2826 Ignore,
2927}
Modified lib/models/src/issue/issue_event_line.rs View lib/models/src/issue/issue_event_line.rs
@@ -1,7 +1,69 @@
11use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
23use time::OffsetDateTime;
34use uuid::Uuid;
45
6#[derive(
7 Debug,
8 Clone,
9 Copy,
10 PartialEq,
11 Eq,
12 EnumIter,
13 DeriveActiveEnum,
14 Serialize,
15 Deserialize,
16 utoipa::ToSchema,
17)]
18#[serde(rename_all = "snake_case")]
19#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(32))")]
20pub enum IssueEventType {
21 #[sea_orm(string_value = "created")]
22 Created,
23 #[sea_orm(string_value = "title_updated")]
24 TitleUpdated,
25 #[sea_orm(string_value = "description_updated")]
26 DescriptionUpdated,
27 #[sea_orm(string_value = "content_updated")]
28 ContentUpdated,
29 #[sea_orm(string_value = "milestone_updated")]
30 MilestoneUpdated,
31 #[sea_orm(string_value = "priority_updated")]
32 PriorityUpdated,
33 #[sea_orm(string_value = "due_updated")]
34 DueUpdated,
35 #[sea_orm(string_value = "closed")]
36 Closed,
37 #[sea_orm(string_value = "reopened")]
38 Reopened,
39 #[sea_orm(string_value = "comment_created")]
40 CommentCreated,
41 #[sea_orm(string_value = "comment_updated")]
42 CommentUpdated,
43 #[sea_orm(string_value = "comment_deleted")]
44 CommentDeleted,
45 #[sea_orm(string_value = "member_added")]
46 MemberAdded,
47 #[sea_orm(string_value = "member_removed")]
48 MemberRemoved,
49 #[sea_orm(string_value = "label_added")]
50 LabelAdded,
51 #[sea_orm(string_value = "label_removed")]
52 LabelRemoved,
53 #[sea_orm(string_value = "dependency_added")]
54 DependencyAdded,
55 #[sea_orm(string_value = "dependency_removed")]
56 DependencyRemoved,
57 #[sea_orm(string_value = "pinned")]
58 Pinned,
59 #[sea_orm(string_value = "unpinned")]
60 Unpinned,
61 #[sea_orm(string_value = "repo_relation_added")]
62 RepoRelationAdded,
63 #[sea_orm(string_value = "repo_relation_removed")]
64 RepoRelationRemoved,
65}
66
567#[sea_orm::model]
668#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
769#[sea_orm(table_name = "issue_event_line")]
@@ -12,7 +74,7 @@
1274 // project 模块暂时移除后保留列,恢复时加回 belongs_to(同 comment/issue_pin/issue_reaction/issue_watch)
1375 pub project_id: Option<Uuid>,
1476 pub user_id: Uuid,
15 pub event_type: String,
77 pub event_type: IssueEventType,
1678 pub comment_id: Option<Uuid>,
1779 pub commit_sha: Option<String>,
1880 pub old_title: Option<String>,
Modified lib/models/src/issue/issue_item.rs View lib/models/src/issue/issue_item.rs
@@ -1,7 +1,37 @@
11use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
23use time::OffsetDateTime;
34use uuid::Uuid;
45
6#[derive(
7 Debug,
8 Clone,
9 Copy,
10 PartialEq,
11 Eq,
12 Default,
13 EnumIter,
14 DeriveActiveEnum,
15 Serialize,
16 Deserialize,
17 utoipa::ToSchema,
18)]
19#[serde(rename_all = "snake_case")]
20#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
21pub enum IssuePriority {
22 #[default]
23 #[sea_orm(num_value = 0)]
24 None,
25 #[sea_orm(num_value = 1)]
26 Low,
27 #[sea_orm(num_value = 2)]
28 Medium,
29 #[sea_orm(num_value = 3)]
30 High,
31 #[sea_orm(num_value = 4)]
32 Urgent,
33}
34
535#[sea_orm::model]
636#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
737#[sea_orm(table_name = "issue")]
@@ -17,6 +47,9 @@
1747 pub description: Option<String>,
1848 pub body: String,
1949 pub milestone_id: Option<Uuid>,
50 #[sea_orm(default_value = 0)]
51 pub priority: IssuePriority,
52 pub due_at: Option<OffsetDateTime>,
2053 pub created_at: OffsetDateTime,
2154 pub updated_at: Option<OffsetDateTime>,
2255 pub closed_at: Option<OffsetDateTime>,
Modified lib/models/src/repo/mod.rs View lib/models/src/repo/mod.rs
@@ -8,4 +8,5 @@
88pub mod repo_tag_protection_whitelists;
99pub mod repo_topic;
1010pub mod repo_watch;
11pub mod pipeline_run;
1112pub mod repos;
Added lib/models/src/repo/pipeline_run.rs View lib/models/src/repo/pipeline_run.rs
@@ -0,0 +1,58 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug,
8 Clone,
9 Copy,
10 PartialEq,
11 Eq,
12 Default,
13 EnumIter,
14 DeriveActiveEnum,
15 Serialize,
16 Deserialize,
17 utoipa::ToSchema,
18)]
19#[serde(rename_all = "snake_case")]
20#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
21pub enum PipelineRunState {
22 #[default]
23 #[sea_orm(num_value = 0)]
24 Queued,
25 #[sea_orm(num_value = 1)]
26 Running,
27 #[sea_orm(num_value = 2)]
28 Success,
29 #[sea_orm(num_value = 3)]
30 Failure,
31 #[sea_orm(num_value = 4)]
32 CancelRequested,
33 #[sea_orm(num_value = 5)]
34 Cancelled,
35}
36
37#[sea_orm::model]
38#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
39#[sea_orm(table_name = "pipeline_run")]
40pub struct Model {
41 #[sea_orm(primary_key, auto_increment = false)]
42 pub id: Uuid,
43 #[sea_orm(indexed)]
44 pub repo_id: Uuid,
45 pub trigger_user_id: Uuid,
46 pub commit_sha: String,
47 pub branch: String,
48 #[sea_orm(default_value = 0)]
49 pub state: PipelineRunState,
50 pub started_at: Option<OffsetDateTime>,
51 pub finished_at: Option<OffsetDateTime>,
52 pub created_at: OffsetDateTime,
53 pub updated_at: OffsetDateTime,
54 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
55 pub repo: BelongsTo<super::repos::Entity>,
56}
57
58impl ActiveModelBehavior for ActiveModel {}
Modified lib/models/src/room/mod.rs View lib/models/src/room/mod.rs
@@ -1,4 +1,5 @@
11pub mod room_invites;
2pub mod room_bans;
23pub mod room_members;
34pub mod rooms;
45
Added lib/models/src/room/room_bans.rs View lib/models/src/room/room_bans.rs
@@ -0,0 +1,24 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "room_bans")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub room_id: Uuid,
11 #[sea_orm(primary_key, auto_increment = false)]
12 pub user_id: Uuid,
13 pub reason: Option<String>,
14 pub banned_by: Uuid,
15 pub expires_at: Option<OffsetDateTime>,
16 pub created_at: OffsetDateTime,
17 pub updated_at: Option<OffsetDateTime>,
18 #[sea_orm(belongs_to, from = "room_id", to = "id")]
19 pub room: BelongsTo<super::rooms::Entity>,
20 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
21 pub user: BelongsTo<super::super::users::user::Entity>,
22}
23
24impl ActiveModelBehavior for ActiveModel {}
Modified lib/models/src/room/room_members.rs View lib/models/src/room/room_members.rs
@@ -1,7 +1,37 @@
11use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
23use time::OffsetDateTime;
34use uuid::Uuid;
45
6#[derive(
7 Debug,
8 Clone,
9 Copy,
10 PartialEq,
11 Eq,
12 PartialOrd,
13 Ord,
14 Default,
15 EnumIter,
16 DeriveActiveEnum,
17 Serialize,
18 Deserialize,
19 utoipa::ToSchema,
20)]
21#[serde(rename_all = "snake_case")]
22#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
23pub enum RoomMemberRole {
24 #[default]
25 #[sea_orm(num_value = 0)]
26 Member,
27 #[sea_orm(num_value = 1)]
28 Moderator,
29 #[sea_orm(num_value = 2)]
30 Admin,
31 #[sea_orm(num_value = 3)]
32 Owner,
33}
34
535#[sea_orm::model]
636#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
737#[sea_orm(table_name = "room_members")]
@@ -10,6 +40,8 @@
1040 pub room_id: Uuid,
1141 #[sea_orm(primary_key, auto_increment = false)]
1242 pub user_id: Uuid,
43 #[sea_orm(default_value = 0)]
44 pub role: RoomMemberRole,
1345 pub nick: Option<String>,
1446 pub joined_at: OffsetDateTime,
1547 pub left_at: Option<OffsetDateTime>,
Modified lib/models/src/users/user_access_keys.rs View lib/models/src/users/user_access_keys.rs
@@ -22,18 +22,14 @@
2222 Read,
2323 #[sea_orm(string_value = "write")]
2424 Write,
25 #[sea_orm(string_value = "repo")]
26 Repo,
27 #[sea_orm(string_value = "all")]
28 All,
2925}
3026
3127impl AccessKeyScope {
3228 pub const fn allows(self, want_write: bool) -> bool {
33 matches!(
34 (self, want_write),
35 (Self::All | Self::Repo | Self::Write, _) | (Self::Read, false)
36 )
29 match self {
30 Self::Write => true,
31 Self::Read => !want_write,
32 }
3733 }
3834}
3935
@@ -57,6 +53,8 @@
5753 pub version: i32,
5854 pub created_at: OffsetDateTime,
5955 pub updated_at: OffsetDateTime,
56 pub expires_at: Option<OffsetDateTime>,
57 pub last_used_at: Option<OffsetDateTime>,
6058}
6159
6260impl ActiveModelBehavior for ActiveModel {}
Modified lib/models/schema_extras.sql View lib/models/schema_extras.sql
@@ -183,4 +183,14 @@
183183 BEFORE INSERT OR UPDATE OF content ON chat_message
184184 FOR EACH ROW EXECUTE FUNCTION chat_message_search_vector_update();
185185UPDATE chat_message SET search_vector = to_tsvector('simple', coalesce(content, ''))
186 WHERE search_vector IS NULL;
No newline at end of file
186 WHERE search_vector IS NULL;
187-- phase-2 data backfills (idempotent)
188UPDATE repo_watch SET mode = 0 WHERE mode = 1;
189UPDATE issue_watch SET mode = 0 WHERE mode = 1;
190UPDATE room_members rm SET role = 3
191 FROM rooms r WHERE rm.room_id = r.id AND rm.user_id = r.owner_id AND rm.left_at IS NULL;
192CREATE INDEX IF NOT EXISTS idx_pipeline_run_repo
193 ON pipeline_run (repo_id, created_at);
194
195UPDATE user_access_key SET scope = 'write' WHERE scope IN ('repo', 'all');
196UPDATE user_access_key SET scope = 'read' WHERE scope NOT IN ('read', 'write');
Modified lib/models/schema_pre_sync.sql View lib/models/schema_pre_sync.sql
@@ -15,3 +15,12 @@
1515DROP INDEX IF EXISTS idx_chat_message_room_nonce;
1616DROP INDEX IF EXISTS idx_chat_dm_pair;
1717DROP INDEX IF EXISTS idx_chat_draft_unique;
18
19-- idempotent column adds for phase-2 model changes (sync layer cannot
20-- express NOT NULL DEFAULT additions on populated tables)
21ALTER TABLE room_members ADD COLUMN IF NOT EXISTS role SMALLINT NOT NULL DEFAULT 0;
22ALTER TABLE issue ADD COLUMN IF NOT EXISTS priority SMALLINT NOT NULL DEFAULT 0;
23ALTER TABLE issue ADD COLUMN IF NOT EXISTS due_at TIMESTAMP WITH TIME ZONE;
24ALTER TABLE user_access_key ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP WITH TIME ZONE;
25ALTER TABLE user_access_key ADD COLUMN IF NOT EXISTS last_used_at TIMESTAMP WITH TIME ZONE;
26ALTER TABLE chat_message_reminder ADD COLUMN IF NOT EXISTS note TEXT;
Modified lib/space/src/services/members.rs View lib/space/src/services/members.rs
@@ -204,6 +204,7 @@
204204 for room in &public_rooms {
205205 let now = OffsetDateTime::now_utc();
206206 let rm = models::room::room_members::ActiveModel {
207 role: Set(models::room::room_members::RoomMemberRole::Member),
207208 room_id: Set(room.id),
208209 user_id: Set(saved.user_id),
209210 nick: Set(None),
Modified lib/space/src/services/rooms.rs View lib/space/src/services/rooms.rs
@@ -196,6 +196,7 @@
196196 let r = room_row.insert(txn).await?;
197197 for &uid in &member_uids_clone {
198198 let rm = room_members::ActiveModel {
199 role: Set(room_members::RoomMemberRole::Member),
199200 room_id: Set(room_id),
200201 user_id: Set(uid),
201202 nick: Set(None),