Skip to main content

vectorizer_sdk/client/
hub.rs

1//! HiveHub surface.
2//!
3//! Covers user-scoped backup management (`/hub/backups/*`), usage
4//! statistics (`/hub/usage/*`), and API key validation
5//! (`/hub/validate-key`).
6//!
7//! These endpoints are only meaningful when the server is running
8//! in HiveHub cluster mode (hub integration enabled in `config.yml`).
9//! Calling them on a standalone instance returns a 503 that the SDK
10//! surfaces as a [`VectorizerError`].
11
12use super::VectorizerClient;
13use crate::error::{Result, VectorizerError};
14use crate::models::{
15    CreateUserBackupRequest, HubApiKeyValidation, QuotaInfo, RestoreUserBackupRequest,
16    UploadUserBackupRequest, UsageStatistics, UserBackup,
17};
18
19impl VectorizerClient {
20    /// List all backups owned by a user.
21    ///
22    /// Calls `GET /hub/backups?user_id={user_id}`.
23    pub async fn list_user_backups(&self, user_id: &str) -> Result<Vec<UserBackup>> {
24        let endpoint = format!("/hub/backups?user_id={user_id}");
25        let response = self.make_request("GET", &endpoint, None).await?;
26        let val: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
27            VectorizerError::server(format!("Failed to parse list_user_backups response: {e}"))
28        })?;
29        let arr = val
30            .get("backups")
31            .and_then(|b| b.as_array())
32            .cloned()
33            .unwrap_or_default();
34        arr.into_iter()
35            .map(|v| {
36                serde_json::from_value(v).map_err(|e| {
37                    VectorizerError::server(format!("Failed to parse user backup entry: {e}"))
38                })
39            })
40            .collect()
41    }
42
43    /// Create a new backup for a user.
44    ///
45    /// Calls `POST /hub/backups` with `{user_id, name, description?, collections?}`.
46    pub async fn create_user_backup(&self, request: CreateUserBackupRequest) -> Result<UserBackup> {
47        let payload = serde_json::to_value(&request).map_err(|e| {
48            VectorizerError::server(format!(
49                "Failed to serialize create_user_backup request: {e}"
50            ))
51        })?;
52        let response = self
53            .make_request("POST", "/hub/backups", Some(payload))
54            .await?;
55        let val: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
56            VectorizerError::server(format!("Failed to parse create_user_backup response: {e}"))
57        })?;
58        serde_json::from_value(val.get("backup").cloned().unwrap_or(val)).map_err(|e| {
59            VectorizerError::server(format!("Failed to parse user backup from response: {e}"))
60        })
61    }
62
63    /// Restore a previously created user backup.
64    ///
65    /// Calls `POST /hub/backups/restore`.
66    pub async fn restore_user_backup(&self, request: RestoreUserBackupRequest) -> Result<()> {
67        let payload = serde_json::to_value(&request).map_err(|e| {
68            VectorizerError::server(format!(
69                "Failed to serialize restore_user_backup request: {e}"
70            ))
71        })?;
72        self.make_request("POST", "/hub/backups/restore", Some(payload))
73            .await?;
74        Ok(())
75    }
76
77    /// Upload a backup file (raw bytes).
78    ///
79    /// Calls `POST /hub/backups/upload?user_id={user_id}&name={name}`.
80    ///
81    /// The request body is sent as raw bytes via a POST. The SDK sends
82    /// the binary data as a JSON-encoded base64 string because the
83    /// underlying `Transport::post` takes `Option<&serde_json::Value>`.
84    /// Callers that need true multipart uploads should use the HTTP
85    /// transport directly via `reqwest`.
86    pub async fn upload_user_backup(&self, request: UploadUserBackupRequest) -> Result<UserBackup> {
87        let mut qs = format!("user_id={}", request.user_id);
88        if let Some(name) = &request.name {
89            qs.push_str(&format!("&name={name}"));
90        }
91        let endpoint = format!("/hub/backups/upload?{qs}");
92        // Encode binary data as a JSON value so the transport layer can
93        // forward it. The server accepts raw bytes; this is a best-effort
94        // path. For production uploads use the raw HTTP client.
95        let payload = serde_json::json!({ "data": request.data });
96        let response = self.make_request("POST", &endpoint, Some(payload)).await?;
97        let val: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
98            VectorizerError::server(format!("Failed to parse upload_user_backup response: {e}"))
99        })?;
100        serde_json::from_value(val.get("backup").cloned().unwrap_or(val)).map_err(|e| {
101            VectorizerError::server(format!(
102                "Failed to parse user backup from upload response: {e}"
103            ))
104        })
105    }
106
107    /// Fetch metadata for a single backup.
108    ///
109    /// Calls `GET /hub/backups/{backup_id}?user_id={user_id}`.
110    pub async fn get_user_backup(&self, user_id: &str, backup_id: &str) -> Result<UserBackup> {
111        let endpoint = format!("/hub/backups/{backup_id}?user_id={user_id}");
112        let response = self.make_request("GET", &endpoint, None).await?;
113        let val: serde_json::Value = serde_json::from_str(&response).map_err(|e| {
114            VectorizerError::server(format!("Failed to parse get_user_backup response: {e}"))
115        })?;
116        serde_json::from_value(val.get("backup").cloned().unwrap_or(val)).map_err(|e| {
117            VectorizerError::server(format!(
118                "Failed to parse user backup from get response: {e}"
119            ))
120        })
121    }
122
123    /// Delete a user backup by id.
124    ///
125    /// Calls `DELETE /hub/backups/{backup_id}?user_id={user_id}`.
126    pub async fn delete_user_backup(&self, user_id: &str, backup_id: &str) -> Result<()> {
127        let endpoint = format!("/hub/backups/{backup_id}?user_id={user_id}");
128        self.make_request("DELETE", &endpoint, None).await?;
129        Ok(())
130    }
131
132    /// Download the raw binary data for a backup.
133    ///
134    /// Calls `GET /hub/backups/{backup_id}/download?user_id={user_id}`.
135    ///
136    /// The transport layer returns the response body as a `String`;
137    /// the SDK re-encodes as UTF-8 bytes. For compressed binary
138    /// backups the caller should use the raw HTTP client.
139    pub async fn download_user_backup(&self, user_id: &str, backup_id: &str) -> Result<Vec<u8>> {
140        let endpoint = format!("/hub/backups/{backup_id}/download?user_id={user_id}");
141        let response = self.make_request("GET", &endpoint, None).await?;
142        Ok(response.into_bytes())
143    }
144
145    /// Get aggregate usage statistics for a user.
146    ///
147    /// Calls `GET /hub/usage/statistics?user_id={user_id}`.
148    pub async fn get_usage_statistics(&self, user_id: &str) -> Result<UsageStatistics> {
149        let endpoint = format!("/hub/usage/statistics?user_id={user_id}");
150        let response = self.make_request("GET", &endpoint, None).await?;
151        serde_json::from_str(&response).map_err(|e| {
152            VectorizerError::server(format!(
153                "Failed to parse get_usage_statistics response: {e}"
154            ))
155        })
156    }
157
158    /// Get quota information for a user.
159    ///
160    /// Calls `GET /hub/usage/quota?user_id={user_id}`.
161    pub async fn get_quota_info(&self, user_id: &str) -> Result<QuotaInfo> {
162        let endpoint = format!("/hub/usage/quota?user_id={user_id}");
163        let response = self.make_request("GET", &endpoint, None).await?;
164        serde_json::from_str(&response).map_err(|e| {
165            VectorizerError::server(format!("Failed to parse get_quota_info response: {e}"))
166        })
167    }
168
169    /// Validate a HiveHub API key.
170    ///
171    /// Calls `POST /hub/validate-key`. The key is sent in the
172    /// `Authorization: Bearer <key>` header by the transport layer
173    /// when `api_key` is configured on the client; the `key` parameter
174    /// here is forwarded in the request body for callers that need to
175    /// validate a *different* key than the one on the client.
176    pub async fn validate_hub_api_key(&self, key: &str) -> Result<HubApiKeyValidation> {
177        let payload = serde_json::json!({ "key": key });
178        let response = self
179            .make_request("POST", "/hub/validate-key", Some(payload))
180            .await?;
181        serde_json::from_str(&response).map_err(|e| {
182            VectorizerError::server(format!(
183                "Failed to parse validate_hub_api_key response: {e}"
184            ))
185        })
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    #![allow(clippy::unwrap_used)]
192
193    use serde_json::json;
194
195    use crate::models::{
196        CreateUserBackupRequest, HubApiKeyValidation, QuotaInfo, RestoreUserBackupRequest,
197        UsageStatistics, UserBackup,
198    };
199
200    #[test]
201    fn user_backup_deserializes() {
202        let raw = json!({
203            "id": "b-1",
204            "user_id": "u-1",
205            "name": "my-backup",
206            "collections": ["docs"],
207            "created_at": "2026-05-02T00:00:00Z",
208            "size": 8192u64,
209            "status": "active"
210        });
211        let b: UserBackup = serde_json::from_value(raw).unwrap();
212        assert_eq!(b.id, "b-1");
213        assert_eq!(b.status, "active");
214        assert_eq!(b.size, 8192);
215    }
216
217    #[test]
218    fn user_backup_round_trip() {
219        let b = UserBackup {
220            id: "b-2".into(),
221            user_id: "u-2".into(),
222            name: "weekly".into(),
223            description: Some("desc".into()),
224            collections: vec!["col1".into()],
225            created_at: "2026-05-02T00:00:00Z".into(),
226            size: 1024,
227            status: "active".into(),
228        };
229        let serialized = serde_json::to_value(&b).unwrap();
230        let parsed: UserBackup = serde_json::from_value(serialized).unwrap();
231        assert_eq!(parsed.id, "b-2");
232        assert_eq!(parsed.description.as_deref(), Some("desc"));
233    }
234
235    #[test]
236    fn create_user_backup_request_serializes() {
237        let req = CreateUserBackupRequest {
238            user_id: "u-1".into(),
239            name: "nightly".into(),
240            description: None,
241            collections: Some(vec!["code".into()]),
242        };
243        let v = serde_json::to_value(&req).unwrap();
244        assert_eq!(v["user_id"], "u-1");
245        assert_eq!(v["name"], "nightly");
246        assert_eq!(v["collections"][0], "code");
247        // description should be absent
248        assert!(v.get("description").is_none());
249    }
250
251    #[test]
252    fn restore_user_backup_request_serializes() {
253        let req = RestoreUserBackupRequest {
254            user_id: "u-1".into(),
255            backup_id: "b-99".into(),
256            overwrite: true,
257        };
258        let v = serde_json::to_value(&req).unwrap();
259        assert_eq!(v["backup_id"], "b-99");
260        assert!(v["overwrite"].as_bool().unwrap());
261    }
262
263    #[test]
264    fn usage_statistics_deserializes() {
265        let raw = json!({
266            "success": true,
267            "message": "ok",
268            "stats": {
269                "user_id": "u-1",
270                "total_collections": 3,
271                "total_vectors": 500u64,
272                "total_storage": 512000u64
273            }
274        });
275        let us: UsageStatistics = serde_json::from_value(raw).unwrap();
276        assert!(us.success);
277        assert!(us.stats.is_some());
278    }
279
280    #[test]
281    fn quota_info_deserializes() {
282        let raw = json!({
283            "success": true,
284            "message": "ok",
285            "quota": {
286                "tenant_id": "t-1",
287                "storage": { "limit": 1_000_000u64, "used": 50_000u64 }
288            }
289        });
290        let qi: QuotaInfo = serde_json::from_value(raw).unwrap();
291        assert!(qi.success);
292        assert!(qi.quota.is_some());
293    }
294
295    #[test]
296    fn hub_api_key_validation_deserializes_valid() {
297        let raw = json!({
298            "valid": true,
299            "tenant_id": "t-abc",
300            "tenant_name": "Acme",
301            "permissions": ["Read", "Write"],
302            "validated_at": "2026-05-02T00:00:00Z"
303        });
304        let v: HubApiKeyValidation = serde_json::from_value(raw).unwrap();
305        assert!(v.valid);
306        assert_eq!(v.tenant_id, "t-abc");
307        assert_eq!(v.permissions.len(), 2);
308    }
309
310    #[test]
311    fn hub_api_key_validation_deserializes_invalid() {
312        let raw = json!({
313            "valid": false,
314            "tenant_id": "",
315            "tenant_name": "",
316            "permissions": [],
317            "validated_at": ""
318        });
319        let v: HubApiKeyValidation = serde_json::from_value(raw).unwrap();
320        assert!(!v.valid);
321    }
322}