post_archiver/manager/
collection.rs

1use rusqlite::{params, OptionalExtension};
2
3use crate::{Collection, CollectionId, FileMetaId, Post, PostId};
4
5use super::{PostArchiverConnection, PostArchiverManager};
6
7//=============================================================
8// Querying
9//=============================================================
10impl<T> PostArchiverManager<T>
11where
12    T: PostArchiverConnection,
13{
14    /// Retrieve all collections in the archive.
15    ///
16    /// # Errors
17    ///
18    /// Returns `rusqlite::Error` if there was an error accessing the database.
19    pub fn list_collections(&self) -> Result<Vec<crate::Collection>, rusqlite::Error> {
20        let mut stmt = self.conn().prepare_cached("SELECT * FROM collections")?;
21        let collections = stmt.query_map([], Collection::from_row)?;
22        collections.collect()
23    }
24
25    /// Find a collection by its source.
26    ///
27    /// # Errors
28    ///
29    /// Returns `rusqlite::Error` if there was an error accessing the database.
30    pub fn find_collection(&self, source: &str) -> Result<Option<CollectionId>, rusqlite::Error> {
31        if let Some(id) = self.cache.collections.get(source) {
32            return Ok(Some(*id));
33        }
34
35        let mut stmt = self
36            .conn()
37            .prepare_cached("SELECT id FROM collections WHERE source = ?")?;
38        let id = stmt.query_row([source], |row| row.get(0)).optional();
39
40        if let Ok(Some(id)) = id {
41            self.cache.collections.insert(source.to_string(), id);
42        }
43
44        id
45    }
46    /// Retrieve a collection by their ID.
47    ///
48    /// Fetches all information about a collection including its name, source, and thumb.
49    ///
50    /// # Errors
51    ///
52    /// Returns `rusqlite::Error` if:
53    /// * The collection ID does not exist
54    /// * There was an error accessing the database
55    pub fn get_collection(&self, id: &CollectionId) -> Result<Option<Collection>, rusqlite::Error> {
56        let mut stmt = self
57            .conn()
58            .prepare_cached("SELECT * FROM collections WHERE id = ?")?;
59
60        stmt.query_row([id], crate::Collection::from_row).optional()
61    }
62}
63
64//=============================================================
65// Modifying
66//=============================================================
67impl<T> PostArchiverManager<T>
68where
69    T: PostArchiverConnection,
70{
71    /// Add a new collection to the archive.
72    ///
73    /// inserts a new collection with the given name, an optional source, and an optional thumb.
74    ///
75    /// # Errors
76    ///
77    /// Returns `rusqlite::Error` if:
78    /// * If the source is already in use by another collection
79    /// * There was an error accessing the database
80    pub fn add_collection(
81        &self,
82        name: String,
83        source: Option<String>,
84        thumb: Option<FileMetaId>,
85    ) -> Result<CollectionId, rusqlite::Error> {
86        let mut stmt = self.conn().prepare_cached(
87            "INSERT INTO collections (name, source, thumb) VALUES (?, ?, ?) RETURNING id",
88        )?;
89        let id = stmt.query_row(params![name, source, thumb], |row| row.get(0))?;
90
91        if let Some(source) = &source {
92            self.cache.collections.insert(source.clone(), id);
93        }
94        Ok(id)
95    }
96
97    /// Remove a collection from the archive.
98    ///
99    /// The operation will also remove Author-Post relationships associated with the collection.
100    ///
101    /// # Errors
102    ///
103    /// Returns `rusqlite::Error` if there was an error accessing the database.
104    pub fn remove_collection(&self, id: CollectionId) -> Result<(), rusqlite::Error> {
105        let mut stmt = self
106            .conn()
107            .prepare_cached("DELETE FROM collection_posts WHERE collection = ?")?;
108        stmt.execute([id])?;
109        Ok(())
110    }
111
112    /// Set an name of a collection.
113    ///
114    /// # Errors
115    ///
116    /// Returns `rusqlite::Error` if there was an error accessing the database.
117    pub fn set_collection_name(
118        &self,
119        id: CollectionId,
120        name: String,
121    ) -> Result<(), rusqlite::Error> {
122        let mut stmt = self
123            .conn()
124            .prepare_cached("UPDATE collections SET name = ? WHERE id = ?")?;
125        stmt.execute(params![name, id])?;
126        Ok(())
127    }
128
129    /// Set the source of a collection.
130    ///
131    /// # Errors
132    ///
133    /// Returns `rusqlite::Error` if:
134    /// * The source is already in use by another collection.
135    /// * There was an error accessing the database.
136    pub fn set_collection_source(
137        &self,
138        id: CollectionId,
139        source: Option<String>,
140    ) -> Result<(), rusqlite::Error> {
141        let mut stmt = self
142            .conn()
143            .prepare_cached("UPDATE collections SET source = ? WHERE id = ?")?;
144        stmt.execute(params![&source, id])?;
145        if let Some(source) = source {
146            self.cache.collections.insert(source, id);
147        }
148        Ok(())
149    }
150
151    /// Set the thumb of a collection.
152    ///
153    /// # Errors
154    ///
155    /// Returns `rusqlite::Error` if:
156    /// * The thumb ID does not exist.
157    /// * There was an error accessing the database.
158    pub fn set_collection_thumb(
159        &self,
160        id: CollectionId,
161        thumb: Option<String>,
162    ) -> Result<(), rusqlite::Error> {
163        let mut stmt = self
164            .conn()
165            .prepare_cached("UPDATE collections SET thumb = ? WHERE id = ?")?;
166        stmt.execute(params![thumb, id])?;
167        Ok(())
168    }
169
170    /// Set the collection's thumb to the latest post's thumb  that has a non-null thumb.
171    ///
172    /// # Errors
173    ///
174    /// Returns `rusqlite::Error` if there was an error accessing the database.
175    ///
176    pub fn set_collection_thumb_by_latest(&self, id: CollectionId) -> Result<(), rusqlite::Error> {
177        let mut stmt = self.conn().prepare_cached(
178            "UPDATE collections SET thumb = (
179                SELECT posts.thumb FROM posts 
180                INNER JOIN collection_posts ON collection_posts.post = posts.id 
181                WHERE collection_posts.collection = ? AND posts.thumb IS NOT NULL
182                ORDER BY posts.updated DESC LIMIT 1
183            ) WHERE id = ?",
184        )?;
185        stmt.execute(params![id, id])?;
186        Ok(())
187    }
188}
189
190//=============================================================
191// Relationships
192//=============================================================
193impl<T> PostArchiverManager<T>
194where
195    T: PostArchiverConnection,
196{
197    /// Retrieve all collections associated with a post.
198    ///
199    /// # Errors
200    ///
201    /// Returns `rusqlite::Error` if there was an error accessing the database.
202    pub fn list_post_collections(&self, post: &PostId) -> Result<Vec<Collection>, rusqlite::Error> {
203        let mut stmt = self
204            .conn()
205            .prepare_cached("SELECT collections.* FROM collections INNER JOIN collection_posts ON collection_posts.collection = collections.id WHERE collection_posts.post = ?")?;
206        let collections = stmt.query_map([post], crate::Collection::from_row)?;
207        collections.collect()
208    }
209
210    /// Retrieve all posts associated with a collection.
211    ///
212    /// # Errors
213    ///
214    /// Returns `rusqlite::Error` if there was an error accessing the database.
215    pub fn list_collection_posts(
216        &self,
217        collection: &CollectionId,
218    ) -> Result<Vec<Post>, rusqlite::Error> {
219        let mut stmt = self
220            .conn()
221            .prepare_cached("SELECT posts.* FROM posts INNER JOIN collection_posts ON collection_posts.post = posts.id WHERE collection_posts.collection = ?")?;
222        let posts = stmt.query_map([collection], Post::from_row)?;
223        posts.collect()
224    }
225}
226
227impl Post {
228    /// Retrieve all collections associated with this post.
229    ///
230    /// # Errors
231    ///
232    /// Returns `rusqlite::Error` if there was an error accessing the database.
233    pub fn collections(
234        &self,
235        manager: &PostArchiverManager,
236    ) -> Result<Vec<Collection>, rusqlite::Error> {
237        manager.list_post_collections(&self.id)
238    }
239}
240
241impl Collection {
242    /// Retrieve all posts associated with this collection.
243    ///
244    /// # Errors
245    ///
246    /// Returns `rusqlite::Error` if there was an error accessing the database.
247    pub fn posts(&self, manager: &PostArchiverManager) -> Result<Vec<Post>, rusqlite::Error> {
248        manager.list_collection_posts(&self.id)
249    }
250}