remove old tags code (#1767)

This commit is contained in:
Brandon Thomas
2026-07-10 02:31:37 -04:00
committed by GitHub
parent 1dfb62454a
commit 698c330734
28 changed files with 0 additions and 942 deletions
@@ -1,36 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n t.token as `token: tokens::tokens::tags::TagToken`,\n t.tag_value\n\nFROM\n tags AS t\nJOIN tag_uses AS u\n ON t.token = u.tag_token\nWHERE\n u.entity_type = ?\n AND u.entity_token = ?\n AND u.maybe_deleted_at IS NULL\nORDER BY t.id DESC\nLIMIT 500\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: tokens::tokens::tags::TagToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "tag_value",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false
]
},
"hash": "3929e9fe74e75fad7252681880c6c3bd7e76e96b0bf603f77f6a9b2d4fcda18d"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nDELETE FROM tag_uses\nWHERE entity_type = ?\nAND entity_token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "41b16ccf04ef1ffb8b48bc59edf9389f61f54b871f6361f35529706b98d419ec"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO tags\nSET\n token = ?,\n tag_value = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "c7e848e4c906a34ac5a4ad8cc73354baa8d5cd81170dd06d66d7f414505d538b"
}
@@ -1,2 +0,0 @@
$kind: collection
order: 8000
@@ -1,15 +0,0 @@
$kind: http-request
name: Edit Tags
url: "{{api_host}}/v1/tags/edit/model_weight/weight_84t9zeafn72wkje1x59f9rm81"
method: POST
headers:
Accept: application/json
Content-Type: application/json
Cookie: ""
body:
type: json
content: |-
{
"tags": "video games, legend of zelda"
}
order: 1000
@@ -1,6 +0,0 @@
$kind: http-request
url: "{{api_host}}/v1/tags/list/model_weight/weight_84t9zeafn72wkje1x59f9rm81"
method: GET
headers:
Accept: application/json
order: 2000
@@ -32,7 +32,6 @@ pub mod prompts;
pub mod public_event_feed;
pub mod staff_audit_logs;
pub mod stats;
pub mod tags;
pub mod trending_model_analytics;
pub mod tts;
pub mod twitch;
@@ -1,27 +0,0 @@
use crate::errors::database_insert_error::DatabaseInsertError;
use crate::utils::transactor::Transactor;
use tokens::tokens::tags::TagToken;
pub async fn create_tag(
tag: &str,
transactor: Transactor<'_, '_>,
) -> Result<TagToken, DatabaseInsertError>
{
let tag = tag.trim().to_lowercase();
let token = TagToken::generate();
let query = sqlx::query!(
r#"
INSERT INTO tags
SET
token = ?,
tag_value = ?
"#,
token,
tag,
);
let _r = transactor.execute(query).await?;
Ok(token)
}
@@ -1,64 +0,0 @@
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
use errors::AnyhowResult;
use tokens::tokens::tags::TagToken;
use crate::utils::transactor::Transactor;
pub struct EntityTag {
pub token: TagToken,
pub tag_value: String,
}
pub async fn list_tags_for_entity(
tag_entity: TagUseEntity,
transactor: Transactor<'_, '_>,
) -> AnyhowResult<Vec<EntityTag>> {
let (entity_type, entity_token) = tag_entity.get_composite_keys();
let query = sqlx::query_as!(
RawTag,
r#"
SELECT
t.token as `token: tokens::tokens::tags::TagToken`,
t.tag_value
FROM
tags AS t
JOIN tag_uses AS u
ON t.token = u.tag_token
WHERE
u.entity_type = ?
AND u.entity_token = ?
AND u.maybe_deleted_at IS NULL
ORDER BY t.id DESC
LIMIT 500
"#,
entity_type,
entity_token
);
let results = match transactor {
Transactor::Pool { pool } => {
query.fetch_all(pool).await?
},
Transactor::Connection { connection } => {
query.fetch_all(connection).await?
},
Transactor::Transaction { transaction } => {
query.fetch_all(&mut **transaction).await?
},
};
Ok(results.into_iter()
.map(|record| EntityTag {
token: record.token,
tag_value: record.tag_value,
})
.collect())
}
struct RawTag {
token: TagToken,
tag_value: String,
}
@@ -1,4 +0,0 @@
pub mod create_tag;
pub mod list_tags_for_entity;
pub mod select_matching_tags;
pub mod update_tags_for_entity;
@@ -1,78 +0,0 @@
use crate::utils::transactor::Transactor;
use errors::AnyhowResult;
use sqlx::mysql::MySqlRow;
use sqlx::{FromRow, MySql, QueryBuilder, Row};
use tokens::tokens::tags::TagToken;
pub struct MatchingTag {
pub token: TagToken,
pub tag_value: String,
}
pub async fn select_matching_tags(
tag_values: &[String],
transactor: Transactor<'_, '_>,
) -> AnyhowResult<Vec<MatchingTag>> {
if tag_values.is_empty() {
return Ok(Vec::new()) // This prevents a SQL error when the IN clause is empty.
}
let tag_values = tag_values.iter()
.map(|tag| tag.trim().to_lowercase())
.collect::<Vec<_>>();
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new(
r#"
SELECT
token,
tag_value
FROM
tags
WHERE
tag_value IN (
"#,
);
let mut separated = query_builder.separated(", ");
for tag_value in tag_values.iter() {
separated.push_bind(tag_value);
}
separated.push_unseparated(") ");
let query = query_builder.build_query_as::<RawTag>();
let results = match transactor {
Transactor::Pool { pool } => {
query.fetch_all(pool).await?
},
Transactor::Connection { connection } => {
query.fetch_all(connection).await?
},
Transactor::Transaction { transaction } => {
query.fetch_all(&mut **transaction).await?
},
};
Ok(results.into_iter()
.map(|record| MatchingTag {
token: record.token,
tag_value: record.tag_value,
})
.collect())
}
struct RawTag {
token: TagToken,
tag_value: String,
}
impl FromRow<'_, MySqlRow> for RawTag {
fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
Ok(Self {
token: TagToken::new(row.try_get("token")?),
tag_value: row.try_get("tag_value")?,
})
}
}
@@ -1,111 +0,0 @@
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
use errors::AnyhowResult;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, Executor, MySql, QueryBuilder, Transaction};
use tokens::tokens::tags::TagToken;
pub async fn update_tags_for_entity(
entity: TagUseEntity,
tags: &[TagToken],
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<()>
{
if tags.is_empty() {
delete_all_query(&entity, mysql_connection).await?;
return Ok(());
}
let mut transaction = mysql_connection.begin().await?;
delete_query(&entity, tags, &mut transaction).await?;
insert_query(&entity, tags, &mut transaction).await?;
transaction.commit().await?;
Ok(())
}
async fn delete_query(
entity: &TagUseEntity,
tags: &[TagToken],
transaction: &mut Transaction<'_, MySql>,
) -> AnyhowResult<()> {
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new("DELETE FROM tag_uses ");
query_builder.push(" WHERE entity_type = ");;
query_builder.push_bind(entity.get_entity_type().to_str());
query_builder.push(" AND entity_token = ");;
query_builder.push_bind(entity.get_entity_token_str());
query_builder.push(" AND tag_token NOT IN ( ");
let mut separated = query_builder.separated(", ");
for tag in tags.iter() {
separated.push_bind(tag.as_str());
}
separated.push_unseparated(") ");
let query = query_builder.build();
transaction.execute(query).await?;
Ok(())
}
async fn delete_all_query(
entity: &TagUseEntity,
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<()> {
let query = sqlx::query!(
r#"
DELETE FROM tag_uses
WHERE entity_type = ?
AND entity_token = ?
LIMIT 1
"#,
entity.get_entity_type().to_str(),
entity.get_entity_token_str(),
);
mysql_connection.execute(query).await?;
Ok(())
}
async fn insert_query(
entity: &TagUseEntity,
tags: &[TagToken],
transaction: &mut Transaction<'_, MySql>,
) -> AnyhowResult<()> {
if tags.is_empty() {
return Ok(()) // This prevents a SQL error when the IN clause is empty.
}
// NB: Insert ignore will insert non-duplicate rows and silently ignore duplicates
let mut query_builder: QueryBuilder<MySql> = QueryBuilder::new("INSERT IGNORE INTO tag_uses ");
query_builder.push(" (entity_type, entity_token, tag_token) VALUES ");
for (i, tag) in tags.iter().enumerate() {
query_builder.push(" ( ");;
query_builder.push_bind(entity.get_entity_type().to_str());
query_builder.push(", ");;
query_builder.push_bind(entity.get_entity_token_str());
query_builder.push(", ");;
query_builder.push_bind(tag.as_str());
query_builder.push(" ) ");
if i < tags.len() - 1 {
query_builder.push(", ");
}
}
let query = query_builder.build();
transaction.execute(query).await?;
Ok(())
}
@@ -1,4 +1,3 @@
pub mod audit_logs;
pub mod batch_generations;
pub mod featured_items;
pub mod tag_uses;
@@ -1 +0,0 @@
pub mod tag_use_entity;
@@ -1,40 +0,0 @@
use enums::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
// TODO(bt, 2024-01-16): A ref type with <'a> lifetime of inner data instead of ownership?
pub enum TagUseEntity {
MediaFile(MediaFileToken),
ModelWeight(ModelWeightToken),
}
// TODO(bt, 2024-01-16): Codegen? Make traits for these? Maybe overkill.
impl TagUseEntity {
pub fn from_entity_type_and_token(entity_type: TagUseEntityType, token: &str) -> Self {
match entity_type {
TagUseEntityType::ModelWeight => Self::ModelWeight(ModelWeightToken::new_from_str(token)),
TagUseEntityType::MediaFile => Self::MediaFile(MediaFileToken::new_from_str(token)),
}
}
pub fn get_entity_type(&self) -> TagUseEntityType {
match self {
Self::MediaFile(_) => TagUseEntityType::MediaFile,
Self::ModelWeight(_) => TagUseEntityType::ModelWeight,
}
}
pub fn get_entity_token_str(&self) -> &str {
match self {
Self::MediaFile(token) => token.as_str(),
Self::ModelWeight(token) => token.as_str(),
}
}
pub fn get_composite_keys(&self) -> (TagUseEntityType, &str) {
match self {
Self::MediaFile(token) => (TagUseEntityType::MediaFile, token.as_str()),
Self::ModelWeight(token) => (TagUseEntityType::ModelWeight, token.as_str()),
}
}
}
@@ -18,7 +18,6 @@ pub mod model_weights;
pub mod prompt_context_items;
pub mod prompts;
pub mod staff_audit_logs;
pub mod tag_uses;
pub mod trending_model_analytics;
pub mod tts_models;
pub mod uploaded_video_notes;
@@ -1 +0,0 @@
pub mod tag_use_entity_type;
@@ -1,127 +0,0 @@
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 `tag_uses` table in a `VARCHAR(32)` field named `entity_type`.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, ToSchema)]
pub enum TagUseEntityType {
/// Media files
#[serde(rename = "media_file")]
MediaFile,
/// Model weights
#[serde(rename = "model_weight")]
ModelWeight,
}
// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests!
impl_enum_display_and_debug_using_to_str!(TagUseEntityType);
impl_mysql_enum_coders!(TagUseEntityType);
/// NB: Legacy API for older code.
impl TagUseEntityType {
pub fn to_str(&self) -> &'static str {
match self {
Self::MediaFile => "media_file",
Self::ModelWeight => "model_weight",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"media_file" => Ok(Self::MediaFile),
"model_weight" => Ok(Self::ModelWeight),
_ => 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,
])
}
}
#[cfg(test)]
mod tests {
use crate::test_helpers::assert_serialization;
use crate::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
mod serde {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(TagUseEntityType::MediaFile, "media_file");
assert_serialization(TagUseEntityType::ModelWeight, "model_weight");
}
}
mod impl_methods {
use super::*;
#[test]
fn test_to_str() {
assert_eq!(TagUseEntityType::MediaFile.to_str(), "media_file");
assert_eq!(TagUseEntityType::ModelWeight.to_str(), "model_weight");
}
#[test]
fn test_from_str() {
assert_eq!(TagUseEntityType::from_str("media_file").unwrap(), TagUseEntityType::MediaFile);
assert_eq!(TagUseEntityType::from_str("model_weight").unwrap(), TagUseEntityType::ModelWeight);
assert!(TagUseEntityType::from_str("foo").is_err());
}
}
mod manual_variant_checks {
use super::*;
#[test]
fn all_variants() {
let mut variants = TagUseEntityType::all_variants();
assert_eq!(variants.len(), 2);
assert_eq!(variants.pop_first(), Some(TagUseEntityType::MediaFile));
assert_eq!(variants.pop_first(), Some(TagUseEntityType::ModelWeight));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(TagUseEntityType::all_variants().len(), TagUseEntityType::iter().len());
}
#[test]
fn round_trip() {
for variant in TagUseEntityType::all_variants() {
assert_eq!(variant, TagUseEntityType::from_str(variant.to_str()).unwrap());
assert_eq!(variant, TagUseEntityType::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, TagUseEntityType::from_str(&format!("{:?}", variant)).unwrap());
}
}
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 32;
for variant in TagUseEntityType::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);
}
}
}
}
@@ -12,7 +12,6 @@ use crate::http_server::common_responses::pagination_cursors::PaginationCursors;
use crate::http_server::common_responses::pagination_page::PaginationPage;
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
use crate::http_server::common_responses::simple_response::SimpleResponse;
use crate::http_server::common_responses::tag_info::TagInfo;
use crate::http_server::common_responses::user_details_lite::{UserDefaultAvatarInfo, UserDetailsLight};
use crate::http_server::deprecated_endpoints::conversion::enqueue_fbx_to_gltf_handler::*;
use crate::http_server::deprecated_endpoints::workflows::enqueue::enqueue_face_fusion_workflow_handler::*;
@@ -222,8 +221,6 @@ use billing_component::stripe::http_endpoints::checkout::create::stripe_create_c
use crate::http_server::endpoints::billing_fakeyou::list_active_user_subscriptions_handler::*;
use crate::http_server::endpoints::service::status_alert_handler::*;
use crate::http_server::endpoints::stats::get_unified_queue_stats_handler::*;
use crate::http_server::endpoints::tags::list_tags_for_entity_handler::*;
use crate::http_server::endpoints::tags::set_tags_for_entity_handler::*;
use crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::*;
use crate::http_server::endpoints::user_bookmarks::batch_get_user_bookmarks_handler::*;
use crate::http_server::endpoints::user_bookmarks::create_user_bookmark_handler::*;
@@ -512,8 +509,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::prompts::get_prompt_handler::get_prompt_handler,
crate::http_server::endpoints::service::status_alert_handler::status_alert_handler,
crate::http_server::endpoints::stats::get_unified_queue_stats_handler::get_unified_queue_stats_handler,
crate::http_server::endpoints::tags::list_tags_for_entity_handler::list_tags_for_entity_handler,
crate::http_server::endpoints::tags::set_tags_for_entity_handler::set_tags_for_entity_handler,
crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::enqueue_infer_tts_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,
@@ -762,7 +757,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
SimpleEntityStats,
SimpleGenericJsonSuccess,
SimpleResponse,
TagInfo,
UserDetailsLight,
VideoPreviews,
Visibility,
@@ -1101,8 +1095,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ListSessionRequestDetailsResponse,
ListSessionResultDetailsResponse,
ListSessionStatusDetailsResponse,
ListTagsForEntityPathInfo,
ListTagsForEntitySuccessResponse,
ListUserBookmarksForEntityError,
ListUserBookmarksForEntityPathInfo,
ListUserBookmarksForEntitySuccessResponse,
@@ -1168,9 +1160,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
SetModelWeightCoverImagePathInfo,
SetModelWeightCoverImageRequest,
SetModelWeightCoverImageResponse,
SetTagsForEntityPathInfo,
SetTagsForEntityRequest,
SetTagsForEntitySuccessResponse,
SetUserRatingRequest,
SetUserRatingResponse,
StatusAlertCategory,
@@ -6,7 +6,6 @@ pub mod pagination_cursors;
pub mod pagination_page;
pub mod simple_entity_stats;
pub mod simple_response;
pub mod tag_info;
pub mod user_avatars;
pub mod user_details_lite;
pub mod user_details_lite_builder;
@@ -1,8 +0,0 @@
use tokens::tokens::tags::TagToken;
use utoipa::ToSchema;
#[derive(Serialize, ToSchema)]
pub struct TagInfo {
pub token: TagToken,
pub value: String,
}
@@ -23,7 +23,6 @@ pub mod prompts;
pub mod service;
pub mod stats;
pub mod subscriptions;
pub mod tags;
pub mod trending;
pub mod tts;
pub mod user_bookmarks;
@@ -1,101 +0,0 @@
//// NB: Incrementally getting rid of build warnings...
//#![forbid(unused_imports)]
//#![forbid(unused_mut)]
//#![forbid(unused_variables)]
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_responses::tag_info::TagInfo;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest, HttpResponse};
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
use enums::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
use log::warn;
use mysql_queries::queries::tags::list_tags_for_entity::list_tags_for_entity;
use mysql_queries::utils::transactor::Transactor;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use utoipa::ToSchema;
/// For the URL PathInfo
#[derive(Deserialize, ToSchema)]
pub struct ListTagsForEntityPathInfo {
entity_type: TagUseEntityType,
entity_token: String,
}
#[derive(Serialize, ToSchema)]
pub struct ListTagsForEntitySuccessResponse {
pub success: bool,
pub tags: Vec<TagInfo>,
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
/// List the tags for an entity
#[utoipa::path(
get,
tag = "Tags",
path = "/v1/tags/list/{entity_type}/{entity_token}",
responses(
(status = 200, description = "Success", body = ListTagsForEntitySuccessResponse),
(status = 400, description = "Bad input", body = CommonWebError),
(status = 401, description = "Not authorized", body = CommonWebError),
(status = 500, description = "Server error", body = CommonWebError),
),
params(
("request" = ListTagsForEntityPathInfo, description = "Payload for Request"),
)
)]
pub async fn list_tags_for_entity_handler(
http_request: HttpRequest,
path: Path<ListTagsForEntityPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListTagsForEntitySuccessResponse>, CommonWebError>
{
let token = path.entity_token.as_str();
let token_prefix_matches = match path.entity_type {
TagUseEntityType::MediaFile => token.starts_with(MediaFileToken::token_prefix()),
TagUseEntityType::ModelWeight => token.starts_with(ModelWeightToken::token_prefix()),
};
if !token_prefix_matches {
warn!("invalid token prefix: {:?} for {:?}", path.entity_token, path.entity_type);
return Err(CommonWebError::BadInputWithSimpleMessage("invalid token prefix".to_string()));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
CommonWebError::from_error(err)
})?;
let entity = TagUseEntity::from_entity_type_and_token(
path.entity_type, &path.entity_token);
let mut tags = list_tags_for_entity(entity, Transactor::for_connection(&mut *mysql_connection))
.await
.map_err(|e| {
warn!("error listing tags: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
tags.sort_by(|a, b| a.tag_value.cmp(&b.tag_value));
Ok(Json(ListTagsForEntitySuccessResponse {
success: true,
tags: tags
.into_iter()
.map(|tag| TagInfo {
token: tag.token,
value: tag.tag_value,
})
.collect(),
}))
}
@@ -1,2 +0,0 @@
pub mod list_tags_for_entity_handler;
pub mod set_tags_for_entity_handler;
@@ -1,245 +0,0 @@
//// NB: Incrementally getting rid of build warnings...
//#![forbid(unused_imports)]
//#![forbid(unused_mut)]
//#![forbid(unused_variables)]
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_responses::tag_info::TagInfo;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::http_server::user_lookup::user_session::require_user_session_extended::require_user_session_extended;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::{Data, Json, Path};
use actix_web::{HttpRequest, HttpResponse};
use composite_identifiers::by_table::tag_uses::tag_use_entity::TagUseEntity;
use enums::by_table::tag_uses::tag_use_entity_type::TagUseEntityType;
use log::warn;
use mysql_queries::queries::media_files::get::get_media_file::get_media_file_with_transactor;
use mysql_queries::queries::model_weights::get::get_weight::get_weight_by_token_with_transactor;
use mysql_queries::queries::tags::create_tag::create_tag;
use mysql_queries::queries::tags::select_matching_tags::select_matching_tags;
use mysql_queries::queries::tags::update_tags_for_entity::update_tags_for_entity;
use mysql_queries::utils::transactor::Transactor;
use sqlx::MySqlConnection;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::tags::TagToken;
use tokens::tokens::users::UserToken;
use utoipa::ToSchema;
/// Maximum number of tags allowed per item.
const MAX_TAGS : usize = 30;
/// For the URL PathInfo
#[derive(Deserialize, ToSchema)]
pub struct SetTagsForEntityPathInfo {
entity_type: TagUseEntityType,
entity_token: String,
}
#[derive(Deserialize, ToSchema)]
pub struct SetTagsForEntityRequest {
/// A comma separated list of tags.
tags: String,
}
#[derive(Serialize, ToSchema)]
pub struct SetTagsForEntitySuccessResponse {
pub success: bool,
pub tags: Vec<TagInfo>,
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
/// Edit the tags (in bulk) for an entity. Comma-separated.
#[utoipa::path(
post,
tag = "Tags",
path = "/v1/tags/edit/{entity_type}/{entity_token}",
responses(
(status = 200, description = "Success", body = SetTagsForEntitySuccessResponse),
(status = 400, description = "Bad input", body = CommonWebError),
(status = 401, description = "Not authorized", body = CommonWebError),
(status = 500, description = "Server error", body = CommonWebError),
),
params(
("request" = SetTagsForEntityRequest, description = "Payload for Request"),
)
)]
pub async fn set_tags_for_entity_handler(
http_request: HttpRequest,
request: Json<SetTagsForEntityRequest>,
path: Path<SetTagsForEntityPathInfo>,
server_state: Data<Arc<ServerState>>,
) -> Result<Json<SetTagsForEntitySuccessResponse>, CommonWebError>
{
let token = path.entity_token.as_str();
let token_prefix_matches = match path.entity_type {
TagUseEntityType::MediaFile => token.starts_with(MediaFileToken::token_prefix()),
TagUseEntityType::ModelWeight => token.starts_with(ModelWeightToken::token_prefix()),
};
if !token_prefix_matches {
warn!("invalid token prefix: {:?} for {:?}", path.entity_token, path.entity_type);
return Err(CommonWebError::BadInputWithSimpleMessage("invalid token prefix".to_string()));
}
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
CommonWebError::from_error(err)
})?;
let user_session = require_user_session_extended(
&http_request,
&server_state.session_checker,
&mut *mysql_connection).await?;
if user_session.role.is_banned {
return Err(CommonWebError::NotAuthorized);
}
let is_mod = user_session.role.can_ban_users;
let is_creator = get_is_creator(
path.entity_type,
&path.entity_token,
&user_session.user_token_typed,
&mut *mysql_connection
).await?;
if !is_creator && !is_mod {
warn!("user is not allowed to modify entity: {:?}", user_session.user_token);
return Err(CommonWebError::NotAuthorized);
}
let request_tags = to_normalized_tags(&request.tags);
let matching_tags = select_matching_tags(&request_tags, Transactor::for_connection(&mut *mysql_connection))
.await
.map(|tags| tags.into_iter()
.map(|tag| TagInfo {
token: tag.token,
value: tag.tag_value.to_string(),
})
.collect::<Vec<TagInfo>>())
.map_err(|e| {
warn!("error selecting tags: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
let matching_tag_values = matching_tags
.iter()
.map(|tag| tag.value.to_string())
.collect::<Vec<String>>();
let new_tag_values = request_tags.iter()
.filter(|tag| !matching_tag_values.contains(tag))
.map(|tag| tag.as_str())
.collect::<Vec<&str>>();
let mut new_tags = Vec::new();
// TODO(bt): Bulk insert instead of insert per tag.
for new_tag_value in new_tag_values {
let token = create_tag(new_tag_value, Transactor::for_connection(&mut *mysql_connection))
.await
.map_err(|e| {
warn!("error creating tag: {:?}", e);
CommonWebError::from_error(e)
})?;
new_tags.push(TagInfo {
token,
value: new_tag_value.to_string(),
});
}
let final_tag_set = matching_tags.into_iter()
.chain(new_tags.into_iter())
.collect::<Vec<TagInfo>>();
let final_tag_tokens = final_tag_set.iter()
.map(|tag_info| tag_info.token.clone())
.collect::<Vec<TagToken>>();
let entity = TagUseEntity::from_entity_type_and_token(
path.entity_type, &path.entity_token);
update_tags_for_entity(
entity,
&final_tag_tokens,
&mut mysql_connection)
.await
.map_err(|e| {
warn!("error updating tags: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
Ok(Json(SetTagsForEntitySuccessResponse {
success: true,
tags: final_tag_set,
}))
}
async fn get_is_creator(
entity_type: TagUseEntityType,
entity_token: &str,
user_token: &UserToken,
mysql_connection: &mut MySqlConnection
) -> Result<bool, CommonWebError> {
match entity_type {
TagUseEntityType::MediaFile => {
let entity_token = MediaFileToken::new_from_str(entity_token);
let result = get_media_file_with_transactor(
&entity_token,
false,
Transactor::for_connection(mysql_connection),
).await;
match result {
Ok(Some(media_file)) => {
Ok(media_file.maybe_creator_user_token
.is_some_and(|t| t.as_str() == user_token.as_str()))
},
Ok(None) => Err(CommonWebError::NotFound),
Err(err) => {
warn!("Error looking up media_file: {:?}", err);
Err(CommonWebError::from_anyhow_error(err))
}
}
}
TagUseEntityType::ModelWeight => {
let entity_token = ModelWeightToken::new_from_str(entity_token);
let result = get_weight_by_token_with_transactor(
&entity_token,
false,
Transactor::for_connection(mysql_connection),
).await;
match result {
Ok(Some(model_weight)) => {
Ok(model_weight.creator_user_token.as_str() == user_token.as_str())
},
Ok(None) => Err(CommonWebError::NotFound),
Err(err) => {
warn!("Error looking up model_weight: {:?}", err);
Err(CommonWebError::from_anyhow_error(err))
}
}
}
}
}
fn to_normalized_tags(tags: &str) -> Vec<String> {
let mut tags : Vec<String> = tags.split(',')
.map(|tag| tag.trim().to_lowercase())
.filter(|tag| !tag.is_empty())
.collect();
tags.truncate(MAX_TAGS);
tags
}
@@ -16,7 +16,6 @@ use crate::http_server::routes::application_routes::omni_gen_routes::add_omni_ge
use crate::http_server::routes::application_routes::prompts_routes::add_prompts_routes;
use crate::http_server::routes::application_routes::stripe_artcraft_routes::add_stripe_artcraft_routes;
use crate::http_server::routes::application_routes::subscription_routes::add_subscription_routes;
use crate::http_server::routes::application_routes::tag_routes::add_tag_routes;
use crate::http_server::routes::application_routes::user_bookmarks_routes::add_user_bookmarks_routes;
use crate::http_server::routes::application_routes::user_rating_routes::add_user_rating_routes;
use crate::http_server::routes::application_routes::user_referral_code_routes::add_user_referral_code_routes;
@@ -55,7 +54,6 @@ where
app = add_webhook_routes(app); // /v1/webhooks/... (fal)
// Legacy FakeYou surface area that might be useful again one day
app = add_tag_routes(app); // /v1/tags
app = add_weights_routes(app); // v1/weights
// Media files routes
@@ -16,7 +16,6 @@ mod omni_gen_routes;
mod prompts_routes;
mod stripe_artcraft_routes;
mod subscription_routes;
mod tag_routes;
mod user_bookmarks_routes;
mod user_rating_routes;
mod user_referral_code_routes;
@@ -1,31 +0,0 @@
use crate::http_server::endpoints::tags::list_tags_for_entity_handler::list_tags_for_entity_handler;
use crate::http_server::endpoints::tags::set_tags_for_entity_handler::set_tags_for_entity_handler;
use actix_http::body::MessageBody;
use actix_service::ServiceFactory;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{web, App, Error, HttpResponse};
pub fn add_tag_routes<T, B>(app: App<T>) -> App<T>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = ()
>
{
app.service(
web
::scope("/v1/tags")
.service(web::resource("/list/{entity_type}/{entity_token}")
.route(web::get().to(list_tags_for_entity_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/edit/{entity_type}/{entity_token}")
.route(web::post().to(set_tags_for_entity_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
}