make media endpoint for profiles follow public pagination

This commit is contained in:
kasisnu committed 2023-12-14 16:27:21 +05:30
1 parent b85415c03a
commit ff7ea2a4db
5 files changed
+96 -494

No files matched your search

@@ -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"
}
@@ -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"
}
@@ -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<MediaFileListItem>,
pub sort_ascending: bool,
/// ID of the first record.
pub first_id: Option<i64>,
/// ID of the last record.
pub last_id: Option<i64>,
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<MediaFileType>,
pub maybe_offset: Option<usize>,
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<MediaFileListPage> {
/// 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::<i64>();
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::<MediaFileListItemInternal>();
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<MediaFileType>,
username: &'a str,
limit: usize,
maybe_offset: Option<usize>,
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<MySql> = 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
}
@@ -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)]
@@ -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<bool>,
pub limit: Option<usize>,
pub page_size: Option<usize>,
pub page_index: Option<usize>,
pub cursor: Option<String>,
pub cursor_is_reversed: Option<bool>,
pub filter_media_type: Option<MediaFileType>,
@@ -37,7 +39,7 @@ pub struct QueryParams {
pub struct SuccessResponse {
pub success: bool,
pub results: Vec<MediaFileListItem>,
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,
}
};