metabase_api_rs/repository/
dashboard.rs

1//! Dashboard repository trait and implementations
2//!
3//! This module provides the repository abstraction for Dashboard entities.
4
5use super::traits::{
6    FilterParams, PaginationParams, Repository, RepositoryError, RepositoryResult,
7};
8use crate::core::models::common::DashboardId;
9use crate::core::models::Dashboard;
10use crate::transport::http_provider_safe::{HttpProviderExt, HttpProviderSafe};
11use async_trait::async_trait;
12use std::sync::Arc;
13
14/// Dashboard-specific filter parameters
15#[derive(Debug, Clone, Default)]
16pub struct DashboardFilterParams {
17    /// Base filters
18    pub base: FilterParams,
19    /// Filter by collection ID
20    pub collection_id: Option<i32>,
21    /// Filter by creator ID
22    pub creator_id: Option<i32>,
23    /// Filter by favorite status
24    pub is_favorite: Option<bool>,
25}
26
27impl DashboardFilterParams {
28    /// Create new dashboard 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 creator ID filter
40    pub fn with_creator(mut self, creator_id: i32) -> Self {
41        self.creator_id = Some(creator_id);
42        self
43    }
44
45    /// Set favorite filter
46    pub fn with_favorite(mut self, is_favorite: bool) -> Self {
47        self.is_favorite = Some(is_favorite);
48        self
49    }
50}
51
52/// Repository trait for Dashboard entities
53#[async_trait]
54pub trait DashboardRepository:
55    Repository<Entity = Dashboard, Id = DashboardId> + Send + Sync
56{
57    /// List dashboards with dashboard-specific filters
58    async fn list_with_filters(
59        &self,
60        pagination: Option<PaginationParams>,
61        filters: Option<DashboardFilterParams>,
62    ) -> RepositoryResult<Vec<Dashboard>>;
63
64    /// Get dashboards in a specific collection
65    async fn get_by_collection(&self, collection_id: i32) -> RepositoryResult<Vec<Dashboard>>;
66
67    /// Get dashboard cards (visualizations on the dashboard)
68    async fn get_cards(&self, id: &DashboardId) -> RepositoryResult<Vec<serde_json::Value>>;
69
70    /// Add a card to a dashboard
71    async fn add_card(
72        &self,
73        id: &DashboardId,
74        card_data: &serde_json::Value,
75    ) -> RepositoryResult<serde_json::Value>;
76
77    /// Remove a card from a dashboard
78    async fn remove_card(&self, id: &DashboardId, card_id: i32) -> RepositoryResult<()>;
79
80    /// Update card position/size on dashboard
81    async fn update_card(
82        &self,
83        id: &DashboardId,
84        card_id: i32,
85        updates: &serde_json::Value,
86    ) -> RepositoryResult<serde_json::Value>;
87
88    /// Duplicate a dashboard
89    async fn duplicate(&self, id: &DashboardId, new_name: &str) -> RepositoryResult<Dashboard>;
90
91    /// Archive a dashboard
92    async fn archive(&self, id: &DashboardId) -> RepositoryResult<()>;
93
94    /// Unarchive a dashboard
95    async fn unarchive(&self, id: &DashboardId) -> RepositoryResult<()>;
96
97    /// Favorite a dashboard
98    async fn favorite(&self, id: &DashboardId) -> RepositoryResult<()>;
99
100    /// Unfavorite a dashboard
101    async fn unfavorite(&self, id: &DashboardId) -> RepositoryResult<()>;
102}
103
104/// HTTP implementation of DashboardRepository
105pub struct HttpDashboardRepository {
106    http_provider: Arc<dyn HttpProviderSafe>,
107}
108
109impl HttpDashboardRepository {
110    /// Create a new HTTP dashboard repository
111    pub fn new(http_provider: Arc<dyn HttpProviderSafe>) -> Self {
112        Self { http_provider }
113    }
114
115    /// Convert filter params to query string
116    fn build_query_params(
117        &self,
118        pagination: Option<PaginationParams>,
119        filters: Option<FilterParams>,
120    ) -> String {
121        let mut params = Vec::new();
122
123        if let Some(p) = pagination {
124            if let Some(page) = p.page {
125                params.push(format!("page={}", page));
126            }
127            if let Some(limit) = p.limit {
128                params.push(format!("limit={}", limit));
129            }
130            if let Some(offset) = p.offset {
131                params.push(format!("offset={}", offset));
132            }
133        }
134
135        if let Some(f) = filters {
136            if let Some(query) = f.query {
137                params.push(format!("q={}", query.replace(' ', "+")));
138            }
139            if let Some(archived) = f.archived {
140                params.push(format!("archived={}", archived));
141            }
142        }
143
144        if params.is_empty() {
145            String::new()
146        } else {
147            format!("?{}", params.join("&"))
148        }
149    }
150}
151
152#[async_trait]
153impl Repository for HttpDashboardRepository {
154    type Entity = Dashboard;
155    type Id = DashboardId;
156
157    async fn get(&self, id: &DashboardId) -> RepositoryResult<Dashboard> {
158        let path = format!("/api/dashboard/{}", id.0);
159        self.http_provider.get(&path).await.map_err(|e| e.into())
160    }
161
162    async fn list(
163        &self,
164        pagination: Option<PaginationParams>,
165        filters: Option<FilterParams>,
166    ) -> RepositoryResult<Vec<Dashboard>> {
167        let query = self.build_query_params(pagination, filters);
168        let path = format!("/api/dashboard{}", query);
169        self.http_provider.get(&path).await.map_err(|e| e.into())
170    }
171
172    async fn create(&self, entity: &Dashboard) -> RepositoryResult<Dashboard> {
173        self.http_provider
174            .post("/api/dashboard", entity)
175            .await
176            .map_err(|e| e.into())
177    }
178
179    async fn update(&self, id: &DashboardId, entity: &Dashboard) -> RepositoryResult<Dashboard> {
180        let path = format!("/api/dashboard/{}", id.0);
181        self.http_provider
182            .put(&path, entity)
183            .await
184            .map_err(|e| e.into())
185    }
186
187    async fn delete(&self, id: &DashboardId) -> RepositoryResult<()> {
188        let path = format!("/api/dashboard/{}", id.0);
189        self.http_provider.delete(&path).await.map_err(|e| e.into())
190    }
191}
192
193#[async_trait]
194impl DashboardRepository for HttpDashboardRepository {
195    async fn list_with_filters(
196        &self,
197        pagination: Option<PaginationParams>,
198        filters: Option<DashboardFilterParams>,
199    ) -> RepositoryResult<Vec<Dashboard>> {
200        // Convert DashboardFilterParams to FilterParams
201        let base_filters = filters.map(|f| f.base);
202        self.list(pagination, base_filters).await
203    }
204
205    async fn get_by_collection(&self, collection_id: i32) -> RepositoryResult<Vec<Dashboard>> {
206        let path = format!("/api/collection/{}/items", collection_id);
207        // This returns collection items, we need to filter for dashboards
208        let _items: serde_json::Value = self
209            .http_provider
210            .get(&path)
211            .await
212            .map_err(RepositoryError::from)?;
213
214        // Extract dashboards from the response
215        // This is a simplified version, actual implementation would parse properly
216        Ok(Vec::new())
217    }
218
219    async fn get_cards(&self, id: &DashboardId) -> RepositoryResult<Vec<serde_json::Value>> {
220        let path = format!("/api/dashboard/{}/cards", id.0);
221        self.http_provider.get(&path).await.map_err(|e| e.into())
222    }
223
224    async fn add_card(
225        &self,
226        id: &DashboardId,
227        card_data: &serde_json::Value,
228    ) -> RepositoryResult<serde_json::Value> {
229        let path = format!("/api/dashboard/{}/cards", id.0);
230        self.http_provider
231            .post(&path, card_data)
232            .await
233            .map_err(|e| e.into())
234    }
235
236    async fn remove_card(&self, id: &DashboardId, card_id: i32) -> RepositoryResult<()> {
237        let path = format!("/api/dashboard/{}/cards/{}", id.0, card_id);
238        self.http_provider.delete(&path).await.map_err(|e| e.into())
239    }
240
241    async fn update_card(
242        &self,
243        id: &DashboardId,
244        card_id: i32,
245        updates: &serde_json::Value,
246    ) -> RepositoryResult<serde_json::Value> {
247        let path = format!("/api/dashboard/{}/cards/{}", id.0, card_id);
248        self.http_provider
249            .put(&path, updates)
250            .await
251            .map_err(|e| e.into())
252    }
253
254    async fn duplicate(&self, id: &DashboardId, new_name: &str) -> RepositoryResult<Dashboard> {
255        let path = format!("/api/dashboard/{}/copy", id.0);
256        let body = serde_json::json!({ "name": new_name });
257        self.http_provider
258            .post(&path, &body)
259            .await
260            .map_err(|e| e.into())
261    }
262
263    async fn archive(&self, id: &DashboardId) -> RepositoryResult<()> {
264        let path = format!("/api/dashboard/{}", id.0);
265        let body = serde_json::json!({ "archived": true });
266        self.http_provider
267            .put(&path, &body)
268            .await
269            .map(|_: serde_json::Value| ())
270            .map_err(|e| e.into())
271    }
272
273    async fn unarchive(&self, id: &DashboardId) -> RepositoryResult<()> {
274        let path = format!("/api/dashboard/{}", id.0);
275        let body = serde_json::json!({ "archived": false });
276        self.http_provider
277            .put(&path, &body)
278            .await
279            .map(|_: serde_json::Value| ())
280            .map_err(|e| e.into())
281    }
282
283    async fn favorite(&self, id: &DashboardId) -> RepositoryResult<()> {
284        let path = format!("/api/dashboard/{}/favorite", id.0);
285        self.http_provider
286            .post(&path, &serde_json::json!({}))
287            .await
288            .map(|_: serde_json::Value| ())
289            .map_err(|e| e.into())
290    }
291
292    async fn unfavorite(&self, id: &DashboardId) -> RepositoryResult<()> {
293        let path = format!("/api/dashboard/{}/favorite", id.0);
294        self.http_provider.delete(&path).await.map_err(|e| e.into())
295    }
296}
297
298/// Mock implementation of DashboardRepository for testing
299pub struct MockDashboardRepository {
300    dashboards: Arc<tokio::sync::RwLock<Vec<Dashboard>>>,
301    dashboard_cards:
302        Arc<tokio::sync::RwLock<std::collections::HashMap<DashboardId, Vec<serde_json::Value>>>>,
303    favorites: Arc<tokio::sync::RwLock<std::collections::HashSet<DashboardId>>>,
304    should_fail: bool,
305}
306
307impl MockDashboardRepository {
308    /// Create a new mock dashboard repository
309    pub fn new() -> Self {
310        Self {
311            dashboards: Arc::new(tokio::sync::RwLock::new(Vec::new())),
312            dashboard_cards: Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
313            favorites: Arc::new(tokio::sync::RwLock::new(std::collections::HashSet::new())),
314            should_fail: false,
315        }
316    }
317
318    /// Set whether operations should fail
319    pub fn set_should_fail(&mut self, should_fail: bool) {
320        self.should_fail = should_fail;
321    }
322
323    /// Add a dashboard to the mock repository
324    pub async fn add_dashboard(&self, dashboard: Dashboard) {
325        let mut dashboards = self.dashboards.write().await;
326        dashboards.push(dashboard);
327    }
328}
329
330impl Default for MockDashboardRepository {
331    fn default() -> Self {
332        Self::new()
333    }
334}
335
336#[async_trait]
337impl Repository for MockDashboardRepository {
338    type Entity = Dashboard;
339    type Id = DashboardId;
340
341    async fn get(&self, id: &DashboardId) -> RepositoryResult<Dashboard> {
342        if self.should_fail {
343            return Err(RepositoryError::Other("Mock failure".to_string()));
344        }
345
346        let dashboards = self.dashboards.read().await;
347        dashboards
348            .iter()
349            .find(|d| d.id == Some(*id))
350            .cloned()
351            .ok_or_else(|| RepositoryError::NotFound(format!("Dashboard {} not found", id.0)))
352    }
353
354    async fn list(
355        &self,
356        _pagination: Option<PaginationParams>,
357        _filters: Option<FilterParams>,
358    ) -> RepositoryResult<Vec<Dashboard>> {
359        if self.should_fail {
360            return Err(RepositoryError::Other("Mock failure".to_string()));
361        }
362
363        let dashboards = self.dashboards.read().await;
364        Ok(dashboards.clone())
365    }
366
367    async fn create(&self, entity: &Dashboard) -> RepositoryResult<Dashboard> {
368        if self.should_fail {
369            return Err(RepositoryError::Other("Mock failure".to_string()));
370        }
371
372        let mut dashboards = self.dashboards.write().await;
373        let mut new_dashboard = entity.clone();
374        // Generate a mock ID if not present
375        if new_dashboard.id.is_none() {
376            new_dashboard.id = Some(DashboardId((dashboards.len() + 1) as i32));
377        }
378        dashboards.push(new_dashboard.clone());
379        Ok(new_dashboard)
380    }
381
382    async fn update(&self, id: &DashboardId, entity: &Dashboard) -> RepositoryResult<Dashboard> {
383        if self.should_fail {
384            return Err(RepositoryError::Other("Mock failure".to_string()));
385        }
386
387        let mut dashboards = self.dashboards.write().await;
388        if let Some(dashboard) = dashboards.iter_mut().find(|d| d.id == Some(*id)) {
389            *dashboard = entity.clone();
390            dashboard.id = Some(*id); // Ensure ID is preserved
391            Ok(dashboard.clone())
392        } else {
393            Err(RepositoryError::NotFound(format!(
394                "Dashboard {} not found",
395                id.0
396            )))
397        }
398    }
399
400    async fn delete(&self, id: &DashboardId) -> RepositoryResult<()> {
401        if self.should_fail {
402            return Err(RepositoryError::Other("Mock failure".to_string()));
403        }
404
405        let mut dashboards = self.dashboards.write().await;
406        let initial_len = dashboards.len();
407        dashboards.retain(|d| d.id != Some(*id));
408
409        if dashboards.len() < initial_len {
410            // Also clean up related data
411            let mut cards = self.dashboard_cards.write().await;
412            cards.remove(id);
413            let mut favorites = self.favorites.write().await;
414            favorites.remove(id);
415            Ok(())
416        } else {
417            Err(RepositoryError::NotFound(format!(
418                "Dashboard {} not found",
419                id.0
420            )))
421        }
422    }
423}
424
425#[async_trait]
426impl DashboardRepository for MockDashboardRepository {
427    async fn list_with_filters(
428        &self,
429        pagination: Option<PaginationParams>,
430        filters: Option<DashboardFilterParams>,
431    ) -> RepositoryResult<Vec<Dashboard>> {
432        let base_filters = filters.map(|f| f.base);
433        self.list(pagination, base_filters).await
434    }
435
436    async fn get_by_collection(&self, collection_id: i32) -> RepositoryResult<Vec<Dashboard>> {
437        if self.should_fail {
438            return Err(RepositoryError::Other("Mock failure".to_string()));
439        }
440
441        let dashboards = self.dashboards.read().await;
442        Ok(dashboards
443            .iter()
444            .filter(|d| d.collection_id == Some(collection_id))
445            .cloned()
446            .collect())
447    }
448
449    async fn get_cards(&self, id: &DashboardId) -> RepositoryResult<Vec<serde_json::Value>> {
450        if self.should_fail {
451            return Err(RepositoryError::Other("Mock failure".to_string()));
452        }
453
454        // Verify dashboard exists
455        self.get(id).await?;
456
457        let cards = self.dashboard_cards.read().await;
458        Ok(cards.get(id).cloned().unwrap_or_default())
459    }
460
461    async fn add_card(
462        &self,
463        id: &DashboardId,
464        card_data: &serde_json::Value,
465    ) -> RepositoryResult<serde_json::Value> {
466        if self.should_fail {
467            return Err(RepositoryError::Other("Mock failure".to_string()));
468        }
469
470        // Verify dashboard exists
471        self.get(id).await?;
472
473        let mut cards = self.dashboard_cards.write().await;
474        let dashboard_cards = cards.entry(*id).or_insert_with(Vec::new);
475
476        // Add an ID to the card data
477        let mut new_card = card_data.clone();
478        if let serde_json::Value::Object(ref mut map) = new_card {
479            map.insert(
480                "id".to_string(),
481                serde_json::json!(dashboard_cards.len() + 1),
482            );
483        }
484
485        dashboard_cards.push(new_card.clone());
486        Ok(new_card)
487    }
488
489    async fn remove_card(&self, id: &DashboardId, card_id: i32) -> RepositoryResult<()> {
490        if self.should_fail {
491            return Err(RepositoryError::Other("Mock failure".to_string()));
492        }
493
494        // Verify dashboard exists
495        self.get(id).await?;
496
497        let mut cards = self.dashboard_cards.write().await;
498        if let Some(dashboard_cards) = cards.get_mut(id) {
499            dashboard_cards.retain(|card| {
500                card.get("id")
501                    .and_then(|v| v.as_i64())
502                    .map(|id| id != card_id as i64)
503                    .unwrap_or(true)
504            });
505            Ok(())
506        } else {
507            Ok(())
508        }
509    }
510
511    async fn update_card(
512        &self,
513        id: &DashboardId,
514        card_id: i32,
515        updates: &serde_json::Value,
516    ) -> RepositoryResult<serde_json::Value> {
517        if self.should_fail {
518            return Err(RepositoryError::Other("Mock failure".to_string()));
519        }
520
521        // Verify dashboard exists
522        self.get(id).await?;
523
524        let mut cards = self.dashboard_cards.write().await;
525        if let Some(dashboard_cards) = cards.get_mut(id) {
526            for card in dashboard_cards.iter_mut() {
527                if card
528                    .get("id")
529                    .and_then(|v| v.as_i64())
530                    .map(|id| id == card_id as i64)
531                    .unwrap_or(false)
532                {
533                    // Merge updates into the card
534                    if let serde_json::Value::Object(card_map) = card {
535                        if let serde_json::Value::Object(updates_map) = updates {
536                            for (key, value) in updates_map {
537                                card_map.insert(key.clone(), value.clone());
538                            }
539                        }
540                    }
541                    return Ok(card.clone());
542                }
543            }
544        }
545
546        Err(RepositoryError::NotFound(format!(
547            "Card {} not found on dashboard {}",
548            card_id, id.0
549        )))
550    }
551
552    async fn duplicate(&self, id: &DashboardId, new_name: &str) -> RepositoryResult<Dashboard> {
553        if self.should_fail {
554            return Err(RepositoryError::Other("Mock failure".to_string()));
555        }
556
557        let mut dashboards = self.dashboards.write().await;
558        if let Some(original) = dashboards.iter().find(|d| d.id == Some(*id)) {
559            let mut new_dashboard = original.clone();
560            new_dashboard.id = Some(DashboardId((dashboards.len() + 1) as i32));
561            new_dashboard.name = new_name.to_string();
562
563            // Clone cards as well
564            let cards = self.dashboard_cards.read().await;
565            if let Some(original_cards) = cards.get(id) {
566                let mut cards_mut = self.dashboard_cards.write().await;
567                cards_mut.insert(new_dashboard.id.unwrap(), original_cards.clone());
568            }
569
570            dashboards.push(new_dashboard.clone());
571            Ok(new_dashboard)
572        } else {
573            Err(RepositoryError::NotFound(format!(
574                "Dashboard {} not found",
575                id.0
576            )))
577        }
578    }
579
580    async fn archive(&self, id: &DashboardId) -> RepositoryResult<()> {
581        if self.should_fail {
582            return Err(RepositoryError::Other("Mock failure".to_string()));
583        }
584
585        let mut dashboards = self.dashboards.write().await;
586        if let Some(dashboard) = dashboards.iter_mut().find(|d| d.id == Some(*id)) {
587            dashboard.archived = Some(true);
588            Ok(())
589        } else {
590            Err(RepositoryError::NotFound(format!(
591                "Dashboard {} not found",
592                id.0
593            )))
594        }
595    }
596
597    async fn unarchive(&self, id: &DashboardId) -> RepositoryResult<()> {
598        if self.should_fail {
599            return Err(RepositoryError::Other("Mock failure".to_string()));
600        }
601
602        let mut dashboards = self.dashboards.write().await;
603        if let Some(dashboard) = dashboards.iter_mut().find(|d| d.id == Some(*id)) {
604            dashboard.archived = Some(false);
605            Ok(())
606        } else {
607            Err(RepositoryError::NotFound(format!(
608                "Dashboard {} not found",
609                id.0
610            )))
611        }
612    }
613
614    async fn favorite(&self, id: &DashboardId) -> RepositoryResult<()> {
615        if self.should_fail {
616            return Err(RepositoryError::Other("Mock failure".to_string()));
617        }
618
619        // Verify dashboard exists
620        self.get(id).await?;
621
622        let mut favorites = self.favorites.write().await;
623        favorites.insert(*id);
624        Ok(())
625    }
626
627    async fn unfavorite(&self, id: &DashboardId) -> RepositoryResult<()> {
628        if self.should_fail {
629            return Err(RepositoryError::Other("Mock failure".to_string()));
630        }
631
632        // Verify dashboard exists
633        self.get(id).await?;
634
635        let mut favorites = self.favorites.write().await;
636        favorites.remove(id);
637        Ok(())
638    }
639}