Add title to popular posts route to improve site efficiency

This commit is contained in:
Alex Selimov 2026-08-03 23:16:12 -04:00
parent 3ee97b5d43
commit 1228112213
5 changed files with 59 additions and 22 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target /target
.env .env
upvoters

View file

@ -0,0 +1,7 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS titles (
slug text not null,
title text not null,
primary key (slug)
);

View file

@ -13,6 +13,7 @@ use uuid::Uuid;
use crate::{ use crate::{
state::AppState, state::AppState,
votes::{ votes::{
model::VoteRequest,
repository::{delete_vote, get_top_n_slugs, insert_new_vote}, repository::{delete_vote, get_top_n_slugs, insert_new_vote},
service::get_votes_and_voted, service::get_votes_and_voted,
}, },
@ -51,10 +52,12 @@ async fn upvote_handler(
jar: CookieJar, jar: CookieJar,
Path(slug): Path<String>, Path(slug): Path<String>,
State(state): State<AppState>, State(state): State<AppState>,
Json(upvote_payload): Json<VoteRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let (jar, voter_id) = get_or_init_voter_id(jar); let (jar, voter_id) = get_or_init_voter_id(jar);
match insert_new_vote(&slug, &voter_id, &state.db).await { info!("{:?}", upvote_payload);
match insert_new_vote(&slug, &voter_id, upvote_payload.title, &state.db).await {
Ok(()) => { Ok(()) => {
info!(slug = %slug, voter_id = %voter_id, "upvoted successfully"); info!(slug = %slug, voter_id = %voter_id, "upvoted successfully");
(StatusCode::OK, jar, "Successfully upvoted") (StatusCode::OK, jar, "Successfully upvoted")
@ -260,7 +263,7 @@ mod tests {
let slug = "delete_vote_handler_removes_vote_for_cookie_voter"; let slug = "delete_vote_handler_removes_vote_for_cookie_voter";
let voter_id = Uuid::from_u128(0xdef); let voter_id = Uuid::from_u128(0xdef);
delete_votes_for_slug(slug, &db).await.unwrap(); delete_votes_for_slug(slug, &db).await.unwrap();
insert_new_vote(slug, &voter_id, &db).await.unwrap(); insert_new_vote(slug, &voter_id, None, &db).await.unwrap();
let app = crate::app(test_state(db.clone())); let app = crate::app(test_state(db.clone()));
let request = Request::builder() let request = Request::builder()
@ -309,8 +312,10 @@ mod tests {
let voter_id = Uuid::from_u128(0x123); let voter_id = Uuid::from_u128(0x123);
let other_voter_id = Uuid::from_u128(0x456); let other_voter_id = Uuid::from_u128(0x456);
delete_votes_for_slug(slug, &db).await.unwrap(); delete_votes_for_slug(slug, &db).await.unwrap();
insert_new_vote(slug, &voter_id, &db).await.unwrap(); insert_new_vote(slug, &voter_id, None, &db).await.unwrap();
insert_new_vote(slug, &other_voter_id, &db).await.unwrap(); insert_new_vote(slug, &other_voter_id, None, &db)
.await
.unwrap();
let app = crate::app(test_state(db.clone())); let app = crate::app(test_state(db.clone()));
let request = Request::builder() let request = Request::builder()
@ -337,7 +342,7 @@ mod tests {
let slug = "get_votes_and_voted_handler_sets_cookie_for_new_voter"; let slug = "get_votes_and_voted_handler_sets_cookie_for_new_voter";
let existing_voter_id = Uuid::from_u128(0x789); let existing_voter_id = Uuid::from_u128(0x789);
delete_votes_for_slug(slug, &db).await.unwrap(); delete_votes_for_slug(slug, &db).await.unwrap();
insert_new_vote(slug, &existing_voter_id, &db) insert_new_vote(slug, &existing_voter_id, None, &db)
.await .await
.unwrap(); .unwrap();
@ -378,12 +383,16 @@ mod tests {
Uuid::from_u128(0x101), Uuid::from_u128(0x101),
Uuid::from_u128(0x102), Uuid::from_u128(0x102),
] { ] {
insert_new_vote(slugs[0], &voter_id, &db).await.unwrap(); insert_new_vote(slugs[0], &voter_id, None, &db)
.await
.unwrap();
} }
for voter_id in [Uuid::from_u128(0x200), Uuid::from_u128(0x201)] { for voter_id in [Uuid::from_u128(0x200), Uuid::from_u128(0x201)] {
insert_new_vote(slugs[1], &voter_id, &db).await.unwrap(); insert_new_vote(slugs[1], &voter_id, None, &db)
.await
.unwrap();
} }
insert_new_vote(slugs[2], &Uuid::from_u128(0x300), &db) insert_new_vote(slugs[2], &Uuid::from_u128(0x300), None, &db)
.await .await
.unwrap(); .unwrap();

View file

@ -3,6 +3,12 @@ use serde::{Deserialize, Serialize};
use sqlx::prelude::FromRow; use sqlx::prelude::FromRow;
use uuid::Uuid; use uuid::Uuid;
/// Struct representing a vote request
#[derive(Debug, Deserialize)]
pub struct VoteRequest {
pub title: Option<String>,
}
/// Struct representing a single vote (row) in the votes table /// Struct representing a single vote (row) in the votes table
#[derive(Debug, FromRow)] #[derive(Debug, FromRow)]
pub struct Vote { pub struct Vote {
@ -16,4 +22,5 @@ pub struct Vote {
pub struct BestSlugs { pub struct BestSlugs {
pub slug: String, pub slug: String,
pub vote_count: i64, pub vote_count: i64,
pub title: String,
} }

View file

@ -19,7 +19,12 @@ pub async fn vote_exists(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<boo
.await?; .await?;
Ok(count > 0) Ok(count > 0)
} }
pub async fn insert_new_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> { pub async fn insert_new_vote(
slug: &str,
voter_id: &Uuid,
title: Option<String>,
db: &PgPool,
) -> Result<()> {
query( query(
r#"insert into votes (slug, voter_id) r#"insert into votes (slug, voter_id)
values ($1, $2) values ($1, $2)
@ -29,6 +34,14 @@ pub async fn insert_new_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result
.bind(voter_id) .bind(voter_id)
.execute(db) .execute(db)
.await?; .await?;
if let Some(title) = title {
query(r#"insert into titles values ($1, $2)"#)
.bind(slug)
.bind(title)
.execute(db)
.await?;
}
Ok(()) Ok(())
} }
@ -43,9 +56,9 @@ pub async fn get_vote_count_for_slug(slug: &str, db: &PgPool) -> Result<i64> {
pub async fn get_top_n_slugs(n: i64, db: &PgPool) -> Result<Vec<BestSlugs>> { pub async fn get_top_n_slugs(n: i64, db: &PgPool) -> Result<Vec<BestSlugs>> {
if n > 0 { if n > 0 {
let top_slugs = query_as::<_, BestSlugs>( let top_slugs = query_as::<_, BestSlugs>(
r#"select slug, count(*) AS vote_count r#"select slug, title, count(*) AS vote_count
from votes from votes natural join titles
group by slug group by slug, title
order by vote_count desc order by vote_count desc
limit $1 limit $1
"#, "#,
@ -130,7 +143,7 @@ mod postgres_tests {
cleanup(&db, &votes).await; cleanup(&db, &votes).await;
for (slug, voter_id) in votes.iter() { for (slug, voter_id) in votes.iter() {
insert_new_vote(slug, voter_id, &db) insert_new_vote(slug, voter_id, None, &db)
.await .await
.expect("Insertions to db failed"); .expect("Insertions to db failed");
} }
@ -196,22 +209,22 @@ mod postgres_tests {
)]; )];
cleanup(&db, &votes).await; cleanup(&db, &votes).await;
insert_new_vote(&votes[0].0, &votes[0].1, &db) insert_new_vote(&votes[0].0, &votes[0].1, None, &db)
.await .await
.unwrap(); .unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db) insert_new_vote(&votes[0].0, &votes[0].1, None, &db)
.await .await
.unwrap(); .unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db) insert_new_vote(&votes[0].0, &votes[0].1, None, &db)
.await .await
.unwrap(); .unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db) insert_new_vote(&votes[0].0, &votes[0].1, None, &db)
.await .await
.unwrap(); .unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db) insert_new_vote(&votes[0].0, &votes[0].1, None, &db)
.await .await
.unwrap(); .unwrap();
insert_new_vote(&votes[0].0, &votes[0].1, &db) insert_new_vote(&votes[0].0, &votes[0].1, None, &db)
.await .await
.unwrap(); .unwrap();
@ -231,7 +244,7 @@ mod postgres_tests {
)]; )];
cleanup(&db, &votes).await; cleanup(&db, &votes).await;
insert_new_vote(&votes[0].0, &votes[0].1, &db) insert_new_vote(&votes[0].0, &votes[0].1, None, &db)
.await .await
.unwrap(); .unwrap();
@ -265,10 +278,10 @@ mod postgres_tests {
]; ];
cleanup(&db, &votes).await; cleanup(&db, &votes).await;
insert_new_vote(&votes[0].0, &votes[0].1, &db) insert_new_vote(&votes[0].0, &votes[0].1, None, &db)
.await .await
.unwrap(); .unwrap();
insert_new_vote(&votes[1].0, &votes[1].1, &db) insert_new_vote(&votes[1].0, &votes[1].1, None, &db)
.await .await
.unwrap(); .unwrap();