diff --git a/src/routes/v2/statistics.rs b/src/routes/v2/statistics.rs index b5f5b781..f6647ede 100644 --- a/src/routes/v2/statistics.rs +++ b/src/routes/v2/statistics.rs @@ -1,7 +1,10 @@ -use crate::routes::{ - v2_reroute, - v3::{self, statistics::V3Stats}, - ApiError, +use crate::{ + database::redis::RedisPool, + routes::{ + v2_reroute, + v3::{self, statistics::V3Stats}, + ApiError, + }, }; use actix_web::{get, web, HttpResponse}; use sqlx::PgPool; @@ -12,15 +15,18 @@ pub fn config(cfg: &mut web::ServiceConfig) { #[derive(serde::Serialize)] pub struct V2Stats { - pub projects: Option, - pub versions: Option, - pub authors: Option, - pub files: Option, + pub projects: i64, + pub versions: i64, + pub authors: i64, + pub files: i64, } #[get("statistics")] -pub async fn get_stats(pool: web::Data) -> Result { - let response = v3::statistics::get_stats(pool) +pub async fn get_stats( + pool: web::Data, + redis: web::Data, +) -> Result { + let response = v3::statistics::get_stats(pool, redis) .await .or_else(v2_reroute::flatten_404_error)?; diff --git a/src/routes/v3/statistics.rs b/src/routes/v3/statistics.rs index c6c24e1a..6e87787a 100644 --- a/src/routes/v3/statistics.rs +++ b/src/routes/v3/statistics.rs @@ -1,91 +1,179 @@ -use crate::routes::ApiError; +use crate::{database::redis::RedisPool, routes::ApiError}; use actix_web::{web, HttpResponse}; use sqlx::PgPool; +const STATISTICS_NAMESPACE: &str = "statistics"; +const STATISTICS_EXPIRY: i64 = 60 * 30; // 30 minutes + pub fn config(cfg: &mut web::ServiceConfig) { cfg.route("statistics", web::get().to(get_stats)); } #[derive(serde::Serialize, serde::Deserialize)] pub struct V3Stats { - pub projects: Option, - pub versions: Option, - pub authors: Option, - pub files: Option, + pub projects: i64, + pub versions: i64, + pub authors: i64, + pub files: i64, } -pub async fn get_stats(pool: web::Data) -> Result { - let projects = sqlx::query!( - " - SELECT COUNT(id) - FROM mods - WHERE status = ANY($1) - ", - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_searchable()) - .map(|x| x.to_string()) - .collect::>(), - ) - .fetch_one(&**pool) - .await?; - - let versions = sqlx::query!( - " +pub async fn get_stats( + pool: web::Data, + redis: web::Data, +) -> Result { + let mut redis = redis.connect().await?; + + let projects = if let Some(project_count) = redis + .get_deserialized_from_json::(STATISTICS_NAMESPACE, "projects") + .await? + { + project_count + } else { + let count = sqlx::query!( + " + SELECT COUNT(id) + FROM mods + WHERE status = ANY($1) + ", + &*crate::models::projects::ProjectStatus::iterator() + .filter(|x| x.is_searchable()) + .map(|x| x.to_string()) + .collect::>(), + ) + .fetch_one(&**pool) + .await? + .count + .unwrap(); + + redis + .set_serialized_to_json( + STATISTICS_NAMESPACE, + "projects", + count, + Some(STATISTICS_EXPIRY), + ) + .await?; + + count + }; + + let versions = if let Some(version_count) = redis + .get_deserialized_from_json::(STATISTICS_NAMESPACE, "versions") + .await? + { + version_count + } else { + let count = sqlx::query!( + " SELECT COUNT(v.id) FROM versions v INNER JOIN mods m on v.mod_id = m.id AND m.status = ANY($1) WHERE v.status = ANY($2) ", - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_searchable()) - .map(|x| x.to_string()) - .collect::>(), - &*crate::models::projects::VersionStatus::iterator() - .filter(|x| x.is_listed()) - .map(|x| x.to_string()) - .collect::>(), - ) - .fetch_one(&**pool) - .await?; - - let authors = sqlx::query!( - " + &*crate::models::projects::ProjectStatus::iterator() + .filter(|x| x.is_searchable()) + .map(|x| x.to_string()) + .collect::>(), + &*crate::models::projects::VersionStatus::iterator() + .filter(|x| x.is_listed()) + .map(|x| x.to_string()) + .collect::>(), + ) + .fetch_one(&**pool) + .await? + .count + .unwrap(); + + redis + .set_serialized_to_json( + STATISTICS_NAMESPACE, + "versions", + count, + Some(STATISTICS_EXPIRY), + ) + .await?; + + count + }; + + let authors = if let Some(author_count) = redis + .get_deserialized_from_json::(STATISTICS_NAMESPACE, "authors") + .await? + { + author_count + } else { + let count = sqlx::query!( + " SELECT COUNT(DISTINCT u.id) FROM users u INNER JOIN team_members tm on u.id = tm.user_id AND tm.accepted = TRUE INNER JOIN mods m on tm.team_id = m.team_id AND m.status = ANY($1) ", - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_searchable()) - .map(|x| x.to_string()) - .collect::>(), - ) - .fetch_one(&**pool) - .await?; - - let files = sqlx::query!( - " + &*crate::models::projects::ProjectStatus::iterator() + .filter(|x| x.is_searchable()) + .map(|x| x.to_string()) + .collect::>(), + ) + .fetch_one(&**pool) + .await? + .count + .unwrap(); + + redis + .set_serialized_to_json( + STATISTICS_NAMESPACE, + "authors", + count, + Some(STATISTICS_EXPIRY), + ) + .await?; + + count + }; + + let files = if let Some(file_count) = redis + .get_deserialized_from_json::(STATISTICS_NAMESPACE, "files") + .await? + { + file_count + } else { + let count = sqlx::query!( + " SELECT COUNT(f.id) FROM files f INNER JOIN versions v on f.version_id = v.id AND v.status = ANY($2) INNER JOIN mods m on v.mod_id = m.id AND m.status = ANY($1) ", - &*crate::models::projects::ProjectStatus::iterator() - .filter(|x| x.is_searchable()) - .map(|x| x.to_string()) - .collect::>(), - &*crate::models::projects::VersionStatus::iterator() - .filter(|x| x.is_listed()) - .map(|x| x.to_string()) - .collect::>(), - ) - .fetch_one(&**pool) - .await?; + &*crate::models::projects::ProjectStatus::iterator() + .filter(|x| x.is_searchable()) + .map(|x| x.to_string()) + .collect::>(), + &*crate::models::projects::VersionStatus::iterator() + .filter(|x| x.is_listed()) + .map(|x| x.to_string()) + .collect::>(), + ) + .fetch_one(&**pool) + .await? + .count + .unwrap(); + + redis + .set_serialized_to_json( + STATISTICS_NAMESPACE, + "files", + count, + Some(STATISTICS_EXPIRY), + ) + .await?; + + count + }; let v3_stats = V3Stats { - projects: projects.count, - versions: versions.count, - authors: authors.count, - files: files.count, + projects, + versions, + authors, + files, }; Ok(HttpResponse::Ok().json(v3_stats))