metabase_api_rs/repository/
card.rs

1//! Card repository trait and implementations
2//!
3//! This module provides the repository abstraction for Card entities.
4
5use super::traits::{
6    FilterParams, PaginationParams, Repository, RepositoryError, RepositoryResult,
7};
8use crate::core::models::common::CardId;
9use crate::core::models::Card;
10use crate::transport::http_provider_safe::{HttpProviderExt, HttpProviderSafe};
11use async_trait::async_trait;
12use std::sync::Arc;
13
14/// Card-specific filter parameters
15#[derive(Debug, Clone, Default)]
16pub struct CardFilterParams {
17    /// Filter by f parameter
18    pub f: Option<String>,
19    /// Filter by model type
20    pub model_type: Option<String>,
21    /// Filter by archived status
22    pub archived: Option<bool>,
23    /// Filter by collection ID
24    pub collection_id: Option<i32>,
25}
26
27impl CardFilterParams {
28    /// Create new card filter params
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Set collection ID filter
34    pub fn with_collection(mut self, collection_id: i32) -> Self {
35        self.collection_id = Some(collection_id);
36        self
37    }
38
39    // Set database ID filter (removed - not in current struct)
40    // pub fn with_database(mut self, database_id: i32) -> Self {
41    //     self.database_id = Some(database_id);
42    //     self
43    // }
44}
45
46/// Repository trait for Card entities
47#[async_trait]
48pub trait CardRepository: Repository<Entity = Card, Id = CardId> + Send + Sync {
49    /// Cast to Any for type checking in tests
50    fn as_any(&self) -> &dyn std::any::Any;
51    /// List cards with card-specific filters
52    async fn list_with_filters(
53        &self,
54        pagination: Option<PaginationParams>,
55        filters: Option<CardFilterParams>,
56    ) -> RepositoryResult<Vec<Card>>;
57
58    /// Get cards in a specific collection
59    async fn get_by_collection(&self, collection_id: i32) -> RepositoryResult<Vec<Card>>;
60
61    /// Search cards by query
62    async fn search(&self, query: &str) -> RepositoryResult<Vec<Card>>;
63
64    /// Archive a card
65    async fn archive(&self, id: &CardId) -> RepositoryResult<()>;
66
67    /// Unarchive a card
68    async fn unarchive(&self, id: &CardId) -> RepositoryResult<()>;
69
70    /// Copy a card
71    async fn copy(&self, id: &CardId, new_name: &str) -> RepositoryResult<Card>;
72
73    /// Execute a card's query
74    async fn execute_query(
75        &self,
76        id: &CardId,
77        parameters: Option<serde_json::Value>,
78    ) -> RepositoryResult<crate::core::models::QueryResult>;
79
80    /// Export card query results
81    async fn export_query(
82        &self,
83        id: &CardId,
84        format: crate::core::models::common::ExportFormat,
85        parameters: Option<serde_json::Value>,
86    ) -> RepositoryResult<Vec<u8>>;
87
88    /// Execute a pivot query for a card
89    async fn execute_pivot_query(
90        &self,
91        id: &CardId,
92        parameters: Option<serde_json::Value>,
93    ) -> RepositoryResult<crate::core::models::QueryResult>;
94}
95
96/// HTTP implementation of CardRepository
97pub struct HttpCardRepository {
98    http_provider: Arc<dyn HttpProviderSafe>,
99}
100
101impl HttpCardRepository {
102    /// Create a new HTTP card repository
103    pub fn new(http_provider: Arc<dyn HttpProviderSafe>) -> Self {
104        Self { http_provider }
105    }
106
107    /// Convert filter params to query string
108    fn build_query_params(
109        &self,
110        pagination: Option<PaginationParams>,
111        filters: Option<FilterParams>,
112    ) -> String {
113        let mut params = Vec::new();
114
115        if let Some(p) = pagination {
116            if let Some(page) = p.page {
117                params.push(format!("page={}", page));
118            }
119            if let Some(limit) = p.limit {
120                params.push(format!("limit={}", limit));
121            }
122            if let Some(offset) = p.offset {
123                params.push(format!("offset={}", offset));
124            }
125        }
126
127        if let Some(f) = filters {
128            if let Some(query) = f.query {
129                params.push(format!("q={}", query.replace(' ', "+")));
130            }
131            if let Some(archived) = f.archived {
132                params.push(format!("archived={}", archived));
133            }
134        }
135
136        if params.is_empty() {
137            String::new()
138        } else {
139            format!("?{}", params.join("&"))
140        }
141    }
142}
143
144#[async_trait]
145impl Repository for HttpCardRepository {
146    type Entity = Card;
147    type Id = CardId;
148
149    async fn get(&self, id: &CardId) -> RepositoryResult<Card> {
150        let path = format!("/api/card/{}", id.0);
151        self.http_provider.get(&path).await.map_err(|e| e.into())
152    }
153
154    async fn list(
155        &self,
156        pagination: Option<PaginationParams>,
157        filters: Option<FilterParams>,
158    ) -> RepositoryResult<Vec<Card>> {
159        let query = self.build_query_params(pagination, filters);
160        let path = format!("/api/card{}", query);
161        self.http_provider.get(&path).await.map_err(|e| e.into())
162    }
163
164    async fn create(&self, entity: &Card) -> RepositoryResult<Card> {
165        self.http_provider
166            .post("/api/card", entity)
167            .await
168            .map_err(|e| e.into())
169    }
170
171    async fn update(&self, id: &CardId, entity: &Card) -> RepositoryResult<Card> {
172        let path = format!("/api/card/{}", id.0);
173        self.http_provider
174            .put(&path, entity)
175            .await
176            .map_err(|e| e.into())
177    }
178
179    async fn delete(&self, id: &CardId) -> RepositoryResult<()> {
180        let path = format!("/api/card/{}", id.0);
181        self.http_provider.delete(&path).await.map_err(|e| e.into())
182    }
183}
184
185#[async_trait]
186impl CardRepository for HttpCardRepository {
187    fn as_any(&self) -> &dyn std::any::Any {
188        self
189    }
190    async fn list_with_filters(
191        &self,
192        pagination: Option<PaginationParams>,
193        filters: Option<CardFilterParams>,
194    ) -> RepositoryResult<Vec<Card>> {
195        let mut params = Vec::new();
196
197        if let Some(p) = pagination {
198            if let Some(page) = p.page {
199                params.push(format!("page={}", page));
200            }
201            if let Some(limit) = p.limit {
202                params.push(format!("limit={}", limit));
203            }
204            if let Some(offset) = p.offset {
205                params.push(format!("offset={}", offset));
206            }
207        }
208
209        if let Some(f) = filters {
210            if let Some(f_param) = f.f {
211                params.push(format!("f={}", f_param));
212            }
213            if let Some(model_type) = f.model_type {
214                params.push(format!("model_type={}", model_type));
215            }
216            if let Some(archived) = f.archived {
217                params.push(format!("archived={}", archived));
218            }
219            if let Some(collection_id) = f.collection_id {
220                params.push(format!("collection_id={}", collection_id));
221            }
222        }
223
224        let path = if params.is_empty() {
225            "/api/card".to_string()
226        } else {
227            format!("/api/card?{}", params.join("&"))
228        };
229
230        self.http_provider.get(&path).await.map_err(|e| e.into())
231    }
232
233    async fn get_by_collection(&self, collection_id: i32) -> RepositoryResult<Vec<Card>> {
234        let path = format!("/api/collection/{}/items", collection_id);
235        // This returns collection items, we need to filter for cards
236        let _items: serde_json::Value = self
237            .http_provider
238            .get(&path)
239            .await
240            .map_err(RepositoryError::from)?;
241
242        // Extract cards from the response
243        // This is a simplified version, actual implementation would parse properly
244        Ok(Vec::new())
245    }
246
247    async fn search(&self, query: &str) -> RepositoryResult<Vec<Card>> {
248        let filters = FilterParams::new().with_query(query);
249        self.list(None, Some(filters)).await
250    }
251
252    async fn archive(&self, id: &CardId) -> RepositoryResult<()> {
253        let path = format!("/api/card/{}", id.0);
254        let body = serde_json::json!({ "archived": true });
255        self.http_provider
256            .put(&path, &body)
257            .await
258            .map(|_: serde_json::Value| ())
259            .map_err(|e| e.into())
260    }
261
262    async fn unarchive(&self, id: &CardId) -> RepositoryResult<()> {
263        let path = format!("/api/card/{}", id.0);
264        let body = serde_json::json!({ "archived": false });
265        self.http_provider
266            .put(&path, &body)
267            .await
268            .map(|_: serde_json::Value| ())
269            .map_err(|e| e.into())
270    }
271
272    async fn copy(&self, id: &CardId, new_name: &str) -> RepositoryResult<Card> {
273        let path = format!("/api/card/{}/copy", id.0);
274        let body = serde_json::json!({ "name": new_name });
275        self.http_provider
276            .post(&path, &body)
277            .await
278            .map_err(|e| e.into())
279    }
280
281    async fn execute_query(
282        &self,
283        id: &CardId,
284        parameters: Option<serde_json::Value>,
285    ) -> RepositoryResult<crate::core::models::QueryResult> {
286        let path = format!("/api/card/{}/query", id.0);
287        let request = if let Some(params) = parameters {
288            serde_json::json!({ "parameters": params })
289        } else {
290            serde_json::json!({})
291        };
292        self.http_provider
293            .post(&path, &request)
294            .await
295            .map_err(|e| e.into())
296    }
297
298    async fn export_query(
299        &self,
300        id: &CardId,
301        format: crate::core::models::common::ExportFormat,
302        parameters: Option<serde_json::Value>,
303    ) -> RepositoryResult<Vec<u8>> {
304        let path = format!("/api/card/{}/query/{}", id.0, format.as_str());
305        let request = if let Some(params) = parameters {
306            serde_json::json!({ "parameters": params })
307        } else {
308            serde_json::json!({})
309        };
310        // Use post_binary for export operations that return binary data (CSV, XLSX, etc.)
311        self.http_provider
312            .post_binary(&path, request)
313            .await
314            .map_err(RepositoryError::from)
315    }
316
317    async fn execute_pivot_query(
318        &self,
319        id: &CardId,
320        parameters: Option<serde_json::Value>,
321    ) -> RepositoryResult<crate::core::models::QueryResult> {
322        let path = format!("/api/card/pivot/{}/query", id.0);
323        let request = if let Some(params) = parameters {
324            serde_json::json!({ "parameters": params })
325        } else {
326            serde_json::json!({})
327        };
328        self.http_provider
329            .post(&path, &request)
330            .await
331            .map_err(|e| e.into())
332    }
333}
334
335/// Mock implementation of CardRepository for testing
336pub struct MockCardRepository {
337    cards: Arc<tokio::sync::RwLock<Vec<Card>>>,
338    should_fail: bool,
339}
340
341impl MockCardRepository {
342    /// Create a new mock card repository
343    pub fn new() -> Self {
344        Self {
345            cards: Arc::new(tokio::sync::RwLock::new(Vec::new())),
346            should_fail: false,
347        }
348    }
349
350    /// Set whether operations should fail
351    pub fn set_should_fail(&mut self, should_fail: bool) {
352        self.should_fail = should_fail;
353    }
354
355    /// Add a card to the mock repository
356    pub async fn add_card(&self, card: Card) {
357        let mut cards = self.cards.write().await;
358        cards.push(card);
359    }
360}
361
362impl Default for MockCardRepository {
363    fn default() -> Self {
364        Self::new()
365    }
366}
367
368#[async_trait]
369impl Repository for MockCardRepository {
370    type Entity = Card;
371    type Id = CardId;
372
373    async fn get(&self, id: &CardId) -> RepositoryResult<Card> {
374        if self.should_fail {
375            return Err(RepositoryError::Other("Mock failure".to_string()));
376        }
377
378        let cards = self.cards.read().await;
379        cards
380            .iter()
381            .find(|c| c.id == Some(*id))
382            .cloned()
383            .ok_or_else(|| RepositoryError::NotFound(format!("Card {} not found", id.0)))
384    }
385
386    async fn list(
387        &self,
388        _pagination: Option<PaginationParams>,
389        _filters: Option<FilterParams>,
390    ) -> RepositoryResult<Vec<Card>> {
391        if self.should_fail {
392            return Err(RepositoryError::Other("Mock failure".to_string()));
393        }
394
395        let cards = self.cards.read().await;
396        Ok(cards.clone())
397    }
398
399    async fn create(&self, entity: &Card) -> RepositoryResult<Card> {
400        if self.should_fail {
401            return Err(RepositoryError::Other("Mock failure".to_string()));
402        }
403
404        let mut cards = self.cards.write().await;
405        let mut new_card = entity.clone();
406        // Generate a mock ID if not present
407        if new_card.id.is_none() {
408            new_card.id = Some(CardId((cards.len() + 1) as i32));
409        }
410        cards.push(new_card.clone());
411        Ok(new_card)
412    }
413
414    async fn update(&self, id: &CardId, entity: &Card) -> RepositoryResult<Card> {
415        if self.should_fail {
416            return Err(RepositoryError::Other("Mock failure".to_string()));
417        }
418
419        let mut cards = self.cards.write().await;
420        if let Some(card) = cards.iter_mut().find(|c| c.id == Some(*id)) {
421            *card = entity.clone();
422            card.id = Some(*id); // Ensure ID is preserved
423            Ok(card.clone())
424        } else {
425            Err(RepositoryError::NotFound(format!(
426                "Card {} not found",
427                id.0
428            )))
429        }
430    }
431
432    async fn delete(&self, id: &CardId) -> RepositoryResult<()> {
433        if self.should_fail {
434            return Err(RepositoryError::Other("Mock failure".to_string()));
435        }
436
437        let mut cards = self.cards.write().await;
438        let initial_len = cards.len();
439        cards.retain(|c| c.id != Some(*id));
440
441        if cards.len() < initial_len {
442            Ok(())
443        } else {
444            Err(RepositoryError::NotFound(format!(
445                "Card {} not found",
446                id.0
447            )))
448        }
449    }
450}
451
452#[async_trait]
453impl CardRepository for MockCardRepository {
454    fn as_any(&self) -> &dyn std::any::Any {
455        self
456    }
457    async fn list_with_filters(
458        &self,
459        pagination: Option<PaginationParams>,
460        _filters: Option<CardFilterParams>,
461    ) -> RepositoryResult<Vec<Card>> {
462        // For mock, just return all cards
463        self.list(pagination, None).await
464    }
465
466    async fn get_by_collection(&self, collection_id: i32) -> RepositoryResult<Vec<Card>> {
467        if self.should_fail {
468            return Err(RepositoryError::Other("Mock failure".to_string()));
469        }
470
471        let cards = self.cards.read().await;
472        Ok(cards
473            .iter()
474            .filter(|c| c.collection_id == Some(collection_id))
475            .cloned()
476            .collect())
477    }
478
479    async fn search(&self, query: &str) -> RepositoryResult<Vec<Card>> {
480        if self.should_fail {
481            return Err(RepositoryError::Other("Mock failure".to_string()));
482        }
483
484        let cards = self.cards.read().await;
485        Ok(cards
486            .iter()
487            .filter(|c| {
488                c.name.to_lowercase().contains(&query.to_lowercase())
489                    || c.description
490                        .as_ref()
491                        .map(|d| d.to_lowercase().contains(&query.to_lowercase()))
492                        .unwrap_or(false)
493            })
494            .cloned()
495            .collect())
496    }
497
498    async fn archive(&self, id: &CardId) -> RepositoryResult<()> {
499        if self.should_fail {
500            return Err(RepositoryError::Other("Mock failure".to_string()));
501        }
502
503        let mut cards = self.cards.write().await;
504        if let Some(card) = cards.iter_mut().find(|c| c.id == Some(*id)) {
505            card.archived = true;
506            Ok(())
507        } else {
508            Err(RepositoryError::NotFound(format!(
509                "Card {} not found",
510                id.0
511            )))
512        }
513    }
514
515    async fn unarchive(&self, id: &CardId) -> RepositoryResult<()> {
516        if self.should_fail {
517            return Err(RepositoryError::Other("Mock failure".to_string()));
518        }
519
520        let mut cards = self.cards.write().await;
521        if let Some(card) = cards.iter_mut().find(|c| c.id == Some(*id)) {
522            card.archived = false;
523            Ok(())
524        } else {
525            Err(RepositoryError::NotFound(format!(
526                "Card {} not found",
527                id.0
528            )))
529        }
530    }
531
532    async fn copy(&self, id: &CardId, new_name: &str) -> RepositoryResult<Card> {
533        if self.should_fail {
534            return Err(RepositoryError::Other("Mock failure".to_string()));
535        }
536
537        let mut cards = self.cards.write().await;
538        if let Some(original) = cards.iter().find(|c| c.id == Some(*id)) {
539            let mut new_card = original.clone();
540            new_card.id = Some(CardId((cards.len() + 1) as i32));
541            new_card.name = new_name.to_string();
542            cards.push(new_card.clone());
543            Ok(new_card)
544        } else {
545            Err(RepositoryError::NotFound(format!(
546                "Card {} not found",
547                id.0
548            )))
549        }
550    }
551
552    async fn execute_query(
553        &self,
554        id: &CardId,
555        _parameters: Option<serde_json::Value>,
556    ) -> RepositoryResult<crate::core::models::QueryResult> {
557        if self.should_fail {
558            return Err(RepositoryError::Other("Mock failure".to_string()));
559        }
560
561        // Check if card exists
562        let cards = self.cards.read().await;
563        if !cards.iter().any(|c| c.id == Some(*id)) {
564            return Err(RepositoryError::NotFound(format!(
565                "Card {} not found",
566                id.0
567            )));
568        }
569
570        // Return mock query result
571        use crate::core::models::common::MetabaseId;
572        use crate::core::models::query::{Column, QueryData, QueryStatus};
573
574        Ok(crate::core::models::QueryResult {
575            data: QueryData {
576                rows: vec![vec![serde_json::json!(1), serde_json::json!("test")]],
577                cols: vec![
578                    Column {
579                        name: "id".to_string(),
580                        display_name: "ID".to_string(),
581                        base_type: "type/Integer".to_string(),
582                        effective_type: None,
583                        semantic_type: None,
584                        field_ref: None,
585                    },
586                    Column {
587                        name: "name".to_string(),
588                        display_name: "Name".to_string(),
589                        base_type: "type/Text".to_string(),
590                        effective_type: None,
591                        semantic_type: None,
592                        field_ref: None,
593                    },
594                ],
595                native_form: None,
596                insights: vec![],
597            },
598            database_id: MetabaseId(1),
599            started_at: chrono::Utc::now(),
600            finished_at: Some(chrono::Utc::now()),
601            status: QueryStatus::Completed,
602            row_count: Some(1),
603            running_time: Some(100),
604            json_query: serde_json::json!({}),
605        })
606    }
607
608    async fn export_query(
609        &self,
610        id: &CardId,
611        _format: crate::core::models::common::ExportFormat,
612        _parameters: Option<serde_json::Value>,
613    ) -> RepositoryResult<Vec<u8>> {
614        if self.should_fail {
615            return Err(RepositoryError::Other("Mock failure".to_string()));
616        }
617
618        // Check if card exists
619        let cards = self.cards.read().await;
620        if !cards.iter().any(|c| c.id == Some(*id)) {
621            return Err(RepositoryError::NotFound(format!(
622                "Card {} not found",
623                id.0
624            )));
625        }
626
627        // Return mock CSV export data
628        Ok(b"id,name\n1,Test\n2,Data".to_vec())
629    }
630
631    async fn execute_pivot_query(
632        &self,
633        id: &CardId,
634        _parameters: Option<serde_json::Value>,
635    ) -> RepositoryResult<crate::core::models::QueryResult> {
636        if self.should_fail {
637            return Err(RepositoryError::Other("Mock failure".to_string()));
638        }
639
640        // Check if card exists
641        let cards = self.cards.read().await;
642        if !cards.iter().any(|c| c.id == Some(*id)) {
643            return Err(RepositoryError::NotFound(format!(
644                "Card {} not found",
645                id.0
646            )));
647        }
648
649        // Return mock pivot query result
650        use crate::core::models::common::MetabaseId;
651        use crate::core::models::query::{Column, QueryData, QueryStatus};
652
653        Ok(crate::core::models::QueryResult {
654            data: QueryData {
655                rows: vec![vec![serde_json::json!("category1"), serde_json::json!(100)]],
656                cols: vec![
657                    Column {
658                        name: "category".to_string(),
659                        display_name: "Category".to_string(),
660                        base_type: "type/Text".to_string(),
661                        effective_type: None,
662                        semantic_type: None,
663                        field_ref: None,
664                    },
665                    Column {
666                        name: "total".to_string(),
667                        display_name: "Total".to_string(),
668                        base_type: "type/Integer".to_string(),
669                        effective_type: None,
670                        semantic_type: None,
671                        field_ref: None,
672                    },
673                ],
674                native_form: None,
675                insights: vec![],
676            },
677            database_id: MetabaseId(1),
678            started_at: chrono::Utc::now(),
679            finished_at: Some(chrono::Utc::now()),
680            status: QueryStatus::Completed,
681            row_count: Some(1),
682            running_time: Some(150),
683            json_query: serde_json::json!({}),
684        })
685    }
686}