diff --git a/src/controllers/category.rs b/src/controllers/category.rs index 7945bfa77e5..1099ef56aa4 100644 --- a/src/controllers/category.rs +++ b/src/controllers/category.rs @@ -2,14 +2,13 @@ use super::helpers::pagination::*; use crate::app::AppState; use crate::models::Category; use crate::schema::categories; -use crate::tasks::spawn_blocking; use crate::util::errors::AppResult; use crate::util::RequestUtils; use crate::views::{EncodableCategory, EncodableCategoryWithSubcategories}; use axum::extract::Path; use axum::Json; -use diesel::prelude::*; -use diesel_async::async_connection_wrapper::AsyncConnectionWrapper; +use diesel::QueryDsl; +use diesel_async::RunQueryDsl; use http::request::Parts; use serde_json::Value; @@ -20,86 +19,77 @@ pub async fn index(app: AppState, req: Parts) -> AppResult> { // to paginate this. let options = PaginationOptions::builder().gather(&req)?; - let conn = app.db_read().await?; - spawn_blocking(move || { - let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into(); + let mut conn = app.db_read().await?; - let query = req.query(); - let sort = query.get("sort").map_or("alpha", String::as_str); + let query = req.query(); + let sort = query.get("sort").map_or("alpha", String::as_str); - let offset = options.offset().unwrap_or_default(); + let offset = options.offset().unwrap_or_default(); - let categories = Category::toplevel(conn, sort, options.per_page, offset)?; - let categories = categories - .into_iter() - .map(Category::into) - .collect::>(); + let categories = Category::toplevel(&mut conn, sort, options.per_page, offset).await?; + let categories = categories + .into_iter() + .map(Category::into) + .collect::>(); - // Query for the total count of categories - let total = Category::count_toplevel(conn)?; + // Query for the total count of categories + let total = Category::count_toplevel(&mut conn).await?; - Ok(Json(json!({ - "categories": categories, - "meta": { "total": total }, - }))) - }) - .await + Ok(Json(json!({ + "categories": categories, + "meta": { "total": total }, + }))) } /// Handles the `GET /categories/:category_id` route. pub async fn show(state: AppState, Path(slug): Path) -> AppResult> { - let conn = state.db_read().await?; - spawn_blocking(move || { - let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into(); + let mut conn = state.db_read().await?; - let cat: Category = Category::by_slug(&slug).first(conn)?; - let subcats = cat - .subcategories(conn)? - .into_iter() - .map(Category::into) - .collect(); - let parents = cat - .parent_categories(conn)? - .into_iter() - .map(Category::into) - .collect(); + let cat: Category = Category::by_slug(&slug).first(&mut conn).await?; + let subcats = cat + .subcategories(&mut conn) + .await? + .into_iter() + .map(Category::into) + .collect(); + let parents = cat + .parent_categories(&mut conn) + .await? + .into_iter() + .map(Category::into) + .collect(); - let cat = EncodableCategory::from(cat); - let cat_with_subcats = EncodableCategoryWithSubcategories { - id: cat.id, - category: cat.category, - slug: cat.slug, - description: cat.description, - created_at: cat.created_at, - crates_cnt: cat.crates_cnt, - subcategories: subcats, - parent_categories: parents, - }; + let cat = EncodableCategory::from(cat); + let cat_with_subcats = EncodableCategoryWithSubcategories { + id: cat.id, + category: cat.category, + slug: cat.slug, + description: cat.description, + created_at: cat.created_at, + crates_cnt: cat.crates_cnt, + subcategories: subcats, + parent_categories: parents, + }; - Ok(Json(json!({ "category": cat_with_subcats }))) - }) - .await + Ok(Json(json!({ "category": cat_with_subcats }))) } /// Handles the `GET /category_slugs` route. pub async fn slugs(state: AppState) -> AppResult> { - let conn = state.db_read().await?; - spawn_blocking(move || { - let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into(); + let mut conn = state.db_read().await?; - let slugs: Vec = categories::table - .select((categories::slug, categories::slug, categories::description)) - .order(categories::slug) - .load(conn)?; + let slugs: Vec = categories::table + .select((categories::slug, categories::slug, categories::description)) + .order(categories::slug) + .load(&mut conn) + .await?; - #[derive(Serialize, Queryable)] - struct Slug { - id: String, - slug: String, - description: String, - } + #[derive(Serialize, Queryable)] + struct Slug { + id: String, + slug: String, + description: String, + } - Ok(Json(json!({ "category_slugs": slugs }))) - }) - .await + Ok(Json(json!({ "category_slugs": slugs }))) } diff --git a/src/controllers/summary.rs b/src/controllers/summary.rs index fdfc123beec..d92d520f30f 100644 --- a/src/controllers/summary.rs +++ b/src/controllers/summary.rs @@ -14,7 +14,14 @@ use serde_json::Value; /// Handles the `GET /summary` route. pub async fn summary(state: AppState) -> AppResult> { - let conn = state.db_read().await?; + let mut conn = state.db_read().await?; + + let popular_categories = Category::toplevel(&mut conn, "crates", 10, 0) + .await? + .into_iter() + .map(Category::into) + .collect::>(); + spawn_blocking(move || { let conn: &mut AsyncConnectionWrapper<_> = &mut conn.into(); @@ -116,11 +123,6 @@ pub async fn summary(state: AppState) -> AppResult> { .map(Keyword::into) .collect::>(); - let popular_categories = Category::toplevel(conn, "crates", 10, 0)? - .into_iter() - .map(Category::into) - .collect::>(); - Ok(Json(json!({ "num_downloads": num_downloads, "num_crates": num_crates, diff --git a/src/models/category.rs b/src/models/category.rs index 815c38ca3e7..a23ddfd4c2c 100644 --- a/src/models/category.rs +++ b/src/models/category.rs @@ -1,5 +1,9 @@ use chrono::NaiveDateTime; -use diesel::{self, *}; +use diesel::{ + delete, dsl, insert_into, sql_query, ExpressionMethods, QueryDsl, QueryResult, + TextExpressionMethods, +}; +use diesel_async::AsyncPgConnection; use crate::models::Crate; use crate::schema::*; @@ -47,6 +51,7 @@ impl Category { crate_id: i32, slugs: &[&str], ) -> QueryResult> { + use diesel::RunQueryDsl; conn.transaction(|conn| { let categories: Vec = categories::table .filter(categories::slug.eq_any(slugs)) @@ -76,20 +81,23 @@ impl Category { }) } - pub fn count_toplevel(conn: &mut impl Conn) -> QueryResult { + pub async fn count_toplevel(conn: &mut AsyncPgConnection) -> QueryResult { + use diesel_async::RunQueryDsl; categories::table .filter(categories::category.not_like("%::%")) .count() .get_result(conn) + .await } - pub fn toplevel( - conn: &mut impl Conn, + pub async fn toplevel( + conn: &mut AsyncPgConnection, sort: &str, limit: i64, offset: i64, ) -> QueryResult> { use diesel::sql_types::Int8; + use diesel_async::RunQueryDsl; let sort_sql = match sort { "crates" => "ORDER BY crates_cnt DESC", @@ -102,26 +110,34 @@ impl Category { .bind::(limit) .bind::(offset) .load(conn) + .await } - pub fn subcategories(&self, conn: &mut impl Conn) -> QueryResult> { + pub async fn subcategories(&self, conn: &mut AsyncPgConnection) -> QueryResult> { use diesel::sql_types::Text; + use diesel_async::RunQueryDsl; sql_query(include_str!("../subcategories.sql")) .bind::(&self.category) .load(conn) + .await } /// Gathers the parent categories from the top-level Category to the direct parent of this Category. /// Returns categories as a Vector in order of traversal, not including this Category. /// The intention is to be able to have slugs or parent categories arrayed in order, to /// offer the frontend, for examples, slugs to create links to each parent category in turn. - pub fn parent_categories(&self, conn: &mut impl Conn) -> QueryResult> { + pub async fn parent_categories( + &self, + conn: &mut AsyncPgConnection, + ) -> QueryResult> { use diesel::sql_types::Text; + use diesel_async::RunQueryDsl; sql_query(include_str!("../parent_categories.sql")) .bind::(&self.slug) .load(conn) + .await } } @@ -138,12 +154,17 @@ pub struct NewCategory<'a> { #[cfg(test)] mod tests { use super::*; - use crate::test_util::test_db_connection; + use crates_io_test_db::TestDatabase; + use diesel_async::AsyncConnection; + use diesel_async::RunQueryDsl; - #[test] - fn category_toplevel_excludes_subcategories() { + #[tokio::test] + async fn category_toplevel_excludes_subcategories() { use self::categories; - let (_test_db, conn) = &mut test_db_connection(); + + let test_db = TestDatabase::new(); + let mut conn = AsyncPgConnection::establish(test_db.url()).await.unwrap(); + insert_into(categories::table) .values(&vec![ ( @@ -159,10 +180,12 @@ mod tests { categories::slug.eq("cat1::sub"), ), ]) - .execute(conn) + .execute(&mut conn) + .await .unwrap(); - let cats = Category::toplevel(conn, "", 10, 0) + let cats = Category::toplevel(&mut conn, "", 10, 0) + .await .unwrap() .into_iter() .map(|c| c.category) @@ -171,8 +194,8 @@ mod tests { assert_eq!(expected, cats); } - #[test] - fn category_toplevel_orders_by_crates_cnt_when_sort_given() { + #[tokio::test] + async fn category_toplevel_orders_by_crates_cnt_when_sort_given() { use self::categories; let new_cat = |category, slug, crates_cnt| { @@ -183,17 +206,21 @@ mod tests { ) }; - let (_test_db, conn) = &mut test_db_connection(); + let test_db = TestDatabase::new(); + let mut conn = AsyncPgConnection::establish(test_db.url()).await.unwrap(); + insert_into(categories::table) .values(&vec![ new_cat("Cat 1", "cat1", 0), new_cat("Cat 2", "cat2", 2), new_cat("Cat 3", "cat3", 1), ]) - .execute(conn) + .execute(&mut conn) + .await .unwrap(); - let cats = Category::toplevel(conn, "crates", 10, 0) + let cats = Category::toplevel(&mut conn, "crates", 10, 0) + .await .unwrap() .into_iter() .map(|c| c.category) @@ -206,10 +233,13 @@ mod tests { assert_eq!(expected, cats); } - #[test] - fn category_toplevel_applies_limit_and_offset() { + #[tokio::test] + async fn category_toplevel_applies_limit_and_offset() { use self::categories; - let (_test_db, conn) = &mut test_db_connection(); + + let test_db = TestDatabase::new(); + let mut conn = AsyncPgConnection::establish(test_db.url()).await.unwrap(); + insert_into(categories::table) .values(&vec![ ( @@ -221,10 +251,12 @@ mod tests { categories::slug.eq("cat2"), ), ]) - .execute(conn) + .execute(&mut conn) + .await .unwrap(); - let cats = Category::toplevel(conn, "", 1, 0) + let cats = Category::toplevel(&mut conn, "", 1, 0) + .await .unwrap() .into_iter() .map(|c| c.category) @@ -232,7 +264,8 @@ mod tests { let expected = vec!["Cat 1".to_string()]; assert_eq!(expected, cats); - let cats = Category::toplevel(conn, "", 1, 1) + let cats = Category::toplevel(&mut conn, "", 1, 1) + .await .unwrap() .into_iter() .map(|c| c.category) @@ -241,8 +274,8 @@ mod tests { assert_eq!(expected, cats); } - #[test] - fn category_toplevel_includes_subcategories_in_crate_cnt() { + #[tokio::test] + async fn category_toplevel_includes_subcategories_in_crate_cnt() { use self::categories; let new_cat = |category, slug, crates_cnt| { @@ -253,7 +286,9 @@ mod tests { ) }; - let (_test_db, conn) = &mut test_db_connection(); + let test_db = TestDatabase::new(); + let mut conn = AsyncPgConnection::establish(test_db.url()).await.unwrap(); + insert_into(categories::table) .values(&vec![ new_cat("Cat 1", "cat1", 1), @@ -263,10 +298,12 @@ mod tests { new_cat("Cat 2::Sub 2", "cat2::sub2", 5), new_cat("Cat 3", "cat3", 6), ]) - .execute(conn) + .execute(&mut conn) + .await .unwrap(); - let cats = Category::toplevel(conn, "crates", 10, 0) + let cats = Category::toplevel(&mut conn, "crates", 10, 0) + .await .unwrap() .into_iter() .map(|c| (c.category, c.crates_cnt)) @@ -279,8 +316,8 @@ mod tests { assert_eq!(expected, cats); } - #[test] - fn category_toplevel_applies_limit_and_offset_after_grouping() { + #[tokio::test] + async fn category_toplevel_applies_limit_and_offset_after_grouping() { use self::categories; let new_cat = |category, slug, crates_cnt| { @@ -291,7 +328,9 @@ mod tests { ) }; - let (_test_db, conn) = &mut test_db_connection(); + let test_db = TestDatabase::new(); + let mut conn = AsyncPgConnection::establish(test_db.url()).await.unwrap(); + insert_into(categories::table) .values(&vec![ new_cat("Cat 1", "cat1", 1), @@ -301,10 +340,12 @@ mod tests { new_cat("Cat 2::Sub 2", "cat2::sub2", 5), new_cat("Cat 3", "cat3", 6), ]) - .execute(conn) + .execute(&mut conn) + .await .unwrap(); - let cats = Category::toplevel(conn, "crates", 2, 0) + let cats = Category::toplevel(&mut conn, "crates", 2, 0) + .await .unwrap() .into_iter() .map(|c| (c.category, c.crates_cnt)) @@ -312,7 +353,8 @@ mod tests { let expected = vec![("Cat 2".to_string(), 12), ("Cat 3".to_string(), 6)]; assert_eq!(expected, cats); - let cats = Category::toplevel(conn, "crates", 2, 1) + let cats = Category::toplevel(&mut conn, "crates", 2, 1) + .await .unwrap() .into_iter() .map(|c| (c.category, c.crates_cnt)) @@ -321,8 +363,8 @@ mod tests { assert_eq!(expected, cats); } - #[test] - fn category_parent_categories_includes_path_to_node_with_count() { + #[tokio::test] + async fn category_parent_categories_includes_path_to_node_with_count() { use self::categories; let new_cat = |category, slug, crates_cnt| { @@ -333,7 +375,9 @@ mod tests { ) }; - let (_test_db, conn) = &mut test_db_connection(); + let test_db = TestDatabase::new(); + let mut conn = AsyncPgConnection::establish(test_db.url()).await.unwrap(); + insert_into(categories::table) .values(&vec![ new_cat("Cat 1", "cat1", 1), @@ -345,12 +389,17 @@ mod tests { new_cat("Cat 2::Sub 2", "cat2::sub2", 5), new_cat("Cat 3", "cat3", 200), ]) - .execute(conn) + .execute(&mut conn) + .await + .unwrap(); + + let cat: Category = Category::by_slug("cat1::sub1") + .first(&mut conn) + .await .unwrap(); - let cat: Category = Category::by_slug("cat1::sub1").first(conn).unwrap(); - let subcats = cat.subcategories(conn).unwrap(); - let parents = cat.parent_categories(conn).unwrap(); + let subcats = cat.subcategories(&mut conn).await.unwrap(); + let parents = cat.parent_categories(&mut conn).await.unwrap(); assert_eq!(parents.len(), 1); assert_eq!(parents[0].slug, "cat1");