Skip to main content

post_archiver/manager/
collection.rs

1use rusqlite::params;
2
3use crate::{
4    error::Result,
5    manager::{binded::Binded, PostArchiverConnection},
6    query::FromQuery,
7    Collection, CollectionId, FileMetaId, PostId,
8};
9
10/// Specifies how to update a collection's thumbnail.
11#[derive(Debug, Clone)]
12pub enum CollectionThumb {
13    /// Set to an explicit value (or clear with `None`).
14    Set(Option<FileMetaId>),
15    /// Set to the thumb of the most recently updated post in this collection.
16    ByLatest,
17}
18
19/// Builder for updating a collection's fields.
20///
21/// Fields left as `None` are not modified.
22#[derive(Debug, Clone, Default)]
23pub struct UpdateCollection {
24    pub name: Option<String>,
25    pub source: Option<Option<String>>,
26    pub thumb: Option<CollectionThumb>,
27}
28
29impl UpdateCollection {
30    /// Set the collection's name.
31    pub fn name(mut self, name: String) -> Self {
32        self.name = Some(name);
33        self
34    }
35    /// Set or clear the source URL.
36    pub fn source(mut self, source: Option<String>) -> Self {
37        self.source = Some(source);
38        self
39    }
40    /// Set or clear the thumbnail.
41    pub fn thumb(mut self, thumb: Option<FileMetaId>) -> Self {
42        self.thumb = Some(CollectionThumb::Set(thumb));
43        self
44    }
45    /// Set the thumbnail to the latest post's thumb in this collection.
46    pub fn thumb_by_latest(mut self) -> Self {
47        self.thumb = Some(CollectionThumb::ByLatest);
48        self
49    }
50}
51
52//=============================================================
53// Update / Delete
54//=============================================================
55impl<'a, C: PostArchiverConnection> Binded<'a, CollectionId, C> {
56    /// Get this collection's current data from the database.
57    pub fn value(&self) -> Result<Collection> {
58        let mut stmt = self
59            .conn()
60            .prepare_cached("SELECT * FROM collections WHERE id = ?")?;
61        Ok(stmt.query_row([self.id()], Collection::from_row)?)
62    }
63
64    /// Remove this collection from the archive.
65    ///
66    /// Also removes all collection-post relationships.
67    pub fn delete(self) -> Result<()> {
68        self.conn()
69            .execute("DELETE FROM collections WHERE id = ?", [self.id()])?;
70        Ok(())
71    }
72
73    /// Apply a batch of field updates to this collection in a single SQL statement.
74    ///
75    /// Only fields set on `update` (i.e. `Some(...)`) are written to the database.
76    pub fn update(&self, update: UpdateCollection) -> Result<()> {
77        use rusqlite::types::ToSql;
78
79        let id = self.id();
80        let mut sets: Vec<&str> = Vec::new();
81        let mut params: Vec<&dyn ToSql> = Vec::new();
82
83        macro_rules! push {
84            ($field:expr, $col:expr) => {
85                if let Some(ref v) = $field {
86                    sets.push($col);
87                    params.push(v);
88                }
89            };
90        }
91
92        push!(update.name, "name = ?");
93        push!(update.source, "source = ?");
94
95        match &update.thumb {
96            Some(CollectionThumb::Set(v)) => {
97                sets.push("thumb = ?");
98                params.push(v);
99            }
100            Some(CollectionThumb::ByLatest) => {
101                sets.push("thumb = (SELECT posts.thumb FROM posts INNER JOIN collection_posts ON collection_posts.post = posts.id WHERE collection_posts.collection = ? AND posts.thumb IS NOT NULL ORDER BY posts.updated DESC LIMIT 1)");
102                params.push(&id);
103            }
104            None => {}
105        }
106
107        if sets.is_empty() {
108            return Ok(());
109        }
110
111        params.push(&id);
112
113        let sql = format!("UPDATE collections SET {} WHERE id = ?", sets.join(", "));
114        self.conn().execute(&sql, params.as_slice())?;
115        Ok(())
116    }
117}
118
119//=============================================================
120// Relations: Posts
121//=============================================================
122impl<'a, C: PostArchiverConnection> Binded<'a, CollectionId, C> {
123    /// List all post IDs in this collection.
124    pub fn list_posts(&self) -> Result<Vec<PostId>> {
125        let mut stmt = self
126            .conn()
127            .prepare_cached("SELECT post FROM collection_posts WHERE collection = ?")?;
128        let rows = stmt.query_map([self.id()], |row| row.get(0))?;
129        rows.collect::<std::result::Result<_, _>>()
130            .map_err(Into::into)
131    }
132
133    /// Add posts to this collection.
134    pub fn add_posts(&self, posts: &[PostId]) -> Result<()> {
135        let mut stmt = self.conn().prepare_cached(
136            "INSERT OR IGNORE INTO collection_posts (collection, post) VALUES (?, ?)",
137        )?;
138        for post in posts {
139            stmt.execute(params![self.id(), post])?;
140        }
141        Ok(())
142    }
143
144    /// Remove posts from this collection.
145    pub fn remove_posts(&self, posts: &[PostId]) -> Result<()> {
146        let mut stmt = self
147            .conn()
148            .prepare_cached("DELETE FROM collection_posts WHERE collection = ? AND post = ?")?;
149        for post in posts {
150            stmt.execute(params![self.id(), post])?;
151        }
152        Ok(())
153    }
154}