metabase_api_rs/service/
collection.rs1use super::traits::{Service, ServiceError, ServiceResult, ValidationContext};
6use crate::core::models::{common::CollectionId, Collection};
7use crate::repository::{
8 collection::{CollectionFilterParams, CollectionRepository},
9 traits::PaginationParams,
10};
11use async_trait::async_trait;
12use std::sync::Arc;
13
14#[async_trait]
16pub trait CollectionService: Service {
17 async fn get_collection(&self, id: CollectionId) -> ServiceResult<Collection>;
19
20 async fn list_collections(
22 &self,
23 pagination: Option<PaginationParams>,
24 filters: Option<CollectionFilterParams>,
25 ) -> ServiceResult<Vec<Collection>>;
26
27 async fn create_collection(&self, collection: Collection) -> ServiceResult<Collection>;
29
30 async fn update_collection(
32 &self,
33 id: CollectionId,
34 collection: Collection,
35 ) -> ServiceResult<Collection>;
36
37 async fn delete_collection(&self, id: CollectionId) -> ServiceResult<()>;
39
40 async fn archive_collection(&self, id: CollectionId) -> ServiceResult<()>;
42
43 async fn unarchive_collection(&self, id: CollectionId) -> ServiceResult<()>;
45
46 async fn move_collection(
48 &self,
49 id: CollectionId,
50 new_parent_id: Option<CollectionId>,
51 ) -> ServiceResult<Collection>;
52
53 async fn get_root_collections(&self) -> ServiceResult<Vec<Collection>>;
55
56 async fn get_collections_by_parent(
58 &self,
59 parent_id: CollectionId,
60 ) -> ServiceResult<Vec<Collection>>;
61
62 async fn validate_collection(&self, collection: &Collection) -> ServiceResult<()>;
64}
65
66pub struct HttpCollectionService {
68 repository: Arc<dyn CollectionRepository>,
69}
70
71impl HttpCollectionService {
72 pub fn new(repository: Arc<dyn CollectionRepository>) -> Self {
74 Self { repository }
75 }
76
77 fn validate_collection_rules(&self, collection: &Collection) -> ServiceResult<()> {
79 let mut context = ValidationContext::new();
80
81 if collection.name.trim().is_empty() {
83 context.add_error("Collection name cannot be empty");
84 }
85
86 if collection.name.len() > 255 {
87 context.add_error("Collection name cannot exceed 255 characters");
88 }
89
90 if let Some(desc) = &collection.description {
92 if desc.len() > 5000 {
93 context.add_error("Collection description cannot exceed 5000 characters");
94 }
95 }
96
97 if let Some(color) = &collection.color {
99 if !color.starts_with('#') || color.len() != 7 {
101 context.add_error("Collection color must be in hex format (#RRGGBB)");
102 }
103 }
104
105 if let Some(slug) = &collection.slug {
107 if slug.contains(' ') {
108 context.add_error("Collection slug cannot contain spaces");
109 }
110 if slug.len() > 100 {
111 context.add_error("Collection slug cannot exceed 100 characters");
112 }
113 }
114
115 context.to_result()
116 }
117
118 async fn check_circular_reference(
120 &self,
121 id: CollectionId,
122 parent_id: Option<CollectionId>,
123 ) -> ServiceResult<()> {
124 if let Some(parent) = parent_id {
125 if parent == id {
126 return Err(ServiceError::BusinessRule(
127 "Cannot set collection as its own parent".to_string(),
128 ));
129 }
130
131 }
133 Ok(())
134 }
135}
136
137#[async_trait]
138impl Service for HttpCollectionService {
139 fn name(&self) -> &str {
140 "CollectionService"
141 }
142}
143
144#[async_trait]
145impl CollectionService for HttpCollectionService {
146 async fn get_collection(&self, id: CollectionId) -> ServiceResult<Collection> {
147 self.repository.get(&id).await.map_err(ServiceError::from)
148 }
149
150 async fn list_collections(
151 &self,
152 pagination: Option<PaginationParams>,
153 filters: Option<CollectionFilterParams>,
154 ) -> ServiceResult<Vec<Collection>> {
155 self.repository
156 .list_with_filters(pagination, filters)
157 .await
158 .map_err(ServiceError::from)
159 }
160
161 async fn create_collection(&self, collection: Collection) -> ServiceResult<Collection> {
162 self.validate_collection_rules(&collection)?;
164
165 if let Some(parent_id) = collection.parent_id {
167 self.repository
168 .get(&CollectionId(parent_id))
169 .await
170 .map_err(|_| {
171 ServiceError::NotFound(format!("Parent collection {} not found", parent_id))
172 })?;
173 }
174
175 self.repository
177 .create(&collection)
178 .await
179 .map_err(ServiceError::from)
180 }
181
182 async fn update_collection(
183 &self,
184 id: CollectionId,
185 mut collection: Collection,
186 ) -> ServiceResult<Collection> {
187 collection.id = Some(id);
189
190 self.validate_collection_rules(&collection)?;
192
193 self.repository.get(&id).await.map_err(ServiceError::from)?;
195
196 if let Some(parent_id) = collection.parent_id {
198 self.check_circular_reference(id, Some(CollectionId(parent_id)))
199 .await?;
200 }
201
202 self.repository
204 .update(&id, &collection)
205 .await
206 .map_err(ServiceError::from)
207 }
208
209 async fn delete_collection(&self, id: CollectionId) -> ServiceResult<()> {
210 let collection = self.repository.get(&id).await.map_err(ServiceError::from)?;
212
213 if collection.is_personal() {
215 return Err(ServiceError::BusinessRule(
216 "Cannot delete personal collections".to_string(),
217 ));
218 }
219
220 self.repository
224 .delete(&id)
225 .await
226 .map_err(ServiceError::from)
227 }
228
229 async fn archive_collection(&self, id: CollectionId) -> ServiceResult<()> {
230 self.repository
231 .archive(&id)
232 .await
233 .map_err(ServiceError::from)
234 }
235
236 async fn unarchive_collection(&self, id: CollectionId) -> ServiceResult<()> {
237 self.repository
238 .unarchive(&id)
239 .await
240 .map_err(ServiceError::from)
241 }
242
243 async fn move_collection(
244 &self,
245 id: CollectionId,
246 new_parent_id: Option<CollectionId>,
247 ) -> ServiceResult<Collection> {
248 self.check_circular_reference(id, new_parent_id).await?;
250
251 if let Some(parent) = new_parent_id {
253 self.repository.get(&parent).await.map_err(|_| {
254 ServiceError::NotFound(format!("Parent collection {} not found", parent.0))
255 })?;
256 }
257
258 self.repository
259 .move_collection(&id, new_parent_id)
260 .await
261 .map_err(ServiceError::from)
262 }
263
264 async fn get_root_collections(&self) -> ServiceResult<Vec<Collection>> {
265 self.repository
266 .get_root_collections()
267 .await
268 .map_err(ServiceError::from)
269 }
270
271 async fn get_collections_by_parent(
272 &self,
273 parent_id: CollectionId,
274 ) -> ServiceResult<Vec<Collection>> {
275 self.repository
276 .get_by_parent(Some(parent_id))
277 .await
278 .map_err(ServiceError::from)
279 }
280
281 async fn validate_collection(&self, collection: &Collection) -> ServiceResult<()> {
282 self.validate_collection_rules(collection)
283 }
284}