mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:47:53 +00:00
upsert and delete featured items (not yet routed or documented)
This commit is contained in:
1 parent
44f5e14ded
commit
d99eef6b8d
17 files changed
+709
-1
No files matched your search
@@ -0,0 +1,40 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE featured_items (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- ========== ENTITY LINKAGE ==========
|
||||
|
||||
-- The type of record this is attached to
|
||||
-- Currently supported values:
|
||||
-- * 'model_weight'
|
||||
-- * 'media_file'
|
||||
entity_type VARCHAR(32) NOT NULL,
|
||||
|
||||
-- The token for the record.
|
||||
entity_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- ========== SORTING ==========
|
||||
|
||||
-- A sorted ordering (optional) for pagination
|
||||
maybe_sort_order INT UNSIGNED DEFAULT NULL,
|
||||
|
||||
-- ========== TIMESTAMPS ==========
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (entity_type, entity_token),
|
||||
|
||||
KEY index_maybe_sort_order_asc (maybe_sort_order ASC),
|
||||
KEY index_maybe_sort_order_desc (maybe_sort_order DESC),
|
||||
|
||||
KEY index_created_at (created_at),
|
||||
KEY index_deleted_at (deleted_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
// TODO(bt, 2024-01-16): A ref type with <'a> lifetime of inner data instead of ownership?
|
||||
pub enum FeaturedItemEntity {
|
||||
MediaFile(MediaFileToken),
|
||||
ModelWeight(ModelWeightToken),
|
||||
User(UserToken),
|
||||
}
|
||||
|
||||
// TODO(bt, 2024-01-16): Codegen? Make traits for these? Maybe overkill.
|
||||
impl FeaturedItemEntity {
|
||||
|
||||
pub fn from_entity_type_and_token(entity_type: FeaturedItemEntityType, token: &str) -> Self {
|
||||
match entity_type {
|
||||
FeaturedItemEntityType::ModelWeight => Self::ModelWeight(ModelWeightToken::new_from_str(token)),
|
||||
FeaturedItemEntityType::MediaFile => Self::MediaFile(MediaFileToken::new_from_str(token)),
|
||||
FeaturedItemEntityType::User => Self::User(UserToken::new_from_str(token)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_entity_type(&self) -> FeaturedItemEntityType {
|
||||
match self {
|
||||
Self::MediaFile(_) => FeaturedItemEntityType::MediaFile,
|
||||
Self::ModelWeight(_) => FeaturedItemEntityType::ModelWeight,
|
||||
Self::User(_) => FeaturedItemEntityType::User,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_entity_token_str(&self) -> &str {
|
||||
match self {
|
||||
Self::MediaFile(token) => token.as_str(),
|
||||
Self::ModelWeight(token) => token.as_str(),
|
||||
Self::User(token) => token.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_composite_keys(&self) -> (FeaturedItemEntityType, &str) {
|
||||
match self {
|
||||
Self::MediaFile(token) => (FeaturedItemEntityType::MediaFile, token.as_str()),
|
||||
Self::ModelWeight(token) => (FeaturedItemEntityType::ModelWeight, token.as_str()),
|
||||
Self::User(token) => (FeaturedItemEntityType::User, token.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod featured_item_entity;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod audit_logs;
|
||||
pub mod batch_generations;
|
||||
pub mod featured_items;
|
||||
@@ -24,6 +24,14 @@ pub enum AuditLogEntityAction {
|
||||
/// Delete action
|
||||
#[serde(rename = "delete")]
|
||||
Delete,
|
||||
|
||||
/// Create featured item
|
||||
#[serde(rename = "featured_item_create")]
|
||||
FeaturedItemCreate,
|
||||
|
||||
/// Delete featured item
|
||||
#[serde(rename = "featured_item_delete")]
|
||||
FeaturedItemDelete,
|
||||
}
|
||||
|
||||
// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests!
|
||||
@@ -38,6 +46,8 @@ impl AuditLogEntityAction {
|
||||
Self::Edit => "edit",
|
||||
Self::EditFeatures => "edit_features",
|
||||
Self::Delete => "delete",
|
||||
Self::FeaturedItemCreate => "featured_item_create",
|
||||
Self::FeaturedItemDelete => "featured_item_delete",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +57,8 @@ impl AuditLogEntityAction {
|
||||
"edit" => Ok(Self::Edit),
|
||||
"edit_features" => Ok(Self::EditFeatures),
|
||||
"delete" => Ok(Self::Delete),
|
||||
"featured_item_create" => Ok(Self::FeaturedItemCreate),
|
||||
"featured_item_delete" => Ok(Self::FeaturedItemDelete),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -66,6 +78,8 @@ mod tests {
|
||||
assert_serialization(AuditLogEntityAction::Edit, "edit");
|
||||
assert_serialization(AuditLogEntityAction::EditFeatures, "edit_features");
|
||||
assert_serialization(AuditLogEntityAction::Delete, "delete");
|
||||
assert_serialization(AuditLogEntityAction::FeaturedItemCreate, "featured_item_create");
|
||||
assert_serialization(AuditLogEntityAction::FeaturedItemDelete, "featured_item_delete");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +92,8 @@ mod tests {
|
||||
assert_eq!(AuditLogEntityAction::Edit.to_str(), "edit");
|
||||
assert_eq!(AuditLogEntityAction::EditFeatures.to_str(), "edit_features");
|
||||
assert_eq!(AuditLogEntityAction::Delete.to_str(), "delete");
|
||||
assert_eq!(AuditLogEntityAction::FeaturedItemCreate.to_str(), "featured_item_create");
|
||||
assert_eq!(AuditLogEntityAction::FeaturedItemDelete.to_str(), "featured_item_delete");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -86,6 +102,8 @@ mod tests {
|
||||
assert_eq!(AuditLogEntityAction::from_str("edit").unwrap(), AuditLogEntityAction::Edit);
|
||||
assert_eq!(AuditLogEntityAction::from_str("edit_features").unwrap(), AuditLogEntityAction::EditFeatures);
|
||||
assert_eq!(AuditLogEntityAction::from_str("delete").unwrap(), AuditLogEntityAction::Delete);
|
||||
assert_eq!(AuditLogEntityAction::from_str("featured_item_create").unwrap(), AuditLogEntityAction::FeaturedItemCreate);
|
||||
assert_eq!(AuditLogEntityAction::from_str("featured_item_delete").unwrap(), AuditLogEntityAction::FeaturedItemDelete);
|
||||
assert!(AuditLogEntityAction::from_str("foo").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
use std::collections::BTreeSet;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Used in the `user_bookmarks` table in a `VARCHAR(32)` field named `entity_type`.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize, ToSchema)]
|
||||
pub enum FeaturedItemEntityType {
|
||||
/// MediaFile
|
||||
#[serde(rename = "media_file")]
|
||||
MediaFile,
|
||||
|
||||
/// ModelWeight (the new way to store models)
|
||||
#[serde(rename = "model_weight")]
|
||||
ModelWeight,
|
||||
|
||||
/// User
|
||||
#[serde(rename = "user")]
|
||||
User,
|
||||
}
|
||||
|
||||
// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests!
|
||||
impl_enum_display_and_debug_using_to_str!(FeaturedItemEntityType);
|
||||
impl_mysql_enum_coders!(FeaturedItemEntityType);
|
||||
impl_mysql_from_row!(FeaturedItemEntityType);
|
||||
|
||||
/// NB: Legacy API for older code.
|
||||
impl FeaturedItemEntityType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::MediaFile => "media_file",
|
||||
Self::ModelWeight => "model_weight",
|
||||
Self::User => "user",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"media_file" => Ok(Self::MediaFile),
|
||||
"model_weight" => Ok(Self::ModelWeight),
|
||||
"user" => Ok(Self::User),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
// NB: BTreeSet is sorted
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
Self::MediaFile,
|
||||
Self::ModelWeight,
|
||||
Self::User,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod serde {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(FeaturedItemEntityType::MediaFile, "media_file");
|
||||
assert_serialization(FeaturedItemEntityType::ModelWeight, "model_weight");
|
||||
assert_serialization(FeaturedItemEntityType::User, "user");
|
||||
}
|
||||
}
|
||||
|
||||
mod impl_methods {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_to_str() {
|
||||
assert_eq!(FeaturedItemEntityType::MediaFile.to_str(), "media_file");
|
||||
assert_eq!(FeaturedItemEntityType::ModelWeight.to_str(), "model_weight");
|
||||
assert_eq!(FeaturedItemEntityType::User.to_str(), "user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
assert_eq!(FeaturedItemEntityType::from_str("media_file").unwrap(), FeaturedItemEntityType::MediaFile);
|
||||
assert_eq!(FeaturedItemEntityType::from_str("model_weight").unwrap(), FeaturedItemEntityType::ModelWeight);
|
||||
assert_eq!(FeaturedItemEntityType::from_str("user").unwrap(), FeaturedItemEntityType::User);
|
||||
assert!(FeaturedItemEntityType::from_str("foo").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(FeaturedItemEntityType::all_variants().len(), FeaturedItemEntityType::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in FeaturedItemEntityType::all_variants() {
|
||||
assert_eq!(variant, FeaturedItemEntityType::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, FeaturedItemEntityType::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, FeaturedItemEntityType::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
const MAX_LENGTH : usize = 32;
|
||||
for variant in FeaturedItemEntityType::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
|
||||
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod featured_item_entity_type;
|
||||
@@ -4,6 +4,7 @@ pub mod batch_generations;
|
||||
pub mod comments;
|
||||
pub mod email_sender_jobs;
|
||||
pub mod entity_stats;
|
||||
pub mod featured_items;
|
||||
pub mod generic_download_jobs;
|
||||
pub mod generic_inference_jobs;
|
||||
pub mod generic_synthetic_ids;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use composite_identifiers::by_table::featured_items::featured_item_entity::FeaturedItemEntity;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub async fn delete_featured_item<'e, 'c, E>(
|
||||
entity: &'e FeaturedItemEntity,
|
||||
mysql_executor: E
|
||||
)
|
||||
-> AnyhowResult<()>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
let (entity_type, entity_token) = entity.get_composite_keys();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE featured_items
|
||||
SET
|
||||
deleted_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
entity_type = ?
|
||||
AND entity_token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
entity_type,
|
||||
entity_token
|
||||
)
|
||||
.execute(mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod upsert_featured_item;
|
||||
pub mod delete_featured_item;
|
||||
@@ -0,0 +1,51 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use composite_identifiers::by_table::featured_items::featured_item_entity::FeaturedItemEntity;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub struct UpsertFeaturedItemArgs<'e, 'c, E>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
pub entity: &'e FeaturedItemEntity,
|
||||
|
||||
pub mysql_executor: E,
|
||||
|
||||
// TODO: Not sure if this works to tell the compiler we need the lifetime annotation.
|
||||
// See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
pub async fn upsert_featured_item<'e, 'c : 'e, E>(
|
||||
args: UpsertFeaturedItemArgs<'e, 'c, E>,
|
||||
)
|
||||
-> AnyhowResult<()>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
|
||||
let (entity_type, entity_token) = args.entity.get_composite_keys();
|
||||
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO featured_items
|
||||
SET
|
||||
entity_type = ?,
|
||||
entity_token = ?
|
||||
ON DUPLICATE KEY UPDATE
|
||||
deleted_at = NULL
|
||||
"#,
|
||||
entity_type,
|
||||
entity_token,
|
||||
)
|
||||
.execute(args.mysql_executor)
|
||||
.await;
|
||||
|
||||
let _record_id = match query_result {
|
||||
Ok(res) => res.last_insert_id(),
|
||||
Err(err) => return Err(anyhow!("Mysql error: {:?}", err)),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod billing;
|
||||
pub mod comments;
|
||||
pub mod email_sender_jobs;
|
||||
pub mod entity_stats;
|
||||
pub mod featured_items;
|
||||
pub mod generic_download;
|
||||
pub mod generic_inference;
|
||||
pub mod generic_synthetic_ids;
|
||||
|
||||
+2
@@ -9,6 +9,8 @@ use tokens::tokens::w2l_results::W2lResultToken;
|
||||
use tokens::tokens::w2l_templates::W2lTemplateToken;
|
||||
use tokens::tokens::zs_voices::ZsVoiceToken;
|
||||
|
||||
// TODO(bt,2024-05-05): Make this a composite_identifier enum
|
||||
|
||||
pub enum UserBookmarkEntityToken {
|
||||
User(UserToken),
|
||||
ModelWeight(ModelWeightToken),
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use log::{error, warn};
|
||||
use r2d2_redis::redis::transaction;
|
||||
use sqlx::Acquire;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use composite_identifiers::by_table::audit_logs::audit_log_entity::AuditLogEntity;
|
||||
use composite_identifiers::by_table::featured_items::featured_item_entity::FeaturedItemEntity;
|
||||
use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction;
|
||||
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::audit_logs::insert_audit_log::{insert_audit_log, InsertAuditLogArgs};
|
||||
use mysql_queries::queries::audit_logs::transactional_insert_audit_log::{transactional_insert_audit_log, TransactionalInsertAuditLogArgs};
|
||||
use mysql_queries::queries::entity_stats::stats_entity_token::StatsEntityToken;
|
||||
use mysql_queries::queries::entity_stats::upsert_entity_stats_on_bookmark_event::{BookmarkAction, upsert_entity_stats_on_bookmark_event, UpsertEntityStatsArgs};
|
||||
use mysql_queries::queries::featured_items::upsert_featured_item::{upsert_featured_item, UpsertFeaturedItemArgs};
|
||||
use mysql_queries::queries::users::user_bookmarks::get_total_bookmark_count_for_entity::get_total_bookmark_count_for_entity;
|
||||
use mysql_queries::queries::users::user_bookmarks::get_user_bookmark_transactional_locking::{BookmarkIdentifier, get_user_bookmark_transactional_locking};
|
||||
use mysql_queries::queries::users::user_bookmarks::user_bookmark_entity_token::UserBookmarkEntityToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::tts_models::TtsModelToken;
|
||||
use tokens::tokens::tts_results::TtsResultToken;
|
||||
use tokens::tokens::user_bookmarks::UserBookmarkToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use tokens::tokens::voice_conversion_models::VoiceConversionModelToken;
|
||||
use tokens::tokens::w2l_results::W2lResultToken;
|
||||
use tokens::tokens::w2l_templates::W2lTemplateToken;
|
||||
use tokens::tokens::zs_voices::ZsVoiceToken;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::server_state::ServerState;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateFeaturedItemRequest {
|
||||
entity_token: String,
|
||||
entity_type: FeaturedItemEntityType,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CreateFeaturedItemSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum CreateFeaturedItemError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for CreateFeaturedItemError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
CreateFeaturedItemError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
CreateFeaturedItemError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
CreateFeaturedItemError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
CreateFeaturedItemError::BadInput(reason) => reason.to_string(),
|
||||
CreateFeaturedItemError::NotAuthorized => "unauthorized".to_string(),
|
||||
CreateFeaturedItemError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for CreateFeaturedItemError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Featured Items",
|
||||
path = "/v1/featured_item/create",
|
||||
request_body = CreateFeaturedItemRequest,
|
||||
responses(
|
||||
(status = 200, body = CreateFeaturedItemSuccessResponse),
|
||||
(status = 400, body = CreateFeaturedItemError),
|
||||
)
|
||||
)]
|
||||
pub async fn create_featured_item_handler(
|
||||
http_request: HttpRequest,
|
||||
request: web::Json<CreateFeaturedItemRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<HttpResponse, CreateFeaturedItemError>
|
||||
{
|
||||
// NB(bt,2023-12-14): Kasisnu found that we're getting entity type mismatches in production. Apart from
|
||||
// querying the database for entity existence, this is the next best way to prevent incorrect comment
|
||||
// attachment. This is a bit of a bad process, though, since the token types are supposed to be opaque.
|
||||
let token = request.entity_token.as_str();
|
||||
let token_prefix_matches = match request.entity_type {
|
||||
// NB: Users had an older prefix (U:) that got replaced with the new prefix (user_)
|
||||
FeaturedItemEntityType::User => token.starts_with(UserToken::token_prefix()) || token.starts_with("U:"),
|
||||
FeaturedItemEntityType::MediaFile => token.starts_with(MediaFileToken::token_prefix()),
|
||||
FeaturedItemEntityType::ModelWeight => token.starts_with(ModelWeightToken::token_prefix()),
|
||||
};
|
||||
|
||||
if !token_prefix_matches {
|
||||
warn!("invalid token prefix: {:?} for {:?}", request.entity_token, request.entity_type);
|
||||
return Err(CreateFeaturedItemError::BadInput("invalid token prefix".to_string()));
|
||||
}
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
CreateFeaturedItemError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
CreateFeaturedItemError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(CreateFeaturedItemError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
let is_mod = user_session.can_ban_users;
|
||||
|
||||
if !is_mod {
|
||||
warn!("not moderator");
|
||||
return Err(CreateFeaturedItemError::NotAuthorized);
|
||||
}
|
||||
|
||||
let entity = FeaturedItemEntity::from_entity_type_and_token(
|
||||
request.entity_type, &request.entity_token);
|
||||
|
||||
let mut transaction = mysql_connection.begin().await
|
||||
.map_err(|err| {
|
||||
error!("error creating transaction: {:?}", err);
|
||||
CreateFeaturedItemError::ServerError
|
||||
})?;
|
||||
|
||||
let upsert_result = upsert_featured_item(UpsertFeaturedItemArgs {
|
||||
entity: &entity,
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: Default::default(),
|
||||
}).await;
|
||||
|
||||
if let Err(err) = upsert_result {
|
||||
warn!("error upserting user_bookmark: {:?}", err);
|
||||
return Err(CreateFeaturedItemError::ServerError);
|
||||
}
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
// NB: fail open
|
||||
let _r = transactional_insert_audit_log(TransactionalInsertAuditLogArgs {
|
||||
entity: &AuditLogEntity::User(user_session.user_token.clone()),
|
||||
entity_action: AuditLogEntityAction::FeaturedItemCreate,
|
||||
maybe_actor_user_token: Some(&user_session.user_token),
|
||||
maybe_actor_anonymous_visitor_token: None,
|
||||
actor_ip_address: &ip_address,
|
||||
is_actor_moderator: true,
|
||||
transaction: &mut transaction,
|
||||
}).await;
|
||||
|
||||
transaction.commit().await
|
||||
.map_err(|err| {
|
||||
error!("error committing transaction: {:?}", err);
|
||||
CreateFeaturedItemError::ServerError
|
||||
})?;
|
||||
|
||||
let response = CreateFeaturedItemSuccessResponse {
|
||||
success: true,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|_e| CreateFeaturedItemError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
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, info, warn};
|
||||
use sqlx::Acquire;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use composite_identifiers::by_table::audit_logs::audit_log_entity::AuditLogEntity;
|
||||
use composite_identifiers::by_table::featured_items::featured_item_entity::FeaturedItemEntity;
|
||||
use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction;
|
||||
use enums::by_table::featured_items::featured_item_entity_type::FeaturedItemEntityType;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::audit_logs::insert_audit_log::{insert_audit_log, InsertAuditLogArgs};
|
||||
use mysql_queries::queries::audit_logs::transactional_insert_audit_log::{transactional_insert_audit_log, TransactionalInsertAuditLogArgs};
|
||||
use mysql_queries::queries::entity_stats::stats_entity_token::StatsEntityToken;
|
||||
use mysql_queries::queries::entity_stats::upsert_entity_stats_on_bookmark_event::{BookmarkAction, upsert_entity_stats_on_bookmark_event, UpsertEntityStatsArgs};
|
||||
use mysql_queries::queries::featured_items::delete_featured_item::delete_featured_item;
|
||||
use mysql_queries::queries::users::user_bookmarks::delete_user_bookmark::delete_user_bookmark;
|
||||
use mysql_queries::queries::users::user_bookmarks::get_user_bookmark_transactional_locking::{BookmarkIdentifier, get_user_bookmark_transactional_locking};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::user_bookmarks::UserBookmarkToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::http_server::endpoints::featured_items::create_featured_item_handler::CreateFeaturedItemError;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::server_state::ServerState;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct DeleteFeaturedItemRequest {
|
||||
entity_token: String,
|
||||
entity_type: FeaturedItemEntityType,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum DeleteFeaturedItemError {
|
||||
BadInput(String),
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for DeleteFeaturedItemError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
DeleteFeaturedItemError::BadInput(_) => StatusCode::BAD_REQUEST,
|
||||
DeleteFeaturedItemError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
DeleteFeaturedItemError::NotFound => StatusCode::NOT_FOUND,
|
||||
DeleteFeaturedItemError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
DeleteFeaturedItemError::BadInput(reason) => reason.to_string(),
|
||||
DeleteFeaturedItemError::NotAuthorized => "unauthorized".to_string(),
|
||||
DeleteFeaturedItemError::NotFound => "not found".to_string(),
|
||||
DeleteFeaturedItemError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for DeleteFeaturedItemError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
tag = "Featured Items",
|
||||
path = "/v1/featured_item/delete",
|
||||
params(
|
||||
("user_bookmark_token", description = "UserBookmarkToken"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Delete User Bookmark", body = SimpleGenericJsonSuccess),
|
||||
(status = 400, description = "Bad input", body = DeleteFeaturedItemError),
|
||||
(status = 401, description = "Not authorized", body = DeleteFeaturedItemError),
|
||||
(status = 404, description = "Not found", body = DeleteFeaturedItemError),
|
||||
(status = 500, description = "Server error", body = DeleteFeaturedItemError),
|
||||
),
|
||||
)]
|
||||
pub async fn delete_user_bookmark_handler(
|
||||
http_request: HttpRequest,
|
||||
request: web::Json<DeleteFeaturedItemRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, DeleteFeaturedItemError> {
|
||||
|
||||
// NB(bt,2023-12-14): Kasisnu found that we're getting entity type mismatches in production. Apart from
|
||||
// querying the database for entity existence, this is the next best way to prevent incorrect comment
|
||||
// attachment. This is a bit of a bad process, though, since the token types are supposed to be opaque.
|
||||
let token = request.entity_token.as_str();
|
||||
let token_prefix_matches = match request.entity_type {
|
||||
// NB: Users had an older prefix (U:) that got replaced with the new prefix (user_)
|
||||
FeaturedItemEntityType::User => token.starts_with(UserToken::token_prefix()) || token.starts_with("U:"),
|
||||
FeaturedItemEntityType::MediaFile => token.starts_with(MediaFileToken::token_prefix()),
|
||||
FeaturedItemEntityType::ModelWeight => token.starts_with(ModelWeightToken::token_prefix()),
|
||||
};
|
||||
|
||||
if !token_prefix_matches {
|
||||
warn!("invalid token prefix: {:?} for {:?}", request.entity_token, request.entity_type);
|
||||
return Err(DeleteFeaturedItemError::BadInput("invalid token prefix".to_string()));
|
||||
}
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
DeleteFeaturedItemError::ServerError
|
||||
})?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
DeleteFeaturedItemError::ServerError
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
return Err(DeleteFeaturedItemError::NotAuthorized);
|
||||
}
|
||||
};
|
||||
|
||||
let is_mod = user_session.can_ban_users;
|
||||
|
||||
if !is_mod {
|
||||
warn!("not moderator");
|
||||
return Err(DeleteFeaturedItemError::NotAuthorized);
|
||||
}
|
||||
|
||||
let entity = FeaturedItemEntity::from_entity_type_and_token(
|
||||
request.entity_type, &request.entity_token);
|
||||
|
||||
let mut transaction = mysql_connection.begin().await
|
||||
.map_err(|err| {
|
||||
error!("error creating transaction: {:?}", err);
|
||||
DeleteFeaturedItemError::ServerError
|
||||
})?;
|
||||
|
||||
let delete_result = delete_featured_item(
|
||||
&entity,
|
||||
&mut *transaction,
|
||||
).await;
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
// NB: fail open
|
||||
let _r = transactional_insert_audit_log(TransactionalInsertAuditLogArgs {
|
||||
entity: &AuditLogEntity::User(user_session.user_token.clone()),
|
||||
entity_action: AuditLogEntityAction::FeaturedItemDelete,
|
||||
maybe_actor_user_token: Some(&user_session.user_token),
|
||||
maybe_actor_anonymous_visitor_token: None,
|
||||
actor_ip_address: &ip_address,
|
||||
is_actor_moderator: true,
|
||||
transaction: &mut transaction,
|
||||
}).await;
|
||||
|
||||
transaction.commit().await
|
||||
.map_err(|err| {
|
||||
error!("error committing transaction: {:?}", err);
|
||||
DeleteFeaturedItemError::ServerError
|
||||
})?;
|
||||
|
||||
match delete_result {
|
||||
Ok(_) => {},
|
||||
Err(_err) => {
|
||||
return Err(DeleteFeaturedItemError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(simple_json_success())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod create_featured_item_handler;
|
||||
pub mod delete_featured_item_handler;
|
||||
@@ -6,6 +6,7 @@ pub mod conversion;
|
||||
pub mod download_job;
|
||||
pub mod engine;
|
||||
pub mod events;
|
||||
pub mod featured_items;
|
||||
pub mod flags;
|
||||
pub mod image_gen;
|
||||
pub mod inference_job;
|
||||
@@ -30,6 +31,6 @@ pub mod vocoders;
|
||||
pub mod voice_clone_requests;
|
||||
pub mod voice_conversion;
|
||||
pub mod voice_designer;
|
||||
pub mod w2l;
|
||||
pub mod weights;
|
||||
pub mod workflows;
|
||||
pub mod w2l;
|
||||
Reference in new issue
Block a user