mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-01 16:08:30 +00:00
Rating type instead of bool, upsert query, enums
This commit is contained in:
1 parent
bcbb36a9ed
commit
1bb0dda330
10 files changed
+332
-1
No files matched your search
Generated
+1
@@ -6,6 +6,7 @@
|
||||
<w>cashapp</w>
|
||||
<w>cheermote</w>
|
||||
<w>crockford</w>
|
||||
<w>downvote</w>
|
||||
<w>fakeyou</w>
|
||||
<w>ffprobe</w>
|
||||
<w>hifigan</w>
|
||||
|
||||
@@ -20,6 +20,14 @@ CREATE TABLE user_ratings (
|
||||
-- Whether the vote is up/positive (TRUE) or down/negative (FALSE)
|
||||
is_positive_rating BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- Whether the vote is "soft deleted" (="neutral"), up/positive, or down/negative.
|
||||
-- Rather than a nullable bool for ratings, we'll provide an enum.
|
||||
rating_type ENUM(
|
||||
'neutral',
|
||||
'positive',
|
||||
'negative'
|
||||
) NOT NULL DEFAULT 'neutral',
|
||||
|
||||
-- For abuse tracking.
|
||||
-- Wide enough for IPv4/6
|
||||
vote_ip_address VARCHAR(40) NOT NULL,
|
||||
@@ -38,6 +46,7 @@ CREATE TABLE user_ratings (
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (user_token, entity_type, entity_token),
|
||||
KEY index_entity_type_entity_token (entity_type, entity_token),
|
||||
KEY index_user_token (user_token)
|
||||
KEY index_user_token (user_token),
|
||||
KEY index_rating_type (rating_type)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod media_uploads;
|
||||
pub mod model_categories;
|
||||
pub mod trending_model_analytics;
|
||||
pub mod user_ratings;
|
||||
@@ -0,0 +1,93 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
|
||||
/// Used in the `user_ratings` table in a `VARCHAR(32)` field named `entity_type`.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Deserialize, Serialize)]
|
||||
pub enum UserRatingEntityType {
|
||||
/// TTS model (architecture does not matter)
|
||||
#[serde(rename = "tts_model")]
|
||||
TtsModel,
|
||||
|
||||
/// TTS result (architecture does not matter)
|
||||
#[serde(rename = "tts_result")]
|
||||
TtsResult,
|
||||
|
||||
/// W2L template
|
||||
#[serde(rename = "w2l_template")]
|
||||
W2lTemplate,
|
||||
|
||||
/// W2L result
|
||||
#[serde(rename = "w2l_result")]
|
||||
W2lResult,
|
||||
}
|
||||
|
||||
// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests!
|
||||
impl_enum_display_and_debug_using_to_str!(UserRatingEntityType);
|
||||
impl_enum_sqlx_coders!(UserRatingEntityType);
|
||||
|
||||
/// NB: Legacy API for older code.
|
||||
impl UserRatingEntityType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::TtsModel => "tts_model",
|
||||
Self::TtsResult => "tts_result",
|
||||
Self::W2lTemplate => "w2l_template",
|
||||
Self::W2lResult => "w2l_result",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"tts_model" => Ok(Self::TtsModel),
|
||||
"tts_result" => Ok(Self::TtsResult),
|
||||
"w2l_template" => Ok(Self::W2lTemplate),
|
||||
"w2l_result" => Ok(Self::W2lResult),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::by_table::user_ratings::entity_type::UserRatingEntityType;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod serde {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(UserRatingEntityType::TtsModel, "tts_model");
|
||||
assert_serialization(UserRatingEntityType::TtsResult, "tts_result");
|
||||
assert_serialization(UserRatingEntityType::W2lTemplate, "w2l_template");
|
||||
assert_serialization(UserRatingEntityType::W2lResult, "w2l_result");
|
||||
}
|
||||
}
|
||||
|
||||
mod impl_methods {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_to_str() {
|
||||
assert_eq!(UserRatingEntityType::TtsModel.to_str(), "tts_model");
|
||||
assert_eq!(UserRatingEntityType::TtsResult.to_str(), "tts_result");
|
||||
assert_eq!(UserRatingEntityType::W2lTemplate.to_str(), "w2l_template");
|
||||
assert_eq!(UserRatingEntityType::W2lResult.to_str(), "w2l_result");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
assert_eq!(UserRatingEntityType::from_str("tts_model").unwrap(), UserRatingEntityType::TtsModel);
|
||||
assert_eq!(UserRatingEntityType::from_str("tts_result").unwrap(), UserRatingEntityType::TtsResult);
|
||||
assert_eq!(UserRatingEntityType::from_str("w2l_template").unwrap(), UserRatingEntityType::W2lTemplate);
|
||||
assert_eq!(UserRatingEntityType::from_str("w2l_result").unwrap(), UserRatingEntityType::W2lResult);
|
||||
assert!(UserRatingEntityType::from_str("foo").is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod entity_type;
|
||||
pub mod rating_type;
|
||||
@@ -0,0 +1,161 @@
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
|
||||
/// UserRatingType
|
||||
///
|
||||
/// - Used in the `user_ratings` table as an `ENUM` field named `rating_type`.
|
||||
/// - Used in the HTTP API.
|
||||
///
|
||||
/// To use this in a query, the query must have type annotations.
|
||||
/// See: https://www.gitmemory.com/issue/launchbadge/sqlx/1241/847154375
|
||||
/// eg. preferred_tts_result_visibility as `rating_type: enums::by_table::user_ratings::rating_type::UserRatingType`
|
||||
///
|
||||
/// See also: https://docs.rs/sqlx/0.4.0-beta.1/sqlx/trait.Type.html
|
||||
///
|
||||
/// *DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY!*
|
||||
///
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize, sqlx::Type)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[sqlx(rename_all = "lowercase")]
|
||||
pub enum UserRatingType {
|
||||
/// This is considered a ratings "soft deletion" and does not count towards a total score.
|
||||
/// This is the default rating.
|
||||
Neutral,
|
||||
/// This is a positive vote / upvote / like.
|
||||
/// They are available to non-logged-in users as long as they have the URL.
|
||||
Positive,
|
||||
/// This is a negative vote / downvote / dislike.
|
||||
Negative,
|
||||
}
|
||||
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(UserRatingType);
|
||||
|
||||
impl Default for UserRatingType {
|
||||
fn default() -> Self { Self::Neutral }
|
||||
}
|
||||
|
||||
/// NB: Legacy API for older code.
|
||||
impl UserRatingType {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Neutral => "neutral",
|
||||
Self::Positive => "positive",
|
||||
Self::Negative => "negative",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"neutral" => Ok(Self::Neutral),
|
||||
"positive" => Ok(Self::Positive),
|
||||
"negative" => Ok(Self::Negative),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::by_table::user_ratings::rating_type::UserRatingType;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(UserRatingType::Neutral, "neutral");
|
||||
assert_serialization(UserRatingType::Positive, "positive");
|
||||
assert_serialization(UserRatingType::Negative, "negative");
|
||||
}
|
||||
|
||||
mod impl_methods {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(UserRatingType::Neutral.to_str(), "neutral");
|
||||
assert_eq!(UserRatingType::Positive.to_str(), "positive");
|
||||
assert_eq!(UserRatingType::Negative.to_str(), "negative");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(UserRatingType::from_str("neutral").unwrap(), UserRatingType::Neutral);
|
||||
assert_eq!(UserRatingType::from_str("positive").unwrap(), UserRatingType::Positive);
|
||||
assert_eq!(UserRatingType::from_str("negative").unwrap(), UserRatingType::Negative);
|
||||
assert!(UserRatingType::from_str("foo").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod traits {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
assert_eq!(UserRatingType::default(), UserRatingType::Neutral);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let visibility = UserRatingType::Positive;
|
||||
assert_eq!(format!("{}", visibility), "positive".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug() {
|
||||
let visibility = UserRatingType::Negative;
|
||||
assert_eq!(format!("{:?}", visibility), "negative".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct CompositeType {
|
||||
visibility: UserRatingType,
|
||||
string: String,
|
||||
}
|
||||
|
||||
mod serde_serialization {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let expected = "\"positive\"".to_string(); // NB: Quoted
|
||||
|
||||
assert_eq!(expected, toml::to_string(&UserRatingType::Positive).unwrap());
|
||||
|
||||
// Just to show this serializes the same as a string
|
||||
assert_eq!(expected, toml::to_string("positive").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_serialize() {
|
||||
let value = CompositeType { visibility: UserRatingType::Negative, string: "bar".to_string() };
|
||||
let expected = r#"{"visibility":"negative","string":"bar"}"#.to_string();
|
||||
assert_eq!(expected, serde_json::to_string(&value).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
mod serde_deserialization {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
let payload = "\"positive\""; // NB: Quoted
|
||||
let value: UserRatingType = serde_json::from_str(payload).unwrap();
|
||||
assert_eq!(value, UserRatingType::Positive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_deserialize() {
|
||||
let payload = r#"{"visibility":"neutral","string":"bar"}"#.to_string();
|
||||
let expected = CompositeType {
|
||||
visibility: UserRatingType::Neutral,
|
||||
string: "bar".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, serde_json::from_str::<CompositeType>(&payload).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@
|
||||
//! This should also work for CHAR/VARCHAR fields that work similarly to enums (typically
|
||||
//! as part of a composite key)
|
||||
//!
|
||||
//! These types should also be friendly for API usage in JSON payloads.
|
||||
//!
|
||||
//! In the future this should be *CODEGEN DRIVEN* and should get checked into source control.
|
||||
//!
|
||||
|
||||
// Never allow these
|
||||
#![forbid(private_in_public)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod user;
|
||||
pub mod user_badges;
|
||||
pub mod user_profiles;
|
||||
pub mod user_ratings;
|
||||
pub mod user_roles;
|
||||
pub mod user_sessions;
|
||||
pub mod user_subscriptions;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod upsert_user_rating;
|
||||
@@ -0,0 +1,58 @@
|
||||
use container_common::anyhow_result::AnyhowResult;
|
||||
use enums::by_table::user_ratings::entity_type::UserRatingEntityType;
|
||||
use sqlx::MySql;
|
||||
use sqlx::pool::PoolConnection;
|
||||
use enums::by_table::user_ratings::rating_type::UserRatingType;
|
||||
use tokens::tokens::tts_models::TtsModelToken;
|
||||
use tokens::tokens::w2l_templates::W2lTemplateToken;
|
||||
use tokens::users::user::UserToken;
|
||||
|
||||
pub enum UserRatingEntityToken<'a> {
|
||||
TtsModel(&'a TtsModelToken),
|
||||
W2lTemplate(&'a W2lTemplateToken),
|
||||
}
|
||||
|
||||
pub struct Args<'a> {
|
||||
pub user_token: &'a UserToken,
|
||||
pub entity_token: UserRatingEntityToken<'a>,
|
||||
pub rating_type: UserRatingType,
|
||||
pub ip_address: &'a str,
|
||||
pub mysql_connection: &'a mut PoolConnection<MySql>,
|
||||
}
|
||||
|
||||
pub async fn upsert_user_rating(args: Args<'_>) -> AnyhowResult<()> {
|
||||
|
||||
let (entity_type, entity_token) = match args.entity_token {
|
||||
EntityToken::TtsModel(token) => (UserRatingEntityType::TtsModel, token.as_str()),
|
||||
EntityToken::W2lTemplate(token) => (UserRatingEntityType::W2lTemplate, token.as_str()),
|
||||
};
|
||||
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO user_ratings
|
||||
SET
|
||||
user_token = ?,
|
||||
entity_type = ?,
|
||||
entity_token = ?,
|
||||
rating_type = ?,
|
||||
vote_ip_address = ?,
|
||||
version = 1
|
||||
|
||||
ON DUPLICATE KEY UPDATE
|
||||
rating_type = ?,
|
||||
vote_ip_address = ?,
|
||||
version = version + 1
|
||||
"#,
|
||||
// Insert
|
||||
args.user_token.as_str(),
|
||||
entity_type,
|
||||
entity_token,
|
||||
args.rating_type.to_str(),
|
||||
args.ip_address,
|
||||
args.rating_type.to_str(),
|
||||
args.ip_address
|
||||
);
|
||||
|
||||
let _r = query.execute(args.mysql_connection).await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user