Skip to content
Clone

feat(api): update response types to use pagination for various endpoints

ZhenYi committed
parent 05f6fe1 commit 653bd9f Browse files

44 files changed

+375 −142

This change is very large, so some diffs are not shown. Clone the repository to see the full change.

File tree 44
Modified app/src/main.rs View app/src/main.rs
@@ -99,7 +99,7 @@
9999 ],
100100 );
101101
102 spawn_dependency_health_sampler(state.clone());
102 spawn_dependency_health_sampler(state.clone(), conn.clone());
103103
104104 let mut workers = tokio::task::JoinSet::new();
105105 workers.spawn(chat::worker::run_background_tasks(
@@ -448,19 +448,23 @@
448448const HEALTH_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
449449
450450/// 周期性采样 RocketMQ/S3 健康:`/readyz` 只读缓存,避免每次探活都新建连接。
451fn spawn_dependency_health_sampler(state: bootstrap::AppState) {
451fn spawn_dependency_health_sampler(state: bootstrap::AppState, conn: queue::RocketMqConn) {
452452 tokio::spawn(async move {
453453 let mut ticker = tokio::time::interval(HEALTH_SAMPLE_INTERVAL);
454454 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
455 let auth_topic = state
456 .config
457 .rocketmq
458 .topic(events::EventKind::AuthLoginCreate.topic());
455459 loop {
456460 ticker.tick().await;
457461
458 let rocketmq_up = queue::probe_nameservers_with(
459 &state.config.rocketmq,
460 std::time::Duration::from_secs(1),
462 let rocketmq_up = tokio::time::timeout(
463 std::time::Duration::from_secs(6),
464 conn.health(&[&auth_topic, "TBW102"]),
461465 )
462466 .await
463 .is_ok();
467 .is_ok_and(|result| result.is_ok());
464468 state.health.mark_rocketmq(rocketmq_up);
465469 oltp::metrics::health_dependency("rocketmq", rocketmq_up);
466470
Modified lib/infra/actor/src/actor/host/builder.rs View lib/infra/actor/src/actor/host/builder.rs
@@ -97,6 +97,8 @@
9797 needed: true,
9898 generation: 0,
9999 last_pass: None,
100 next_attempt: None,
101 attempts: HashMap::new(),
100102 }),
101103 });
102104 let host = ActorHost { inner };
Modified lib/infra/actor/src/actor/host/core.rs View lib/infra/actor/src/actor/host/core.rs
@@ -37,6 +37,8 @@
3737 pub(super) needed: bool,
3838 pub(super) generation: u64,
3939 pub(super) last_pass: Option<Instant>,
40 pub(super) next_attempt: Option<Instant>,
41 pub(super) attempts: HashMap<String, (u32, Instant)>,
4042}
4143
4244pub struct ActorEvent {
@@ -60,6 +62,11 @@
6062 Heartbeat,
6163}
6264
65pub(super) fn backoff(failures: u32) -> Duration {
66 let shift = failures.saturating_sub(1).min(5);
67 (Duration::from_millis(250) * 2u32.pow(shift)).min(Duration::from_secs(8))
68}
69
6370pub(super) enum ReplyTarget {
6471 Local(oneshot::Sender<Result<Value, ActorError>>),
6572 Remote { stream: String, id: String },
@@ -120,9 +127,20 @@
120127
121128 pub(super) async fn release(&self, entry: &str) {
122129 self.inner.transport.inflight.lock().await.remove(entry);
130 let now = Instant::now();
123131 let mut recovery = self.inner.recovery.lock().await;
132 let attempts = {
133 let attempts = recovery
134 .attempts
135 .entry(entry.to_string())
136 .or_insert((0, now));
137 attempts.0 = attempts.0.saturating_add(1);
138 attempts.1 = now;
139 attempts.0
140 };
124141 recovery.needed = true;
125142 recovery.generation = recovery.generation.wrapping_add(1);
143 recovery.next_attempt = Some(now + backoff(attempts));
126144 let start = rewind_point(entry);
127145 if id_before(&start, &recovery.cursor) {
128146 recovery.cursor = start;
Modified lib/infra/actor/src/actor/host/reader.rs View lib/infra/actor/src/actor/host/reader.rs
@@ -2,11 +2,11 @@
22
33use super::super::error::ActorError;
44use super::super::transport::WireMessage;
5use super::core::{ActorEvent, ActorHost, LocalRequest, ReplyTarget};
5use super::core::{ActorEvent, ActorHost, LocalRequest, ReplyTarget, backoff};
66
7const ERROR_BACKOFF: Duration = Duration::from_millis(250);
8const MAX_ERROR_BACKOFF: Duration = Duration::from_secs(8);
97const RECOVERY_INTERVAL: Duration = Duration::from_secs(1);
8const MAX_ENTRY_ATTEMPTS: u32 = 8;
9const ATTEMPT_TTL: Duration = Duration::from_secs(3600);
1010
1111impl ActorHost {
1212 pub(super) async fn run_reader(self) {
@@ -64,11 +64,15 @@
6464 && recovery
6565 .last_pass
6666 .is_none_or(|at| at.elapsed() >= RECOVERY_INTERVAL)
67 && recovery.next_attempt.is_none_or(|at| at <= Instant::now())
6768 }
6869
6970 async fn recover_pending(&self) -> Result<(), ActorError> {
7071 let (mut after, generation) = {
71 let recovery = self.inner.recovery.lock().await;
72 let mut recovery = self.inner.recovery.lock().await;
73 recovery
74 .attempts
75 .retain(|_, (_, at)| at.elapsed() < ATTEMPT_TTL);
7276 (recovery.cursor.clone(), recovery.generation)
7377 };
7478
@@ -86,6 +90,41 @@
8690 after = last;
8791
8892 for (entry, wire) in entries {
93 let attempts = self
94 .inner
95 .recovery
96 .lock()
97 .await
98 .attempts
99 .get(&entry)
100 .map(|(count, _)| *count)
101 .unwrap_or(0);
102 if attempts >= MAX_ENTRY_ATTEMPTS {
103 if let Err(error) = self
104 .inner
105 .transport
106 .dead_letter(&entry, wire.as_ref())
107 .await
108 {
109 tracing::error!(%entry, %error, "actor entry could not be dead-lettered");
110 }
111 if let Err(error) = self.inner.transport.ack(&entry).await {
112 tracing::error!(
113 %entry,
114 %error,
115 "actor dead-lettered entry could not be acknowledged"
116 );
117 self.release(&entry).await;
118 continue;
119 }
120 self.inner.recovery.lock().await.attempts.remove(&entry);
121 tracing::error!(
122 %entry,
123 attempts,
124 "actor entry exceeded the retry limit; dead-lettered"
125 );
126 continue;
127 }
89128 self.handle_entry(entry, wire).await;
90129 }
91130
@@ -169,9 +208,4 @@
169208 }
170209 }
171210 }
172}
173
174fn backoff(failures: u32) -> Duration {
175 let shift = failures.saturating_sub(1).min(5);
176 (ERROR_BACKOFF * 2u32.pow(shift)).min(MAX_ERROR_BACKOFF)
177211}
Modified lib/infra/actor/src/actor/transport/stream.rs View lib/infra/actor/src/actor/transport/stream.rs
@@ -63,6 +63,29 @@
6363 result.map(|_| ()).map_err(Into::into)
6464 }
6565
66 pub(crate) async fn dead_letter(
67 &self,
68 entry: &str,
69 wire: Option<&WireMessage>,
70 ) -> Result<(), ActorError> {
71 let stream = format!("{}:dead:{}", self.prefix, self.consumer);
72 let payload = match wire {
73 Some(wire) => serde_json::to_string(wire).map_err(ActorError::Encode)?,
74 None => String::new(),
75 };
76 let mut cmd = redis::cmd("EVAL");
77 cmd.arg(
78 "redis.call('XADD',KEYS[1],'MAXLEN','~',ARGV[1],'*','entry',ARGV[2],'payload',ARGV[3]); return 1",
79 )
80 .arg(1)
81 .arg(&stream)
82 .arg(self.max_len)
83 .arg(entry)
84 .arg(payload);
85 let _: i64 = self.commands.query(&cmd).await?;
86 Ok(())
87 }
88
6689 pub(crate) async fn receive(&self) -> Result<Vec<(String, Option<WireMessage>)>, ActorError> {
6790 self.pending
6891 .lock()
Modified lib/infra/db/src/migrate.rs View lib/infra/db/src/migrate.rs
@@ -1,6 +1,5 @@
1use models::migrate::MIGRATION_LOCK_KEY;
12use sea_orm::{ConnectionTrait, DatabaseConnection, DbErr, TransactionTrait};
2
3const LOCK_KEY: i64 = 5_241_915_330_551;
43
54/// 实体优先迁移:实体定义是表结构的唯一真相。
65///
@@ -42,8 +41,10 @@
4241
4342 pub async fn run(&self, db: &DatabaseConnection) -> Result<(), DbErr> {
4443 let txn = db.begin().await?;
45 txn.execute_unprepared(&format!("SELECT pg_advisory_xact_lock({LOCK_KEY})"))
46 .await?;
44 txn.execute_unprepared(&format!(
45 "SELECT pg_advisory_xact_lock({MIGRATION_LOCK_KEY})"
46 ))
47 .await?;
4748 for statement in &self.before {
4849 txn.execute_unprepared(statement).await?;
4950 }
Modified lib/infra/oltp/src/telemetry.rs View lib/infra/oltp/src/telemetry.rs
@@ -33,29 +33,31 @@
3333 }
3434
3535 pub fn force_flush(&self) -> Result<(), TelemetryError> {
36 self.tracer_provider
37 .force_flush()
38 .map_err(TelemetryError::exporter)?;
39 self.meter_provider
40 .force_flush()
41 .map_err(TelemetryError::exporter)?;
42 self.logger_provider
43 .force_flush()
44 .map_err(TelemetryError::exporter)?;
45 Ok(())
36 first_error([
37 self.tracer_provider
38 .force_flush()
39 .map_err(TelemetryError::exporter),
40 self.meter_provider
41 .force_flush()
42 .map_err(TelemetryError::exporter),
43 self.logger_provider
44 .force_flush()
45 .map_err(TelemetryError::exporter),
46 ])
4647 }
4748
4849 pub fn shutdown(&self) -> Result<(), TelemetryError> {
49 self.tracer_provider
50 .shutdown()
51 .map_err(TelemetryError::shutdown)?;
52 self.meter_provider
53 .shutdown()
54 .map_err(TelemetryError::shutdown)?;
55 self.logger_provider
56 .shutdown()
57 .map_err(TelemetryError::shutdown)?;
58 Ok(())
50 first_error([
51 self.tracer_provider
52 .shutdown()
53 .map_err(TelemetryError::shutdown),
54 self.meter_provider
55 .shutdown()
56 .map_err(TelemetryError::shutdown),
57 self.logger_provider
58 .shutdown()
59 .map_err(TelemetryError::shutdown),
60 ])
5961 }
6062
6163 pub fn guard(&self) -> TelemetryGuard<'_> {
@@ -75,6 +77,13 @@
7577 }
7678}
7779
80fn first_error(errors: [Result<(), TelemetryError>; 3]) -> Result<(), TelemetryError> {
81 match errors.into_iter().find(Result::is_err) {
82 Some(error) => error,
83 None => Ok(()),
84 }
85}
86
7887pub fn install(config: &TelemetryConfig) -> Telemetry {
7988 let resource = resource::build(config);
8089 let mut warnings = Vec::new();
Modified lib/infra/queue/src/conn.rs View lib/infra/queue/src/conn.rs
@@ -59,6 +59,23 @@
5959 self.config.max_message_bytes
6060 }
6161
62 pub async fn health(&self, topics: &[&str]) -> QueueResult<()> {
63 let mut producer = self.producer.clone();
64 let mut last_error = None;
65 for topic in topics {
66 match producer.fetch_publish_message_queues(topic).await {
67 Ok(_) => return Ok(()),
68 Err(error) => last_error = Some((*topic, error)),
69 }
70 }
71 let detail = last_error
72 .map(|(topic, error)| format!("`{topic}`: {error}"))
73 .unwrap_or_default();
74 Err(QueueError::connect(format!(
75 "rocketmq route lookup failed ({detail})"
76 )))
77 }
78
6279 pub async fn send(&self, topic: &str, message: Message) -> QueueResult<()> {
6380 let result = self
6481 .producer
@@ -89,7 +106,7 @@
89106 probe_nameservers_with(config, std::time::Duration::from_secs(3)).await
90107}
91108
92pub async fn probe_nameservers_with(
109async fn probe_nameservers_with(
93110 config: &RocketMqConfig,
94111 timeout: std::time::Duration,
95112) -> QueueResult<()> {
Modified lib/infra/queue/src/lib.rs View lib/infra/queue/src/lib.rs
@@ -8,9 +8,7 @@
88pub mod runtime;
99pub mod subject;
1010
11pub use conn::{
12 DEFAULT_MAX_MESSAGE_BYTES, MAX_ENVELOPE_BYTES, RocketMqConn, probe_nameservers_with,
13};
11pub use conn::{DEFAULT_MAX_MESSAGE_BYTES, MAX_ENVELOPE_BYTES, RocketMqConn};
1412pub use consumer::{Consumer, ConsumerConfig, MessageHandler};
1513pub use envelope::{DeliveryClass, EVENT_PREFIX, Envelope, Labels, SCHEMA_VERSION};
1614pub use error::{QueueError, QueueResult};
Modified lib/service/auth/src/migrate.rs View lib/service/auth/src/migrate.rs
@@ -11,10 +11,12 @@
1111 "DROP TABLE IF EXISTS oauth_clients",
1212 "DROP TABLE IF EXISTS user_access_tokens",
1313 "DROP TABLE IF EXISTS service_token_revocations",
14 "DROP INDEX IF EXISTS idx_space_join_requests_pending",
1415 ])
1516 .after([
1617 // 邮箱查询一律走 lower(email),表达式唯一索引把大小写变体挡在数据库层。
1718 "CREATE UNIQUE INDEX IF NOT EXISTS auth_users_lower_email_idx ON auth_users (lower(email))",
19 "CREATE UNIQUE INDEX IF NOT EXISTS idx_space_join_requests_pending ON space_join_requests (organization_id, user_id) WHERE status = 'pending'",
1820 ])
1921 .run(db.write())
2022 .await?;
Modified lib/service/chat/src/handlers/emojis.rs View lib/service/chat/src/handlers/emojis.rs
@@ -1,5 +1,6 @@
11use ::session::AuthedUser;
2use api::http::{Json, Path, Response, State};
2use api::http::{Json, Path, Query, Response, State};
3use api::pagination::{Page, PageQuery};
34use api::rest::{AppResponseData, NoData};
45use uuid::Uuid;
56
@@ -13,9 +14,9 @@
1314 path = "/chat/spaces/{space_name}/emojis",
1415 tag = "chat",
1516 description = "返回指定空间可用的自定义表情列表。",
16 params(("space_name" = String, Path, description = "space name")),
17 params(("space_name" = String, Path, description = "space name"), PageQuery),
1718 responses(
18 (status = 200, description = "emoji list", body = AppResponseData<Vec<EmojiDto>>),
19 (status = 200, description = "emoji list", body = AppResponseData<Page<EmojiDto>>),
1920 (status = 404, description = "not found", body = AppResponseData<NoData>),
2021 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
2122 )
@@ -24,8 +25,9 @@
2425 State(service): State<ChatService>,
2526 AuthedUser(uid): AuthedUser,
2627 Path(space_name): Path<String>,
27) -> Result<AppResponseData<Vec<EmojiDto>>, ApiError> {
28 let emojis = service.list_emojis(uid, &space_name).await?;
28 Query(page): Query<PageQuery>,
29) -> Result<AppResponseData<Page<EmojiDto>>, ApiError> {
30 let emojis = service.list_emojis(uid, &space_name, page).await?;
2931 Ok(AppResponseData::ok(emojis))
3032}
3133
Modified lib/service/chat/src/handlers/messages.rs View lib/service/chat/src/handlers/messages.rs
@@ -39,10 +39,10 @@
3939 get,
4040 path = "/chat/rooms/{room_id}/messages",
4141 tag = "chat",
42 description = "按房间序号游标读取消息页,并附带作者、附件和回应状态。",
42 description = "按房间序号过滤并分页读取消息;has_more 可用 offset + items.len() < total 判断。",
4343 params(("room_id" = Uuid, Path, description = "room id"), ListMessageQuery),
4444 responses(
45 (status = 200, description = "message page", body = AppResponseData<MessagePage>),
45 (status = 200, description = "message page", body = AppResponseData<api::pagination::Page<MessageDto>>),
4646 (status = 404, description = "not found", body = AppResponseData<NoData>),
4747 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
4848 )
Modified lib/service/chat/src/handlers/threads.rs View lib/service/chat/src/handlers/threads.rs
@@ -44,7 +44,7 @@
4444 description = "分页列出房间话题,可按归档状态过滤。",
4545 params(("room_id" = Uuid, Path, description = "room id"), ListThreadQuery),
4646 responses(
47 (status = 200, description = "thread page", body = AppResponseData<ThreadPage>),
47 (status = 200, description = "thread page", body = AppResponseData<api::pagination::Page<ThreadDto>>),
4848 (status = 404, description = "not found", body = AppResponseData<NoData>),
4949 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
5050 )
Modified lib/service/chat/src/services/emojis.rs View lib/service/chat/src/services/emojis.rs
@@ -1,5 +1,9 @@
1use api::pagination::{Page, PageQuery};
12use models::chat::emojis;
2use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
3use sea_orm::{
4 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
5 QuerySelect, Set,
6};
37use serde::{Deserialize, Serialize};
48use space::Space;
59use time::OffsetDateTime;
@@ -62,15 +66,24 @@
6266 &self,
6367 caller: i64,
6468 space_name: &str,
65 ) -> Result<Vec<EmojiDto>, ChatError> {
69 page: PageQuery,
70 ) -> Result<Page<EmojiDto>, ChatError> {
6671 let space = lookup_space_by_name(self, space_name).await?;
6772 require_space_member(self, &space, caller).await?;
68 let rows = emojis::Entity::find()
69 .filter(emojis::Column::SpaceId.eq(space.id()))
73 let (limit, offset) = page.resolve(
74 self.settings.chat.default_pagination,
75 self.settings.chat.max_pagination,
76 )?;
77 let db = self.database.read();
78 let select = emojis::Entity::find().filter(emojis::Column::SpaceId.eq(space.id()));
79 let total = select.clone().count(db).await?;
80 let rows = select
7081 .order_by_asc(emojis::Column::LowerName)
71 .all(self.database.read())
82 .limit(limit)
83 .offset(offset)
84 .all(db)
7285 .await?;
73 Ok(rows
86 let items = rows
7487 .into_iter()
7588 .map(|e| EmojiDto {
7689 id: e.id,
@@ -80,7 +93,8 @@
8093 disabled: e.disabled,
8194 created_at: e.created_at,
8295 })
83 .collect())
96 .collect();
97 Ok(Page::new(items, total, limit, offset))
8498 }
8599
86100 pub async fn upload_emoji(
Modified lib/service/chat/src/openapi.rs View lib/service/chat/src/openapi.rs
@@ -99,6 +99,7 @@
9999 api::pagination::Page<crate::services::bookmarks::BookmarkDto>,
100100 api::pagination::Page<crate::services::messages::MessageDto>,
101101 api::pagination::Page<crate::services::rooms::RoomBriefDto>,
102 api::pagination::Page<crate::services::emojis::EmojiDto>,
102103 AppResponseData<api::rest::NoData>,
103104 api::rest::NoData,
104105 api::users::UserDto,
@@ -122,7 +123,6 @@
122123 crate::services::rooms::RoomBriefDto,
123124 crate::services::rooms::UpdateRoomCmd,
124125 crate::services::threads::ThreadDto,
125 crate::services::threads::ThreadPage,
126126 crate::services::threads::CreateThreadCmd,
127127 crate::services::threads::UpdateThreadCmd,
128128 crate::services::threads::ThreadMemberCmd,
Modified lib/service/deletion/src/lib.rs View lib/service/deletion/src/lib.rs
@@ -19,8 +19,8 @@
1919use proto::KenShellClient;
2020use sea_orm::sea_query::Expr;
2121use sea_orm::{
22 ActiveModelTrait, ColumnTrait, ConnectionTrait, DbBackend, EntityTrait, PaginatorTrait,
23 QueryFilter, QuerySelect, Set, Statement,
22 ActiveModelTrait, ColumnTrait, Condition, ConnectionTrait, DbBackend, EntityTrait,
23 PaginatorTrait, QueryFilter, QuerySelect, Set, Statement,
2424};
2525use serde::Serialize;
2626use time::OffsetDateTime;
@@ -30,6 +30,7 @@
3030
3131const RETRY_BASE_SECS: u64 = 30;
3232const RETRY_MAX_SECS: u64 = 3600;
33const MAX_PURGE_ATTEMPTS: i32 = 20;
3334/// 一次只领取一行:purge 会跨 KenShell / KenAuth 多步执行,逐行领取能让关停
3435/// 在行与行之间生效,而不是把已领取的其余行晾到租约过期。
3536const CLAIM_BATCH: i64 = 1;
@@ -234,14 +235,32 @@
234235 accounts: &auth::Accounts,
235236 user_id: i64,
236237) -> Result<(), DeletionError> {
238 let db = state.database.write();
239 let deleted = account_deletion::Entity::delete_many()
240 .filter(account_deletion::Column::UserId.eq(user_id))
241 .filter(
242 Condition::any()
243 .add(account_deletion::Column::LeaseId.is_null())
244 .add(account_deletion::Column::LeasedUntil.lte(now())),
245 )
246 .exec(db)
247 .await?;
248 if deleted.rows_affected == 0 {
249 let exists = account_deletion::Entity::find()
250 .filter(account_deletion::Column::UserId.eq(user_id))
251 .one(db)
252 .await?
253 .is_some();
254 if exists {
255 return Err(DeletionError::Conflict(
256 "account deletion is currently being purged".into(),
257 ));
258 }
259 }
237260 accounts
238261 .cancel_account_deletion(user_id)
239262 .await
240263 .map_err(ken_auth)?;
241 account_deletion::Entity::delete_many()
242 .filter(account_deletion::Column::UserId.eq(user_id))
243 .exec(state.database.write())
244 .await?;
245264 Ok(())
246265}
247266
@@ -683,6 +702,7 @@
683702 error: &DeletionError,
684703) -> Result<(), DeletionError> {
685704 let attempts = row.attempts.max(1);
705 let retryable = error.is_retryable() && attempts < MAX_PURGE_ATTEMPTS;
686706 let updated = space_deletion::Entity::update_many()
687707 .col_expr(space_deletion::Column::Attempts, Expr::value(attempts))
688708 .col_expr(
@@ -693,8 +713,11 @@
693713 space_deletion::Column::LastError,
694714 Expr::value(Some(error.to_string())),
695715 )
696 .col_expr(space_deletion::Column::Status, Expr::value("pending"))
697716 .col_expr(
717 space_deletion::Column::Status,
718 Expr::value(if retryable { "pending" } else { "failed" }),
719 )
720 .col_expr(
698721 space_deletion::Column::LeaseId,
699722 Expr::value(Option::<Uuid>::None),
700723 )
@@ -715,7 +738,11 @@
715738 );
716739 return Ok(());
717740 }
718 tracing::warn!(space_id = %row.space_id, %error, attempts, "space purge failed; will retry");
741 if retryable {
742 tracing::warn!(space_id = %row.space_id, %error, attempts, "space purge failed; will retry");
743 } else {
744 tracing::warn!(space_id = %row.space_id, %error, attempts, "space purge failed permanently");
745 }
719746 Ok(())
720747}
721748
@@ -725,6 +752,7 @@
725752 error: &DeletionError,
726753) -> Result<(), DeletionError> {
727754 let attempts = row.attempts.max(1);
755 let retryable = error.is_retryable() && attempts < MAX_PURGE_ATTEMPTS;
728756 let updated = account_deletion::Entity::update_many()
729757 .col_expr(account_deletion::Column::Attempts, Expr::value(attempts))
730758 .col_expr(
@@ -735,8 +763,11 @@
735763 account_deletion::Column::LastError,
736764 Expr::value(Some(error.to_string())),
737765 )
738 .col_expr(account_deletion::Column::Status, Expr::value("pending"))
739766 .col_expr(
767 account_deletion::Column::Status,
768 Expr::value(if retryable { "pending" } else { "failed" }),
769 )
770 .col_expr(
740771 account_deletion::Column::LeaseId,
741772 Expr::value(Option::<Uuid>::None),
742773 )
@@ -757,7 +788,11 @@
757788 );
758789 return Ok(());
759790 }
760 tracing::warn!(user_id = %row.user_id, %error, attempts, "account purge failed; will retry");
791 if retryable {
792 tracing::warn!(user_id = %row.user_id, %error, attempts, "account purge failed; will retry");
793 } else {
794 tracing::warn!(user_id = %row.user_id, %error, attempts, "account purge failed permanently");
795 }
761796 Ok(())
762797}
763798
Modified lib/service/git/src/handler/code_comments/list_code_comments.rs View lib/service/git/src/handler/code_comments/list_code_comments.rs
@@ -14,7 +14,7 @@
1414 description = "列出代码行级评论;可按 path 与 revision 过滤,锚定提交非当前分支提示时标记 outdated。",
1515 params(crate::handler::RepoPath, ListCodeCommentsQuery),
1616 responses(
17 (status = 200, description = "code comment page", body = AppResponseData<CodeCommentPage>)
17 (status = 200, description = "code comment page", body = AppResponseData<api::pagination::Page<crate::service::code_comments::CodeCommentDto>>)
1818 )
1919)]
2020pub async fn list_code_comments(
Modified lib/service/git/src/handler/releases/list_releases.rs View lib/service/git/src/handler/releases/list_releases.rs
@@ -14,7 +14,7 @@
1414 description = "列出仓库发布记录(新→旧),可按草稿状态过滤。",
1515 params(crate::handler::RepoPath, ListReleasesQuery),
1616 responses(
17 (status = 200, description = "release page", body = AppResponseData<ReleasePage>)
17 (status = 200, description = "release page", body = AppResponseData<api::pagination::Page<crate::service::releases::ReleaseDto>>)
1818 )
1919)]
2020pub async fn list_releases(
Modified lib/service/issues/src/handlers/boards/list_boards.rs View lib/service/issues/src/handlers/boards/list_boards.rs
@@ -14,7 +14,7 @@
1414 description = "分页列出调用者可见的看板,并统计开放和已关闭议题数量。",
1515 params(SpacePath, ListBoardQuery),
1616 responses(
17 (status = 200, description = "list boards", body = AppResponseData<BoardPage>),
17 (status = 200, description = "list boards", body = AppResponseData<api::pagination::Page<crate::services::boards::BoardDto>>),
1818 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1919 (status = 404, description = "not found", body = AppResponseData<NoData>),
2020 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/handlers/comments/list_comments.rs View lib/service/issues/src/handlers/comments/list_comments.rs
@@ -14,7 +14,7 @@
1414 description = "分页列出议题评论,可按父评论过滤并调整排序。",
1515 params(IssuePath, ListCommentQuery),
1616 responses(
17 (status = 200, description = "list comments", body = AppResponseData<CommentPage>),
17 (status = 200, description = "list comments", body = AppResponseData<api::pagination::Page<crate::services::comments::CommentDto>>),
1818 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1919 (status = 404, description = "not found", body = AppResponseData<NoData>),
2020 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/handlers/content_history/list_content_history.rs View lib/service/issues/src/handlers/content_history/list_content_history.rs
@@ -15,7 +15,7 @@
1515 description = "分页读取议题正文或评论的修改前后历史记录。",
1616 params(ContentHistoryPath, ListContentHistoryQuery),
1717 responses(
18 (status = 200, description = "list content history", body = AppResponseData<ContentHistoryPage>),
18 (status = 200, description = "list content history", body = AppResponseData<api::pagination::Page<crate::services::content_history::ContentHistoryDto>>),
1919 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
2020 (status = 404, description = "not found", body = AppResponseData<NoData>),
2121 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/handlers/events/list_events.rs View lib/service/issues/src/handlers/events/list_events.rs
@@ -13,7 +13,7 @@
1313 description = "按时间倒序读取议题的创建、编辑、状态和关联活动事件。",
1414 params(EventPath, ListEventQuery),
1515 responses(
16 (status = 200, description = "list issue events", body = AppResponseData<EventPage>),
16 (status = 200, description = "list issue events", body = AppResponseData<api::pagination::Page<crate::services::events::EventDto>>),
1717 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1818 (status = 404, description = "not found", body = AppResponseData<NoData>),
1919 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/handlers/issue_users/list_issue_users.rs View lib/service/issues/src/handlers/issue_users/list_issue_users.rs
@@ -14,7 +14,7 @@
1414 description = "分页列出议题的已读用户及对应阅读状态。",
1515 params(IssuePath, ListIssueUserQuery),
1616 responses(
17 (status = 200, description = "list issue user states", body = AppResponseData<IssueUserPage>),
17 (status = 200, description = "list issue user states", body = AppResponseData<api::pagination::Page<crate::services::issue_users::IssueUserDto>>),
1818 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1919 (status = 404, description = "not found", body = AppResponseData<NoData>),
2020 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/handlers/issues/list_issues.rs View lib/service/issues/src/handlers/issues/list_issues.rs
@@ -14,7 +14,7 @@
1414 description = "按状态、作者、里程碑、标签、指派人、优先级或关键字分页查询议题。",
1515 params(BoardPath, ListIssueQuery),
1616 responses(
17 (status = 200, description = "list issues", body = AppResponseData<IssuePage>),
17 (status = 200, description = "list issues", body = AppResponseData<api::pagination::Page<crate::services::issues::IssueListItemDto>>),
1818 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1919 (status = 404, description = "not found", body = AppResponseData<NoData>),
2020 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/handlers/labels/list_labels.rs View lib/service/issues/src/handlers/labels/list_labels.rs
@@ -14,7 +14,7 @@
1414 description = "分页列出看板标签,可按名称与归档状态过滤。",
1515 params(BoardPath, ListLabelQuery),
1616 responses(
17 (status = 200, description = "list labels", body = AppResponseData<LabelPage>),
17 (status = 200, description = "list labels", body = AppResponseData<api::pagination::Page<crate::services::labels::LabelDto>>),
1818 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1919 (status = 404, description = "not found", body = AppResponseData<NoData>),
2020 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/handlers/members/list_members.rs View lib/service/issues/src/handlers/members/list_members.rs
@@ -14,7 +14,7 @@
1414 description = "分页列出议题成员,可按指派、提及或参与角色过滤。",
1515 params(IssuePath, ListIssueMemberQuery),
1616 responses(
17 (status = 200, description = "list issue members", body = AppResponseData<IssueMemberPage>),
17 (status = 200, description = "list issue members", body = AppResponseData<api::pagination::Page<crate::services::members::IssueMemberDto>>),
1818 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1919 (status = 404, description = "not found", body = AppResponseData<NoData>),
2020 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/handlers/milestones/list_milestones.rs View lib/service/issues/src/handlers/milestones/list_milestones.rs
@@ -14,7 +14,7 @@
1414 description = "分页列出看板里程碑及其议题完成统计。",
1515 params(BoardPath, ListMilestoneQuery),
1616 responses(
17 (status = 200, description = "list milestones", body = AppResponseData<MilestonePage>),
17 (status = 200, description = "list milestones", body = AppResponseData<api::pagination::Page<crate::services::milestones::MilestoneDto>>),
1818 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1919 (status = 404, description = "not found", body = AppResponseData<NoData>),
2020 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/handlers/tracked_time/list_tracked_time.rs View lib/service/issues/src/handlers/tracked_time/list_tracked_time.rs
@@ -13,7 +13,7 @@
1313 description = "分页读取议题工时记录,可按登记用户过滤。",
1414 params(TrackedTimePath, ListTrackedTimeQuery),
1515 responses(
16 (status = 200, description = "list tracked times", body = AppResponseData<TrackedTimePage>),
16 (status = 200, description = "list tracked times", body = AppResponseData<api::pagination::Page<crate::services::tracked_time::TrackedTimeDto>>),
1717 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1818 (status = 404, description = "not found", body = AppResponseData<NoData>),
1919 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/issues/src/openapi.rs View lib/service/issues/src/openapi.rs
@@ -77,39 +77,38 @@
7777 components(schemas(
7878 AppResponseData<api::rest::NoData>,
7979 AppResponseData<crate::services::boards::BoardDto>,
80 AppResponseData<crate::services::boards::BoardPage>,
80 AppResponseData<api::pagination::Page<crate::services::boards::BoardDto>>,
8181 AppResponseData<crate::services::issues::IssueDto>,
82 AppResponseData<crate::services::issues::IssuePage>,
82 AppResponseData<api::pagination::Page<crate::services::issues::IssueListItemDto>>,
8383 AppResponseData<crate::services::comments::CommentDto>,
84 AppResponseData<crate::services::comments::CommentPage>,
84 AppResponseData<api::pagination::Page<crate::services::comments::CommentDto>>,
8585 AppResponseData<crate::services::labels::LabelDto>,
86 AppResponseData<crate::services::labels::LabelPage>,
86 AppResponseData<api::pagination::Page<crate::services::labels::LabelDto>>,
8787 AppResponseData<Vec<crate::services::labels::LabelDto>>,
8888 AppResponseData<crate::services::milestones::MilestoneDto>,
89 AppResponseData<crate::services::milestones::MilestonePage>,
89 AppResponseData<api::pagination::Page<crate::services::milestones::MilestoneDto>>,
9090 AppResponseData<crate::services::reactions::ReactionDto>,
9191 AppResponseData<Vec<crate::services::reactions::ReactionAggDto>>,
9292 AppResponseData<crate::services::watches::WatchDto>,
9393 AppResponseData<crate::services::members::IssueMemberDto>,
94 AppResponseData<crate::services::members::IssueMemberPage>,
94 AppResponseData<api::pagination::Page<crate::services::members::IssueMemberDto>>,
9595 AppResponseData<crate::services::pins::PinDto>,
9696 AppResponseData<Vec<crate::services::pins::PinDto>>,
97 AppResponseData<crate::services::events::EventPage>,
97 AppResponseData<api::pagination::Page<crate::services::events::EventDto>>,
9898 AppResponseData<crate::services::dependencies::DependencyDto>,
9999 AppResponseData<Vec<crate::services::dependencies::DependencyDto>>,
100 AppResponseData<crate::services::content_history::ContentHistoryPage>,
100 AppResponseData<api::pagination::Page<crate::services::content_history::ContentHistoryDto>>,
101101 AppResponseData<Option<crate::services::stopwatch::StopwatchDto>>,
102102 AppResponseData<crate::services::stopwatch::StopwatchDto>,
103 AppResponseData<crate::services::tracked_time::TrackedTimePage>,
103 AppResponseData<api::pagination::Page<crate::services::tracked_time::TrackedTimeDto>>,
104104 AppResponseData<crate::services::issue_users::IssueUserDto>,
105 AppResponseData<crate::services::issue_users::IssueUserPage>,
105 AppResponseData<api::pagination::Page<crate::services::issue_users::IssueUserDto>>,
106106 AppResponseData<crate::services::repo_relations::RepoRelDto>,
107107 AppResponseData<Vec<crate::services::repo_relations::RepoRelDto>>,
108108 api::rest::NoData,
109109 api::users::UserDto,
110110 space::services::dto::SpacePath,
111111 crate::services::boards::BoardDto,
112 crate::services::boards::BoardPage,
113112 crate::services::boards::CreateBoardCmd,
114113 crate::services::boards::UpdateBoardCmd,
115114 crate::services::boards::ListBoardQuery,
@@ -124,20 +123,17 @@
124123 crate::services::issues::IssuePath,
125124 crate::services::comments::CommentDto,
126125 crate::services::comments::CommentTreeNode,
127 crate::services::comments::CommentPage,
128126 crate::services::comments::CreateCommentCmd,
129127 crate::services::comments::UpdateCommentCmd,
130128 crate::services::comments::ListCommentQuery,
131129 crate::services::comments::CommentPath,
132130 crate::services::labels::LabelDto,
133 crate::services::labels::LabelPage,
134131 crate::services::labels::CreateLabelCmd,
135132 crate::services::labels::UpdateLabelCmd,
136133 crate::services::labels::ListLabelQuery,
137134 crate::services::labels::LabelPath,
138135 crate::services::labels::IssueLabelPath,
139136 crate::services::milestones::MilestoneDto,
140 crate::services::milestones::MilestonePage,
141137 crate::services::milestones::CreateMilestoneCmd,
142138 crate::services::milestones::UpdateMilestoneCmd,
143139 crate::services::milestones::ListMilestoneQuery,
@@ -153,7 +149,6 @@
153149 crate::services::watches::WatchPath,
154150 models::common::watch_mode::WatchMode,
155151 crate::services::members::IssueMemberDto,
156 crate::services::members::IssueMemberPage,
157152 crate::services::members::CreateIssueMemberCmd,
158153 crate::services::members::ListIssueMemberQuery,
159154 crate::services::members::IssueMemberPath,
@@ -163,20 +158,17 @@
163158 crate::services::pins::UpdatePinCmd,
164159 crate::services::pins::PinPath,
165160 crate::services::events::EventDto,
166 crate::services::events::EventPage,
167161 crate::services::events::EventPath,
168162 crate::services::events::ListEventQuery,
169163 crate::services::dependencies::DependencyDto,
170164 crate::services::dependencies::CreateDependencyCmd,
171165 crate::services::dependencies::DependencyPath,
172166 crate::services::content_history::ContentHistoryDto,
173 crate::services::content_history::ContentHistoryPage,
174167 crate::services::content_history::ContentHistoryPath,
175168 crate::services::content_history::ListContentHistoryQuery,
176169 crate::services::stopwatch::StopwatchDto,
177170 crate::services::stopwatch::StopwatchPath,
178171 crate::services::tracked_time::TrackedTimeDto,
179 crate::services::tracked_time::TrackedTimePage,
180172 crate::services::tracked_time::TrackedTimePath,
181173 crate::services::tracked_time::TrackedTimeIdPath,
182174 crate::services::tracked_time::CreateTrackedTimeCmd,
@@ -187,7 +179,6 @@
187179 crate::services::tracked_time::TrackedTimePath,
188180 crate::services::tracked_time::ListTrackedTimeQuery,
189181 crate::services::issue_users::IssueUserDto,
190 crate::services::issue_users::IssueUserPage,
191182 crate::services::issue_users::UpdateIssueUserCmd,
192183 crate::services::issue_users::IssueUserPath,
193184 crate::services::issue_users::ListIssueUserQuery,
Modified lib/service/pulls/src/handlers/comments/list_pull_comments.rs View lib/service/pulls/src/handlers/comments/list_pull_comments.rs
@@ -16,7 +16,7 @@
1616 description = "分页读取合并请求的全部评论(含行内评论)。",
1717 params(PullPath, PageQuery),
1818 responses(
19 (status = 200, description = "comments", body = AppResponseData<CommentPage>)
19 (status = 200, description = "comments", body = AppResponseData<api::pagination::Page<crate::services::comments::PullCommentDto>>)
2020 )
2121)]
2222pub async fn list_pull_comments(
Modified lib/service/pulls/src/handlers/pulls/list_pull_events.rs View lib/service/pulls/src/handlers/pulls/list_pull_events.rs
@@ -16,7 +16,7 @@
1616 description = "分页读取合并请求时间线(创建、评审、合并、推送更新等)。",
1717 params(PullPath, PageQuery),
1818 responses(
19 (status = 200, description = "pull request timeline", body = AppResponseData<EventPage>)
19 (status = 200, description = "pull request timeline", body = AppResponseData<api::pagination::Page<crate::services::events::PullEventDto>>)
2020 )
2121)]
2222pub async fn list_pull_events(
Modified lib/service/pulls/src/handlers/pulls/list_pulls.rs View lib/service/pulls/src/handlers/pulls/list_pulls.rs
@@ -15,7 +15,7 @@
1515 description = "按状态、作者、分支、草稿标记或关键字分页查询合并请求。",
1616 params(RepoPath, ListPullQuery),
1717 responses(
18 (status = 200, description = "list pull requests", body = AppResponseData<PullPage>)
18 (status = 200, description = "list pull requests", body = AppResponseData<api::pagination::Page<crate::services::pulls::PullDto>>)
1919 )
2020)]
2121pub async fn list_pulls(
Modified lib/service/pulls/src/handlers/inbox.rs View lib/service/pulls/src/handlers/inbox.rs
@@ -19,7 +19,7 @@
1919 description = "跨仓库的个人合并请求收件箱:我创建的、待我评审的、我评审过的。",
2020 params(InboxPath, InboxQuery),
2121 responses(
22 (status = 200, description = "inbox", body = AppResponseData<InboxPage>)
22 (status = 200, description = "inbox", body = AppResponseData<api::pagination::Page<crate::services::pulls::PullDto>>)
2323 )
2424)]
2525pub async fn list_my_pulls(
Modified lib/service/pulls/src/openapi.rs View lib/service/pulls/src/openapi.rs
@@ -56,9 +56,9 @@
5656 api::pagination::SortOrder,
5757 crate::services::pulls::PullSort,
5858 AppResponseData<crate::services::pulls::PullDto>,
59 AppResponseData<crate::services::pulls::PullPage>,
60 AppResponseData<crate::services::comments::CommentPage>,
61 AppResponseData<crate::services::events::EventPage>,
59 AppResponseData<api::pagination::Page<crate::services::pulls::PullDto>>,
60 AppResponseData<api::pagination::Page<crate::services::comments::PullCommentDto>>,
61 AppResponseData<api::pagination::Page<crate::services::events::PullEventDto>>,
6262 AppResponseData<Vec<git::facade::MergeCommitDto>>,
6363 AppResponseData<Vec<git::facade::ChangedPathDiffDto>>,
6464 AppResponseData<crate::services::changes::PullSummaryDto>,
@@ -74,7 +74,7 @@
7474 AppResponseData<Vec<crate::services::relations::IssuePullLinkDto>>,
7575 AppResponseData<crate::services::checks::PullChecksDto>,
7676 AppResponseData<crate::services::checks::PullGateDto>,
77 AppResponseData<crate::services::inbox::InboxPage>,
77 AppResponseData<api::pagination::Page<crate::services::pulls::PullDto>>,
7878 NoData,
7979 api::users::UserDto,
8080 issues::services::labels::LabelDto,
Modified lib/service/space/src/handlers/explore_spaces.rs View lib/service/space/src/handlers/explore_spaces.rs
@@ -17,7 +17,7 @@
1717 description = "分页列出公开的活跃空间;已登录时返回调用者的成员与申请状态。",
1818 params(ExploreQuery),
1919 responses(
20 (status = 200, description = "explore public spaces", body = AppResponseData<ExploreSpacePage>),
20 (status = 200, description = "explore public spaces", body = AppResponseData<api::pagination::Page<crate::services::join_requests::ExploreSpaceDto>>),
2121 (status = 400, description = "bad request", body = AppResponseData<NoData>)
2222 )
2323)]
@@ -37,7 +37,7 @@
3737 description = "分页列出空间的加入申请,需要成员管理权限。",
3838 params(SpacePath, ListJoinRequestsQuery),
3939 responses(
40 (status = 200, description = "list join requests", body = AppResponseData<JoinRequestPage>),
40 (status = 200, description = "list join requests", body = AppResponseData<api::pagination::Page<crate::services::join_requests::JoinRequestDto>>),
4141 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
4242 (status = 404, description = "not found", body = AppResponseData<NoData>),
4343 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/space/src/handlers/list_groups.rs View lib/service/space/src/handlers/list_groups.rs
@@ -14,7 +14,7 @@
1414 description = "分页列出空间群组及权限;需要角色管理权限。",
1515 params(SpacePath, PageQuery),
1616 responses(
17 (status = 200, description = "list space groups", body = AppResponseData<GroupPage>),
17 (status = 200, description = "list space groups", body = AppResponseData<api::pagination::Page<crate::services::groups::GroupDto>>),
1818 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
1919 (status = 404, description = "space not found", body = AppResponseData<NoData>),
2020 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
Modified lib/service/space/src/services/join.rs View lib/service/space/src/services/join.rs
@@ -210,7 +210,7 @@
210210 .filter(space_join_requests::Column::OrganizationId.eq(organization_id))
211211 .filter(space_join_requests::Column::UserId.eq(actor_user_id))
212212 .filter(space_join_requests::Column::Status.eq(SpaceJoinRequestState::Pending))
213 .one(self.access.database.read())
213 .one(self.access.database.write())
214214 .await?
215215 {
216216 let mut active: space_join_requests::ActiveModel = pending.into();
Modified lib/service/space/src/services/join_requests.rs View lib/service/space/src/services/join_requests.rs
@@ -355,7 +355,7 @@
355355 .filter(space_join_requests::Column::OrganizationId.eq(space.id))
356356 .filter(space_join_requests::Column::UserId.eq(caller))
357357 .filter(space_join_requests::Column::Status.eq(SpaceJoinRequestState::Pending))
358 .one(self.access.database.read())
358 .one(self.access.database.write())
359359 .await?;
360360 if pending.is_some() {
361361 return Err(SpaceError::Conflict(
Modified lib/service/space/src/openapi.rs View lib/service/space/src/openapi.rs
@@ -82,11 +82,11 @@
8282 api::pagination::SortOrder,
8383 crate::services::join_requests::ExploreSort,
8484 AppResponseData<crate::services::dto::SpaceDto>,
85 AppResponseData<crate::services::join_requests::ExploreSpacePage>,
85 AppResponseData<api::pagination::Page<crate::services::join_requests::ExploreSpaceDto>>,
8686 AppResponseData<crate::services::join_requests::JoinRequestDto>,
87 AppResponseData<crate::services::join_requests::JoinRequestPage>,
87 AppResponseData<api::pagination::Page<crate::services::join_requests::JoinRequestDto>>,
8888 AppResponseData<crate::services::groups::GroupDto>,
89 AppResponseData<crate::services::groups::GroupPage>,
89 AppResponseData<api::pagination::Page<crate::services::groups::GroupDto>>,
9090 AppResponseData<crate::services::dto::MemberDto>,
9191 AppResponseData<crate::services::images::SpaceImageDto>,
9292 AppResponseData<Option<crate::services::images::SpaceImageDto>>,
@@ -99,17 +99,14 @@
9999 AppResponseData<Vec<crate::services::dir::UserOverwriteDto>>,
100100 crate::services::dto::SpaceDto,
101101 crate::services::join_requests::ExploreSpaceDto,
102 crate::services::join_requests::ExploreSpacePage,
103102 crate::services::join_requests::ExploreViewerDto,
104103 crate::services::join_requests::ExploreViewerRequestDto,
105104 crate::services::join_requests::JoinRequestDto,
106 crate::services::join_requests::JoinRequestPage,
107105 crate::services::join_requests::CreateJoinRequestCmd,
108106 models::space::SpaceJoinRequestState,
109107 api::pagination::Page<crate::services::dto::SpaceDto>,
110108 crate::services::dto::UserDto,
111109 crate::services::groups::GroupDto,
112 crate::services::groups::GroupPage,
113110 crate::services::groups::CreateGroupCmd,
114111 crate::services::groups::UpdateGroupCmd,
115112 crate::services::groups::AssignGroupCmd,
Modified lib/service/webapi/src/account_deletion.rs View lib/service/webapi/src/account_deletion.rs
@@ -82,6 +82,7 @@
8282 responses(
8383 (status = 200, description = "deletion cancelled", body = AppResponseData<NoData>),
8484 (status = 404, description = "no deletion is pending", body = AppResponseData<NoData>),
85 (status = 409, description = "account deletion is currently being purged", body = AppResponseData<NoData>),
8586 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
8687 )
8788)]
Modified lib/share/models/src/migrate/mod.rs View lib/share/models/src/migrate/mod.rs
@@ -1,23 +1,62 @@
11use std::hash::{DefaultHasher, Hash, Hasher};
22
3use sea_orm::{ConnectionTrait, DatabaseBackend, DatabaseConnection, Statement};
3use sea_orm::{
4 ConnectionTrait, DatabaseBackend, DatabaseConnection, DbErr, Statement, TransactionTrait,
5};
46
57const SCHEMA_PRE_SYNC: &str = include_str!("../../schema_pre_sync.sql");
68const SCHEMA_EXTRAS: &str = include_str!("../../schema_extras.sql");
9/// Bump whenever `sync_schema` gains a Rust-side step that is not captured by
10/// the entity registry or by `schema_pre_sync.sql` / `schema_extras.sql`.
711const SYNC_FORMAT_VERSION: u32 = 1;
12pub const MIGRATION_LOCK_KEY: i64 = 5_241_915_330_551;
813const SYNC_STATE_DDL: &str = "CREATE TABLE IF NOT EXISTS schema_sync_state (
914 id smallint PRIMARY KEY CHECK (id = 1),
1015 fingerprint text NOT NULL,
16 live_fingerprint text,
1117 synced_at timestamptz NOT NULL DEFAULT now()
1218)";
19const SYNC_STATE_LIVE_COLUMN: &str =
20 "ALTER TABLE schema_sync_state ADD COLUMN IF NOT EXISTS live_fingerprint text";
21const LIVE_FINGERPRINT_SQL: &str = r#"
22SELECT COALESCE(md5(string_agg(entry, E'\n' ORDER BY entry)), '') AS fingerprint
23FROM (
24 SELECT format('table:%s', table_name) AS entry
25 FROM information_schema.tables
26 WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
27 UNION ALL
28 SELECT format('column:%s.%s:%s:%s:%s',
29 table_name, column_name, data_type, is_nullable,
30 COALESCE(column_default, ''))
31 FROM information_schema.columns
32 WHERE table_schema = 'public'
33 AND table_name IN (
34 SELECT table_name
35 FROM information_schema.tables
36 WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
37 )
38 UNION ALL
39 SELECT format('index:%s:%s', tablename, indexdef)
40 FROM pg_indexes
41 WHERE schemaname = 'public'
42 UNION ALL
43 SELECT format('constraint:%s:%s', conrelid::regclass, pg_get_constraintdef(oid))
44 FROM pg_constraint
45 WHERE connamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
46 UNION ALL
47 SELECT format('enum:%s:%s', t.typname, e.enumlabel)
48 FROM pg_type t
49 JOIN pg_enum e ON e.enumtypid = t.oid
50 JOIN pg_namespace n ON n.oid = t.typnamespace
51 WHERE n.nspname = 'public'
52) AS entries
53"#;
1354
14pub async fn sync_schema(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> {
55pub async fn sync_schema(db: &DatabaseConnection) -> Result<(), DbErr> {
1556 let builder = db.get_schema_registry("models::");
1657 if db.get_database_backend() != DatabaseBackend::Postgres {
1758 return builder.sync(db).await;
1859 }
19
20 db.execute_unprepared(SYNC_STATE_DDL).await?;
2160
2261 let mut hasher = DefaultHasher::new();
2362 SYNC_FORMAT_VERSION.hash(&mut hasher);
@@ -26,26 +65,57 @@
2665 format!("{builder:?}").hash(&mut hasher);
2766 let fingerprint = format!("{:016x}", hasher.finish());
2867
29 let stored = db
68 let txn = db.begin().await?;
69 txn.execute_unprepared(&format!(
70 "SELECT pg_advisory_xact_lock({MIGRATION_LOCK_KEY})"
71 ))
72 .await?;
73 txn.execute_unprepared(SYNC_STATE_DDL).await?;
74 txn.execute_unprepared(SYNC_STATE_LIVE_COLUMN).await?;
75
76 let live = live_fingerprint(&txn).await?;
77 let stored = txn
3078 .query_one_raw(Statement::from_string(
3179 DatabaseBackend::Postgres,
32 "SELECT fingerprint FROM schema_sync_state WHERE id = 1",
80 "SELECT fingerprint, live_fingerprint FROM schema_sync_state WHERE id = 1",
3381 ))
3482 .await?;
3583 if let Some(row) = stored {
36 if row.try_get::<String>("", "fingerprint")? == fingerprint {
84 let stored_fingerprint = row.try_get::<String>("", "fingerprint")?;
85 let stored_live = row.try_get::<Option<String>>("", "live_fingerprint")?;
86 if stored_fingerprint == fingerprint && stored_live.as_deref() == Some(live.as_str()) {
87 txn.commit().await?;
3788 return Ok(());
3889 }
3990 }
4091
41 db.execute_unprepared(SCHEMA_PRE_SYNC).await?;
42 builder.sync(db).await?;
43 db.execute_unprepared(SCHEMA_EXTRAS).await?;
92 txn.execute_unprepared(SCHEMA_PRE_SYNC).await?;
93 builder.sync(&txn).await?;
94 txn.execute_unprepared(SCHEMA_EXTRAS).await?;
4495
96 let live = live_fingerprint(&txn).await?;
4597 let update = format!(
46 "INSERT INTO schema_sync_state (id, fingerprint) VALUES (1, '{fingerprint}') \
47 ON CONFLICT (id) DO UPDATE SET fingerprint = EXCLUDED.fingerprint, synced_at = now()"
98 "INSERT INTO schema_sync_state (id, fingerprint, live_fingerprint) \
99 VALUES (1, '{fingerprint}', '{live}') \
100 ON CONFLICT (id) DO UPDATE SET fingerprint = EXCLUDED.fingerprint, \
101 live_fingerprint = EXCLUDED.live_fingerprint, synced_at = now()"
48102 );
49 db.execute_unprepared(&update).await?;
50 Ok(())
103 txn.execute_unprepared(&update).await?;
104 txn.commit().await
105}
106
107async fn live_fingerprint<C>(db: &C) -> Result<String, DbErr>
108where
109 C: ConnectionTrait,
110{
111 let row = db
112 .query_one_raw(Statement::from_string(
113 DatabaseBackend::Postgres,
114 LIVE_FINGERPRINT_SQL,
115 ))
116 .await?;
117 match row {
118 Some(row) => row.try_get::<String>("", "fingerprint"),
119 None => Ok(String::new()),
120 }
51121}
Modified lib/share/models/schema_extras.sql View lib/share/models/schema_extras.sql
@@ -2,7 +2,7 @@
22-- Idempotent PostgreSQL DDL for constraints that SeaORM entity-first `sync`
33-- cannot express via entity attributes. Applied by models::migrate::sync_schema
44-- after each successful entity sync (fingerprint-gated), so it only runs when
5-- the entity schema or one of the two SQL files changed.
5-- the entity schema, one of the two SQL files, or the live database changed.
66-- Safe to re-run on every schema change; every statement is guarded with
77-- IF NOT EXISTS / DO-block checks.
88
@@ -29,6 +29,19 @@
2929CREATE UNIQUE INDEX IF NOT EXISTS idx_branch_protection_whitelist_group
3030 ON repo_branch_protection_whitelist (policy_id, kind, group_id) WHERE group_id IS NOT NULL;
3131
32-- One pending join request per (organization, user). Existing duplicates are
33-- collapsed to the newest row before the unique index is (re)built.
34DELETE FROM space_join_requests AS stale
35USING space_join_requests AS keep
36WHERE stale.organization_id = keep.organization_id
37 AND stale.user_id = keep.user_id
38 AND stale.status = 'pending'
39 AND keep.status = 'pending'
40 AND (stale.created_at, stale.id) < (keep.created_at, keep.id);
41
42CREATE UNIQUE INDEX IF NOT EXISTS idx_space_join_requests_pending
43 ON space_join_requests (organization_id, user_id) WHERE status = 'pending';
44
3245-- ---------------------------------------------------------------------------
3346-- Composite non-unique indexes (entity attributes only express single-column
3447-- indexed/unique; lookups below span two columns).
Modified lib/share/models/schema_pre_sync.sql View lib/share/models/schema_pre_sync.sql
@@ -6,21 +6,22 @@
66-- stale unique key, and Postgres then issues DROP CONSTRAINT on a plain index,
77-- which fails. These indexes are recreated by schema_extras.sql after sync.
88--
9-- Only indexes whose column list differs from an entity unique key belong
10-- here. Indexes that do match (idx_issue_watch_issue_user, ..._project_user,
11-- idx_issue_reaction_issue, idx_issue_reaction_comment) must NOT be dropped:
12-- sync keeps them as-is and the drop would only rebuild them needlessly.
13--
14-- The migrate command only runs this file when the entity schema fingerprint
15-- changed, so the rebuild cost is paid on schema changes, not on every start.
9-- Partial indexes are dropped even when their column list matches an entity
10-- unique key: sync compares only column lists and then issues DROP CONSTRAINT,
11-- which fails because these are plain indexes. Recreating them is cheap enough
12-- because the gate below runs on schema changes or drift, not on every start.
1613
1714-- Legacy plain indexes left behind by older unique_key definitions. They no
1815-- longer match the entity column list, so sync would try to drop them and
1916-- abort because they are plain indexes.
2017DROP INDEX IF EXISTS "idx-issue_watch-issue_user";
21DROP INDEX IF EXISTS "idx-issue-reaction-issue_user_emoji";
18DROP INDEX IF EXISTS "idx-issue_reaction-issue_user_emoji";
2219
2320-- Hand-managed per-target unique keys (predicates not expressible in entities).
21DROP INDEX IF EXISTS idx_issue_watch_issue_user;
22DROP INDEX IF EXISTS idx_issue_watch_project_user;
23DROP INDEX IF EXISTS idx_issue_reaction_issue;
24DROP INDEX IF EXISTS idx_issue_reaction_comment;
2425DROP INDEX IF EXISTS idx_issue_reaction_project;
2526DROP INDEX IF EXISTS idx_branch_protection_whitelist_user;
2627DROP INDEX IF EXISTS idx_branch_protection_whitelist_group;
@@ -28,6 +29,7 @@
2829DROP INDEX IF EXISTS idx_chat_dm_pair;
2930DROP INDEX IF EXISTS idx_chat_gdm_key;
3031DROP INDEX IF EXISTS idx_chat_draft_unique;
32DROP INDEX IF EXISTS idx_space_join_requests_pending;
3133-- scheduled messages feature removed; drop the legacy table before sync
3234DROP TABLE IF EXISTS user_scheduled_message;
3335
Modified openapi.json View openapi.json

Diff too large to display.