mecomp_storage/db/crud/
collection.rs

1//! CRUD operations for the collection table
2use std::time::Duration;
3
4use surrealdb::{Connection, Surreal};
5use tracing::instrument;
6
7use crate::{
8    db::{
9        queries::collection::{add_songs, read_songs, remove_songs},
10        schemas::{
11            collection::{Collection, CollectionChangeSet, CollectionId, TABLE_NAME},
12            playlist::Playlist,
13            song::{Song, SongId},
14        },
15    },
16    errors::{Error, StorageResult},
17};
18
19impl Collection {
20    #[instrument]
21    pub async fn create<C: Connection>(
22        db: &Surreal<C>,
23        collection: Self,
24    ) -> StorageResult<Option<Self>> {
25        Ok(db.create(collection.id.clone()).content(collection).await?)
26    }
27
28    #[instrument]
29    pub async fn read_all<C: Connection>(db: &Surreal<C>) -> StorageResult<Vec<Self>> {
30        Ok(db.select(TABLE_NAME).await?)
31    }
32
33    #[instrument]
34    pub async fn read<C: Connection>(
35        db: &Surreal<C>,
36        id: CollectionId,
37    ) -> StorageResult<Option<Self>> {
38        Ok(db.select(id).await?)
39    }
40
41    #[instrument]
42    pub async fn update<C: Connection>(
43        db: &Surreal<C>,
44        id: CollectionId,
45        changes: CollectionChangeSet,
46    ) -> StorageResult<Option<Self>> {
47        Ok(db.update(id).merge(changes).await?)
48    }
49
50    #[instrument]
51    pub async fn delete<C: Connection>(
52        db: &Surreal<C>,
53        id: CollectionId,
54    ) -> StorageResult<Option<Self>> {
55        // first remove all the songs from the collection
56        let songs = Self::read_songs(db, id.clone())
57            .await?
58            .into_iter()
59            .map(|song| song.id)
60            .collect::<Vec<_>>();
61        Self::remove_songs(db, id.clone(), songs).await?;
62
63        Ok(db.delete(id).await?)
64    }
65
66    #[instrument]
67    pub async fn add_songs<C: Connection>(
68        db: &Surreal<C>,
69        id: CollectionId,
70        song_ids: Vec<SongId>,
71    ) -> StorageResult<()> {
72        db.query(add_songs())
73            .bind(("id", id.clone()))
74            .bind(("songs", song_ids))
75            .await?;
76        Self::repair(db, id).await?;
77        Ok(())
78    }
79
80    #[instrument]
81    pub async fn read_songs<C: Connection>(
82        db: &Surreal<C>,
83        id: CollectionId,
84    ) -> StorageResult<Vec<Song>> {
85        Ok(db.query(read_songs()).bind(("id", id)).await?.take(0)?)
86    }
87
88    #[instrument]
89    /// removes songs from a collection
90    ///
91    /// # Returns
92    ///
93    /// * `bool` - True if the collection is empty
94    pub async fn remove_songs<C: Connection>(
95        db: &Surreal<C>,
96        id: CollectionId,
97        song_ids: Vec<SongId>,
98    ) -> StorageResult<bool> {
99        db.query(remove_songs())
100            .bind(("id", id.clone()))
101            .bind(("songs", song_ids))
102            .await?;
103        Self::repair(db, id).await
104    }
105
106    /// updates the song_count and runtime of the collection
107    ///
108    /// # Arguments
109    ///
110    /// * `id` - The id of the collection to repair
111    ///
112    /// # Returns
113    ///
114    /// * `bool` - True if the collection is empty
115    #[instrument]
116    pub async fn repair<C: Connection>(db: &Surreal<C>, id: CollectionId) -> StorageResult<bool> {
117        let songs = Self::read_songs(db, id.clone()).await?;
118
119        Self::update(
120            db,
121            id,
122            CollectionChangeSet {
123                song_count: Some(songs.len()),
124                runtime: Some(songs.iter().map(|song| song.runtime).sum::<Duration>()),
125                ..Default::default()
126            },
127        )
128        .await?;
129
130        Ok(songs.is_empty())
131    }
132
133    /// "Freeze" a collection, this will create a playlist with the given name that contains all the songs in the given collection
134    #[instrument]
135    pub async fn freeze<C: Connection>(
136        db: &Surreal<C>,
137        id: CollectionId,
138        name: String,
139    ) -> StorageResult<Playlist> {
140        // create the new playlist
141        let playlist = Playlist::create(
142            db,
143            Playlist {
144                id: Playlist::generate_id(),
145                name,
146                runtime: Duration::default(),
147                song_count: 0,
148            },
149        )
150        .await?
151        .ok_or(Error::NotFound)?;
152
153        // get the songs in the collection
154        let songs = Self::read_songs(db, id.clone()).await?;
155        let song_ids = songs.iter().map(|song| song.id.clone()).collect::<Vec<_>>();
156
157        // add the songs to the playlist
158        Playlist::add_songs(db, playlist.id.clone(), song_ids).await?;
159
160        // get the playlist
161        let playlist = Playlist::read(db, playlist.id.clone())
162            .await?
163            .ok_or(Error::NotFound)?;
164
165        Ok(playlist)
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use std::time::Duration;
172
173    use super::*;
174    use crate::{
175        db::schemas::song::SongChangeSet,
176        test_utils::{arb_song_case, create_song_with_overrides, init_test_database},
177    };
178
179    use anyhow::{Result, anyhow};
180    use pretty_assertions::assert_eq;
181
182    fn create_collection() -> Collection {
183        Collection {
184            id: Collection::generate_id(),
185            name: "Test Collection".into(),
186            runtime: Duration::from_secs(0),
187            song_count: 0,
188        }
189    }
190
191    #[tokio::test]
192    async fn test_create() -> Result<()> {
193        let db = init_test_database().await?;
194        let collection = create_collection();
195        let result = Collection::create(&db, collection.clone()).await?;
196        assert_eq!(result, Some(collection));
197        Ok(())
198    }
199
200    #[tokio::test]
201    async fn test_read_all() -> Result<()> {
202        let db = init_test_database().await?;
203        let collection = create_collection();
204        Collection::create(&db, collection.clone()).await?;
205        let result = Collection::read_all(&db).await?;
206        assert!(!result.is_empty());
207        Ok(())
208    }
209
210    #[tokio::test]
211    async fn test_read() -> Result<()> {
212        let db = init_test_database().await?;
213        let collection = create_collection();
214        Collection::create(&db, collection.clone()).await?;
215        let result = Collection::read(&db, collection.id.clone()).await?;
216        assert_eq!(result, Some(collection));
217        Ok(())
218    }
219
220    #[tokio::test]
221    async fn test_update() -> Result<()> {
222        let db = init_test_database().await?;
223        let collection = create_collection();
224        Collection::create(&db, collection.clone()).await?;
225        let changes = CollectionChangeSet {
226            name: Some("Updated Name".into()),
227            ..Default::default()
228        };
229
230        let updated = Collection::update(&db, collection.id.clone(), changes).await?;
231        let read = Collection::read(&db, collection.id.clone())
232            .await?
233            .ok_or_else(|| anyhow!("Collection not found"))?;
234
235        assert_eq!(read.name, "Updated Name");
236        assert_eq!(Some(read), updated);
237        Ok(())
238    }
239
240    #[tokio::test]
241    async fn test_delete() -> Result<()> {
242        let db = init_test_database().await?;
243        let collection = create_collection();
244        Collection::create(&db, collection.clone()).await?;
245        let result = Collection::delete(&db, collection.id.clone()).await?;
246        assert_eq!(result, Some(collection.clone()));
247        let result = Collection::read(&db, collection.id).await?;
248        assert_eq!(result, None);
249        Ok(())
250    }
251
252    #[tokio::test]
253    async fn test_add_songs() -> Result<()> {
254        let db = init_test_database().await?;
255        let collection = create_collection();
256        Collection::create(&db, collection.clone()).await?;
257        let song =
258            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default()).await?;
259
260        Collection::add_songs(&db, collection.id.clone(), vec![song.id.clone()]).await?;
261
262        let result = Collection::read_songs(&db, collection.id.clone()).await?;
263        assert_eq!(result, vec![song.clone()]);
264
265        let read = Collection::read(&db, collection.id.clone())
266            .await?
267            .ok_or_else(|| anyhow!("Collection not found"))?;
268        assert_eq!(read.song_count, 1);
269        assert_eq!(read.runtime, song.runtime);
270
271        Ok(())
272    }
273
274    #[tokio::test]
275    async fn test_remove_songs() -> Result<()> {
276        let db = init_test_database().await?;
277        let collection = create_collection();
278        Collection::create(&db, collection.clone()).await?;
279        let song =
280            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default()).await?;
281
282        Collection::add_songs(&db, collection.id.clone(), vec![song.id.clone()]).await?;
283        assert!(Collection::remove_songs(&db, collection.id.clone(), vec![song.id.clone()]).await?);
284
285        let result = Collection::read_songs(&db, collection.id.clone()).await?;
286        assert_eq!(result, vec![]);
287
288        let read = Collection::read(&db, collection.id.clone())
289            .await?
290            .ok_or_else(|| anyhow!("Collection not found"))?;
291        assert_eq!(read.song_count, 0);
292        assert_eq!(read.runtime, Duration::from_secs(0));
293
294        Ok(())
295    }
296
297    #[tokio::test]
298    async fn test_freeze() -> Result<()> {
299        let db = init_test_database().await?;
300        let collection = create_collection();
301        Collection::create(&db, collection.clone()).await?;
302        let song =
303            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default()).await?;
304
305        Collection::add_songs(&db, collection.id.clone(), vec![song.id.clone()]).await?;
306
307        let playlist =
308            Collection::freeze(&db, collection.id.clone(), "Frozen Playlist".into()).await?;
309
310        let songs = Playlist::read_songs(&db, playlist.id.clone()).await?;
311
312        assert_eq!(songs, vec![song.clone()]);
313        assert_eq!(playlist.song_count, 1);
314        assert_eq!(playlist.runtime, song.runtime);
315        assert_eq!(playlist.name, "Frozen Playlist");
316
317        Ok(())
318    }
319}