From c24b0caeaf325ea2933236a4cd22bcd84a6cee5d Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Tue, 20 May 2025 15:32:54 +0200 Subject: [PATCH] Add a API entry point to get all tags --- Cargo.lock | 42 ++++++++++-------------------- backend/src/app.rs | 3 +-- backend/src/data/db/recipe.rs | 42 ++++++++++++++---------------- backend/src/services/ron/mod.rs | 3 +-- backend/src/services/ron/recipe.rs | 13 +++++++++ common/src/ron_api.rs | 12 +++------ frontend/src/pages/recipe_edit.rs | 3 ++- 7 files changed, 55 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5afc5d7..f1c5419 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2890,9 +2890,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2903,9 +2903,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64", "bytes", @@ -2938,9 +2938,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", @@ -2951,9 +2951,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", @@ -2970,16 +2970,15 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn", - "tempfile", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", @@ -3020,9 +3019,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", @@ -3058,9 +3057,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -3248,19 +3247,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "tempfile" -version = "3.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - [[package]] name = "tendril" version = "0.4.3" diff --git a/backend/src/app.rs b/backend/src/app.rs index d720b70..7b4b10c 100644 --- a/backend/src/app.rs +++ b/backend/src/app.rs @@ -155,8 +155,7 @@ pub fn make_service( .post(services::ron::recipe::add_tags) .delete(services::ron::recipe::rm_tags), ) - // TODO - // .route("/recipe/tags".get(services::ron::recipe::get_all_tags)) + .route("/recipe/tags", get(services::ron::recipe::get_all_tags)) .route( "/recipe/{id}/difficulty", patch(services::ron::recipe::set_difficulty), diff --git a/backend/src/data/db/recipe.rs b/backend/src/data/db/recipe.rs index b3e21c8..4ec6259 100644 --- a/backend/src/data/db/recipe.rs +++ b/backend/src/data/db/recipe.rs @@ -1,3 +1,5 @@ +use std::u32; + use chrono::prelude::*; use common::ron_api::Difficulty; use itertools::Itertools; @@ -380,29 +382,20 @@ WHERE [Recipe].[user_id] = $1 .map_err(DBError::from) } - pub async fn get_all_tags(&self) -> Result> { + pub async fn get_all_tags(&self, lang: &str, max_number: Option) -> Result> { sqlx::query_scalar( r#" -SELECT [name] FROM [Tag] -ORDER BY [name] - "#, - ) - .fetch_all(&self.pool) - .await - .map_err(DBError::from) - } - - pub async fn get_all_tags_by_lang(&self, lang: &str) -> Result> { - sqlx::query_scalar( - r#" -SELECT DISTINCT [name] FROM [Tag] -INNER JOIN [RecipeTag] ON [RecipeTag].[tag_id] = [Tag].[id] -INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTag].[recipe_id] +SELECT [name], COUNT([name]) as [nb_used] FROM [Tag] + INNER JOIN [RecipeTag] ON [RecipeTag].[tag_id] = [Tag].[id] + INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTag].[recipe_id] WHERE [Recipe].[lang] = $1 -ORDER BY [name] +GROUP BY [Tag].[name] +ORDER BY [nb_used] DESC, [name] +LIMIT $2 "#, ) .bind(lang) + .bind(max_number.unwrap_or(u32::MAX)) .fetch_all(&self.pool) .await .map_err(DBError::from) @@ -1078,9 +1071,15 @@ VALUES assert_eq!(connection.get_recipes_tags(recipe_id_1).await?, tags_1); assert_eq!(connection.get_recipes_tags(recipe_id_2).await?, tags_2); - assert_eq!(connection.get_all_tags().await?, ["abc", "def", "xyz"]); + assert_eq!( + connection.get_all_tags("en", None).await?, + ["abc", "xyz", "def"] + ); connection.rm_recipe_tags(recipe_id_2, &["abc"]).await?; - assert_eq!(connection.get_all_tags().await?, ["abc", "def", "xyz"]); + assert_eq!( + connection.get_all_tags("en", None).await?, + ["xyz", "abc", "def"] // Most used tags come first. + ); assert_eq!( connection.get_recipes_tags(recipe_id_1).await?, @@ -1098,13 +1097,12 @@ VALUES connection.get_recipes_tags(recipe_id_2).await?, ["def", "xyz"] ); - assert_eq!(connection.get_all_tags().await?, ["def", "xyz"]); - assert_eq!(connection.get_all_tags_by_lang("en").await?, ["def", "xyz"]); + assert_eq!(connection.get_all_tags("en", None).await?, ["xyz", "def"]); connection.rm_recipe_tags(recipe_id_1, &tags_1).await?; connection.rm_recipe_tags(recipe_id_2, &tags_2).await?; - assert!(connection.get_all_tags().await?.is_empty()); + assert!(connection.get_all_tags("en", None).await?.is_empty()); Ok(()) } diff --git a/backend/src/services/ron/mod.rs b/backend/src/services/ron/mod.rs index e48f7b4..4d5bafe 100644 --- a/backend/src/services/ron/mod.rs +++ b/backend/src/services/ron/mod.rs @@ -1,11 +1,10 @@ use axum::{ debug_handler, - extract::{Extension, Path, Query, State}, + extract::{Extension, Path, State}, http::{HeaderMap, StatusCode}, response::{IntoResponse, Result}, }; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; -use common::ron_api; use crate::{ app::Context, diff --git a/backend/src/services/ron/recipe.rs b/backend/src/services/ron/recipe.rs index 52d0473..09ba0f4 100644 --- a/backend/src/services/ron/recipe.rs +++ b/backend/src/services/ron/recipe.rs @@ -88,6 +88,19 @@ pub async fn get_tags( )) } +#[debug_handler] +pub async fn get_all_tags( + State(connection): State, + Extension(context): Extension, + nb_max_tags: Query>, + lang: Query>, +) -> Result { + let lang = lang.0.unwrap_or(context.tr.current_lang_code().to_string()); + Ok(ron_response_ok( + connection.get_all_tags(&lang, nb_max_tags.0).await?, + )) +} + #[debug_handler] pub async fn add_tags( State(connection): State, diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index a741552..dd9c78e 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -1,6 +1,7 @@ use chrono::NaiveDate; use ron::ser::{PrettyConfig, to_string_pretty}; use serde::{Deserialize, Serialize}; +use strum::FromRepr; /*** Generic types ***/ @@ -20,7 +21,7 @@ pub struct DateRange { /*** Recipe ***/ #[repr(u32)] -#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)] +#[derive(Serialize, Deserialize, FromRepr, Clone, Copy, PartialEq, Debug)] pub enum Difficulty { Unknown = 0, Easy = 1, @@ -29,14 +30,9 @@ pub enum Difficulty { } impl TryFrom for Difficulty { - type Error = &'static str; + type Error = String; fn try_from(value: u32) -> Result { - Ok(match value { - 1 => Self::Easy, - 2 => Self::Medium, - 3 => Self::Hard, - _ => Self::Unknown, - }) + Difficulty::from_repr(value).ok_or(format!("Unable to convert difficulty from {}", value)) } } diff --git a/frontend/src/pages/recipe_edit.rs b/frontend/src/pages/recipe_edit.rs index 2ff7491..66ac41b 100644 --- a/frontend/src/pages/recipe_edit.rs +++ b/frontend/src/pages/recipe_edit.rs @@ -134,7 +134,8 @@ pub fn setup_page(recipe_id: i64) { if difficulty.value() != current_difficulty { current_difficulty = difficulty.value(); let difficulty = - ron_api::Difficulty::try_from(difficulty.value().parse::().unwrap()); + ron_api::Difficulty::from_repr(difficulty.value().parse::().unwrap()) + .unwrap_or(ron_api::Difficulty::Unknown); spawn_local(async move { let _ = request::patch::<(), _>( &format!("recipe/{recipe_id}/difficulty"),