metabase_api_rs/repository/
dashboard.rs1use 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#[derive(Debug, Clone, Default)]
16pub struct DashboardFilterParams {
17 pub base: FilterParams,
19 pub collection_id: Option<i32>,
21 pub creator_id: Option<i32>,
23 pub is_favorite: Option<bool>,
25}
26
27impl DashboardFilterParams {
28 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn with_collection(mut self, collection_id: i32) -> Self {
35 self.collection_id = Some(collection_id);
36 self
37 }
38
39 pub fn with_creator(mut self, creator_id: i32) -> Self {
41 self.creator_id = Some(creator_id);
42 self
43 }
44
45 pub fn with_favorite(mut self, is_favorite: bool) -> Self {
47 self.is_favorite = Some(is_favorite);
48 self
49 }
50}
51
52#[async_trait]
54pub trait DashboardRepository:
55 Repository<Entity = Dashboard, Id = DashboardId> + Send + Sync
56{
57 async fn list_with_filters(
59 &self,
60 pagination: Option<PaginationParams>,
61 filters: Option<DashboardFilterParams>,
62 ) -> RepositoryResult<Vec<Dashboard>>;
63
64 async fn get_by_collection(&self, collection_id: i32) -> RepositoryResult<Vec<Dashboard>>;
66
67 async fn get_cards(&self, id: &DashboardId) -> RepositoryResult<Vec<serde_json::Value>>;
69
70 async fn add_card(
72 &self,
73 id: &DashboardId,
74 card_data: &serde_json::Value,
75 ) -> RepositoryResult<serde_json::Value>;
76
77 async fn remove_card(&self, id: &DashboardId, card_id: i32) -> RepositoryResult<()>;
79
80 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 async fn duplicate(&self, id: &DashboardId, new_name: &str) -> RepositoryResult<Dashboard>;
90
91 async fn archive(&self, id: &DashboardId) -> RepositoryResult<()>;
93
94 async fn unarchive(&self, id: &DashboardId) -> RepositoryResult<()>;
96
97 async fn favorite(&self, id: &DashboardId) -> RepositoryResult<()>;
99
100 async fn unfavorite(&self, id: &DashboardId) -> RepositoryResult<()>;
102}
103
104pub struct HttpDashboardRepository {
106 http_provider: Arc<dyn HttpProviderSafe>,
107}
108
109impl HttpDashboardRepository {
110 pub fn new(http_provider: Arc<dyn HttpProviderSafe>) -> Self {
112 Self { http_provider }
113 }
114
115 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 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 let _items: serde_json::Value = self
209 .http_provider
210 .get(&path)
211 .await
212 .map_err(RepositoryError::from)?;
213
214 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
298pub 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 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 pub fn set_should_fail(&mut self, should_fail: bool) {
320 self.should_fail = should_fail;
321 }
322
323 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 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); 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 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 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 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 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 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 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 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 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 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 self.get(id).await?;
634
635 let mut favorites = self.favorites.write().await;
636 favorites.remove(id);
637 Ok(())
638 }
639}