You've already forked AstralRinth
forked from didirus/AstralRinth
feat(labrinth): rework v3 side types to a single environment field (#3701)
* feat(labrinth): rework v3 side types to a single `environment` field This field is meant to be able to represent the existing v2 side type information and beyond, in a way that may also be slightly easier to comprehend. * chore(labrinth/migrations): use proper val for `HAVING` clause * feat(labrinth): add `side_types_migration_review_status` field to projects
This commit is contained in:
committed by
GitHub
parent
65126b3a23
commit
ef04dcc37b
@@ -158,10 +158,12 @@ pub async fn project_create(
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
let mut fields = HashMap::new();
|
||||
fields.extend(v2_reroute::convert_side_types_v3(
|
||||
client_side,
|
||||
server_side,
|
||||
));
|
||||
fields.extend(
|
||||
v2_reroute::convert_v2_side_types_to_v3_side_types(
|
||||
client_side,
|
||||
server_side,
|
||||
),
|
||||
);
|
||||
fields.insert(
|
||||
"game_versions".to_string(),
|
||||
json!(v.game_versions),
|
||||
|
||||
@@ -511,6 +511,7 @@ pub async fn project_edit(
|
||||
moderation_message: v2_new_project.moderation_message,
|
||||
moderation_message_body: v2_new_project.moderation_message_body,
|
||||
monetization_status: v2_new_project.monetization_status,
|
||||
side_types_migration_review_status: None, // Not to be exposed in v2
|
||||
};
|
||||
|
||||
// This returns 204 or failure so we don't need to do anything with it
|
||||
@@ -547,10 +548,12 @@ pub async fn project_edit(
|
||||
let version = Version::from(version);
|
||||
let mut fields = version.fields;
|
||||
let (current_client_side, current_server_side) =
|
||||
v2_reroute::convert_side_types_v2(&fields, None);
|
||||
v2_reroute::convert_v3_side_types_to_v2_side_types(
|
||||
&fields, None,
|
||||
);
|
||||
let client_side = client_side.unwrap_or(current_client_side);
|
||||
let server_side = server_side.unwrap_or(current_server_side);
|
||||
fields.extend(v2_reroute::convert_side_types_v3(
|
||||
fields.extend(v2_reroute::convert_v2_side_types_to_v3_side_types(
|
||||
client_side,
|
||||
server_side,
|
||||
));
|
||||
|
||||
@@ -105,7 +105,7 @@ pub async fn version_create(
|
||||
json!(legacy_create.game_versions),
|
||||
);
|
||||
|
||||
// Get all possible side-types for loaders given- we will use these to check if we need to convert/apply singleplayer, etc.
|
||||
// Get all possible side-types for loaders given- we will use these to check if we need to convert/apply side types
|
||||
let loaders =
|
||||
match v3::tags::loader_list(client.clone(), redis.clone())
|
||||
.await
|
||||
@@ -136,53 +136,32 @@ pub async fn version_create(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Copies side types of another version of the project.
|
||||
// If no version exists, defaults to all false.
|
||||
// If no version exists, defaults to an unknown side type.
|
||||
// This is inherently lossy, but not much can be done about it, as side types are no longer associated with projects,
|
||||
// so the 'missing' ones can't be easily accessed, and versions do need to have these fields explicitly set.
|
||||
let side_type_loader_field_names = [
|
||||
"singleplayer",
|
||||
"client_and_server",
|
||||
"client_only",
|
||||
"server_only",
|
||||
];
|
||||
// so the 'missing' ones can't be easily accessed, and versions do need to have that field explicitly set.
|
||||
|
||||
// Check if loader_fields_aggregate contains any of these side types
|
||||
// Check if loader_fields_aggregate contains the side types
|
||||
// We assume these four fields are linked together.
|
||||
if loader_fields_aggregate
|
||||
.iter()
|
||||
.any(|f| side_type_loader_field_names.contains(&f.as_str()))
|
||||
.any(|field| field == "environment")
|
||||
{
|
||||
// If so, we get the fields of the example version of the project, and set the side types to match.
|
||||
fields.extend(
|
||||
side_type_loader_field_names
|
||||
.iter()
|
||||
.map(|f| (f.to_string(), json!(false))),
|
||||
);
|
||||
if let Some(example_version_fields) =
|
||||
// If so, we get the field of an example version of the project, and set the side types to match.
|
||||
fields.insert(
|
||||
"environment".into(),
|
||||
get_example_version_fields(
|
||||
legacy_create.project_id,
|
||||
client,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
fields.extend(
|
||||
example_version_fields.into_iter().filter_map(
|
||||
|f| {
|
||||
if side_type_loader_field_names
|
||||
.contains(&f.field_name.as_str())
|
||||
{
|
||||
Some((
|
||||
f.field_name,
|
||||
f.value.serialize_internal(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|f| f.field_name == "environment")
|
||||
.map_or(json!("unknown"), |f| {
|
||||
f.value.serialize_internal()
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Handle project type via file extension prediction
|
||||
let mut project_type = None;
|
||||
|
||||
@@ -164,69 +164,46 @@ where
|
||||
Ok(new_multipart)
|
||||
}
|
||||
|
||||
// Converts a "client_side" and "server_side" pair into the new v3 corresponding fields
|
||||
pub fn convert_side_types_v3(
|
||||
/// Converts V2 side types to V3 side types.
|
||||
pub fn convert_v2_side_types_to_v3_side_types(
|
||||
client_side: LegacySideType,
|
||||
server_side: LegacySideType,
|
||||
) -> HashMap<String, Value> {
|
||||
use LegacySideType::{Optional, Required};
|
||||
use LegacySideType::{Optional, Required, Unsupported};
|
||||
|
||||
let singleplayer = client_side == Required
|
||||
|| client_side == Optional
|
||||
|| server_side == Required
|
||||
|| server_side == Optional;
|
||||
let client_and_server = singleplayer;
|
||||
let client_only = (client_side == Required || client_side == Optional)
|
||||
&& server_side != Required;
|
||||
let server_only = (server_side == Required || server_side == Optional)
|
||||
&& client_side != Required;
|
||||
let environment = match (client_side, server_side) {
|
||||
(Required, Required) => "client_and_server", // Or "singleplayer_only"
|
||||
(Required, Unsupported) => "client_only",
|
||||
(Required, Optional) => "client_only_server_optional",
|
||||
(Unsupported, Required) => "server_only", // Or "dedicated_server_only"
|
||||
(Optional, Required) => "server_only_client_optional",
|
||||
(Optional, Optional) => "client_or_server", // Or "client_or_server_prefers_both"
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
let mut fields = HashMap::new();
|
||||
fields.insert("singleplayer".to_string(), json!(singleplayer));
|
||||
fields.insert("client_and_server".to_string(), json!(client_and_server));
|
||||
fields.insert("client_only".to_string(), json!(client_only));
|
||||
fields.insert("server_only".to_string(), json!(server_only));
|
||||
fields
|
||||
[("environment".to_string(), json!(environment))]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Convert search facets from V3 back to v2
|
||||
// this is not lossless. (See tests)
|
||||
pub fn convert_side_types_v2(
|
||||
/// Converts a V3 side types map into the corresponding V2 side types.
|
||||
pub fn convert_v3_side_types_to_v2_side_types(
|
||||
side_types: &HashMap<String, Value>,
|
||||
project_type: Option<&str>,
|
||||
) -> (LegacySideType, LegacySideType) {
|
||||
let client_and_server = side_types
|
||||
.get("client_and_server")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
let singleplayer = side_types
|
||||
.get("singleplayer")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(client_and_server);
|
||||
let client_only = side_types
|
||||
.get("client_only")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
let server_only = side_types
|
||||
.get("server_only")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
convert_side_types_v2_bools(
|
||||
Some(singleplayer),
|
||||
client_only,
|
||||
server_only,
|
||||
Some(client_and_server),
|
||||
convert_v3_environment_to_v2_side_types(
|
||||
side_types
|
||||
.get("environment")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("unknown"),
|
||||
project_type,
|
||||
)
|
||||
}
|
||||
|
||||
// Client side, server side
|
||||
pub fn convert_side_types_v2_bools(
|
||||
singleplayer: Option<bool>,
|
||||
client_only: bool,
|
||||
server_only: bool,
|
||||
client_and_server: Option<bool>,
|
||||
/// Converts a V3 environment and project type into the corresponding V2 side types.
|
||||
/// The first side type is for the client, the second is for the server.
|
||||
pub fn convert_v3_environment_to_v2_side_types(
|
||||
environment: &str,
|
||||
project_type: Option<&str>,
|
||||
) -> (LegacySideType, LegacySideType) {
|
||||
use LegacySideType::{Optional, Required, Unknown, Unsupported};
|
||||
@@ -236,30 +213,18 @@ pub fn convert_side_types_v2_bools(
|
||||
Some("datapack") => (Optional, Required),
|
||||
Some("shader") => (Required, Unsupported),
|
||||
Some("resourcepack") => (Required, Unsupported),
|
||||
_ => {
|
||||
let singleplayer =
|
||||
singleplayer.or(client_and_server).unwrap_or(false);
|
||||
|
||||
match (singleplayer, client_only, server_only) {
|
||||
// Only singleplayer
|
||||
(true, false, false) => (Required, Required),
|
||||
|
||||
// Client only and not server only
|
||||
(false, true, false) => (Required, Unsupported),
|
||||
(true, true, false) => (Required, Unsupported),
|
||||
|
||||
// Server only and not client only
|
||||
(false, false, true) => (Unsupported, Required),
|
||||
(true, false, true) => (Unsupported, Required),
|
||||
|
||||
// Both server only and client only
|
||||
(true, true, true) => (Optional, Optional),
|
||||
(false, true, true) => (Optional, Optional),
|
||||
|
||||
// Bad type
|
||||
(false, false, false) => (Unknown, Unknown),
|
||||
}
|
||||
}
|
||||
_ => match environment {
|
||||
"client_and_server" => (Required, Required),
|
||||
"client_only" => (Required, Unsupported),
|
||||
"client_only_server_optional" => (Required, Optional),
|
||||
"singleplayer_only" => (Required, Required),
|
||||
"server_only" => (Unsupported, Required),
|
||||
"server_only_client_optional" => (Optional, Required),
|
||||
"dedicated_server_only" => (Unsupported, Required),
|
||||
"client_or_server" => (Optional, Optional),
|
||||
"client_or_server_prefers_both" => (Optional, Optional),
|
||||
_ => (Unknown, Unknown), // "unknown"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,13 +244,14 @@ mod tests {
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn convert_types() {
|
||||
// Converting types from V2 to V3 and back should be idempotent- for certain pairs
|
||||
fn v2_v3_side_type_conversion() {
|
||||
// Only nonsensical V2 side types cannot be round-tripped from V2 to V3 and back.
|
||||
// When converting from V3 to V2, only additional information about the
|
||||
// singleplayer-only, multiplayer-only, or install on both sides nature of the
|
||||
// project is lost.
|
||||
let lossy_pairs = [
|
||||
(Optional, Unsupported),
|
||||
(Unsupported, Optional),
|
||||
(Required, Optional),
|
||||
(Optional, Required),
|
||||
(Unsupported, Unsupported),
|
||||
];
|
||||
|
||||
@@ -294,10 +260,13 @@ mod tests {
|
||||
if lossy_pairs.contains(&(client_side, server_side)) {
|
||||
continue;
|
||||
}
|
||||
let side_types =
|
||||
convert_side_types_v3(client_side, server_side);
|
||||
let side_types = convert_v2_side_types_to_v3_side_types(
|
||||
client_side,
|
||||
server_side,
|
||||
);
|
||||
let (client_side2, server_side2) =
|
||||
convert_side_types_v2(&side_types, None);
|
||||
convert_v3_side_types_to_v2_side_types(&side_types, None);
|
||||
|
||||
assert_eq!(client_side, client_side2);
|
||||
assert_eq!(server_side, server_side2);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ use crate::models::ids::{ImageId, OrganizationId, ProjectId, VersionId};
|
||||
use crate::models::images::{Image, ImageContext};
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::projects::{
|
||||
License, Link, MonetizationStatus, ProjectStatus, VersionStatus,
|
||||
License, Link, MonetizationStatus, ProjectStatus,
|
||||
SideTypesMigrationReviewStatus, VersionStatus,
|
||||
};
|
||||
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
|
||||
use crate::models::threads::ThreadType;
|
||||
@@ -901,6 +902,9 @@ async fn project_create_inner(
|
||||
color: project_builder.color,
|
||||
thread_id: thread_id.into(),
|
||||
monetization_status: MonetizationStatus::Monetized,
|
||||
// New projects are considered reviewed with respect to side types migrations
|
||||
side_types_migration_review_status:
|
||||
SideTypesMigrationReviewStatus::Reviewed,
|
||||
fields: HashMap::new(), // Fields instantiate to empty
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::models::notifications::NotificationBody;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::projects::{
|
||||
MonetizationStatus, Project, ProjectStatus, SearchRequest,
|
||||
SideTypesMigrationReviewStatus,
|
||||
};
|
||||
use crate::models::teams::ProjectPermissions;
|
||||
use crate::models::threads::MessageBody;
|
||||
@@ -247,6 +248,8 @@ pub struct EditProject {
|
||||
#[validate(length(max = 65536))]
|
||||
pub moderation_message_body: Option<Option<String>>,
|
||||
pub monetization_status: Option<MonetizationStatus>,
|
||||
pub side_types_migration_review_status:
|
||||
Option<SideTypesMigrationReviewStatus>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -844,6 +847,29 @@ pub async fn project_edit(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(side_types_migration_review_status) =
|
||||
&new_project.side_types_migration_review_status
|
||||
{
|
||||
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You do not have the permissions to edit the side types migration review status of this project!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET side_types_migration_review_status = $1
|
||||
WHERE id = $2
|
||||
",
|
||||
side_types_migration_review_status.as_str(),
|
||||
id as db_ids::DBProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// check new description and body for links to associated images
|
||||
// if they no longer exist in the description or body, delete them
|
||||
let checkable_strings: Vec<&str> =
|
||||
|
||||
Reference in New Issue
Block a user