From e7db750ffb2f2a119f2b525992037a1a1896830f Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Mon, 31 Aug 2026 17:02:52 +0800 Subject: [PATCH 1/3] fix(notes): restore editor sync on Hocuspocus v4 and align env sync URLs Connect via websocketProvider attach/connect (provider.connect is a no-op), derive SYNC_URL from WEB_URL for rust/rk8s/dev, and allow local HMR origins. --- moon/apps/sync-server/.env.example | 15 +++++++ moon/apps/sync-server/README.md | 5 +++ moon/apps/sync-server/turbo.json | 4 +- moon/apps/web/.env.example | 44 +++++++++++++++++++ .../components/Post/Notes/useEditorSync.ts | 20 ++++++--- moon/apps/web/next.config.js | 3 ++ moon/packages/config/src/index.ts | 34 +++++++++++++- 7 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 moon/apps/sync-server/.env.example create mode 100644 moon/apps/web/.env.example diff --git a/moon/apps/sync-server/.env.example b/moon/apps/sync-server/.env.example new file mode 100644 index 000000000..1a9f160e9 --- /dev/null +++ b/moon/apps/sync-server/.env.example @@ -0,0 +1,15 @@ +# Copy to .env.local for local moon `pnpm run dev`. +# Must point at the same Campsite API the web app uses (notes sync_state + sync-token). + +# --- local --- +API_URL=http://api.gitmono.local:3001 +PORT=9000 + +# --- k3s-dev --- +# API_URL=https://api.xuanwu.openatom.cn + +# --- k3s-rust --- +# API_URL=https://api.rust.xuanwu.openatom.cn + +# --- k3s-rk8s --- +# API_URL=https://api.rk8s.xuanwu.openatom.cn diff --git a/moon/apps/sync-server/README.md b/moon/apps/sync-server/README.md index 78f0057a9..bc2e5a59a 100644 --- a/moon/apps/sync-server/README.md +++ b/moon/apps/sync-server/README.md @@ -1,5 +1,10 @@ # sync-server +Local development: copy `.env.example` to `.env.local` and set `API_URL` to the +same Campsite API the web app uses. Web `NEXT_PUBLIC_SYNC_URL` must point at this +process (`ws://localhost:9000` locally, or `wss://sync.` for +k3s-dev / k3s-rust / k3s-rk8s — see `apps/web/.env.example`). + ## Troubleshooting To test the Docker build locally, run the following command in your terminal (from the repo root): diff --git a/moon/apps/sync-server/turbo.json b/moon/apps/sync-server/turbo.json index 2e51c1a6e..8ed32731d 100644 --- a/moon/apps/sync-server/turbo.json +++ b/moon/apps/sync-server/turbo.json @@ -4,10 +4,10 @@ "tasks": { "dev": { "dependsOn": ["^build", "build"], - "passThroughEnv": ["PORT"] + "passThroughEnv": ["PORT", "API_URL"] }, "build": { - "env": ["TIPTAP_PRIVATE_REGISTRY_KEY", "NODE_ENV", "PORT"] + "env": ["TIPTAP_PRIVATE_REGISTRY_KEY", "NODE_ENV", "PORT", "API_URL"] } } } diff --git a/moon/apps/web/.env.example b/moon/apps/web/.env.example new file mode 100644 index 000000000..8b2640dc2 --- /dev/null +++ b/moon/apps/web/.env.example @@ -0,0 +1,44 @@ +# ============================================================================= +# moon/apps/web — copy to .env.local and keep ONE profile uncommented. +# SYNC must match the same environment as WEB / API (app. → sync.). +# ============================================================================= + +# --- local (moon sync-server on :9000 + campsite api on :3001) --- +NEXT_PUBLIC_API_URL=http://api.gitmono.local:3001 +NEXT_PUBLIC_INTERNAL_API_URL=http://api.gitmono.local:3001 +NEXT_PUBLIC_MONO_API_URL=http://git.gitmono.local:8000 +NEXT_PUBLIC_ORION_API_URL=http://orion.gitmono.local:8004 +NEXT_PUBLIC_AUTH_URL=http://auth.gitmono.local:3001 +NEXT_PUBLIC_WEB_URL=http://app.gitmono.local +NEXT_PUBLIC_SYNC_URL=ws://localhost:9000 +NEXT_PUBLIC_CRATES_PRO_URL=http://cratespro.gitmega.nju:8080 + +# --- k3s-dev (base_domain = xuanwu.openatom.cn) --- +# NEXT_PUBLIC_API_URL=https://api.xuanwu.openatom.cn +# NEXT_PUBLIC_INTERNAL_API_URL=https://api.xuanwu.openatom.cn +# NEXT_PUBLIC_MONO_API_URL=https://git.xuanwu.openatom.cn +# NEXT_PUBLIC_ORION_API_URL=https://orion.xuanwu.openatom.cn +# NEXT_PUBLIC_AUTH_URL=https://auth.xuanwu.openatom.cn +# NEXT_PUBLIC_WEB_URL=https://app.xuanwu.openatom.cn +# NEXT_PUBLIC_SYNC_URL=wss://sync.xuanwu.openatom.cn +# NEXT_PUBLIC_CRATES_PRO_URL=https://cratespro.xuanwu.openatom.cn + +# --- k3s-rust (base_domain = rust.xuanwu.openatom.cn) --- +# NEXT_PUBLIC_API_URL=https://api.rust.xuanwu.openatom.cn +# NEXT_PUBLIC_INTERNAL_API_URL=https://api.rust.xuanwu.openatom.cn +# NEXT_PUBLIC_MONO_API_URL=https://git.rust.xuanwu.openatom.cn +# NEXT_PUBLIC_ORION_API_URL=https://orion.rust.xuanwu.openatom.cn +# NEXT_PUBLIC_AUTH_URL=https://auth.rust.xuanwu.openatom.cn +# NEXT_PUBLIC_WEB_URL=https://app.rust.xuanwu.openatom.cn +# NEXT_PUBLIC_SYNC_URL=wss://sync.rust.xuanwu.openatom.cn +# NEXT_PUBLIC_CRATES_PRO_URL=https://cratespro.xuanwu.openatom.cn + +# --- k3s-rk8s (base_domain = rk8s.xuanwu.openatom.cn) --- +# NEXT_PUBLIC_API_URL=https://api.rk8s.xuanwu.openatom.cn +# NEXT_PUBLIC_INTERNAL_API_URL=https://api.rk8s.xuanwu.openatom.cn +# NEXT_PUBLIC_MONO_API_URL=https://git.rk8s.xuanwu.openatom.cn +# NEXT_PUBLIC_ORION_API_URL=https://orion.rk8s.xuanwu.openatom.cn +# NEXT_PUBLIC_AUTH_URL=https://auth.rk8s.xuanwu.openatom.cn +# NEXT_PUBLIC_WEB_URL=https://app.rk8s.xuanwu.openatom.cn +# NEXT_PUBLIC_SYNC_URL=wss://sync.rk8s.xuanwu.openatom.cn +# NEXT_PUBLIC_CRATES_PRO_URL=https://cratespro.xuanwu.openatom.cn diff --git a/moon/apps/web/components/Post/Notes/useEditorSync.ts b/moon/apps/web/components/Post/Notes/useEditorSync.ts index 820a094e8..39b35ee87 100644 --- a/moon/apps/web/components/Post/Notes/useEditorSync.ts +++ b/moon/apps/web/components/Post/Notes/useEditorSync.ts @@ -38,7 +38,7 @@ export function useEditorSync({ resourceId, resourceType, initialState }: Props) const [syncError, setSyncError] = useState(null) const [syncState, setSyncState] = useState('connecting') - const [provider] = useState(() => { + const [{ provider, websocketProvider }] = useState(() => { let document: Y.Doc | undefined if (initialState) { @@ -50,12 +50,15 @@ export function useEditorSync({ resourceId, resourceType, initialState }: Props) document = ydoc } + // @hocuspocus/provider@4: when a custom websocketProvider is passed, + // HocuspocusProvider.connect()/disconnect() are no-ops. Connect the socket + // directly and attach/detach the provider around the connection lifetime. const websocketProvider = new HocuspocusProviderWebsocket({ url: buildSyncUrl(scope, resourceType), autoConnect: false }) - return new HocuspocusProvider({ + const provider = new HocuspocusProvider({ document, websocketProvider, name: resourceId, @@ -88,19 +91,24 @@ export function useEditorSync({ resourceId, resourceType, initialState }: Props) setSyncState(data.status) } }) + + return { provider, websocketProvider } }) useEffect(() => { + provider.attach() + if (isLoggedIn) { - void provider.connect() + void websocketProvider.connect() } else { - provider.disconnect() + websocketProvider.disconnect() } return () => { - provider.disconnect() + websocketProvider.disconnect() + provider.detach() } - }, [provider, isLoggedIn]) + }, [provider, websocketProvider, isLoggedIn]) return [provider, syncState, syncError] as const } diff --git a/moon/apps/web/next.config.js b/moon/apps/web/next.config.js index 208271245..ffa511461 100644 --- a/moon/apps/web/next.config.js +++ b/moon/apps/web/next.config.js @@ -118,6 +118,9 @@ const ContentSecurityPolicy = Object.keys(cspResourcesByDirective).reduce((prevP /** @type {import('next').NextConfig} */ const moduleExports = { output: 'standalone', + // Allow HMR /_next/webpack-hmr when browsing via local hostnames (not localhost). + // Without this, Next blocks cross-origin dev websockets from app.gitmono.local. + allowedDevOrigins: ['app.gitmono.local', '*.gitmono.local', 'local.gitmega.com', '*.gitmega.nju'], experimental: { // Keep monorepo package resolution working for local workspace sources. externalDir: true diff --git a/moon/packages/config/src/index.ts b/moon/packages/config/src/index.ts index 2958e7083..019e8481f 100644 --- a/moon/packages/config/src/index.ts +++ b/moon/packages/config/src/index.ts @@ -12,8 +12,40 @@ export const IS_NGROK = !!process.env.NEXT_PUBLIC_IS_NGROK // eslint-disable-next-line turbo/no-undeclared-env-vars export const WEB_URL = process.env.NEXT_PUBLIC_WEB_URL || 'https://app.gitmega.com' export const SITE_URL = IS_PRODUCTION ? SITE_URL_PROD : SITE_URL_DEV + +// Runtime placeholder used by docker-entrypoint.sh / .env.runtime. Prefer an +// explicit NEXT_PUBLIC_SYNC_URL (set per env by terraform: sync.). +// When unset, derive from WEB_URL so rust / rk8s / dev stay aligned with their +// app host (app. → sync.); local *.local / *.test → localhost:9000. +const SYNC_URL_PLACEHOLDER = 'wss://rt-sync.placeholder.local' +const SYNC_URL_LOCAL = 'ws://localhost:9000' + +function deriveSyncUrlFromWebUrl(webUrl: string): string | undefined { + try { + const url = new URL(webUrl) + + if (url.hostname.includes('placeholder')) return undefined + + if (url.hostname === 'localhost' || url.hostname.endsWith('.local') || url.hostname.endsWith('.test')) { + return SYNC_URL_LOCAL + } + + // Matches mega-terraform gitmono_stack: app_subdomains["mega-ui"]="app", + // app_subdomains["mega-web-sync"]="sync" → wss://sync. + if (url.hostname.startsWith('app.')) { + const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' + + return `${protocol}//sync.${url.hostname.slice('app.'.length)}` + } + } catch { + // ignore invalid WEB_URL + } + + return undefined +} + // eslint-disable-next-line turbo/no-undeclared-env-vars -export const SYNC_URL = process.env.NEXT_PUBLIC_SYNC_URL || 'wss://sync.gitmega.com' +export const SYNC_URL = process.env.NEXT_PUBLIC_SYNC_URL || deriveSyncUrlFromWebUrl(WEB_URL) || SYNC_URL_PLACEHOLDER export const DESKTOP_APP_PROTOCOL = IS_PRODUCTION ? 'campsite://' : 'campsite-dev://' export const LAST_CLIENT_JS_BUILD_ID_LS_KEY = 'latest-js-time' From 2a7cc35d0e9bae4f5f1155c2a7b68c1416ff9c7a Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Tue, 1 Sep 2026 10:03:56 +0800 Subject: [PATCH 2/3] fix(api): serialize snowflake ids as strings for JS precision Snowflake i64s exceed Number.MAX_SAFE_INTEGER and truncated in the browser (e.g. Admin Group delete 404). Serde/OpenAPI/path params and web clients now keep ids as strings; merge-swagger rewrites path refs for gen-client. Skip mono notes lookup until that table is migrated. --- .../api_service/mono/admin/group.rs | 35 +- ceres/src/model/bots.rs | 38 ++ ceres/src/model/buck.rs | 2 + ceres/src/model/change_list.rs | 23 +- ceres/src/model/code_review.rs | 46 ++ ceres/src/model/conversation.rs | 4 + ceres/src/model/group.rs | 49 +- ceres/src/model/issue.rs | 34 +- ceres/src/model/label.rs | 34 + ceres/src/model/mod.rs | 1 + ceres/src/model/serde_snowflake.rs | 441 +++++++++++++ ceres/src/model/user.rs | 12 + ceres/src/model/webhook.rs | 4 + jupiter/src/storage/group_storage.rs | 20 +- jupiter/src/utils/id_generator.rs | 59 ++ mono/src/api/router/bot_router.rs | 55 +- mono/src/api/router/build_trigger_router.rs | 13 +- mono/src/api/router/code_review_router.rs | 33 +- mono/src/api/router/conv_router.rs | 17 +- mono/src/api/router/group_router.rs | 41 +- mono/src/api/router/label_router.rs | 9 +- mono/src/api/router/user_router.rs | 9 +- mono/src/api/router/webhook_router.rs | 9 +- moon/api/README.md | 2 + moon/api/merge-swagger.js | 132 ++++ moon/apps/web/.env.runtime | 2 +- .../AdminGroups/AddMembersDialog.tsx | 4 +- .../AdminGroups/AdminGroupEditDialog.tsx | 50 +- .../components/AdminGroups/AdminGroupItem.tsx | 6 +- .../AdminGroups/AdminGroupsList.tsx | 6 +- .../AdminGroups/DeleteGroupDialog.tsx | 2 +- .../AdminGroups/GroupMembersDialog.tsx | 4 +- .../apps/web/components/AdminGroups/index.tsx | 8 +- moon/apps/web/components/ClView/LabelItem.tsx | 6 +- .../web/components/ClView/TimelineItems.tsx | 2 +- .../ClView/components/Checks/cpns/Task.tsx | 4 +- .../ClView/components/Checks/cpns/store.ts | 2 +- .../ClView/components/HandleTime.tsx | 8 +- .../CodeView/TreeView/CloneTabs.tsx | 111 ++-- .../DiffView/comment/CommentThread.tsx | 8 +- .../DiffView/hooks/useDeleteComment.ts | 2 +- .../DiffView/hooks/useDeleteThread.ts | 2 +- .../DiffView/hooks/useReopenThread.ts | 2 +- .../DiffView/hooks/useReplyComment.ts | 2 +- .../DiffView/hooks/useResolveThread.ts | 2 +- .../DiffView/hooks/useUpdateComment.ts | 2 +- .../web/components/Issues/IssueDetailPage.tsx | 4 +- .../components/Issues/utils/sideEffect.tsx | 13 +- .../web/components/Issues/utils/store.tsx | 8 +- moon/apps/web/components/NoteView/index.tsx | 3 +- .../web/components/NotesIndex/NoteRow.tsx | 2 +- .../web/components/NotesIndex/NotesGrid.tsx | 2 +- .../web/hooks/CL/useDeleteClCommentDelete.ts | 2 +- moon/apps/web/hooks/SSE/useGetClTask.ts | 2 +- .../hooks/admin/useAddAdminGroupMembers.ts | 2 +- .../hooks/admin/useAdminGroupMembersList.ts | 2 +- .../web/hooks/admin/useDeleteAdminGroup.ts | 4 +- .../hooks/admin/useDeleteAdminGroupMember.ts | 2 +- .../web/hooks/admin/useGetAdminGroupById.ts | 2 +- .../web/hooks/admin/useGetNotesPermissions.ts | 11 +- .../hooks/admin/usePostResourcePermissions.ts | 2 +- .../web/hooks/admin/useUpdateAdminGroup.ts | 2 +- .../usePostConversationReactions.ts | 2 +- .../web/hooks/issues/useDeleteIssueComment.ts | 2 +- moon/apps/web/hooks/issues/usePostComment.ts | 2 +- moon/apps/web/hooks/useDeleteSSHKeyById.ts | 2 +- moon/apps/web/hooks/useDeleteTokenById.ts | 2 +- moon/apps/web/pages/[org]/cl/[link]/index.tsx | 4 +- moon/apps/web/utils/types.ts | 2 +- moon/packages/types/generated.ts | 591 +++++++----------- 70 files changed, 1398 insertions(+), 629 deletions(-) create mode 100644 ceres/src/model/serde_snowflake.rs diff --git a/ceres/src/application/api_service/mono/admin/group.rs b/ceres/src/application/api_service/mono/admin/group.rs index 6d6e8857c..6aab02bf4 100644 --- a/ceres/src/application/api_service/mono/admin/group.rs +++ b/ceres/src/application/api_service/mono/admin/group.rs @@ -308,22 +308,25 @@ impl AdminApplicationService { match resource_type { ResourceTypeValue::Note => { - let note = self - .ctx - .storage() - .note_storage() - .get_note_by_public_id(normalized_resource_id) - .await?; - match note { - Some(note) => Ok(note.public_id), - None => { - tracing::warn!( - resource_id = normalized_resource_id, - "note resource missing in mono notes table; falling back to raw public_id" - ); - Ok(normalized_resource_id.to_string()) - } - } + // TODO: mono `notes` is not populated yet (Campsite remains source of truth). + // Re-enable lookup once notes are migrated/synced into mono: + // let note = self + // .ctx + // .storage() + // .note_storage() + // .get_note_by_public_id(normalized_resource_id) + // .await?; + // match note { + // Some(note) => Ok(note.public_id), + // None => { + // tracing::warn!( + // resource_id = normalized_resource_id, + // "note resource missing in mono notes table; falling back to raw public_id" + // ); + // Ok(normalized_resource_id.to_string()) + // } + // } + Ok(normalized_resource_id.to_string()) } } } diff --git a/ceres/src/model/bots.rs b/ceres/src/model/bots.rs index effac2d8d..cda7908a9 100644 --- a/ceres/src/model/bots.rs +++ b/ceres/src/model/bots.rs @@ -6,20 +6,52 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use super::serde_snowflake::{deserialize_i64_from_string_or_number, serialize_i64_as_string}; + #[derive(Serialize, Deserialize, ToSchema)] pub struct BotRes { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub id: i64, + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub bot_id: i64, pub target_type: InstallationTargetType, + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub target_id: i64, pub status: InstallationBotStatus, + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub installed_by: i64, } #[derive(Serialize, Deserialize, ToSchema)] pub struct InstallBotReq { pub target_type: InstallationTargetType, + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub target_id: i64, + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub installed_by: i64, } @@ -120,6 +152,8 @@ pub struct CreateBotTokenRequest { /// Note: `token_plain` is only returned once and is never stored in plaintext. #[derive(Serialize, ToSchema)] pub struct CreateBotTokenResponse { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, pub token_name: String, pub expires_at: Option>, @@ -132,6 +166,8 @@ pub struct CreateBotTokenResponse { /// `token` is a `bot_` push token returned once; use as Bearer (or Basic password). #[derive(Serialize, ToSchema)] pub struct BootstrapInitBotResponse { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub bot_id: i64, pub bot_name: String, pub token: String, @@ -140,6 +176,8 @@ pub struct BootstrapInitBotResponse { /// Item in the list bot tokens response. #[derive(Serialize, ToSchema)] pub struct ListBotTokenItem { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, pub token_name: String, pub expires_at: Option>, diff --git a/ceres/src/model/buck.rs b/ceres/src/model/buck.rs index ade5d78a3..235ec8f8d 100644 --- a/ceres/src/model/buck.rs +++ b/ceres/src/model/buck.rs @@ -160,6 +160,8 @@ pub struct CompletePayload {} #[derive(Debug, Serialize, ToSchema)] pub struct CompleteResponse { /// Change List ID + #[serde(serialize_with = "crate::model::serde_snowflake::serialize_i64_as_string")] + #[schema(value_type = String)] pub cl_id: i64, /// CL link (same as session_id) pub cl_link: String, diff --git a/ceres/src/model/change_list.rs b/ceres/src/model/change_list.rs index ca8ed9fc5..ea648e766 100644 --- a/ceres/src/model/change_list.rs +++ b/ceres/src/model/change_list.rs @@ -10,12 +10,21 @@ use uuid::Uuid; use crate::{ merge_checker::{CheckType, ConditionResult}, - model::{conversation::ConversationItem, label::LabelItem}, + model::{ + conversation::ConversationItem, + label::LabelItem, + serde_snowflake::{ + deserialize_i64_from_string_or_number, + deserialize_option_vec_i64_from_string_or_number, serialize_i64_as_string, + }, + }, }; #[derive(Deserialize, ToSchema)] pub struct AssigneeUpdatePayload { pub assignees: Vec, + #[serde(deserialize_with = "deserialize_i64_from_string_or_number")] + #[schema(value_type = String)] pub item_id: i64, pub link: String, } @@ -24,6 +33,11 @@ pub struct AssigneeUpdatePayload { pub struct ListPayload { pub status: String, pub author: Option, + #[serde( + default, + deserialize_with = "deserialize_option_vec_i64_from_string_or_number" + )] + #[schema(value_type = Option>)] pub labels: Option>, pub assignees: Option>, pub sort_by: Option, @@ -45,6 +59,8 @@ impl From for ListParams { #[derive(Serialize, ToSchema)] pub struct CLDetailRes { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, pub link: String, pub title: String, @@ -337,6 +353,11 @@ pub struct ChangeReviewerStatePayload { #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] pub struct ChangeReviewStatePayload { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub conversation_id: i64, pub resolved: bool, } diff --git a/ceres/src/model/code_review.rs b/ceres/src/model/code_review.rs index 76558ccf4..e50f19966 100644 --- a/ceres/src/model/code_review.rs +++ b/ceres/src/model/code_review.rs @@ -8,6 +8,11 @@ use jupiter::model::code_review_dto::{ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use super::serde_snowflake::{ + deserialize_i64_from_string_or_number, deserialize_option_i64_from_string_or_number, + serialize_i64_as_string, serialize_option_i64_as_string, +}; + #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct InitializeCommentRequest { pub file_path: String, @@ -22,6 +27,11 @@ pub struct InitializeCommentRequest { #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct CommentReplyRequest { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub parent_comment_id: i64, pub content: String, } @@ -66,6 +76,11 @@ pub struct FileReviewResponse { #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct ThreadReviewResponse { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub thread_id: i64, pub status: ThreadStatus, pub created_at: String, @@ -77,6 +92,11 @@ pub struct ThreadReviewResponse { #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct ThreadStatusResponse { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub thread_id: i64, pub link: String, pub status: ThreadStatus, @@ -84,6 +104,11 @@ pub struct ThreadStatusResponse { #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct AnchorResponse { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub anchor_id: i64, pub file_path: String, pub diff_side: DiffSide, @@ -96,7 +121,17 @@ pub struct AnchorResponse { #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct PositionResponse { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub position_id: i64, + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub anchor_id: i64, pub commit_sha: String, pub line_number: i32, @@ -106,9 +141,20 @@ pub struct PositionResponse { #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct CommentReviewResponse { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub comment_id: i64, pub user_name: String, pub content: Option, + #[serde( + default, + serialize_with = "serialize_option_i64_as_string", + deserialize_with = "deserialize_option_i64_from_string_or_number" + )] + #[schema(value_type = Option)] pub parent_id: Option, pub created_at: String, pub updated_at: String, diff --git a/ceres/src/model/conversation.rs b/ceres/src/model/conversation.rs index b62f60ca5..736d27165 100644 --- a/ceres/src/model/conversation.rs +++ b/ceres/src/model/conversation.rs @@ -4,8 +4,12 @@ use callisto::{mega_conversation, reactions, sea_orm_active_enums::ConvTypeEnum} use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use super::serde_snowflake::serialize_i64_as_string; + #[derive(Serialize, ToSchema)] pub struct ConversationItem { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, pub username: String, /// True when `username` matches a registered bot (or legacy `"system"` actor). diff --git a/ceres/src/model/group.rs b/ceres/src/model/group.rs index 82a4911e3..1921d0909 100644 --- a/ceres/src/model/group.rs +++ b/ceres/src/model/group.rs @@ -5,6 +5,8 @@ use callisto::{ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use super::serde_snowflake::{deserialize_i64_from_string_or_number, serialize_i64_as_string}; + #[derive(Debug, Deserialize, ToSchema)] pub struct EmptyListAdditional {} @@ -22,6 +24,9 @@ pub struct UpdateGroupRequest { #[derive(Debug, Serialize, ToSchema)] pub struct GroupResponse { + /// Snowflake id; JSON string so JS keeps full precision. + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, pub name: String, pub description: Option, @@ -37,7 +42,11 @@ pub struct AddMembersRequest { #[derive(Debug, Serialize, ToSchema)] pub struct GroupMemberResponse { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub group_id: i64, /// Campsite public user id (field name kept for API compat). pub username: String, @@ -85,6 +94,8 @@ impl TryFrom<&str> for ResourceTypeValue { #[derive(Debug, Deserialize, ToSchema)] pub struct PermissionBindingRequest { + #[serde(deserialize_with = "deserialize_i64_from_string_or_number")] + #[schema(value_type = String)] pub group_id: i64, pub permission: PermissionValue, } @@ -96,9 +107,13 @@ pub struct SetPermissionsRequest { #[derive(Debug, Serialize, ToSchema)] pub struct ResourcePermissionResponse { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, pub resource_type: ResourceTypeValue, pub resource_id: String, + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub group_id: i64, pub permission: PermissionValue, pub created_at: i64, @@ -107,6 +122,8 @@ pub struct ResourcePermissionResponse { #[derive(Debug, Serialize, ToSchema)] pub struct DeleteGroupResponse { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub group_id: i64, pub deleted_members_count: u64, pub deleted_permissions_count: u64, @@ -115,6 +132,8 @@ pub struct DeleteGroupResponse { #[derive(Debug, Serialize, ToSchema)] pub struct RemoveMemberResponse { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub group_id: i64, pub username: String, pub removed: bool, @@ -222,7 +241,7 @@ impl From for ResourceTypeValue { mod tests { use callisto::sea_orm_active_enums::PermissionEnum; - use super::{PermissionValue, ResourceTypeValue}; + use super::{GroupResponse, PermissionBindingRequest, PermissionValue, ResourceTypeValue}; #[test] fn permission_value_satisfies_hierarchy() { @@ -247,4 +266,32 @@ mod tests { let back: PermissionValue = as_enum.into(); assert_eq!(back, write); } + + #[test] + fn group_response_id_serializes_as_json_string() { + let group = GroupResponse { + id: 13_502_510_928_822_277, + name: "hhh".into(), + description: None, + created_at: 0, + updated_at: 0, + }; + let value = serde_json::to_value(&group).unwrap(); + match &value["id"] { + serde_json::Value::String(s) => assert_eq!(s, "13502510928822277"), + other => panic!("expected JSON string id, got {other:?}"), + } + } + + #[test] + fn permission_binding_accepts_string_or_number_group_id() { + let from_string: PermissionBindingRequest = + serde_json::from_str(r#"{"group_id":"13502510928822277","permission":"read"}"#) + .unwrap(); + assert_eq!(from_string.group_id, 13_502_510_928_822_277); + + let from_number: PermissionBindingRequest = + serde_json::from_str(r#"{"group_id":3306264941936901,"permission":"write"}"#).unwrap(); + assert_eq!(from_number.group_id, 3_306_264_941_936_901); + } } diff --git a/ceres/src/model/issue.rs b/ceres/src/model/issue.rs index d42dcb454..2eb554e26 100644 --- a/ceres/src/model/issue.rs +++ b/ceres/src/model/issue.rs @@ -9,10 +9,17 @@ use jupiter::model::{ use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; -use crate::model::{conversation::ConversationItem, label::LabelItem}; +use crate::model::{ + conversation::ConversationItem, label::LabelItem, serde_snowflake::serialize_i64_as_string, +}; #[derive(Serialize, Deserialize, ToSchema)] pub struct ItemRes { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "crate::model::serde_snowflake::deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub id: i64, pub link: String, pub title: String, @@ -98,6 +105,8 @@ pub struct NewIssue { #[derive(Serialize, ToSchema)] pub struct IssueDetailRes { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, pub link: String, pub title: String, @@ -138,6 +147,8 @@ impl From for IssueDetailRes { #[derive(Serialize, ToSchema, PartialEq, Eq)] pub struct IssueSuggestions { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, pub link: String, pub title: String, @@ -195,3 +206,24 @@ impl From for IssueSuggestions { pub struct QueryPayload { pub query: String, } + +#[cfg(test)] +mod tests { + use super::IssueSuggestions; + + #[test] + fn issue_suggestions_id_serializes_as_json_string() { + let item = IssueSuggestions { + id: 13_502_510_928_822_277, + link: "abc".into(), + title: "t".into(), + suggest_type: "issue_open".into(), + created_at: chrono::NaiveDateTime::default(), + }; + let value = serde_json::to_value(&item).unwrap(); + match &value["id"] { + serde_json::Value::String(s) => assert_eq!(s, "13502510928822277"), + other => panic!("expected JSON string id, got {other:?}"), + } + } +} diff --git a/ceres/src/model/label.rs b/ceres/src/model/label.rs index c4604c545..a390419cd 100644 --- a/ceres/src/model/label.rs +++ b/ceres/src/model/label.rs @@ -2,8 +2,18 @@ use callisto::label; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use super::serde_snowflake::{ + deserialize_i64_from_string_or_number, deserialize_vec_i64_from_string_or_number, + serialize_i64_as_string, +}; + #[derive(Serialize, Deserialize, ToSchema)] pub struct LabelItem { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub id: i64, pub name: String, pub color: String, @@ -30,7 +40,31 @@ pub struct NewLabel { #[derive(Deserialize, ToSchema)] pub struct LabelUpdatePayload { + #[serde(deserialize_with = "deserialize_vec_i64_from_string_or_number")] + #[schema(value_type = Vec)] pub label_ids: Vec, + #[serde(deserialize_with = "deserialize_i64_from_string_or_number")] + #[schema(value_type = String)] pub item_id: i64, pub link: String, } + +#[cfg(test)] +mod tests { + use super::LabelItem; + + #[test] + fn label_item_id_serializes_as_json_string() { + let item = LabelItem { + id: 13_502_510_928_822_277, + name: "bug".into(), + color: "#f00".into(), + description: String::new(), + }; + let value = serde_json::to_value(&item).unwrap(); + match &value["id"] { + serde_json::Value::String(s) => assert_eq!(s, "13502510928822277"), + other => panic!("expected JSON string id, got {other:?}"), + } + } +} diff --git a/ceres/src/model/mod.rs b/ceres/src/model/mod.rs index 1400dad8f..1b067fea6 100644 --- a/ceres/src/model/mod.rs +++ b/ceres/src/model/mod.rs @@ -16,6 +16,7 @@ pub mod merge_queue; pub mod note; pub mod notification; pub mod orion_runner; +pub mod serde_snowflake; pub mod tag; pub mod third_party; pub mod user; diff --git a/ceres/src/model/serde_snowflake.rs b/ceres/src/model/serde_snowflake.rs new file mode 100644 index 000000000..97fefc2eb --- /dev/null +++ b/ceres/src/model/serde_snowflake.rs @@ -0,0 +1,441 @@ +//! Serialize snowflake `i64` IDs as JSON strings so JS clients keep full precision +//! (`Number.MAX_SAFE_INTEGER` is 2^53-1; snowflake IDs exceed that). + +use std::{fmt, ops::Deref, str::FromStr}; + +use serde::{ + Deserialize, Deserializer, Serialize, Serializer, + de::{self, SeqAccess, Visitor}, + ser::SerializeSeq, +}; +use utoipa::ToSchema; + +/// Path/body snowflake id: OpenAPI + JSON as string, runtime `i64`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ToSchema)] +#[schema(value_type = String)] +pub struct SnowflakeId(pub i64); + +impl SnowflakeId { + pub fn get(self) -> i64 { + self.0 + } +} + +impl From for SnowflakeId { + fn from(value: i64) -> Self { + Self(value) + } +} + +impl From for i64 { + fn from(value: SnowflakeId) -> Self { + value.0 + } +} + +impl Deref for SnowflakeId { + type Target = i64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for SnowflakeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for SnowflakeId { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result { + Ok(Self(s.parse()?)) + } +} + +impl Serialize for SnowflakeId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serialize_i64_as_string(&self.0, serializer) + } +} + +impl<'de> Deserialize<'de> for SnowflakeId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_i64_from_string_or_number(deserializer).map(Self) + } +} + +pub fn serialize_i64_as_string(value: &i64, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&value.to_string()) +} + +pub fn deserialize_i64_from_string_or_number<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct SnowflakeVisitor; + + impl<'de> Visitor<'de> for SnowflakeVisitor { + type Value = i64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an i64 snowflake id as a string or number") + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Ok(value) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + i64::try_from(value).map_err(E::custom) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.parse().map_err(E::custom) + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + self.visit_str(&value) + } + } + + deserializer.deserialize_any(SnowflakeVisitor) +} + +pub fn serialize_option_i64_as_string( + value: &Option, + serializer: S, +) -> Result +where + S: Serializer, +{ + match value { + Some(v) => serializer.serialize_some(&v.to_string()), + None => serializer.serialize_none(), + } +} + +pub fn deserialize_option_i64_from_string_or_number<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct OptVisitor; + + impl<'de> Visitor<'de> for OptVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an optional i64 snowflake id as a string or number") + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_i64_from_string_or_number(deserializer).map(Some) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Ok(Some(value)) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + i64::try_from(value).map(Some).map_err(E::custom) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.parse().map(Some).map_err(E::custom) + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + self.visit_str(&value) + } + } + + deserializer.deserialize_any(OptVisitor) +} + +pub fn serialize_vec_i64_as_string(value: &[i64], serializer: S) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(value.len()))?; + for id in value { + seq.serialize_element(&id.to_string())?; + } + seq.end() +} + +pub fn deserialize_vec_i64_from_string_or_number<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct VecVisitor; + + impl<'de> Visitor<'de> for VecVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a sequence of i64 snowflake ids as strings or numbers") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(value) = seq.next_element::()? { + match value { + serde_json::Value::Number(n) => { + let id = n + .as_i64() + .or_else(|| n.as_u64().and_then(|u| i64::try_from(u).ok())) + .ok_or_else(|| de::Error::custom("invalid numeric snowflake id"))?; + out.push(id); + } + serde_json::Value::String(s) => { + out.push(s.parse().map_err(de::Error::custom)?); + } + other => { + return Err(de::Error::custom(format!( + "expected string or number snowflake id, got {other}" + ))); + } + } + } + Ok(out) + } + } + + deserializer.deserialize_seq(VecVisitor) +} + +pub fn deserialize_option_vec_i64_from_string_or_number<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + struct OptVecVisitor; + + impl<'de> Visitor<'de> for OptVecVisitor { + type Value = Option>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an optional sequence of i64 snowflake ids") + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserialize_vec_i64_from_string_or_number(deserializer).map(Some) + } + + fn visit_seq(self, seq: A) -> Result + where + A: SeqAccess<'de>, + { + // Direct array without Option wrapper. + let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + let mut seq = seq; + while let Some(value) = seq.next_element::()? { + match value { + serde_json::Value::Number(n) => { + let id = n + .as_i64() + .or_else(|| n.as_u64().and_then(|u| i64::try_from(u).ok())) + .ok_or_else(|| de::Error::custom("invalid numeric snowflake id"))?; + out.push(id); + } + serde_json::Value::String(s) => { + out.push(s.parse().map_err(de::Error::custom)?); + } + other => { + return Err(de::Error::custom(format!( + "expected string or number snowflake id, got {other}" + ))); + } + } + } + Ok(Some(out)) + } + } + + deserializer.deserialize_any(OptVecVisitor) +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use serde_json::Value; + + use super::*; + + /// Post-#2177 snowflake that exceeds JS `Number.MAX_SAFE_INTEGER`. + const UNSAFE_ID: i64 = 13_502_510_928_822_277; + /// Pre-#2177 magnitude; still fits in a JS Number. + const SAFE_ID: i64 = 3_306_264_941_936_901; + + #[derive(Serialize, Deserialize)] + struct IdField { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + id: i64, + } + + #[derive(Serialize, Deserialize)] + struct OptIdField { + #[serde( + serialize_with = "serialize_option_i64_as_string", + deserialize_with = "deserialize_option_i64_from_string_or_number" + )] + id: Option, + } + + #[derive(Serialize, Deserialize)] + struct VecIdField { + #[serde( + serialize_with = "serialize_vec_i64_as_string", + deserialize_with = "deserialize_vec_i64_from_string_or_number" + )] + ids: Vec, + } + + #[test] + fn json_number_loses_precision_above_js_max_safe() { + let as_number = serde_json::to_string(&UNSAFE_ID).unwrap(); + assert!( + !as_number.contains('"'), + "bare i64 serializes as a JSON number" + ); + let as_f64: f64 = serde_json::from_str(&as_number).unwrap(); + assert_ne!( + as_f64 as i64, UNSAFE_ID, + "parsing the JSON number as f64 (JS Number) must lose precision" + ); + assert_ne!(UNSAFE_ID, UNSAFE_ID as f64 as i64); + } + + #[test] + fn serialize_i64_as_string_preserves_unsafe_id() { + let value = serde_json::to_value(IdField { id: UNSAFE_ID }).unwrap(); + match &value["id"] { + Value::String(s) => assert_eq!(s, "13502510928822277"), + other => panic!("expected JSON string id, got {other:?}"), + } + } + + #[test] + fn deserialize_accepts_string_or_safe_number() { + let from_string: IdField = serde_json::from_str(r#"{"id":"13502510928822277"}"#).unwrap(); + assert_eq!(from_string.id, UNSAFE_ID); + + let from_number: IdField = serde_json::from_str(r#"{"id":3306264941936901}"#).unwrap(); + assert_eq!(from_number.id, SAFE_ID); + } + + #[test] + fn string_round_trip_preserves_unsafe_id() { + let json = serde_json::to_string(&IdField { id: UNSAFE_ID }).unwrap(); + let back: IdField = serde_json::from_str(&json).unwrap(); + assert_eq!(back.id, UNSAFE_ID); + } + + #[test] + fn option_and_vec_serialize_unsafe_ids_as_strings() { + let opt = serde_json::to_value(OptIdField { + id: Some(UNSAFE_ID), + }) + .unwrap(); + assert!(matches!(opt["id"], Value::String(_))); + assert_eq!(opt["id"], "13502510928822277"); + + let none = serde_json::to_value(OptIdField { id: None }).unwrap(); + assert!(none["id"].is_null()); + + let vec = serde_json::to_value(VecIdField { + ids: vec![UNSAFE_ID, SAFE_ID], + }) + .unwrap(); + assert_eq!( + vec["ids"], + Value::Array(vec![ + Value::String("13502510928822277".into()), + Value::String("3306264941936901".into()), + ]) + ); + + let back: VecIdField = serde_json::from_value(vec).unwrap(); + assert_eq!(back.ids, vec![UNSAFE_ID, SAFE_ID]); + } +} diff --git a/ceres/src/model/user.rs b/ceres/src/model/user.rs index 85c79bfd4..5ee90939d 100644 --- a/ceres/src/model/user.rs +++ b/ceres/src/model/user.rs @@ -2,6 +2,8 @@ use callisto::{access_token, ssh_keys}; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; +use super::serde_snowflake::{deserialize_i64_from_string_or_number, serialize_i64_as_string}; + #[derive(Debug, Deserialize, ToSchema)] pub struct AddSSHKey { pub title: String, @@ -10,6 +12,11 @@ pub struct AddSSHKey { #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct ListSSHKey { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub id: i64, pub title: String, pub ssh_key: String, @@ -31,6 +38,11 @@ impl From for ListSSHKey { #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct ListToken { + #[serde( + serialize_with = "serialize_i64_as_string", + deserialize_with = "deserialize_i64_from_string_or_number" + )] + #[schema(value_type = String)] pub id: i64, pub token: String, pub created_at: i64, diff --git a/ceres/src/model/webhook.rs b/ceres/src/model/webhook.rs index 93cce3af5..d14075ceb 100644 --- a/ceres/src/model/webhook.rs +++ b/ceres/src/model/webhook.rs @@ -3,6 +3,8 @@ use jupiter::{sea_orm::ActiveEnum, storage::webhook_storage::WebhookWithEventTyp use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use super::serde_snowflake::serialize_i64_as_string; + #[derive(Debug, Deserialize, ToSchema)] pub struct CreateWebhookRequest { pub target_url: String, @@ -15,6 +17,8 @@ pub struct CreateWebhookRequest { #[derive(Debug, Serialize, ToSchema)] pub struct WebhookResponse { + #[serde(serialize_with = "serialize_i64_as_string")] + #[schema(value_type = String)] pub id: i64, pub target_url: String, pub event_types: Vec, diff --git a/jupiter/src/storage/group_storage.rs b/jupiter/src/storage/group_storage.rs index 9f8885893..83d910e68 100644 --- a/jupiter/src/storage/group_storage.rs +++ b/jupiter/src/storage/group_storage.rs @@ -5,7 +5,8 @@ use std::{ use api_model::common::Pagination; use callisto::{ - mega_group, mega_group_member, mega_resource_permission, sea_orm_active_enums::ResourceTypeEnum, + mega_group, mega_group_member, mega_resource_permission, + sea_orm_active_enums::{PermissionEnum, ResourceTypeEnum}, }; use common::{errors::MegaError, utils::generate_id}; use sea_orm::{ @@ -426,7 +427,14 @@ fn normalize_permission_bindings( ) -> Vec { let mut by_group = BTreeMap::new(); for permission in permissions { - by_group.insert(permission.group_id, permission.permission.clone()); + by_group + .entry(permission.group_id) + .and_modify(|current: &mut PermissionEnum| { + if permission_level(&permission.permission) > permission_level(current) { + *current = permission.permission.clone(); + } + }) + .or_insert_with(|| permission.permission.clone()); } by_group @@ -438,6 +446,14 @@ fn normalize_permission_bindings( .collect() } +fn permission_level(permission: &PermissionEnum) -> u8 { + match permission { + PermissionEnum::Read => 1, + PermissionEnum::Write => 2, + PermissionEnum::Admin => 3, + } +} + /// Returns true when the database error indicates a foreign-key violation. fn is_fk_constraint_error(err: &DbErr) -> bool { let msg = err.to_string().to_lowercase(); diff --git a/jupiter/src/utils/id_generator.rs b/jupiter/src/utils/id_generator.rs index b5f4b3d9d..0ccacb738 100644 --- a/jupiter/src/utils/id_generator.rs +++ b/jupiter/src/utils/id_generator.rs @@ -9,6 +9,20 @@ pub const SEQ_BIT_LEN: u8 = 8; pub const MAX_WORKER_ID: u32 = (1 << WORKER_ID_BIT_LEN) - 1; pub const ENV_WORKER_ID: &str = "MEGA_ID_GENERATOR_WORKER_ID"; +/// Default `base_time` from the `idgenerator` crate (2020-02-20T00:00:02Z). +pub const IDGENERATOR_BASE_TIME_MS: u64 = 1_582_136_402_000; + +/// JavaScript `Number.MAX_SAFE_INTEGER` (`2^53 - 1`). Post-#2177 8+8 layout +/// IDs exceed this; HTTP JSON must serialize snowflake ids as strings +/// (`ceres::model::serde_snowflake`). +pub const JS_MAX_SAFE_INTEGER: u64 = (1 << 53) - 1; + +/// Approximate snowflake magnitude at `delta_ms` after [`IDGENERATOR_BASE_TIME_MS`] +/// (ignores worker/seq low bits). Pure / deterministic — no `IdInstance`. +pub fn approx_id_at(delta_ms: u64) -> u64 { + delta_ms << (WORKER_ID_BIT_LEN + SEQ_BIT_LEN) as u32 +} + static ID_GENERATOR_INIT: Once = Once::new(); static CLAIMED_WORKER_ID: OnceLock = OnceLock::new(); @@ -162,4 +176,49 @@ mod tests { hash_worker_id("mono-engine-5f6d8d7cc9-rknxw") ); } + + #[test] + fn timestamp_shift_is_explicit() { + assert_eq!( + WORKER_ID_BIT_LEN + SEQ_BIT_LEN, + 16, + "timestamp_shift changed; review ceres serde_snowflake + OpenAPI/generated.ts \ + (JS Number.MAX_SAFE_INTEGER is {JS_MAX_SAFE_INTEGER})" + ); + } + + #[test] + fn current_layout_exceeds_js_safe_integer_at_pinned_now() { + // 2026-09-01T00:00:00Z relative to idgenerator default base_time. + const PINNED_NOW_MS: u64 = 1_788_220_800_000; + let delta_ms = PINNED_NOW_MS - IDGENERATOR_BASE_TIME_MS; + let approx = approx_id_at(delta_ms); + assert!( + approx > JS_MAX_SAFE_INTEGER, + "expected 8+8 layout to exceed JS MAX_SAFE at pinned now; \ + approx={approx}, MAX_SAFE={JS_MAX_SAFE_INTEGER}. \ + If you intentionally shrank the layout, update the JSON string contract." + ); + // Pre-#2177 was effectively shift=14 (worker 6 + default seq 8); same + // moment stayed under the JS budget. + let legacy_approx = delta_ms << 14; + assert!( + legacy_approx <= JS_MAX_SAFE_INTEGER, + "fixture assumption broken: legacy shift=14 should still fit JS Number" + ); + } + + #[test] + fn js_f64_truncates_post_2177_id() { + // Real group id that lost a digit in the browser (…277 → …276). + let id: i64 = 13_502_510_928_822_277; + assert!( + id as u64 > JS_MAX_SAFE_INTEGER, + "fixture must be above JS MAX_SAFE_INTEGER" + ); + assert_ne!( + id, id as f64 as i64, + "IEEE f64 (JS Number) must truncate this snowflake; if not, update the fixture" + ); + } } diff --git a/mono/src/api/router/bot_router.rs b/mono/src/api/router/bot_router.rs index e3cad8d9f..73b0cbb91 100644 --- a/mono/src/api/router/bot_router.rs +++ b/mono/src/api/router/bot_router.rs @@ -5,9 +5,12 @@ use axum::{ extract::{Path, State}, http::{HeaderMap, StatusCode}, }; -use ceres::model::bots::{ - BootstrapInitBotResponse, BotRes, ChangeInstallationStatus, CreateBotTokenRequest, - CreateBotTokenResponse, InstallBotReq, InstallationTargetType, ListBotTokenItem, +use ceres::model::{ + bots::{ + BootstrapInitBotResponse, BotRes, ChangeInstallationStatus, CreateBotTokenRequest, + CreateBotTokenResponse, InstallBotReq, InstallationTargetType, ListBotTokenItem, + }, + serde_snowflake::SnowflakeId, }; use chrono::{Duration, Utc}; use jupiter::sea_orm::prelude::DateTimeWithTimeZone; @@ -140,7 +143,7 @@ async fn bootstrap_init_bot( #[utoipa::path( post, params( - ("id", description = "Bots ID"), + ("id" = SnowflakeId, Path, description = "Bots ID"), ), path = "/{id}/installations", responses( @@ -150,7 +153,7 @@ async fn bootstrap_init_bot( )] async fn install_bot( state: State, - Path(id): Path, + Path(SnowflakeId(id)): Path, Json(json): Json, ) -> Result>, ApiError> { let bot = state.services().admin().install_bot(id, json).await?; @@ -162,7 +165,7 @@ async fn install_bot( #[utoipa::path( get, params( - ("id", description = "Bots ID"), + ("id" = SnowflakeId, Path, description = "Bots ID"), ), path = "/{id}/installations", responses( @@ -172,7 +175,7 @@ async fn install_bot( )] async fn list_installed_bot( state: State, - Path(id): Path, + Path(SnowflakeId(id)): Path, ) -> Result>>, ApiError> { let models = state.services().admin().list_installed_bots(id).await?; @@ -182,8 +185,8 @@ async fn list_installed_bot( #[utoipa::path( patch, params( - ("id", description = "Bot ID"), - ("installation_id", description = "Installation ID"), + ("id" = SnowflakeId, Path, description = "Bot ID"), + ("installation_id" = SnowflakeId, Path, description = "Installation ID"), ), path = "/{id}/installations/{installation_id}", responses( @@ -193,13 +196,13 @@ async fn list_installed_bot( )] async fn change_installation_status( state: State, - Path((id, installation_id)): Path<(i64, i64)>, + Path((id, installation_id)): Path<(SnowflakeId, SnowflakeId)>, Json(json): Json, ) -> Result>, ApiError> { let model = state .services() .admin() - .change_bot_installation_status(id, installation_id, json) + .change_bot_installation_status(id.into(), installation_id.into(), json) .await?; Ok(Json(CommonResult::success(Some(model)))) @@ -208,8 +211,8 @@ async fn change_installation_status( #[utoipa::path( delete, params( - ("id", description = "Bot ID"), - ("installation_id", description = "Installation ID"), + ("id" = SnowflakeId, Path, description = "Bot ID"), + ("installation_id" = SnowflakeId, Path, description = "Installation ID"), ), path = "/{id}/installations/{installation_id}", responses( @@ -219,13 +222,13 @@ async fn change_installation_status( )] async fn uninstall_bot( state: State, - Path((id, installation_id)): Path<(i64, i64)>, + Path((id, installation_id)): Path<(SnowflakeId, SnowflakeId)>, Json(target_type): Json, ) -> Result>, ApiError> { state .services() .admin() - .uninstall_bot(id, target_type, installation_id) + .uninstall_bot(id.into(), target_type, installation_id.into()) .await?; Ok(Json(CommonResult::success(Some( @@ -241,7 +244,7 @@ async fn uninstall_bot( path = "/{bot_id}/tokens", request_body = CreateBotTokenRequest, params( - ("bot_id" = i64, Path, description = "Bot ID") + ("bot_id" = SnowflakeId, Path, description = "Bot ID") ), responses( (status = 200, body = CommonResult), @@ -255,7 +258,7 @@ async fn uninstall_bot( async fn create_bot_token( user: LoginUser, State(state): State, - Path(bot_id): Path, + Path(SnowflakeId(bot_id)): Path, Json(req): Json, ) -> Result>, ApiError> { ensure_admin(&state, &user).await?; @@ -292,7 +295,7 @@ async fn create_bot_token( get, path = "/{bot_id}/tokens", params( - ("bot_id" = i64, Path, description = "Bot ID") + ("bot_id" = SnowflakeId, Path, description = "Bot ID") ), responses( (status = 200, body = CommonResult>), @@ -305,7 +308,7 @@ async fn create_bot_token( async fn list_bot_tokens( user: LoginUser, State(state): State, - Path(bot_id): Path, + Path(SnowflakeId(bot_id)): Path, ) -> Result>>, ApiError> { ensure_admin(&state, &user).await?; ensure_bot_exists(&state, bot_id).await?; @@ -322,8 +325,8 @@ async fn list_bot_tokens( delete, path = "/{bot_id}/tokens/{id}", params( - ("bot_id" = i64, Path, description = "Bot ID"), - ("id" = i64, Path, description = "Token ID") + ("bot_id" = SnowflakeId, Path, description = "Bot ID"), + ("id" = SnowflakeId, Path, description = "Token ID") ), responses( (status = 200, description = "Token revoked successfully"), @@ -336,15 +339,15 @@ async fn list_bot_tokens( async fn revoke_bot_token( user: LoginUser, State(state): State, - Path((bot_id, token_id)): Path<(i64, i64)>, + Path((bot_id, token_id)): Path<(SnowflakeId, SnowflakeId)>, ) -> Result>, ApiError> { ensure_admin(&state, &user).await?; - ensure_bot_exists(&state, bot_id).await?; + ensure_bot_exists(&state, bot_id.into()).await?; state .services() .admin() - .revoke_bot_token(bot_id, token_id) + .revoke_bot_token(bot_id.into(), token_id.into()) .await?; Ok(Json(CommonResult::success(None))) @@ -357,7 +360,7 @@ async fn revoke_bot_token( post, path = "/{bot_id}/tokens/revoke_all", params( - ("bot_id" = i64, Path, description = "Bot ID") + ("bot_id" = SnowflakeId, Path, description = "Bot ID") ), responses( (status = 200, description = "All tokens revoked successfully"), @@ -370,7 +373,7 @@ async fn revoke_bot_token( async fn revoke_all_bot_tokens( user: LoginUser, State(state): State, - Path(bot_id): Path, + Path(SnowflakeId(bot_id)): Path, ) -> Result>, ApiError> { ensure_admin(&state, &user).await?; ensure_bot_exists(&state, bot_id).await?; diff --git a/mono/src/api/router/build_trigger_router.rs b/mono/src/api/router/build_trigger_router.rs index ce61138e3..a2851c9c2 100644 --- a/mono/src/api/router/build_trigger_router.rs +++ b/mono/src/api/router/build_trigger_router.rs @@ -8,8 +8,9 @@ use axum::{ Json, extract::{Path, State}, }; -use ceres::application::build_trigger::{ - CreateTriggerRequest, ListTriggersParams, TriggerResponse, +use ceres::{ + application::build_trigger::{CreateTriggerRequest, ListTriggersParams, TriggerResponse}, + model::serde_snowflake::SnowflakeId, }; use utoipa_axum::{router::OpenApiRouter, routes}; @@ -103,7 +104,7 @@ async fn list_triggers( get, path = "/{id}", params( - ("id" = i64, Path, description = "Trigger ID") + ("id" = SnowflakeId, Path, description = "Trigger ID") ), responses( (status = 200, body = CommonResult, content_type = "application/json"), @@ -115,7 +116,7 @@ async fn list_triggers( async fn get_trigger( _user: LoginUser, state: State, - Path(id): Path, + Path(SnowflakeId(id)): Path, ) -> Result>, ApiError> { let service = state.services().build_trigger(); let response = service.get_trigger(id).await?; @@ -131,7 +132,7 @@ async fn get_trigger( post, path = "/{id}/retry", params( - ("id" = i64, Path, description = "Original trigger ID to retry") + ("id" = SnowflakeId, Path, description = "Original trigger ID to retry") ), responses( (status = 200, body = CommonResult, content_type = "application/json"), @@ -143,7 +144,7 @@ async fn get_trigger( async fn retry_trigger( user: LoginUser, state: State, - Path(id): Path, + Path(SnowflakeId(id)): Path, ) -> Result>, ApiError> { let actor = collaboration_actor(&user)?; let service = state.services().build_trigger(); diff --git a/mono/src/api/router/code_review_router.rs b/mono/src/api/router/code_review_router.rs index 8a31394df..a70048e83 100644 --- a/mono/src/api/router/code_review_router.rs +++ b/mono/src/api/router/code_review_router.rs @@ -3,9 +3,12 @@ use axum::{ Json, extract::{Path, State}, }; -use ceres::model::code_review::{ - CodeReviewResponse, CommentReplyRequest, CommentReviewResponse, InitializeCommentRequest, - ThreadReviewResponse, ThreadStatusResponse, UpdateCommentRequest, +use ceres::model::{ + code_review::{ + CodeReviewResponse, CommentReplyRequest, CommentReviewResponse, InitializeCommentRequest, + ThreadReviewResponse, ThreadStatusResponse, UpdateCommentRequest, + }, + serde_snowflake::SnowflakeId, }; use utoipa_axum::{router::OpenApiRouter, routes}; @@ -86,7 +89,7 @@ async fn initialize_code_review_comment( #[utoipa::path( post, params( - ("thread_id", description = "Code Review Comment Thread ID"), + ("thread_id" = SnowflakeId, Path, description = "Code Review Comment Thread ID"), ), path = "/{thread_id}/comment/reply", responses( @@ -96,7 +99,7 @@ async fn initialize_code_review_comment( )] async fn reply_code_review_comment( user: LoginUser, - Path(thread_id): Path, + Path(SnowflakeId(thread_id)): Path, state: State, Json(payload): Json, ) -> Result>, ApiError> { @@ -114,7 +117,7 @@ async fn reply_code_review_comment( #[utoipa::path( post, params( - ("comment_id", description = "A numeric ID representing a comment"), + ("comment_id" = SnowflakeId, Path, description = "A numeric ID representing a comment"), ), path = "/{comment_id}/update", responses( @@ -124,7 +127,7 @@ async fn reply_code_review_comment( )] async fn update_code_review_comment( user: LoginUser, - Path(comment_id): Path, + Path(SnowflakeId(comment_id)): Path, state: State, Json(payload): Json, ) -> Result>, ApiError> { @@ -142,7 +145,7 @@ async fn update_code_review_comment( #[utoipa::path( post, params( - ("thread_id", description = "A numeric ID representing a code review thread"), + ("thread_id" = SnowflakeId, Path, description = "A numeric ID representing a code review thread"), ), path = "/{thread_id}/resolve", responses( @@ -151,7 +154,7 @@ async fn update_code_review_comment( tag = CODE_REVIEW_TAG, )] async fn resolve_code_review_thread( - Path(thread_id): Path, + Path(SnowflakeId(thread_id)): Path, state: State, ) -> Result>, ApiError> { let thread = state @@ -167,7 +170,7 @@ async fn resolve_code_review_thread( #[utoipa::path( post, params( - ("thread_id", description = "A numeric ID representing a code review thread"), + ("thread_id" = SnowflakeId, Path, description = "A numeric ID representing a code review thread"), ), path = "/{thread_id}/reopen", responses( @@ -176,7 +179,7 @@ async fn resolve_code_review_thread( tag = CODE_REVIEW_TAG, )] async fn reopen_code_review_thread( - Path(thread_id): Path, + Path(SnowflakeId(thread_id)): Path, state: State, ) -> Result>, ApiError> { let thread = state @@ -192,7 +195,7 @@ async fn reopen_code_review_thread( #[utoipa::path( delete, params( - ("thread_id", description = "A numeric ID representing a code review thread"), + ("thread_id" = SnowflakeId, Path, description = "A numeric ID representing a code review thread"), ), path = "/thread/{thread_id}", responses( @@ -201,7 +204,7 @@ async fn reopen_code_review_thread( tag = CODE_REVIEW_TAG, )] async fn delete_code_review_thread( - Path(thread_id): Path, + Path(SnowflakeId(thread_id)): Path, state: State, ) -> Result>, ApiError> { state @@ -217,7 +220,7 @@ async fn delete_code_review_thread( #[utoipa::path( delete, params( - ("comment_id", description = "A numeric ID representing a code review comment"), + ("comment_id" = SnowflakeId, Path, description = "A numeric ID representing a code review comment"), ), path = "/comment/{comment_id}", responses( @@ -227,7 +230,7 @@ async fn delete_code_review_thread( )] async fn delete_code_review_comment( user: LoginUser, - Path(comment_id): Path, + Path(SnowflakeId(comment_id)): Path, state: State, ) -> Result>, ApiError> { let actor = collaboration_actor(&user)?; diff --git a/mono/src/api/router/conv_router.rs b/mono/src/api/router/conv_router.rs index adc5a9215..006da8a93 100644 --- a/mono/src/api/router/conv_router.rs +++ b/mono/src/api/router/conv_router.rs @@ -3,7 +3,10 @@ use axum::{ Json, extract::{Path, State}, }; -use ceres::model::conversation::{ContentPayload, ReactionRequest}; +use ceres::model::{ + conversation::{ContentPayload, ReactionRequest}, + serde_snowflake::SnowflakeId, +}; use utoipa_axum::{router::OpenApiRouter, routes}; use crate::api::{ @@ -26,7 +29,7 @@ pub fn routers() -> OpenApiRouter { #[utoipa::path( post, params( - ("comment_id", description = "A numeric ID representing either a comment or a conversation. Specify the type in the request body."), + ("comment_id" = SnowflakeId, Path, description = "A numeric ID representing either a comment or a conversation. Specify the type in the request body."), ), path = "/{comment_id}/reactions", request_body = ReactionRequest, @@ -37,7 +40,7 @@ pub fn routers() -> OpenApiRouter { )] async fn comment_reactions( user: LoginUser, - Path(comment_id): Path, + Path(SnowflakeId(comment_id)): Path, state: State, Json(payload): Json, ) -> Result>, ApiError> { @@ -85,7 +88,7 @@ async fn delete_comment_reaction( #[utoipa::path( delete, params( - ("comment_id", description = "A numeric ID representing a comment"), + ("comment_id" = SnowflakeId, Path, description = "A numeric ID representing a comment"), ), path = "/{comment_id}", responses( @@ -94,7 +97,7 @@ async fn delete_comment_reaction( tag = CONV_TAG )] async fn delete_comment( - Path(comment_id): Path, + Path(SnowflakeId(comment_id)): Path, state: State, ) -> Result>, ApiError> { state @@ -109,7 +112,7 @@ async fn delete_comment( #[utoipa::path( post, params( - ("comment_id", description = "A numeric ID representing a comment"), + ("comment_id" = SnowflakeId, Path, description = "A numeric ID representing a comment"), ), path = "/{comment_id}", request_body = ContentPayload, @@ -120,7 +123,7 @@ async fn delete_comment( )] async fn edit_comment( _: LoginUser, - Path(comment_id): Path, + Path(SnowflakeId(comment_id)): Path, state: State, Json(payload): Json, ) -> Result>, ApiError> { diff --git a/mono/src/api/router/group_router.rs b/mono/src/api/router/group_router.rs index 9c3abd577..9d58c87af 100644 --- a/mono/src/api/router/group_router.rs +++ b/mono/src/api/router/group_router.rs @@ -4,11 +4,14 @@ use axum::{ Json, extract::{Path, State}, }; -use ceres::model::group::{ - AddMembersRequest, CreateGroupRequest, DeleteGroupResponse, DeletePermissionsResponse, - EmptyListAdditional, GroupMemberResponse, GroupResponse, RemoveMemberResponse, - ResourcePermissionResponse, SetPermissionsRequest, UpdateGroupRequest, - UserEffectivePermissionResponse, UserGroupsResponse, +use ceres::model::{ + group::{ + AddMembersRequest, CreateGroupRequest, DeleteGroupResponse, DeletePermissionsResponse, + EmptyListAdditional, GroupMemberResponse, GroupResponse, RemoveMemberResponse, + ResourcePermissionResponse, SetPermissionsRequest, UpdateGroupRequest, + UserEffectivePermissionResponse, UserGroupsResponse, + }, + serde_snowflake::SnowflakeId, }; use utoipa_axum::{router::OpenApiRouter, routes}; @@ -104,7 +107,7 @@ async fn list_groups( get, path = "/groups/{group_id}", params( - ("group_id" = i64, Path, description = "Group ID") + ("group_id" = SnowflakeId, Path, description = "Group ID") ), responses( (status = 200, body = CommonResult), @@ -117,7 +120,7 @@ async fn list_groups( async fn get_group( user: LoginUser, State(state): State, - Path(group_id): Path, + Path(SnowflakeId(group_id)): Path, ) -> Result>, ApiError> { ensure_admin(&state, &user).await?; @@ -143,7 +146,7 @@ async fn get_group( path = "/groups/{group_id}", request_body = UpdateGroupRequest, params( - ("group_id" = i64, Path, description = "Group ID") + ("group_id" = SnowflakeId, Path, description = "Group ID") ), responses( (status = 200, body = CommonResult), @@ -158,7 +161,7 @@ async fn get_group( async fn update_group( user: LoginUser, State(state): State, - Path(group_id): Path, + Path(SnowflakeId(group_id)): Path, Json(req): Json, ) -> Result>, ApiError> { ensure_admin(&state, &user).await?; @@ -182,7 +185,7 @@ async fn update_group( delete, path = "/groups/{group_id}", params( - ("group_id" = i64, Path, description = "Group ID") + ("group_id" = SnowflakeId, Path, description = "Group ID") ), responses( (status = 200, body = CommonResult), @@ -195,7 +198,7 @@ async fn update_group( async fn delete_group( user: LoginUser, State(state): State, - Path(group_id): Path, + Path(SnowflakeId(group_id)): Path, ) -> Result>, ApiError> { ensure_admin(&state, &user).await?; @@ -214,7 +217,7 @@ async fn delete_group( path = "/groups/{group_id}/members", request_body = AddMembersRequest, params( - ("group_id" = i64, Path, description = "Group ID") + ("group_id" = SnowflakeId, Path, description = "Group ID") ), responses( (status = 200, body = CommonResult>), @@ -228,7 +231,7 @@ async fn delete_group( async fn add_group_members( user: LoginUser, State(state): State, - Path(group_id): Path, + Path(SnowflakeId(group_id)): Path, Json(req): Json, ) -> Result>>, ApiError> { ensure_admin(&state, &user).await?; @@ -247,7 +250,7 @@ async fn add_group_members( delete, path = "/groups/{group_id}/members/{username}", params( - ("group_id" = i64, Path, description = "Group ID"), + ("group_id" = SnowflakeId, Path, description = "Group ID"), ("username" = String, Path, description = "Campsite user id of the member") ), responses( @@ -261,18 +264,18 @@ async fn add_group_members( async fn remove_group_member( user: LoginUser, State(state): State, - Path((group_id, username)): Path<(i64, String)>, + Path((group_id, username)): Path<(SnowflakeId, String)>, ) -> Result>, ApiError> { ensure_admin(&state, &user).await?; // Path `username` is campsite_user_id. let removed = state .services() .admin() - .remove_group_member(group_id, &username) + .remove_group_member(group_id.into(), &username) .await?; Ok(Json(CommonResult::success(Some(RemoveMemberResponse { - group_id, + group_id: group_id.into(), username, removed, })))) @@ -283,7 +286,7 @@ async fn remove_group_member( path = "/groups/{group_id}/members/list", request_body = PageParams, params( - ("group_id" = i64, Path, description = "Group ID") + ("group_id" = SnowflakeId, Path, description = "Group ID") ), responses( (status = 200, body = CommonResult>), @@ -297,7 +300,7 @@ async fn remove_group_member( async fn list_group_members( user: LoginUser, State(state): State, - Path(group_id): Path, + Path(SnowflakeId(group_id)): Path, Json(json): Json>, ) -> Result>>, ApiError> { ensure_admin(&state, &user).await?; diff --git a/mono/src/api/router/label_router.rs b/mono/src/api/router/label_router.rs index 9bf2c8155..511da1585 100644 --- a/mono/src/api/router/label_router.rs +++ b/mono/src/api/router/label_router.rs @@ -3,7 +3,10 @@ use axum::{ Json, extract::{Path, State}, }; -use ceres::model::label::{LabelItem, NewLabel}; +use ceres::model::{ + label::{LabelItem, NewLabel}, + serde_snowflake::SnowflakeId, +}; use utoipa_axum::{router::OpenApiRouter, routes}; use crate::api::{ @@ -72,7 +75,7 @@ async fn new_label( #[utoipa::path( get, params( - ("id", description = "Label's id"), + ("id" = SnowflakeId, Path, description = "Label's id"), ), path = "/{id}", responses( @@ -82,7 +85,7 @@ async fn new_label( )] async fn fetch_label( state: State, - Path(id): Path, + Path(SnowflakeId(id)): Path, ) -> Result>, ApiError> { let label = state.services().issue().get_label_by_id(id).await?; Ok(Json(CommonResult::success(label))) diff --git a/mono/src/api/router/user_router.rs b/mono/src/api/router/user_router.rs index 07ebd67e7..a5b0c8c25 100644 --- a/mono/src/api/router/user_router.rs +++ b/mono/src/api/router/user_router.rs @@ -8,6 +8,7 @@ use ceres::model::{ notification::{ NotificationEventTypeInfo, UpdateUserNotificationConfig, UserNotificationConfig, }, + serde_snowflake::SnowflakeId, user::{ AddSSHKey, ClaContentRes, ClaSignStatusRes, ListSSHKey, ListToken, UpdateClaContentPayload, UserApprovalStatusRes, @@ -101,7 +102,7 @@ async fn add_key( #[utoipa::path( delete, params( - ("key_id", description = "A numeric ID representing a SSH"), + ("key_id" = SnowflakeId, Path, description = "A numeric ID representing a SSH"), ), path = "/ssh/{key_id}", responses( @@ -112,7 +113,7 @@ async fn add_key( async fn remove_key( user: LoginUser, state: State, - Path(key_id): Path, + Path(SnowflakeId(key_id)): Path, ) -> Result>, ApiError> { let campsite_user_id = collaboration_actor(&user)?.to_string(); state @@ -172,7 +173,7 @@ async fn generate_token( #[utoipa::path( delete, params( - ("key_id", description = "A numeric ID representing a User Token"), + ("key_id" = SnowflakeId, Path, description = "A numeric ID representing a User Token"), ), path = "/token/{key_id}", responses( @@ -183,7 +184,7 @@ async fn generate_token( async fn remove_token( user: LoginUser, state: State, - Path(key_id): Path, + Path(SnowflakeId(key_id)): Path, ) -> Result>, ApiError> { let campsite_user_id = collaboration_actor(&user)?.to_string(); state diff --git a/mono/src/api/router/webhook_router.rs b/mono/src/api/router/webhook_router.rs index daa8470d4..c227913df 100644 --- a/mono/src/api/router/webhook_router.rs +++ b/mono/src/api/router/webhook_router.rs @@ -3,7 +3,10 @@ use axum::{ Json, extract::{Path, Query, State}, }; -use ceres::model::webhook::{CreateWebhookRequest, ListWebhooksQuery, WebhookResponse}; +use ceres::model::{ + serde_snowflake::SnowflakeId, + webhook::{CreateWebhookRequest, ListWebhooksQuery, WebhookResponse}, +}; use utoipa_axum::{router::OpenApiRouter, routes}; use crate::api::{MonoApiServiceState, api_doc::WEBHOOK_TAG, error::ApiError}; @@ -61,7 +64,7 @@ async fn list_webhooks( /// Delete a webhook #[utoipa::path( delete, - params(("id", description = "Webhook ID")), + params(("id" = SnowflakeId, Path, description = "Webhook ID")), path = "/webhooks/{id}", responses( (status = 200, body = CommonResult, content_type = "application/json"), @@ -71,7 +74,7 @@ async fn list_webhooks( )] async fn delete_webhook( state: State, - Path(id): Path, + Path(SnowflakeId(id)): Path, ) -> Result>, ApiError> { state.services().webhook().delete_webhook(id).await?; Ok(Json(CommonResult::success(None))) diff --git a/moon/api/README.md b/moon/api/README.md index d8054e668..b1b92c064 100644 --- a/moon/api/README.md +++ b/moon/api/README.md @@ -33,6 +33,8 @@ curl -sS http://localhost:8004/api-doc/openapi.json -o api/gen/orion.json `script/gen-client` refreshes `gitmono.json` from the live mono OpenAPI, runs [`merge-swagger.js`](merge-swagger.js) (merges every `api/gen/*.json` except `merged_swagger.json` / `openapi_schema.json`), then regenerates `packages/types/generated.ts`. +`merge-swagger.js` also rewrites Mega snowflake path params / known id fields from OpenAPI `int64` to `string` so the generated TypeScript client keeps full precision in JavaScript. + ## Related endpoints | Service | Swagger UI | OpenAPI JSON | diff --git a/moon/api/merge-swagger.js b/moon/api/merge-swagger.js index ddc19e24c..d3dd78c10 100644 --- a/moon/api/merge-swagger.js +++ b/moon/api/merge-swagger.js @@ -51,6 +51,138 @@ const merged = files { openapi: '3.0.0', info: {}, paths: {}, components: {} } ) +/** + * Mega snowflake ids exceed JS Number.MAX_SAFE_INTEGER. Response schemas already + * use string (serde + utoipa value_type), but path params often emit int64 or a + * `$ref: SnowflakeId` (sometimes without the schema component). Rewrite those + * path params to string so swagger-typescript-api generates `groupId: string`. + */ +const SNOWFLAKE_PATH_PARAM_NAMES = new Set([ + 'group_id', + 'thread_id', + 'comment_id', + 'key_id', + 'bot_id', + 'installation_id', + 'token_id', + 'cl_id', + 'item_id' +]) + +function isMegaSnowflakeIdPath(apiPath, paramName) { + if (SNOWFLAKE_PATH_PARAM_NAMES.has(paramName)) return true + if (paramName !== 'id') return false + return ( + apiPath.includes('/label/') || + apiPath.includes('/bots/') || + apiPath.includes('/webhooks/') || + apiPath.includes('/triggers/') || + apiPath.includes('/ssh/') || + apiPath.includes('/token/') || + apiPath.includes('/user/token') || + apiPath.includes('/user/ssh') + ) +} + +function isSnowflakeIdRef(schema) { + return typeof schema?.$ref === 'string' && schema.$ref.includes('SnowflakeId') +} + +/** + * Path params may be int64 (axum_extras) or `$ref: SnowflakeId` (utoipa Path type). + * The SnowflakeId component is often missing from the emitted OpenAPI, so rewrite + * both forms to inline `string` for STA. + */ +function rewriteSnowflakePathParams(doc) { + let rewritten = 0 + for (const [apiPath, pathItem] of Object.entries(doc.paths || {})) { + if (!pathItem || typeof pathItem !== 'object') continue + for (const op of Object.values(pathItem)) { + if (!op || typeof op !== 'object' || !Array.isArray(op.parameters)) continue + for (const param of op.parameters) { + if (param.in !== 'path') continue + const schema = param.schema + if (!schema) continue + if (schema.type === 'string') continue + + const refSnowflake = isSnowflakeIdRef(schema) + if (!refSnowflake && !isMegaSnowflakeIdPath(apiPath, param.name)) continue + + if (refSnowflake || schema.type === 'integer' || schema.format === 'int64' || schema.format === 'int32') { + param.schema = { + type: 'string', + description: schema.description || param.description + } + rewritten += 1 + } + } + } + } + + // Keep a string SnowflakeId component if anything still $refs it. + if (!doc.components) doc.components = {} + if (!doc.components.schemas) doc.components.schemas = {} + const existing = doc.components.schemas.SnowflakeId + if (!existing || existing.type !== 'string') { + doc.components.schemas.SnowflakeId = { + type: 'string', + description: 'Snowflake id; JSON string so JS keeps full precision.' + } + } + + return rewritten +} + +/** Also rewrite request/response body properties that are snowflake ids (e.g. Orion cl_id). */ +function rewriteInt64PropToString(propSchema) { + if (!propSchema || propSchema.type === 'string') return false + if (propSchema.type === 'integer' || propSchema.format === 'int64') { + const description = propSchema.description + Object.keys(propSchema).forEach((k) => delete propSchema[k]) + propSchema.type = 'string' + if (description) propSchema.description = description + return true + } + return false +} + +function rewriteSnowflakeSchemaProps(doc) { + let rewritten = 0 + const schemas = doc.components?.schemas || {} + for (const schema of Object.values(schemas)) { + const props = schema?.properties + if (!props) continue + for (const [name, prop] of Object.entries(props)) { + if ( + name === 'cl_id' || + name === 'group_id' || + name === 'item_id' || + name === 'bot_id' || + name === 'thread_id' || + name === 'comment_id' || + name === 'token_id' || + name === 'installation_id' || + name === 'key_id' || + name === 'conversation_id' || + name === 'parent_comment_id' || + name === 'anchor_id' || + name === 'position_id' + ) { + if (rewriteInt64PropToString(prop)) rewritten += 1 + } + } + } + return rewritten +} + +const rewrittenPathCount = rewriteSnowflakePathParams(merged) +const rewrittenSchemaCount = rewriteSnowflakeSchemaProps(merged) + // 输出文件 fs.writeFileSync(outputFile, JSON.stringify(merged, null, 2)) console.log(`Swagger JSON 文件合并完成,已生成 ${outputFile} 🎉`) +if (rewrittenPathCount > 0 || rewrittenSchemaCount > 0) { + console.log( + `Rewrote snowflake ids to string (path params: ${rewrittenPathCount}, schema props: ${rewrittenSchemaCount})` + ) +} diff --git a/moon/apps/web/.env.runtime b/moon/apps/web/.env.runtime index 753962fea..5f6fe28ab 100644 --- a/moon/apps/web/.env.runtime +++ b/moon/apps/web/.env.runtime @@ -9,7 +9,7 @@ # Placeholders are intentionally valid absolute URLs so that `next build` # rewrite/URL validation passes. Do NOT reuse these hostnames for anything real. # -# Provide the real values at runtime (ECS task definition / Cloud Run env): +# Provide the real values at runtime (ECS task definition / Cloud Run env / k8s): # NEXT_PUBLIC_API_URL, NEXT_PUBLIC_INTERNAL_API_URL, NEXT_PUBLIC_MONO_API_URL, # NEXT_PUBLIC_ORION_API_URL, NEXT_PUBLIC_AUTH_URL, NEXT_PUBLIC_WEB_URL, # NEXT_PUBLIC_SYNC_URL, NEXT_PUBLIC_CRATES_PRO_URL diff --git a/moon/apps/web/components/AdminGroups/AddMembersDialog.tsx b/moon/apps/web/components/AdminGroups/AddMembersDialog.tsx index 268b2af3a..39ab044a5 100644 --- a/moon/apps/web/components/AdminGroups/AddMembersDialog.tsx +++ b/moon/apps/web/components/AdminGroups/AddMembersDialog.tsx @@ -8,7 +8,7 @@ import { useGetSyncMembers } from '@/hooks/useGetSyncMembers' import { megaUserHandle } from '@/utils/megaUser' interface AddMembersDialogProps { - groupId: number | null + groupId: string | null onClose: () => void } @@ -29,7 +29,7 @@ export const AddMembersDialog = ({ groupId, onClose }: AddMembersDialogProps) => }) // Get current group's existing users - const { data: groupMembersData, isLoading: isGroupMembersLoading } = useAdminGroupMembersList(groupId || 0, { + const { data: groupMembersData, isLoading: isGroupMembersLoading } = useAdminGroupMembersList(groupId ?? '', { pagination: { page: 1, per_page: 1000 }, // Get all group members additional: {} }) diff --git a/moon/apps/web/components/AdminGroups/AdminGroupEditDialog.tsx b/moon/apps/web/components/AdminGroups/AdminGroupEditDialog.tsx index a15bac4ca..5a80cde1d 100644 --- a/moon/apps/web/components/AdminGroups/AdminGroupEditDialog.tsx +++ b/moon/apps/web/components/AdminGroups/AdminGroupEditDialog.tsx @@ -14,7 +14,7 @@ import { legacyApiClient } from '@/utils/queryClient' interface AdminGroupEditDialogProps { open: boolean onOpenChange: (open: boolean) => void - groupId: number | null + groupId: string | null onSuccess?: (updatedData: { name: string; description: string | null }) => void } @@ -39,13 +39,20 @@ export function AdminGroupEditDialog({ open, onOpenChange, groupId, onSuccess }: const handlePermissionToggle = (resourceId: string, type: 'read' | 'write' | 'admin') => { setResourcePermissions((prev) => { const current = prev[resourceId] || { read: false, write: false, admin: false } + let next = { ...current } + + if (type === 'read') { + // Clearing read also clears higher levels; enabling read alone is read-only. + next = current.read ? { read: false, write: false, admin: false } : { read: true, write: false, admin: false } + } else if (type === 'write') { + next = current.write ? { read: true, write: false, admin: false } : { read: true, write: true, admin: false } + } else { + next = current.admin ? { read: true, write: true, admin: false } : { read: true, write: true, admin: true } + } return { ...prev, - [resourceId]: { - ...current, - [type]: !current[type] - } + [resourceId]: next } }) } @@ -94,17 +101,23 @@ export function AdminGroupEditDialog({ open, onOpenChange, groupId, onSuccess }: setSavingResources((prev) => new Set(prev).add(resourceId)) try { - const permissions = [] + // DB stores one level per (resource, group). Persist the highest selected. + let permission: PermissionValue | null = null - if (perms.read) permissions.push({ group_id: groupId, permission: PermissionValue.Read }) - if (perms.write) permissions.push({ group_id: groupId, permission: PermissionValue.Write }) - if (perms.admin) permissions.push({ group_id: groupId, permission: PermissionValue.Admin }) + if (perms.admin) permission = PermissionValue.Admin + else if (perms.write) permission = PermissionValue.Write + else if (perms.read) permission = PermissionValue.Read + + if (!permission) { + toast.error('Please select at least one permission') + return + } await postResourcePermissions.mutateAsync({ resourceType: 'note', resourceId: resourceId, data: { - permissions + permissions: [{ group_id: groupId, permission }] } }) @@ -127,7 +140,7 @@ export function AdminGroupEditDialog({ open, onOpenChange, groupId, onSuccess }: return perms && (perms.read || perms.write || perms.admin) } - const { data: groupData, isLoading, error } = useGetAdminGroupById(groupId || 0, { enabled: !!groupId && open }) + const { data: groupData, isLoading, error } = useGetAdminGroupById(groupId ?? '', { enabled: !!groupId && open }) const { data: resourcesData, @@ -172,15 +185,22 @@ export function AdminGroupEditDialog({ open, onOpenChange, groupId, onSuccess }: const response = await legacyApiClient.v1.getApiAdminResourcesPermissions().request('note', resource.id) if (response?.data && Array.isArray(response.data)) { - const groupPermissions = response.data.filter((p) => p.group_id === groupId) + const groupPermissions = response.data.filter((p) => String(p.group_id) === String(groupId)) if (groupPermissions.length > 0) { + // Stored value is a single max level; expand for hierarchical checkboxes. const perms = { read: false, write: false, admin: false } groupPermissions.forEach((p) => { - if (p.permission === 'read') perms.read = true - if (p.permission === 'write') perms.write = true - if (p.permission === 'admin') perms.admin = true + if (p.permission === 'read' || p.permission === 'write' || p.permission === 'admin') { + perms.read = true + } + if (p.permission === 'write' || p.permission === 'admin') { + perms.write = true + } + if (p.permission === 'admin') { + perms.admin = true + } }) newPermissions[resource.id] = perms diff --git a/moon/apps/web/components/AdminGroups/AdminGroupItem.tsx b/moon/apps/web/components/AdminGroups/AdminGroupItem.tsx index 8d824d297..e30fb570e 100644 --- a/moon/apps/web/components/AdminGroups/AdminGroupItem.tsx +++ b/moon/apps/web/components/AdminGroups/AdminGroupItem.tsx @@ -5,7 +5,7 @@ import { UsersIcon } from '@gitmono/ui' import { AdminGroupEditDialog } from './AdminGroupEditDialog' interface GroupItem { - id: number + id: string name: string description?: string | null created_at: number @@ -14,8 +14,8 @@ interface GroupItem { interface AdminGroupItemProps { group: GroupItem - onDelete: (id: number) => void - onManageMembers: (id: number) => void + onDelete: (id: string) => void + onManageMembers: (id: string) => void onUpdate?: () => void } diff --git a/moon/apps/web/components/AdminGroups/AdminGroupsList.tsx b/moon/apps/web/components/AdminGroups/AdminGroupsList.tsx index 0ae5a85a7..f5eea6c71 100644 --- a/moon/apps/web/components/AdminGroups/AdminGroupsList.tsx +++ b/moon/apps/web/components/AdminGroups/AdminGroupsList.tsx @@ -5,7 +5,7 @@ import { LoadingSpinner } from '@gitmono/ui' import { AdminGroupItem } from './AdminGroupItem' interface GroupItem { - id: number + id: string name: string description?: string | null created_at: number @@ -17,8 +17,8 @@ interface AdminGroupsListProps { total: number isLoading: boolean isError: boolean - onDelete: (id: number) => void - onManageMembers: (id: number) => void + onDelete: (id: string) => void + onManageMembers: (id: string) => void onUpdate?: () => void } diff --git a/moon/apps/web/components/AdminGroups/DeleteGroupDialog.tsx b/moon/apps/web/components/AdminGroups/DeleteGroupDialog.tsx index 2869584f9..bdd0d746a 100644 --- a/moon/apps/web/components/AdminGroups/DeleteGroupDialog.tsx +++ b/moon/apps/web/components/AdminGroups/DeleteGroupDialog.tsx @@ -5,7 +5,7 @@ import { Button } from '@gitmono/ui' import { useDeleteAdminGroup } from '@/hooks/admin/useDeleteAdminGroup' interface DeleteGroupDialogProps { - groupId: number | null + groupId: string | null onClose: () => void } diff --git a/moon/apps/web/components/AdminGroups/GroupMembersDialog.tsx b/moon/apps/web/components/AdminGroups/GroupMembersDialog.tsx index 58fc28c94..3e2b93107 100644 --- a/moon/apps/web/components/AdminGroups/GroupMembersDialog.tsx +++ b/moon/apps/web/components/AdminGroups/GroupMembersDialog.tsx @@ -10,7 +10,7 @@ import { megaUserHandle } from '@/utils/megaUser' import { AddMembersDialog } from './AddMembersDialog' interface GroupMembersDialogProps { - groupId: number | null + groupId: string | null groupName?: string onClose: () => void } @@ -21,7 +21,7 @@ export const GroupMembersDialog = ({ groupId, groupName, onClose }: GroupMembers const memberMap = useMemberMap() // Get current group's member list - const { data: groupMembersData, isLoading } = useAdminGroupMembersList(groupId || 0, { + const { data: groupMembersData, isLoading } = useAdminGroupMembersList(groupId ?? '', { pagination: { page: 1, per_page: 1000 }, // Get all group members additional: {} }) diff --git a/moon/apps/web/components/AdminGroups/index.tsx b/moon/apps/web/components/AdminGroups/index.tsx index e894026ba..78571ebe3 100644 --- a/moon/apps/web/components/AdminGroups/index.tsx +++ b/moon/apps/web/components/AdminGroups/index.tsx @@ -11,8 +11,8 @@ import { GroupMembersDialog } from './GroupMembersDialog' export const AdminGroups = () => { const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) - const [deleteConfirmId, setDeleteConfirmId] = useState(null) - const [manageMembersGroupId, setManageMembersGroupId] = useState(null) + const [deleteConfirmId, setDeleteConfirmId] = useState(null) + const [manageMembersGroupId, setManageMembersGroupId] = useState(null) const { data, isLoading, isError } = useAdminGroupsList({ pagination: { page: 1, per_page: 20 }, @@ -22,11 +22,11 @@ export const AdminGroups = () => { const groups = data?.data?.items || [] const total = data?.data?.total || 0 - const handleDeleteGroup = (groupId: number) => { + const handleDeleteGroup = (groupId: string) => { setDeleteConfirmId(groupId) } - const handleManageMembers = (groupId: number) => { + const handleManageMembers = (groupId: string) => { setManageMembersGroupId(groupId) } diff --git a/moon/apps/web/components/ClView/LabelItem.tsx b/moon/apps/web/components/ClView/LabelItem.tsx index 50a543395..4080c54ff 100644 --- a/moon/apps/web/components/ClView/LabelItem.tsx +++ b/moon/apps/web/components/ClView/LabelItem.tsx @@ -30,15 +30,15 @@ function LabelItem({ conv }: LabelItemProps) { if (!match || match.length <= 1) return [] return match[1] .split(', ') - .map((id) => parseInt(id, 10)) - .filter((id) => !isNaN(id)) + .map((id) => id.trim()) + .filter((id) => id.length > 0) }, [conv.comment]) const labelQueries = useQueries({ queries: idList.map((id) => ({ queryKey: ['label', id], queryFn: () => legacyApiClient.v1.getApiLabelById().request(id), - enabled: id > 0 + enabled: !!id })) }) diff --git a/moon/apps/web/components/ClView/TimelineItems.tsx b/moon/apps/web/components/ClView/TimelineItems.tsx index 26dbf2dcf..cf4d47843 100644 --- a/moon/apps/web/components/ClView/TimelineItems.tsx +++ b/moon/apps/web/components/ClView/TimelineItems.tsx @@ -36,7 +36,7 @@ interface TimelineItemProps { } interface ConvItem { - id: number + id: string badge?: React.ReactNode children?: React.ReactNode isOver: boolean diff --git a/moon/apps/web/components/ClView/components/Checks/cpns/Task.tsx b/moon/apps/web/components/ClView/components/Checks/cpns/Task.tsx index 6345178ec..a57f975c5 100644 --- a/moon/apps/web/components/ClView/components/Checks/cpns/Task.tsx +++ b/moon/apps/web/components/ClView/components/Checks/cpns/Task.tsx @@ -280,7 +280,7 @@ const TaskItem = memo(function TaskItem({ onSelectBuild: (buildId: string) => void isLast?: boolean cl: string - clId?: number + clId?: string changes?: StatusProjectRelativePath[] isQueued?: boolean isLatestBuild?: boolean @@ -299,7 +299,7 @@ const TaskItem = memo(function TaskItem({ retryBuild({ build_id: build.id, cl_link: cl, - cl_id: clId ?? 0, + cl_id: clId ?? '', changes: changes ?? [], targets: [] }) diff --git a/moon/apps/web/components/ClView/components/Checks/cpns/store.ts b/moon/apps/web/components/ClView/components/Checks/cpns/store.ts index 967ee207b..e2c835b5c 100644 --- a/moon/apps/web/components/ClView/components/Checks/cpns/store.ts +++ b/moon/apps/web/components/ClView/components/Checks/cpns/store.ts @@ -33,7 +33,7 @@ export interface TargetDTO { export interface TaskInfoDTO { build_list: BuildDTO[] changes: StatusProjectRelativePath[] - cl_id: number + cl_id: string created_at: string targets: TargetDTO[] task_id: string diff --git a/moon/apps/web/components/ClView/components/HandleTime.tsx b/moon/apps/web/components/ClView/components/HandleTime.tsx index 8687020c7..763522c16 100644 --- a/moon/apps/web/components/ClView/components/HandleTime.tsx +++ b/moon/apps/web/components/ClView/components/HandleTime.tsx @@ -15,11 +15,9 @@ const HandleTime = ({ created_at }: HandleTimeProps) => { } return ( - <> - -
{time}
-
- + + {time} + ) } diff --git a/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx b/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx index a40d66ccf..30c65c580 100644 --- a/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx +++ b/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx @@ -49,62 +49,61 @@ const CloneTabs = () => { ] return ( - <> - - - - - - {open && ( - e.preventDefault()} - asChild - addDismissibleLayer - > - - - - - - - - - - - {tabContent?.map((_item) => { - return ( - - - - - -
{_item.info}
-
- ) - })} -
-
-
- )} -
-
- + + + + + + {open && ( + e.preventDefault()} + asChild + addDismissibleLayer + > + + + + + + + + + + + {tabContent?.map((_item) => { + return ( + + + + + +
{_item.info}
+
+ ) + })} +
+
+
+ )} +
+
) } diff --git a/moon/apps/web/components/DiffView/comment/CommentThread.tsx b/moon/apps/web/components/DiffView/comment/CommentThread.tsx index a11108b03..5b0cc2d02 100644 --- a/moon/apps/web/components/DiffView/comment/CommentThread.tsx +++ b/moon/apps/web/components/DiffView/comment/CommentThread.tsx @@ -45,7 +45,7 @@ export function CommentThread({ thread, clLink }: CommentThreadProps) { const [isCollapsed, setIsCollapsed] = useState(thread.status === 'Resolved') const [showReplyInput, setShowReplyInput] = useState(false) const [replyContent, setReplyContent] = useState('') - const [editingCommentId, setEditingCommentId] = useState(null) + const [editingCommentId, setEditingCommentId] = useState(null) const [editContent, setEditContent] = useState('') const { data: currentUser } = useGetCurrentUser() @@ -109,16 +109,16 @@ export function CommentThread({ thread, clLink }: CommentThreadProps) { deleteThread({ threadId: thread.thread_id }) } - const handleDeleteComment = (commentId: number) => { + const handleDeleteComment = (commentId: string) => { deleteComment({ commentId }) } - const handleStartEdit = (commentId: number, currentContent: string) => { + const handleStartEdit = (commentId: string, currentContent: string) => { setEditingCommentId(commentId) setEditContent(currentContent) } - const handleUpdateComment = (commentId: number) => { + const handleUpdateComment = (commentId: string) => { if (!editContent.trim()) return updateComment( diff --git a/moon/apps/web/components/DiffView/hooks/useDeleteComment.ts b/moon/apps/web/components/DiffView/hooks/useDeleteComment.ts index 202bf8ad6..b302d4af9 100644 --- a/moon/apps/web/components/DiffView/hooks/useDeleteComment.ts +++ b/moon/apps/web/components/DiffView/hooks/useDeleteComment.ts @@ -13,7 +13,7 @@ const deleteCommentMutation = legacyApiClient.v1.deleteApiCodeReviewCommentByCom export function useDeleteComment(link: string) { const queryClient = useQueryClient() - return useMutation({ + return useMutation({ mutationFn: ({ commentId, params }) => deleteCommentMutation.request(commentId, params), onSuccess: () => { diff --git a/moon/apps/web/components/DiffView/hooks/useDeleteThread.ts b/moon/apps/web/components/DiffView/hooks/useDeleteThread.ts index 011c6ea07..a8f470ef9 100644 --- a/moon/apps/web/components/DiffView/hooks/useDeleteThread.ts +++ b/moon/apps/web/components/DiffView/hooks/useDeleteThread.ts @@ -13,7 +13,7 @@ const deleteThreadMutation = legacyApiClient.v1.deleteApiCodeReviewThreadByThrea export function useDeleteThread(link: string) { const queryClient = useQueryClient() - return useMutation({ + return useMutation({ mutationFn: ({ threadId, params }) => deleteThreadMutation.request(threadId, params), onSuccess: () => { diff --git a/moon/apps/web/components/DiffView/hooks/useReopenThread.ts b/moon/apps/web/components/DiffView/hooks/useReopenThread.ts index c3d9206fe..8a88b3137 100644 --- a/moon/apps/web/components/DiffView/hooks/useReopenThread.ts +++ b/moon/apps/web/components/DiffView/hooks/useReopenThread.ts @@ -13,7 +13,7 @@ const reopenThreadMutation = legacyApiClient.v1.postApiCodeReviewReopen() export function useReopenThread(link: string) { const queryClient = useQueryClient() - return useMutation({ + return useMutation({ mutationFn: ({ threadId, params }) => reopenThreadMutation.request(threadId, params), onSuccess: () => { diff --git a/moon/apps/web/components/DiffView/hooks/useReplyComment.ts b/moon/apps/web/components/DiffView/hooks/useReplyComment.ts index 1003dfda5..727ca221f 100644 --- a/moon/apps/web/components/DiffView/hooks/useReplyComment.ts +++ b/moon/apps/web/components/DiffView/hooks/useReplyComment.ts @@ -16,7 +16,7 @@ export function useReplyComment(link: string) { return useMutation< PostApiCodeReviewCommentReplyData, Error, - { threadId: number; data: CommentReplyRequest; params?: RequestParams } + { threadId: string; data: CommentReplyRequest; params?: RequestParams } >({ mutationFn: ({ threadId, data, params }) => replyCommentMutation.request(threadId, data, params), diff --git a/moon/apps/web/components/DiffView/hooks/useResolveThread.ts b/moon/apps/web/components/DiffView/hooks/useResolveThread.ts index 5545b9215..b8be0ee92 100644 --- a/moon/apps/web/components/DiffView/hooks/useResolveThread.ts +++ b/moon/apps/web/components/DiffView/hooks/useResolveThread.ts @@ -13,7 +13,7 @@ const resolveThreadMutation = legacyApiClient.v1.postApiCodeReviewResolve() export function useResolveThread(link: string) { const queryClient = useQueryClient() - return useMutation({ + return useMutation({ mutationFn: ({ threadId, params }) => resolveThreadMutation.request(threadId, params), onSuccess: () => { diff --git a/moon/apps/web/components/DiffView/hooks/useUpdateComment.ts b/moon/apps/web/components/DiffView/hooks/useUpdateComment.ts index 08339c405..9b53e225f 100644 --- a/moon/apps/web/components/DiffView/hooks/useUpdateComment.ts +++ b/moon/apps/web/components/DiffView/hooks/useUpdateComment.ts @@ -16,7 +16,7 @@ export function useUpdateComment(link: string) { return useMutation< PostApiCodeReviewUpdateData, Error, - { commentId: number; data: UpdateCommentRequest; params?: RequestParams } + { commentId: string; data: UpdateCommentRequest; params?: RequestParams } >({ mutationFn: ({ commentId, data, params }) => updateCommentMutation.request(commentId, data, params), diff --git a/moon/apps/web/components/Issues/IssueDetailPage.tsx b/moon/apps/web/components/Issues/IssueDetailPage.tsx index cdeef3423..8e47771fb 100644 --- a/moon/apps/web/components/Issues/IssueDetailPage.tsx +++ b/moon/apps/web/components/Issues/IssueDetailPage.tsx @@ -245,7 +245,7 @@ export default function IssueDetailPage({ link }: { link: string }) { { data: { assignees: selected, - item_id: Number(id), + item_id: id, link } }, @@ -274,7 +274,7 @@ export default function IssueDetailPage({ link }: { link: string }) { issueLabels( { data: { - item_id: Number(id), + item_id: id, label_ids: selected, link } diff --git a/moon/apps/web/components/Issues/utils/sideEffect.tsx b/moon/apps/web/components/Issues/utils/sideEffect.tsx index 840d279c5..c53608eb9 100644 --- a/moon/apps/web/components/Issues/utils/sideEffect.tsx +++ b/moon/apps/web/components/Issues/utils/sideEffect.tsx @@ -206,16 +206,21 @@ export const useLabelsSelector = ({ labelList }: { labels: LabelItem[] - updateLabelsRequest: (selected_id: number[]) => void + updateLabelsRequest: (selected_id: string[]) => void labelList: ReturnType }) => { - const selectRef = useRef([]) - let selects: number[] = labels.map((i) => i.id) + const selectRef = useRef([]) + let selects: string[] = labels.map((i) => String(i.id)) const shouldFetch = useRef(false) const [open, setOpen] = useState(false) const handleLabels = (selected: ItemInput[]) => { - selects = [...selected.map((i) => i.id).filter((t): t is number => typeof t === 'number')] + selects = [ + ...selected + .map((i) => i.id) + .filter((t): t is string | number => t !== undefined && t !== null) + .map((t) => String(t)) + ] } const handleOpenChange = (open: boolean) => { diff --git a/moon/apps/web/components/Issues/utils/store.tsx b/moon/apps/web/components/Issues/utils/store.tsx index 0d27fc16d..383804896 100644 --- a/moon/apps/web/components/Issues/utils/store.tsx +++ b/moon/apps/web/components/Issues/utils/store.tsx @@ -18,10 +18,10 @@ import { atom } from 'jotai' // (a, b) => a.part === b.part // ) -export const issueIdAtom = atom(0) -export const clIdAtom = atom(0) +export const issueIdAtom = atom('') +export const clIdAtom = atom('') -export const FALSE_EDIT_VAL = -1 -export const editIdAtom = atom(0) +export const FALSE_EDIT_VAL = '' +export const editIdAtom = atom('') export const refreshAtom = atom(0) diff --git a/moon/apps/web/components/NoteView/index.tsx b/moon/apps/web/components/NoteView/index.tsx index 9f41d6b59..268f1397a 100644 --- a/moon/apps/web/components/NoteView/index.tsx +++ b/moon/apps/web/components/NoteView/index.tsx @@ -93,7 +93,8 @@ function InnerNoteView({ note }: { note: Note }) { }) const permission = notesPermissions?.[note.id] - const hasWritePermission = permission?.hasWrite ?? note.viewer_can_edit + // Authors always keep write; group ACL only gates non-authors. + const hasWritePermission = note.viewer_is_author || (permission?.hasWrite ?? note.viewer_can_edit) // prefetch comments useGetNoteComments({ noteId: note.id }) diff --git a/moon/apps/web/components/NotesIndex/NoteRow.tsx b/moon/apps/web/components/NotesIndex/NoteRow.tsx index 4bb603ec6..13462e89c 100644 --- a/moon/apps/web/components/NotesIndex/NoteRow.tsx +++ b/moon/apps/web/components/NotesIndex/NoteRow.tsx @@ -29,7 +29,7 @@ export const NoteRow = memo(({ note, display = 'default', hideProject = false, p const hasRead = permission?.hasRead || false const href = `/${scope}/notes/${note.id}` - const canAccess = isAdmin || hasRead + const canAccess = isAdmin || hasRead || !!note.viewer_is_author const isDisabled = !canAccess const handleSelectWithPermission = (value: string) => { diff --git a/moon/apps/web/components/NotesIndex/NotesGrid.tsx b/moon/apps/web/components/NotesIndex/NotesGrid.tsx index c74fd36cd..8a10cf044 100644 --- a/moon/apps/web/components/NotesIndex/NotesGrid.tsx +++ b/moon/apps/web/components/NotesIndex/NotesGrid.tsx @@ -75,7 +75,7 @@ function NoteGridItem({ const isAdmin = permission?.isAdmin || false const hasRead = permission?.hasRead || false - const canAccess = isAdmin || hasRead + const canAccess = isAdmin || hasRead || !!note.viewer_is_author const isDisabled = !canAccess const gridContent = ( diff --git a/moon/apps/web/hooks/CL/useDeleteClCommentDelete.ts b/moon/apps/web/hooks/CL/useDeleteClCommentDelete.ts index 763465470..583526ebc 100644 --- a/moon/apps/web/hooks/CL/useDeleteClCommentDelete.ts +++ b/moon/apps/web/hooks/CL/useDeleteClCommentDelete.ts @@ -7,7 +7,7 @@ import { legacyApiClient } from '@/utils/queryClient' export function useDeleteClCommentDelete(id: string, params?: RequestParams) { const queryClient = useQueryClient() - return useMutation({ + return useMutation({ mutationKey: legacyApiClient.v1.deleteApiConversationByCommentId().baseKey, mutationFn: (convId) => legacyApiClient.v1.deleteApiConversationByCommentId().request(convId, params), onSuccess: (_data, _convId) => { diff --git a/moon/apps/web/hooks/SSE/useGetClTask.ts b/moon/apps/web/hooks/SSE/useGetClTask.ts index 99a2d0113..22d4b82fd 100644 --- a/moon/apps/web/hooks/SSE/useGetClTask.ts +++ b/moon/apps/web/hooks/SSE/useGetClTask.ts @@ -95,7 +95,7 @@ export function useGetClTask(cl: string, params?: RequestParams) { return { build_list: buildList, changes: (task.changes ?? []) as StatusProjectRelativePath[], - cl_id: 0, + cl_id: '', created_at: task.created_at, targets: mappedTargets, task_id: task.id, diff --git a/moon/apps/web/hooks/admin/useAddAdminGroupMembers.ts b/moon/apps/web/hooks/admin/useAddAdminGroupMembers.ts index 998aaef26..7a0cf4147 100644 --- a/moon/apps/web/hooks/admin/useAddAdminGroupMembers.ts +++ b/moon/apps/web/hooks/admin/useAddAdminGroupMembers.ts @@ -7,7 +7,7 @@ import { apiErrorToast } from '@/utils/apiErrorToast' import { legacyApiClient } from '@/utils/queryClient' interface AddMembersParams { - groupId: number + groupId: string usernames: string[] } diff --git a/moon/apps/web/hooks/admin/useAdminGroupMembersList.ts b/moon/apps/web/hooks/admin/useAdminGroupMembersList.ts index f19d324a7..636e6da12 100644 --- a/moon/apps/web/hooks/admin/useAdminGroupMembersList.ts +++ b/moon/apps/web/hooks/admin/useAdminGroupMembersList.ts @@ -4,7 +4,7 @@ import type { PageParamsEmptyListAdditional, PostApiAdminGroupsMembersListData, import { legacyApiClient } from '@/utils/queryClient' -export function useAdminGroupMembersList(groupId: number, data: PageParamsEmptyListAdditional, params?: RequestParams) { +export function useAdminGroupMembersList(groupId: string, data: PageParamsEmptyListAdditional, params?: RequestParams) { return useQuery({ queryKey: [...legacyApiClient.v1.postApiAdminGroupsMembersList().requestKey(groupId), data, params], queryFn: () => legacyApiClient.v1.postApiAdminGroupsMembersList().request(groupId, data, params), diff --git a/moon/apps/web/hooks/admin/useDeleteAdminGroup.ts b/moon/apps/web/hooks/admin/useDeleteAdminGroup.ts index 90c68a485..7565c56f0 100644 --- a/moon/apps/web/hooks/admin/useDeleteAdminGroup.ts +++ b/moon/apps/web/hooks/admin/useDeleteAdminGroup.ts @@ -9,8 +9,8 @@ import { legacyApiClient } from '@/utils/queryClient' export function useDeleteAdminGroup() { const queryClient = useQueryClient() - return useMutation({ - mutationFn: (groupId: number) => legacyApiClient.v1.deleteApiAdminGroupsByGroupId().request(groupId), + return useMutation({ + mutationFn: (groupId: string) => legacyApiClient.v1.deleteApiAdminGroupsByGroupId().request(groupId), onSuccess: () => { // Refresh user groups list queryClient.invalidateQueries({ diff --git a/moon/apps/web/hooks/admin/useDeleteAdminGroupMember.ts b/moon/apps/web/hooks/admin/useDeleteAdminGroupMember.ts index 25d28417c..5fb468164 100644 --- a/moon/apps/web/hooks/admin/useDeleteAdminGroupMember.ts +++ b/moon/apps/web/hooks/admin/useDeleteAdminGroupMember.ts @@ -7,7 +7,7 @@ import { apiErrorToast } from '@/utils/apiErrorToast' import { legacyApiClient } from '@/utils/queryClient' interface DeleteMemberParams { - groupId: number + groupId: string username: string } diff --git a/moon/apps/web/hooks/admin/useGetAdminGroupById.ts b/moon/apps/web/hooks/admin/useGetAdminGroupById.ts index 108917c17..7ca5ed18c 100644 --- a/moon/apps/web/hooks/admin/useGetAdminGroupById.ts +++ b/moon/apps/web/hooks/admin/useGetAdminGroupById.ts @@ -8,7 +8,7 @@ interface UseGetAdminGroupByIdOptions extends RequestParams { enabled?: boolean } -export function useGetAdminGroupById(groupId: number, options?: UseGetAdminGroupByIdOptions) { +export function useGetAdminGroupById(groupId: string, options?: UseGetAdminGroupByIdOptions) { const { enabled = true, ...params } = options || {} return useQuery({ diff --git a/moon/apps/web/hooks/admin/useGetNotesPermissions.ts b/moon/apps/web/hooks/admin/useGetNotesPermissions.ts index 4e30fb15b..17354c560 100644 --- a/moon/apps/web/hooks/admin/useGetNotesPermissions.ts +++ b/moon/apps/web/hooks/admin/useGetNotesPermissions.ts @@ -47,6 +47,8 @@ export function useGetNotesPermissions({ notes, enabled = true }: UseGetNotesPer // Batch query permissions for all notes const permissionsPromises = notes.map(async (note) => { + const isAuthor = !!note.viewer_is_author + try { let response @@ -63,8 +65,9 @@ export function useGetNotesPermissions({ notes, enabled = true }: UseGetNotesPer if (response?.data) { return { noteId: note.id, - hasRead: response.data.has_read, - hasWrite: response.data.has_write, + // Authors always retain access even when group ACL has no grant. + hasRead: response.data.has_read || isAuthor, + hasWrite: response.data.has_write || isAuthor, isAdmin: response.data.is_admin } } @@ -75,8 +78,8 @@ export function useGetNotesPermissions({ notes, enabled = true }: UseGetNotesPer return { noteId: note.id, - hasRead: false, - hasWrite: false, + hasRead: isAuthor, + hasWrite: isAuthor, isAdmin: false } }) diff --git a/moon/apps/web/hooks/admin/usePostResourcePermissions.ts b/moon/apps/web/hooks/admin/usePostResourcePermissions.ts index 7b2ec614e..f8ae6cc94 100644 --- a/moon/apps/web/hooks/admin/usePostResourcePermissions.ts +++ b/moon/apps/web/hooks/admin/usePostResourcePermissions.ts @@ -17,7 +17,7 @@ export function usePostResourcePermissions() { return useMutation({ mutationFn: async ({ resourceType, resourceId, data, params }) => { const response = await legacyApiClient.v1 - .postApiAdminResourcesPermissions() + .putApiAdminResourcesPermissions() .request(resourceType, resourceId, data, params) if (response && typeof response === 'object' && 'req_result' in response && !response.req_result) { diff --git a/moon/apps/web/hooks/admin/useUpdateAdminGroup.ts b/moon/apps/web/hooks/admin/useUpdateAdminGroup.ts index 9eae4414d..a835fe5f6 100644 --- a/moon/apps/web/hooks/admin/useUpdateAdminGroup.ts +++ b/moon/apps/web/hooks/admin/useUpdateAdminGroup.ts @@ -10,7 +10,7 @@ export function useUpdateAdminGroup() { return useMutation< PutApiAdminGroupsByGroupIdData, Error, - { groupId: number; data: UpdateGroupRequest; params?: RequestParams } + { groupId: string; data: UpdateGroupRequest; params?: RequestParams } >({ mutationFn: async ({ groupId, data, params }) => { const response = await legacyApiClient.v1.putApiAdminGroupsByGroupId().request(groupId, data, params) diff --git a/moon/apps/web/hooks/conversation/usePostConversationReactions.ts b/moon/apps/web/hooks/conversation/usePostConversationReactions.ts index c8e09861a..3a263d0e6 100644 --- a/moon/apps/web/hooks/conversation/usePostConversationReactions.ts +++ b/moon/apps/web/hooks/conversation/usePostConversationReactions.ts @@ -10,7 +10,7 @@ interface Props { reaction: StandardReaction | SyncCustomReaction } -export function usePostConversationReactions(commentId: number, id: string, type: string) { +export function usePostConversationReactions(commentId: string, id: string, type: string) { const queryClient = useQueryClient() return useMutation({ diff --git a/moon/apps/web/hooks/issues/useDeleteIssueComment.ts b/moon/apps/web/hooks/issues/useDeleteIssueComment.ts index 2b834b5d0..1ad434928 100644 --- a/moon/apps/web/hooks/issues/useDeleteIssueComment.ts +++ b/moon/apps/web/hooks/issues/useDeleteIssueComment.ts @@ -7,7 +7,7 @@ import { legacyApiClient } from '@/utils/queryClient' export function useDeleteIssueComment(id: string, params?: RequestParams) { const queryClient = useQueryClient() - return useMutation({ + return useMutation({ mutationKey: legacyApiClient.v1.deleteApiConversationByCommentId().baseKey, mutationFn: (convId) => legacyApiClient.v1.deleteApiConversationByCommentId().request(convId, params), onSuccess: () => { diff --git a/moon/apps/web/hooks/issues/usePostComment.ts b/moon/apps/web/hooks/issues/usePostComment.ts index 916b2c48a..a2123daf7 100644 --- a/moon/apps/web/hooks/issues/usePostComment.ts +++ b/moon/apps/web/hooks/issues/usePostComment.ts @@ -8,7 +8,7 @@ export function usePostComment() { return useMutation< PostApiConversationByCommentIdData, Error, - { commentId: number; data: ContentPayload; params?: RequestParams } + { commentId: string; data: ContentPayload; params?: RequestParams } >({ mutationFn: ({ commentId, data, params }) => legacyApiClient.v1.postApiConversationByCommentId().request(commentId, data, params) diff --git a/moon/apps/web/hooks/useDeleteSSHKeyById.ts b/moon/apps/web/hooks/useDeleteSSHKeyById.ts index 9cf98f20e..7862e648c 100644 --- a/moon/apps/web/hooks/useDeleteSSHKeyById.ts +++ b/moon/apps/web/hooks/useDeleteSSHKeyById.ts @@ -5,7 +5,7 @@ import { DeleteApiUserSshByKeyIdData } from '@gitmono/types' import { legacyApiClient } from '@/utils/queryClient' export function useDeleteSSHKeyById() { - return useMutation({ + return useMutation({ mutationFn: ({ keyId }) => legacyApiClient.v1.deleteApiUserSshByKeyId().request(keyId) }) } diff --git a/moon/apps/web/hooks/useDeleteTokenById.ts b/moon/apps/web/hooks/useDeleteTokenById.ts index b1f4c6809..aee5cc0df 100644 --- a/moon/apps/web/hooks/useDeleteTokenById.ts +++ b/moon/apps/web/hooks/useDeleteTokenById.ts @@ -5,7 +5,7 @@ import { DeleteApiUserTokenByKeyIdData } from '@gitmono/types' import { legacyApiClient } from '@/utils/queryClient' export function useDeleteTokenById() { - return useMutation({ + return useMutation({ mutationFn: ({ keyId }) => legacyApiClient.v1.deleteApiUserTokenByKeyId().request(keyId) }) } diff --git a/moon/apps/web/pages/[org]/cl/[link]/index.tsx b/moon/apps/web/pages/[org]/cl/[link]/index.tsx index 763a14d75..0148356cf 100644 --- a/moon/apps/web/pages/[org]/cl/[link]/index.tsx +++ b/moon/apps/web/pages/[org]/cl/[link]/index.tsx @@ -160,7 +160,7 @@ const CLDetailPage: PageWithLayout = () => { { data: { link: id, - item_id: Number(item_id), + item_id: item_id, assignees: selected } }, @@ -212,7 +212,7 @@ const CLDetailPage: PageWithLayout = () => { clLabels( { data: { - item_id: Number(item_id), + item_id: item_id, label_ids: selected, link: `${tempId}` } diff --git a/moon/apps/web/utils/types.ts b/moon/apps/web/utils/types.ts index 7209a4dd2..1978a11e3 100644 --- a/moon/apps/web/utils/types.ts +++ b/moon/apps/web/utils/types.ts @@ -13,7 +13,7 @@ export interface ApiErrorResponse { export interface CommonDetailData { assignees: string[] conversations: ConversationItem[] - id: number + id: string labels: LabelItem[] link: string open_timestamp: number diff --git a/moon/packages/types/generated.ts b/moon/packages/types/generated.ts index 90d9fceff..c707e0559 100644 --- a/moon/packages/types/generated.ts +++ b/moon/packages/types/generated.ts @@ -3255,8 +3255,7 @@ export type AdminListResponse = { export type AnchorResponse = { anchor_commit_sha: string - /** @format int64 */ - anchor_id: number + anchor_id: string context_after: string context_before: string diff_side: DiffSide @@ -3471,8 +3470,7 @@ export type ArtifactSetListItem = { export type AssigneeUpdatePayload = { assignees: string[] - /** @format int64 */ - item_id: number + item_id: string link: string } @@ -3543,22 +3541,17 @@ export type BlameResult = { * `token` is a `bot_` push token returned once; use as Bearer (or Basic password). */ export type BootstrapInitBotResponse = { - /** @format int64 */ - bot_id: number + bot_id: string bot_name: string token: string } export type BotRes = { - /** @format int64 */ - bot_id: number - /** @format int64 */ - id: number - /** @format int64 */ - installed_by: number + bot_id: string + id: string + installed_by: string status: InstallationBotStatus - /** @format int64 */ - target_id: number + target_id: string target_type: InstallationTargetType } @@ -3576,8 +3569,7 @@ export type CLDetailRes = { /** True when `author` matches a registered bot. */ author_is_bot?: boolean conversations: ConversationItem[] - /** @format int64 */ - id: number + id: string labels: LabelItem[] link: string /** @format int64 */ @@ -3595,8 +3587,7 @@ export type ChangeInstallationStatus = { } export type ChangeReviewStatePayload = { - /** @format int64 */ - conversation_id: number + conversation_id: string resolved: boolean } @@ -3640,17 +3631,14 @@ export type CodeReviewResponse = { export type CommentReplyRequest = { content: string - /** @format int64 */ - parent_comment_id: number + parent_comment_id: string } export type CommentReviewResponse = { - /** @format int64 */ - comment_id: number + comment_id: string content?: string | null created_at: string - /** @format int64 */ - parent_id?: number | null + parent_id?: string | null updated_at: string user_name: string } @@ -3769,8 +3757,7 @@ export type CommonResultBootstrapInitBotResponse = { * `token` is a `bot_` push token returned once; use as Bearer (or Basic password). */ data?: { - /** @format int64 */ - bot_id: number + bot_id: string bot_name: string token: string } @@ -3780,15 +3767,11 @@ export type CommonResultBootstrapInitBotResponse = { export type CommonResultBotRes = { data?: { - /** @format int64 */ - bot_id: number - /** @format int64 */ - id: number - /** @format int64 */ - installed_by: number + bot_id: string + id: string + installed_by: string status: InstallationBotStatus - /** @format int64 */ - target_id: number + target_id: string target_type: InstallationTargetType } err_message: string @@ -3803,8 +3786,7 @@ export type CommonResultCLDetailRes = { /** True when `author` matches a registered bot. */ author_is_bot?: boolean conversations: ConversationItem[] - /** @format int64 */ - id: number + id: string labels: LabelItem[] link: string /** @format int64 */ @@ -3849,12 +3831,10 @@ export type CommonResultCodeReviewResponse = { export type CommonResultCommentReviewResponse = { data?: { - /** @format int64 */ - comment_id: number + comment_id: string content?: string | null created_at: string - /** @format int64 */ - parent_id?: number | null + parent_id?: string | null updated_at: string user_name: string } @@ -3917,10 +3897,8 @@ export type CommonResultCommonPageCommitSummary = { export type CommonResultCommonPageGroupMemberResponse = { data?: { items: { - /** @format int64 */ - group_id: number - /** @format int64 */ - id: number + group_id: string + id: string /** @format int64 */ joined_at: number /** Campsite public user id (field name kept for API compat). */ @@ -3942,8 +3920,8 @@ export type CommonResultCommonPageGroupResponse = { /** @format int64 */ created_at: number description?: string | null - /** @format int64 */ - id: number + /** Snowflake id; JSON string so JS keeps full precision. */ + id: string name: string /** @format int64 */ updated_at: number @@ -3974,8 +3952,7 @@ export type CommonResultCommonPageItemRes = { closed_at?: number | null /** @min 0 */ comment_num: number - /** @format int64 */ - id: number + id: string labels: LabelItem[] link: string /** @format int64 */ @@ -4002,8 +3979,7 @@ export type CommonResultCommonPageLabelItem = { items: { color: string description: string - /** @format int64 */ - id: number + id: string name: string }[] /** @@ -4054,8 +4030,7 @@ export type CommonResultCommonPageWebhookResponse = { active: boolean created_at: string event_types: string[] - /** @format int64 */ - id: number + id: string path_filter?: string | null target_url: string updated_at: string @@ -4082,11 +4057,8 @@ export type CommonResultCompleteResponse = { * for the "no-change" completion path. */ data?: { - /** - * Change List ID - * @format int64 - */ - cl_id: number + /** Change List ID */ + cl_id: string /** CL link (same as session_id) */ cl_link: string /** Created commit hash */ @@ -4117,8 +4089,7 @@ export type CommonResultCreateBotTokenResponse = { data?: { /** @format date-time */ expires_at?: string | null - /** @format int64 */ - id: number + id: string token_name: string token_plain: string } @@ -4158,8 +4129,7 @@ export type CommonResultDeleteGroupResponse = { * @min 0 */ deleted_permissions_count: number - /** @format int64 */ - group_id: number + group_id: string } err_message: string req_result: boolean @@ -4255,8 +4225,8 @@ export type CommonResultGroupResponse = { /** @format int64 */ created_at: number description?: string | null - /** @format int64 */ - id: number + /** Snowflake id; JSON string so JS keeps full precision. */ + id: string name: string /** @format int64 */ updated_at: number @@ -4279,8 +4249,7 @@ export type CommonResultIssueDetailRes = { author: string author_is_bot?: boolean conversations: ConversationItem[] - /** @format int64 */ - id: number + id: string labels: LabelItem[] link: string /** @format int64 */ @@ -4296,8 +4265,7 @@ export type CommonResultLabelItem = { data?: { color: string description: string - /** @format int64 */ - id: number + id: string name: string } err_message: string @@ -4379,8 +4347,7 @@ export type CommonResultQueueStatusResponse = { export type CommonResultRemoveMemberResponse = { data?: { - /** @format int64 */ - group_id: number + group_id: string removed: boolean username: string } @@ -4539,8 +4506,7 @@ export type CommonResultThreadReviewResponse = { created_at: string position: PositionResponse status: ThreadStatus - /** @format int64 */ - thread_id: number + thread_id: string updated_at: string } err_message: string @@ -4551,8 +4517,7 @@ export type CommonResultThreadStatusResponse = { data?: { link: string status: ThreadStatus - /** @format int64 */ - thread_id: number + thread_id: string } err_message: string req_result: boolean @@ -4693,15 +4658,11 @@ export type CommonResultVec = { export type CommonResultVecBotRes = { data?: { - /** @format int64 */ - bot_id: number - /** @format int64 */ - id: number - /** @format int64 */ - installed_by: number + bot_id: string + id: string + installed_by: string status: InstallationBotStatus - /** @format int64 */ - target_id: number + target_id: string target_type: InstallationTargetType }[] err_message: string @@ -4740,10 +4701,8 @@ export type CommonResultVecGpgKey = { export type CommonResultVecGroupMemberResponse = { data?: { - /** @format int64 */ - group_id: number - /** @format int64 */ - id: number + group_id: string + id: string /** @format int64 */ joined_at: number /** Campsite public user id (field name kept for API compat). */ @@ -4755,8 +4714,7 @@ export type CommonResultVecGroupMemberResponse = { export type CommonResultVecIssueSuggestions = { data?: { - /** @format int64 */ - id: number + id: string link: string title: string type: string @@ -4771,8 +4729,7 @@ export type CommonResultVecListBotTokenItem = { created_at: string /** @format date-time */ expires_at?: string | null - /** @format int64 */ - id: number + id: string revoked: boolean token_name: string }[] @@ -4785,8 +4742,7 @@ export type CommonResultVecListSSHKey = { /** @format int64 */ created_at: number finger: string - /** @format int64 */ - id: number + id: string ssh_key: string title: string }[] @@ -4798,8 +4754,7 @@ export type CommonResultVecListToken = { data?: { /** @format int64 */ created_at: number - /** @format int64 */ - id: number + id: string token: string }[] err_message: string @@ -4833,10 +4788,8 @@ export type CommonResultVecResourcePermissionResponse = { data?: { /** @format int64 */ created_at: number - /** @format int64 */ - group_id: number - /** @format int64 */ - id: number + group_id: string + id: string permission: PermissionValue resource_id: string resource_type: ResourceTypeValue @@ -4889,8 +4842,7 @@ export type CommonResultWebhookResponse = { active: boolean created_at: string event_types: string[] - /** @format int64 */ - id: number + id: string path_filter?: string | null target_url: string updated_at: string @@ -4923,11 +4875,8 @@ export type CompletePayload = object * for the "no-change" completion path. */ export type CompleteResponse = { - /** - * Change List ID - * @format int64 - */ - cl_id: number + /** Change List ID */ + cl_id: string /** CL link (same as session_id) */ cl_link: string /** Created commit hash */ @@ -4974,8 +4923,7 @@ export type ConversationItem = { /** @format int64 */ created_at: number grouped_reactions: ReactionItem[] - /** @format int64 */ - id: number + id: string /** True when `username` matches a registered bot (or legacy `"system"` actor). */ is_bot?: boolean resolved?: boolean | null @@ -5003,8 +4951,7 @@ export type CreateBotTokenRequest = { export type CreateBotTokenResponse = { /** @format date-time */ expires_at?: string | null - /** @format int64 */ - id: number + id: string token_name: string token_plain: string } @@ -5105,8 +5052,7 @@ export type DeleteGroupResponse = { * @min 0 */ deleted_permissions_count: number - /** @format int64 */ - group_id: number + group_id: string } export type DeletePermissionsResponse = { @@ -5245,10 +5191,8 @@ export type GpgKey = { } export type GroupMemberResponse = { - /** @format int64 */ - group_id: number - /** @format int64 */ - id: number + group_id: string + id: string /** @format int64 */ joined_at: number /** Campsite public user id (field name kept for API compat). */ @@ -5259,8 +5203,8 @@ export type GroupResponse = { /** @format int64 */ created_at: number description?: string | null - /** @format int64 */ - id: number + /** Snowflake id; JSON string so JS keeps full precision. */ + id: string name: string /** @format int64 */ updated_at: number @@ -5279,10 +5223,8 @@ export type InitializeCommentRequest = { } export type InstallBotReq = { - /** @format int64 */ - installed_by: number - /** @format int64 */ - target_id: number + installed_by: string + target_id: string target_type: InstallationTargetType } @@ -5295,8 +5237,7 @@ export type IssueDetailRes = { author: string author_is_bot?: boolean conversations: ConversationItem[] - /** @format int64 */ - id: number + id: string labels: LabelItem[] link: string /** @format int64 */ @@ -5306,8 +5247,7 @@ export type IssueDetailRes = { } export type IssueSuggestions = { - /** @format int64 */ - id: number + id: string link: string title: string type: string @@ -5327,8 +5267,7 @@ export type ItemRes = { closed_at?: number | null /** @min 0 */ comment_num: number - /** @format int64 */ - id: number + id: string labels: LabelItem[] link: string /** @format int64 */ @@ -5344,15 +5283,13 @@ export type ItemRes = { export type LabelItem = { color: string description: string - /** @format int64 */ - id: number + id: string name: string } export type LabelUpdatePayload = { - /** @format int64 */ - item_id: number - label_ids: number[] + item_id: string + label_ids: string[] link: string } @@ -5378,8 +5315,7 @@ export type ListBotTokenItem = { created_at: string /** @format date-time */ expires_at?: string | null - /** @format int64 */ - id: number + id: string revoked: boolean token_name: string } @@ -5388,7 +5324,7 @@ export type ListPayload = { asc: boolean assignees?: string[] | null author?: string | null - labels?: number[] | null + labels?: string[] | null sort_by?: string | null status: string } @@ -5397,8 +5333,7 @@ export type ListSSHKey = { /** @format int64 */ created_at: number finger: string - /** @format int64 */ - id: number + id: string ssh_key: string title: string } @@ -5406,8 +5341,7 @@ export type ListSSHKey = { export type ListToken = { /** @format int64 */ created_at: number - /** @format int64 */ - id: number + id: string token: string } @@ -5604,7 +5538,7 @@ export type PageParamsListPayload = { asc: boolean assignees?: string[] | null author?: string | null - labels?: number[] | null + labels?: string[] | null sort_by?: string | null status: string } @@ -5655,21 +5589,18 @@ export type Pagination = { } export type PermissionBindingRequest = { - /** @format int64 */ - group_id: number + group_id: string permission: PermissionValue } export type PositionResponse = { - /** @format int64 */ - anchor_id: number + anchor_id: string commit_sha: string /** @format int32 */ confidence: number /** @format int32 */ line_number: number - /** @format int64 */ - position_id: number + position_id: string position_status: PositionStatus } @@ -5756,8 +5687,7 @@ export type RemoveGpgRequest = { } export type RemoveMemberResponse = { - /** @format int64 */ - group_id: number + group_id: string removed: boolean username: string } @@ -5776,10 +5706,8 @@ export type RequestObject = { export type ResourcePermissionResponse = { /** @format int64 */ created_at: number - /** @format int64 */ - group_id: number - /** @format int64 */ - id: number + group_id: string + id: string permission: PermissionValue resource_id: string resource_type: ResourceTypeValue @@ -5970,16 +5898,14 @@ export type ThreadReviewResponse = { created_at: string position: PositionResponse status: ThreadStatus - /** @format int64 */ - thread_id: number + thread_id: string updated_at: string } export type ThreadStatusResponse = { link: string status: ThreadStatus - /** @format int64 */ - thread_id: number + thread_id: string } export type TreeBriefItem = { @@ -6009,8 +5935,7 @@ export type TreeResponse = { /** Trigger detail response (new RESTful API) */ export type TriggerResponse = { - /** @format int64 */ - cl_id?: number | null + cl_id?: string cl_link?: string | null commit_hash: string /** @format int64 */ @@ -6169,8 +6094,7 @@ export type WebhookResponse = { active: boolean created_at: string event_types: string[] - /** @format int64 */ - id: number + id: string path_filter?: string | null target_url: string updated_at: string @@ -6273,8 +6197,7 @@ export type RetryBuildRequest = { build_id: string /** The list of changed files in the hybrid path contract used by Orion. */ changes: StatusProjectRelativePath[] - /** @format int64 */ - cl_id: number + cl_id: string cl_link: string targets?: string[] | null } @@ -6352,8 +6275,7 @@ export type TaskBuildRequest = { * (for example `common/lib.rs`) */ changes: StatusProjectRelativePath[] - /** @format int64 */ - cl_id: number + cl_id: string /** The change list link (URL) */ cl_link: string /** The Buck2 project path within the monorepo (for example `/jupiter/callisto`). */ @@ -6373,6 +6295,9 @@ export type TaskHistoryQuery = { task_id: string } +/** Snowflake id; JSON string so JS keeps full precision. */ +export type SnowflakeId = string + export type PostActivityViewsParams = { orgSlug: string } @@ -8671,61 +8596,43 @@ export type PostApiAdminGroupsData = CommonResultGroupResponse export type PostApiAdminGroupsListData = CommonResultCommonPageGroupResponse export type GetApiAdminGroupsByGroupIdParams = { - /** - * Group ID - * @format int64 - */ - groupId: number + /** Group ID */ + groupId: string } export type GetApiAdminGroupsByGroupIdData = CommonResultGroupResponse export type PutApiAdminGroupsByGroupIdParams = { - /** - * Group ID - * @format int64 - */ - groupId: number + /** Group ID */ + groupId: string } export type PutApiAdminGroupsByGroupIdData = CommonResultGroupResponse export type DeleteApiAdminGroupsByGroupIdParams = { - /** - * Group ID - * @format int64 - */ - groupId: number + /** Group ID */ + groupId: string } export type DeleteApiAdminGroupsByGroupIdData = CommonResultDeleteGroupResponse export type PostApiAdminGroupsMembersParams = { - /** - * Group ID - * @format int64 - */ - groupId: number + /** Group ID */ + groupId: string } export type PostApiAdminGroupsMembersData = CommonResultVecGroupMemberResponse export type PostApiAdminGroupsMembersListParams = { - /** - * Group ID - * @format int64 - */ - groupId: number + /** Group ID */ + groupId: string } export type PostApiAdminGroupsMembersListData = CommonResultCommonPageGroupMemberResponse export type DeleteApiAdminGroupsMembersByUsernameParams = { - /** - * Group ID - * @format int64 - */ - groupId: number + /** Group ID */ + groupId: string /** Campsite user id of the member */ username: string } @@ -8842,96 +8749,63 @@ export type GetApiBlobData = CommonResultString export type PostApiBotsBootstrapInitData = CommonResultBootstrapInitBotResponse export type GetApiBotsTokensParams = { - /** - * Bot ID - * @format int64 - */ - botId: number + /** Bot ID */ + botId: string } export type GetApiBotsTokensData = CommonResultVecListBotTokenItem export type PostApiBotsTokensParams = { - /** - * Bot ID - * @format int64 - */ - botId: number + /** Bot ID */ + botId: string } export type PostApiBotsTokensData = CommonResultCreateBotTokenResponse export type PostApiBotsTokensRevokeAllParams = { - /** - * Bot ID - * @format int64 - */ - botId: number + /** Bot ID */ + botId: string } export type PostApiBotsTokensRevokeAllData = any export type DeleteApiBotsTokensByIdParams = { - /** - * Bot ID - * @format int64 - */ - botId: number - /** - * Token ID - * @format int64 - */ - id: number + /** Bot ID */ + botId: string + /** Token ID */ + id: string } export type DeleteApiBotsTokensByIdData = any export type GetApiBotsInstallationsParams = { - /** - * Bots ID - * @format int64 - */ - id: number + /** Bots ID */ + id: string } export type GetApiBotsInstallationsData = CommonResultVecBotRes export type PostApiBotsInstallationsParams = { - /** - * Bots ID - * @format int64 - */ - id: number + /** Bots ID */ + id: string } export type PostApiBotsInstallationsData = CommonResultBotRes export type DeleteApiBotsInstallationsByInstallationIdParams = { - /** - * Bot ID - * @format int64 - */ - id: number - /** - * Installation ID - * @format int64 - */ - installationId: number + /** Bot ID */ + id: string + /** Installation ID */ + installationId: string } export type DeleteApiBotsInstallationsByInstallationIdData = CommonResultString export type PatchApiBotsInstallationsByInstallationIdParams = { - /** - * Bot ID - * @format int64 - */ - id: number - /** - * Installation ID - * @format int64 - */ - installationId: number + /** Bot ID */ + id: string + /** Installation ID */ + installationId: string } export type PatchApiBotsInstallationsByInstallationIdData = CommonResultBotRes @@ -9102,31 +8976,22 @@ export type GetApiClUpdateStatusParams = { export type GetApiClUpdateStatusData = CommonResultUpdateBranchStatusRes export type DeleteApiCodeReviewCommentByCommentIdParams = { - /** - * A numeric ID representing a code review comment - * @format int64 - */ - commentId: number + /** A numeric ID representing a code review comment */ + commentId: string } export type DeleteApiCodeReviewCommentByCommentIdData = CommonResultString export type DeleteApiCodeReviewThreadByThreadIdParams = { - /** - * A numeric ID representing a code review thread - * @format int64 - */ - threadId: number + /** A numeric ID representing a code review thread */ + threadId: string } export type DeleteApiCodeReviewThreadByThreadIdData = CommonResultString export type PostApiCodeReviewUpdateParams = { - /** - * A numeric ID representing a comment - * @format int64 - */ - commentId: number + /** A numeric ID representing a comment */ + commentId: string } export type PostApiCodeReviewUpdateData = CommonResultCommentReviewResponse @@ -9146,31 +9011,22 @@ export type GetApiCodeReviewCommentsParams = { export type GetApiCodeReviewCommentsData = CommonResultCodeReviewResponse export type PostApiCodeReviewCommentReplyParams = { - /** - * Code Review Comment Thread ID - * @format int64 - */ - threadId: number + /** Code Review Comment Thread ID */ + threadId: string } export type PostApiCodeReviewCommentReplyData = CommonResultCommentReviewResponse export type PostApiCodeReviewReopenParams = { - /** - * A numeric ID representing a code review thread - * @format int64 - */ - threadId: number + /** A numeric ID representing a code review thread */ + threadId: string } export type PostApiCodeReviewReopenData = CommonResultThreadStatusResponse export type PostApiCodeReviewResolveParams = { - /** - * A numeric ID representing a code review thread - * @format int64 - */ - threadId: number + /** A numeric ID representing a code review thread */ + threadId: string } export type PostApiCodeReviewResolveData = CommonResultThreadStatusResponse @@ -9210,31 +9066,22 @@ export type DeleteApiConversationReactionsByIdParams = { export type DeleteApiConversationReactionsByIdData = CommonResultString export type PostApiConversationByCommentIdParams = { - /** - * A numeric ID representing a comment - * @format int64 - */ - commentId: number + /** A numeric ID representing a comment */ + commentId: string } export type PostApiConversationByCommentIdData = CommonResultString export type DeleteApiConversationByCommentIdParams = { - /** - * A numeric ID representing a comment - * @format int64 - */ - commentId: number + /** A numeric ID representing a comment */ + commentId: string } export type DeleteApiConversationByCommentIdData = CommonResultString export type PostApiConversationReactionsParams = { - /** - * A numeric ID representing either a comment or a conversation. Specify the type in the request body. - * @format int64 - */ - commentId: number + /** A numeric ID representing either a comment or a conversation. Specify the type in the request body. */ + commentId: string } export type PostApiConversationReactionsData = CommonResultString @@ -9305,11 +9152,8 @@ export type PostApiLabelListData = CommonResultCommonPageLabelItem export type PostApiLabelNewData = CommonResultString export type GetApiLabelByIdParams = { - /** - * Label's id - * @format int64 - */ - id: number + /** Label's id */ + id: string } export type GetApiLabelByIdData = CommonResultLabelItem @@ -9636,21 +9480,15 @@ export type PostApiTriggersData = CommonResultTriggerResponse export type PostApiTriggersListData = CommonResultCommonPageTriggerResponse export type GetApiTriggersByIdParams = { - /** - * Trigger ID - * @format int64 - */ - id: number + /** Trigger ID */ + id: string } export type GetApiTriggersByIdData = CommonResultTriggerResponse export type PostApiTriggersRetryParams = { - /** - * Original trigger ID to retry - * @format int64 - */ - id: number + /** Original trigger ID to retry */ + id: string } export type PostApiTriggersRetryData = CommonResultTriggerResponse @@ -9676,11 +9514,8 @@ export type PostApiUserSshData = CommonResultString export type GetApiUserSshListData = CommonResultVecListSSHKey export type DeleteApiUserSshByKeyIdParams = { - /** - * A numeric ID representing a SSH - * @format int64 - */ - keyId: number + /** A numeric ID representing a SSH */ + keyId: string } export type DeleteApiUserSshByKeyIdData = CommonResultString @@ -9690,11 +9525,8 @@ export type PostApiUserTokenGenerateData = CommonResultString export type GetApiUserTokenListData = CommonResultVecListToken export type DeleteApiUserTokenByKeyIdParams = { - /** - * A numeric ID representing a User Token - * @format int64 - */ - keyId: number + /** A numeric ID representing a User Token */ + keyId: string } export type DeleteApiUserTokenByKeyIdData = CommonResultString @@ -9719,11 +9551,8 @@ export type GetApiWebhooksData = CommonResultCommonPageWebhookResponse export type PostApiWebhooksData = CommonResultWebhookResponse export type DeleteApiWebhooksByIdParams = { - /** - * Webhook ID - * @format int64 - */ - id: number + /** Webhook ID */ + id: string } export type DeleteApiWebhooksByIdData = CommonResultString @@ -18621,8 +18450,8 @@ export class Api extends HttpClient([base]), - requestKey: (groupId: number) => dataTaggedQueryKey([base, groupId]), - request: (groupId: number, params: RequestParams = {}) => + requestKey: (groupId: string) => dataTaggedQueryKey([base, groupId]), + request: (groupId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/admin/groups/${groupId}`, method: 'GET', @@ -18644,8 +18473,8 @@ export class Api extends HttpClient([base]), - requestKey: (groupId: number) => dataTaggedQueryKey([base, groupId]), - request: (groupId: number, data: UpdateGroupRequest, params: RequestParams = {}) => + requestKey: (groupId: string) => dataTaggedQueryKey([base, groupId]), + request: (groupId: string, data: UpdateGroupRequest, params: RequestParams = {}) => this.request({ path: `/api/v1/admin/groups/${groupId}`, method: 'PUT', @@ -18669,8 +18498,8 @@ export class Api extends HttpClient([base]), - requestKey: (groupId: number) => dataTaggedQueryKey([base, groupId]), - request: (groupId: number, params: RequestParams = {}) => + requestKey: (groupId: string) => dataTaggedQueryKey([base, groupId]), + request: (groupId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/admin/groups/${groupId}`, method: 'DELETE', @@ -18692,8 +18521,8 @@ export class Api extends HttpClient([base]), - requestKey: (groupId: number) => dataTaggedQueryKey([base, groupId]), - request: (groupId: number, data: AddMembersRequest, params: RequestParams = {}) => + requestKey: (groupId: string) => dataTaggedQueryKey([base, groupId]), + request: (groupId: string, data: AddMembersRequest, params: RequestParams = {}) => this.request({ path: `/api/v1/admin/groups/${groupId}/members`, method: 'POST', @@ -18717,8 +18546,8 @@ export class Api extends HttpClient([base]), - requestKey: (groupId: number) => dataTaggedQueryKey([base, groupId]), - request: (groupId: number, data: PageParamsEmptyListAdditional, params: RequestParams = {}) => + requestKey: (groupId: string) => dataTaggedQueryKey([base, groupId]), + request: (groupId: string, data: PageParamsEmptyListAdditional, params: RequestParams = {}) => this.request({ path: `/api/v1/admin/groups/${groupId}/members/list`, method: 'POST', @@ -18742,9 +18571,9 @@ export class Api extends HttpClient([base]), - requestKey: (groupId: number, username: string) => + requestKey: (groupId: string, username: string) => dataTaggedQueryKey([base, groupId, username]), - request: (groupId: number, username: string, params: RequestParams = {}) => + request: (groupId: string, username: string, params: RequestParams = {}) => this.request({ path: `/api/v1/admin/groups/${groupId}/members/${username}`, method: 'DELETE', @@ -19110,8 +18939,8 @@ export class Api extends HttpClient([base]), - requestKey: (botId: number) => dataTaggedQueryKey([base, botId]), - request: (botId: number, params: RequestParams = {}) => + requestKey: (botId: string) => dataTaggedQueryKey([base, botId]), + request: (botId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/bots/${botId}/tokens`, method: 'GET', @@ -19134,8 +18963,8 @@ export class Api extends HttpClient([base]), - requestKey: (botId: number) => dataTaggedQueryKey([base, botId]), - request: (botId: number, data: CreateBotTokenRequest, params: RequestParams = {}) => + requestKey: (botId: string) => dataTaggedQueryKey([base, botId]), + request: (botId: string, data: CreateBotTokenRequest, params: RequestParams = {}) => this.request({ path: `/api/v1/bots/${botId}/tokens`, method: 'POST', @@ -19160,8 +18989,8 @@ export class Api extends HttpClient([base]), - requestKey: (botId: number) => dataTaggedQueryKey([base, botId]), - request: (botId: number, params: RequestParams = {}) => + requestKey: (botId: string) => dataTaggedQueryKey([base, botId]), + request: (botId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/bots/${botId}/tokens/revoke_all`, method: 'POST', @@ -19183,8 +19012,8 @@ export class Api extends HttpClient([base]), - requestKey: (botId: number, id: number) => dataTaggedQueryKey([base, botId, id]), - request: (botId: number, id: number, params: RequestParams = {}) => + requestKey: (botId: string, id: string) => dataTaggedQueryKey([base, botId, id]), + request: (botId: string, id: string, params: RequestParams = {}) => this.request({ path: `/api/v1/bots/${botId}/tokens/${id}`, method: 'DELETE', @@ -19206,8 +19035,8 @@ export class Api extends HttpClient([base]), - requestKey: (id: number) => dataTaggedQueryKey([base, id]), - request: (id: number, params: RequestParams = {}) => + requestKey: (id: string) => dataTaggedQueryKey([base, id]), + request: (id: string, params: RequestParams = {}) => this.request({ path: `/api/v1/bots/${id}/installations`, method: 'GET', @@ -19230,8 +19059,8 @@ export class Api extends HttpClient([base]), - requestKey: (id: number) => dataTaggedQueryKey([base, id]), - request: (id: number, data: InstallBotReq, params: RequestParams = {}) => + requestKey: (id: string) => dataTaggedQueryKey([base, id]), + request: (id: string, data: InstallBotReq, params: RequestParams = {}) => this.request({ path: `/api/v1/bots/${id}/installations`, method: 'POST', @@ -19255,9 +19084,9 @@ export class Api extends HttpClient([base]), - requestKey: (id: number, installationId: number) => + requestKey: (id: string, installationId: string) => dataTaggedQueryKey([base, id, installationId]), - request: (id: number, installationId: number, data: InstallationTargetType, params: RequestParams = {}) => + request: (id: string, installationId: string, data: InstallationTargetType, params: RequestParams = {}) => this.request({ path: `/api/v1/bots/${id}/installations/${installationId}`, method: 'DELETE', @@ -19281,9 +19110,9 @@ export class Api extends HttpClient([base]), - requestKey: (id: number, installationId: number) => + requestKey: (id: string, installationId: string) => dataTaggedQueryKey([base, id, installationId]), - request: (id: number, installationId: number, data: ChangeInstallationStatus, params: RequestParams = {}) => + request: (id: string, installationId: string, data: ChangeInstallationStatus, params: RequestParams = {}) => this.request({ path: `/api/v1/bots/${id}/installations/${installationId}`, method: 'PATCH', @@ -19955,9 +19784,9 @@ export class Api extends HttpClient([base]), - requestKey: (commentId: number) => + requestKey: (commentId: string) => dataTaggedQueryKey([base, commentId]), - request: (commentId: number, params: RequestParams = {}) => + request: (commentId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/code_review/comment/${commentId}`, method: 'DELETE', @@ -19980,8 +19809,8 @@ export class Api extends HttpClient([base]), - requestKey: (threadId: number) => dataTaggedQueryKey([base, threadId]), - request: (threadId: number, params: RequestParams = {}) => + requestKey: (threadId: string) => dataTaggedQueryKey([base, threadId]), + request: (threadId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/code_review/thread/${threadId}`, method: 'DELETE', @@ -20004,8 +19833,8 @@ export class Api extends HttpClient([base]), - requestKey: (commentId: number) => dataTaggedQueryKey([base, commentId]), - request: (commentId: number, data: UpdateCommentRequest, params: RequestParams = {}) => + requestKey: (commentId: string) => dataTaggedQueryKey([base, commentId]), + request: (commentId: string, data: UpdateCommentRequest, params: RequestParams = {}) => this.request({ path: `/api/v1/code_review/${commentId}/update`, method: 'POST', @@ -20080,8 +19909,8 @@ export class Api extends HttpClient([base]), - requestKey: (threadId: number) => dataTaggedQueryKey([base, threadId]), - request: (threadId: number, data: CommentReplyRequest, params: RequestParams = {}) => + requestKey: (threadId: string) => dataTaggedQueryKey([base, threadId]), + request: (threadId: string, data: CommentReplyRequest, params: RequestParams = {}) => this.request({ path: `/api/v1/code_review/${threadId}/comment/reply`, method: 'POST', @@ -20106,8 +19935,8 @@ export class Api extends HttpClient([base]), - requestKey: (threadId: number) => dataTaggedQueryKey([base, threadId]), - request: (threadId: number, params: RequestParams = {}) => + requestKey: (threadId: string) => dataTaggedQueryKey([base, threadId]), + request: (threadId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/code_review/${threadId}/reopen`, method: 'POST', @@ -20130,8 +19959,8 @@ export class Api extends HttpClient([base]), - requestKey: (threadId: number) => dataTaggedQueryKey([base, threadId]), - request: (threadId: number, params: RequestParams = {}) => + requestKey: (threadId: string) => dataTaggedQueryKey([base, threadId]), + request: (threadId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/code_review/${threadId}/resolve`, method: 'POST', @@ -20284,8 +20113,8 @@ export class Api extends HttpClient([base]), - requestKey: (commentId: number) => dataTaggedQueryKey([base, commentId]), - request: (commentId: number, data: ContentPayload, params: RequestParams = {}) => + requestKey: (commentId: string) => dataTaggedQueryKey([base, commentId]), + request: (commentId: string, data: ContentPayload, params: RequestParams = {}) => this.request({ path: `/api/v1/conversation/${commentId}`, method: 'POST', @@ -20310,8 +20139,8 @@ export class Api extends HttpClient([base]), - requestKey: (commentId: number) => dataTaggedQueryKey([base, commentId]), - request: (commentId: number, params: RequestParams = {}) => + requestKey: (commentId: string) => dataTaggedQueryKey([base, commentId]), + request: (commentId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/conversation/${commentId}`, method: 'DELETE', @@ -20334,8 +20163,8 @@ export class Api extends HttpClient([base]), - requestKey: (commentId: number) => dataTaggedQueryKey([base, commentId]), - request: (commentId: number, data: ReactionRequest, params: RequestParams = {}) => + requestKey: (commentId: string) => dataTaggedQueryKey([base, commentId]), + request: (commentId: string, data: ReactionRequest, params: RequestParams = {}) => this.request({ path: `/api/v1/conversation/${commentId}/reactions`, method: 'POST', @@ -20817,8 +20646,8 @@ export class Api extends HttpClient([base]), - requestKey: (id: number) => dataTaggedQueryKey([base, id]), - request: (id: number, params: RequestParams = {}) => + requestKey: (id: string) => dataTaggedQueryKey([base, id]), + request: (id: string, params: RequestParams = {}) => this.request({ path: `/api/v1/label/${id}`, method: 'GET', @@ -22067,8 +21896,8 @@ export class Api extends HttpClient([base]), - requestKey: (id: number) => dataTaggedQueryKey([base, id]), - request: (id: number, params: RequestParams = {}) => + requestKey: (id: string) => dataTaggedQueryKey([base, id]), + request: (id: string, params: RequestParams = {}) => this.request({ path: `/api/v1/triggers/${id}`, method: 'GET', @@ -22091,8 +21920,8 @@ export class Api extends HttpClient([base]), - requestKey: (id: number) => dataTaggedQueryKey([base, id]), - request: (id: number, params: RequestParams = {}) => + requestKey: (id: string) => dataTaggedQueryKey([base, id]), + request: (id: string, params: RequestParams = {}) => this.request({ path: `/api/v1/triggers/${id}/retry`, method: 'POST', @@ -22361,8 +22190,8 @@ export class Api extends HttpClient([base]), - requestKey: (keyId: number) => dataTaggedQueryKey([base, keyId]), - request: (keyId: number, params: RequestParams = {}) => + requestKey: (keyId: string) => dataTaggedQueryKey([base, keyId]), + request: (keyId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/user/ssh/${keyId}`, method: 'DELETE', @@ -22433,8 +22262,8 @@ export class Api extends HttpClient([base]), - requestKey: (keyId: number) => dataTaggedQueryKey([base, keyId]), - request: (keyId: number, params: RequestParams = {}) => + requestKey: (keyId: string) => dataTaggedQueryKey([base, keyId]), + request: (keyId: string, params: RequestParams = {}) => this.request({ path: `/api/v1/user/token/${keyId}`, method: 'DELETE', @@ -22508,8 +22337,8 @@ export class Api extends HttpClient([base]), - requestKey: (id: number) => dataTaggedQueryKey([base, id]), - request: (id: number, params: RequestParams = {}) => + requestKey: (id: string) => dataTaggedQueryKey([base, id]), + request: (id: string, params: RequestParams = {}) => this.request({ path: `/api/v1/webhooks/${id}`, method: 'DELETE', From 8ce36fe218e6e614ae867ac7dafaaa7865906a40 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Tue, 1 Sep 2026 10:17:19 +0800 Subject: [PATCH 3/3] fix ui build err --- moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx b/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx index 30c65c580..b1722d42a 100644 --- a/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx +++ b/moon/apps/web/components/CodeView/TreeView/CloneTabs.tsx @@ -72,11 +72,9 @@ const CloneTabs = () => { > - - - - - + HTTP + + SSH