Merge pull request #265 from storytold/brandon-batch-get-user-bookmarks

batch get user bookmarks
This commit is contained in:
Brandon Thomas authored and GitHub committed 2024-01-03 05:04:08 -05:00
commit 6aef6fa99e
7 files changed
+291 -3

No files matched your search

@@ -0,0 +1,83 @@
use std::collections::HashSet;
use anyhow::anyhow;
use log::error;
use sqlx::{MySql, QueryBuilder};
use sqlx::pool::PoolConnection;
use enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType;
use errors::AnyhowResult;
use tokens::tokens::user_bookmarks::UserBookmarkToken;
use tokens::tokens::users::UserToken;
pub struct BatchUserBookmark {
pub token: UserBookmarkToken,
pub entity_token: String,
pub entity_type: UserBookmarkEntityType,
}
pub async fn batch_get_user_bookmarks(
user_token: &UserToken,
tokens: &HashSet<String>,
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<Vec<BatchUserBookmark>>
{
if tokens.len() == 0 {
return Ok(Vec::new());
}
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
r#"
SELECT
token,
entity_type,
entity_token
FROM user_bookmarks
WHERE user_token =
"#);
query_builder.push_bind(user_token.as_str());
query_builder.push(" AND entity_token IN ( ");
// NB: Syntax will be wrong if list has zero length
let mut separated = query_builder.separated(", ");
for token in tokens.iter() {
separated.push_bind(token);
}
separated.push_unseparated(") ");
let query = query_builder.build_query_as::<RawRating>();
let maybe_results = query.fetch_all(&mut **mysql_connection).await;
match maybe_results {
Ok(records) => Ok(records
.into_iter()
.map(|record| BatchUserBookmark {
token: UserBookmarkToken::new_from_str(&record.token),
// NB: Fail open; W2lTemplates are dead, so this is a good sentinel value
entity_type: UserBookmarkEntityType::from_str(&record.entity_type)
.unwrap_or(UserBookmarkEntityType::W2lTemplate),
entity_token: record.entity_token,
}).collect::<Vec<_>>()),
Err(err) => match err {
sqlx::Error::RowNotFound => Ok(Vec::new()),
_ => {
error!("error querying : {:?}", err);
Err(anyhow!("error querying : {:?}", err))
}
}
}
}
#[derive(sqlx::FromRow)]
struct RawRating {
token: String,
entity_token: String,
entity_type: String,
}
@@ -1,3 +1,4 @@
pub mod batch_get_user_bookmarks;
pub mod create_user_bookmark;
pub mod delete_user_bookmark;
pub mod get_total_bookmark_count_for_entity;
@@ -15,7 +15,6 @@ pub struct BatchUserRating {
pub rating_value: UserRatingValue,
}
/// Look up the user rating
pub async fn batch_get_user_ratings(
user_token: &UserToken,
tokens: &HashSet<String>,
@@ -70,8 +69,8 @@ WHERE user_token =
Err(err) => match err {
sqlx::Error::RowNotFound => Ok(Vec::new()),
_ => {
error!("error querying job record: {:?}", err);
Err(anyhow!("error querying job record: {:?}", err))
error!("error querying : {:?}", err);
Err(anyhow!("error querying : {:?}", err))
}
}
}
@@ -25,6 +25,7 @@ use crate::http_server::endpoints::media_files::list_media_files::*;
use crate::http_server::endpoints::media_files::list_media_files_for_user::*;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::endpoints::media_files::upload_media_file::*;
use crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::*;
use crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::*;
use crate::http_server::endpoints::user_bookmarks::delete_user_bookmark_handler::*;
use crate::http_server::endpoints::user_bookmarks::list_user_bookmarks_for_entity_handler::*;
@@ -49,6 +50,7 @@ use crate::http_server::web_utils::response_success_helpers::*;
crate::http_server::endpoints::media_files::list_media_files::list_media_files_handler,
crate::http_server::endpoints::media_files::list_media_files_for_user::list_media_files_for_user_handler,
crate::http_server::endpoints::media_files::upload_media_file::upload_media_file_handler,
crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::batch_get_user_bookmarks_handler,
crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::create_user_bookmark_handler,
crate::http_server::endpoints::user_bookmarks::delete_user_bookmark_handler::delete_user_bookmark_handler,
crate::http_server::endpoints::user_bookmarks::list_user_bookmarks_for_entity_handler::list_user_bookmarks_for_entity_handler,
@@ -104,6 +106,7 @@ use crate::http_server::web_utils::response_success_helpers::*;
ListWeightsByUserError,ListWeightsByUserSuccessResponse,ListWeightsByUserPathInfo,Weight,
WeightsData,MediaFileData,
BatchGetUserRatingQueryParams,BatchGetUserRatingResponse,BatchGetUserRatingError,RatingRow,
BatchGetUserBookmarksQueryParams,BatchGetUserBookmarksResponse,BatchGetUserBookmarksError,BookmarkRow,
ListFeaturedMediaFilesSuccessResponse,MediaFile, ListFeaturedMediaFilesError,
ListMediaFilesSuccessResponse, ListMediaFilesError, MediaFileListItem, ListMediaFilesQueryParams,
ListMediaFilesForUserSuccessResponse,ListMediaFilesForUserQueryParams,ListMediaFilesForUserError,ListMediaFilesForUserPathInfo,
@@ -0,0 +1,199 @@
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, ResponseError, web};
use actix_web::http::StatusCode;
use actix_web_lab::extract::Query;
use log::error;
use utoipa::{IntoParams, ToSchema};
use enums::by_table::user_bookmarks::user_bookmark_entity_type::UserBookmarkEntityType;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use mysql_queries::queries::user_bookmarks::batch_get_user_bookmarks::{batch_get_user_bookmarks, BatchUserBookmark};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::tts_models::TtsModelToken;
use crate::server_state::ServerState;
const MAX_BATCH_SIZE : usize = 200;
// =============== Request ===============
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct BatchGetUserBookmarksQueryParams {
/// A grab bag of tokens of various types.
/// Technically we should pair with token types, as that's the unique index.
/// But since each token family generally has its own prefix, this should be fine.
///
/// NB: We're using actix_web_lab's Query<T>, because the default actix_web Query<T> doesn't support URL
/// decoding sequences yet.
/// See https://github.com/actix/actix-web/issues/1301
///
pub tokens: HashSet<String>,
}
// =============== Success Response ===============
#[derive(Serialize, ToSchema)]
pub struct BatchGetUserBookmarksResponse {
pub success: bool,
/// Ratings on each item passed to us.
pub bookmarks: Vec<BookmarkRow>,
}
#[derive(Serialize, ToSchema)]
pub struct BookmarkRow {
/// The passed token
pub entity_token: String,
/// The type of entity
pub entity_type: UserBookmarkEntityType,
/// Whether the entity is bookmarked or not
pub is_bookmarked: bool,
}
// =============== Error Response ===============
#[derive(Debug, Serialize, ToSchema)]
pub enum BatchGetUserBookmarksError {
BadInput(String),
NotAuthorized,
ServerError,
}
impl ResponseError for BatchGetUserBookmarksError {
fn status_code(&self) -> StatusCode {
match *self {
BatchGetUserBookmarksError::BadInput(_) => StatusCode::BAD_REQUEST,
BatchGetUserBookmarksError::NotAuthorized => StatusCode::UNAUTHORIZED,
BatchGetUserBookmarksError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
serialize_as_json_error(self)
}
}
// NB: Not using DeriveMore since Clion doesn't understand it.
impl std::fmt::Display for BatchGetUserBookmarksError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
// =============== Handler ===============
#[utoipa::path(
get,
path = "/v1/user_bookmarks/batch",
params(
BatchGetUserBookmarksQueryParams,
),
responses(
(status = 200, description = "List User Bookmarks", body = BatchGetUserBookmarksResponse),
(status = 400, description = "Bad input", body = BatchGetUserBookmarksError),
(status = 401, description = "Not authorized", body = BatchGetUserBookmarksError),
(status = 500, description = "Server error", body = BatchGetUserBookmarksError),
),
)]
pub async fn batch_get_user_bookmarks_handler(
http_request: HttpRequest,
query: Query<BatchGetUserBookmarksQueryParams>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, BatchGetUserBookmarksError>
{
let mut mysql_connection = server_state.mysql_pool.acquire()
.await
.map_err(|e| {
error!("Could not acquire DB pool: {:?}", e);
BatchGetUserBookmarksError::ServerError
})?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
error!("Session checker error: {:?}", e);
BatchGetUserBookmarksError::ServerError
})?;
// NB: Force move of tokens from the Query<T>.
// The auto-magical Query<T> will ordinarily try to force a Copy, which isn't on HashSet.
let mut tokens = query.0.tokens;
// Don't allow bad actors to flood our DB.
tokens.shrink_to(MAX_BATCH_SIZE);
let user_session = match maybe_user_session {
Some(session) => session,
None => {
let bookmarks = fill_in_missed_bookmarks(&tokens, Vec::new());
// NB: Just return "neutral" for everything.
return Ok(HttpResponse::Ok()
.content_type("application/json")
.json(BatchGetUserBookmarksResponse {
success: true,
bookmarks,
}));
}
};
batch_get_user_bookmarks(
&user_session.user_token_typed,
&tokens,
&mut mysql_connection
).await
.map_err(|e| {
error!("Batch get user ratings DB error: {:?}", e);
BatchGetUserBookmarksError::ServerError
})
.map(|bookmarks| {
HttpResponse::Ok()
.content_type("application/json")
.json(BatchGetUserBookmarksResponse {
success: true,
bookmarks: fill_in_missed_bookmarks(&tokens, bookmarks),
})
})
}
fn fill_in_missed_bookmarks(request_tokens: &HashSet<String>, db_response: Vec<BatchUserBookmark>) -> Vec<BookmarkRow> {
let mut outputs = HashMap::with_capacity(request_tokens.len());
for record in db_response.into_iter() {
outputs.insert(record.entity_token.clone(), BookmarkRow {
entity_token: record.entity_token,
entity_type: record.entity_type,
is_bookmarked: true,
});
}
for request_token in request_tokens.iter() {
if !outputs.contains_key(request_token) {
outputs.insert(request_token.clone(), BookmarkRow {
entity_token: request_token.clone(),
entity_type: {
if request_token.starts_with(MediaFileToken::token_prefix()) {
UserBookmarkEntityType::MediaFile
} else if request_token.starts_with(ModelWeightToken::token_prefix()) {
UserBookmarkEntityType::ModelWeight
} else if request_token.starts_with(TtsModelToken::token_prefix()) {
UserBookmarkEntityType::TtsModel
} else {
// NB: Fail open; W2lTemplates are dead, so this is a good sentinel value
UserBookmarkEntityType::W2lTemplate
}
},
is_bookmarked: false,
});
}
}
outputs.into_iter()
.map(|(_key, value)| value)
.collect::<Vec<_>>()
}
@@ -1,3 +1,4 @@
pub mod batch_get_user_bookmarks_handler;
pub mod create_user_bookmark_handler;
pub mod delete_user_bookmark_handler;
pub mod list_user_bookmarks_for_entity_handler;
@@ -108,6 +108,7 @@ use crate::http_server::endpoints::twitch::oauth::check_oauth_status::check_oaut
use crate::http_server::endpoints::twitch::oauth::oauth_begin_json::oauth_begin_enroll_json;
use crate::http_server::endpoints::twitch::oauth::oauth_begin_redirect::oauth_begin_enroll_redirect;
use crate::http_server::endpoints::twitch::oauth::oauth_end::oauth_end_enroll_from_redirect;
use crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::batch_get_user_bookmarks_handler;
use crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::create_user_bookmark_handler;
use crate::http_server::endpoints::user_bookmarks::delete_user_bookmark_handler::delete_user_bookmark_handler;
use crate::http_server::endpoints::user_bookmarks::list_user_bookmarks_for_entity_handler::list_user_bookmarks_for_entity_handler;
@@ -210,6 +211,7 @@ pub fn add_routes<T, B> (app: App<T>, server_environment: ServerEnvironment) ->
// ==================== User Bookmarks ====================
let mut app = RouteBuilder::from_app(app)
.add_get("/v1/user_bookmarks/batch", batch_get_user_bookmarks_handler)
.add_post("/v1/user_bookmarks/create", create_user_bookmark_handler)
.add_post("/v1/user_bookmarks/delete/{user_bookmark_token}", delete_user_bookmark_handler)
.add_get("/v1/user_bookmarks/list/session", list_user_bookmarks_for_session_handler)