Side types overhaul (#762)

* side types overhaul

* fixes, fmt clippy

* migration fix for v3 bug

* fixed migration issues

* more tested migration changes

* fmt, clippy

* bump cicd

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Wyatt Verchere
2023-11-28 10:36:59 -08:00
committed by GitHub
parent fd18185ef0
commit f731c1080d
28 changed files with 957 additions and 555 deletions

View File

@@ -3,8 +3,8 @@ use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::ids::ImageId;
use crate::models::projects::{DonationLink, Loader, Project, ProjectStatus, SideType};
use crate::models::v2::projects::LegacyProject;
use crate::models::projects::{DonationLink, Loader, Project, ProjectStatus};
use crate::models::v2::projects::{LegacyProject, LegacySideType};
use crate::queue::session::AuthQueue;
use crate::routes::v3::project_creation::default_project_type;
use crate::routes::v3::project_creation::{CreateError, NewGalleryItem};
@@ -60,9 +60,9 @@ struct ProjectCreateData {
pub body: String,
/// The support range for the client project
pub client_side: SideType,
pub client_side: LegacySideType,
/// The support range for the server project
pub server_side: SideType,
pub server_side: LegacySideType,
#[validate(length(max = 32))]
#[validate]
@@ -146,7 +146,7 @@ pub async fn project_create(
let payload = v2_reroute::alter_actix_multipart(
payload,
req.headers().clone(),
|legacy_create: ProjectCreateData| {
|legacy_create: ProjectCreateData| async move {
// Side types will be applied to each version
let client_side = legacy_create.client_side;
let server_side = legacy_create.server_side;
@@ -158,8 +158,7 @@ pub async fn project_create(
.into_iter()
.map(|v| {
let mut fields = HashMap::new();
fields.insert("client_side".to_string(), json!(client_side));
fields.insert("server_side".to_string(), json!(server_side));
fields.extend(v2_reroute::convert_side_types_v3(client_side, server_side));
fields.insert("game_versions".to_string(), json!(v.game_versions));
// Modpacks now use the "mrpack" loader, and loaders are converted to loader fields.

View File

@@ -3,9 +3,9 @@ use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::projects::{
DonationLink, MonetizationStatus, Project, ProjectStatus, SearchRequest, SideType,
DonationLink, MonetizationStatus, Project, ProjectStatus, SearchRequest, Version,
};
use crate::models::v2::projects::LegacyProject;
use crate::models::v2::projects::{LegacyProject, LegacySideType};
use crate::models::v2::search::LegacySearchResults;
use crate::queue::session::AuthQueue;
use crate::routes::v3::projects::ProjectIds;
@@ -13,10 +13,9 @@ use crate::routes::{v2_reroute, v3, ApiError};
use crate::search::{search_for_project, SearchConfig, SearchError};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use validator::Validate;
@@ -59,27 +58,55 @@ pub async fn project_search(
// Search now uses loader_fields instead of explicit 'client_side' and 'server_side' fields
// While the backend for this has changed, it doesnt affect much
// in the API calls except that 'versions:x' is now 'game_versions:x'
let facets: Option<Vec<Vec<String>>> = if let Some(facets) = info.facets {
let facets = serde_json::from_str::<Vec<Vec<&str>>>(&facets)?;
let facets: Option<Vec<Vec<Vec<String>>>> = if let Some(facets) = info.facets {
let facets = serde_json::from_str::<Vec<Vec<serde_json::Value>>>(&facets)?;
// Search can now *optionally* have a third inner array: So Vec(AND)<Vec(OR)<Vec(AND)< _ >>>
// For every inner facet, we will check if it can be deserialized into a Vec<&str>, and do so.
// If not, we will assume it is a single facet and wrap it in a Vec.
let facets: Vec<Vec<Vec<String>>> = facets
.into_iter()
.map(|facets| {
facets
.into_iter()
.map(|facet| {
if facet.is_array() {
serde_json::from_value::<Vec<String>>(facet).unwrap_or_default()
} else {
vec![serde_json::from_value::<String>(facet.clone())
.unwrap_or_default()]
}
})
.collect_vec()
})
.collect_vec();
// We will now convert side_types to their new boolean format
let facets = v2_reroute::convert_side_type_facets_v3(facets);
Some(
facets
.into_iter()
.map(|facet| {
facet
.into_iter()
.map(|facet| {
let val = match facet.split(':').nth(1) {
Some(val) => val,
None => return facet.to_string(),
};
.map(|facets| {
facets
.into_iter()
.map(|facet| {
let val = match facet.split(':').nth(1) {
Some(val) => val,
None => return facet.to_string(),
};
if facet.starts_with("versions:") {
format!("game_versions:{}", val)
} else if facet.starts_with("project_type:") {
format!("project_types:{}", val)
} else {
facet.to_string()
}
if facet.starts_with("versions:") {
format!("game_versions:{}", val)
} else if facet.starts_with("project_type:") {
format!("project_types:{}", val)
} else {
facet.to_string()
}
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
})
@@ -279,8 +306,8 @@ pub struct EditProject {
#[validate]
pub donation_urls: Option<Vec<DonationLink>>,
pub license_id: Option<String>,
pub client_side: Option<SideType>,
pub server_side: Option<SideType>,
pub client_side: Option<LegacySideType>,
pub server_side: Option<LegacySideType>,
#[validate(
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
@@ -321,8 +348,8 @@ pub async fn project_edit(
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let v2_new_project = new_project.into_inner();
let client_side = v2_new_project.client_side.clone();
let server_side = v2_new_project.server_side.clone();
let client_side = v2_new_project.client_side;
let server_side = v2_new_project.server_side;
let new_slug = v2_new_project.slug.clone();
// TODO: Some kind of handling here to ensure project type is fine.
@@ -376,12 +403,17 @@ pub async fn project_edit(
let version_ids = project_item.map(|x| x.versions).unwrap_or_default();
let versions = version_item::Version::get_many(&version_ids, &**pool, &redis).await?;
for version in versions {
let mut fields = HashMap::new();
fields.insert("client_side".to_string(), json!(client_side));
fields.insert("server_side".to_string(), json!(server_side));
let version = Version::from(version);
let mut fields = version.fields;
let (current_client_side, current_server_side) =
v2_reroute::convert_side_types_v2(&fields);
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(client_side, server_side));
response = v3::versions::version_edit_helper(
req.clone(),
(version.inner.id.into(),),
(version.id,),
pool.clone(),
redis.clone(),
v3::versions::EditVersion {

View File

@@ -3,10 +3,12 @@ use std::collections::HashMap;
use super::ApiError;
use crate::database::models::loader_fields::LoaderFieldEnumValue;
use crate::database::redis::RedisPool;
use crate::models::v2::projects::LegacySideType;
use crate::routes::v3::tags::{LoaderData as LoaderDataV3, LoaderFieldsEnumQuery};
use crate::routes::{v2_reroute, v3};
use actix_web::{get, web, HttpResponse};
use chrono::{DateTime, Utc};
use itertools::Itertools;
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
@@ -191,28 +193,15 @@ pub async fn project_type_list(
}
#[get("side_type")]
pub async fn side_type_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let response = v3::tags::loader_fields_list(
pool,
web::Query(LoaderFieldsEnumQuery {
loader_field: "client_side".to_string(), // same as server_side
filters: None,
}),
redis,
)
.await?;
// Convert to V2 format
Ok(
match v2_reroute::extract_ok_json::<Vec<LoaderFieldEnumValue>>(response).await {
Ok(fields) => {
let fields = fields.into_iter().map(|f| f.value).collect::<Vec<_>>();
HttpResponse::Ok().json(fields)
}
Err(response) => response,
},
)
pub async fn side_type_list() -> Result<HttpResponse, ApiError> {
// Original side types are no longer reflected in the database.
// Therefore, we hardcode and return all the fields that are supported by our v2 conversion logic.
let side_types = [
LegacySideType::Required,
LegacySideType::Optional,
LegacySideType::Unsupported,
LegacySideType::Unknown,
];
let side_types = side_types.iter().map(|s| s.to_string()).collect_vec();
Ok(HttpResponse::Ok().json(side_types))
}

View File

@@ -1,3 +1,5 @@
use crate::database::models::loader_fields::VersionField;
use crate::database::models::{project_item, version_item};
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models::ids::ImageId;
@@ -88,63 +90,90 @@ pub async fn version_create(
payload,
req.headers().clone(),
|legacy_create: InitialVersionData| {
// Convert input data to V3 format
let mut fields = HashMap::new();
fields.insert(
"game_versions".to_string(),
json!(legacy_create.game_versions),
);
let client = client.clone();
let redis = redis.clone();
async move {
// Convert input data to V3 format
let mut fields = HashMap::new();
fields.insert(
"game_versions".to_string(),
json!(legacy_create.game_versions),
);
// TODO: will be overhauled with side-types overhaul
// TODO: if not, should default to previous version
fields.insert("client_side".to_string(), json!("required"));
fields.insert("server_side".to_string(), json!("optional"));
// Handle project type via file extension prediction
let mut project_type = None;
for file_part in &legacy_create.file_parts {
if let Some(ext) = file_part.split('.').last() {
match ext {
"mrpack" | "mrpack-primary" => {
project_type = Some("modpack");
break;
// Copies side types of another version of the project.
// If no version exists, defaults to all false.
// TODO: write test for this to ensure predictible unchanging behaviour
// 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.
let side_type_loader_field_names = [
"singleplayer",
"client_and_server",
"client_only",
"server_only",
];
fields.extend(
side_type_loader_field_names
.iter()
.map(|f| (f.to_string(), json!(false))),
);
if let Some(example_version_fields) =
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
}
// No other type matters
_ => {}
}
break;
}));
}
// Handle project type via file extension prediction
let mut project_type = None;
for file_part in &legacy_create.file_parts {
if let Some(ext) = file_part.split('.').last() {
match ext {
"mrpack" | "mrpack-primary" => {
project_type = Some("modpack");
break;
}
// No other type matters
_ => {}
}
break;
}
}
// Modpacks now use the "mrpack" loader, and loaders are converted to loader fields.
// Setting of 'project_type' directly is removed, it's loader-based now.
if project_type == Some("modpack") {
fields.insert("mrpack_loaders".to_string(), json!(legacy_create.loaders));
}
let loaders = if project_type == Some("modpack") {
vec![Loader("mrpack".to_string())]
} else {
legacy_create.loaders
};
Ok(v3::version_creation::InitialVersionData {
project_id: legacy_create.project_id,
file_parts: legacy_create.file_parts,
version_number: legacy_create.version_number,
version_title: legacy_create.version_title,
version_body: legacy_create.version_body,
dependencies: legacy_create.dependencies,
release_channel: legacy_create.release_channel,
loaders,
featured: legacy_create.featured,
primary_file: legacy_create.primary_file,
status: legacy_create.status,
file_types: legacy_create.file_types,
uploaded_images: legacy_create.uploaded_images,
ordering: legacy_create.ordering,
fields,
})
}
// Modpacks now use the "mrpack" loader, and loaders are converted to loader fields.
// Setting of 'project_type' directly is removed, it's loader-based now.
if project_type == Some("modpack") {
fields.insert("mrpack_loaders".to_string(), json!(legacy_create.loaders));
}
let loaders = if project_type == Some("modpack") {
vec![Loader("mrpack".to_string())]
} else {
legacy_create.loaders
};
Ok(v3::version_creation::InitialVersionData {
project_id: legacy_create.project_id,
file_parts: legacy_create.file_parts,
version_number: legacy_create.version_number,
version_title: legacy_create.version_title,
version_body: legacy_create.version_body,
dependencies: legacy_create.dependencies,
release_channel: legacy_create.release_channel,
loaders,
featured: legacy_create.featured,
primary_file: legacy_create.primary_file,
status: legacy_create.status,
file_types: legacy_create.file_types,
uploaded_images: legacy_create.uploaded_images,
ordering: legacy_create.ordering,
fields,
})
},
)
.await?;
@@ -170,6 +199,32 @@ pub async fn version_create(
}
}
// Gets version fields of an example version of a project, if one exists.
async fn get_example_version_fields(
project_id: Option<ProjectId>,
pool: Data<PgPool>,
redis: &RedisPool,
) -> Result<Option<Vec<VersionField>>, CreateError> {
let project_id = match project_id {
Some(project_id) => project_id,
None => return Ok(None),
};
let vid = match project_item::Project::get_id(project_id.into(), &**pool, redis)
.await?
.and_then(|p| p.versions.first().cloned())
{
Some(vid) => vid,
None => return Ok(None),
};
let example_version = match version_item::Version::get(vid, &**pool, redis).await? {
Some(version) => version,
None => return Ok(None),
};
Ok(Some(example_version.version_fields))
}
// under /api/v1/version/{version_id}
#[post("{version_id}/file")]
pub async fn upload_file_to_version(