diff --git a/crates/_types/enums/src/by_table/generic_synthetic_ids/id_category.rs b/crates/_types/enums/src/by_table/generic_synthetic_ids/id_category.rs index 4bb8a5452b..716a214ff1 100644 --- a/crates/_types/enums/src/by_table/generic_synthetic_ids/id_category.rs +++ b/crates/_types/enums/src/by_table/generic_synthetic_ids/id_category.rs @@ -20,6 +20,10 @@ pub enum IdCategory { /// Results from lipsync animations (which may live in the media_files table) #[serde(rename = "lipsync_animation")] LipsyncAnimation, + + /// Zs dataset which lives in the zs_voice_datasets table + #[serde(rename = "zs_dataset")] + ZsDataset, } // TODO(bt, 2022-12-21): This desperately needs MySQL integration tests! @@ -32,6 +36,7 @@ impl IdCategory { match self { Self::MediaFile => "media_file", Self::LipsyncAnimation => "lipsync_animation", + Self::ZsDataset => "zs_dataset", } } @@ -39,6 +44,7 @@ impl IdCategory { match value { "media_file" => Ok(Self::MediaFile), "lipsync_animation" => Ok(Self::LipsyncAnimation), + "zs_dataset" => Ok(Self::ZsDataset), _ => Err(format!("invalid value: {:?}", value)), } } @@ -49,6 +55,7 @@ impl IdCategory { BTreeSet::from([ Self::MediaFile, Self::LipsyncAnimation, + Self::ZsDataset, ]) } } @@ -68,21 +75,24 @@ mod tests { fn to_str() { assert_eq!(IdCategory::MediaFile.to_str(), "media_file"); assert_eq!(IdCategory::LipsyncAnimation.to_str(), "lipsync_animation"); + assert_eq!(IdCategory::ZsDataset.to_str(), "zs_dataset"); } #[test] fn from_str() { assert_eq!(IdCategory::from_str("media_file").unwrap(), IdCategory::MediaFile); assert_eq!(IdCategory::from_str("lipsync_animation").unwrap(), IdCategory::LipsyncAnimation); + assert_eq!(IdCategory::from_str("zs_dataset").unwrap(), IdCategory::ZsDataset); } #[test] fn all_variants() { // Static check let mut variants = IdCategory::all_variants(); - assert_eq!(variants.len(), 2); + assert_eq!(variants.len(), 3); assert_eq!(variants.pop_first(), Some(IdCategory::MediaFile)); assert_eq!(variants.pop_first(), Some(IdCategory::LipsyncAnimation)); + assert_eq!(variants.pop_first(), Some(IdCategory::ZsDataset)); assert_eq!(variants.pop_first(), None); // Generated check diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/create_dataset.rs b/crates/lib/mysql_queries/src/queries/voice_designer/create_dataset.rs index 238389aac5..c573bc9104 100644 --- a/crates/lib/mysql_queries/src/queries/voice_designer/create_dataset.rs +++ b/crates/lib/mysql_queries/src/queries/voice_designer/create_dataset.rs @@ -1,9 +1,12 @@ use anyhow::anyhow; use sqlx::MySqlPool; use enums::common::visibility::Visibility; +use enums::by_table::generic_synthetic_ids::id_category::IdCategory; use errors::AnyhowResult; use tokens::tokens::dataset::ZsDatasetToken; +use tokens::users::user::UserToken; +use crate::queries::generic_synthetic_ids::transactional_increment_generic_synthetic_id::transactional_increment_generic_synthetic_id; pub struct CreateDatasetArgs<'a> { pub dataset_token: &'a ZsDatasetToken, @@ -24,7 +27,20 @@ pub async fn create_dataset(args: CreateDatasetArgs<'_>) -> AnyhowResult<()>{ // ie, how are users anonymous if user session is validated before we reach here // * could creator token be null? // TODO: enforce checks for idempotency token + let mut maybe_creator_synthetic_id : Option = None; + let mut transaction = args.mysql_pool.begin().await?; + if let Some(creator_user_token) = args.maybe_creator_user_token.as_deref() { + let user_token = UserToken::new_from_str(creator_user_token); + + let next_zs_dataset_synthetic_id = transactional_increment_generic_synthetic_id( + &user_token, + IdCategory::ZsDataset, + &mut transaction + ).await?; + + maybe_creator_synthetic_id = Some(next_zs_dataset_synthetic_id); + } let query_result = sqlx::query!( r#" INSERT INTO zs_voice_datasets @@ -34,16 +50,19 @@ pub async fn create_dataset(args: CreateDatasetArgs<'_>) -> AnyhowResult<()>{ maybe_creator_user_token = ?, creator_ip_address = ?, creator_set_visibility = ?, - maybe_mod_user_token = ? + maybe_mod_user_token = ?, + maybe_creator_synthetic_id = ? "#, args.dataset_token.as_str(), args.dataset_title, args.maybe_creator_user_token, args.creator_ip_address, args.creator_set_visibility.to_str(), - args.maybe_mod_user_token + args.maybe_mod_user_token, + maybe_creator_synthetic_id ).execute(args.mysql_pool).await; - + // TODO(Kasisnu): This should probably rollback + transaction.commit().await?; match query_result { Ok(_) => Ok(()), Err(err) => Err(anyhow!("zs dataset creation error: {:?}", err)), diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/get_dataset.rs b/crates/lib/mysql_queries/src/queries/voice_designer/get_dataset.rs new file mode 100644 index 0000000000..0587b5848e --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/voice_designer/get_dataset.rs @@ -0,0 +1,90 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use log::error; +use sqlx::{MySql, MySqlPool}; +use sqlx::pool::PoolConnection; + +use enums::by_table::media_uploads::media_upload_type::MediaUploadType; +use enums::common::visibility::Visibility; +use errors::AnyhowResult; +use tokens::files::media_upload::MediaUploadToken; +use tokens::tokens::dataset::ZsDatasetToken; + + +pub struct ZsDataset { + pub token: ZsDatasetToken, + pub title: String, + pub ietf_language_tag: String, + pub ietf_primary_language_subtag: String, + pub maybe_creator_user_token: Option, +} + +pub async fn get_dataset_by_token( + dataset_token: &ZsDatasetToken, + mysql_pool: &MySqlPool, +) -> AnyhowResult> { + let mut connection = mysql_pool.acquire().await?; + get_dataset_by_token_with_connection( + dataset_token, + &mut connection + ).await +} + +pub async fn get_dataset_by_token_with_connection( + dataset_token: &ZsDatasetToken, + mysql_connection: &mut PoolConnection, +) -> AnyhowResult> { + let maybe_result = sqlx::query_as!( + RawDataset, + r#" + SELECT + zd.token as `token: tokens::tokens::dataset::ZsDatasetToken`, + zd.title, + zd.ietf_language_tag, + zd.ietf_primary_language_subtag, + zd.maybe_creator_user_token, + zd.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility` + FROM zs_voice_datasets as zd + WHERE + zd.token = ? + AND zd.user_deleted_at IS NULL + AND zd.mod_deleted_at IS NULL + "#, + dataset_token.as_str() + ) + .fetch_one(mysql_connection).await; + + let record = match maybe_result { + Ok(record) => record, + Err(sqlx::Error::RowNotFound) => { + return Ok(None); + }, + Err(err) => { + error!( + "Error fetching dataset by token: {:?}", + err + ); + return Err(anyhow!( + "Error fetching dataset by token: {:?}", + err + )); + } + }; + + Ok(Some(ZsDataset { + token: record.token, + title: record.title, + ietf_language_tag: record.ietf_language_tag, + ietf_primary_language_subtag: record.ietf_primary_language_subtag, + maybe_creator_user_token: record.maybe_creator_user_token, + })) +} +#[derive(Serialize)] +pub struct RawDataset { + token: ZsDatasetToken, + title: String, + ietf_language_tag: String, + ietf_primary_language_subtag: String, + maybe_creator_user_token: Option, + creator_set_visibility: Visibility, +} \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs b/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs index df583c8a54..03143d407d 100644 --- a/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs +++ b/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs @@ -1 +1,3 @@ -pub mod create_dataset; \ No newline at end of file +pub mod create_dataset; +pub mod update_dataset; +pub mod get_dataset; \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/update_dataset.rs b/crates/lib/mysql_queries/src/queries/voice_designer/update_dataset.rs new file mode 100644 index 0000000000..b89425f96f --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/voice_designer/update_dataset.rs @@ -0,0 +1,72 @@ +use anyhow::anyhow; +use sqlx::MySqlPool; +use enums::common::visibility::Visibility; +use enums::by_table::generic_synthetic_ids::id_category::IdCategory; + +use errors::AnyhowResult; +use tokens::tokens::dataset::ZsDatasetToken; +use tokens::users::user::UserToken; +use crate::queries::generic_synthetic_ids::transactional_increment_generic_synthetic_id::transactional_increment_generic_synthetic_id; + +pub struct UpdateDatasetArgs<'a> { + pub dataset_token: &'a ZsDatasetToken, + pub dataset_title: Option<&'a str>, + pub maybe_creator_user_token: Option<&'a str>, + pub creator_ip_address: &'a str, + pub creator_set_visibility: &'a Visibility, + pub maybe_mod_user_token: Option<&'a str>, + pub ietf_language_tag: Option<&'a str>, + pub ietf_primary_language_subtag: Option<&'a str>, + pub mysql_pool: &'a MySqlPool +} + +pub async fn update_dataset(args: UpdateDatasetArgs<'_>) -> AnyhowResult<()>{ + + // TODO: enforce checks for idempotency token + let mut maybe_creator_synthetic_id : Option = None; + + let mut transaction = args.mysql_pool.begin().await?; + if let Some(creator_user_token) = args.maybe_creator_user_token.as_deref() { + let user_token = UserToken::new_from_str(creator_user_token); + + let next_zs_dataset_synthetic_id = transactional_increment_generic_synthetic_id( + &user_token, + IdCategory::ZsDataset, + &mut transaction + ).await?; + + maybe_creator_synthetic_id = Some(next_zs_dataset_synthetic_id); + } + let query_result = sqlx::query!( + r#" + UPDATE zs_voice_datasets + SET + title = ?, + maybe_creator_user_token = ?, + creator_ip_address = ?, + creator_set_visibility = ?, + maybe_mod_user_token = ?, + maybe_creator_synthetic_id = ?, + + + version = version + 1 + WHERE token = ? + LIMIT 1 + "#, + args.dataset_title, + args.maybe_creator_user_token, + args.creator_ip_address, + args.creator_set_visibility.to_str(), + args.maybe_mod_user_token, + maybe_creator_synthetic_id, + args.dataset_token.as_str(), + ).execute(args.mysql_pool).await; + // TODO(Kasisnu): This should probably rollback + transaction.commit().await?; + match query_result { + Ok(_) => Ok(()), + Err(err) => Err(anyhow!("zs dataset creation error: {:?}", err)), + } + +} + diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/create_dataset.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/create_dataset.rs index 5e073989f7..5ca2841537 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/create_dataset.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/create_dataset.rs @@ -18,6 +18,7 @@ use crate::server_state::ServerState; #[derive(Deserialize)] pub struct CreateDatasetRequest { pub title: String, + pub creator_set_visibility: Option, pub idempotency_token: Option, @@ -26,7 +27,7 @@ pub struct CreateDatasetRequest { #[derive(Serialize)] pub struct CreateDatasetResponse { pub success: bool, - pub token: Option, + pub token: Option, } // =============== Error Response =============== @@ -111,7 +112,7 @@ pub async fn create_dataset_handler(http_request: HttpRequest, request: web::Jso let response = CreateDatasetResponse { success: true, - token: Some(dataset_token.to_string()), + token: Some(dataset_token), }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/update_dataset.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/update_dataset.rs index 18473032d6..53113430e6 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/update_dataset.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/update_dataset.rs @@ -1,14 +1,200 @@ - +use std::fmt; use std::sync::Arc; use actix_web::{HttpRequest, HttpResponse, web}; use actix_web::error::ResponseError; use actix_web::http::StatusCode; +use actix_web::web::Path; +use log::{error, log, warn}; +use enums::common::visibility::Visibility; -use log::{info, warn}; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use mysql_queries::queries::voice_designer::create_dataset::{create_dataset, CreateDatasetArgs}; +use mysql_queries::queries::voice_designer::get_dataset::get_dataset_by_token; +use mysql_queries::queries::voice_designer::update_dataset::{update_dataset, UpdateDatasetArgs}; +use tokens::tokens::dataset::ZsDatasetToken; +use crate::server_state::ServerState; -pub async fn update_dataset(dataset_token: web::Path) -> HttpResponse { - // Implementation for updating a dataset - HttpResponse::Ok().json(format!("Dataset {} updated successfully", dataset_token)) +#[derive(Deserialize)] +pub struct UpdateDatasetRequest { + // ========== Author + Moderator options ========== + + pub title: Option, + pub creator_set_visibility: Option, + + // NB: We calculate 'ietf_primary_language_subtag' from this value. + pub ietf_language_tag: Option, +} + +#[derive(Serialize)] +pub struct UpdateDatasetResponse { + pub success: bool, +} + +/// For the URL PathInfo +#[derive(Deserialize)] +pub struct UpdateDatasetPathInfo { + dataset_token: String, +} + +// =============== Error Response =============== + +#[derive(Debug, Serialize)] +pub enum UpdateDatasetError { + BadInput(String), + NotFound, + NotAuthorized, + ServerError, +} + +impl ResponseError for UpdateDatasetError { + fn status_code(&self) -> StatusCode { + match *self { + UpdateDatasetError::BadInput(_) => StatusCode::BAD_REQUEST, + UpdateDatasetError::NotFound => StatusCode::NOT_FOUND, + UpdateDatasetError::NotAuthorized => StatusCode::UNAUTHORIZED, + UpdateDatasetError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for UpdateDatasetError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +// =============== Handler =============== + +pub async fn update_dataset_handler( + http_request: HttpRequest, + path: Path, + request: web::Json, + server_state: web::Data>) -> Result +{ + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + UpdateDatasetError::ServerError + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => { + warn!("not logged in"); + return Err(UpdateDatasetError::NotAuthorized); + } + }; + + + let dataset_token = path.dataset_token.clone(); + let dataset_lookup_result = get_dataset_by_token( + &ZsDatasetToken::new(dataset_token.clone()), + &server_state.mysql_pool, + ).await; + + let dataset = match dataset_lookup_result { + Ok(Some(dataset)) => dataset, + Ok(None) => { + warn!("Dataset not found: {:?}", dataset_token); + return Err(UpdateDatasetError::NotFound); + }, + Err(err) => { + warn!("Error looking up dataset: {:?}", err); + return Err(UpdateDatasetError::ServerError); + } + }; + + let mut title = None; + let mut ietf_language_tag = None; + let mut ietf_primary_language_subtag = None; + let mut creator_set_visibility = Visibility::Public; + + if let Some(payload) = request.title.as_deref() { + if user_input_common::check_for_slurs::contains_slurs(payload) { + return Err(UpdateDatasetError::BadInput("title contains slurs".to_string())); + } + + title = Some(payload.trim().to_string()); + } + + if let Some(tag) = request.ietf_language_tag.as_deref() { + // eg. en, en-US, es-419, ja-JP, etc. + let maybe_full_canonical_tag = container_common::i18n::supported_languages_for_models::get_canonicalized_language_tag_for_model(tag); + + // eg. en, es, ja, etc. + let maybe_primary_language_subtag = maybe_full_canonical_tag + .map(|t| language_tags::LanguageTag::parse(t) + .map(|language_tag| language_tag.primary_language().to_string()) + ) + .transpose() + .map_err(|e| { + error!("Error parsing language tag '{}': {:?}", tag, e); + UpdateDatasetError::BadInput("bad locale string".to_string()) + })?; + + if let Some(full_tag) = maybe_full_canonical_tag { + if let Some(primary_subtag) = maybe_primary_language_subtag.as_deref() { + ietf_language_tag = Some(full_tag.to_string()); + ietf_primary_language_subtag = Some(primary_subtag.to_string()); + } + } + } + + if let Some(visibility) = request.creator_set_visibility.as_deref() { + creator_set_visibility = Visibility::from_str(visibility) + .map_err(|_| UpdateDatasetError::BadInput("bad record visibility".to_string()))?; + } + + let is_mod = user_session.can_ban_users; + + let ip_address = get_request_ip(&http_request); + + let mut maybe_mod_user_token = None; + + if is_mod { + maybe_mod_user_token = Some(user_session.user_token.clone()); + } + let query_result = update_dataset( + UpdateDatasetArgs { + dataset_token: &ZsDatasetToken::new(path.dataset_token.clone()), + dataset_title: title.as_deref(), + maybe_creator_user_token: Some(user_session.user_token.clone().as_ref()), + creator_ip_address: ip_address.as_ref(), + creator_set_visibility: &creator_set_visibility, + maybe_mod_user_token: maybe_mod_user_token.as_deref(), + ietf_language_tag: ietf_language_tag.as_deref(), + ietf_primary_language_subtag: ietf_primary_language_subtag.as_deref(), + mysql_pool: &server_state.mysql_pool + } + ).await; + + match query_result { + Ok(_) => {}, + Err(err) => { + warn!("Update Dataset DB error: {:?}", err); + return Err(UpdateDatasetError::ServerError); + } + }; + + let response = UpdateDatasetResponse { + success: true, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| UpdateDatasetError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) } diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index c3baa353eb..dc24ea971d 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -121,7 +121,7 @@ use crate::http_server::endpoints::w2l::list_w2l_templates::list_w2l_templates_h use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_template_mod_approval_handler; use crate::http_server::endpoints::voice_designer::create_dataset::create_dataset_handler; -use crate::http_server::endpoints::voice_designer::update_dataset::update_dataset; +use crate::http_server::endpoints::voice_designer::update_dataset::update_dataset_handler; use crate::http_server::endpoints::voice_designer::delete_dataset::delete_dataset; use crate::http_server::endpoints::voice_designer::list_datasets_by_user::list_datasets_by_user; @@ -1141,7 +1141,10 @@ fn add_voice_designer_routes (app:App)-> App .service( web::scope("/datasets") .route("/create", web::post().to(create_dataset_handler)) - .route("/{dataset_token}/update", web::post().to(update_dataset)) + .service(web::resource("/{dataset_token}/update") + .route(web::post().to(update_dataset_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) .route("/{dataset_token}/delete", web::delete().to(delete_dataset)) .route("/user/{user_token}/list", web::get().to(list_datasets_by_user)) )