From adc365cf44d2fb89eee253e1da93545308988f54 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 30 Dec 2021 01:39:36 -0500 Subject: [PATCH] list assigned category query builder --- ...t_assigned_tts_categories_query_builder.rs | 255 ++++++++++++++++++ .../src/database/query_builders/mod.rs | 3 +- .../endpoints/categories/edit_category.rs | 2 +- .../endpoints/tts/get_tts_model.rs | 23 +- 4 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 crates/app/storyteller_web/src/database/query_builders/list_assigned_tts_categories_query_builder.rs diff --git a/crates/app/storyteller_web/src/database/query_builders/list_assigned_tts_categories_query_builder.rs b/crates/app/storyteller_web/src/database/query_builders/list_assigned_tts_categories_query_builder.rs new file mode 100644 index 0000000000..a58fe8e79f --- /dev/null +++ b/crates/app/storyteller_web/src/database/query_builders/list_assigned_tts_categories_query_builder.rs @@ -0,0 +1,255 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use crate::database::enums::record_visibility::RecordVisibility; +use crate::database::helpers::boolean_converters::{i8_to_bool, nullable_i8_to_optional_bool}; +use crate::util::anyhow_result::AnyhowResult; +use log::{warn, info}; +use sqlx::MySqlPool; + +/// List of assigned categories for a TTS model +/// This is *NOT* paginated, as that defeats the purpose of having a category system. +#[derive(Serialize)] +pub struct AssignedCategoryList { + pub categories: Vec, +} + +#[derive(Serialize)] +pub struct AssignedCategory { + pub category_token: String, + + pub model_type: String, // TODO: ENUM + + pub maybe_super_category_token: Option, + + pub can_directly_have_models: bool, + pub can_have_subcategories: bool, + pub can_only_mods_apply: bool, + + pub name: String, + pub maybe_dropdown_name: Option, + + pub category_creator_user_token: Option, + pub category_creator_username: Option, + pub category_creator_display_name: Option, + pub category_creator_gravatar_hash: Option, + + // Moderator fields + pub is_mod_approved: Option, + pub maybe_mod_comments: Option, + //pub maybe_mod_user_token: Option, + //pub maybe_mod_username: Option, + //pub maybe_mod_display_name: Option, + + pub category_created_at: DateTime, + pub category_updated_at: DateTime, + pub category_deleted_at: Option>, + + pub category_assignment_created_at: DateTime, + pub category_assignment_updated_at: DateTime, + pub category_assignment_deleted_at: Option>, +} + +/// Query Builder for listing categories +/// These are very difficult queries, so the builder helps for +/// testability, construction, and correctness. +pub struct ListAssignedTtsCategoriesQueryBuilder { + // REQUIRED. + scope_tts_model_token: String, + + // The public lists won't include unapproved, but mod views + // will want to see them. + show_unapproved: bool, + + // The public lists won't include deleted models, but mod views + // will want to see them. + show_deleted: bool, +} + +impl ListAssignedTtsCategoriesQueryBuilder { + pub fn for_model_token(model_token: &str) -> Self { + Self { + scope_tts_model_token: model_token.to_string(), + show_unapproved: false, + show_deleted: false, + } + } + + pub fn show_unapproved(mut self, show_unapproved: bool) -> Self { + self.show_unapproved = show_unapproved; + self + } + + pub fn show_deleted(mut self, show_deleted: bool) -> Self { + self.show_deleted = show_deleted; + self + } + + /// Perform the query based on the set predicates. + pub async fn perform_query( + &self, + mysql_pool: &MySqlPool + ) -> AnyhowResult { + + let internal_results = self.perform_internal_query(mysql_pool).await?; + + let categories = internal_results + .iter() + .map(|c| { + AssignedCategory { + category_token: c.category_token.clone(), + model_type: c.model_type.clone(), + maybe_super_category_token: c.maybe_super_category_token.clone(), + can_directly_have_models: i8_to_bool(c.can_directly_have_models), + can_have_subcategories: i8_to_bool(c.can_have_subcategories), + can_only_mods_apply: i8_to_bool(c.can_only_mods_apply), + name: c.name.clone(), + maybe_dropdown_name: c.maybe_dropdown_name.clone(), + category_creator_user_token: c.category_creator_user_token.clone(), + category_creator_username: c.category_creator_username.clone(), + category_creator_display_name: c.category_creator_display_name.clone(), + category_creator_gravatar_hash: c.category_creator_gravatar_hash.clone(), + is_mod_approved: nullable_i8_to_optional_bool(c.is_mod_approved), + maybe_mod_comments: c.maybe_mod_comments.clone(), + category_created_at: c.category_created_at, + category_updated_at: c.category_updated_at, + category_deleted_at: c.category_deleted_at.clone(), + category_assignment_created_at: c.category_assignment_created_at, + category_assignment_updated_at: c.category_assignment_updated_at, + category_assignment_deleted_at: c.category_assignment_deleted_at.clone(), + } + }) + .collect::>(); + + Ok(AssignedCategoryList { + categories, + }) + } + + /// Perform the query based on the set predicates. + async fn perform_internal_query( + &self, + mysql_pool: &MySqlPool + ) -> AnyhowResult> { + + let query = self.build_query_string(); + let mut query = sqlx::query_as::<_, RawInternalCategoryRecord>(&query); + + // NB: The following bindings must match the order of the query builder !! + + // NB: Model token is always used for scoping. + query = query.bind(&self.scope_tts_model_token); + + let mut results = query.fetch_all(mysql_pool) + .await?; + + Ok(results) + } + + fn build_query_string(&self) -> String { + // TODO: I haven't figured out how to get field name disambiguation and type coercion working here. + // (1) tts_results.creator_set_visibility `creator_set_visibility: crate::database::enums::record_visibility::RecordVisibility`, + // Query error: no column found for name: creator_set_visibility + // (2) creator_set_visibility `creator_set_visibility: crate::database::enums::record_visibility::RecordVisibility`, + // Column 'creator_set_visibility' in field list is ambiguous + + // TODO/NB: Unfortunately SQLx can't statically typecheck this query + let mut query = r#" +SELECT + model_categories.token as category_token, + + model_categories.model_type, + + model_categories.maybe_super_category_token, + + model_categories.can_directly_have_models, + model_categories.can_have_subcategories, + model_categories.can_only_mods_apply, + + model_categories.name, + model_categories.maybe_dropdown_name, + + users.token as category_creator_user_token, + users.username as category_creator_username, + users.display_name as category_creator_display_name, + users.email_gravatar_hash as category_creator_gravatar_hash, + + model_categories.is_mod_approved, + model_categories.maybe_mod_comments, + + model_categories.created_at as category_created_at, + model_categories.updated_at as category_updated_at, + model_categories.deleted_at as category_deleted_at, + + tts_category_assignments.created_at as category_assignment_created_at, + tts_category_assignments.updated_at as category_assignment_updated_at, + tts_category_assignments.deleted_at as category_assignment_deleted_at + +FROM model_categories +JOIN tts_category_assignments + ON tts_category_assignments.category_token = model_categories.token +LEFT OUTER JOIN users + ON model_categories.creator_user_token = users.token + "#.to_string(); + + query.push_str(&self.build_predicates()); + query + } + + pub fn build_predicates(&self) -> String { + let mut query = "".to_string(); + + // Scoping to tts model is required. + query.push_str(" WHERE model_categories.model_token = ?"); + query.push_str(" AND model_categories.model_type = 'tts'"); + + // We also don't care to ever show soft-deleted assignments + query.push_str(" AND tts_category_assignments.deleted_at IS NULL"); + + if !self.show_unapproved { + query.push_str(" AND model_categories.is_mod_approved IS TRUE"); + } + + if !self.show_deleted { + query.push_str(" AND model_categories.deleted_at IS NULL"); + } + + query + } +} + +#[derive(sqlx::FromRow)] +pub struct RawInternalCategoryRecord { + pub category_token: String, + + pub model_type: String, // TODO: ENUM + + pub maybe_super_category_token: Option, + + pub can_directly_have_models: i8, + pub can_have_subcategories: i8, + pub can_only_mods_apply: i8, + + pub name: String, + pub maybe_dropdown_name: Option, + + pub category_creator_user_token: Option, + pub category_creator_username: Option, + pub category_creator_display_name: Option, + pub category_creator_gravatar_hash: Option, + + // Moderator fields + pub is_mod_approved: Option, + pub maybe_mod_comments: Option, + + pub category_created_at: DateTime, + pub category_updated_at: DateTime, + pub category_deleted_at: Option>, + + pub category_assignment_created_at: DateTime, + pub category_assignment_updated_at: DateTime, + pub category_assignment_deleted_at: Option>, +} + +#[cfg(test)] +mod tests { +} diff --git a/crates/app/storyteller_web/src/database/query_builders/mod.rs b/crates/app/storyteller_web/src/database/query_builders/mod.rs index 072361672c..9cd5018348 100644 --- a/crates/app/storyteller_web/src/database/query_builders/mod.rs +++ b/crates/app/storyteller_web/src/database/query_builders/mod.rs @@ -1,4 +1,5 @@ +pub mod list_assigned_tts_categories_query_builder; pub mod list_categories_query_builder; pub mod list_tts_inference_results_query_builder; pub mod list_users_query_builder; -pub mod list_w2l_inference_results_query_builder; +pub mod list_w2l_inference_results_query_builder; \ No newline at end of file diff --git a/crates/app/storyteller_web/src/http_server/endpoints/categories/edit_category.rs b/crates/app/storyteller_web/src/http_server/endpoints/categories/edit_category.rs index 91dfa56a8e..cb8bad069d 100644 --- a/crates/app/storyteller_web/src/http_server/endpoints/categories/edit_category.rs +++ b/crates/app/storyteller_web/src/http_server/endpoints/categories/edit_category.rs @@ -89,7 +89,7 @@ impl ResponseError for EditCategoryError { } } -// NB: Not using DeriveMore since Clion doesn't understand it. +// NB: Not using derive_more::Display since Clion doesn't understand it. impl fmt::Display for EditCategoryError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) diff --git a/crates/app/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs b/crates/app/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs index baf749499a..d062308624 100644 --- a/crates/app/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs +++ b/crates/app/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs @@ -1,39 +1,45 @@ -use actix_http::Error; use actix_http::http::header; -use actix_web::cookie::Cookie; use actix_web::HttpResponseBuilder; +use actix_web::cookie::Cookie; use actix_web::error::ResponseError; use actix_web::http::StatusCode; use actix_web::web::Path; use actix_web::{Responder, web, HttpResponse, error, HttpRequest, HttpMessage}; +use anyhow::Error; use chrono::{DateTime, Utc}; use crate::AnyhowResult; use crate::database::queries::query_tts_model::TtsModelRecordForResponse; use crate::database::queries::query_tts_model::select_tts_model_by_token; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use derive_more::{Display, Error}; use log::{info, warn, log}; use regex::Regex; use sqlx::MySqlPool; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; +use std::fmt; use std::sync::Arc; +// =============== Request =============== + /// For the URL PathInfo #[derive(Deserialize)] pub struct GetTtsModelPathInfo { token: String, } +// =============== Success Response =============== + #[derive(Serialize)] pub struct GetTtsModelSuccessResponse { pub success: bool, pub model: TtsModelRecordForResponse, } -#[derive(Debug, Display)] +// =============== Error Response =============== + +#[derive(Debug)] pub enum GetTtsModelError { ServerError, NotFound, @@ -57,6 +63,15 @@ impl ResponseError for GetTtsModelError { } } +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for GetTtsModelError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +// =============== Handler =============== + pub async fn get_tts_model_handler( http_request: HttpRequest, path: Path,