1use crate::api::auth::{AuthManager, Credentials};
4use crate::api::CardListParams;
5use crate::core::error::{Error, Result};
6use crate::core::models::common::{ExportFormat, UserId};
7#[cfg(feature = "query-builder")]
8use crate::core::models::mbql::MbqlQuery;
9use crate::core::models::{
10 Card, Collection, Dashboard, DatabaseMetadata, DatasetQuery, Field, HealthStatus, MetabaseId,
11 NativeQuery, Pagination, QueryResult, SyncResult, User,
12};
13use crate::transport::HttpClient;
14use secrecy::ExposeSecret;
15use serde::Deserialize;
16use serde_json::{json, Value};
17use std::collections::HashMap;
18
19#[cfg(feature = "cache")]
20use crate::cache::{cache_key, CacheConfig, CacheLayer};
21
22#[derive(Debug, Clone)]
24pub struct MetabaseClient {
25 pub(super) http_client: HttpClient,
26 pub(super) auth_manager: AuthManager,
27 pub(super) base_url: String,
28 #[cfg(feature = "cache")]
29 pub(super) cache: CacheLayer,
30}
31
32impl MetabaseClient {
33 pub fn new(base_url: impl Into<String>) -> Result<Self> {
35 let base_url = base_url.into();
36
37 if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
39 return Err(Error::Config(
40 "Invalid URL: must start with http:// or https://".to_string(),
41 ));
42 }
43
44 let http_client = HttpClient::new(&base_url)?;
45 let auth_manager = AuthManager::new();
46
47 Ok(Self {
48 http_client,
49 auth_manager,
50 base_url,
51 #[cfg(feature = "cache")]
52 cache: CacheLayer::new(CacheConfig::default()),
53 })
54 }
55
56 #[cfg(feature = "cache")]
58 pub fn with_cache(base_url: impl Into<String>, cache_config: CacheConfig) -> Result<Self> {
59 let base_url = base_url.into();
60
61 if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
63 return Err(Error::Config(
64 "Invalid URL: must start with http:// or https://".to_string(),
65 ));
66 }
67
68 let http_client = HttpClient::new(&base_url)?;
69 let auth_manager = AuthManager::new();
70
71 Ok(Self {
72 http_client,
73 auth_manager,
74 base_url,
75 cache: CacheLayer::new(cache_config),
76 })
77 }
78
79 pub fn base_url(&self) -> &str {
81 &self.base_url
82 }
83
84 pub fn is_authenticated(&self) -> bool {
86 self.auth_manager.is_authenticated()
87 }
88
89 #[cfg(feature = "cache")]
91 pub fn is_cache_enabled(&self) -> bool {
92 self.cache.is_enabled()
93 }
94
95 #[cfg(feature = "cache")]
97 pub fn set_cache_enabled(&mut self, enabled: bool) {
98 self.cache.set_enabled(enabled);
99 }
100
101 #[cfg(not(feature = "cache"))]
103 pub fn is_cache_enabled(&self) -> bool {
104 false
105 }
106
107 #[cfg(not(feature = "cache"))]
109 pub fn set_cache_enabled(&mut self, _enabled: bool) {
110 }
112
113 pub async fn authenticate(&mut self, credentials: Credentials) -> Result<()> {
115 let request_body = match &credentials {
116 Credentials::EmailPassword { email, password } => {
117 json!({
118 "username": email,
119 "password": password.expose_secret()
120 })
121 }
122 Credentials::ApiKey { key } => {
123 json!({
124 "api_key": key.expose_secret()
125 })
126 }
127 };
128
129 #[derive(Deserialize)]
130 struct SessionResponse {
131 id: String,
132 #[serde(flatten)]
133 user_data: serde_json::Value,
134 }
135
136 let response: SessionResponse =
137 self.http_client.post("/api/session", &request_body).await?;
138
139 let user = User {
141 id: UserId(response.user_data["id"].as_i64().unwrap_or(1)),
142 email: response.user_data["email"]
143 .as_str()
144 .unwrap_or("unknown@example.com")
145 .to_string(),
146 first_name: response.user_data["first_name"]
147 .as_str()
148 .unwrap_or("Unknown")
149 .to_string(),
150 last_name: response.user_data["last_name"]
151 .as_str()
152 .unwrap_or("User")
153 .to_string(),
154 is_superuser: response.user_data["is_superuser"]
155 .as_bool()
156 .unwrap_or(false),
157 is_active: true,
158 is_qbnewb: response.user_data["is_qbnewb"].as_bool().unwrap_or(false),
159 date_joined: chrono::Utc::now(),
160 last_login: Some(chrono::Utc::now()),
161 common_name: None,
162 group_ids: Vec::new(),
163 locale: response.user_data["locale"].as_str().map(|s| s.to_string()),
164 google_auth: response.user_data["google_auth"].as_bool().unwrap_or(false),
165 ldap_auth: response.user_data["ldap_auth"].as_bool().unwrap_or(false),
166 login_attributes: None,
167 user_group_memberships: Vec::new(),
168 };
169
170 self.auth_manager.set_session(response.id, user);
171 Ok(())
172 }
173
174 pub async fn logout(&mut self) -> Result<()> {
176 if !self.is_authenticated() {
177 return Ok(());
178 }
179
180 self.http_client.delete("/api/session").await?;
182
183 self.auth_manager.clear_session();
185 Ok(())
186 }
187
188 pub async fn health_check(&self) -> Result<HealthStatus> {
190 self.http_client.get("/api/health").await
191 }
192
193 pub async fn get_current_user(&self) -> Result<User> {
195 if !self.is_authenticated() {
196 return Err(Error::Authentication("Not authenticated".to_string()));
197 }
198
199 self.http_client.get("/api/user/current").await
200 }
201
202 pub async fn get_card(&self, id: i64) -> Result<Card> {
206 #[cfg(feature = "cache")]
207 {
208 let cache_key = cache_key("card", id);
209 if let Some(card) = self.cache.get_metadata::<Card>(&cache_key) {
210 return Ok(card);
211 }
212 }
213
214 let path = format!("/api/card/{}", id);
215 let card: Card = self.http_client.get(&path).await?;
216
217 #[cfg(feature = "cache")]
218 {
219 let cache_key = cache_key("card", id);
220 let _ = self.cache.set_metadata(cache_key, &card);
221 }
222
223 Ok(card)
224 }
225
226 pub async fn list_cards(&self, params: Option<CardListParams>) -> Result<Vec<Card>> {
228 let path = if let Some(p) = params {
229 let mut query_params = Vec::new();
230 if let Some(f) = p.f {
231 query_params.push(format!("f={}", f));
232 }
233 if let Some(model_type) = p.model_type {
234 query_params.push(format!("model_type={}", model_type));
235 }
236
237 if !query_params.is_empty() {
238 format!("/api/card?{}", query_params.join("&"))
239 } else {
240 "/api/card".to_string()
241 }
242 } else {
243 "/api/card".to_string()
244 };
245 self.http_client.get(&path).await
246 }
247
248 pub async fn create_card(&self, card: Card) -> Result<Card> {
250 if !self.is_authenticated() {
251 return Err(Error::Authentication(
252 "Authentication required to create card".to_string(),
253 ));
254 }
255 self.http_client.post("/api/card", &card).await
256 }
257
258 pub async fn update_card(&self, id: i64, updates: serde_json::Value) -> Result<Card> {
260 if !self.is_authenticated() {
261 return Err(Error::Authentication(
262 "Authentication required to update card".to_string(),
263 ));
264 }
265
266 #[cfg(feature = "cache")]
267 {
268 let cache_key = cache_key("card", id);
269 self.cache.invalidate(&cache_key);
270 }
271
272 let path = format!("/api/card/{}", id);
273 self.http_client.put(&path, &updates).await
274 }
275
276 pub async fn delete_card(&self, id: i64) -> Result<()> {
278 if !self.is_authenticated() {
279 return Err(Error::Authentication(
280 "Authentication required to delete card".to_string(),
281 ));
282 }
283
284 #[cfg(feature = "cache")]
285 {
286 let cache_key = cache_key("card", id);
287 self.cache.invalidate(&cache_key);
288 }
289
290 let path = format!("/api/card/{}", id);
291 self.http_client.delete(&path).await
292 }
293
294 pub async fn get_collection(&self, id: MetabaseId) -> Result<Collection> {
298 #[cfg(feature = "cache")]
299 {
300 let cache_key = cache_key("collection", id.0);
301 if let Some(collection) = self.cache.get_metadata::<Collection>(&cache_key) {
302 return Ok(collection);
303 }
304 }
305
306 let path = format!("/api/collection/{}", id.0);
307 let collection: Collection = self.http_client.get(&path).await?;
308
309 #[cfg(feature = "cache")]
310 {
311 let cache_key = cache_key("collection", id.0);
312 let _ = self.cache.set_metadata(cache_key, &collection);
313 }
314
315 Ok(collection)
316 }
317
318 pub async fn list_collections(&self) -> Result<Vec<Collection>> {
320 self.http_client.get("/api/collection").await
321 }
322
323 pub async fn create_collection(&self, collection: Collection) -> Result<Collection> {
325 if !self.is_authenticated() {
326 return Err(Error::Authentication(
327 "Authentication required to create collection".to_string(),
328 ));
329 }
330 self.http_client.post("/api/collection", &collection).await
331 }
332
333 pub async fn update_collection(
335 &self,
336 id: MetabaseId,
337 updates: serde_json::Value,
338 ) -> Result<Collection> {
339 if !self.is_authenticated() {
340 return Err(Error::Authentication(
341 "Authentication required to update collection".to_string(),
342 ));
343 }
344
345 #[cfg(feature = "cache")]
346 {
347 let cache_key = cache_key("collection", id.0);
348 self.cache.invalidate(&cache_key);
349 }
350
351 let path = format!("/api/collection/{}", id.0);
352 self.http_client.put(&path, &updates).await
353 }
354
355 pub async fn archive_collection(&self, id: MetabaseId) -> Result<Collection> {
357 if !self.is_authenticated() {
358 return Err(Error::Authentication(
359 "Authentication required to archive collection".to_string(),
360 ));
361 }
362
363 #[cfg(feature = "cache")]
364 {
365 let cache_key = cache_key("collection", id.0);
366 self.cache.invalidate(&cache_key);
367 }
368
369 let path = format!("/api/collection/{}", id.0);
370 let updates = json!({ "archived": true });
371 self.http_client.put(&path, &updates).await
372 }
373
374 pub async fn get_dashboard(&self, id: MetabaseId) -> Result<Dashboard> {
378 #[cfg(feature = "cache")]
379 {
380 let cache_key = cache_key("dashboard", id.0);
381 if let Some(dashboard) = self.cache.get_metadata::<Dashboard>(&cache_key) {
382 return Ok(dashboard);
383 }
384 }
385
386 let path = format!("/api/dashboard/{}", id.0);
387 let dashboard: Dashboard = self.http_client.get(&path).await?;
388
389 #[cfg(feature = "cache")]
390 {
391 let cache_key = cache_key("dashboard", id.0);
392 let _ = self.cache.set_metadata(cache_key, &dashboard);
393 }
394
395 Ok(dashboard)
396 }
397
398 pub async fn list_dashboards(&self, pagination: Option<Pagination>) -> Result<Vec<Dashboard>> {
400 let path = if let Some(p) = pagination {
401 format!("/api/dashboard?limit={}&offset={}", p.limit(), p.offset())
402 } else {
403 "/api/dashboard".to_string()
404 };
405 self.http_client.get(&path).await
406 }
407
408 pub async fn create_dashboard(&self, dashboard: Dashboard) -> Result<Dashboard> {
410 if !self.is_authenticated() {
411 return Err(Error::Authentication(
412 "Authentication required to create dashboard".to_string(),
413 ));
414 }
415 self.http_client.post("/api/dashboard", &dashboard).await
416 }
417
418 pub async fn update_dashboard(
420 &self,
421 id: MetabaseId,
422 updates: serde_json::Value,
423 ) -> Result<Dashboard> {
424 if !self.is_authenticated() {
425 return Err(Error::Authentication(
426 "Authentication required to update dashboard".to_string(),
427 ));
428 }
429
430 #[cfg(feature = "cache")]
431 {
432 let cache_key = cache_key("dashboard", id.0);
433 self.cache.invalidate(&cache_key);
434 }
435
436 let path = format!("/api/dashboard/{}", id.0);
437 self.http_client.put(&path, &updates).await
438 }
439
440 pub async fn delete_dashboard(&self, id: MetabaseId) -> Result<()> {
442 if !self.is_authenticated() {
443 return Err(Error::Authentication(
444 "Authentication required to delete dashboard".to_string(),
445 ));
446 }
447
448 #[cfg(feature = "cache")]
449 {
450 let cache_key = cache_key("dashboard", id.0);
451 self.cache.invalidate(&cache_key);
452 }
453
454 let path = format!("/api/dashboard/{}", id.0);
455 self.http_client.delete(&path).await
456 }
457
458 pub async fn execute_query(&self, query: DatasetQuery) -> Result<QueryResult> {
462 if !self.is_authenticated() {
463 return Err(Error::Authentication(
464 "Authentication required to execute query".to_string(),
465 ));
466 }
467 let request = json!({
468 "database": query.database.0,
469 "type": query.query_type,
470 "query": query.query,
471 "parameters": query.parameters
472 });
473 self.http_client.post("/api/dataset", &request).await
474 }
475
476 pub async fn execute_native_query(
478 &self,
479 database: MetabaseId,
480 native_query: NativeQuery,
481 ) -> Result<QueryResult> {
482 if !self.is_authenticated() {
483 return Err(Error::Authentication(
484 "Authentication required to execute native query".to_string(),
485 ));
486 }
487 let request = json!({
488 "database": database.0,
489 "type": "native",
490 "native": {
491 "query": native_query.query,
492 "template-tags": native_query.template_tags
493 }
494 });
495 self.http_client.post("/api/dataset", &request).await
496 }
497
498 pub async fn execute_card_query(
502 &self,
503 card_id: i64,
504 parameters: Option<Value>,
505 ) -> Result<QueryResult> {
506 if !self.is_authenticated() {
507 return Err(Error::Authentication(
508 "Authentication required to execute card query".to_string(),
509 ));
510 }
511
512 let path = format!("/api/card/{}/query", card_id);
513 let request = if let Some(params) = parameters {
514 json!({ "parameters": params })
515 } else {
516 json!({})
517 };
518
519 self.http_client.post(&path, &request).await
520 }
521
522 pub async fn export_card_query(
524 &self,
525 card_id: i64,
526 format: ExportFormat,
527 parameters: Option<Value>,
528 ) -> Result<Vec<u8>> {
529 if !self.is_authenticated() {
530 return Err(Error::Authentication(
531 "Authentication required to export card query".to_string(),
532 ));
533 }
534
535 let path = format!("/api/card/{}/query/{}", card_id, format.as_str());
536 let request = if let Some(params) = parameters {
537 json!({ "parameters": params })
538 } else {
539 json!({})
540 };
541
542 self.http_client.post_binary(&path, &request).await
543 }
544
545 pub async fn execute_card_pivot_query(
547 &self,
548 card_id: i64,
549 parameters: Option<Value>,
550 ) -> Result<QueryResult> {
551 if !self.is_authenticated() {
552 return Err(Error::Authentication(
553 "Authentication required to execute pivot query".to_string(),
554 ));
555 }
556
557 let path = format!("/api/card/pivot/{}/query", card_id);
558 let request = if let Some(params) = parameters {
559 json!({ "parameters": params })
560 } else {
561 json!({})
562 };
563
564 self.http_client.post(&path, &request).await
565 }
566
567 pub async fn get_database_metadata(&self, database_id: MetabaseId) -> Result<DatabaseMetadata> {
571 #[cfg(feature = "cache")]
572 {
573 let cache_key = cache_key("database_metadata", database_id.0);
574 if let Some(metadata) = self.cache.get_metadata::<DatabaseMetadata>(&cache_key) {
575 return Ok(metadata);
576 }
577 }
578
579 let path = format!("/api/database/{}/metadata", database_id.0);
580 let metadata: DatabaseMetadata = self.http_client.get(&path).await?;
581
582 #[cfg(feature = "cache")]
583 {
584 let cache_key = cache_key("database_metadata", database_id.0);
585 let _ = self.cache.set_metadata(cache_key, &metadata);
586 }
587
588 Ok(metadata)
589 }
590
591 pub async fn sync_database_schema(&self, database_id: MetabaseId) -> Result<SyncResult> {
593 if !self.is_authenticated() {
594 return Err(Error::Authentication(
595 "Authentication required to sync database schema".to_string(),
596 ));
597 }
598
599 #[cfg(feature = "cache")]
600 {
601 let cache_key = cache_key("database_metadata", database_id.0);
602 self.cache.invalidate(&cache_key);
603 }
604
605 let path = format!("/api/database/{}/sync_schema", database_id.0);
606 self.http_client.post(&path, &json!({})).await
607 }
608
609 pub async fn get_database_fields(&self, database_id: MetabaseId) -> Result<Vec<Field>> {
611 let path = format!("/api/database/{}/fields", database_id.0);
612 self.http_client.get(&path).await
613 }
614
615 pub async fn get_database_schemas(&self, database_id: MetabaseId) -> Result<Vec<String>> {
617 let path = format!("/api/database/{}/schemas", database_id.0);
618 self.http_client.get(&path).await
619 }
620
621 pub async fn execute_dataset_query(&self, query: Value) -> Result<QueryResult> {
625 if !self.is_authenticated() {
626 return Err(Error::Authentication(
627 "Authentication required to execute dataset query".to_string(),
628 ));
629 }
630
631 self.http_client.post("/api/dataset", &query).await
632 }
633
634 pub async fn execute_dataset_native(&self, query: Value) -> Result<QueryResult> {
636 if !self.is_authenticated() {
637 return Err(Error::Authentication(
638 "Authentication required to execute native dataset query".to_string(),
639 ));
640 }
641
642 self.http_client.post("/api/dataset/native", &query).await
643 }
644
645 pub async fn execute_dataset_pivot(&self, query: Value) -> Result<QueryResult> {
647 if !self.is_authenticated() {
648 return Err(Error::Authentication(
649 "Authentication required to execute pivot dataset query".to_string(),
650 ));
651 }
652
653 self.http_client.post("/api/dataset/pivot", &query).await
654 }
655
656 pub async fn export_dataset(&self, format: ExportFormat, query: Value) -> Result<Vec<u8>> {
658 if !self.is_authenticated() {
659 return Err(Error::Authentication(
660 "Authentication required to export dataset".to_string(),
661 ));
662 }
663
664 let path = format!("/api/dataset/{}", format.as_str());
665 self.http_client.post_binary(&path, &query).await
666 }
667
668 #[cfg(feature = "query-builder")]
672 pub async fn execute_mbql_query(
673 &self,
674 database_id: MetabaseId,
675 query: MbqlQuery,
676 ) -> Result<QueryResult> {
677 if !self.is_authenticated() {
678 return Err(Error::Authentication(
679 "Authentication required to execute MBQL query".to_string(),
680 ));
681 }
682
683 let dataset_query = query.to_dataset_query(database_id);
684 self.http_client.post("/api/dataset", &dataset_query).await
685 }
686
687 #[cfg(feature = "query-builder")]
689 pub async fn export_mbql_query(
690 &self,
691 database_id: MetabaseId,
692 query: MbqlQuery,
693 format: ExportFormat,
694 ) -> Result<Vec<u8>> {
695 if !self.is_authenticated() {
696 return Err(Error::Authentication(
697 "Authentication required to export MBQL query".to_string(),
698 ));
699 }
700
701 let dataset_query = query.to_dataset_query(database_id);
702 let path = format!("/api/dataset/{}", format.as_str());
703 self.http_client.post_binary(&path, &dataset_query).await
704 }
705
706 pub async fn execute_sql(&self, database_id: MetabaseId, sql: &str) -> Result<QueryResult> {
710 let native_query = NativeQuery::new(sql);
711 self.execute_native_query(database_id, native_query).await
712 }
713
714 pub async fn execute_sql_with_params(
716 &self,
717 database_id: MetabaseId,
718 sql: &str,
719 params: HashMap<String, Value>,
720 ) -> Result<QueryResult> {
721 let mut native_query = NativeQuery::new(sql);
722 for (name, value) in params {
723 native_query = native_query.with_param(&name, value);
724 }
725 self.execute_native_query(database_id, native_query).await
726 }
727
728 pub async fn export_sql_query(
730 &self,
731 database_id: MetabaseId,
732 sql: &str,
733 format: ExportFormat,
734 ) -> Result<Vec<u8>> {
735 if !self.is_authenticated() {
736 return Err(Error::Authentication(
737 "Authentication required to export SQL query".to_string(),
738 ));
739 }
740
741 let native_query = NativeQuery::new(sql);
742 let request = json!({
743 "database": database_id.0,
744 "type": "native",
745 "native": {
746 "query": native_query.query,
747 "template-tags": native_query.template_tags
748 }
749 });
750
751 let path = format!("/api/dataset/{}", format.as_str());
752 self.http_client.post_binary(&path, &request).await
753 }
754}