diff --git a/.sqlx/query-d9c00b3c78ed6ec31b281c7ea5fb1efd0cfa6b20899151f9e7f1a122c1f7a70e.json b/.sqlx/query-d9c00b3c78ed6ec31b281c7ea5fb1efd0cfa6b20899151f9e7f1a122c1f7a70e.json deleted file mode 100644 index 33334d1769..0000000000 --- a/.sqlx/query-d9c00b3c78ed6ec31b281c7ea5fb1efd0cfa6b20899151f9e7f1a122c1f7a70e.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "db_name": "MySQL", - "query": "\nSELECT\n f.token as `token: tokens::tokens::user_bookmarks::UserBookmarkToken`,\n f.entity_type as `entity_type: enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType`,\n f.entity_token,\n f.user_token as `user_token: tokens::tokens::users::UserToken`,\n u.username,\n u.display_name as user_display_name,\n u.email_gravatar_hash as user_gravatar_hash,\n f.created_at,\n f.updated_at,\n f.deleted_at,\n\n media_files.media_type as `maybe_media_file_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n media_files.origin_product_category as `maybe_media_file_origin_category: enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory`,\n\n tts_models.title as maybe_descriptive_text_tts_model_title,\n tts_results.raw_inference_text as maybe_descriptive_text_tts_result_inference_text,\n users.display_name as maybe_descriptive_text_user_display_name,\n voice_conversion_models.title as maybe_descriptive_text_voice_conversion_model_title,\n zs_voices.title as maybe_descriptive_text_zs_voice_title\n\nFROM\n user_bookmarks AS f\nJOIN users AS u\n ON f.user_token = u.token\n\nLEFT OUTER JOIN media_files ON media_files.token = f.entity_token\nLEFT OUTER JOIN tts_models ON tts_models.token = f.entity_token\nLEFT OUTER JOIN tts_results ON tts_results.token = f.entity_token\nLEFT OUTER JOIN users ON users.token = f.entity_token\nLEFT OUTER JOIN voice_conversion_models ON voice_conversion_models.token = f.entity_token\nLEFT OUTER JOIN zs_voices ON zs_voices.token = f.entity_token\n\nWHERE\n u.username = ?\n AND f.entity_type = ?\n AND f.deleted_at IS NULL\nORDER BY f.id DESC\nLIMIT 50\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "token: tokens::tokens::user_bookmarks::UserBookmarkToken", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 1, - "name": "entity_type: enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 2, - "name": "entity_token", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 3, - "name": "user_token: tokens::tokens::users::UserToken", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 4, - "name": "username", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 5, - "name": "user_display_name", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 6, - "name": "user_gravatar_hash", - "type_info": { - "type": "String", - "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 7, - "name": "created_at", - "type_info": { - "type": "Timestamp", - "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP", - "char_set": 63, - "max_size": 19 - } - }, - { - "ordinal": 8, - "name": "updated_at", - "type_info": { - "type": "Timestamp", - "flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW", - "char_set": 63, - "max_size": 19 - } - }, - { - "ordinal": 9, - "name": "deleted_at", - "type_info": { - "type": "Timestamp", - "flags": "MULTIPLE_KEY | BINARY", - "char_set": 63, - "max_size": 19 - } - }, - { - "ordinal": 10, - "name": "maybe_media_file_type: enums::by_table::media_files::media_file_type::MediaFileType", - "type_info": { - "type": "VarString", - "flags": "MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 64 - } - }, - { - "ordinal": 11, - "name": "maybe_media_file_origin_category: enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory", - "type_info": { - "type": "VarString", - "flags": "MULTIPLE_KEY | BINARY", - "char_set": 224, - "max_size": 64 - } - }, - { - "ordinal": 12, - "name": "maybe_descriptive_text_tts_model_title", - "type_info": { - "type": "VarString", - "flags": "BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 13, - "name": "maybe_descriptive_text_tts_result_inference_text", - "type_info": { - "type": "Blob", - "flags": "BLOB | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 262140 - } - }, - { - "ordinal": 14, - "name": "maybe_descriptive_text_user_display_name", - "type_info": { - "type": "VarString", - "flags": "BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 15, - "name": "maybe_descriptive_text_voice_conversion_model_title", - "type_info": { - "type": "VarString", - "flags": "BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 16, - "name": "maybe_descriptive_text_zs_voice_title", - "type_info": { - "type": "VarString", - "flags": "BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 1020 - } - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - true, - true - ] - }, - "hash": "d9c00b3c78ed6ec31b281c7ea5fb1efd0cfa6b20899151f9e7f1a122c1f7a70e" -} diff --git a/.sqlx/query-eea9531d5261833b93baadee8b99449362c074fc2397e23ecd175b6a0425b16b.json b/.sqlx/query-eea9531d5261833b93baadee8b99449362c074fc2397e23ecd175b6a0425b16b.json deleted file mode 100644 index 55d497beee..0000000000 --- a/.sqlx/query-eea9531d5261833b93baadee8b99449362c074fc2397e23ecd175b6a0425b16b.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "db_name": "MySQL", - "query": "\nSELECT\n f.token as `token: tokens::tokens::user_bookmarks::UserBookmarkToken`,\n f.entity_type as `entity_type: enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType`,\n f.entity_token,\n f.user_token as `user_token: tokens::tokens::users::UserToken`,\n u.username,\n u.display_name as user_display_name,\n u.email_gravatar_hash as user_gravatar_hash,\n f.created_at,\n f.updated_at,\n f.deleted_at,\n\n media_files.media_type as `maybe_media_file_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n media_files.origin_product_category as `maybe_media_file_origin_category: enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory`,\n\n tts_models.title as maybe_descriptive_text_tts_model_title,\n tts_results.raw_inference_text as maybe_descriptive_text_tts_result_inference_text,\n users.display_name as maybe_descriptive_text_user_display_name,\n voice_conversion_models.title as maybe_descriptive_text_voice_conversion_model_title,\n zs_voices.title as maybe_descriptive_text_zs_voice_title\n\nFROM\n user_bookmarks AS f\nJOIN users AS u\n ON f.user_token = u.token\n\nLEFT OUTER JOIN media_files ON media_files.token = f.entity_token\nLEFT OUTER JOIN tts_models ON tts_models.token = f.entity_token\nLEFT OUTER JOIN tts_results ON tts_results.token = f.entity_token\nLEFT OUTER JOIN users ON users.token = f.entity_token\nLEFT OUTER JOIN voice_conversion_models ON voice_conversion_models.token = f.entity_token\nLEFT OUTER JOIN zs_voices ON zs_voices.token = f.entity_token\n\nWHERE\n u.username = ?\n AND f.deleted_at IS NULL\nORDER BY f.id DESC\nLIMIT 50\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "token: tokens::tokens::user_bookmarks::UserBookmarkToken", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 1, - "name": "entity_type: enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 2, - "name": "entity_token", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 3, - "name": "user_token: tokens::tokens::users::UserToken", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 4, - "name": "username", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 5, - "name": "user_display_name", - "type_info": { - "type": "VarString", - "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 6, - "name": "user_gravatar_hash", - "type_info": { - "type": "String", - "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 128 - } - }, - { - "ordinal": 7, - "name": "created_at", - "type_info": { - "type": "Timestamp", - "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP", - "char_set": 63, - "max_size": 19 - } - }, - { - "ordinal": 8, - "name": "updated_at", - "type_info": { - "type": "Timestamp", - "flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW", - "char_set": 63, - "max_size": 19 - } - }, - { - "ordinal": 9, - "name": "deleted_at", - "type_info": { - "type": "Timestamp", - "flags": "MULTIPLE_KEY | BINARY", - "char_set": 63, - "max_size": 19 - } - }, - { - "ordinal": 10, - "name": "maybe_media_file_type: enums::by_table::media_files::media_file_type::MediaFileType", - "type_info": { - "type": "VarString", - "flags": "MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 64 - } - }, - { - "ordinal": 11, - "name": "maybe_media_file_origin_category: enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory", - "type_info": { - "type": "VarString", - "flags": "MULTIPLE_KEY | BINARY", - "char_set": 224, - "max_size": 64 - } - }, - { - "ordinal": 12, - "name": "maybe_descriptive_text_tts_model_title", - "type_info": { - "type": "VarString", - "flags": "BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 13, - "name": "maybe_descriptive_text_tts_result_inference_text", - "type_info": { - "type": "Blob", - "flags": "BLOB | BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 262140 - } - }, - { - "ordinal": 14, - "name": "maybe_descriptive_text_user_display_name", - "type_info": { - "type": "VarString", - "flags": "BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 80 - } - }, - { - "ordinal": 15, - "name": "maybe_descriptive_text_voice_conversion_model_title", - "type_info": { - "type": "VarString", - "flags": "BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 1020 - } - }, - { - "ordinal": 16, - "name": "maybe_descriptive_text_zs_voice_title", - "type_info": { - "type": "VarString", - "flags": "BINARY | NO_DEFAULT_VALUE", - "char_set": 224, - "max_size": 1020 - } - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - true, - true - ] - }, - "hash": "eea9531d5261833b93baadee8b99449362c074fc2397e23ecd175b6a0425b16b" -} diff --git a/crates/lib/mysql_queries/src/queries/media_files/list_media_files_for_user.rs b/crates/lib/mysql_queries/src/queries/media_files/list_media_files_for_user.rs index 87bcc96ee8..d7115cd5a6 100644 --- a/crates/lib/mysql_queries/src/queries/media_files/list_media_files_for_user.rs +++ b/crates/lib/mysql_queries/src/queries/media_files/list_media_files_for_user.rs @@ -1,4 +1,5 @@ use chrono::{DateTime, Utc}; +use log::warn; use sqlx::{FromRow, MySql, MySqlPool, QueryBuilder, Row}; use sqlx::mysql::MySqlRow; @@ -11,16 +12,15 @@ use enums::traits::mysql_from_row::MySqlFromRow; use errors::AnyhowResult; use tokens::tokens::media_files::MediaFileToken; + + pub struct MediaFileListPage { pub records: Vec, pub sort_ascending: bool, - /// ID of the first record. - pub first_id: Option, - - /// ID of the last record. - pub last_id: Option, + pub current_page: usize, + pub total_page_count: usize, } pub struct MediaFileListItem { @@ -51,9 +51,9 @@ pub enum ViewAs { pub struct ListMediaFileForUserArgs<'a> { pub username: &'a str, - pub limit: usize, pub maybe_filter_media_type: Option, - pub maybe_offset: Option, + pub page_size: usize, + pub page_index: usize, pub cursor_is_reversed: bool, pub view_as: ViewAs, pub mysql_pool: &'a MySqlPool, @@ -61,25 +61,42 @@ pub struct ListMediaFileForUserArgs<'a> { pub async fn list_media_files_for_user(args: ListMediaFileForUserArgs<'_>) -> AnyhowResult { + /// Let's figure out how many results we could have returned total + let count_fields = select_total_count_field(); + let mut count_query_builder = query_builder( + args.maybe_filter_media_type, + args.username, + false, + 0, + 0, + args.cursor_is_reversed, + args.view_as, + count_fields.as_str(), + ); + + let row_count_query = count_query_builder.build_query_scalar::(); + let row_count_result = row_count_query.fetch_one(args.mysql_pool).await?; + /// Figure out limit start and end based on page size and indexes as requested + let limit_start = args.page_size * args.page_index; + let limit_end = limit_start + args.page_size; + + /// Now fetch the actual results with all the fields + let result_fields = select_result_fields(); let mut query = query_builder( args.maybe_filter_media_type, args.username, - args.limit, - args.maybe_offset, + true, + limit_start, + limit_end, args.cursor_is_reversed, args.view_as, + result_fields.as_str(), ); let query = query.build_query_as::(); - let results = query.fetch_all(args.mysql_pool).await?; - let first_id = results.first() - .map(|raw_result| raw_result.id); - - let last_id = results.last() - .map(|raw_result| raw_result.id); - + let number_of_pages = (row_count_result / args.page_size as i64) as usize; let results = results.into_iter() .map(|record| { MediaFileListItem { @@ -102,43 +119,59 @@ pub async fn list_media_files_for_user(args: ListMediaFileForUserArgs<'_>) -> An Ok(MediaFileListPage { records: results, sort_ascending: !args.cursor_is_reversed, - first_id, - last_id, + current_page: args.page_index, + total_page_count: number_of_pages, }) } + +fn select_result_fields() -> String { + r#" + m.id, + m.token, + + m.origin_category, + m.origin_product_category, + + m.maybe_origin_model_type, + m.maybe_origin_model_token, + + m.media_type, + + m.public_bucket_directory_hash, + m.maybe_public_bucket_prefix, + m.maybe_public_bucket_extension, + + m.creator_set_visibility, + m.created_at, + m.updated_at + "# + .to_string() +} + +fn select_total_count_field() -> String { + r#" + COUNT(m.id) AS total_count + "# + .to_string() +} + fn query_builder<'a>( maybe_filter_media_type: Option, username: &'a str, - limit: usize, - maybe_offset: Option, + enforce_limits: bool, + limit_start: usize, + limit_end: usize, cursor_is_reversed: bool, view_as: ViewAs, + select_fields: &'a str, ) -> QueryBuilder<'a, MySql> { // NB: Query cannot be statically checked by sqlx let mut query_builder: QueryBuilder = QueryBuilder::new( - r#" + format!(r#" SELECT - m.id, - m.token, - - m.origin_category, - m.origin_product_category, - - m.maybe_origin_model_type, - m.maybe_origin_model_token, - - m.media_type, - - m.public_bucket_directory_hash, - m.maybe_public_bucket_prefix, - m.maybe_public_bucket_extension, - - m.creator_set_visibility, - m.created_at, - m.updated_at - + {select_fields} FROM media_files AS m LEFT OUTER JOIN users AS u ON m.maybe_creator_user_token = u.token @@ -146,7 +179,7 @@ LEFT OUTER JOIN users AS u WHERE m.user_deleted_at IS NULL AND m.mod_deleted_at IS NULL "# - ); + )); query_builder.push(" AND u.username = "); query_builder.push_bind(username); @@ -171,21 +204,19 @@ WHERE m.user_deleted_at IS NULL } } - if let Some(offset) = maybe_offset { - if cursor_is_reversed { - query_builder.push(format!(" AND m.id < {offset} ")); - } else { - query_builder.push(format!(" AND m.id > {offset} ")); - } - } - if cursor_is_reversed { query_builder.push(" ORDER BY m.id ASC "); } else { query_builder.push(" ORDER BY m.id DESC "); } - query_builder.push(format!(" LIMIT {limit} ")); + if (enforce_limits) { + if cursor_is_reversed { + query_builder.push(format!(" LIMIT {limit_start}, {limit_end} ")); + } else { + query_builder.push(format!(" LIMIT {limit_end}, {limit_start} ")); + } + } query_builder } diff --git a/crates/service/web/storyteller_web/src/http_server/common_responses/pagination_page.rs b/crates/service/web/storyteller_web/src/http_server/common_responses/pagination_page.rs index 3e64af9626..50534944ef 100644 --- a/crates/service/web/storyteller_web/src/http_server/common_responses/pagination_page.rs +++ b/crates/service/web/storyteller_web/src/http_server/common_responses/pagination_page.rs @@ -8,10 +8,10 @@ #[derive(Serialize)] pub struct PaginationPage { /// The current page number - pub current: u64, + pub current: usize, /// How many pages there are in total - pub total_page_count: u64, + pub total_page_count: usize, } #[cfg(test)] diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/list_media_files_for_user.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/list_media_files_for_user.rs index bd3eda2b78..ef03b3dd48 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/list_media_files_for_user.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/list_media_files_for_user.rs @@ -16,6 +16,7 @@ use mysql_queries::queries::media_files::list_media_files_for_user::{list_media_ use tokens::tokens::media_files::MediaFileToken; use crate::http_server::common_responses::pagination_cursors::PaginationCursors; +use crate::http_server::common_responses::pagination_page::PaginationPage; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; @@ -27,7 +28,8 @@ pub struct PathInfo { #[derive(Deserialize)] pub struct QueryParams { pub sort_ascending: Option, - pub limit: Option, + pub page_size: Option, + pub page_index: Option, pub cursor: Option, pub cursor_is_reversed: Option, pub filter_media_type: Option, @@ -37,7 +39,7 @@ pub struct QueryParams { pub struct SuccessResponse { pub success: bool, pub results: Vec, - pub pagination: PaginationCursors, + pub pagination: PaginationPage, } #[derive(Serialize)] @@ -115,21 +117,13 @@ pub async fn list_media_files_for_user_handler( }; // TODO(bt,2023-12-04): Enforce real maximums and defaults - let limit = query.limit.unwrap_or(25); + let limit = query.page_size.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); - ErrorResponse::ServerError - })?; - Some(cursor as usize) - } else { - None - }; + let page_size = query.page_size.unwrap_or_else(|| 25); + let page_index = query.page_index.unwrap_or_else(|| 0); let view_as = if is_author { ViewAs::Author @@ -139,11 +133,12 @@ pub async fn list_media_files_for_user_handler( ViewAs::AnotherUser }; + let query_results = list_media_files_for_user(ListMediaFileForUserArgs { username: &path.username, - limit, maybe_filter_media_type: query.filter_media_type, - maybe_offset: cursor, + page_size, + page_index, cursor_is_reversed, view_as, mysql_pool: &server_state.mysql_pool, @@ -158,27 +153,6 @@ pub async fn list_media_files_for_user_handler( } }; - let cursor_next = if let Some(id) = results_page.last_id { - let cursor = server_state.sort_key_crypto.encrypt_id(id as u64) - .map_err(|e| { - warn!("crypto error: {:?}", e); - ErrorResponse::ServerError - })?; - Some(cursor) - } else { - None - }; - - let cursor_previous = if let Some(id) = results_page.first_id { - let cursor = server_state.sort_key_crypto.encrypt_id(id as u64) - .map_err(|e| { - warn!("crypto error: {:?}", e); - ErrorResponse::ServerError - })?; - Some(cursor) - } else { - None - }; let results = results_page.records.into_iter() .map(|record| MediaFileListItem { @@ -200,10 +174,9 @@ pub async fn list_media_files_for_user_handler( let response = SuccessResponse { success: true, results, - pagination: PaginationCursors { - maybe_next: cursor_next, - maybe_previous: cursor_previous, - cursor_is_reversed, + pagination: PaginationPage{ + current: results_page.current_page, + total_page_count: results_page.total_page_count, } };