metabase_api_rs/service/
dashboard.rs

1//! Dashboard service implementation
2//!
3//! This module provides business logic for Dashboard operations.
4
5use super::traits::{Service, ServiceError, ServiceResult, ValidationContext};
6use crate::core::models::{common::DashboardId, Dashboard};
7use crate::repository::{
8    dashboard::{DashboardFilterParams, DashboardRepository},
9    traits::PaginationParams,
10};
11use async_trait::async_trait;
12use std::sync::Arc;
13
14/// Service trait for Dashboard operations
15#[async_trait]
16pub trait DashboardService: Service {
17    /// Get a dashboard by ID
18    async fn get_dashboard(&self, id: DashboardId) -> ServiceResult<Dashboard>;
19
20    /// List dashboards with filters
21    async fn list_dashboards(
22        &self,
23        pagination: Option<PaginationParams>,
24        filters: Option<DashboardFilterParams>,
25    ) -> ServiceResult<Vec<Dashboard>>;
26
27    /// Create a new dashboard
28    async fn create_dashboard(&self, dashboard: Dashboard) -> ServiceResult<Dashboard>;
29
30    /// Update a dashboard
31    async fn update_dashboard(
32        &self,
33        id: DashboardId,
34        dashboard: Dashboard,
35    ) -> ServiceResult<Dashboard>;
36
37    /// Delete a dashboard
38    async fn delete_dashboard(&self, id: DashboardId) -> ServiceResult<()>;
39
40    /// Archive a dashboard
41    async fn archive_dashboard(&self, id: DashboardId) -> ServiceResult<()>;
42
43    /// Unarchive a dashboard
44    async fn unarchive_dashboard(&self, id: DashboardId) -> ServiceResult<()>;
45
46    /// Duplicate a dashboard
47    async fn duplicate_dashboard(
48        &self,
49        id: DashboardId,
50        new_name: &str,
51    ) -> ServiceResult<Dashboard>;
52
53    /// Add a card to a dashboard
54    async fn add_card_to_dashboard(
55        &self,
56        dashboard_id: DashboardId,
57        card_data: &serde_json::Value,
58    ) -> ServiceResult<serde_json::Value>;
59
60    /// Remove a card from a dashboard
61    async fn remove_card_from_dashboard(
62        &self,
63        dashboard_id: DashboardId,
64        card_id: i32,
65    ) -> ServiceResult<()>;
66
67    /// Validate dashboard data
68    async fn validate_dashboard(&self, dashboard: &Dashboard) -> ServiceResult<()>;
69}
70
71/// HTTP implementation of DashboardService
72pub struct HttpDashboardService {
73    repository: Arc<dyn DashboardRepository>,
74}
75
76impl HttpDashboardService {
77    /// Create a new HTTP dashboard service
78    pub fn new(repository: Arc<dyn DashboardRepository>) -> Self {
79        Self { repository }
80    }
81
82    /// Validate dashboard business rules
83    fn validate_dashboard_rules(&self, dashboard: &Dashboard) -> ServiceResult<()> {
84        let mut context = ValidationContext::new();
85
86        // Name validation
87        if dashboard.name.trim().is_empty() {
88            context.add_error("Dashboard name cannot be empty");
89        }
90
91        if dashboard.name.len() > 255 {
92            context.add_error("Dashboard name cannot exceed 255 characters");
93        }
94
95        // Description validation
96        if let Some(desc) = &dashboard.description {
97            if desc.len() > 5000 {
98                context.add_error("Dashboard description cannot exceed 5000 characters");
99            }
100        }
101
102        // Cache TTL validation
103        if let Some(ttl) = dashboard.cache_ttl {
104            if ttl < 0 {
105                context.add_error("Cache TTL cannot be negative");
106            }
107            if ttl > 86400 {
108                // 24 hours
109                context.add_error("Cache TTL cannot exceed 24 hours");
110            }
111        }
112
113        context.to_result()
114    }
115}
116
117#[async_trait]
118impl Service for HttpDashboardService {
119    fn name(&self) -> &str {
120        "DashboardService"
121    }
122}
123
124#[async_trait]
125impl DashboardService for HttpDashboardService {
126    async fn get_dashboard(&self, id: DashboardId) -> ServiceResult<Dashboard> {
127        self.repository.get(&id).await.map_err(ServiceError::from)
128    }
129
130    async fn list_dashboards(
131        &self,
132        pagination: Option<PaginationParams>,
133        filters: Option<DashboardFilterParams>,
134    ) -> ServiceResult<Vec<Dashboard>> {
135        self.repository
136            .list_with_filters(pagination, filters)
137            .await
138            .map_err(ServiceError::from)
139    }
140
141    async fn create_dashboard(&self, dashboard: Dashboard) -> ServiceResult<Dashboard> {
142        // Validate business rules
143        self.validate_dashboard_rules(&dashboard)?;
144
145        // Create via repository
146        self.repository
147            .create(&dashboard)
148            .await
149            .map_err(ServiceError::from)
150    }
151
152    async fn update_dashboard(
153        &self,
154        id: DashboardId,
155        mut dashboard: Dashboard,
156    ) -> ServiceResult<Dashboard> {
157        // Ensure ID matches
158        dashboard.id = Some(id);
159
160        // Validate business rules
161        self.validate_dashboard_rules(&dashboard)?;
162
163        // Check if dashboard exists
164        self.repository.get(&id).await.map_err(ServiceError::from)?;
165
166        // Update via repository
167        self.repository
168            .update(&id, &dashboard)
169            .await
170            .map_err(ServiceError::from)
171    }
172
173    async fn delete_dashboard(&self, id: DashboardId) -> ServiceResult<()> {
174        // Check if dashboard exists
175        self.repository.get(&id).await.map_err(ServiceError::from)?;
176
177        // Delete via repository
178        self.repository
179            .delete(&id)
180            .await
181            .map_err(ServiceError::from)
182    }
183
184    async fn archive_dashboard(&self, id: DashboardId) -> ServiceResult<()> {
185        self.repository
186            .archive(&id)
187            .await
188            .map_err(ServiceError::from)
189    }
190
191    async fn unarchive_dashboard(&self, id: DashboardId) -> ServiceResult<()> {
192        self.repository
193            .unarchive(&id)
194            .await
195            .map_err(ServiceError::from)
196    }
197
198    async fn duplicate_dashboard(
199        &self,
200        id: DashboardId,
201        new_name: &str,
202    ) -> ServiceResult<Dashboard> {
203        // Validate new name
204        if new_name.trim().is_empty() {
205            return Err(ServiceError::Validation(
206                "New dashboard name cannot be empty".to_string(),
207            ));
208        }
209
210        self.repository
211            .duplicate(&id, new_name)
212            .await
213            .map_err(ServiceError::from)
214    }
215
216    async fn add_card_to_dashboard(
217        &self,
218        dashboard_id: DashboardId,
219        card_data: &serde_json::Value,
220    ) -> ServiceResult<serde_json::Value> {
221        // Validate card data
222        if card_data.is_null() {
223            return Err(ServiceError::Validation(
224                "Card data cannot be null".to_string(),
225            ));
226        }
227
228        self.repository
229            .add_card(&dashboard_id, card_data)
230            .await
231            .map_err(ServiceError::from)
232    }
233
234    async fn remove_card_from_dashboard(
235        &self,
236        dashboard_id: DashboardId,
237        card_id: i32,
238    ) -> ServiceResult<()> {
239        self.repository
240            .remove_card(&dashboard_id, card_id)
241            .await
242            .map_err(ServiceError::from)
243    }
244
245    async fn validate_dashboard(&self, dashboard: &Dashboard) -> ServiceResult<()> {
246        self.validate_dashboard_rules(dashboard)
247    }
248}