fabrica

hanna/fabrica

3421 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Bookmarks (stars): a user marking a repository. A bookmark is a `(user, repo)`
6//! pair; the listing joins back to `repos` so callers can filter by visibility.
7
8use model::Repo;
9use sqlx::{AssertSqlSafe, Row};
10
11use crate::repos::REPO_COLUMNS;
12use crate::{Store, StoreError, new_id, now_ms};
13
14impl Store {
15 /// Bookmark a repository for a user. Idempotent.
16 ///
17 /// # Errors
18 ///
19 /// Returns [`StoreError::Query`] on a database failure.
20 pub async fn add_bookmark(&self, user_id: &str, repo_id: &str) -> Result<(), StoreError> {
21 sqlx::query(
22 "INSERT INTO bookmarks (id, user_id, repo_id, created_at) VALUES ($1, $2, $3, $4) \
23 ON CONFLICT (user_id, repo_id) DO NOTHING",
24 )
25 .bind(new_id())
26 .bind(user_id)
27 .bind(repo_id)
28 .bind(now_ms())
29 .execute(&self.pool)
30 .await?;
31 Ok(())
32 }
33
34 /// Remove a user's bookmark of a repository.
35 ///
36 /// # Errors
37 ///
38 /// Returns [`StoreError::Query`] on a database failure.
39 pub async fn remove_bookmark(&self, user_id: &str, repo_id: &str) -> Result<(), StoreError> {
40 sqlx::query("DELETE FROM bookmarks WHERE user_id = $1 AND repo_id = $2")
41 .bind(user_id)
42 .bind(repo_id)
43 .execute(&self.pool)
44 .await?;
45 Ok(())
46 }
47
48 /// Whether `user_id` has bookmarked `repo_id`.
49 ///
50 /// # Errors
51 ///
52 /// Returns [`StoreError::Query`] on a database failure.
53 pub async fn is_bookmarked(&self, user_id: &str, repo_id: &str) -> Result<bool, StoreError> {
54 let row = sqlx::query("SELECT 1 AS n FROM bookmarks WHERE user_id = $1 AND repo_id = $2")
55 .bind(user_id)
56 .bind(repo_id)
57 .fetch_optional(&self.pool)
58 .await?;
59 Ok(row.is_some())
60 }
61
62 /// How many users have bookmarked `repo_id`.
63 ///
64 /// # Errors
65 ///
66 /// Returns [`StoreError::Query`] on a database failure.
67 pub async fn count_repo_bookmarks(&self, repo_id: &str) -> Result<i64, StoreError> {
68 let n: i64 = sqlx::query("SELECT COUNT(*) AS n FROM bookmarks WHERE repo_id = $1")
69 .bind(repo_id)
70 .fetch_one(&self.pool)
71 .await?
72 .try_get("n")?;
73 Ok(n)
74 }
75
76 /// The repositories `user_id` has bookmarked, most recent first. Callers
77 /// filter by the viewer's read access before display.
78 ///
79 /// # Errors
80 ///
81 /// Returns [`StoreError::Query`] on a database failure.
82 pub async fn bookmarked_repos(&self, user_id: &str) -> Result<Vec<Repo>, StoreError> {
83 // Qualify every repo column so nothing collides with `bookmarks`.
84 let cols = format!("r.{}", REPO_COLUMNS.replace(", ", ", r."));
85 let sql = format!(
86 "SELECT {cols} FROM repos r JOIN bookmarks b ON b.repo_id = r.id \
87 WHERE b.user_id = $1 ORDER BY b.created_at DESC"
88 );
89 let rows = sqlx::query(AssertSqlSafe(sql))
90 .bind(user_id)
91 .fetch_all(&self.pool)
92 .await?;
93 rows.iter()
94 .map(|r| self.map_repo(r))
95 .collect::<Result<_, _>>()
96 .map_err(Into::into)
97 }
98}