// 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/. //! Follows: one user following another. Public — profiles show follower and //! following counts and lists. // `follower_id` / `followee_id` are the natural, clear parameter names here. #![allow(clippy::similar_names)] use model::User; use sqlx::{AssertSqlSafe, Row}; use crate::users::USER_COLUMNS; use crate::{Store, StoreError, new_id, now_ms}; impl Store { /// Follow `followee` as `follower`. Idempotent; a self-follow is a no-op. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn follow(&self, follower_id: &str, followee_id: &str) -> Result<(), StoreError> { if follower_id == followee_id { return Ok(()); } sqlx::query( "INSERT INTO follows (id, follower_id, followee_id, created_at) VALUES ($1, $2, $3, $4) \ ON CONFLICT (follower_id, followee_id) DO NOTHING", ) .bind(new_id()) .bind(follower_id) .bind(followee_id) .bind(now_ms()) .execute(&self.pool) .await?; Ok(()) } /// Unfollow `followee`. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn unfollow(&self, follower_id: &str, followee_id: &str) -> Result<(), StoreError> { sqlx::query("DELETE FROM follows WHERE follower_id = $1 AND followee_id = $2") .bind(follower_id) .bind(followee_id) .execute(&self.pool) .await?; Ok(()) } /// Whether `follower` follows `followee`. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn is_following( &self, follower_id: &str, followee_id: &str, ) -> Result { let row = sqlx::query("SELECT 1 AS n FROM follows WHERE follower_id = $1 AND followee_id = $2") .bind(follower_id) .bind(followee_id) .fetch_optional(&self.pool) .await?; Ok(row.is_some()) } /// The number of users following `user_id`. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn count_followers(&self, user_id: &str) -> Result { let n: i64 = sqlx::query("SELECT COUNT(*) AS n FROM follows WHERE followee_id = $1") .bind(user_id) .fetch_one(&self.pool) .await? .try_get("n")?; Ok(n) } /// The number of users `user_id` follows. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn count_following(&self, user_id: &str) -> Result { let n: i64 = sqlx::query("SELECT COUNT(*) AS n FROM follows WHERE follower_id = $1") .bind(user_id) .fetch_one(&self.pool) .await? .try_get("n")?; Ok(n) } /// The users following `user_id`, most recent first. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn followers(&self, user_id: &str) -> Result, StoreError> { let cols = format!("u.{}", USER_COLUMNS.replace(", ", ", u.")); let sql = format!( "SELECT {cols} FROM users u JOIN follows f ON f.follower_id = u.id \ WHERE f.followee_id = $1 ORDER BY f.created_at DESC" ); let rows = sqlx::query(AssertSqlSafe(sql)) .bind(user_id) .fetch_all(&self.pool) .await?; rows.iter() .map(|r| self.map_user(r)) .collect::>() .map_err(Into::into) } /// The users `user_id` follows, most recent first. /// /// # Errors /// /// Returns [`StoreError::Query`] on a database failure. pub async fn following(&self, user_id: &str) -> Result, StoreError> { let cols = format!("u.{}", USER_COLUMNS.replace(", ", ", u.")); let sql = format!( "SELECT {cols} FROM users u JOIN follows f ON f.followee_id = u.id \ WHERE f.follower_id = $1 ORDER BY f.created_at DESC" ); let rows = sqlx::query(AssertSqlSafe(sql)) .bind(user_id) .fetch_all(&self.pool) .await?; rows.iter() .map(|r| self.map_user(r)) .collect::>() .map_err(Into::into) } }