diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/datasets/list_datasets.rs b/crates/lib/mysql_queries/src/queries/voice_designer/datasets/list_datasets.rs index 3f8750d4a6..f6231dd378 100644 --- a/crates/lib/mysql_queries/src/queries/voice_designer/datasets/list_datasets.rs +++ b/crates/lib/mysql_queries/src/queries/voice_designer/datasets/list_datasets.rs @@ -16,9 +16,10 @@ pub struct DatasetRecordForList { pub dataset_token: String, pub title: String, pub creator_set_visibility: Visibility, + pub creator_user_token: String, + pub creator_username: String, pub ietf_language_tag: String, pub ietf_primary_language_subtag: String, - pub maybe_creator_user_token: Option, pub created_at: DateTime, pub updated_at: DateTime, @@ -26,23 +27,21 @@ pub struct DatasetRecordForList { pub async fn list_datasets_by_user_token( mysql_pool: &MySqlPool, - user_token: &str, + username: &str, can_see_deleted: bool, - is_owner: bool, ) -> AnyhowResult> { let mut connection = mysql_pool.acquire().await?; - list_datasets_with_connection(&mut connection, user_token, can_see_deleted, is_owner).await + list_datasets_with_connection(&mut connection, username, can_see_deleted).await } pub async fn list_datasets_with_connection( mysql_connection: &mut PoolConnection, - user_token: &str, + creator_username: &str, can_see_deleted: bool, - is_owner: bool, ) -> AnyhowResult> { let datasets = - list_datasets_by_creator_token(mysql_connection, user_token, can_see_deleted) + list_datasets_by_creator_username(mysql_connection, creator_username, can_see_deleted) .await; let datasets : Vec = match datasets { @@ -68,22 +67,23 @@ pub async fn list_datasets_with_connection( creator_set_visibility: dataset.creator_set_visibility, ietf_language_tag: dataset.ietf_language_tag, ietf_primary_language_subtag: dataset.ietf_primary_language_subtag, - maybe_creator_user_token: dataset.maybe_creator_user_token, + creator_user_token: dataset.creator_user_token, + creator_username: dataset.creator_username, created_at: dataset.created_at, updated_at: dataset.updated_at, } }) .filter(|dataset| { - is_owner || dataset.creator_set_visibility == Visibility::Public || can_see_deleted + dataset.creator_username == creator_username || dataset.creator_set_visibility == Visibility::Public || can_see_deleted }) .collect::>()) } -async fn list_datasets_by_creator_token( +async fn list_datasets_by_creator_username( mysql_connection: &mut PoolConnection, - creator_token: &str, + creator_username: &str, can_see_deleted: bool, ) -> AnyhowResult> { // TODO: There has to be a better way. @@ -98,17 +98,20 @@ async fn list_datasets_by_creator_token( zd.title, zd.ietf_language_tag, zd.ietf_primary_language_subtag, - zd.maybe_creator_user_token, + users.token as creator_user_token, + users.username as creator_username, zd.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`, zd.created_at, zd.updated_at FROM zs_voice_datasets as zd + JOIN users + ON users.token = zd.maybe_creator_user_token WHERE - zd.maybe_creator_user_token = ? + users.username = ? AND zd.user_deleted_at IS NULL AND zd.mod_deleted_at IS NULL "#, - creator_token) + creator_username) .fetch_all(mysql_connection) .await? } else { @@ -122,15 +125,18 @@ async fn list_datasets_by_creator_token( zd.title, zd.ietf_language_tag, zd.ietf_primary_language_subtag, - zd.maybe_creator_user_token, + users.token as creator_user_token, + users.username as creator_username, zd.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`, zd.created_at, zd.updated_at FROM zs_voice_datasets as zd + JOIN users + ON users.token = zd.maybe_creator_user_token WHERE - zd.maybe_creator_user_token = ? + users.username = ? "#, - creator_token) + creator_username) .fetch_all(mysql_connection) .await? }; @@ -143,7 +149,8 @@ struct InternalRawDatasetRecordForList { title: String, ietf_language_tag: String, ietf_primary_language_subtag: String, - maybe_creator_user_token: Option, + creator_user_token: String, + creator_username: String, creator_set_visibility: Visibility, created_at: DateTime, updated_at: DateTime, diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/inventory/list_voices_query_builder.rs b/crates/lib/mysql_queries/src/queries/voice_designer/inventory/list_voices_query_builder.rs new file mode 100644 index 0000000000..a8cbadb892 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/voice_designer/inventory/list_voices_query_builder.rs @@ -0,0 +1,365 @@ +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use chrono::{DateTime, Utc}; +use sqlx::MySqlPool; + +use config::shared_constants::DEFAULT_MYSQL_QUERY_RESULT_PAGE_SIZE; +use enums::common::visibility::Visibility; +use errors::AnyhowResult; + + +#[derive(Serialize)] +pub struct ZsVoiceListPage { + pub voices: Vec, + pub sort_ascending: bool, + + pub first_id: Option, + + pub last_id: Option, +} + +#[derive(Serialize)] +pub struct ZsVoiceRecordForList { + pub voice_token: String, + pub title: String, + pub creator_set_visibility: Visibility, + pub ietf_language_tag: String, + pub ietf_primary_language_subtag: String, + pub creator_user_token: String, + pub creator_username: String, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +pub struct ListVoicesQueryBuilder { + scope_creator_username: Option, + include_user_hidden: bool, + include_mod_deleted_results: bool, + include_user_deleted_results: bool, + sort_ascending: bool, + offset: Option, + limit: u16, + cursor_is_reversed: bool, +} + +impl ListVoicesQueryBuilder { + pub fn new() -> Self { + Self { + scope_creator_username: None, + include_user_hidden: false, + include_mod_deleted_results: false, + include_user_deleted_results: false, + sort_ascending: false, + offset: None, + limit: DEFAULT_MYSQL_QUERY_RESULT_PAGE_SIZE, + cursor_is_reversed: false, + } + } + + pub fn scope_creator_username(mut self, scope_creator_username: Option<&str>) -> Self { + self.scope_creator_username = scope_creator_username.map(|u| u.to_string()); + self + } + + pub fn include_user_hidden(mut self, include_user_hidden: bool) -> Self { + self.include_user_hidden = include_user_hidden; + self + } + + pub fn include_mod_deleted_results(mut self, include_mod_deleted_results: bool) -> Self { + self.include_mod_deleted_results = include_mod_deleted_results; + self + } + + pub fn include_user_deleted_results(mut self, include_user_deleted_results: bool) -> Self { + self.include_user_deleted_results = include_user_deleted_results; + self + } + + pub fn sort_ascending(mut self, sort_ascending: bool) -> Self { + self.sort_ascending = sort_ascending; + self + } + + pub fn offset(mut self, offset: Option) -> Self { + self.offset = offset; + self + } + + pub fn limit(mut self, limit: u16) -> Self { + self.limit = limit; + self + } + + pub fn cursor_is_reversed(mut self, cursor_is_reversed: bool) -> Self { + self.cursor_is_reversed = cursor_is_reversed; + self + } + + pub async fn perform_internal_query( + &self, + mysql_pool: &MySqlPool + ) -> AnyhowResult> { + let query = self.build_query_string(); + let mut query = sqlx::query_as::<_, RawInternalVoiceRecordForList>(&query); + + // NB: The following bindings must match the order of the query builder !! + + if let Some(offset) = self.offset { + query = query.bind(offset); + } + + if let Some(username) = self.scope_creator_username.as_deref() { + query = query.bind(username); + } + + query = query.bind(self.limit); + + let mut results = query.fetch_all(mysql_pool) + .await?; + + if self.cursor_is_reversed { + results.reverse() + } + + Ok(results) + } + + + + pub async fn perform_query_for_page( + &self, + mysql_pool: &MySqlPool + ) -> AnyhowResult { + let internal_voices = self.perform_internal_query(mysql_pool).await?; + + let first_id = internal_voices.first() + .map(|raw_result| raw_result.voice_id); + + let last_id = internal_voices.last() + .map(|raw_result| raw_result.voice_id); + + let voices = internal_voices.into_iter().map( + |v| { + ZsVoiceRecordForList { + voice_token: v.token.to_string(), + title: v.title, + creator_set_visibility: Visibility::from_str(&v.creator_set_visibility).unwrap_or(Visibility::Public), + ietf_language_tag: v.ietf_language_tag, + ietf_primary_language_subtag: v.ietf_primary_language_subtag, + creator_user_token: v.creator_user_token, + creator_username: v.creator_username, + created_at: v.created_at, + updated_at: v.updated_at, + } + }) + .collect::>(); + + Ok(ZsVoiceListPage { + voices, + sort_ascending: self.sort_ascending, + first_id, + last_id, + }) + } + + pub fn build_query_string(&self) -> String { + let mut query = r#" + SELECT + zs_voices.id as voice_id, + zs_voices.token, + zs_voices.title, + zs_voices.ietf_language_tag, + zs_voices.ietf_primary_language_subtag, + users.token as creator_user_token, + users.username as creator_username, + zs_voices.creator_set_visibility, + zs_voices.created_at, + zs_voices.updated_at + FROM zs_voices + JOIN users + ON users.token = zs_voices.maybe_creator_user_token + "#.to_string(); + + query.push_str(&self.build_predicates()); + query + } + + pub fn build_predicates(&self) -> String { + // NB: Reverse cursors require us to invert the sort direction. + let mut sort_ascending = self.sort_ascending; + + let mut first_predicate_added = false; + + let mut query = "".to_string(); + + if let Some(_offset) = self.offset { + if !first_predicate_added { + query.push_str(" WHERE"); + first_predicate_added = true; + } else { + query.push_str(" AND"); + } + + if sort_ascending { + if self.cursor_is_reversed { + // NB: We're searching backwards. + query.push_str(" zs_voices.id < ?"); + sort_ascending = !sort_ascending; + } else { + query.push_str(" zs_voices.id > ?"); + } + } else { + if self.cursor_is_reversed { + // NB: We're searching backwards. + query.push_str(" zs_voices.id > ?"); + sort_ascending = !sort_ascending; + } else { + query.push_str(" zs_voices.id < ?"); + } + } + } + + if let Some(_username) = self.scope_creator_username.as_deref() { + if !first_predicate_added { + query.push_str(" WHERE users.username = ?"); + first_predicate_added = true; + } else { + query.push_str(" AND users.username = ?"); + } + } + + if !self.include_user_hidden { + if !first_predicate_added { + query.push_str(" WHERE zs_voices.creator_set_visibility = 'public'"); + first_predicate_added = true; + } else { + query.push_str(" AND zs_voices.creator_set_visibility = 'public'"); + } + } + + if !self.include_mod_deleted_results { + if !first_predicate_added { + query.push_str(" WHERE zs_voices.mod_deleted_at IS NULL"); + first_predicate_added = true; + } else { + query.push_str(" AND zs_voices.mod_deleted_at IS NULL"); + } + } + + if !self.include_user_deleted_results { + if !first_predicate_added { + query.push_str(" WHERE zs_voices.user_deleted_at IS NULL"); + first_predicate_added = true; + } else { + query.push_str(" AND zs_voices.user_deleted_at IS NULL"); + } + } + + if sort_ascending { + query.push_str(" ORDER BY zs_voices.id ASC"); + } else { + query.push_str(" ORDER BY zs_voices.id DESC"); + } + + query.push_str(" LIMIT ?"); + + query + } +} + +#[derive(sqlx::FromRow)] +pub struct RawInternalVoiceRecordForList { + pub voice_id: i64, + pub token: String, + pub title: String, + pub ietf_language_tag: String, + pub ietf_primary_language_subtag: String, + pub creator_user_token: String, + pub creator_username: String, + pub creator_set_visibility: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[cfg(test)] +mod tests { + use crate::queries::voice_designer::inventory::list_voices_query_builder::ListVoicesQueryBuilder; + + #[test] + fn predicates_without_scoping() { + let query_builder = ListVoicesQueryBuilder::new(); + + assert_eq!(&query_builder.build_predicates(), + " WHERE zs_voices.creator_set_visibility = 'public' \ + AND zs_voices.mod_deleted_at IS NULL \ + AND zs_voices.user_deleted_at IS NULL \ + ORDER BY zs_voices.id DESC \ + LIMIT ?"); + } + + #[test] + fn predicates_scoped_to_user() { + let query_builder = ListVoicesQueryBuilder::new() + .scope_creator_username(Some("echelon")); + + assert_eq!(&query_builder.build_predicates(), + " WHERE users.username = ? \ + AND zs_voices.creator_set_visibility = 'public' \ + AND zs_voices.mod_deleted_at IS NULL \ + AND zs_voices.user_deleted_at IS NULL \ + ORDER BY zs_voices.id DESC \ + LIMIT ?"); + } + + #[test] + fn predicates_including_user_hidden() { + let query_builder = ListVoicesQueryBuilder::new() + .include_user_hidden(true); + + assert_eq!(&query_builder.build_predicates(), + " WHERE zs_voices.mod_deleted_at IS NULL \ + AND zs_voices.user_deleted_at IS NULL \ + ORDER BY zs_voices.id DESC \ + LIMIT ?"); + } + + #[test] + fn predicates_including_mod_deleted() { + let query_builder = ListVoicesQueryBuilder::new() + .include_mod_deleted_results(true); + + assert_eq!(&query_builder.build_predicates(), + " WHERE zs_voices.creator_set_visibility = 'public' \ + AND zs_voices.user_deleted_at IS NULL \ + ORDER BY zs_voices.id DESC \ + LIMIT ?"); + } + + #[test] + fn predicates_including_user_deleted() { + let query_builder = ListVoicesQueryBuilder::new() + .include_user_deleted_results(true); + + assert_eq!(&query_builder.build_predicates(), + " WHERE zs_voices.creator_set_visibility = 'public' \ + AND zs_voices.mod_deleted_at IS NULL \ + ORDER BY zs_voices.id DESC \ + LIMIT ?"); + } + + #[test] + fn predicates_including_mod_deleted_and_user_deleted() { + let query_builder = ListVoicesQueryBuilder::new() + .include_mod_deleted_results(true) + .include_user_deleted_results(true); + + assert_eq!(&query_builder.build_predicates(), + " WHERE zs_voices.creator_set_visibility = 'public' \ + ORDER BY zs_voices.id DESC \ + LIMIT ?"); + } +} \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/inventory/mod.rs b/crates/lib/mysql_queries/src/queries/voice_designer/inventory/mod.rs new file mode 100644 index 0000000000..8bdc5dbb20 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/voice_designer/inventory/mod.rs @@ -0,0 +1 @@ +pub mod list_voices_query_builder; \ 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 70b4a115a5..2d51d3134c 100644 --- a/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs +++ b/crates/lib/mysql_queries/src/queries/voice_designer/mod.rs @@ -1,3 +1,4 @@ pub mod datasets; pub mod voice_samples; pub mod voices; +pub mod inventory; \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/voice_samples/list_dataset_samples.rs b/crates/lib/mysql_queries/src/queries/voice_designer/voice_samples/list_dataset_samples.rs new file mode 100644 index 0000000000..ae2e29d6ab --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/voice_designer/voice_samples/list_dataset_samples.rs @@ -0,0 +1,131 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use log::warn; +use sqlx::{MySql, MySqlPool}; +use sqlx::pool::PoolConnection; + +use errors::AnyhowResult; +use tokens::tokens::users::UserToken; +use tokens::tokens::zs_voice_dataset_samples::ZsVoiceDatasetSampleToken; +use tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken; + +pub struct DatasetSampleRecordForList { + pub token: ZsVoiceDatasetSampleToken, + pub dataset_token: ZsVoiceDatasetToken, + + pub maybe_creator_user_token: Option, + + pub created_at: DateTime, + pub updated_at: DateTime, + // TODO: Other fields +} + + +pub async fn list_samples( + dataset_token: &ZsVoiceDatasetToken, + can_see_deleted: bool, + mysql_pool: &MySqlPool, +) -> AnyhowResult> { + let mut connection = mysql_pool.acquire().await?; + list_samples_by_dataset_token_with_connection( + dataset_token, + can_see_deleted, + &mut connection + ).await +} + +pub async fn list_samples_by_dataset_token_with_connection( + dataset_token: &ZsVoiceDatasetToken, + can_see_deleted: bool, + mysql_connection: &mut PoolConnection, +) -> AnyhowResult> { + + + let maybe_samples = list_samples_by_dataset_token( + mysql_connection, + dataset_token, + can_see_deleted + ).await; + + let records = match maybe_samples { + Ok(records) => records, + Err(err) => { + match err { + RowNotFound => { + return Ok(Vec::new()); + }, + _ => { + warn!("dataset sample list query error: {:?}", err); + return Err(anyhow!("Error fetching dataset samples by dataset token: {:?}", err)); + } + } + } + }; + + Ok(records.into_iter() + .map(|record| { + DatasetSampleRecordForList { + token: record.token, + dataset_token: record.dataset_token, + maybe_creator_user_token: record.maybe_creator_user_token, + created_at: record.created_at, + updated_at: record.updated_at, + } + }) + .collect() + ) +} + +async fn list_samples_by_dataset_token( + mysql_connection: &mut PoolConnection, + dataset_token: &ZsVoiceDatasetToken, + can_see_deleted: bool, +) -> AnyhowResult> { + let maybe_samples = if !can_see_deleted { + sqlx::query_as!( + InternalRawDatasetSampleRecordForList, + r#" + SELECT + zds.token as `token: tokens::tokens::zs_voice_dataset_samples::ZsVoiceDatasetSampleToken`, + zds.dataset_token as `dataset_token: tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken`, + zds.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`, + zds.created_at, + zds.updated_at + FROM zs_voice_dataset_samples as zds + WHERE zds.dataset_token = ? + AND zds.user_deleted_at IS NULL + AND zds.mod_deleted_at IS NULL + "#, + dataset_token + ).fetch_all(mysql_connection).await? + } else { + sqlx::query_as!( + InternalRawDatasetSampleRecordForList, + r#" + SELECT + zds.token as `token: tokens::tokens::zs_voice_dataset_samples::ZsVoiceDatasetSampleToken`, + zds.dataset_token as `dataset_token: tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken`, + zds.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`, + zds.created_at, + zds.updated_at + FROM zs_voice_dataset_samples as zds + WHERE zds.dataset_token = ? + "#, + dataset_token + ).fetch_all(mysql_connection).await? + }; + + Ok(maybe_samples) +} + + +struct InternalRawDatasetSampleRecordForList { + token: ZsVoiceDatasetSampleToken, + dataset_token: ZsVoiceDatasetToken, + + maybe_creator_user_token: Option, + + created_at: DateTime, + updated_at: DateTime, + // TODO: Other fields +} diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/voice_samples/mod.rs b/crates/lib/mysql_queries/src/queries/voice_designer/voice_samples/mod.rs index 2bbe2ae971..42210d73f5 100644 --- a/crates/lib/mysql_queries/src/queries/voice_designer/voice_samples/mod.rs +++ b/crates/lib/mysql_queries/src/queries/voice_designer/voice_samples/mod.rs @@ -1,2 +1,3 @@ pub mod delete_sample; -pub mod get_dataset_sample; \ No newline at end of file +pub mod get_dataset_sample; +pub mod list_dataset_samples; \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/queries/voice_designer/voices/list_voices.rs b/crates/lib/mysql_queries/src/queries/voice_designer/voices/list_voices.rs index b633c0be56..70d823d864 100644 --- a/crates/lib/mysql_queries/src/queries/voice_designer/voices/list_voices.rs +++ b/crates/lib/mysql_queries/src/queries/voice_designer/voices/list_voices.rs @@ -18,7 +18,8 @@ pub struct VoiceRecordForList { pub creator_set_visibility: Visibility, pub ietf_language_tag: String, pub ietf_primary_language_subtag: String, - pub maybe_creator_user_token: Option, + pub creator_user_token: String, + pub creator_username: String, pub created_at: DateTime, pub updated_at: DateTime, @@ -26,22 +27,20 @@ pub struct VoiceRecordForList { pub async fn list_voices_by_user_token( mysql_pool: &MySqlPool, - user_token: &str, + creator_username: &str, can_see_deleted: bool, - is_owner: bool, ) -> AnyhowResult> { let mut connection = mysql_pool.acquire().await?; - list_voices_with_connection(&mut connection, user_token, can_see_deleted, is_owner).await + list_voices_with_connection(&mut connection, creator_username, can_see_deleted).await } pub async fn list_voices_with_connection( mysql_connection: &mut PoolConnection, - user_token: &str, + creator_username: &str, can_see_deleted: bool, - is_owner: bool, ) -> AnyhowResult> { - let maybe_voices = list_dataset_by_creator(mysql_connection, user_token, can_see_deleted) + let maybe_voices = list_voices_by_creator_username(mysql_connection, creator_username, can_see_deleted) .await; let voices : Vec = match maybe_voices { @@ -67,22 +66,23 @@ pub async fn list_voices_with_connection( creator_set_visibility: voice.creator_set_visibility, ietf_language_tag: voice.ietf_language_tag, ietf_primary_language_subtag: voice.ietf_primary_language_subtag, - maybe_creator_user_token: voice.maybe_creator_user_token, + creator_user_token: voice.creator_user_token, + creator_username: voice.creator_username, created_at: voice.created_at, updated_at: voice.updated_at, } }) - .filter(|dataset| { - is_owner || dataset.creator_set_visibility == Visibility::Public || can_see_deleted + .filter(|voice| { + creator_username == voice.creator_username || voice.creator_set_visibility == Visibility::Public || can_see_deleted }) .collect::>()) } -async fn list_dataset_by_creator( +async fn list_voices_by_creator_username( mysql_connection: &mut PoolConnection, - creator_token: &str, + creator_username: &str, can_see_deleted: bool, ) -> AnyhowResult> { // TODO: There has to be a better way. @@ -97,17 +97,20 @@ async fn list_dataset_by_creator( zv.title, zv.ietf_language_tag, zv.ietf_primary_language_subtag, - zv.maybe_creator_user_token, + users.token as creator_user_token, + users.username as creator_username, zv.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`, zv.created_at, zv.updated_at FROM zs_voices as zv + JOIN users + ON users.token = zv.maybe_creator_user_token WHERE - zv.maybe_creator_user_token = ? + users.username = ? AND zv.user_deleted_at IS NULL AND zv.mod_deleted_at IS NULL "#, - creator_token) + creator_username) .fetch_all(mysql_connection) .await? } else { @@ -121,15 +124,18 @@ async fn list_dataset_by_creator( zv.title, zv.ietf_language_tag, zv.ietf_primary_language_subtag, - zv.maybe_creator_user_token, + users.token as creator_user_token, + users.username as creator_username, zv.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`, zv.created_at, zv.updated_at FROM zs_voices as zv + JOIN users + ON users.token = zv.maybe_creator_user_token WHERE - zv.maybe_creator_user_token = ? + users.username = ? "#, - creator_token) + creator_username) .fetch_all(mysql_connection) .await? }; @@ -142,7 +148,8 @@ struct InternalRawVoiceRecordForList { title: String, ietf_language_tag: String, ietf_primary_language_subtag: String, - maybe_creator_user_token: Option, + creator_user_token: String, + creator_username: String, creator_set_visibility: Visibility, created_at: DateTime, updated_at: DateTime, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voice_dataset_samples/list_samples_by_dataset.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voice_dataset_samples/list_samples_by_dataset.rs index 7d8126e102..69866c64f5 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voice_dataset_samples/list_samples_by_dataset.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voice_dataset_samples/list_samples_by_dataset.rs @@ -1,14 +1,121 @@ - - use std::sync::Arc; use actix_web::{HttpRequest, HttpResponse, web}; use actix_web::error::ResponseError; use actix_web::http::StatusCode; +use chrono::{DateTime, Utc}; use log::{info, warn}; +use mysql_queries::queries::voice_designer::voice_samples::list_dataset_samples::list_samples; +use tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken; +use crate::server_state::ServerState; -pub async fn list_samples_by_dataset_handler(data_set_token: web::Path) -> HttpResponse { - // Implementation for listing samples for a dataset - HttpResponse::Ok().json(format!("List of samples for dataset {}", data_set_token)) +#[derive(Serialize, Clone)] +pub struct ZsSampleRecordForResponse { + sample_token: String, + dataset_token: String, + maybe_creator_user_token: Option, + + created_at: DateTime, + updated_at: DateTime, +} + +#[derive(Serialize)] +pub struct ListSamplesByDatasetSuccessResponse { + pub success: bool, + pub samples: Vec, +} + +#[derive(Deserialize)] +pub struct ListSamplesByDatasetPathInfo { + dataset_token: String, +} + +#[derive(Debug)] +pub enum ListSamplesByDatasetError { + NotAuthorized, + ServerError, +} + +impl std::fmt::Display for ListSamplesByDatasetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl ResponseError for ListSamplesByDatasetError { + fn status_code(&self) -> StatusCode { + match *self { + ListSamplesByDatasetError::NotAuthorized => StatusCode::UNAUTHORIZED, + ListSamplesByDatasetError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn list_samples_by_dataset_handler( + http_request: HttpRequest, + path: web::Path, + 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); + ListSamplesByDatasetError::ServerError + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => { + warn!("not logged in"); + return Err(ListSamplesByDatasetError::NotAuthorized); + }, + }; + + let dataset_token = ZsVoiceDatasetToken(path.dataset_token.clone()); + let is_mod = user_session.can_ban_users; + + let query_results = list_samples( + &dataset_token, + is_mod, + &server_state.mysql_pool + ).await.map_err(|e| { + warn!("list_samples error: {:?}", e); + ListSamplesByDatasetError::ServerError + }); + + let samples = match query_results { + Ok(samples) => samples, + Err(e) => { + warn!("list_samples error: {:?}", e); + return Err(ListSamplesByDatasetError::ServerError); + }, + }; + + let samples = samples + .into_iter() + .map(|sample| ZsSampleRecordForResponse { + sample_token: sample.token.to_string(), + dataset_token: sample.dataset_token.to_string(), + maybe_creator_user_token: match sample.maybe_creator_user_token { + Some(user_token) => Some(user_token.to_string()), + None => None, + }, + created_at: sample.created_at, + updated_at: sample.updated_at, + }) + .collect(); + + let response = ListSamplesByDatasetSuccessResponse { + success: true, + samples + }; + + let body = serde_json::to_string(&response).map_err(|e| { + warn!("json serialization error: {:?}", e); + ListSamplesByDatasetError::ServerError + })?; + + Ok(HttpResponse::Ok().content_type("application/json").body(body)) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voice_datasets/list_datasets_by_user.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voice_datasets/list_datasets_by_user.rs index 24a6201e4d..3f50ebd128 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voice_datasets/list_datasets_by_user.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voice_datasets/list_datasets_by_user.rs @@ -20,7 +20,8 @@ pub struct ZsDatasetRecordForResponse { creator_set_visibility: String, ietf_language_tag: String, ietf_primary_language_subtag: String, - maybe_creator_user_token: Option, + creator_user_token: String, + creator_username: String, created_at: DateTime, updated_at: DateTime, @@ -35,7 +36,7 @@ pub struct ListDatasetsByUserSuccessResponse { #[derive(Deserialize)] pub struct ListDatasetsByUserPathInfo { - user_token: String, + username: String, } #[derive(Debug)] @@ -82,15 +83,14 @@ pub async fn list_datasets_by_user_handler( } }; - let user_token = path.user_token.clone(); + let username = path.username.as_ref(); let creator_user_token = user_session.user_token.clone(); let is_mod = user_session.can_ban_users; let query_results = list_datasets_by_user_token( &server_state.mysql_pool, - &user_token, + &username, is_mod, - creator_user_token == user_token, ).await.map_err(|e| { warn!("Error querying for datasets: {:?}", e); ListDatasetsByUserError::ServerError @@ -110,7 +110,8 @@ pub async fn list_datasets_by_user_handler( creator_set_visibility: dataset.creator_set_visibility.to_string() , ietf_language_tag: dataset.ietf_language_tag, ietf_primary_language_subtag: dataset.ietf_primary_language_subtag, - maybe_creator_user_token: dataset.maybe_creator_user_token, + creator_user_token: dataset.creator_user_token, + creator_username: dataset.creator_username, created_at: dataset.created_at, updated_at: dataset.updated_at, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/list_available_voices.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/list_available_voices.rs index 661bf2e1b5..b2216813b5 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/list_available_voices.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/list_available_voices.rs @@ -7,10 +7,150 @@ use actix_web::error::ResponseError; use actix_web::http::StatusCode; use log::{info, warn}; +use mysql_queries::queries::voice_designer::inventory::list_voices_query_builder::{ListVoicesQueryBuilder, ZsVoiceRecordForList}; +use crate::server_state::ServerState; - -pub async fn list_available_voices() -> HttpResponse { - // Implementation for listing available voices (paginated) - HttpResponse::Ok().json("List of available voices") +#[derive(Deserialize)] +pub struct ListZsVoicesQuery { + pub sort_ascending: Option, + pub limit: Option, + pub cursor: Option, + pub cursor_is_reversed: Option, } + +#[derive(Serialize)] +pub struct ListZsVoicesSuccessResponse { + pub success: bool, + pub voices: Vec, + pub cursor_next: Option, + pub cursor_previous: Option +} + +#[derive(Debug)] +pub enum ListZsVoicesError { + NotAuthorized, + ServerError, +} + +impl std::fmt::Display for ListZsVoicesError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl ResponseError for ListZsVoicesError { + fn status_code(&self) -> StatusCode { + match *self { + ListZsVoicesError::NotAuthorized => StatusCode::UNAUTHORIZED, + ListZsVoicesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn list_available_voices( + http_request: HttpRequest, + query: web::Query, + 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); + ListZsVoicesError::ServerError + })?; + + let mut is_mod = false; + let user_session = match maybe_user_session { + Some(session) => { + is_mod = session.can_ban_users; + session + } + None => { + info!("not logged in"); + return Err(ListZsVoicesError::NotAuthorized); + } + }; + + + let limit = query.limit.unwrap_or(25); + + let sort_ascending = query.sort_ascending.unwrap_or(false); + let cursor_is_reversed = query.cursor_is_reversed.unwrap_or(false); + + let cursor = if let Some(cursor) = query.cursor.as_deref() { + let cursor = server_state.sort_key_crypto.decrypt_id(cursor) + .map_err(|e| { + warn!("crypto error: {:?}", e); + ListZsVoicesError::ServerError + })?; + Some(cursor) + } else { + None + }; + + let include_user_hidden = is_mod; + + + let mut query_builder = ListVoicesQueryBuilder::new() + .sort_ascending(sort_ascending) + .scope_creator_username(None) + .include_user_hidden(include_user_hidden) + .include_user_deleted_results(is_mod) + .include_mod_deleted_results(is_mod) + .limit(limit) + .cursor_is_reversed(cursor_is_reversed) + .offset(cursor); + + let query_results = query_builder.perform_query_for_page(&server_state.mysql_pool).await; + + + let voices_page = match query_results { + Ok(results) => results, + Err(e) => { + warn!("Query error: {:?}", e); + return Err(ListZsVoicesError::ServerError); + } + }; + + let cursor_next = if let Some(id) = voices_page.last_id { + let cursor = server_state.sort_key_crypto.encrypt_id(id as u64) + .map_err(|e| { + warn!("crypto error: {:?}", e); + ListZsVoicesError::ServerError + })?; + Some(cursor) + } else { + None + }; + + let cursor_previous = if let Some(id) = voices_page.first_id { + let cursor = server_state.sort_key_crypto.encrypt_id(id as u64) + .map_err(|e| { + warn!("crypto error: {:?}", e); + ListZsVoicesError::ServerError + })?; + Some(cursor) + } else { + None + }; + + let response = ListZsVoicesSuccessResponse { + success: true, + voices: voices_page.voices, + cursor_next, + cursor_previous, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| ListZsVoicesError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} + + + diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/list_voices_by_user.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/list_voices_by_user.rs index cf869440a3..2609a5a9b9 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/list_voices_by_user.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/voices/list_voices_by_user.rs @@ -21,7 +21,8 @@ pub struct ZsVoiceRecordForResponse { creator_set_visibility: String, ietf_language_tag: String, ietf_primary_language_subtag: String, - maybe_creator_user_token: Option, + creator_user_token: String, + creator_username: String, created_at: DateTime, updated_at: DateTime, @@ -36,7 +37,7 @@ pub struct ListVoicesByUserSuccessResponse { #[derive(Deserialize)] pub struct ListVoicesByUserPathInfo { - user_token: String, + username: String, } #[derive(Debug)] @@ -83,15 +84,14 @@ pub async fn list_voices_by_user( } }; - let user_token = path.user_token.clone(); + let username = path.username.as_ref(); let creator_user_token = user_session.user_token.clone(); let is_mod = user_session.can_ban_users; let query_results = list_voices_by_user_token( &server_state.mysql_pool, - &user_token, + &username, is_mod, - creator_user_token == user_token, ).await.map_err(|e| { warn!("Error querying for voices: {:?}", e); ListVoicesByUserError::ServerError @@ -111,7 +111,8 @@ pub async fn list_voices_by_user( creator_set_visibility: voice.creator_set_visibility.to_string() , ietf_language_tag: voice.ietf_language_tag, ietf_primary_language_subtag: voice.ietf_primary_language_subtag, - maybe_creator_user_token: voice.maybe_creator_user_token, + creator_user_token: voice.creator_user_token, + creator_username: voice.creator_username, created_at: voice.created_at, updated_at: voice.updated_at, diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index b39f216759..ca3392bb3d 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -169,7 +169,7 @@ pub fn add_routes (app: App) -> App app = add_subscription_routes(app); /* /v1/subscriptions/... */ // TODO find a long term feature flag solution, since this code is likely deployed into production we don't want the route found. - let enable_voice_designer_route = true; + let enable_voice_designer_route = false; if (enable_voice_designer_route) { app = add_voice_designer_routes(app); /* /v1/voice_designer */ } @@ -1140,20 +1140,20 @@ fn add_voice_designer_routes (app:App)-> App .route(web::head().to(|| HttpResponse::Ok())) ) .route("/{dataset_token}/delete", web::delete().to(delete_dataset_handler)) - .route("/user/{user_token}/list", web::get().to(list_datasets_by_user_handler)) + .route("/user/{username}/list", web::get().to(list_datasets_by_user_handler)) ) .service( web::scope("/voice") .route("/create", web::post().to(create_voice_handler)) .route("/{voice_token}/update", web::post().to(update_voice_handler)) .route("/{voice_token}/delete", web::delete().to(delete_voice_handler)) - .route("/user/{user_token}/list", web::get().to(list_voices_by_user)) + .route("/user/{username}/list", web::get().to(list_voices_by_user)) ) .service( web::scope("/sample") .route("/upload", web::post().to(upload_sample_handler)) .route("/{sample_token}/delete", web::delete().to(delete_sample_handler)) - .route("/data_set/{data_set_token}/list", web::get().to(list_samples_by_dataset_handler)) + .route("/dataset/{dataset_token}/list", web::get().to(list_samples_by_dataset_handler)) ) .service( web::scope("/inference")