You've already forked AstralRinth
forked from didirus/AstralRinth
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:
@@ -320,6 +320,23 @@ impl LoaderField {
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<Vec<LoaderField>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let found_loader_fields = Self::get_fields_per_loader(loader_ids, exec, redis).await?;
|
||||
let result = found_loader_fields
|
||||
.into_values()
|
||||
.flatten()
|
||||
.unique_by(|x| x.id)
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_fields_per_loader<'a, E>(
|
||||
loader_ids: &[LoaderId],
|
||||
exec: E,
|
||||
redis: &RedisPool,
|
||||
) -> Result<HashMap<LoaderId, Vec<LoaderField>>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
@@ -336,11 +353,11 @@ impl LoaderField {
|
||||
.filter_map(|x: String| serde_json::from_str::<RedisLoaderFieldTuple>(&x).ok())
|
||||
.collect();
|
||||
|
||||
let mut found_loader_fields = vec![];
|
||||
let mut found_loader_fields = HashMap::new();
|
||||
if !cached_fields.is_empty() {
|
||||
for (loader_id, fields) in cached_fields {
|
||||
if loader_ids.contains(&loader_id) {
|
||||
found_loader_fields.extend(fields);
|
||||
found_loader_fields.insert(loader_id, fields);
|
||||
loader_ids.retain(|x| x != &loader_id);
|
||||
}
|
||||
}
|
||||
@@ -388,14 +405,10 @@ impl LoaderField {
|
||||
redis
|
||||
.set_serialized_to_json(LOADER_FIELDS_NAMESPACE, k.0, (k, &v), None)
|
||||
.await?;
|
||||
found_loader_fields.extend(v);
|
||||
found_loader_fields.insert(k, v);
|
||||
}
|
||||
}
|
||||
let result = found_loader_fields
|
||||
.into_iter()
|
||||
.unique_by(|x| x.id)
|
||||
.collect();
|
||||
Ok(result)
|
||||
Ok(found_loader_fields)
|
||||
}
|
||||
|
||||
// Gets all fields for a given loader(s)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::super::ids::OrganizationId;
|
||||
use super::super::teams::TeamId;
|
||||
use super::super::users::UserId;
|
||||
@@ -10,6 +12,7 @@ use crate::models::projects::{
|
||||
Project, ProjectStatus, Version, VersionFile, VersionStatus, VersionType,
|
||||
};
|
||||
use crate::models::threads::ThreadId;
|
||||
use crate::routes::v2_reroute;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -85,26 +88,6 @@ impl LegacyProject {
|
||||
let mut loaders = data.loaders;
|
||||
|
||||
if let Some(versions_item) = versions_item {
|
||||
client_side = versions_item
|
||||
.version_fields
|
||||
.iter()
|
||||
.find(|f| f.field_name == "client_side")
|
||||
.and_then(|f| {
|
||||
Some(LegacySideType::from_string(
|
||||
f.value.serialize_internal().as_str()?,
|
||||
))
|
||||
})
|
||||
.unwrap_or(LegacySideType::Unknown);
|
||||
server_side = versions_item
|
||||
.version_fields
|
||||
.iter()
|
||||
.find(|f| f.field_name == "server_side")
|
||||
.and_then(|f| {
|
||||
Some(LegacySideType::from_string(
|
||||
f.value.serialize_internal().as_str()?,
|
||||
))
|
||||
})
|
||||
.unwrap_or(LegacySideType::Unknown);
|
||||
game_versions = versions_item
|
||||
.version_fields
|
||||
.iter()
|
||||
@@ -113,6 +96,14 @@ impl LegacyProject {
|
||||
.map(|v| v.into_iter().map(|v| v.version).collect())
|
||||
.unwrap_or(Vec::new());
|
||||
|
||||
// Extract side types from remaining fields (singleplayer, client_only, etc)
|
||||
let fields = versions_item
|
||||
.version_fields
|
||||
.iter()
|
||||
.map(|f| (f.field_name.clone(), f.value.clone().serialize_internal()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
(client_side, server_side) = v2_reroute::convert_side_types_v2(&fields);
|
||||
|
||||
// - if loader is mrpack, this is a modpack
|
||||
// the loaders are whatever the corresponding loader fields are
|
||||
if versions_item.loaders == vec!["mrpack".to_string()] {
|
||||
@@ -194,7 +185,7 @@ impl LegacyProject {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Copy)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LegacySideType {
|
||||
Required,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{models::projects::SideType, util::env::parse_strings_from_var};
|
||||
use crate::{models::v2::projects::LegacySideType, util::env::parse_strings_from_var};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
@@ -23,7 +23,7 @@ pub struct PackFormat {
|
||||
pub struct PackFile {
|
||||
pub path: String,
|
||||
pub hashes: std::collections::HashMap<PackFileHash, String>,
|
||||
pub env: Option<std::collections::HashMap<EnvType, SideType>>,
|
||||
pub env: Option<std::collections::HashMap<EnvType, LegacySideType>>, // TODO: Should this use LegacySideType? Will probably require a overhaul of mrpack format to change this
|
||||
#[validate(custom(function = "validate_download_url"))]
|
||||
pub downloads: Vec<String>,
|
||||
pub file_size: u32,
|
||||
|
||||
@@ -216,42 +216,6 @@ pub struct ModeratorMessage {
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SideType {
|
||||
Required,
|
||||
Optional,
|
||||
Unsupported,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SideType {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl SideType {
|
||||
// These are constant, so this can remove unneccessary allocations (`to_string`)
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SideType::Required => "required",
|
||||
SideType::Optional => "optional",
|
||||
SideType::Unsupported => "unsupported",
|
||||
SideType::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> SideType {
|
||||
match string {
|
||||
"required" => SideType::Required,
|
||||
"optional" => SideType::Optional,
|
||||
"unsupported" => SideType::Unsupported,
|
||||
_ => SideType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const DEFAULT_LICENSE_ID: &str = "LicenseRef-All-Rights-Reserved";
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::v3::project_creation::CreateError;
|
||||
use crate::models::v2::projects::LegacySideType;
|
||||
use crate::util::actix::{generate_multipart, MultipartSegment, MultipartSegmentData};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::http::header::{HeaderMap, TryIntoHeaderPair};
|
||||
use actix_web::HttpResponse;
|
||||
use futures::{stream, StreamExt};
|
||||
use serde_json::json;
|
||||
use futures::{stream, Future, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub async fn extract_ok_json<T>(response: HttpResponse) -> Result<T, HttpResponse>
|
||||
where
|
||||
@@ -29,14 +33,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn alter_actix_multipart<T, U>(
|
||||
pub async fn alter_actix_multipart<T, U, Fut>(
|
||||
mut multipart: Multipart,
|
||||
mut headers: HeaderMap,
|
||||
mut closure: impl FnMut(T) -> Result<U, CreateError>,
|
||||
mut closure: impl FnMut(T) -> Fut,
|
||||
) -> Result<Multipart, CreateError>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
U: serde::Serialize,
|
||||
Fut: Future<Output = Result<U, CreateError>>,
|
||||
{
|
||||
let mut segments: Vec<MultipartSegment> = Vec::new();
|
||||
|
||||
@@ -56,7 +61,7 @@ where
|
||||
|
||||
{
|
||||
let json_value: T = serde_json::from_slice(&buffer)?;
|
||||
let json_value: U = closure(json_value)?;
|
||||
let json_value: U = closure(json_value).await?;
|
||||
buffer = serde_json::to_vec(&json_value)?;
|
||||
}
|
||||
|
||||
@@ -110,3 +115,353 @@ where
|
||||
|
||||
Ok(new_multipart)
|
||||
}
|
||||
|
||||
// Converts a "client_side" and "server_side" pair into the new v3 corresponding fields
|
||||
pub fn convert_side_types_v3(
|
||||
client_side: LegacySideType,
|
||||
server_side: LegacySideType,
|
||||
) -> HashMap<String, Value> {
|
||||
use LegacySideType::{Optional, Required};
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
// Convert search facets from V2 to V3
|
||||
// Less trivial as we need to handle the case where one side is set and the other is not, which does not convert cleanly
|
||||
pub fn convert_side_type_facets_v3(facets: Vec<Vec<Vec<String>>>) -> Vec<Vec<Vec<String>>> {
|
||||
use LegacySideType::{Optional, Required, Unsupported};
|
||||
let possible_side_types = [Required, Optional, Unsupported]; // Should not include Unknown
|
||||
|
||||
let mut v3_facets = vec![];
|
||||
|
||||
// Outer facets are joined by AND
|
||||
for inner_facets in facets {
|
||||
// Inner facets are joined by OR
|
||||
// These may change as the inner facets are converted
|
||||
// ie:
|
||||
// for A v B v C, if A is converted to X^Y v Y^Z, then the new facets are X^Y v Y^Z v B v C
|
||||
let mut new_inner_facets = vec![];
|
||||
|
||||
for inner_inner_facets in inner_facets {
|
||||
// Inner inner facets are joined by AND
|
||||
let mut client_side = None;
|
||||
let mut server_side = None;
|
||||
|
||||
// Extract client_side and server_side facets, and remove them from the list
|
||||
let inner_inner_facets = inner_inner_facets
|
||||
.into_iter()
|
||||
.filter_map(|facet| {
|
||||
let val = match facet.split(':').nth(1) {
|
||||
Some(val) => val,
|
||||
None => return Some(facet.to_string()),
|
||||
};
|
||||
|
||||
if facet.starts_with("client_side:") {
|
||||
client_side = Some(LegacySideType::from_string(val));
|
||||
None
|
||||
} else if facet.starts_with("server_side:") {
|
||||
server_side = Some(LegacySideType::from_string(val));
|
||||
None
|
||||
} else {
|
||||
Some(facet.to_string())
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
// Depending on whether client_side and server_side are set, we can convert the facets to the new loader fields differently
|
||||
let mut new_possibilities = match (client_side, server_side) {
|
||||
// Both set or unset is a trivial case
|
||||
(Some(client_side), Some(server_side)) => {
|
||||
vec![convert_side_types_v3(client_side, server_side)
|
||||
.into_iter()
|
||||
.map(|(k, v)| format!("{}:{}", k, v))
|
||||
.collect()]
|
||||
}
|
||||
(None, None) => vec![vec![]],
|
||||
|
||||
(Some(client_side), None) => possible_side_types
|
||||
.iter()
|
||||
.map(|server_side| {
|
||||
convert_side_types_v3(client_side, *server_side)
|
||||
.into_iter()
|
||||
.map(|(k, v)| format!("{}:{}", k, v))
|
||||
.unique()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
(None, Some(server_side)) => possible_side_types
|
||||
.iter()
|
||||
.map(|client_side| {
|
||||
convert_side_types_v3(*client_side, server_side)
|
||||
.into_iter()
|
||||
.map(|(k, v)| format!("{}:{}", k, v))
|
||||
.unique()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
|
||||
// Add the new possibilities to the list
|
||||
for new_possibility in &mut new_possibilities {
|
||||
new_possibility.extend(inner_inner_facets.clone());
|
||||
}
|
||||
new_inner_facets.extend(new_possibilities);
|
||||
}
|
||||
v3_facets.push(new_inner_facets);
|
||||
}
|
||||
v3_facets
|
||||
}
|
||||
|
||||
// Convert search facets from V3 back to v2
|
||||
// this is not lossless. (See tests)
|
||||
pub fn convert_side_types_v2(
|
||||
side_types: &HashMap<String, Value>,
|
||||
) -> (LegacySideType, LegacySideType) {
|
||||
use LegacySideType::{Optional, Required, Unsupported};
|
||||
|
||||
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);
|
||||
|
||||
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) => (Unsupported, Unsupported),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::v2::projects::LegacySideType::{Optional, Required, Unsupported};
|
||||
|
||||
#[test]
|
||||
fn convert_types() {
|
||||
// Converting types from V2 to V3 and back should be idempotent- for certain pairs
|
||||
let lossy_pairs = [
|
||||
(Optional, Unsupported),
|
||||
(Unsupported, Optional),
|
||||
(Required, Optional),
|
||||
(Optional, Required),
|
||||
];
|
||||
|
||||
for client_side in [Required, Optional, Unsupported] {
|
||||
for server_side in [Required, Optional, Unsupported] {
|
||||
if lossy_pairs.contains(&(client_side, server_side)) {
|
||||
continue;
|
||||
}
|
||||
let side_types = convert_side_types_v3(client_side, server_side);
|
||||
let (client_side2, server_side2) = convert_side_types_v2(&side_types);
|
||||
assert_eq!(client_side, client_side2);
|
||||
assert_eq!(server_side, server_side2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_facets() {
|
||||
let pre_facets = vec![
|
||||
// Test combinations of both sides being set
|
||||
vec![vec![
|
||||
"client_side:required".to_string(),
|
||||
"server_side:required".to_string(),
|
||||
]],
|
||||
vec![vec![
|
||||
"client_side:required".to_string(),
|
||||
"server_side:optional".to_string(),
|
||||
]],
|
||||
vec![vec![
|
||||
"client_side:required".to_string(),
|
||||
"server_side:unsupported".to_string(),
|
||||
]],
|
||||
vec![vec![
|
||||
"client_side:optional".to_string(),
|
||||
"server_side:required".to_string(),
|
||||
]],
|
||||
vec![vec![
|
||||
"client_side:optional".to_string(),
|
||||
"server_side:optional".to_string(),
|
||||
]],
|
||||
// Test multiple inner facets
|
||||
vec![
|
||||
vec![
|
||||
"client_side:required".to_string(),
|
||||
"server_side:required".to_string(),
|
||||
],
|
||||
vec![
|
||||
"client_side:required".to_string(),
|
||||
"server_side:optional".to_string(),
|
||||
],
|
||||
],
|
||||
// Test additional fields
|
||||
vec![
|
||||
vec![
|
||||
"random_field_test_1".to_string(),
|
||||
"client_side:required".to_string(),
|
||||
"server_side:required".to_string(),
|
||||
],
|
||||
vec![
|
||||
"random_field_test_2".to_string(),
|
||||
"client_side:required".to_string(),
|
||||
"server_side:optional".to_string(),
|
||||
],
|
||||
],
|
||||
// Test only one facet being set
|
||||
vec![vec!["client_side:required".to_string()]],
|
||||
];
|
||||
|
||||
let converted_facets = convert_side_type_facets_v3(pre_facets)
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
x.into_iter()
|
||||
.map(|mut y| {
|
||||
y.sort();
|
||||
y
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let post_facets = vec![
|
||||
vec![vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:false".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
]],
|
||||
vec![vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:true".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
]],
|
||||
vec![vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:true".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
]],
|
||||
vec![vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:false".to_string(),
|
||||
"server_only:true".to_string(),
|
||||
]],
|
||||
vec![vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:true".to_string(),
|
||||
"server_only:true".to_string(),
|
||||
]],
|
||||
vec![
|
||||
vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:false".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
],
|
||||
vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:true".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
],
|
||||
],
|
||||
vec![
|
||||
vec![
|
||||
"random_field_test_1".to_string(),
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:false".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
],
|
||||
vec![
|
||||
"random_field_test_2".to_string(),
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:true".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
],
|
||||
],
|
||||
// Test only one facet being set
|
||||
// Iterates over all possible side types
|
||||
vec![
|
||||
// C: Required, S: Required
|
||||
vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:false".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
],
|
||||
// C: Required, S: Optional
|
||||
vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:true".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
],
|
||||
// C: Required, S: Unsupported
|
||||
vec![
|
||||
"singleplayer:true".to_string(),
|
||||
"client_and_server:true".to_string(),
|
||||
"client_only:true".to_string(),
|
||||
"server_only:false".to_string(),
|
||||
],
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
x.into_iter()
|
||||
.map(|mut y| {
|
||||
y.sort();
|
||||
y
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(converted_facets, post_facets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +511,6 @@ pub async fn revoke_oauth_authorization(
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
println!("Inside revoke_oauth_authorization");
|
||||
let current_user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::database::models::loader_fields::{
|
||||
use crate::database::redis::RedisPool;
|
||||
use actix_web::{web, HttpResponse};
|
||||
|
||||
use itertools::Itertools;
|
||||
use serde_json::Value;
|
||||
use sqlx::PgPool;
|
||||
|
||||
@@ -84,6 +85,7 @@ pub struct LoaderData {
|
||||
pub name: String,
|
||||
pub supported_project_types: Vec<String>,
|
||||
pub supported_games: Vec<String>,
|
||||
pub supported_fields: Vec<String>, // Available loader fields for this loader
|
||||
pub metadata: Value,
|
||||
}
|
||||
|
||||
@@ -91,14 +93,26 @@ pub async fn loader_list(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let mut results = Loader::list(&**pool, &redis)
|
||||
.await?
|
||||
let loaders = Loader::list(&**pool, &redis).await?;
|
||||
|
||||
let loader_fields = LoaderField::get_fields_per_loader(
|
||||
&loaders.iter().map(|x| x.id).collect_vec(),
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut results = loaders
|
||||
.into_iter()
|
||||
.map(|x| LoaderData {
|
||||
icon: x.icon,
|
||||
name: x.loader,
|
||||
supported_project_types: x.supported_project_types,
|
||||
supported_games: x.supported_games,
|
||||
supported_fields: loader_fields
|
||||
.get(&x.id)
|
||||
.map(|x| x.iter().map(|x| x.field.clone()).collect_vec())
|
||||
.unwrap_or_default(),
|
||||
metadata: x.metadata,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -3,8 +3,10 @@ use crate::models::projects::SearchRequest;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::HttpResponse;
|
||||
use chrono::{DateTime, Utc};
|
||||
use itertools::Itertools;
|
||||
use meilisearch_sdk::client::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::borrow::Cow;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
@@ -177,7 +179,7 @@ pub async fn search_for_project(
|
||||
query.with_filter(new_filters);
|
||||
} else {
|
||||
let facets = if let Some(facets) = &info.facets {
|
||||
Some(serde_json::from_str::<Vec<Vec<&str>>>(facets)?)
|
||||
Some(serde_json::from_str::<Vec<Vec<Value>>>(facets)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -190,14 +192,42 @@ pub async fn search_for_project(
|
||||
};
|
||||
|
||||
if let Some(facets) = 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();
|
||||
|
||||
filter_string.push('(');
|
||||
for (index, facet_list) in facets.iter().enumerate() {
|
||||
for (index, facet_outer_list) in facets.iter().enumerate() {
|
||||
filter_string.push('(');
|
||||
|
||||
for (facet_index, facet) in facet_list.iter().enumerate() {
|
||||
filter_string.push_str(&facet.replace(':', " = "));
|
||||
for (facet_outer_index, facet_inner_list) in facet_outer_list.iter().enumerate()
|
||||
{
|
||||
filter_string.push('(');
|
||||
for (facet_inner_index, facet) in facet_inner_list.iter().enumerate() {
|
||||
filter_string.push_str(&facet.replace(':', " = "));
|
||||
if facet_inner_index != (facet_inner_list.len() - 1) {
|
||||
filter_string.push_str(" AND ")
|
||||
}
|
||||
}
|
||||
filter_string.push(')');
|
||||
|
||||
if facet_index != (facet_list.len() - 1) {
|
||||
if facet_outer_index != (facet_outer_list.len() - 1) {
|
||||
filter_string.push_str(" OR ")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user