post_archiver/manager/
post.rs

1use chrono::{DateTime, Utc};
2use rusqlite::{params, OptionalExtension};
3
4use crate::{
5    manager::{PostArchiverConnection, PostArchiverManager},
6    AuthorId, CollectionId, Comment, Content, FileMetaId, PlatformId, Post, PostId, TagId,
7};
8
9//=============================================================
10// Querying
11//=============================================================
12impl<T> PostArchiverManager<T>
13where
14    T: PostArchiverConnection,
15{
16    /// Retrieve all posts in the archive.
17    ///
18    /// # Errors
19    ///
20    /// Returns `rusqlite::Error` if there was an error accessing the database.
21    pub fn list_posts(&self) -> Result<Vec<Post>, rusqlite::Error> {
22        let mut stmt = self.conn().prepare_cached("SELECT * FROM posts")?;
23        let posts = stmt.query_map([], Post::from_row)?;
24        posts.collect()
25    }
26
27    /// Find a post by its source.
28    ///
29    /// # errors
30    ///
31    /// Returns `rusqlite::error` if there was an error accessing the database.
32    pub fn find_post(&self, source: &str) -> Result<Option<PostId>, rusqlite::Error> {
33        let mut stmt = self
34            .conn()
35            .prepare_cached("SELECT id FROM posts WHERE source = ?")?;
36
37        stmt.query_row(params![source], |row| row.get(0)).optional()
38    }
39
40    /// Find a post by its source.
41    ///     
42    /// If you want to check if the post exists in the archive, use [`find_post`](Self::find_post) instead.
43    ///
44    /// # errors
45    ///
46    /// Returns `rusqlite::error` if there was an error accessing the database.
47    /// Check if a the post exists in the archive by their source and updated date.
48    pub fn find_post_with_updated(
49        &self,
50        source: &str,
51        updated: &DateTime<Utc>,
52    ) -> Result<Option<PostId>, rusqlite::Error> {
53        let mut stmt = self
54            .conn()
55            .prepare_cached("SELECT id, updated FROM posts WHERE source = ?")?;
56
57        stmt.query_row::<(PostId, DateTime<Utc>), _, _>(params![source], |row| {
58            Ok((row.get_unwrap(0), row.get_unwrap(1)))
59        })
60        .optional()
61        .map(|query| {
62            query.and_then(|(id, last_update)| {
63                if &last_update >= updated {
64                    Some(id)
65                } else {
66                    None
67                }
68            })
69        })
70    }
71    /// Retrieve a post by its ID.
72    ///
73    /// # Errors
74    ///
75    /// Returns `rusqlite::Error` if:
76    /// * The post ID does not exist
77    /// * There was an error accessing the database
78    pub fn get_post(&self, id: &PostId) -> Result<Post, rusqlite::Error> {
79        let mut stmt = self
80            .conn()
81            .prepare_cached("SELECT * FROM posts WHERE id = ?")?;
82
83        stmt.query_row([id], Post::from_row)
84    }
85}
86
87//=============================================================
88// Modifying
89//=============================================================
90impl<T> PostArchiverManager<T>
91where
92    T: PostArchiverConnection,
93{
94    /// Add a new post to the archive.
95    ///
96    /// # Errors
97    ///
98    /// Returns `rusqlite::Error` if:
99    /// * The post already exists with the same source
100    /// * There was an error accessing the database
101    pub fn add_post(
102        &self,
103        title: String,
104        source: Option<String>,
105        platform: Option<PlatformId>,
106        published: Option<DateTime<Utc>>,
107        updated: Option<DateTime<Utc>>,
108    ) -> Result<PostId, rusqlite::Error> {
109        let mut stmt = self
110            .conn()
111            .prepare_cached(
112                "INSERT INTO posts (title, source, platform, published, updated) VALUES (?, ?, ?, ?, ?) RETURNING id",
113            )?;
114
115        stmt.query_row(
116            params![title, source, platform, published, updated],
117            |row| row.get(0),
118        )
119    }
120    /// Remove a post from the archive.
121    ///
122    /// This operation will also remove all file metadata, author associations, tag associations, and collection associations for the post.
123    ///
124    /// # Errors
125    ///
126    /// Returns `rusqlite::Error` if there was an error accessing the database.
127    pub fn remove_post(&self, post: PostId) -> Result<(), rusqlite::Error> {
128        let mut stmt = self
129            .conn()
130            .prepare_cached("DELETE FROM posts WHERE id = ?")?;
131        stmt.execute([post])?;
132        Ok(())
133    }
134    /// Associate one or more authors with a post.
135    ///
136    /// Creates author associations between a post and the provided author IDs.
137    /// Duplicate associations are silently ignored.
138    ///
139    /// # Errors
140    ///
141    /// Returns `rusqlite::Error` if there was an error accessing the database.
142    pub fn add_post_authors(
143        &self,
144        post: PostId,
145        authors: &[AuthorId],
146    ) -> Result<(), rusqlite::Error> {
147        let mut stmt = self
148            .conn()
149            .prepare_cached("INSERT OR IGNORE INTO author_posts (author, post) VALUES (?, ?)")?;
150        for author in authors {
151            stmt.execute(params![author, post])?;
152        }
153        Ok(())
154    }
155    /// Remove one or more authors from a post.
156    ///
157    /// # Errors
158    ///
159    /// Returns `rusqlite::Error` if there was an error accessing the database.
160    pub fn remove_post_authors(
161        &self,
162        post: PostId,
163        authors: &[AuthorId],
164    ) -> Result<(), rusqlite::Error> {
165        let mut stmt = self
166            .conn()
167            .prepare_cached("DELETE FROM author_posts WHERE post = ? AND author = ?")?;
168        for author in authors {
169            stmt.execute(params![post, author])?;
170        }
171        Ok(())
172    }
173
174    /// Associate one or more tags with a post.
175    ///
176    /// Creates tag associations between a post and the provided tag IDs.
177    /// Duplicate associations are silently ignored.
178    ///
179    /// # Errors
180    ///
181    /// Returns `rusqlite::Error` if there was an error accessing the database.
182    pub fn add_post_tags(&self, post: PostId, tags: &[TagId]) -> Result<(), rusqlite::Error> {
183        let mut stmt = self
184            .conn()
185            .prepare_cached("INSERT OR IGNORE INTO post_tags (post, tag) VALUES (?, ?)")?;
186
187        for tag in tags {
188            stmt.execute(params![post, tag])?;
189        }
190        Ok(())
191    }
192
193    /// Remove one or more tags from a post.
194    ///
195    /// # Errors
196    ///
197    /// Returns `rusqlite::Error` if there was an error accessing the database.
198    pub fn remove_post_tags(&self, post: PostId, tags: &[TagId]) -> Result<(), rusqlite::Error> {
199        let mut stmt = self
200            .conn()
201            .prepare_cached("DELETE FROM post_tags WHERE post = ? AND tag = ?")?;
202        for tag in tags {
203            stmt.execute(params![post, tag])?;
204        }
205        Ok(())
206    }
207    /// Associate one or more tags with a post.
208    ///
209    /// Creates tag associations between a post and the provided tag IDs.
210    /// Duplicate associations are silently ignored.
211    ///
212    /// # Errors
213    ///
214    /// Returns `rusqlite::Error` if there was an error accessing the database.
215    pub fn add_post_collections(
216        &self,
217        post: PostId,
218        collections: &[CollectionId],
219    ) -> Result<(), rusqlite::Error> {
220        let mut stmt = self.conn().prepare_cached(
221            "INSERT OR IGNORE INTO collection_posts (collection, post) VALUES (?, ?)",
222        )?;
223
224        for collection in collections {
225            stmt.execute(params![collection, post])?;
226        }
227        Ok(())
228    }
229
230    /// Remove one or more collections from a post.
231    ///
232    /// # Errors
233    ///
234    /// Returns `rusqlite::Error` if there was an error accessing the database.
235    pub fn remove_post_collections(
236        &self,
237        post: PostId,
238        collections: &[CollectionId],
239    ) -> Result<(), rusqlite::Error> {
240        let mut stmt = self
241            .conn()
242            .prepare_cached("DELETE FROM collection_posts WHERE collection = ? AND post = ?")?;
243        for collection in collections {
244            stmt.execute(params![collection, post])?;
245        }
246        Ok(())
247    }
248    /// Set a post's source URL.
249    ///
250    /// Sets the source identifier for a post, or removes it by passing `None`.
251    ///
252    /// # Errors
253    ///
254    /// Returns `rusqlite::Error` if there was an error accessing the database.
255    pub fn set_post_source(
256        &self,
257        post: PostId,
258        source: Option<String>,
259    ) -> Result<(), rusqlite::Error> {
260        let mut stmt = self
261            .conn()
262            .prepare_cached("UPDATE posts SET source = ? WHERE id = ?")?;
263        stmt.execute(params![source, post])?;
264        Ok(())
265    }
266    /// Set a post's platform.
267    ///
268    /// Associates a file metadata ID as the post's thumbnail, or removes it by passing `None`.
269    ///
270    /// # Errors
271    ///
272    /// Returns `rusqlite::Error` if there was an error accessing the database.
273    pub fn set_post_platform(
274        &self,
275        post: PostId,
276        platform: Option<PlatformId>,
277    ) -> Result<(), rusqlite::Error> {
278        let mut stmt = self
279            .conn()
280            .prepare_cached("UPDATE posts SET platform = ? WHERE id = ?")?;
281        stmt.execute(params![platform, post])?;
282        Ok(())
283    }
284    /// Set a post's title.
285    ///
286    /// Sets a new title for the specified post.
287    ///
288    /// # Errors
289    ///
290    /// Returns `rusqlite::Error` if there was an error accessing the database.
291    pub fn set_post_title(&self, post: PostId, title: String) -> Result<(), rusqlite::Error> {
292        let mut stmt = self
293            .conn()
294            .prepare_cached("UPDATE posts SET title = ? WHERE id = ?")?;
295        stmt.execute(params![title, post])?;
296        Ok(())
297    }
298
299    /// Set a post's thumbnail.
300    ///
301    /// Associates a file metadata ID as the post's thumbnail, or removes it by passing `None`.
302    ///
303    /// # Errors
304    ///
305    /// Returns `rusqlite::Error` if there was an error accessing the database.
306    pub fn set_post_thumb(
307        &self,
308        post: PostId,
309        thumb: Option<FileMetaId>,
310    ) -> Result<(), rusqlite::Error> {
311        let mut stmt = self
312            .conn()
313            .prepare_cached("UPDATE posts SET thumb = ? WHERE id = ?")?;
314        stmt.execute(params![thumb, post])?;
315        Ok(())
316    }
317
318    /// Set a post's content.
319    ///
320    /// Replaces the entire content of a post with new text and file entries.
321    ///
322    /// # Errors
323    ///
324    /// Returns `rusqlite::Error` if there was an error accessing the database.
325    pub fn set_post_content(
326        &self,
327        post: PostId,
328        content: Vec<Content>,
329    ) -> Result<(), rusqlite::Error> {
330        let content = serde_json::to_string(&content).unwrap();
331
332        let mut stmt = self
333            .conn()
334            .prepare_cached("UPDATE posts SET content = ? WHERE id = ?")?;
335
336        stmt.execute(params![content, post])?;
337        Ok(())
338    }
339    /// Set a post's comments.
340    ///
341    /// # Errors
342    ///
343    /// Returns `rusqlite::Error` if there was an error accessing the database.
344    pub fn set_post_comments(
345        &self,
346        post: PostId,
347        comments: Vec<Comment>,
348    ) -> Result<(), rusqlite::Error> {
349        let comments = serde_json::to_string(&comments).unwrap();
350
351        let mut stmt = self
352            .conn()
353            .prepare_cached("UPDATE posts SET comments = ? WHERE id = ?")?;
354        stmt.execute(params![comments, post])?;
355        Ok(())
356    }
357    /// Set a post's published timestamp.
358    ///
359    /// # Errors
360    ///
361    /// Returns `rusqlite::Error` if there was an error accessing the database.
362    pub fn set_post_published(
363        &self,
364        post: PostId,
365        published: DateTime<Utc>,
366    ) -> Result<(), rusqlite::Error> {
367        let mut stmt = self
368            .conn()
369            .prepare_cached("UPDATE posts SET published = ? WHERE id = ?")?;
370        stmt.execute(params![published, post])?;
371        Ok(())
372    }
373    ///Set a post's updated timestamp.
374    ///
375    /// # Errors
376    ///
377    /// Returns `rusqlite::Error` if there was an error accessing the database.
378    pub fn set_post_updated(
379        &self,
380        post: PostId,
381        updated: DateTime<Utc>,
382    ) -> Result<(), rusqlite::Error> {
383        let mut stmt = self
384            .conn()
385            .prepare_cached("UPDATE posts SET updated = ? WHERE id = ?")?;
386        stmt.execute(params![updated, post])?;
387        Ok(())
388    }
389    /// Set a post's updated timestamp if current timestamp is more recent.
390    ///
391    /// # Errors
392    ///
393    /// Returns `rusqlite::Error` if there was an error accessing the database.
394    pub fn set_post_updated_by_latest(
395        &self,
396        post: PostId,
397        updated: DateTime<Utc>,
398    ) -> Result<(), rusqlite::Error> {
399        let mut stmt = self
400            .conn()
401            .prepare_cached("UPDATE posts SET updated = ? WHERE id = ? AND updated < ?")?;
402        stmt.execute(params![updated, post, updated])?;
403        Ok(())
404    }
405}