Skip to content
Clone

feat(pagination): update pagination handling across multiple services and queries

ZhenYi committed
parent 81224d4 commit 9818851 Browse files

125 files changed

+1268 −736

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

File tree 125
Modified lib/infra/api/src/api_error.rs View lib/infra/api/src/api_error.rs
@@ -55,6 +55,17 @@
5555 };
5656}
5757
58#[macro_export]
59macro_rules! page_error_conversion {
60 ($name:ident, $variant:ident) => {
61 impl From<$crate::pagination::PageError> for $name {
62 fn from(error: $crate::pagination::PageError) -> Self {
63 Self::$variant(error.to_string())
64 }
65 }
66 };
67}
68
5869/// Standard variant-to-variant mapping between two domain error enums sharing
5970/// the `BadRequest/Unauthorized/Forbidden/NotFound/Conflict/RateLimited/Internal`
6071/// shape; non-matching source variants fall through to `Internal`.
@@ -253,6 +264,12 @@
253264 }
254265 }
255266
267 impl From<$crate::pagination::PageError> for $name {
268 fn from(error: $crate::pagination::PageError) -> Self {
269 Self::bad_request(error.to_string())
270 }
271 }
272
256273 impl From<(::actix_web::http::StatusCode, &'static str)> for $name {
257274 fn from((status, message): (::actix_web::http::StatusCode, &'static str)) -> Self {
258275 ::tracing::error!(%status, %message, "unhandled (status, message) converted to 500");
Modified lib/infra/api/src/pagination.rs View lib/infra/api/src/pagination.rs
@@ -1,3 +1,5 @@
1use std::fmt;
2
13use serde::{Deserialize, Serialize};
24use utoipa::{IntoParams, ToSchema};
35
@@ -5,56 +7,110 @@
57pub const MAX_PAGE_LIMIT: u64 = 100;
68pub const MAX_OFFSET: u64 = 5000;
79
8#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
10#[derive(Debug, Clone, Copy, Default, Deserialize, IntoParams, ToSchema)]
11#[serde(deny_unknown_fields)]
912pub struct PageQuery {
1013 pub limit: Option<u64>,
1114 pub offset: Option<u64>,
12 pub sort: Option<String>,
1315}
1416
15pub fn page_bounds(limit: Option<u64>, offset: Option<u64>, default: u64, max: u64) -> (u64, u64) {
16 (
17 limit.unwrap_or(default).clamp(1, max),
18 offset.unwrap_or(0).clamp(0, MAX_OFFSET),
19 )
17impl PageQuery {
18 pub fn resolve(&self, default: u64, max: u64) -> Result<(u64, u64), PageError> {
19 resolve_bounds(self.limit, self.offset, default, max)
20 }
21
22 pub fn bounds(&self) -> Result<(u64, u64), PageError> {
23 self.resolve(DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT)
24 }
2025}
2126
22impl PageQuery {
23 pub fn limit(&self) -> u64 {
24 self.limit
25 .unwrap_or(DEFAULT_PAGE_LIMIT)
26 .clamp(1, MAX_PAGE_LIMIT)
27pub fn page_bounds(
28 limit: Option<u64>,
29 offset: Option<u64>,
30 default: u64,
31 max: u64,
32) -> Result<(u64, u64), PageError> {
33 resolve_bounds(limit, offset, default, max)
34}
35
36fn resolve_bounds(
37 limit: Option<u64>,
38 offset: Option<u64>,
39 default: u64,
40 max: u64,
41) -> Result<(u64, u64), PageError> {
42 let limit = limit.unwrap_or(default);
43 if limit == 0 || limit > max {
44 return Err(PageError(format!("limit must be between 1 and {max}")));
45 }
46 let offset = offset.unwrap_or(0);
47 if offset > MAX_OFFSET {
48 return Err(PageError(format!("offset must not exceed {MAX_OFFSET}")));
2749 }
50 Ok((limit, offset))
51}
52
53#[derive(Debug, Clone)]
54pub struct PageError(String);
2855
29 pub fn offset(&self) -> u64 {
30 self.offset.unwrap_or(0).clamp(0, MAX_OFFSET)
56impl PageError {
57 pub fn message(&self) -> &str {
58 &self.0
3159 }
60}
3261
33 pub fn page(&self) -> (u64, u64) {
34 (self.limit(), self.offset())
62impl fmt::Display for PageError {
63 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64 formatter.write_str(&self.0)
3565 }
3666}
3767
68impl std::error::Error for PageError {}
69
3870#[derive(Debug, Clone, Serialize, ToSchema)]
3971pub struct Page<T> {
4072 pub items: Vec<T>,
41 pub total: i64,
73 pub total: u64,
4274 pub limit: u64,
4375 pub offset: u64,
4476}
4577
46pub fn paginate<T>(items: Vec<T>, page: &PageQuery) -> Page<T> {
47 let total = items.len() as i64;
48 let (limit, offset) = page.page();
78impl<T> Page<T> {
79 pub fn new(items: Vec<T>, total: u64, limit: u64, offset: u64) -> Self {
80 Self {
81 items,
82 total,
83 limit,
84 offset,
85 }
86 }
87}
88
89pub fn paginate<T>(items: Vec<T>, page: &PageQuery) -> Result<Page<T>, PageError> {
90 let (limit, offset) = page.bounds()?;
91 let total = items.len() as u64;
4992 let items = items
5093 .into_iter()
5194 .skip(offset as usize)
5295 .take(limit as usize)
5396 .collect();
54 Page {
97 Ok(Page {
5598 items,
5699 total,
57100 limit,
58101 offset,
102 })
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
106#[serde(rename_all = "snake_case")]
107pub enum SortOrder {
108 Asc,
109 Desc,
110}
111
112impl SortOrder {
113 pub fn is_desc(self) -> bool {
114 matches!(self, Self::Desc)
59115 }
60116}
Modified lib/service/auth/src/handlers/devices.rs View lib/service/auth/src/handlers/devices.rs
@@ -60,15 +60,15 @@
6060 description = "按会话的用户代理归并出当前账号出现过的设备及其最近活跃时间。",
6161 params(PageQuery),
6262 responses(
63 (status = 200, description = "devices derived from active sessions", body = AppResponseData<Vec<DeviceDto>>),
64 (status = 401, description = "authentication required", body = AppResponseData<Vec<DeviceDto>>)
63 (status = 200, description = "devices derived from active sessions", body = AppResponseData<api::pagination::Page<DeviceDto>>),
64 (status = 401, description = "authentication required", body = AppResponseData<api::pagination::Page<DeviceDto>>)
6565 )
6666)]
6767pub async fn list_devices(
6868 State(service): State<AuthService>,
6969 session: Session,
7070 Query(query): Query<PageQuery>,
71) -> Result<AppResponseData<Vec<DeviceDto>>, ApiError> {
71) -> Result<AppResponseData<api::pagination::Page<DeviceDto>>, ApiError> {
7272 let uid = session::require_uid(&session)?;
7373 let listed = service
7474 .accounts
@@ -159,7 +159,8 @@
159159 }
160160
161161 rows.sort_by(|left, right| right.last_seen_at.cmp(&left.last_seen_at));
162 let (limit, offset) = query.page();
162 let (limit, offset) = query.bounds()?;
163 let total = rows.len() as u64;
163164 let devices = rows
164165 .into_iter()
165166 .skip(offset as usize)
@@ -174,5 +175,7 @@
174175 last_seen_at: row.last_seen_at,
175176 })
176177 .collect();
177 Ok(AppResponseData::ok(devices))
178 Ok(AppResponseData::ok(api::pagination::Page::new(
179 devices, total, limit, offset,
180 )))
178181}
Modified lib/service/auth/src/handlers/list_notifications.rs View lib/service/auth/src/handlers/list_notifications.rs
@@ -10,6 +10,7 @@
1010use ::session::share::session;
1111
1212#[derive(Debug, Clone, Copy, Default, Deserialize, IntoParams)]
13#[serde(deny_unknown_fields)]
1314pub struct ListNotificationsQuery {
1415 pub limit: Option<u64>,
1516 pub offset: Option<u64>,
@@ -18,6 +19,15 @@
1819 pub unread: Option<bool>,
1920}
2021
22impl ListNotificationsQuery {
23 fn page(&self) -> api::pagination::PageQuery {
24 api::pagination::PageQuery {
25 limit: self.limit,
26 offset: self.offset,
27 }
28 }
29}
30
2131#[utoipa::path(
2232 get,
2333 path = "/me/notifications",
@@ -36,28 +46,11 @@
3646 Query(query): Query<ListNotificationsQuery>,
3747) -> Result<AppResponseData<api::pagination::Page<NotificationDto>>, ApiError> {
3848 let uid = session::require_uid(&session)?;
39 let notifications = service
40 .list_notifications(
41 uid,
42 query.limit,
43 query.offset,
44 query.notification_type,
45 query.unread,
46 )
49 let notification_type = query.notification_type;
50 let unread = query.unread;
51 let page = service
52 .list_notifications(uid, query.page(), notification_type, unread)
4753 .await
4854 .map_err(ApiError::from)?;
49 let limit = query.limit.unwrap_or(50).min(100);
50 let offset = query.offset.unwrap_or(0);
51 let total = notifications.len() as i64;
52 let items: Vec<NotificationDto> = notifications
53 .into_iter()
54 .skip(offset as usize)
55 .take(limit as usize)
56 .collect();
57 Ok(AppResponseData::ok(api::pagination::Page {
58 items,
59 total,
60 limit,
61 offset,
62 }))
55 Ok(AppResponseData::ok(page))
6356}
Modified lib/service/auth/src/handlers/sessions.rs View lib/service/auth/src/handlers/sessions.rs
@@ -43,6 +43,7 @@
4343}
4444
4545#[derive(Debug, Deserialize, utoipa::IntoParams)]
46#[serde(deny_unknown_fields)]
4647pub struct SessionQuery {
4748 pub kind: Option<SessionKind>,
4849 pub include_revoked: Option<bool>,
@@ -55,7 +56,6 @@
5556 PageQuery {
5657 limit: self.limit,
5758 offset: self.offset,
58 sort: None,
5959 }
6060 }
6161}
@@ -142,7 +142,7 @@
142142 .map_err(|error| ApiError::internal(error.to_string()))?;
143143 let current_session = session.session_id();
144144 let sessions = session_list(listed, current_session.as_deref())?;
145 Ok(AppResponseData::ok(paginate(sessions, &query.page())))
145 Ok(AppResponseData::ok(paginate(sessions, &query.page())?))
146146}
147147
148148#[utoipa::path(
Modified lib/service/auth/src/services/notifications.rs View lib/service/auth/src/services/notifications.rs
@@ -1,3 +1,4 @@
1use api::pagination::{Page, PageQuery};
12use models::users::user_notifications;
23use models::users::user_notifications::NotificationType;
34use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
@@ -25,19 +26,13 @@
2526 pub async fn list_notifications(
2627 &self,
2728 uid: i64,
28 limit: Option<u64>,
29 offset: Option<u64>,
29 page: PageQuery,
3030 notification_type: Option<NotificationType>,
3131 unread: Option<bool>,
32 ) -> Result<Vec<NotificationDto>, AuthBusinessError> {
33 let max_limit = limit.unwrap_or(50).min(100);
34 let mut query = user_notifications::Entity::find()
35 .filter(user_notifications::Column::UserId.eq(uid))
36 .order_by_desc(user_notifications::Column::CreatedAt)
37 .limit(max_limit);
38 if let Some(offset) = offset {
39 query = query.offset(offset);
40 }
32 ) -> Result<Page<NotificationDto>, AuthBusinessError> {
33 let (limit, offset) = page.bounds()?;
34 let mut query =
35 user_notifications::Entity::find().filter(user_notifications::Column::UserId.eq(uid));
4136 if let Some(notification_type) = notification_type {
4237 query =
4338 query.filter(user_notifications::Column::NotificationType.eq(notification_type));
@@ -45,8 +40,14 @@
4540 if unread == Some(true) {
4641 query = query.filter(user_notifications::Column::ReadAt.is_null());
4742 }
48 let rows = query.all(self.db().read()).await?;
49 Ok(rows
43 let total = query.clone().count(self.db().read()).await?;
44 let rows = query
45 .order_by_desc(user_notifications::Column::CreatedAt)
46 .limit(limit)
47 .offset(offset)
48 .all(self.db().read())
49 .await?;
50 let items = rows
5051 .into_iter()
5152 .map(|row| NotificationDto {
5253 id: row.id,
@@ -58,7 +59,8 @@
5859 read_at: row.read_at,
5960 created_at: row.created_at,
6061 })
61 .collect())
62 .collect();
63 Ok(Page::new(items, total, limit, offset))
6264 }
6365
6466 pub async fn unread_notifications_count(&self, uid: i64) -> Result<u64, AuthBusinessError> {
Modified lib/service/auth/src/share/mod.rs View lib/service/auth/src/share/mod.rs
@@ -14,7 +14,7 @@
1414pub mod user;
1515pub mod validate;
1616
17pub use types::{AgreementDoc, ListQuery, UserPublic};
17pub use types::{AgreementDoc, UserPublic};
1818pub mod auth_convert;
1919pub mod auth_error;
2020pub mod auth_store;
Modified lib/service/auth/src/share/types.rs View lib/service/auth/src/share/types.rs
@@ -1,9 +1,6 @@
11use serde::{Deserialize, Serialize};
22use utoipa::ToSchema;
33
4pub const DEFAULT_PAGE_SIZE: u64 = 20;
5pub const MAX_PAGE_SIZE: u64 = 100;
6
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
85pub struct UserPublic {
96 pub user_id: i64,
@@ -20,25 +17,4 @@
2017 pub version: String,
2118 pub title: String,
2219 pub content_md: String,
23}
24
25#[derive(Clone, Debug, Default, Deserialize, Serialize, ToSchema)]
26pub struct ListQuery {
27 pub page: Option<u64>,
28 pub page_size: Option<u64>,
29}
30
31impl ListQuery {
32 pub fn limit(&self) -> u64 {
33 self.page_size
34 .unwrap_or(DEFAULT_PAGE_SIZE)
35 .clamp(1, MAX_PAGE_SIZE)
36 }
37
38 pub fn offset(&self) -> u64 {
39 self.page
40 .unwrap_or(1)
41 .saturating_sub(1)
42 .saturating_mul(self.limit())
43 }
4420}
Modified lib/service/auth/src/errors.rs View lib/service/auth/src/errors.rs
@@ -171,6 +171,8 @@
171171 }
172172}
173173
174api::page_error_conversion!(AuthError, BadRequest);
175
174176impl AuthError {
175177 pub fn internal(message: impl Into<String>) -> Self {
176178 Self::Internal(message.into())
@@ -381,6 +383,7 @@
381383api::db_error_conversions!(AuthBusinessError);
382384cache::redis_error_conversion!(AuthBusinessError);
383385api::json_payload_conversion!(AuthBusinessError);
386api::page_error_conversion!(AuthBusinessError, BadRequest);
384387
385388api::api_error! {
386389 log: "auth",
Modified lib/service/chat/src/handlers/bookmarks.rs View lib/service/chat/src/handlers/bookmarks.rs
@@ -30,7 +30,7 @@
3030 let bookmarks = service.list_bookmarks(uid, room_id).await?;
3131 Ok(AppResponseData::ok(api::pagination::paginate(
3232 bookmarks, &page,
33 )))
33 )?))
3434}
3535
3636#[utoipa::path(
Modified lib/service/chat/src/handlers/dms.rs View lib/service/chat/src/handlers/dms.rs
@@ -52,7 +52,7 @@
5252) -> Result<AppResponseData<api::pagination::Page<DmDto>>, ApiError> {
5353 let uid = session::require_uid(&session)?;
5454 let dms = service.list_dms(uid).await?;
55 Ok(AppResponseData::ok(api::pagination::paginate(dms, &page)))
55 Ok(AppResponseData::ok(api::pagination::paginate(dms, &page)?))
5656}
5757
5858#[utoipa::path(
Modified lib/service/chat/src/handlers/drafts.rs View lib/service/chat/src/handlers/drafts.rs
@@ -80,7 +80,7 @@
8080 let drafts = service.list_my_drafts(uid).await?;
8181 Ok(AppResponseData::ok(api::pagination::paginate(
8282 drafts, &page,
83 )))
83 )?))
8484}
8585
8686#[utoipa::path(
Modified lib/service/chat/src/handlers/emojis.rs View lib/service/chat/src/handlers/emojis.rs
@@ -16,7 +16,7 @@
1616 description = "返回指定空间可用的自定义表情列表。",
1717 params(("space_name" = String, Path, description = "space name")),
1818 responses(
19 (status = 200, description = "emoji list", body = AppResponseData<api::pagination::Page<EmojiDto>>),
19 (status = 200, description = "emoji list", body = AppResponseData<Vec<EmojiDto>>),
2020 (status = 404, description = "not found", body = AppResponseData<NoData>),
2121 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
2222 )
Modified lib/service/chat/src/handlers/read_state.rs View lib/service/chat/src/handlers/read_state.rs
@@ -5,7 +5,7 @@
55use uuid::Uuid;
66
77use crate::errors::ApiError;
8use crate::services::read_state::{MarkReadCmd, ReadStateDto, SpaceReadStatePage};
8use crate::services::read_state::{MarkReadCmd, ReadStateDto, SpaceReadStates};
99use crate::services::service::ChatService;
1010
1111#[utoipa::path(
@@ -62,7 +62,7 @@
6262 description = "汇总当前用户在一个空间内可见房间的未读状态。",
6363 params(("space_name" = String, Path, description = "space name")),
6464 responses(
65 (status = 200, description = "space read states", body = AppResponseData<SpaceReadStatePage>),
65 (status = 200, description = "space read states", body = AppResponseData<SpaceReadStates>),
6666 (status = 404, description = "not found", body = AppResponseData<NoData>),
6767 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
6868 )
@@ -71,7 +71,7 @@
7171 State(service): State<ChatService>,
7272 session: Session,
7373 Path(space_name): Path<String>,
74) -> Result<AppResponseData<SpaceReadStatePage>, ApiError> {
74) -> Result<AppResponseData<SpaceReadStates>, ApiError> {
7575 let uid = session::require_uid(&session)?;
7676 let page = service.list_space_read_states(uid, &space_name).await?;
7777 Ok(AppResponseData::ok(page))
Modified lib/service/chat/src/handlers/threads.rs View lib/service/chat/src/handlers/threads.rs
@@ -83,7 +83,7 @@
8383 let threads = service.list_my_threads(uid).await?;
8484 Ok(AppResponseData::ok(api::pagination::paginate(
8585 threads, &page,
86 )))
86 )?))
8787}
8888
8989#[utoipa::path(
Modified lib/service/chat/src/handlers/webhooks.rs View lib/service/chat/src/handlers/webhooks.rs
@@ -57,7 +57,7 @@
5757) -> Result<AppResponseData<api::pagination::Page<WebhookDto>>, ApiError> {
5858 let uid = session::require_uid(&session)?;
5959 let rows = service.list_webhooks(uid, room_id).await?;
60 Ok(AppResponseData::ok(api::pagination::paginate(rows, &page)))
60 Ok(AppResponseData::ok(api::pagination::paginate(rows, &page)?))
6161}
6262
6363#[utoipa::path(
Modified lib/service/chat/src/services/messages.rs View lib/service/chat/src/services/messages.rs
@@ -132,13 +132,7 @@
132132
133133pub use social::MessageEmbed as MessageEmbedDto;
134134
135#[derive(Debug, Clone, Serialize, ToSchema)]
136pub struct MessagePage {
137 pub items: Vec<MessageDto>,
138 pub oldest_seq: Option<i64>,
139 pub newest_seq: Option<i64>,
140 pub has_more: bool,
141}
135pub type MessagePage = api::pagination::Page<MessageDto>;
142136
143137#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
144138pub struct SendMessageCmd {
@@ -185,10 +179,12 @@
185179}
186180
187181#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
182#[serde(deny_unknown_fields)]
188183pub struct ListMessageQuery {
189184 pub before_seq: Option<i64>,
190185 pub after_seq: Option<i64>,
191186 pub limit: Option<u64>,
187 pub offset: Option<u64>,
192188 pub thread_id: Option<Uuid>,
193189 pub author_id: Option<i64>,
194190}
@@ -878,12 +874,12 @@
878874 ) -> Result<MessagePage, ChatError> {
879875 let _ctx = require_room_readable(self, room_id, caller).await?;
880876
881 let (limit, _) = api::pagination::page_bounds(
877 let (limit, offset) = api::pagination::page_bounds(
882878 q.limit,
883 None,
879 q.offset,
884880 self.settings.chat.default_pagination,
885881 self.settings.chat.max_pagination,
886 );
882 )?;
887883
888884 let mut select = messages::Entity::find()
889885 .filter(messages::Column::RoomId.eq(room_id))
@@ -906,38 +902,35 @@
906902 select = select.filter(messages::Column::AuthorId.eq(author_id));
907903 }
908904
909 let (rows, has_more) = if let Some(after_seq) = q.after_seq {
905 let (rows, total) = if let Some(after_seq) = q.after_seq {
906 let select = select.filter(messages::Column::Seq.gt(after_seq));
907 let total = select.clone().count(self.database.read()).await?;
910908 let rows = select
911 .filter(messages::Column::Seq.gt(after_seq))
912909 .order_by_asc(messages::Column::Seq)
913 .limit(limit + 1)
910 .limit(limit)
911 .offset(offset)
914912 .all(self.database.read())
915913 .await?;
916 let has_more = rows.len() as u64 > limit;
917 let rows: Vec<_> = rows.into_iter().take(limit as usize).collect();
918 (rows, has_more)
914 (rows, total)
919915 } else {
920 let mut desc = select.order_by_desc(messages::Column::Seq).limit(limit + 1);
916 let mut select = select;
921917 if let Some(before_seq) = q.before_seq {
922 desc = desc.filter(messages::Column::Seq.lt(before_seq));
918 select = select.filter(messages::Column::Seq.lt(before_seq));
923919 }
924 let mut rows = desc.all(self.database.read()).await?;
925 let has_more = rows.len() as u64 > limit;
926 rows.truncate(limit as usize);
920 let total = select.clone().count(self.database.read()).await?;
921 let mut rows = select
922 .order_by_desc(messages::Column::Seq)
923 .limit(limit)
924 .offset(offset)
925 .all(self.database.read())
926 .await?;
927927 rows.reverse();
928 (rows, has_more)
928 (rows, total)
929929 };
930930
931 let oldest_seq = rows.first().map(|m| m.seq);
932 let newest_seq = rows.last().map(|m| m.seq);
933931 let items = self.build_message_dto(rows, caller).await?;
934932
935 Ok(MessagePage {
936 items,
937 oldest_seq,
938 newest_seq,
939 has_more,
940 })
933 Ok(MessagePage::new(items, total, limit, offset))
941934 }
942935
943936 pub async fn get_message(
Modified lib/service/chat/src/services/read_state.rs View lib/service/chat/src/services/read_state.rs
@@ -26,7 +26,7 @@
2626}
2727
2828#[derive(Debug, Clone, Serialize, ToSchema)]
29pub struct SpaceReadStatePage {
29pub struct SpaceReadStates {
3030 pub items: Vec<ReadStateDto>,
3131}
3232
@@ -151,7 +151,7 @@
151151 &self,
152152 caller: i64,
153153 space_name: &str,
154 ) -> Result<SpaceReadStatePage, ChatError> {
154 ) -> Result<SpaceReadStates, ChatError> {
155155 let space = lookup_space_by_name(self, space_name).await?;
156156 require_space_member(self, &space, caller).await?;
157157
@@ -177,7 +177,7 @@
177177 }
178178 items.push(self.get_read_state(caller, room.id).await?);
179179 }
180 Ok(SpaceReadStatePage { items })
180 Ok(SpaceReadStates { items })
181181 }
182182
183183 pub async fn mark_thread_read(
Modified lib/service/chat/src/services/threads.rs View lib/service/chat/src/services/threads.rs
@@ -253,13 +253,13 @@
253253 q.offset,
254254 self.settings.chat.default_pagination,
255255 self.settings.chat.max_pagination,
256 );
256 )?;
257257
258258 let mut select = threads::Entity::find().filter(threads::Column::RoomId.eq(room_id));
259259 if let Some(archived) = q.archived {
260260 select = select.filter(threads::Column::Archived.eq(archived));
261261 }
262 let total = select.clone().count(self.database.read()).await? as i64;
262 let total = select.clone().count(self.database.read()).await?;
263263 let rows = select
264264 .order_by_desc(threads::Column::LastReplyAt)
265265 .order_by_desc(threads::Column::CreatedAt)
Modified lib/service/chat/src/errors.rs View lib/service/chat/src/errors.rs
@@ -34,6 +34,7 @@
3434storage::storage_error_conversion!(ChatError);
3535cache::redis_error_conversion!(ChatError);
3636api::json_payload_conversion!(ChatError);
37api::page_error_conversion!(ChatError, BadRequest);
3738api::access_error_conversion!(ChatError, BadRequest);
3839
3940impl From<dir::Error> for ChatError {
Modified lib/service/chat/src/openapi.rs View lib/service/chat/src/openapi.rs
@@ -97,7 +97,6 @@
9797 api::pagination::Page<crate::services::drafts::DraftDto>,
9898 api::pagination::Page<crate::services::webhooks::WebhookDto>,
9999 api::pagination::Page<crate::services::bookmarks::BookmarkDto>,
100 api::pagination::Page<crate::services::emojis::EmojiDto>,
101100 api::pagination::Page<crate::services::messages::MessageDto>,
102101 api::pagination::Page<crate::services::rooms::RoomBriefDto>,
103102 AppResponseData<api::rest::NoData>,
@@ -112,7 +111,6 @@
112111 models::chat::message_reminders::ReminderStatus,
113112 models::room::rooms::RoomKind,
114113 crate::services::messages::MessageDto,
115 crate::services::messages::MessagePage,
116114 crate::services::messages::MessageEditDto,
117115 crate::services::messages::SendMessageCmd,
118116 crate::services::messages::UpdateMessageCmd,
@@ -135,7 +133,7 @@
135133 crate::services::reactions::ReactionUserDto,
136134 crate::services::reactions::ReactionCmd,
137135 crate::services::read_state::ReadStateDto,
138 crate::services::read_state::SpaceReadStatePage,
136 crate::services::read_state::SpaceReadStates,
139137 crate::services::read_state::MarkReadCmd,
140138 crate::handlers::read_state::MarkThreadReadCmd,
141139 crate::services::attachments::AttachmentDto,
Modified lib/service/git/src/handler/branches/list.rs View lib/service/git/src/handler/branches/list.rs
@@ -34,8 +34,8 @@
3434 .list_branches(&ctx)
3535 .await
3636 .map_err(GitApiError::from)?;
37 let total = all.len() as i64;
38 let (limit, offset) = page.page();
37 let total = all.len() as u64;
38 let (limit, offset) = page.bounds()?;
3939 let items = all
4040 .into_iter()
4141 .skip(offset as usize)
Modified lib/service/git/src/handler/commits/list.rs View lib/service/git/src/handler/commits/list.rs
@@ -5,7 +5,7 @@
55use crate::errors::{ApiError, GitApiError};
66use crate::handler;
77use crate::service::GitService;
8use crate::service::commits::{CommitsPageDto, ListCommitsCmd};
8use crate::service::commits::{CommitDto, ListCommitsCmd};
99
1010#[utoipa::path(
1111 get,
@@ -16,14 +16,14 @@
1616 crate::handler::RepoPath,
1717 ("revision" = Option<String>, Query, description = "start revision"),
1818 ("path" = Option<String>, Query, description = "path filter"),
19 ("max_count" = Option<u32>, Query, description = "max commits"),
20 ("skip" = Option<u32>, Query, description = "skip commits before returning (offset)"),
19 ("limit" = Option<u64>, Query, description = "max commits per page (1-100)"),
20 ("offset" = Option<u64>, Query, description = "skip commits before returning (max 5000)"),
2121 ("since" = Option<String>, Query, description = "since date"),
2222 ("until" = Option<String>, Query, description = "until date"),
2323 ("first_parent" = bool, Query, description = "first parent only"),
2424 ),
2525 responses(
26 (status = 200, description = "list commits", body = AppResponseData<CommitsPageDto>)
26 (status = 200, description = "list commits", body = AppResponseData<api::pagination::Page<CommitDto>>)
2727 )
2828)]
2929pub async fn list_commits(
@@ -31,8 +31,8 @@
3131 session: Session,
3232 Path(path): Path<crate::handler::RepoPath>,
3333 Query(cmd): Query<ListCommitsCmd>,
34) -> Result<AppResponseData<CommitsPageDto>, ApiError> {
35 let (ctx, viewer) = handler::resolve_reader(
34) -> Result<AppResponseData<api::pagination::Page<CommitDto>>, ApiError> {
35 let (ctx, _viewer) = handler::resolve_reader(
3636 &service,
3737 &session,
3838 path.space_name.clone(),
@@ -40,7 +40,7 @@
4040 )
4141 .await?;
4242 let out = service
43 .list_commits(&ctx, cmd, viewer)
43 .list_commits(&ctx, cmd)
4444 .await
4545 .map_err(GitApiError::from)?;
4646 Ok(AppResponseData::ok(out))
Modified lib/service/git/src/handler/html/commits.rs View lib/service/git/src/handler/html/commits.rs
@@ -20,7 +20,7 @@
2020 #[serde(default)]
2121 pub limit: Option<usize>,
2222 #[serde(default)]
23 pub skip: Option<usize>,
23 pub offset: Option<usize>,
2424}
2525
2626#[utoipa::path(
@@ -57,8 +57,8 @@
5757 } else {
5858 Some(PathBuf::from(clean_path.clone()))
5959 },
60 max_count: Some(limit as u32),
61 skip: q.skip.map(|s| s as u32),
60 limit: Some(limit as u64),
61 offset: q.offset.map(|s| s as u64),
6262 since: None,
6363 until: None,
6464 first_parent: false,
@@ -74,23 +74,24 @@
7474 return error::html_err(500, "internal");
7575 }
7676 };
77 let page = match service.list_commits(&repo_ctx, cmd, ctx.uid).await {
77 let page = match service.list_commits(&repo_ctx, cmd).await {
7878 Ok(p) => p,
7979 Err(e) => {
8080 tracing::warn!(error = %e, "commits list internal");
8181 return error::html_err(500, "internal");
8282 }
8383 };
84 let commits = page.commits;
84 let diff_view_style = service.user_diff_view_style(ctx.uid).await;
85 let commits = page.items;
8586
8687 let base =
8788 "git-commits w-full rounded-lg border border-zinc-200 bg-white overflow-hidden text-sm";
8889 let cls = archived_class(&ctx.dto, base);
8990 let space = ctx.space_name.clone();
9091 let repo = ctx.repo_name.clone();
91 let skip = page.skip as usize;
92 let skip = page.offset as usize;
9293 let older_skip = skip + limit;
93 let has_older = page.has_more;
94 let has_more = page.offset + (commits.len() as u64) < page.total;
9495
9596 let markup = html! {
9697 div class=(cls)
@@ -101,10 +102,10 @@
101102 data-path=(clean_path.as_str())
102103 data-count=(commits.len())
103104 data-limit=(page.limit)
104 data-skip=(page.skip)
105 data-skip=(page.offset)
105106 data-total=(page.total)
106 data-has-more=(if page.has_more { "true" } else { "false" })
107 data-diff-style=(page.diff_view_style)
107 data-has-more=(if has_more { "true" } else { "false" })
108 data-diff-style=(diff_view_style)
108109 data-archived=(if ctx.dto.is_archived { "true" } else { "false" }) {
109110 div class="flex items-center justify-between border-b border-zinc-200 bg-zinc-50 px-4 py-2 text-xs text-zinc-500 font-medium" {
110111 span { "Commit history" }
@@ -161,11 +162,11 @@
161162 }
162163 }
163164 }
164 @if page.total > page.limit.into() {
165 @if page.total > page.limit {
165166 div class="flex items-center justify-between border-t border-zinc-200 bg-zinc-50 px-4 py-2 text-xs" {
166167 @if skip > 0 {
167168 a class="rounded border border-zinc-200 bg-white px-3 py-1 font-medium text-zinc-700 hover:bg-zinc-100"
168 href=(format!("?revision={}&path={}&limit={}&skip={}", revision, clean_path, limit, skip.saturating_sub(limit))) {
169 href=(format!("?revision={}&path={}&limit={}&offset={}", revision, clean_path, limit, skip.saturating_sub(limit))) {
169170 "Newer"
170171 }
171172 } @else {
@@ -176,9 +177,9 @@
176177 span class="text-zinc-500 font-mono" {
177178 "page " (skip / limit + 1) " / " ((page.total as usize + limit - 1) / limit.max(1))
178179 }
179 @if has_older {
180 @if has_more {
180181 a class="rounded border border-zinc-200 bg-white px-3 py-1 font-medium text-zinc-700 hover:bg-zinc-100"
181 href=(format!("?revision={}&path={}&limit={}&skip={}", revision, clean_path, limit, older_skip)) {
182 href=(format!("?revision={}&path={}&limit={}&offset={}", revision, clean_path, limit, older_skip)) {
182183 "Older"
183184 }
184185 } @else {
Modified lib/service/git/src/handler/refs/branches.rs View lib/service/git/src/handler/refs/branches.rs
@@ -7,30 +7,22 @@
77use crate::service::GitService;
88use crate::service::refs::BranchInfoDto;
99
10#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
11pub struct RefsBranchesQuery {
12 #[serde(default)]
13 pub skip: Option<u64>,
14 #[serde(default)]
15 pub limit: Option<u64>,
16}
17
1810#[utoipa::path(
1911 get,
2012 path = "/repos/{space_name}/{repo_name}/refs/branches",
2113 tag = "refs",
2214 description = "返回分支引用摘要及保护标记,供内部 Git 流程分页使用。",
23 params(crate::handler::RepoPath, RefsBranchesQuery),
15 params(crate::handler::RepoPath, api::pagination::PageQuery),
2416 responses(
25 (status = 200, description = "list branch refs", body = AppResponseData<Vec<BranchInfoDto>>)
17 (status = 200, description = "list branch refs", body = AppResponseData<api::pagination::Page<BranchInfoDto>>)
2618 )
2719)]
2820pub async fn refs_branches(
2921 State(service): State<GitService>,
3022 session: Session,
3123 Path(path): Path<crate::handler::RepoPath>,
32 Query(query): Query<RefsBranchesQuery>,
33) -> Result<AppResponseData<Vec<BranchInfoDto>>, ApiError> {
24 Query(query): Query<api::pagination::PageQuery>,
25) -> Result<AppResponseData<api::pagination::Page<BranchInfoDto>>, ApiError> {
3426 let (ctx, _) = handler::resolve_reader(
3527 &service,
3628 &session,
@@ -38,8 +30,9 @@
3830 path.repo_name.clone(),
3931 )
4032 .await?;
33 let (limit, offset) = query.bounds()?;
4134 let out = service
42 .refs_branches(&ctx, query.skip.unwrap_or(0), query.limit.unwrap_or(0))
35 .refs_branches(&ctx, limit, offset)
4336 .await
4437 .map_err(GitApiError::from)?;
4538 Ok(AppResponseData::ok(out))
Modified lib/service/git/src/handler/repos/list.rs View lib/service/git/src/handler/repos/list.rs
@@ -4,7 +4,7 @@
44
55use crate::errors::{ApiError, GitApiError};
66use crate::service::GitService;
7use crate::service::repos::RepoSummaryDto;
7use crate::service::repos::{RepoListQuery, RepoSummaryDto};
88
99#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
1010pub struct SpacePath {
@@ -16,7 +16,7 @@
1616 path = "/repos/{space_name}",
1717 tag = "repos",
1818 description = "分页列出空间中当前调用者可见的仓库,并支持名称、时间或星数排序。",
19 params(SpacePath, api::pagination::PageQuery),
19 params(SpacePath, RepoListQuery),
2020 responses(
2121 (status = 200, description = "list repos", body = AppResponseData<api::pagination::Page<RepoSummaryDto>>)
2222 )
@@ -25,7 +25,7 @@
2525 State(service): State<GitService>,
2626 session: Session,
2727 Path(path): Path<SpacePath>,
28 Query(page): Query<api::pagination::PageQuery>,
28 Query(page): Query<RepoListQuery>,
2929) -> Result<AppResponseData<api::pagination::Page<RepoSummaryDto>>, ApiError> {
3030 let caller = session.uid();
3131 let out = service
Modified lib/service/git/src/handler/tags/list.rs View lib/service/git/src/handler/tags/list.rs
@@ -31,8 +31,8 @@
3131 )
3232 .await?;
3333 let all = service.list_tags(&ctx).await.map_err(GitApiError::from)?;
34 let total = all.len() as i64;
35 let (limit, offset) = page.page();
34 let total = all.len() as u64;
35 let (limit, offset) = page.bounds()?;
3636 let items = all
3737 .into_iter()
3838 .skip(offset as usize)
Modified lib/service/git/src/handler/licenses.rs View lib/service/git/src/handler/licenses.rs
@@ -1,4 +1,5 @@
11use api::http::{Json, Path, Query, State};
2use api::pagination::{Page, PageQuery};
23use api::rest::AppResponseData;
34use serde::Deserialize;
45use utoipa::IntoParams;
@@ -12,31 +13,21 @@
1213 pub name: String,
1314}
1415
15#[derive(Debug, Clone, Deserialize, IntoParams)]
16pub struct LicenseListQuery {
17 pub limit: Option<u64>,
18 pub offset: Option<u64>,
19}
20
2116#[utoipa::path(
2217 get,
2318 path = "/licenses",
2419 tag = "licenses",
2520 description = "分页列出系统登记的许可证。",
26 params(LicenseListQuery),
21 params(PageQuery),
2722 responses(
28 (status = 200, description = "list licenses", body = AppResponseData<Vec<LicenseDto>>)
23 (status = 200, description = "list licenses", body = AppResponseData<Page<LicenseDto>>)
2924 )
3025)]
3126pub async fn list_licenses(
3227 State(service): State<GitService>,
33 Query(q): Query<LicenseListQuery>,
34) -> Result<AppResponseData<Vec<LicenseDto>>, ApiError> {
35 let limit = q
36 .limit
37 .unwrap_or(api::pagination::DEFAULT_PAGE_LIMIT)
38 .clamp(1, api::pagination::MAX_PAGE_LIMIT);
39 let offset = q.offset.unwrap_or(0).clamp(0, api::pagination::MAX_OFFSET);
28 Query(q): Query<PageQuery>,
29) -> Result<AppResponseData<Page<LicenseDto>>, ApiError> {
30 let (limit, offset) = q.bounds()?;
4031 let out = service
4132 .list_licenses(limit, offset)
4233 .await
Modified lib/service/git/src/handler/stars.rs View lib/service/git/src/handler/stars.rs
@@ -15,12 +15,6 @@
1515 pub repo_name: String,
1616}
1717
18#[derive(Debug, Clone, Deserialize, IntoParams)]
19pub struct StarQuery {
20 pub limit: Option<u64>,
21 pub offset: Option<u64>,
22}
23
2418#[utoipa::path(
2519 put,
2620 path = "/repos/{space_name}/{repo_name}/star",
@@ -86,17 +80,17 @@
8680 path = "/repos/{space_name}/{repo_name}/stargazers",
8781 tag = "repos",
8882 description = "分页列出最近为仓库加星的用户。",
89 params(RepoPath, StarQuery),
83 params(RepoPath, api::pagination::PageQuery),
9084 responses(
91 (status = 200, description = "list stargazers", body = AppResponseData<Vec<StargazerDto>>)
85 (status = 200, description = "list stargazers", body = AppResponseData<api::pagination::Page<StargazerDto>>)
9286 )
9387)]
9488pub async fn list_stargazers(
9589 State(service): State<GitService>,
9690 session: Session,
9791 Path(path): Path<RepoPath>,
98 Query(q): Query<StarQuery>,
99) -> Result<AppResponseData<Vec<StargazerDto>>, ApiError> {
92 Query(q): Query<api::pagination::PageQuery>,
93) -> Result<AppResponseData<api::pagination::Page<StargazerDto>>, ApiError> {
10094 let ctx = resolve_reader(
10195 &service,
10296 &session,
@@ -105,8 +99,7 @@
10599 )
106100 .await?
107101 .0;
108 let limit = q.limit.unwrap_or(50).clamp(1, 100);
109 let offset = q.offset.unwrap_or(0).clamp(0, api::pagination::MAX_OFFSET);
102 let (limit, offset) = q.bounds()?;
110103 let out = service
111104 .list_stargazers(&ctx, limit, offset)
112105 .await
Modified lib/service/git/src/handler/topics.rs View lib/service/git/src/handler/topics.rs
@@ -14,12 +14,6 @@
1414 pub name: String,
1515}
1616
17#[derive(Debug, Clone, Deserialize, IntoParams)]
18pub struct TopicListQuery {
19 pub limit: Option<u64>,
20 pub offset: Option<u64>,
21}
22
2317#[derive(Debug, Clone, Deserialize, IntoParams, utoipa::ToSchema)]
2418pub struct RepoTopicPath {
2519 pub space_name: String,
@@ -58,17 +52,16 @@
5852 path = "/topics",
5953 tag = "topics",
6054 description = "分页列出系统全部标签。",
61 params(TopicListQuery),
55 params(api::pagination::PageQuery),
6256 responses(
63 (status = 200, description = "list topics", body = AppResponseData<Vec<TopicDto>>)
57 (status = 200, description = "list topics", body = AppResponseData<api::pagination::Page<TopicDto>>)
6458 )
6559)]
6660pub async fn list_topics(
6761 State(service): State<GitService>,
68 Query(q): Query<TopicListQuery>,
69) -> Result<AppResponseData<Vec<TopicDto>>, ApiError> {
70 let limit = q.limit.unwrap_or(50).clamp(1, 100);
71 let offset = q.offset.unwrap_or(0).clamp(0, api::pagination::MAX_OFFSET);
62 Query(q): Query<api::pagination::PageQuery>,
63) -> Result<AppResponseData<api::pagination::Page<TopicDto>>, ApiError> {
64 let (limit, offset) = q.bounds()?;
7265 let out = service
7366 .list_topics(limit, offset)
7467 .await
@@ -102,18 +95,17 @@
10295 path = "/topics/{name}/repos",
10396 tag = "topics",
10497 description = "分页列出关联某标签的仓库记录。",
105 params(TopicNamePath, TopicListQuery),
98 params(TopicNamePath, api::pagination::PageQuery),
10699 responses(
107 (status = 200, description = "repos using topic", body = AppResponseData<Vec<TopicRepoDto>>)
100 (status = 200, description = "repos using topic", body = AppResponseData<api::pagination::Page<TopicRepoDto>>)
108101 )
109102)]
110103pub async fn list_repos_by_topic(
111104 State(service): State<GitService>,
112105 Path(p): Path<TopicNamePath>,
113 Query(q): Query<TopicListQuery>,
114) -> Result<AppResponseData<Vec<TopicRepoDto>>, ApiError> {
115 let limit = q.limit.unwrap_or(50).clamp(1, 100);
116 let offset = q.offset.unwrap_or(0).clamp(0, api::pagination::MAX_OFFSET);
106 Query(q): Query<api::pagination::PageQuery>,
107) -> Result<AppResponseData<api::pagination::Page<TopicRepoDto>>, ApiError> {
108 let (limit, offset) = q.bounds()?;
117109 let out = service
118110 .list_repos_by_topic(&p.name, limit, offset)
119111 .await
Modified lib/service/git/src/handler/watches.rs View lib/service/git/src/handler/watches.rs
@@ -11,12 +11,19 @@
1111use crate::service::watches::{RepoWatchDto, UpsertWatchCmd, WatcherDto};
1212
1313#[derive(Debug, Clone, Deserialize, IntoParams)]
14#[serde(deny_unknown_fields)]
1415pub struct WatchQuery {
1516 pub limit: Option<u64>,
1617 pub offset: Option<u64>,
1718 pub mode: Option<WatchMode>,
1819}
1920
21impl WatchQuery {
22 fn bounds(&self) -> Result<(u64, u64), api::pagination::PageError> {
23 api::pagination::page_bounds(self.limit, self.offset, 50, 100)
24 }
25}
26
2027#[utoipa::path(
2128 put,
2229 path = "/repos/{space_name}/{repo_name}/watch",
@@ -56,7 +63,7 @@
5663 description = "分页列出仓库关注者,并可按关注模式过滤。",
5764 params(RepoPath, WatchQuery),
5865 responses(
59 (status = 200, description = "list watchers", body = AppResponseData<Vec<WatcherDto>>)
66 (status = 200, description = "list watchers", body = AppResponseData<api::pagination::Page<WatcherDto>>)
6067 )
6168)]
6269pub async fn list_watchers(
@@ -64,7 +71,7 @@
6471 session: Session,
6572 Path(path): Path<RepoPath>,
6673 Query(q): Query<WatchQuery>,
67) -> Result<AppResponseData<Vec<WatcherDto>>, ApiError> {
74) -> Result<AppResponseData<api::pagination::Page<WatcherDto>>, ApiError> {
6875 let ctx = resolve_reader(
6976 &service,
7077 &session,
@@ -73,8 +80,7 @@
7380 )
7481 .await?
7582 .0;
76 let limit = q.limit.unwrap_or(50).clamp(1, 100);
77 let offset = q.offset.unwrap_or(0).clamp(0, api::pagination::MAX_OFFSET);
83 let (limit, offset) = q.bounds()?;
7884 let out = service
7985 .list_watchers(&ctx, q.mode, limit, offset)
8086 .await
Modified lib/service/git/src/service/code_comments.rs View lib/service/git/src/service/code_comments.rs
@@ -56,6 +56,7 @@
5656}
5757
5858#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
59#[serde(deny_unknown_fields)]
5960pub struct ListCodeCommentsQuery {
6061 #[serde(default)]
6162 pub path: Option<String>,
@@ -75,8 +76,7 @@
7576 ctx: &GitContext,
7677 query: ListCodeCommentsQuery,
7778 ) -> Result<CodeCommentPage, GitError> {
78 let limit = query.limit.unwrap_or(50).clamp(1, 200);
79 let offset = query.offset.unwrap_or(0);
79 let (limit, offset) = api::pagination::page_bounds(query.limit, query.offset, 50, 200)?;
8080 let mut select =
8181 code_comment::Entity::find().filter(code_comment::Column::RepoId.eq(ctx.model.id));
8282 if let Some(path) = query
@@ -106,7 +106,7 @@
106106 select = select.filter(code_comment::Column::ResolvedAt.is_null());
107107 }
108108 }
109 let total = select.clone().count(self.db_read()).await? as i64;
109 let total = select.clone().count(self.db_read()).await?;
110110 let rows = select
111111 .order_by_asc(code_comment::Column::Path)
112112 .order_by_asc(code_comment::Column::Line)
Modified lib/service/git/src/service/commits.rs View lib/service/git/src/service/commits.rs
@@ -15,20 +15,23 @@
1515use crate::service::cache::{self, IMMUTABLE_TTL};
1616use crate::share::errors::{GitError, map_baby};
1717
18use api::pagination::Page;
19
1820fn default_revision() -> String {
1921 "HEAD".to_string()
2022}
2123
2224#[derive(Debug, Clone, Deserialize)]
25#[serde(deny_unknown_fields)]
2326pub struct ListCommitsCmd {
2427 #[serde(default = "default_revision")]
2528 pub revision: String,
2629 #[serde(default)]
2730 pub path: Option<PathBuf>,
2831 #[serde(default)]
29 pub max_count: Option<u32>,
32 pub limit: Option<u64>,
3033 #[serde(default)]
31 pub skip: Option<u32>,
34 pub offset: Option<u64>,
3235 #[serde(default)]
3336 pub since: Option<String>,
3437 #[serde(default)]
@@ -68,16 +71,6 @@
6871 }
6972}
7073
71#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
72pub struct CommitsPageDto {
73 pub commits: Vec<CommitDto>,
74 pub total: u64,
75 pub skip: u32,
76 pub limit: u32,
77 pub has_more: bool,
78 pub diff_view_style: String,
79}
80
8174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
8275#[serde(rename_all = "lowercase")]
8376pub enum CommitStatusDtoState {
@@ -172,11 +165,9 @@
172165 &self,
173166 ctx: &GitContext,
174167 cmd: ListCommitsCmd,
175 viewer: Option<i64>,
176 ) -> Result<CommitsPageDto, GitError> {
168 ) -> Result<Page<CommitDto>, GitError> {
177169 let baby = self.baby(&ctx);
178 let limit = cmd.max_count.unwrap_or(50);
179 let skip = cmd.skip.unwrap_or(0);
170 let (limit, offset) = api::pagination::page_bounds(cmd.limit, cmd.offset, 50, 100)?;
180171 let rev_key = if let Some((after, before)) = cmd.revision.split_once("..") {
181172 let a = baby.rev_parse(after).await.map_err(map_baby)?;
182173 let b = baby.rev_parse(before).await.map_err(map_baby)?;
@@ -197,7 +188,7 @@
197188 "repo.{}.commits_page.{}:{}:{}:{}:{}:{}:{}",
198189 ctx.model.id,
199190 rev_oid,
200 skip,
191 offset,
201192 limit,
202193 path_str,
203194 cmd.since.as_deref().unwrap_or(""),
@@ -207,8 +198,8 @@
207198 let opts = ListCommitsOptions {
208199 revision: cmd.revision,
209200 path: cmd.path,
210 max_count: Some(limit),
211 skip: Some(skip),
201 max_count: Some(limit as u32),
202 skip: Some(offset as u32),
212203 since: cmd.since,
213204 until: cmd.until,
214205 first_parent: cmd.first_parent,
@@ -226,16 +217,7 @@
226217 Ok((commits, total))
227218 })
228219 .await?;
229 let has_more = (skip as u64 + commits.len() as u64) < total;
230 let diff_view_style = self.user_diff_view_style(viewer).await;
231 Ok(CommitsPageDto {
232 commits,
233 total,
234 skip,
235 limit,
236 has_more,
237 diff_view_style,
238 })
220 Ok(Page::new(commits, total, limit, offset))
239221 }
240222
241223 pub async fn user_diff_view_style(&self, viewer: Option<i64>) -> String {
Modified lib/service/git/src/service/licenses.rs View lib/service/git/src/service/licenses.rs
@@ -1,5 +1,6 @@
1use api::pagination::Page;
12use models::repo::repo_license;
2use sea_orm::{ActiveModelTrait, EntityTrait, QueryOrder, QuerySelect, Set};
3use sea_orm::{ActiveModelTrait, EntityTrait, PaginatorTrait, QueryOrder, QuerySelect, Set};
34use serde::{Deserialize, Serialize};
45use time::OffsetDateTime;
56use utoipa::ToSchema;
@@ -53,14 +54,16 @@
5354 &self,
5455 limit: u64,
5556 offset: u64,
56 ) -> Result<Vec<LicenseDto>, GitError> {
57 ) -> Result<Page<LicenseDto>, GitError> {
58 let total = repo_license::Entity::find().count(self.db_read()).await?;
5759 let rows = repo_license::Entity::find()
5860 .order_by_asc(repo_license::Column::Name)
5961 .limit(limit)
6062 .offset(offset)
6163 .all(self.db_read())
6264 .await?;
63 Ok(rows.into_iter().map(Into::into).collect())
65 let items = rows.into_iter().map(Into::into).collect();
66 Ok(Page::new(items, total, limit, offset))
6467 }
6568
6669 pub async fn upsert_license(
Modified lib/service/git/src/service/refs.rs View lib/service/git/src/service/refs.rs
@@ -1,6 +1,8 @@
11use crate::rpcgit::refs::types::{ListRefsOptions, RefInfo};
22use serde::Serialize;
33
4use api::pagination::Page;
5
46use crate::context::facade::GitContext;
57use crate::service::GitService;
68use crate::share::errors::{GitError, map_baby};
@@ -92,9 +94,9 @@
9294 pub async fn refs_branches(
9395 &self,
9496 ctx: &GitContext,
95 skip: u64,
9697 limit: u64,
97 ) -> Result<Vec<BranchInfoDto>, GitError> {
98 offset: u64,
99 ) -> Result<Page<BranchInfoDto>, GitError> {
98100 let baby = self.baby(&ctx);
99101 let branches = baby
100102 .list_branches(crate::rpcgit::branch::ListBranchesOptions::default())
@@ -111,13 +113,13 @@
111113 })
112114 .collect();
113115 out.sort_by(|a, b| a.name.cmp(&b.name));
114 if let Some(start) = skip.checked_sub(1) {
115 out = out.into_iter().skip(start as usize).collect();
116 }
117 if limit > 0 {
118 out.truncate(limit as usize);
119 }
120 Ok(out)
116 let total = out.len() as u64;
117 let items = out
118 .into_iter()
119 .skip(offset as usize)
120 .take(limit as usize)
121 .collect();
122 Ok(Page::new(items, total, limit, offset))
121123 }
122124
123125 pub async fn refs_update(
Modified lib/service/git/src/service/releases.rs View lib/service/git/src/service/releases.rs
@@ -38,6 +38,7 @@
3838pub type ReleasePage = Page<ReleaseDto>;
3939
4040#[derive(Debug, Clone, Deserialize, IntoParams)]
41#[serde(deny_unknown_fields)]
4142pub struct ListReleasesQuery {
4243 #[serde(default)]
4344 pub limit: Option<u64>,
@@ -80,14 +81,13 @@
8081 ctx: &GitContext,
8182 query: ListReleasesQuery,
8283 ) -> Result<ReleasePage, GitError> {
83 let limit = query.limit.unwrap_or(20).clamp(1, 100);
84 let offset = query.offset.unwrap_or(0);
84 let (limit, offset) = api::pagination::page_bounds(query.limit, query.offset, 20, 100)?;
8585 let mut select =
8686 repo_release::Entity::find().filter(repo_release::Column::RepoId.eq(ctx.model.id));
8787 if let Some(draft) = query.draft {
8888 select = select.filter(repo_release::Column::IsDraft.eq(draft));
8989 }
90 let total = select.clone().count(self.db_read()).await? as i64;
90 let total = select.clone().count(self.db_read()).await?;
9191 let rows = select
9292 .order_by_desc(repo_release::Column::CreatedAt)
9393 .offset(offset)
Modified lib/service/git/src/service/repos.rs View lib/service/git/src/service/repos.rs
@@ -15,8 +15,46 @@
1515use crate::share::errors::{GitError, map_baby};
1616use crate::share::validate::{validate_branch_name, validate_repo_name};
1717
18use api::pagination::SortOrder;
19
1820pub const DEFAULT_BRANCH: &str = "main";
1921
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, utoipa::ToSchema)]
23#[serde(rename_all = "snake_case")]
24pub enum RepoSort {
25 CreatedAt,
26 UpdatedAt,
27 Stars,
28 Name,
29}
30
31impl RepoSort {
32 fn default_order(self) -> SortOrder {
33 match self {
34 Self::CreatedAt | Self::UpdatedAt | Self::Name => SortOrder::Asc,
35 Self::Stars => SortOrder::Desc,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
41#[serde(deny_unknown_fields)]
42pub struct RepoListQuery {
43 pub limit: Option<u64>,
44 pub offset: Option<u64>,
45 pub sort: Option<RepoSort>,
46 pub order: Option<SortOrder>,
47}
48
49impl RepoListQuery {
50 fn page(&self) -> api::pagination::PageQuery {
51 api::pagination::PageQuery {
52 limit: self.limit,
53 offset: self.offset,
54 }
55 }
56}
57
2058/// 仓库页签中可由设置重排序的中间页签(代码固定最前、设置固定最后)。
2159pub const REORDERABLE_REPO_TABS: [&str; 4] = ["commits", "pulls", "branches", "releases"];
2260
@@ -457,8 +495,9 @@
457495 &self,
458496 space_name: String,
459497 caller: Option<i64>,
460 page: api::pagination::PageQuery,
498 q: RepoListQuery,
461499 ) -> Result<api::pagination::Page<RepoSummaryDto>, GitError> {
500 let (limit, offset) = q.page().bounds()?;
462501 let space = self.lookup_space(&space_name).await?;
463502 let dirs = dirs::Entity::find()
464503 .filter(dirs::Column::SpaceId.eq(space.id()))
@@ -467,13 +506,37 @@
467506 .await?;
468507 let dir_ids: Vec<Uuid> = dirs.iter().map(|d| d.id).collect();
469508 let mut sel = repos::Entity::find().filter(repos::Column::DirId.is_in(dir_ids));
470 sel = match page.sort.as_deref() {
471 Some("created_at") => sel.order_by_asc(repos::Column::CreatedAt),
472 Some("-updated_at") => sel.order_by_desc(repos::Column::UpdatedAt),
473 Some("updated_at") => sel.order_by_asc(repos::Column::UpdatedAt),
474 Some("stars") => sel.order_by_desc(repos::Column::NumsStar),
475 Some("name") => sel.order_by_asc(repos::Column::Name),
476 _ => sel.order_by_asc(repos::Column::CreatedAt),
509 let sort = q.sort.unwrap_or(RepoSort::CreatedAt);
510 let descending = q.order.unwrap_or(sort.default_order()) == SortOrder::Desc;
511 sel = match sort {
512 RepoSort::CreatedAt => {
513 if descending {
514 sel.order_by_desc(repos::Column::CreatedAt)
515 } else {
516 sel.order_by_asc(repos::Column::CreatedAt)
517 }
518 }
519 RepoSort::UpdatedAt => {
520 if descending {
521 sel.order_by_desc(repos::Column::UpdatedAt)
522 } else {
523 sel.order_by_asc(repos::Column::UpdatedAt)
524 }
525 }
526 RepoSort::Stars => {
527 if descending {
528 sel.order_by_desc(repos::Column::NumsStar)
529 } else {
530 sel.order_by_asc(repos::Column::NumsStar)
531 }
532 }
533 RepoSort::Name => {
534 if descending {
535 sel.order_by_desc(repos::Column::Name)
536 } else {
537 sel.order_by_asc(repos::Column::Name)
538 }
539 }
477540 };
478541 let rows = sel.all(self.db_read()).await?;
479542
@@ -495,19 +558,13 @@
495558 .filter(|r| !r.is_private || owner_view || accessible.contains(&r.id))
496559 .map(RepoSummaryDto::from)
497560 .collect();
498 let total = visible.len() as i64;
499 let (limit, offset) = page.page();
561 let total = visible.len() as u64;
500562 let items = visible
501563 .into_iter()
502564 .skip(offset as usize)
503565 .take(limit as usize)
504566 .collect();
505 Ok(api::pagination::Page {
506 items,
507 total,
508 limit,
509 offset,
510 })
567 Ok(api::pagination::Page::new(items, total, limit, offset))
511568 }
512569
513570 pub async fn update_repo(
Modified lib/service/git/src/service/stars.rs View lib/service/git/src/service/stars.rs
@@ -1,7 +1,8 @@
1use api::pagination::Page;
12use models::repo::{repo_star, repos};
23use sea_orm::{
3 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
4 TransactionTrait, Unchanged,
4 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
5 QuerySelect, Set, TransactionTrait, Unchanged,
56};
67use serde::{Deserialize, Serialize};
78use time::OffsetDateTime;
@@ -120,20 +121,18 @@
120121 ctx: &GitContext,
121122 limit: u64,
122123 offset: u64,
123 ) -> Result<Vec<StargazerDto>, GitError> {
124 let rows = repo_star::Entity::find()
125 .filter(repo_star::Column::RepoId.eq(ctx.model.id))
124 ) -> Result<Page<StargazerDto>, GitError> {
125 let select = repo_star::Entity::find().filter(repo_star::Column::RepoId.eq(ctx.model.id));
126 let total = select.clone().count(self.db_read()).await?;
127 let rows = select
126128 .order_by_desc(repo_star::Column::CreatedAt)
127129 .limit(limit)
128130 .offset(offset)
129131 .all(self.db_read())
130132 .await?;
131 if rows.is_empty() {
132 return Ok(Vec::new());
133 }
134133 let user_ids: Vec<i64> = rows.iter().map(|r| r.user_id).collect();
135134 let by_id = auth::users::load_public_users(&self.accounts, &user_ids).await?;
136 Ok(rows
135 let items = rows
137136 .into_iter()
138137 .filter_map(|row| {
139138 by_id.get(&row.user_id).map(|u| StargazerDto {
@@ -148,6 +147,7 @@
148147 starred_at: row.created_at,
149148 })
150149 })
151 .collect())
150 .collect();
151 Ok(Page::new(items, total, limit, offset))
152152 }
153153}
Modified lib/service/git/src/service/topics.rs View lib/service/git/src/service/topics.rs
@@ -1,6 +1,8 @@
1use api::pagination::Page;
12use models::repo::{repo_relation_topic, repo_topic, repos};
23use sea_orm::{
3 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
4 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
5 QuerySelect, Set,
46};
57use serde::{Deserialize, Serialize};
68use time::OffsetDateTime;
@@ -78,14 +80,16 @@
7880 Ok(row.into())
7981 }
8082
81 pub async fn list_topics(&self, limit: u64, offset: u64) -> Result<Vec<TopicDto>, GitError> {
83 pub async fn list_topics(&self, limit: u64, offset: u64) -> Result<Page<TopicDto>, GitError> {
84 let total = repo_topic::Entity::find().count(self.db_read()).await?;
8285 let rows = repo_topic::Entity::find()
8386 .order_by_asc(repo_topic::Column::Name)
8487 .limit(limit)
8588 .offset(offset)
8689 .all(self.db_read())
8790 .await?;
88 Ok(rows.into_iter().map(Into::into).collect())
91 let items = rows.into_iter().map(Into::into).collect();
92 Ok(Page::new(items, total, limit, offset))
8993 }
9094
9195 pub async fn add_topic_to_repo(
@@ -166,27 +170,26 @@
166170 topic_name: &str,
167171 limit: u64,
168172 offset: u64,
169 ) -> Result<Vec<TopicRepoDto>, GitError> {
173 ) -> Result<Page<TopicRepoDto>, GitError> {
170174 let topic = repo_topic::Entity::find()
171175 .filter(repo_topic::Column::Name.eq(topic_name))
172176 .one(self.db_read())
173177 .await?
174178 .ok_or(GitError::NotFound)?;
175 let relations = repo_relation_topic::Entity::find()
176 .filter(repo_relation_topic::Column::TopicId.eq(topic.id))
179 let select = repo_relation_topic::Entity::find()
180 .filter(repo_relation_topic::Column::TopicId.eq(topic.id));
181 let total = select.clone().count(self.db_read()).await?;
182 let relations = select
177183 .limit(limit)
178184 .offset(offset)
179185 .all(self.db_read())
180186 .await?;
181 if relations.is_empty() {
182 return Ok(Vec::new());
183 }
184187 let repo_ids: Vec<Uuid> = relations.iter().map(|r| r.repo_id).collect();
185188 let rows = repos::Entity::find()
186189 .filter(repos::Column::Id.is_in(repo_ids))
187190 .all(self.db_read())
188191 .await?;
189 Ok(rows
192 let items = rows
190193 .into_iter()
191194 .map(|r| TopicRepoDto {
192195 repo_id: r.id,
@@ -194,6 +197,7 @@
194197 space_name: String::new(),
195198 is_private: r.is_private,
196199 })
197 .collect())
200 .collect();
201 Ok(Page::new(items, total, limit, offset))
198202 }
199203}
Modified lib/service/git/src/service/watches.rs View lib/service/git/src/service/watches.rs
@@ -1,8 +1,9 @@
1use api::pagination::Page;
12use models::common::watch_mode::WatchMode;
23use models::repo::{repo_watch, repos};
34use sea_orm::{
4 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
5 TransactionTrait, Unchanged,
5 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
6 QuerySelect, Set, TransactionTrait, Unchanged,
67};
78use serde::{Deserialize, Serialize};
89use time::OffsetDateTime;
@@ -114,22 +115,22 @@
114115 mode: Option<WatchMode>,
115116 limit: u64,
116117 offset: u64,
117 ) -> Result<Vec<WatcherDto>, GitError> {
118 let mut q = repo_watch::Entity::find()
119 .filter(repo_watch::Column::RepoId.eq(ctx.model.id))
118 ) -> Result<Page<WatcherDto>, GitError> {
119 let mut select =
120 repo_watch::Entity::find().filter(repo_watch::Column::RepoId.eq(ctx.model.id));
121 if let Some(m) = mode {
122 select = select.filter(repo_watch::Column::Mode.eq(m));
123 }
124 let total = select.clone().count(self.db_read()).await?;
125 let rows = select
120126 .order_by_desc(repo_watch::Column::CreatedAt)
121127 .limit(limit)
122 .offset(offset);
123 if let Some(m) = mode {
124 q = q.filter(repo_watch::Column::Mode.eq(m));
125 }
126 let rows = q.all(self.db_read()).await?;
127 if rows.is_empty() {
128 return Ok(Vec::new());
129 }
128 .offset(offset)
129 .all(self.db_read())
130 .await?;
130131 let user_ids: Vec<i64> = rows.iter().map(|r| r.user_id).collect();
131132 let by_id = auth::users::load_public_users(&self.accounts, &user_ids).await?;
132 Ok(rows
133 let items = rows
133134 .into_iter()
134135 .filter_map(|row| {
135136 by_id.get(&row.user_id).map(|u| WatcherDto {
@@ -145,6 +146,7 @@
145146 watching_since: row.created_at,
146147 })
147148 })
148 .collect())
149 .collect();
150 Ok(Page::new(items, total, limit, offset))
149151 }
150152}
Modified lib/service/git/src/share/errors.rs View lib/service/git/src/share/errors.rs
@@ -24,6 +24,7 @@
2424
2525auth::auth_error_conversion!(GitError);
2626api::access_error_conversion!(GitError, InvalidArgument);
27api::page_error_conversion!(GitError, InvalidArgument);
2728
2829impl From<sea_orm::DbErr> for GitError {
2930 fn from(err: sea_orm::DbErr) -> Self {
Modified lib/service/git/src/errors.rs View lib/service/git/src/errors.rs
@@ -73,6 +73,18 @@
7373 }
7474}
7575
76impl From<api::pagination::PageError> for GitApiError {
77 fn from(error: api::pagination::PageError) -> Self {
78 Self::BadRequest(error.to_string())
79 }
80}
81
82impl From<api::pagination::PageError> for ApiError {
83 fn from(error: api::pagination::PageError) -> Self {
84 Self::BadRequest(Box::new(AppResponseData::err(400, error.to_string())))
85 }
86}
87
7688fn is_unique_violation(err: &sea_orm::DbErr) -> bool {
7789 let msg = err.to_string().to_lowercase();
7890 msg.contains("unique") || msg.contains("duplicate") || msg.contains("23505")
Modified lib/service/git/src/openapi.rs View lib/service/git/src/openapi.rs
@@ -111,9 +111,18 @@
111111 ),
112112 components(schemas(
113113 api::pagination::PageQuery,
114 api::pagination::SortOrder,
115 crate::service::repos::RepoSort,
114116 api::pagination::Page<crate::service::repos::RepoSummaryDto>,
115117 api::pagination::Page<crate::service::branches::BranchDto>,
116118 api::pagination::Page<crate::service::tags::TagDto>,
119 api::pagination::Page<crate::service::commits::CommitDto>,
120 api::pagination::Page<crate::service::licenses::LicenseDto>,
121 api::pagination::Page<crate::service::stars::StargazerDto>,
122 api::pagination::Page<crate::service::watches::WatcherDto>,
123 api::pagination::Page<crate::service::topics::TopicDto>,
124 api::pagination::Page<crate::service::topics::TopicRepoDto>,
125 api::pagination::Page<crate::service::refs::BranchInfoDto>,
117126 api::rest::NoData,
118127 crate::service::hooks::HookEventKind,
119128 models::common::watch_mode::WatchMode,
@@ -145,7 +154,7 @@
145154 crate::service::protection::UpdateTagPolicyCmd,
146155 crate::service::commits::CommitStatusDto,
147156 crate::service::commits::CommitStatusDtoState,
148 crate::service::commits::CommitsPageDto,
157 crate::service::commits::CommitDto,
149158 crate::service::commits::SetCommitStatusCmd,
150159 crate::service::conflicts::ConflictDetailDto,
151160 crate::service::conflicts::MergeStatusDto,
Modified lib/service/issues/src/services/boards.rs View lib/service/issues/src/services/boards.rs
@@ -59,6 +59,7 @@
5959}
6060
6161#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
62#[serde(deny_unknown_fields)]
6263pub struct ListBoardQuery {
6364 pub dir_id: Option<Uuid>,
6465 pub q: Option<String>,
@@ -274,7 +275,7 @@
274275 ) -> Result<BoardPage, IssueError> {
275276 let space = space_context(self, caller, space_name).await?;
276277
277 let (limit, offset) = self.paginate(q.limit, q.offset);
278 let (limit, offset) = self.paginate(q.limit, q.offset)?;
278279
279280 let dirs_in_space = dirs::Entity::find()
280281 .filter(dirs::Column::SpaceId.eq(Some(space.id())))
@@ -321,7 +322,7 @@
321322
322323 select = select.order_by_desc(issues::Column::CreatedAt);
323324
324 let total = select.clone().count(self.database.read()).await? as i64;
325 let total = select.clone().count(self.database.read()).await?;
325326 let rows = select
326327 .limit(limit)
327328 .offset(offset)
Modified lib/service/issues/src/services/comments.rs View lib/service/issues/src/services/comments.rs
@@ -18,6 +18,8 @@
1818use crate::share::authz;
1919use crate::share::lookup::{issue_context, require_issue_access};
2020
21use api::pagination::SortOrder;
22
2123#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
2224pub struct CommentDto {
2325 pub id: Uuid,
@@ -50,12 +52,30 @@
5052 pub content: String,
5153}
5254
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
56#[serde(rename_all = "snake_case")]
57pub enum CommentSort {
58 CreatedAt,
59 UpdatedAt,
60}
61
62impl CommentSort {
63 fn default_order(self) -> SortOrder {
64 match self {
65 Self::CreatedAt => SortOrder::Asc,
66 Self::UpdatedAt => SortOrder::Desc,
67 }
68 }
69}
70
5371#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
72#[serde(deny_unknown_fields)]
5473pub struct ListCommentQuery {
5574 pub parent_id: Option<Uuid>,
5675 pub limit: Option<u64>,
5776 pub offset: Option<u64>,
58 pub sort: Option<String>,
77 pub sort: Option<CommentSort>,
78 pub order: Option<SortOrder>,
5979}
6080
6181#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
@@ -295,7 +315,7 @@
295315 ) -> Result<CommentPage, IssueError> {
296316 require_issue_access(self, caller, space_name, board_id, issue_id).await?;
297317
298 let (limit, offset) = self.paginate(q.limit, q.offset);
318 let (limit, offset) = self.paginate(q.limit, q.offset)?;
299319
300320 let mut select =
301321 comment::Entity::find().filter(comment::Column::IssueId.eq(Some(issue_id)));
@@ -304,13 +324,26 @@
304324 select = select.filter(comment::Column::ParentId.eq(Some(pid)));
305325 }
306326
307 match q.sort.as_deref() {
308 Some("-created_at") => select = select.order_by_desc(comment::Column::CreatedAt),
309 Some("updated_at") => select = select.order_by_desc(comment::Column::UpdatedAt),
310 _ => select = select.order_by_asc(comment::Column::CreatedAt),
327 let sort = q.sort.unwrap_or(CommentSort::CreatedAt);
328 let descending = q.order.unwrap_or(sort.default_order()) == SortOrder::Desc;
329 match sort {
330 CommentSort::UpdatedAt => {
331 if descending {
332 select = select.order_by_desc(comment::Column::UpdatedAt);
333 } else {
334 select = select.order_by_asc(comment::Column::UpdatedAt);
335 }
336 }
337 CommentSort::CreatedAt => {
338 if descending {
339 select = select.order_by_desc(comment::Column::CreatedAt);
340 } else {
341 select = select.order_by_asc(comment::Column::CreatedAt);
342 }
343 }
311344 }
312345
313 let total = select.clone().count(self.database.read()).await? as i64;
346 let total = select.clone().count(self.database.read()).await?;
314347 let rows = select
315348 .limit(limit)
316349 .offset(offset)
Modified lib/service/issues/src/services/content_history.rs View lib/service/issues/src/services/content_history.rs
@@ -25,6 +25,7 @@
2525pub type ContentHistoryPage = api::pagination::Page<ContentHistoryDto>;
2626
2727#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
28#[serde(deny_unknown_fields)]
2829pub struct ListContentHistoryQuery {
2930 pub comment_id: Option<Uuid>,
3031 pub limit: Option<u64>,
@@ -49,7 +50,7 @@
4950 ) -> Result<ContentHistoryPage, IssueError> {
5051 lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
5152
52 let (limit, offset) = self.paginate(q.limit, q.offset);
53 let (limit, offset) = self.paginate(q.limit, q.offset)?;
5354
5455 let mut select = issue_content_history::Entity::find()
5556 .filter(issue_content_history::Column::IssueId.eq(issue_id));
@@ -60,7 +61,7 @@
6061
6162 select = select.order_by_desc(issue_content_history::Column::EditedAt);
6263
63 let total = select.clone().count(self.database.read()).await? as i64;
64 let total = select.clone().count(self.database.read()).await?;
6465 let rows = select
6566 .limit(limit)
6667 .offset(offset)
Modified lib/service/issues/src/services/events.rs View lib/service/issues/src/services/events.rs
@@ -29,6 +29,7 @@
2929pub type EventPage = api::pagination::Page<EventDto>;
3030
3131#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
32#[serde(deny_unknown_fields)]
3233pub struct ListEventQuery {
3334 pub limit: Option<u64>,
3435 pub offset: Option<u64>,
@@ -52,13 +53,13 @@
5253 ) -> Result<EventPage, IssueError> {
5354 lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
5455
55 let (limit, offset) = self.paginate(q.limit, q.offset);
56 let (limit, offset) = self.paginate(q.limit, q.offset)?;
5657
5758 let select = issue_event_line::Entity::find()
5859 .filter(issue_event_line::Column::IssueId.eq(Some(issue_id)))
5960 .order_by_desc(issue_event_line::Column::CreatedAt);
6061
61 let total = select.clone().count(self.database.read()).await? as i64;
62 let total = select.clone().count(self.database.read()).await?;
6263 let rows = select
6364 .limit(limit)
6465 .offset(offset)
Modified lib/service/issues/src/services/issue_users.rs View lib/service/issues/src/services/issue_users.rs
@@ -32,6 +32,7 @@
3232}
3333
3434#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
35#[serde(deny_unknown_fields)]
3536pub struct ListIssueUserQuery {
3637 pub limit: Option<u64>,
3738 pub offset: Option<u64>,
@@ -56,13 +57,13 @@
5657 ) -> Result<IssueUserPage, IssueError> {
5758 lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
5859
59 let (limit, offset) = self.paginate(q.limit, q.offset);
60 let (limit, offset) = self.paginate(q.limit, q.offset)?;
6061
6162 let select = issue_user::Entity::find()
6263 .filter(issue_user::Column::IssueId.eq(issue_id))
6364 .order_by_desc(issue_user::Column::CreatedAt);
6465
65 let total = select.clone().count(self.database.read()).await? as i64;
66 let total = select.clone().count(self.database.read()).await?;
6667 let rows = select
6768 .limit(limit)
6869 .offset(offset)
Modified lib/service/issues/src/services/issues.rs View lib/service/issues/src/services/issues.rs
@@ -24,6 +24,8 @@
2424use crate::share::authz;
2525use crate::share::lookup::{board_context, issue_context, lookup_issue_by_ref};
2626
27use api::pagination::SortOrder;
28
2729#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
2830#[serde(rename_all = "snake_case")]
2931pub enum IssueState {
@@ -120,7 +122,27 @@
120122 pub label_ids: Option<Vec<Uuid>>,
121123}
122124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
126#[serde(rename_all = "snake_case")]
127pub enum IssueSort {
128 CreatedAt,
129 UpdatedAt,
130 DueAt,
131 Priority,
132 Number,
133}
134
135impl IssueSort {
136 fn default_order(self) -> SortOrder {
137 match self {
138 Self::CreatedAt | Self::UpdatedAt | Self::Priority => SortOrder::Desc,
139 Self::DueAt | Self::Number => SortOrder::Asc,
140 }
141 }
142}
143
123144#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
145#[serde(deny_unknown_fields)]
124146pub struct ListIssueQuery {
125147 pub state: Option<IssueState>,
126148 pub author_id: Option<i64>,
@@ -133,7 +155,8 @@
133155 pub assignee_id: Option<String>,
134156 /// One priority or a comma-separated list (`high,urgent`).
135157 pub priority: Option<String>,
136 pub sort: Option<String>,
158 pub sort: Option<IssueSort>,
159 pub order: Option<SortOrder>,
137160 pub q: Option<String>,
138161 pub limit: Option<u64>,
139162 pub offset: Option<u64>,
@@ -675,7 +698,7 @@
675698 ) -> Result<IssuePage, IssueError> {
676699 let (_, board) = board_context(self, caller, space_name, board_id).await?;
677700
678 let (limit, offset) = self.paginate(q.limit, q.offset);
701 let (limit, offset) = self.paginate(q.limit, q.offset)?;
679702
680703 let mut select =
681704 issue_item::Entity::find().filter(issue_item::Column::CollectionId.eq(board.id));
@@ -746,15 +769,47 @@
746769 select = select.filter(issue_item::Column::Priority.is_in(priorities));
747770 }
748771
749 match q.sort.as_deref() {
750 Some("updated_at") => select = select.order_by_desc(issue_item::Column::UpdatedAt),
751 Some("due_at") => select = select.order_by_asc(issue_item::Column::DueAt),
752 Some("priority") => select = select.order_by_desc(issue_item::Column::Priority),
753 Some("number") => select = select.order_by_asc(issue_item::Column::AutoincrementId),
754 _ => select = select.order_by_desc(issue_item::Column::CreatedAt),
755 }
772 let sort = q.sort.unwrap_or(IssueSort::CreatedAt);
773 let descending = q.order.unwrap_or(sort.default_order()) == SortOrder::Desc;
774 select = match sort {
775 IssueSort::UpdatedAt => {
776 if descending {
777 select.order_by_desc(issue_item::Column::UpdatedAt)
778 } else {
779 select.order_by_asc(issue_item::Column::UpdatedAt)
780 }
781 }
782 IssueSort::DueAt => {
783 if descending {
784 select.order_by_desc(issue_item::Column::DueAt)
785 } else {
786 select.order_by_asc(issue_item::Column::DueAt)
787 }
788 }
789 IssueSort::Priority => {
790 if descending {
791 select.order_by_desc(issue_item::Column::Priority)
792 } else {
793 select.order_by_asc(issue_item::Column::Priority)
794 }
795 }
796 IssueSort::Number => {
797 if descending {
798 select.order_by_desc(issue_item::Column::AutoincrementId)
799 } else {
800 select.order_by_asc(issue_item::Column::AutoincrementId)
801 }
802 }
803 IssueSort::CreatedAt => {
804 if descending {
805 select.order_by_desc(issue_item::Column::CreatedAt)
806 } else {
807 select.order_by_asc(issue_item::Column::CreatedAt)
808 }
809 }
810 };
756811
757 let total = select.clone().count(self.database.read()).await? as i64;
812 let total = select.clone().count(self.database.read()).await?;
758813 let rows = select
759814 .limit(limit)
760815 .offset(offset)
Modified lib/service/issues/src/services/labels.rs View lib/service/issues/src/services/labels.rs
@@ -78,6 +78,7 @@
7878}
7979
8080#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
81#[serde(deny_unknown_fields)]
8182pub struct ListLabelQuery {
8283 pub q: Option<String>,
8384 pub is_archived: Option<bool>,
@@ -190,7 +191,7 @@
190191 ) -> Result<LabelPage, IssueError> {
191192 let (_, board) = board_context(self, caller, space_name, board_id).await?;
192193
193 let (limit, offset) = self.paginate(q.limit, q.offset);
194 let (limit, offset) = self.paginate(q.limit, q.offset)?;
194195
195196 let mut select =
196197 issue_label::Entity::find().filter(issue_label::Column::DirId.eq(board.dir_id));
@@ -208,7 +209,7 @@
208209
209210 select = select.order_by_desc(issue_label::Column::CreatedAt);
210211
211 let total = select.clone().count(self.database.read()).await? as i64;
212 let total = select.clone().count(self.database.read()).await?;
212213 let rows = select
213214 .limit(limit)
214215 .offset(offset)
Modified lib/service/issues/src/services/members.rs View lib/service/issues/src/services/members.rs
@@ -36,6 +36,7 @@
3636}
3737
3838#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
39#[serde(deny_unknown_fields)]
3940pub struct ListIssueMemberQuery {
4041 pub kind: Option<IssueMemberKind>,
4142 pub limit: Option<u64>,
@@ -208,7 +209,7 @@
208209 ) -> Result<IssueMemberPage, IssueError> {
209210 require_issue_access(self, caller, space_name, board_id, issue_id).await?;
210211
211 let (limit, offset) = self.paginate(q.limit, q.offset);
212 let (limit, offset) = self.paginate(q.limit, q.offset)?;
212213
213214 let mut select =
214215 issue_member::Entity::find().filter(issue_member::Column::IssueId.eq(issue_id));
@@ -219,7 +220,7 @@
219220
220221 select = select.order_by_asc(issue_member::Column::CreatedAt);
221222
222 let total = select.clone().count(self.database.read()).await? as i64;
223 let total = select.clone().count(self.database.read()).await?;
223224 let rows = select
224225 .limit(limit)
225226 .offset(offset)
Modified lib/service/issues/src/services/milestones.rs View lib/service/issues/src/services/milestones.rs
@@ -60,6 +60,7 @@
6060}
6161
6262#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
63#[serde(deny_unknown_fields)]
6364pub struct ListMilestoneQuery {
6465 pub is_closed: Option<bool>,
6566 pub q: Option<String>,
@@ -213,7 +214,7 @@
213214 ) -> Result<MilestonePage, IssueError> {
214215 let (_, board) = board_context(self, caller, space_name, board_id).await?;
215216
216 let (limit, offset) = self.paginate(q.limit, q.offset);
217 let (limit, offset) = self.paginate(q.limit, q.offset)?;
217218
218219 let mut select =
219220 milestone::Entity::find().filter(milestone::Column::CollectionId.eq(board.id));
@@ -231,7 +232,7 @@
231232
232233 select = select.order_by_desc(milestone::Column::CreatedAt);
233234
234 let total = select.clone().count(self.database.read()).await? as i64;
235 let total = select.clone().count(self.database.read()).await?;
235236 let rows = select
236237 .limit(limit)
237238 .offset(offset)
Modified lib/service/issues/src/services/service.rs View lib/service/issues/src/services/service.rs
@@ -49,7 +49,11 @@
4949 }
5050 }
5151
52 pub fn paginate(&self, limit: Option<u64>, offset: Option<u64>) -> (u64, u64) {
52 pub fn paginate(
53 &self,
54 limit: Option<u64>,
55 offset: Option<u64>,
56 ) -> Result<(u64, u64), api::pagination::PageError> {
5357 api::pagination::page_bounds(
5458 limit,
5559 offset,
Modified lib/service/issues/src/services/tracked_time.rs View lib/service/issues/src/services/tracked_time.rs
@@ -25,6 +25,7 @@
2525pub type TrackedTimePage = api::pagination::Page<TrackedTimeDto>;
2626
2727#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
28#[serde(deny_unknown_fields)]
2829pub struct ListTrackedTimeQuery {
2930 pub user_id: Option<i64>,
3031 pub limit: Option<u64>,
@@ -162,7 +163,7 @@
162163 ) -> Result<TrackedTimePage, IssueError> {
163164 lookup::require_issue_access(self, caller, space_name, board_id, issue_id).await?;
164165
165 let (limit, offset) = self.paginate(q.limit, q.offset);
166 let (limit, offset) = self.paginate(q.limit, q.offset)?;
166167
167168 let mut select =
168169 tracked_time::Entity::find().filter(tracked_time::Column::IssueId.eq(issue_id));
@@ -173,7 +174,7 @@
173174
174175 select = select.order_by_desc(tracked_time::Column::CreatedAt);
175176
176 let total = select.clone().count(self.database.read()).await? as i64;
177 let total = select.clone().count(self.database.read()).await?;
177178 let rows = select
178179 .limit(limit)
179180 .offset(offset)
Modified lib/service/issues/src/errors.rs View lib/service/issues/src/errors.rs
@@ -41,6 +41,7 @@
4141}
4242
4343api::json_payload_conversion!(IssueError);
44api::page_error_conversion!(IssueError, BadRequest);
4445api::access_error_conversion!(IssueError, BadRequest);
4546
4647api::api_error! {
Modified lib/service/pulls/src/services/comments.rs View lib/service/pulls/src/services/comments.rs
@@ -182,11 +182,11 @@
182182 pull_id: Uuid,
183183 page: PageQuery,
184184 ) -> Result<CommentPage, PullError> {
185 let (limit, offset) = page.page();
185 let (limit, offset) = page.bounds()?;
186186 let total = pull_request_comment::Entity::find()
187187 .filter(pull_request_comment::Column::PullRequestId.eq(pull_id))
188188 .count(self.db_read())
189 .await? as i64;
189 .await?;
190190 let rows = pull_request_comment::Entity::find()
191191 .filter(pull_request_comment::Column::PullRequestId.eq(pull_id))
192192 .order_by_asc(pull_request_comment::Column::CreatedAt)
Modified lib/service/pulls/src/services/events.rs View lib/service/pulls/src/services/events.rs
@@ -178,11 +178,11 @@
178178 pull_id: Uuid,
179179 page: PageQuery,
180180 ) -> Result<EventPage, PullError> {
181 let (limit, offset) = page.page();
181 let (limit, offset) = page.bounds()?;
182182 let total = pull_request_event_line::Entity::find()
183183 .filter(pull_request_event_line::Column::PullRequestId.eq(pull_id))
184184 .count(self.db_read())
185 .await? as i64;
185 .await?;
186186 let rows = pull_request_event_line::Entity::find()
187187 .filter(pull_request_event_line::Column::PullRequestId.eq(pull_id))
188188 .order_by_asc(pull_request_event_line::Column::CreatedAt)
Modified lib/service/pulls/src/services/inbox.rs View lib/service/pulls/src/services/inbox.rs
@@ -20,6 +20,7 @@
2020}
2121
2222#[derive(Debug, Clone, Default, Deserialize, IntoParams)]
23#[serde(deny_unknown_fields)]
2324pub struct InboxQuery {
2425 /// open | closed | merged | all
2526 #[serde(default)]
@@ -39,8 +40,7 @@
3940 scope: InboxScope,
4041 query: InboxQuery,
4142 ) -> Result<InboxPage, PullError> {
42 let limit = query.limit.unwrap_or(50).clamp(1, 100);
43 let offset = query.offset.unwrap_or(0);
43 let (limit, offset) = api::pagination::page_bounds(query.limit, query.offset, 50, 100)?;
4444 let base: Select<pull_requests::Entity> = match scope {
4545 InboxScope::Created => {
4646 pull_requests::Entity::find().filter(pull_requests::Column::AuthorId.eq(caller))
@@ -79,7 +79,7 @@
7979 return Err(PullError::BadRequest(format!("unknown state `{other}`")));
8080 }
8181 };
82 let total = select.clone().count(self.db_read()).await? as i64;
82 let total = select.clone().count(self.db_read()).await?;
8383 select = select.order_by_desc(pull_requests::Column::UpdatedAt);
8484 let rows = select
8585 .limit(limit)
Modified lib/service/pulls/src/services/pulls.rs View lib/service/pulls/src/services/pulls.rs
@@ -1,4 +1,4 @@
1use api::pagination::Page;
1use api::pagination::{Page, SortOrder};
22use api::users::UserDto;
33use git::facade::GitContext;
44use models::issue::issue_relation_pull;
@@ -82,7 +82,22 @@
8282
8383pub type PullPage = Page<PullDto>;
8484
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
86#[serde(rename_all = "snake_case")]
87pub enum PullSort {
88 Number,
89 Created,
90 Updated,
91}
92
93impl PullSort {
94 fn default_order(self) -> SortOrder {
95 SortOrder::Desc
96 }
97}
98
8599#[derive(Debug, Clone, Default, Deserialize, IntoParams)]
100#[serde(deny_unknown_fields)]
86101pub struct ListPullQuery {
87102 /// open | closed | merged | all
88103 #[serde(default)]
@@ -97,12 +112,10 @@
97112 pub draft: Option<bool>,
98113 #[serde(default)]
99114 pub search: Option<String>,
100 /// number | created | updated
101115 #[serde(default)]
102 pub sort: Option<String>,
103 /// asc | desc
116 pub sort: Option<PullSort>,
104117 #[serde(default)]
105 pub order: Option<String>,
118 pub order: Option<SortOrder>,
106119 #[serde(default)]
107120 pub limit: Option<u64>,
108121 #[serde(default)]
@@ -196,8 +209,7 @@
196209 }
197210
198211 pub async fn list_pulls(&self, repo_id: Uuid, q: ListPullQuery) -> Result<PullPage, PullError> {
199 let limit = q.limit.unwrap_or(50).clamp(1, 100);
200 let offset = q.offset.unwrap_or(0);
212 let (limit, offset) = api::pagination::page_bounds(q.limit, q.offset, 50, 100)?;
201213 let state = q.state.clone().unwrap_or_else(|| "open".to_string());
202214 let mut select =
203215 pull_requests::Entity::find().filter(pull_requests::Column::RepoId.eq(repo_id));
@@ -236,24 +248,25 @@
236248 select = select.filter(pull_requests::Column::Title.contains(search));
237249 }
238250
239 let total = select.clone().count(self.db_read()).await? as i64;
240 let descending = q.order.as_deref().unwrap_or("desc") != "asc";
241 select = match q.sort.as_deref().unwrap_or("created") {
242 "number" => {
251 let total = select.clone().count(self.db_read()).await?;
252 let sort = q.sort.unwrap_or(PullSort::Created);
253 let descending = q.order.unwrap_or(sort.default_order()) == SortOrder::Desc;
254 select = match sort {
255 PullSort::Number => {
243256 if descending {
244257 select.order_by_desc(pull_requests::Column::AutoincrementId)
245258 } else {
246259 select.order_by_asc(pull_requests::Column::AutoincrementId)
247260 }
248261 }
249 "updated" => {
262 PullSort::Updated => {
250263 if descending {
251264 select.order_by_desc(pull_requests::Column::UpdatedAt)
252265 } else {
253266 select.order_by_asc(pull_requests::Column::UpdatedAt)
254267 }
255268 }
256 _ => {
269 PullSort::Created => {
257270 if descending {
258271 select.order_by_desc(pull_requests::Column::CreatedAt)
259272 } else {
Modified lib/service/pulls/src/errors.rs View lib/service/pulls/src/errors.rs
@@ -30,6 +30,7 @@
3030auth::auth_error_conversion!(PullError);
3131api::db_error_conversions!(PullError);
3232api::json_payload_conversion!(PullError);
33api::page_error_conversion!(PullError, BadRequest);
3334api::domain_error_passthrough!(PullError from issues::errors::IssueError);
3435
3536impl From<git::facade::GitError> for PullError {
Modified lib/service/pulls/src/openapi.rs View lib/service/pulls/src/openapi.rs
@@ -53,6 +53,8 @@
5353 ),
5454 components(schemas(
5555 AppResponseData<NoData>,
56 api::pagination::SortOrder,
57 crate::services::pulls::PullSort,
5658 AppResponseData<crate::services::pulls::PullDto>,
5759 AppResponseData<crate::services::pulls::PullPage>,
5860 AppResponseData<crate::services::comments::CommentPage>,
Modified lib/service/space/src/handlers/space_admin.rs View lib/service/space/src/handlers/space_admin.rs
@@ -37,38 +37,46 @@
3737 pub invite_id: Uuid,
3838}
3939
40#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
40#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
41#[serde(deny_unknown_fields)]
4142pub struct OrgListQuery {
42 pub page: Option<u64>,
43 pub page_size: Option<u64>,
43 pub limit: Option<u64>,
44 pub offset: Option<u64>,
4445 pub include_archived: Option<bool>,
4546}
4647
4748impl OrgListQuery {
48 fn limit(&self) -> u64 {
49 self.page_size.unwrap_or(20).clamp(1, 100)
50 }
51
52 fn offset(&self) -> u64 {
53 self.page.unwrap_or(1).saturating_sub(1) * self.limit()
49 fn bounds(&self) -> Result<(u64, u64), api::pagination::PageError> {
50 api::pagination::page_bounds(self.limit, self.offset, 20, 100)
5451 }
5552}
5653
5754#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
55#[serde(deny_unknown_fields)]
5856pub struct MemberListQuery {
59 pub page: Option<u64>,
60 pub page_size: Option<u64>,
57 pub limit: Option<u64>,
58 pub offset: Option<u64>,
6159 pub include_inactive: Option<bool>,
6260 pub include_banned: Option<bool>,
6361}
6462
6563impl MemberListQuery {
66 fn limit(&self) -> u64 {
67 self.page_size.unwrap_or(20).clamp(1, 100)
64 fn bounds(&self) -> Result<(u64, u64), api::pagination::PageError> {
65 api::pagination::page_bounds(self.limit, self.offset, 20, 100)
6866 }
67}
6968
70 fn offset(&self) -> u64 {
71 self.page.unwrap_or(1).saturating_sub(1) * self.limit()
69#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
70#[serde(deny_unknown_fields)]
71pub struct InviteListQuery {
72 pub limit: Option<u64>,
73 pub offset: Option<u64>,
74 pub include_revoked: Option<bool>,
75}
76
77impl InviteListQuery {
78 fn bounds(&self) -> Result<(u64, u64), api::pagination::PageError> {
79 api::pagination::page_bounds(self.limit, self.offset, 20, 100)
7280 }
7381}
7482
@@ -164,8 +172,9 @@
164172 operation_id = "space_list",
165173 summary = "列示我加入的组织",
166174 description = "默认只返回 ACTIVE 组织;`include_archived=true` 时一并返回已归档组织。",
175 params(OrgListQuery),
167176 responses(
168 (status = 200, description = "组织列表", body = AppResponseData<Vec<OrganizationSummary>>),
177 (status = 200, description = "组织列表", body = AppResponseData<api::pagination::Page<OrganizationSummary>>),
169178 (status = 401, description = "会话未绑定账号", body = AppResponseData<NoData>)
170179 )
171180)]
@@ -174,18 +183,19 @@
174183 session: Session,
175184 request_id: RequestId,
176185 Query(query): Query<OrgListQuery>,
177) -> Result<AppResponseData<Vec<OrganizationSummary>>, ApiError> {
186) -> Result<AppResponseData<api::pagination::Page<OrganizationSummary>>, ApiError> {
178187 let user_id = svc
179188 .policy
180189 .require_session(&session, Some(request_id.0.as_str()))
181190 .await?;
191 let (limit, offset) = query.bounds()?;
182192 let rows = svc
183193 .list_spaces(
184194 user_id,
185195 0,
186196 query.include_archived.unwrap_or(false),
187 query.limit(),
188 query.offset(),
197 limit,
198 offset,
189199 Some(request_id.0.as_str()),
190200 )
191201 .await?;
@@ -415,11 +425,13 @@
415425 description = "需要 `member.view`。默认只返回活跃成员。",
416426 params(
417427 ("space_name" = String, Path, description = "空间 slug"),
428 ("limit" = Option<u64>, Query, description = "每页条数,默认 20,最多 100"),
429 ("offset" = Option<u64>, Query, description = "跳过条数,最多 5000"),
418430 ("include_inactive" = Option<bool>, Query, description = "包含已退出成员"),
419431 ("include_banned" = Option<bool>, Query, description = "包含已封禁成员")
420432 ),
421433 responses(
422 (status = 200, description = "成员列表", body = AppResponseData<Vec<MemberSummary>>),
434 (status = 200, description = "成员列表", body = AppResponseData<api::pagination::Page<MemberSummary>>),
423435 (status = 403, description = "权限不足", body = AppResponseData<NoData>)
424436 )
425437)]
@@ -429,20 +441,21 @@
429441 request_id: RequestId,
430442 Path(path): Path<SpacePath>,
431443 Query(query): Query<MemberListQuery>,
432) -> Result<AppResponseData<Vec<MemberSummary>>, ApiError> {
444) -> Result<AppResponseData<api::pagination::Page<MemberSummary>>, ApiError> {
433445 let user_id = svc
434446 .policy
435447 .require_session(&session, Some(request_id.0.as_str()))
436448 .await?;
437449 let space_id = svc.space_id_by_name(user_id, &path.space_name).await?;
450 let (limit, offset) = query.bounds()?;
438451 let rows = svc
439452 .admin_list_members(
440453 user_id,
441454 space_id,
442455 query.include_inactive.unwrap_or(false),
443456 query.include_banned.unwrap_or(false),
444 query.limit(),
445 query.offset(),
457 limit,
458 offset,
446459 Some(request_id.0.as_str()),
447460 )
448461 .await?;
@@ -807,11 +820,11 @@
807820 description = "需要 `member.ban`。按封禁时间倒序。",
808821 params(
809822 ("space_name" = String, Path, description = "空间 slug"),
810 ("page" = Option<u64>, Query, description = "页码,从 1 开始"),
811 ("page_size" = Option<u64>, Query, description = "每页条数")
823 ("limit" = Option<u64>, Query, description = "每页条数,默认 20,最多 100"),
824 ("offset" = Option<u64>, Query, description = "跳过条数,最多 5000")
812825 ),
813826 responses(
814 (status = 200, description = "封禁列表", body = AppResponseData<Vec<MemberSummary>>),
827 (status = 200, description = "封禁列表", body = AppResponseData<api::pagination::Page<MemberSummary>>),
815828 (status = 403, description = "权限不足", body = AppResponseData<NoData>)
816829 )
817830)]
@@ -821,18 +834,19 @@
821834 request_id: RequestId,
822835 Path(path): Path<SpacePath>,
823836 Query(query): Query<MemberListQuery>,
824) -> Result<AppResponseData<Vec<MemberSummary>>, ApiError> {
837) -> Result<AppResponseData<api::pagination::Page<MemberSummary>>, ApiError> {
825838 let user_id = svc
826839 .policy
827840 .require_session(&session, Some(request_id.0.as_str()))
828841 .await?;
829842 let space_id = svc.space_id_by_name(user_id, &path.space_name).await?;
843 let (limit, offset) = query.bounds()?;
830844 let rows = svc
831845 .admin_list_bans(
832846 user_id,
833847 space_id,
834 query.limit(),
835 query.offset(),
848 limit,
849 offset,
836850 Some(request_id.0.as_str()),
837851 )
838852 .await?;
@@ -1449,10 +1463,12 @@
14491463 description = "需要 `invite.view`。不含 token 明文。",
14501464 params(
14511465 ("space_name" = String, Path, description = "空间 slug"),
1466 ("limit" = Option<u64>, Query, description = "每页条数,默认 20,最多 100"),
1467 ("offset" = Option<u64>, Query, description = "跳过条数,最多 5000"),
14521468 ("include_revoked" = Option<bool>, Query, description = "包含已撤销邀请")
14531469 ),
14541470 responses(
1455 (status = 200, description = "邀请列表", body = AppResponseData<Vec<InviteSummary>>),
1471 (status = 200, description = "邀请列表", body = AppResponseData<api::pagination::Page<InviteSummary>>),
14561472 (status = 403, description = "权限不足", body = AppResponseData<NoData>)
14571473 )
14581474)]
@@ -1461,20 +1477,21 @@
14611477 session: Session,
14621478 request_id: RequestId,
14631479 Path(path): Path<SpacePath>,
1464 Query(query): Query<OrgListQuery>,
1465) -> Result<AppResponseData<Vec<InviteSummary>>, ApiError> {
1480 Query(query): Query<InviteListQuery>,
1481) -> Result<AppResponseData<api::pagination::Page<InviteSummary>>, ApiError> {
14661482 let user_id = svc
14671483 .policy
14681484 .require_session(&session, Some(request_id.0.as_str()))
14691485 .await?;
14701486 let space_id = svc.space_id_by_name(user_id, &path.space_name).await?;
1487 let (limit, offset) = query.bounds()?;
14711488 let rows = svc
14721489 .admin_list_invites(
14731490 user_id,
14741491 space_id,
1475 query.include_archived.unwrap_or(false),
1476 query.limit(),
1477 query.offset(),
1492 query.include_revoked.unwrap_or(false),
1493 limit,
1494 offset,
14781495 Some(request_id.0.as_str()),
14791496 )
14801497 .await?;
Modified lib/service/space/src/services/groups.rs View lib/service/space/src/services/groups.rs
@@ -516,13 +516,13 @@
516516 q.offset,
517517 self.policy.settings.space.default_pagination,
518518 self.policy.settings.space.max_pagination,
519 );
519 )?;
520520
521521 let select = space_groups::Entity::find()
522522 .filter(space_groups::Column::SpaceId.eq(space.id))
523523 .order_by_asc(space_groups::Column::Position);
524524
525 let total = select.clone().count(self.access.database.read()).await? as i64;
525 let total = select.clone().count(self.access.database.read()).await?;
526526 let rows = select
527527 .limit(limit)
528528 .offset(offset)
Modified lib/service/space/src/services/invite.rs View lib/service/space/src/services/invite.rs
@@ -2,8 +2,8 @@
22use sea_orm::entity::Set;
33use sea_orm::sea_query::{Expr, ExprTrait};
44use sea_orm::{
5 ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QueryOrder,
6 QuerySelect, TransactionTrait,
5 ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter,
6 QueryOrder, QuerySelect, TransactionTrait,
77};
88use uuid::Uuid;
99
@@ -188,7 +188,7 @@
188188 limit: u64,
189189 offset: u64,
190190 request_id: Option<&str>,
191 ) -> SpaceResult<Vec<InviteSummary>> {
191 ) -> SpaceResult<api::pagination::Page<InviteSummary>> {
192192 self.require_permission(actor_user_id, organization_id, "invite.view", request_id)
193193 .await?;
194194 let mut query = space_invites::Entity::find()
@@ -196,13 +196,15 @@
196196 if !include_revoked {
197197 query = query.filter(space_invites::Column::RevokedAt.is_null());
198198 }
199 let total = query.clone().count(self.access.database.read()).await?;
199200 let rows = query
200201 .order_by_desc(space_invites::Column::CreatedAt)
201202 .limit(limit)
202203 .offset(offset)
203204 .all(self.access.database.read())
204205 .await?;
205 Ok(rows.iter().map(invite_summary).collect())
206 let items = rows.iter().map(invite_summary).collect();
207 Ok(api::pagination::Page::new(items, total, limit, offset))
206208 }
207209
208210 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, invite_id = %invite_id, request_id = %envelope::context::resolve_request_id(request_id)))]
Modified lib/service/space/src/services/join_requests.rs View lib/service/space/src/services/join_requests.rs
@@ -12,6 +12,8 @@
1212use utoipa::{IntoParams, ToSchema};
1313use uuid::Uuid;
1414
15use api::pagination::SortOrder;
16
1517use crate::authority;
1618use crate::errors::SpaceError;
1719use crate::services::dto::{UserDto, resolve_space_image_url};
@@ -57,10 +59,25 @@
5759 pub offset: Option<u64>,
5860}
5961
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
63#[serde(rename_all = "snake_case")]
64pub enum ExploreSort {
65 Members,
66 Newest,
67}
68
69impl ExploreSort {
70 fn default_order(self) -> SortOrder {
71 SortOrder::Desc
72 }
73}
74
6075#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
76#[serde(deny_unknown_fields)]
6177pub struct ExploreQuery {
6278 pub q: Option<String>,
63 pub sort: Option<String>,
79 pub sort: Option<ExploreSort>,
80 pub order: Option<SortOrder>,
6481 pub limit: Option<u64>,
6582 pub offset: Option<u64>,
6683}
@@ -179,7 +196,7 @@
179196 query.offset,
180197 self.policy.settings.space.default_pagination,
181198 self.policy.settings.space.max_pagination,
182 );
199 )?;
183200
184201 let rows =
185202 crate::authority::list_public_spaces(self, query.q.as_deref().unwrap_or_default())
@@ -189,15 +206,27 @@
189206 let count = crate::authority::member_count(self, row.id).await?;
190207 ranked.push((row, count));
191208 }
192 if query.sort.as_deref() == Some("newest") {
193 ranked.sort_by(|a, b| b.0.created_at.cmp(&a.0.created_at));
194 } else {
195 ranked.sort_by(|a, b| {
196 b.1.cmp(&a.1)
197 .then_with(|| b.0.created_at.cmp(&a.0.created_at))
198 });
209 let sort = query.sort.unwrap_or(ExploreSort::Members);
210 let descending = query.order.unwrap_or(sort.default_order()) == SortOrder::Desc;
211 match sort {
212 ExploreSort::Newest => ranked.sort_by(|a, b| {
213 if descending {
214 b.0.created_at.cmp(&a.0.created_at)
215 } else {
216 a.0.created_at.cmp(&b.0.created_at)
217 }
218 }),
219 ExploreSort::Members => ranked.sort_by(|a, b| {
220 if descending {
221 b.1.cmp(&a.1)
222 .then_with(|| b.0.created_at.cmp(&a.0.created_at))
223 } else {
224 a.1.cmp(&b.1)
225 .then_with(|| b.0.created_at.cmp(&a.0.created_at))
226 }
227 }),
199228 }
200 let total = ranked.len() as i64;
229 let total = ranked.len() as u64;
201230 let rows: Vec<(spaces::Model, i64)> = ranked
202231 .into_iter()
203232 .skip(offset as usize)
@@ -437,7 +466,7 @@
437466 query.offset,
438467 self.policy.settings.space.default_pagination,
439468 self.policy.settings.space.max_pagination,
440 );
469 )?;
441470
442471 let (rows, total) = self
443472 .admin_list_join_requests(caller, space.id, query.status, limit, offset, None)
@@ -452,7 +481,7 @@
452481 .into_iter()
453482 .map(|row| join_request_dto(row, &users))
454483 .collect(),
455 total: total as i64,
484 total: total,
456485 limit,
457486 offset,
458487 })
Modified lib/service/space/src/services/member.rs View lib/service/space/src/services/member.rs
@@ -2,8 +2,8 @@
22use sea_orm::entity::Set;
33use sea_orm::sea_query::{Expr, ExprTrait};
44use sea_orm::{
5 ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QueryOrder,
6 QuerySelect, TransactionTrait,
5 ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter,
6 QueryOrder, QuerySelect, TransactionTrait,
77};
88use uuid::Uuid;
99
@@ -135,7 +135,7 @@
135135 limit: u64,
136136 offset: u64,
137137 request_id: Option<&str>,
138 ) -> SpaceResult<Vec<MemberSummary>> {
138 ) -> SpaceResult<api::pagination::Page<MemberSummary>> {
139139 self.require_permission(actor_user_id, organization_id, "member.view", request_id)
140140 .await?;
141141 let mut query = space_members::Entity::find()
@@ -146,13 +146,15 @@
146146 if !include_banned {
147147 query = query.filter(space_members::Column::BannedAt.is_null());
148148 }
149 let total = query.clone().count(self.access.database.read()).await?;
149150 let rows = query
150151 .order_by_asc(space_members::Column::JoinedAt)
151152 .limit(limit)
152153 .offset(offset)
153154 .all(self.access.database.read())
154155 .await?;
155 self.member_summaries(&rows).await
156 let items = self.member_summaries(&rows).await?;
157 Ok(api::pagination::Page::new(items, total, limit, offset))
156158 }
157159
158160 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %envelope::context::resolve_request_id(request_id)))]
@@ -771,18 +773,21 @@
771773 limit: u64,
772774 offset: u64,
773775 request_id: Option<&str>,
774 ) -> SpaceResult<Vec<MemberSummary>> {
776 ) -> SpaceResult<api::pagination::Page<MemberSummary>> {
775777 self.require_permission(actor_user_id, organization_id, "member.ban", request_id)
776778 .await?;
777 let rows = space_members::Entity::find()
779 let query = space_members::Entity::find()
778780 .filter(space_members::Column::OrganizationId.eq(organization_id))
779 .filter(space_members::Column::BannedAt.is_not_null())
781 .filter(space_members::Column::BannedAt.is_not_null());
782 let total = query.clone().count(self.access.database.read()).await?;
783 let rows = query
780784 .order_by_desc(space_members::Column::BannedAt)
781785 .limit(limit)
782786 .offset(offset)
783787 .all(self.access.database.read())
784788 .await?;
785 self.member_summaries(&rows).await
789 let items = self.member_summaries(&rows).await?;
790 Ok(api::pagination::Page::new(items, total, limit, offset))
786791 }
787792
788793 #[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, user_id = user_id, request_id = %envelope::context::resolve_request_id(request_id)))]
Modified lib/service/space/src/services/service.rs View lib/service/space/src/services/service.rs
@@ -374,7 +374,7 @@
374374 limit: u64,
375375 offset: u64,
376376 request_id: Option<&str>,
377 ) -> SpaceResult<Vec<OrganizationSummary>> {
377 ) -> SpaceResult<api::pagination::Page<OrganizationSummary>> {
378378 let target = if user_id == 0 { actor_user_id } else { user_id };
379379 if target != actor_user_id && !self.is_platform_admin(actor_user_id).await? {
380380 return Err(SpaceError::Forbidden);
@@ -390,7 +390,7 @@
390390 .all(self.access.database.read())
391391 .await?;
392392 if ids.is_empty() {
393 return Ok(Vec::new());
393 return Ok(api::pagination::Page::new(Vec::new(), 0, limit, offset));
394394 }
395395
396396 let mut query = spaces::Entity::find()
@@ -399,6 +399,7 @@
399399 if !include_archived {
400400 query = query.filter(spaces::Column::Status.ne(SpaceStatus::Archived));
401401 }
402 let total = query.clone().count(self.access.database.read()).await?;
402403 let rows = query
403404 .order_by_desc(spaces::Column::UpdatedAt)
404405 .limit(limit)
@@ -410,7 +411,7 @@
410411 for row in rows {
411412 out.push(self.organization_summary(&row, Some(target)).await?);
412413 }
413 Ok(out)
414 Ok(api::pagination::Page::new(out, total, limit, offset))
414415 }
415416
416417 /// 公开目录:ACTIVE 且非 PRIVATE 的组织。不要求成员身份。
Modified lib/service/space/src/errors.rs View lib/service/space/src/errors.rs
@@ -137,6 +137,7 @@
137137api::db_error_conversions!(SpaceError);
138138storage::storage_error_conversion!(SpaceError);
139139api::json_payload_conversion!(SpaceError);
140api::page_error_conversion!(SpaceError, BadRequest);
140141
141142api::api_error! {
142143 log: "space",
Modified lib/service/space/src/openapi.rs View lib/service/space/src/openapi.rs
@@ -79,6 +79,8 @@
7979 ),
8080 components(
8181 schemas(
82 api::pagination::SortOrder,
83 crate::services::join_requests::ExploreSort,
8284 AppResponseData<crate::services::dto::SpaceDto>,
8385 AppResponseData<crate::services::join_requests::ExploreSpacePage>,
8486 AppResponseData<crate::services::join_requests::JoinRequestDto>,
@@ -133,6 +135,9 @@
133135 crate::services::types::MemberSummary,
134136 crate::services::types::RoleSummary,
135137 crate::services::types::InviteSummary,
138 api::pagination::Page<crate::services::types::OrganizationSummary>,
139 api::pagination::Page<crate::services::types::MemberSummary>,
140 api::pagination::Page<crate::services::types::InviteSummary>,
136141 crate::services::types::PermissionGrant,
137142 crate::services::types::PermissionCheck,
138143 crate::services::types::CreateOrganizationBody,
@@ -148,6 +153,7 @@
148153 crate::handlers::space_admin::SpaceInvitePath,
149154 crate::handlers::space_admin::OrgListQuery,
150155 crate::handlers::space_admin::MemberListQuery,
156 crate::handlers::space_admin::InviteListQuery,
151157 crate::handlers::space_admin::UpdateRoleBody,
152158 crate::handlers::space_admin::TransferBody,
153159 crate::handlers::space_admin::ReasonBody,
Modified lib/service/user/src/handlers/list_push_subscriptions.rs View lib/service/user/src/handlers/list_push_subscriptions.rs
@@ -1,5 +1,6 @@
11use ::session::Session;
22use api::http::{Query, State};
3use api::pagination::Page;
34use api::rest::{AppResponseData, NoData};
45
56use crate::errors::ApiError;
@@ -15,7 +16,7 @@
1516 description = "分页列出当前用户的浏览器推送订阅,不返回密钥。",
1617 params(PageQuery),
1718 responses(
18 (status = 200, description = "list push subscriptions", body = AppResponseData<Vec<PushSubscriptionDto>>),
19 (status = 200, description = "list push subscriptions", body = AppResponseData<Page<PushSubscriptionDto>>),
1920 (status = 404, description = "resource not found", body = AppResponseData<NoData>),
2021 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
2122 )
@@ -24,7 +25,7 @@
2425 State(service): State<UserService>,
2526 session: Session,
2627 Query(query): Query<PageQuery>,
27) -> Result<AppResponseData<Vec<PushSubscriptionDto>>, ApiError> {
28) -> Result<AppResponseData<Page<PushSubscriptionDto>>, ApiError> {
2829 let uid = session::require_uid(&session)?;
2930 let list = service.list_push_subscriptions(uid, query).await?;
3031 Ok(AppResponseData::ok(list))
Modified lib/service/user/src/handlers/list_saved_messages.rs View lib/service/user/src/handlers/list_saved_messages.rs
@@ -1,5 +1,6 @@
11use ::session::Session;
22use api::http::{Query, State};
3use api::pagination::Page;
34use api::rest::{AppResponseData, NoData};
45
56use crate::errors::ApiError;
@@ -15,7 +16,7 @@
1516 description = "分页列出当前用户收藏的消息引用。",
1617 params(PageQuery),
1718 responses(
18 (status = 200, description = "list saved messages", body = AppResponseData<Vec<SavedMessageDto>>),
19 (status = 200, description = "list saved messages", body = AppResponseData<Page<SavedMessageDto>>),
1920 (status = 404, description = "resource not found", body = AppResponseData<NoData>),
2021 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
2122 )
@@ -24,7 +25,7 @@
2425 State(service): State<UserService>,
2526 session: Session,
2627 Query(query): Query<PageQuery>,
27) -> Result<AppResponseData<Vec<SavedMessageDto>>, ApiError> {
28) -> Result<AppResponseData<Page<SavedMessageDto>>, ApiError> {
2829 let uid = session::require_uid(&session)?;
2930 let list = service.list_saved_messages(uid, query).await?;
3031 Ok(AppResponseData::ok(list))
Modified lib/service/user/src/handlers/my_reports.rs View lib/service/user/src/handlers/my_reports.rs
@@ -1,5 +1,6 @@
11use ::session::Session;
22use api::http::{Query, State};
3use api::pagination::Page;
34use api::rest::{AppResponseData, NoData};
45
56use crate::errors::ApiError;
@@ -15,7 +16,7 @@
1516 description = "分页列出当前用户提交的举报及处理结果。",
1617 params(PageQuery),
1718 responses(
18 (status = 200, description = "my filed reports", body = AppResponseData<Vec<UserReportDto>>),
19 (status = 200, description = "my filed reports", body = AppResponseData<Page<UserReportDto>>),
1920 (status = 404, description = "resource not found", body = AppResponseData<NoData>),
2021 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
2122 )
@@ -24,7 +25,7 @@
2425 State(service): State<UserService>,
2526 session: Session,
2627 Query(query): Query<PageQuery>,
27) -> Result<AppResponseData<Vec<UserReportDto>>, ApiError> {
28) -> Result<AppResponseData<Page<UserReportDto>>, ApiError> {
2829 let uid = session::require_uid(&session)?;
2930 let reports = service.my_reports(uid, query).await?;
3031 Ok(AppResponseData::ok(reports))
Modified lib/service/user/src/handlers/my_spaces.rs View lib/service/user/src/handlers/my_spaces.rs
@@ -1,5 +1,6 @@
11use ::session::Session;
22use api::http::{Query, State};
3use api::pagination::Page;
34use api::rest::{AppResponseData, NoData};
45
56use crate::errors::ApiError;
@@ -15,7 +16,7 @@
1516 description = "列出当前用户激活成员身份的空间及有效权限。",
1617 params(PageQuery),
1718 responses(
18 (status = 200, description = "my spaces", body = AppResponseData<Vec<MySpaceDto>>),
19 (status = 200, description = "my spaces", body = AppResponseData<Page<MySpaceDto>>),
1920 (status = 404, description = "resource not found", body = AppResponseData<NoData>),
2021 (status = 401, description = "authentication required", body = AppResponseData<NoData>)
2122 )
@@ -24,7 +25,7 @@
2425 State(service): State<UserService>,
2526 session: Session,
2627 Query(query): Query<PageQuery>,
27) -> Result<AppResponseData<Vec<MySpaceDto>>, ApiError> {
28) -> Result<AppResponseData<Page<MySpaceDto>>, ApiError> {
2829 let uid = session::require_uid(&session)?;
2930 let spaces = service.my_spaces(uid, query).await?;
3031 Ok(AppResponseData::ok(spaces))
Modified lib/service/user/src/handlers/ssh_key.rs View lib/service/user/src/handlers/ssh_key.rs
@@ -2,8 +2,8 @@
22use crate::services::ssh_key::types::{SshKeyAddRequest, SshKeySummary};
33use api::http::{Json, Path, Query, State};
44use api::middleware::RequestId;
5use api::pagination::{Page, PageQuery};
56use api::rest::{AppResponseData, NoData};
6use auth::share::types::ListQuery;
77use session::Session;
88use uuid::Uuid;
99
@@ -13,17 +13,14 @@
1313 tag = "auth",
1414 operation_id = "auth_ssh_key_list",
1515 summary = "列示 SSH 公钥",
16 description = "按创建时间倒序返回当前账号的公钥,含已吊销与已过期的记录。分页参数由 `page` / `page_size` 控制。查询串不是合法分页参数时由 actix-web 直接返回 400。",
17 params(
18 ("page" = Option<u64>, Query, description = "页码,从 1 开始;默认 1"),
19 ("page_size" = Option<u64>, Query, description = "每页条数,默认 20,最多 100")
20 ),
16 description = "按创建时间倒序返回当前账号的公钥,含已吊销与已过期的记录。使用 `limit` / `offset` 分页;`limit` 必须在 1..=100、`offset` 不得超过 5000,越界返回 400。",
17 params(PageQuery),
2118 responses(
2219 (
2320 status = 200,
2421 description = "公钥列表",
25 body = AppResponseData<Vec<SshKeySummary>>,
26 example = json!({"code":200,"message":"success","data":[{"id":"018f2b1c-4d5e-7f80-9a1b-2c3d4e5f6072","name":"thinkpad","description":"笔记本","algorithm":"ed25519","fingerprint":"SHA256:9pQ2mLk1v3sT6yZx8cV0bN4mR7wQ2eH5jK9aD1sF3gU","public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL9kQ2mXv7Tb1sYz8cV0bN4mR7wQ2eH5jK9aD1sF3gU thinkpad","created_at":"2026-01-02T03:04:05Z","expires_at":null,"last_used_at":null,"revoked_at":null}]})
22 body = AppResponseData<Page<SshKeySummary>>,
23 example = json!({"code":200,"message":"success","data":{"items":[{"id":"018f2b1c-4d5e-7f80-9a1b-2c3d4e5f6072","name":"thinkpad","description":"笔记本","algorithm":"ed25519","fingerprint":"SHA256:9pQ2mLk1v3sT6yZx8cV0bN4mR7wQ2eH5jK9aD1sF3gU","public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL9kQ2mXv7Tb1sYz8cV0bN4mR7wQ2eH5jK9aD1sF3gU thinkpad","created_at":"2026-01-02T03:04:05Z","expires_at":null,"last_used_at":null,"revoked_at":null}],"total":1,"limit":50,"offset":0}})
2724 ),
2825 (
2926 status = 401,
@@ -42,9 +39,9 @@
4239pub async fn ssh_key_list(
4340 State(svc): State<crate::services::service::UserService>,
4441 session: Session,
45 Query(query): Query<ListQuery>,
42 Query(query): Query<PageQuery>,
4643 request_id: RequestId,
47) -> Result<AppResponseData<Vec<SshKeySummary>>, ApiError> {
44) -> Result<AppResponseData<Page<SshKeySummary>>, ApiError> {
4845 let svc = crate::services::ssh_key::SshKeyService::new(svc.accounts.clone());
4946 let keys = svc.list_ssh_keys(session, query, request_id.0).await?;
5047 Ok(AppResponseData::ok(keys))
Modified lib/service/user/src/services/ssh_key/service.rs View lib/service/user/src/services/ssh_key/service.rs
@@ -9,11 +9,13 @@
99use sea_orm::entity::Set;
1010use sea_orm::sea_query::{Expr, ExprTrait};
1111use sea_orm::{
12 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, SqlErr,
13 TransactionTrait,
12 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
13 QuerySelect, SqlErr, TransactionTrait,
1414};
1515use uuid::Uuid;
1616
17use api::pagination::{Page, PageQuery};
18
1719use crate::services::ssh_key::SshKeyService;
1820use crate::services::ssh_key::types::{
1921 MAX_SSH_KEY_DESCRIPTION_LEN, MAX_SSH_KEY_NAME_LEN, MAX_SSH_PUBLIC_KEY_LEN,
@@ -21,7 +23,6 @@
2123};
2224use auth::errors::{AuthError, AuthResult};
2325use auth::share::crypto;
24use auth::share::types::ListQuery;
2526
2627pub(crate) fn parse_public_key(raw: &str) -> AuthResult<(PublicKeyType, String)> {
2728 let key_line = raw.trim();
@@ -189,22 +190,25 @@
189190 pub async fn list_ssh_keys(
190191 &self,
191192 session: Session,
192 query: ListQuery,
193 query: PageQuery,
193194 _request_id: String,
194 ) -> AuthResult<Vec<SshKeySummary>> {
195 ) -> AuthResult<Page<SshKeySummary>> {
196 let (limit, offset) = query.bounds()?;
195197 let user_id = self
196198 .accounts
197199 .require_session(&session, Some(_request_id.as_str()))
198200 .await?;
199 let records = user_ssh_keys::Entity::find()
200 .filter(user_ssh_keys::Column::UserId.eq(user_id))
201 let select =
202 user_ssh_keys::Entity::find().filter(user_ssh_keys::Column::UserId.eq(user_id));
203 let total = select.clone().count(self.accounts.db().read()).await?;
204 let records = select
201205 .order_by_desc(user_ssh_keys::Column::CreatedAt)
202 .limit(query.limit())
203 .offset(query.offset())
206 .limit(limit)
207 .offset(offset)
204208 .all(self.accounts.db().read())
205209 .await?;
206
207 Ok(records.into_iter().map(summary).collect())
210 let items = records.into_iter().map(summary).collect();
211 Ok(Page::new(items, total, limit, offset))
208212 }
209213
210214 /// 吊销公钥与提高账号 `security_version` 在同一事务里完成:密钥泄露后,账号既有的访问令牌与
Modified lib/service/user/src/services/notifications.rs View lib/service/user/src/services/notifications.rs
@@ -1,7 +1,8 @@
11use models::users::user_reports::{self, ReportStatus};
22use models::users::{user_recent_mentions, user_saved_messages};
33use sea_orm::{
4 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
4 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
5 QuerySelect, Set,
56};
67use serde::{Deserialize, Serialize};
78use time::OffsetDateTime;
@@ -10,7 +11,7 @@
1011
1112use crate::errors::UserError;
1213use crate::services::service::UserService;
13use crate::share::page::PageQuery;
14use crate::share::page::{Page, PageQuery};
1415
1516#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
1617pub struct RecentMentionDto {
@@ -103,30 +104,38 @@
103104 &self,
104105 uid: i64,
105106 q: PageQuery,
106 ) -> Result<Vec<UserReportDto>, UserError> {
107 let rows = user_reports::Entity::find()
108 .filter(user_reports::Column::ReporterUid.eq(Some(uid)))
107 ) -> Result<Page<UserReportDto>, UserError> {
108 let (limit, offset) = q.bounds()?;
109 let select =
110 user_reports::Entity::find().filter(user_reports::Column::ReporterUid.eq(Some(uid)));
111 let total = select.clone().count(self.database.read()).await?;
112 let rows = select
109113 .order_by_desc(user_reports::Column::CreatedAt)
110 .limit(q.limit())
111 .offset(q.offset())
114 .limit(limit)
115 .offset(offset)
112116 .all(self.database.read())
113117 .await?;
114 Ok(rows.into_iter().map(UserReportDto::from).collect())
118 let items = rows.into_iter().map(UserReportDto::from).collect();
119 Ok(Page::new(items, total, limit, offset))
115120 }
116121
117122 pub async fn list_saved_messages(
118123 &self,
119124 uid: i64,
120125 q: PageQuery,
121 ) -> Result<Vec<SavedMessageDto>, UserError> {
122 let rows = user_saved_messages::Entity::find()
123 .filter(user_saved_messages::Column::UserId.eq(uid))
126 ) -> Result<Page<SavedMessageDto>, UserError> {
127 let (limit, offset) = q.bounds()?;
128 let select =
129 user_saved_messages::Entity::find().filter(user_saved_messages::Column::UserId.eq(uid));
130 let total = select.clone().count(self.database.read()).await?;
131 let rows = select
124132 .order_by_desc(user_saved_messages::Column::SavedAt)
125 .limit(q.limit())
126 .offset(q.offset())
133 .limit(limit)
134 .offset(offset)
127135 .all(self.database.read())
128136 .await?;
129 Ok(rows.into_iter().map(SavedMessageDto::from).collect())
137 let items = rows.into_iter().map(SavedMessageDto::from).collect();
138 Ok(Page::new(items, total, limit, offset))
130139 }
131140
132141 #[tracing::instrument(name = "user.save_message", skip_all, fields(uid = %uid))]
Modified lib/service/user/src/services/profile.rs View lib/service/user/src/services/profile.rs
@@ -10,7 +10,7 @@
1010
1111use crate::errors::UserError;
1212use crate::services::service::UserService;
13use crate::share::page::PageQuery;
13use crate::share::page::{Page, PageQuery};
1414
1515pub const OWNER_PERMISSIONS: [&str; 9] = [
1616 "view_space",
@@ -116,11 +116,11 @@
116116}
117117
118118impl UserService {
119 pub async fn my_spaces(&self, uid: i64, q: PageQuery) -> Result<Vec<MySpaceDto>, UserError> {
120 let limit = q.limit();
121 let offset = q.offset();
119 pub async fn my_spaces(&self, uid: i64, q: PageQuery) -> Result<Page<MySpaceDto>, UserError> {
120 let (limit, offset) = q.bounds()?;
122121 let mut space_rows = self.access.user_spaces(uid).await?;
123122 space_rows.sort_by(|a, b| a.slug().cmp(b.slug()));
123 let total = space_rows.len() as u64;
124124
125125 let mut out = Vec::new();
126126 for space in space_rows
@@ -145,7 +145,7 @@
145145 permissions: perms,
146146 });
147147 }
148 Ok(out)
148 Ok(Page::new(out, total, limit, offset))
149149 }
150150
151151 pub async fn list_memes(
Modified lib/service/user/src/services/push_subscriptions.rs View lib/service/user/src/services/push_subscriptions.rs
@@ -1,6 +1,7 @@
11use models::users::user_push_subscriptions::{self, PushSubscriptionPlatform};
22use sea_orm::{
3 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
3 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
4 QuerySelect, Set,
45};
56use serde::{Deserialize, Serialize};
67use time::OffsetDateTime;
@@ -9,7 +10,7 @@
910
1011use crate::errors::UserError;
1112use crate::services::service::UserService;
12use crate::share::page::PageQuery;
13use crate::share::page::{Page, PageQuery};
1314
1415#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
1516pub struct PushSubscriptionDto {
@@ -48,15 +49,19 @@
4849 &self,
4950 uid: i64,
5051 q: PageQuery,
51 ) -> Result<Vec<PushSubscriptionDto>, UserError> {
52 let rows = user_push_subscriptions::Entity::find()
53 .filter(user_push_subscriptions::Column::UserId.eq(uid))
52 ) -> Result<Page<PushSubscriptionDto>, UserError> {
53 let (limit, offset) = q.bounds()?;
54 let select = user_push_subscriptions::Entity::find()
55 .filter(user_push_subscriptions::Column::UserId.eq(uid));
56 let total = select.clone().count(self.database.read()).await?;
57 let rows = select
5458 .order_by_desc(user_push_subscriptions::Column::CreatedAt)
55 .limit(q.limit())
56 .offset(q.offset())
59 .limit(limit)
60 .offset(offset)
5761 .all(self.database.read())
5862 .await?;
59 Ok(rows.into_iter().map(PushSubscriptionDto::from).collect())
63 let items = rows.into_iter().map(PushSubscriptionDto::from).collect();
64 Ok(Page::new(items, total, limit, offset))
6065 }
6166
6267 #[tracing::instrument(name = "user.upsert_push_subscription", skip_all, fields(uid = %uid))]
Modified lib/service/user/src/errors.rs View lib/service/user/src/errors.rs
@@ -32,6 +32,7 @@
3232storage::storage_error_conversion!(UserError);
3333cache::redis_error_conversion!(UserError);
3434api::json_payload_conversion!(UserError);
35api::page_error_conversion!(UserError, BadRequest);
3536
3637api::api_error! {
3738 log: "user",
Modified lib/service/user/src/openapi.rs View lib/service/user/src/openapi.rs
@@ -46,6 +46,11 @@
4646 ),
4747 components(schemas(
4848 api::pagination::PageQuery,
49 api::pagination::Page<MySpaceDto>,
50 api::pagination::Page<PushSubscriptionDto>,
51 api::pagination::Page<UserReportDto>,
52 api::pagination::Page<SavedMessageDto>,
53 api::pagination::Page<crate::services::ssh_key::types::SshKeySummary>,
4954 imaging::layout::ImageVariant,
5055 NoData,
5156 AvatarFormat,
Modified lib/service/webapi/src/openapi.rs View lib/service/webapi/src/openapi.rs
@@ -45,10 +45,14 @@
4545 partial updates use `PATCH`.
4646
4747### Pagination
48- Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
49 response `data` is `{ items, total, limit, offset }`.
50- Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
51 response contains `has_more`.
48- 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
49 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
50- `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
51 (`error_code: bad_request`),不会静默截断。
52- 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
53 未知值返回 400,未知参数同样返回 400。
54- chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
55- Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
5256
5357### PATCH semantics (three-state fields)
5458Update commands use `Option<Option<T>>` for clearable fields:
Modified packages/apiclient/auth/auth-challenge.schemas.ts View packages/apiclient/auth/auth-challenge.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-challenge.ts View packages/apiclient/auth/auth-challenge.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-login-code-verify.schemas.ts View packages/apiclient/auth/auth-login-code-verify.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-login-code-verify.ts View packages/apiclient/auth/auth-login-code-verify.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-login-code.schemas.ts View packages/apiclient/auth/auth-login-code.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-login-code.ts View packages/apiclient/auth/auth-login-code.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-login-verify.schemas.ts View packages/apiclient/auth/auth-login-verify.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-login-verify.ts View packages/apiclient/auth/auth-login-verify.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-login.schemas.ts View packages/apiclient/auth/auth-login.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-login.ts View packages/apiclient/auth/auth-login.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-logout.schemas.ts View packages/apiclient/auth/auth-logout.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-logout.ts View packages/apiclient/auth/auth-logout.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-me.schemas.ts View packages/apiclient/auth/auth-me.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-me.ts View packages/apiclient/auth/auth-me.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-password-reset-request.schemas.ts View packages/apiclient/auth/auth-password-reset-request.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-password-reset-request.ts View packages/apiclient/auth/auth-password-reset-request.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-password-reset.schemas.ts View packages/apiclient/auth/auth-password-reset.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-password-reset.ts View packages/apiclient/auth/auth-password-reset.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-register-code.schemas.ts View packages/apiclient/auth/auth-register-code.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-register-code.ts View packages/apiclient/auth/auth-register-code.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-register.schemas.ts View packages/apiclient/auth/auth-register.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-register.ts View packages/apiclient/auth/auth-register.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-ssh-key-add.schemas.ts View packages/apiclient/auth/auth-ssh-key-add.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-ssh-key-add.ts View packages/apiclient/auth/auth-ssh-key-add.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-ssh-key-delete.schemas.ts View packages/apiclient/auth/auth-ssh-key-delete.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-ssh-key-delete.ts View packages/apiclient/auth/auth-ssh-key-delete.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/auth-ssh-key-list.schemas.ts View packages/apiclient/auth/auth-ssh-key-list.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
@@ -98,7 +103,7 @@
98103}
99104
100105export type SshKeyAlgorithm = "rsa" | "ed25519" | "ecdsa";
101export type EnvelopeVecSshKeySummaryDataItem = {
106export type EnvelopePageSshKeySummaryDataItemsItem = {
102107 algorithm: SshKeyAlgorithm;
103108 created_at: string;
104109 /** @nullable */
@@ -115,9 +120,19 @@
115120 revoked_at?: string | null;
116121};
117122
118export interface EnvelopeVecSshKeySummary {
123export type EnvelopePageSshKeySummaryData = {
124 items: EnvelopePageSshKeySummaryDataItemsItem[];
125 /** @minimum 0 */
126 limit: number;
127 /** @minimum 0 */
128 offset: number;
129 /** @minimum 0 */
130 total: number;
131};
132
133export interface EnvelopePageSshKeySummary {
119134 code: number;
120 data?: EnvelopeVecSshKeySummaryDataItem[];
135 data?: EnvelopePageSshKeySummaryData;
121136 /** @nullable */
122137 error?: string | null;
123138 /** @nullable */
@@ -128,13 +143,13 @@
128143
129144export type AuthSshKeyListParams = {
130145 /**
131 * 页码,从 1 开始;默认 1
132146 * @minimum 0
147 * @nullable
133148 */
134 page?: number;
149 limit?: number | null;
135150 /**
136 * 每页条数,默认 20,最多 100
137151 * @minimum 0
152 * @nullable
138153 */
139 page_size?: number;
154 offset?: number | null;
140155};
Modified packages/apiclient/auth/auth-ssh-key-list.ts View packages/apiclient/auth/auth-ssh-key-list.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
@@ -89,11 +94,11 @@
8994import type {
9095 AuthSshKeyListParams,
9196 EnvelopeNoData,
92 EnvelopeVecSshKeySummary,
97 EnvelopePageSshKeySummary,
9398} from "./auth-ssh-key-list.schemas";
9499
95100export type authSshKeyListResponse200 = {
96 data: EnvelopeVecSshKeySummary;
101 data: EnvelopePageSshKeySummary;
97102 status: 200;
98103};
99104
@@ -136,7 +141,7 @@
136141};
137142
138143/**
139 * 按创建时间倒序返回当前账号的公钥,含已吊销与已过期的记录。分页参数由 `page` / `page_size` 控制。查询串不是合法分页参数时由 actix-web 直接返回 400。
144 * 按创建时间倒序返回当前账号的公钥,含已吊销与已过期的记录。使用 `limit` / `offset` 分页;`limit` 必须在 1..=100、`offset` 不得超过 5000,越界返回 400。
140145 * @summary 列示 SSH 公钥
141146 */
142147export const authSshKeyList = async (
Modified packages/apiclient/auth/change-password.schemas.ts View packages/apiclient/auth/change-password.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/change-password.ts View packages/apiclient/auth/change-password.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/recovery-codes.schemas.ts View packages/apiclient/auth/recovery-codes.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/recovery-codes.ts View packages/apiclient/auth/recovery-codes.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/regenerate-recovery-codes.schemas.ts View packages/apiclient/auth/regenerate-recovery-codes.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/regenerate-recovery-codes.ts View packages/apiclient/auth/regenerate-recovery-codes.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/totp-disable.schemas.ts View packages/apiclient/auth/totp-disable.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/totp-disable.ts View packages/apiclient/auth/totp-disable.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/totp-enable.schemas.ts View packages/apiclient/auth/totp-enable.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/totp-enable.ts View packages/apiclient/auth/totp-enable.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/totp-setup.schemas.ts View packages/apiclient/auth/totp-setup.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/totp-setup.ts View packages/apiclient/auth/totp-setup.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/totp-status.schemas.ts View packages/apiclient/auth/totp-status.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/auth/totp-status.ts View packages/apiclient/auth/totp-status.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
Modified packages/apiclient/index.schemas.ts View packages/apiclient/index.schemas.ts
@@ -35,10 +35,14 @@
3535 * partial updates use `PATCH`.
3636 *
3737 * ### Pagination
38 * - Offset style: `?limit=&offset=` (default 50, max 100 unless stated),
39 * response `data` is `{ items, total, limit, offset }`.
40 * - Cursor style (chat message/sequence data): `?before_seq=&after_seq=&limit=`,
41 * response contains `has_more`.
38 * - 唯一 offset 风格:`?limit=&offset=`(`limit` 默认 50;`offset` 上限 5000),
39 * 分页响应的 `data` 恒为 `{ items, total, limit, offset }`,字段均为无符号整数。
40 * - `limit` 为 0、超过端点上限或 `offset` 超过 5000 时返回 400
41 * (`error_code: bad_request`),不会静默截断。
42 * - 排序参数为 `?sort=<field>&order=asc|desc`;`sort` 的取值按端点枚举校验,
43 * 未知值返回 400,未知参数同样返回 400。
44 * - chat 消息额外接受 `before_seq` / `after_seq` 作为筛选条件,响应仍是同一 `Page` 形状。
45 * - Git LFS 锁定列表的 `next_cursor` 是 LFS 协议字段,不适用本约定。
4246 *
4347 * ### PATCH semantics (three-state fields)
4448 * Update commands use `Option<Option<T>>` for clearable fields:
@@ -55,7 +59,8 @@
5559 *
5660 * ### Operations
5761 * - `GET /healthz` — liveness probe (no auth, outside `/api/v1`).
58 * - `GET /readyz` — readiness probe: database + redis check, `503` when down.
62 * - `GET /readyz` — readiness probe: database (primary) + redis live checks,
63 * rocketmq + s3 sampled every 10s, `503` when any is down.
5964 * - Runtime metrics are pushed to the configured OTLP collector (30s interval);
6065 * there is no scrape endpoint.
6166 * - Cross-origin browser access is disabled by default; the API expects a
@@ -387,7 +392,7 @@
387392}
388393
389394export type SshKeyAlgorithm = "rsa" | "ed25519" | "ecdsa";
390export type EnvelopeVecSshKeySummaryDataItem = {
395export type EnvelopePageSshKeySummaryDataItemsItem = {
391396 algorithm: SshKeyAlgorithm;
392397 created_at: string;
393398 /** @nullable */
@@ -404,9 +409,19 @@
404409 revoked_at?: string | null;
405410};
406411
407export interface EnvelopeVecSshKeySummary {
412export type EnvelopePageSshKeySummaryData = {
413 items: EnvelopePageSshKeySummaryDataItemsItem[];
414 /** @minimum 0 */
415 limit: number;
416 /** @minimum 0 */
417 offset: number;
418 /** @minimum 0 */
419 total: number;
420};
421
422export interface EnvelopePageSshKeySummary {
408423 code: number;
409 data?: EnvelopeVecSshKeySummaryDataItem[];
424 data?: EnvelopePageSshKeySummaryData;
410425 /** @nullable */
411426 error?: string | null;
412427 /** @nullable */
@@ -454,13 +469,13 @@
454469
455470export type AuthSshKeyListParams = {
456471 /**
457 * 页码,从 1 开始;默认 1
458472 * @minimum 0
473 * @nullable
459474 */
460 page?: number;
475 limit?: number | null;
461476 /**
462 * 每页条数,默认 20,最多 100
463477 * @minimum 0
478 * @nullable
464479 */
465 page_size?: number;
480 offset?: number | null;
466481};
Modified openapi.json View openapi.json

Diff too large to display.