// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Bookmarks (stars): a user marking a repository. A bookmark is a `(user, repo)` //! pair; the listing joins back to `repos` so callers can filter by visibility. use model::Repo; use sqlx::{AssertSqlSafe, Row}; use crate::repos::REPO_COLUMNS; use crate::{Store, StoreError, new_id, now_ms}; impl Store { /// Bookmark a repository for a user. Idempotent. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn add_bookmark(&self, user_id: &str, repo_id: &str) -> Result<(), StoreError> { sqlx::query( "INSERT INTO bookmarks (id, user_id, repo_id, created_at) VALUES ($1, $2, $3, $4) \ ON CONFLICT (user_id, repo_id) DO NOTHING", ) .bind(new_id()) .bind(user_id) .bind(repo_id) .bind(now_ms()) .execute(&self.pool) .await?; Ok(()) } /// Remove a user's bookmark of a repository. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn remove_bookmark(&self, user_id: &str, repo_id: &str) -> Result<(), StoreError> { sqlx::query("DELETE FROM bookmarks WHERE user_id = $1 AND repo_id = $2") .bind(user_id) .bind(repo_id) .execute(&self.pool) .await?; Ok(()) } /// Whether `user_id` has bookmarked `repo_id`. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn is_bookmarked(&self, user_id: &str, repo_id: &str) -> Result { let row = sqlx::query("SELECT 1 AS n FROM bookmarks WHERE user_id = $1 AND repo_id = $2") .bind(user_id) .bind(repo_id) .fetch_optional(&self.pool) .await?; Ok(row.is_some()) } /// How many users have bookmarked `repo_id`. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn count_repo_bookmarks(&self, repo_id: &str) -> Result { let n: i64 = sqlx::query("SELECT COUNT(*) AS n FROM bookmarks WHERE repo_id = $1") .bind(repo_id) .fetch_one(&self.pool) .await? .try_get("n")?; Ok(n) } /// The repositories `user_id` has bookmarked, most recent first. Callers /// filter by the viewer's read access before display. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn bookmarked_repos(&self, user_id: &str) -> Result, StoreError> { // Qualify every repo column so nothing collides with `bookmarks`. let cols = format!("r.{}", REPO_COLUMNS.replace(", ", ", r.")); let sql = format!( "SELECT {cols} FROM repos r JOIN bookmarks b ON b.repo_id = r.id \ WHERE b.user_id = $1 ORDER BY b.created_at DESC" ); let rows = sqlx::query(AssertSqlSafe(sql)) .bind(user_id) .fetch_all(&self.pool) .await?; rows.iter() .map(|r| self.map_repo(r)) .collect::>() .map_err(Into::into) } }