metabase_api_rs/service/
dashboard.rs1use 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#[async_trait]
16pub trait DashboardService: Service {
17 async fn get_dashboard(&self, id: DashboardId) -> ServiceResult<Dashboard>;
19
20 async fn list_dashboards(
22 &self,
23 pagination: Option<PaginationParams>,
24 filters: Option<DashboardFilterParams>,
25 ) -> ServiceResult<Vec<Dashboard>>;
26
27 async fn create_dashboard(&self, dashboard: Dashboard) -> ServiceResult<Dashboard>;
29
30 async fn update_dashboard(
32 &self,
33 id: DashboardId,
34 dashboard: Dashboard,
35 ) -> ServiceResult<Dashboard>;
36
37 async fn delete_dashboard(&self, id: DashboardId) -> ServiceResult<()>;
39
40 async fn archive_dashboard(&self, id: DashboardId) -> ServiceResult<()>;
42
43 async fn unarchive_dashboard(&self, id: DashboardId) -> ServiceResult<()>;
45
46 async fn duplicate_dashboard(
48 &self,
49 id: DashboardId,
50 new_name: &str,
51 ) -> ServiceResult<Dashboard>;
52
53 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 async fn remove_card_from_dashboard(
62 &self,
63 dashboard_id: DashboardId,
64 card_id: i32,
65 ) -> ServiceResult<()>;
66
67 async fn validate_dashboard(&self, dashboard: &Dashboard) -> ServiceResult<()>;
69}
70
71pub struct HttpDashboardService {
73 repository: Arc<dyn DashboardRepository>,
74}
75
76impl HttpDashboardService {
77 pub fn new(repository: Arc<dyn DashboardRepository>) -> Self {
79 Self { repository }
80 }
81
82 fn validate_dashboard_rules(&self, dashboard: &Dashboard) -> ServiceResult<()> {
84 let mut context = ValidationContext::new();
85
86 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 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 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 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 self.validate_dashboard_rules(&dashboard)?;
144
145 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 dashboard.id = Some(id);
159
160 self.validate_dashboard_rules(&dashboard)?;
162
163 self.repository.get(&id).await.map_err(ServiceError::from)?;
165
166 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 self.repository.get(&id).await.map_err(ServiceError::from)?;
176
177 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 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 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}