diff --git a/migrations/20231005230721_dynamic-fields.sql b/migrations/20231005230721_dynamic-fields.sql index 7bf0052f..99c5daf8 100644 --- a/migrations/20231005230721_dynamic-fields.sql +++ b/migrations/20231005230721_dynamic-fields.sql @@ -72,18 +72,20 @@ INSERT INTO loader_fields_loaders (loader_id, loader_field_id) SELECT l.id, lf.i INSERT INTO loader_fields_loaders (loader_id, loader_field_id) SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'server_side' AND l.loader = ANY( ARRAY['forge', 'fabric', 'quilt', 'modloader','rift','liteloader', 'neoforge']); INSERT INTO version_fields (version_id, field_id, enum_value) -SELECT v.id, 1, m.client_side +SELECT v.id, lf.id, lfev.id -- Note: bug fix/edited 2023-11-27 FROM versions v INNER JOIN mods m ON v.mod_id = m.id INNER JOIN loader_field_enum_values lfev ON m.client_side = lfev.original_id -WHERE client_side IS NOT NULL AND lfev.enum_id = 1; +CROSS JOIN loader_fields lf +WHERE client_side IS NOT NULL AND lfev.enum_id = 1 AND lf.field = 'client_side'; INSERT INTO version_fields (version_id, field_id, enum_value) -SELECT v.id, 1, m.server_side +SELECT v.id, lf.id, lfev.id -- Note: bug fix/edited 2023-11-27 FROM versions v INNER JOIN mods m ON v.mod_id = m.id -INNER JOIN loader_field_enum_values lfev ON m.client_side = lfev.original_id -WHERE server_side IS NOT NULL AND lfev.enum_id = 1; +INNER JOIN loader_field_enum_values lfev ON m.server_side = lfev.original_id +CROSS JOIN loader_fields lf +WHERE server_side IS NOT NULL AND lfev.enum_id = 1 AND lf.field = 'server_side'; ALTER TABLE mods DROP COLUMN client_side; ALTER TABLE mods DROP COLUMN server_side; @@ -95,11 +97,13 @@ INSERT INTO loader_field_enum_values (original_id, enum_id, value, created, meta SELECT id, 2, version, created, json_build_object('type', type, 'major', major) FROM game_versions; INSERT INTO loader_fields (field, field_type, enum_type, optional, min_val) VALUES('game_versions', 'array_enum', 2, false, 0); +INSERT INTO loader_fields_loaders (loader_id, loader_field_id) SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'game_versions' AND l.loader = ANY( ARRAY['forge', 'fabric', 'quilt', 'modloader','rift','liteloader', 'neoforge']); INSERT INTO version_fields(version_id, field_id, enum_value) -SELECT gvv.joining_version_id, 2, lfev.id +SELECT gvv.joining_version_id, lf.id, lfev.id FROM game_versions_versions gvv INNER JOIN loader_field_enum_values lfev ON gvv.game_version_id = lfev.original_id -WHERE lfev.enum_id = 2; +CROSS JOIN loader_fields lf +WHERE lf.field = 'game_versions' AND lfev.enum_id = 2; ALTER TABLE mods DROP COLUMN loaders; ALTER TABLE mods DROP COLUMN game_versions; @@ -108,12 +112,13 @@ DROP TABLE game_versions; -- Convert project types -- we are creating a new loader type- 'mrpack'- for minecraft modpacks +SELECT setval('loaders_id_seq', (SELECT MAX(id) FROM loaders) + 1, false); INSERT INTO loaders (loader) VALUES ('mrpack'); -- For the loader 'mrpack', we create loader fields for every loader -- That way we keep information like "this modpack is a fabric modpack" INSERT INTO loader_field_enums (id, enum_name, hidable) VALUES (3, 'mrpack_loaders', true); -INSERT INTO loader_field_enum_values (original_id, enum_id, value) SELECT id, 2, loader FROM loaders WHERE loader != 'mrpack'; +INSERT INTO loader_field_enum_values (original_id, enum_id, value) SELECT id, 3, loader FROM loaders WHERE loader != 'mrpack'; INSERT INTO loader_fields (field, field_type, enum_type, optional, min_val) VALUES('mrpack_loaders', 'array_enum', 3, false, 0); INSERT INTO loader_fields_loaders (loader_id, loader_field_id) SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'mrpack_loaders' AND l.loader = 'mrpack'; @@ -125,11 +130,31 @@ INNER JOIN mods m ON v.mod_id = m.id INNER JOIN loaders_versions lv ON v.id = lv.version_id INNER JOIN loaders l ON lv.loader_id = l.id CROSS JOIN loader_fields lf -LEFT JOIN loader_field_enum_values lfev ON lf.enum_type = lfev.enum_id +LEFT JOIN loader_field_enum_values lfev ON lf.enum_type = lfev.enum_id AND lfev.original_id = l.id WHERE m.project_type = (SELECT id FROM project_types WHERE name = 'modpack') AND lf.field = 'mrpack_loaders'; INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) SELECT DISTINCT l.id, pt.id FROM loaders l CROSS JOIN project_types pt WHERE pt.name = 'modpack' AND l.loader = 'mrpack'; +-- Set those versions to mrpack as their version +INSERT INTO loaders_versions (version_id, loader_id) +SELECT DISTINCT vf.version_id, l.id +FROM version_fields vf +LEFT JOIN loader_fields lf ON lf.id = vf.field_id +CROSS JOIN loaders l +WHERE lf.field = 'mrpack_loaders' +AND l.loader = 'mrpack' +ON CONFLICT DO NOTHING; + +-- Delete the old versions that had mrpack added to them +DELETE FROM loaders_versions lv +WHERE lv.loader_id != (SELECT id FROM loaders WHERE loader = 'mrpack') +AND lv.version_id IN ( + SELECT version_id + FROM loaders_versions + WHERE loader_id = (SELECT id FROM loaders WHERE loader = 'mrpack') +); + + --- Non-mrpack loaders no longer support modpacks DELETE FROM loaders_project_types WHERE joining_loader_id != (SELECT id FROM loaders WHERE loader = 'mrpack') AND joining_project_type_id = (SELECT id FROM project_types WHERE name = 'modpack'); diff --git a/migrations/20241110010322_adds_game_version_minmax.sql b/migrations/20231110010322_adds_game_version_minmax.sql similarity index 100% rename from migrations/20241110010322_adds_game_version_minmax.sql rename to migrations/20231110010322_adds_game_version_minmax.sql diff --git a/migrations/20231116112800_side_types_overhaul.sql b/migrations/20231116112800_side_types_overhaul.sql new file mode 100644 index 00000000..98e2551f --- /dev/null +++ b/migrations/20231116112800_side_types_overhaul.sql @@ -0,0 +1,96 @@ + +INSERT INTO loader_fields (field, field_type, optional) SELECT 'singleplayer', 'boolean', false; +INSERT INTO loader_fields (field, field_type, optional) SELECT 'client_and_server', 'boolean', false; +INSERT INTO loader_fields (field, field_type, optional) SELECT 'client_only', 'boolean', false; +INSERT INTO loader_fields (field, field_type, optional) SELECT 'server_only', 'boolean', false; + +-- Create 4 temporary columns for the four booleans (makes queries easier) +ALTER TABLE versions ADD COLUMN singleplayer boolean; +ALTER TABLE versions ADD COLUMN client_and_server boolean; +ALTER TABLE versions ADD COLUMN client_only boolean; +ALTER TABLE versions ADD COLUMN server_only boolean; + +-- Set singleplayer to be true if either client_side or server_side is 'required' OR 'optional' +UPDATE versions v SET singleplayer = true +FROM version_fields vf +INNER JOIN loader_fields lf ON vf.field_id = lf.id +INNER JOIN loader_field_enum_values lfev ON lf.enum_type = lfev.id AND vf.enum_value = lfev.id +WHERE v.id = vf.version_id +AND (lf.field = 'client_side' OR lf.field = 'server_side') AND (lfev.value = 'required' OR lfev.value = 'optional'); + +-- Set client and server to be true if either client_side or server_side is 'required' OR 'optional' +UPDATE versions v SET client_and_server = true +FROM version_fields vf +INNER JOIN loader_fields lf ON vf.field_id = lf.id +INNER JOIN loader_field_enum_values lfev ON lf.enum_type = lfev.id AND vf.enum_value = lfev.id +WHERE v.id = vf.version_id +AND (lf.field = 'client_side' OR lf.field = 'server_side') AND (lfev.value = 'required' OR lfev.value = 'optional'); + +-- Set client_only to be true if client_side is 'required' or 'optional', and server_side is 'optional', 'unsupported', or 'unknown' +UPDATE versions v SET client_only = true +FROM version_fields vf +INNER JOIN loader_fields lf ON vf.field_id = lf.id +INNER JOIN loader_field_enum_values lfev ON lf.enum_type = lfev.enum_id AND vf.enum_value = lfev.id +CROSS JOIN version_fields vf2 +INNER JOIN loader_fields lf2 ON vf2.field_id = lf2.id +INNER JOIN loader_field_enum_values lfev2 ON lf2.enum_type = lfev2.enum_id AND vf2.enum_value = lfev2.id +WHERE v.id = vf.version_id AND v.id = vf2.version_id +AND lf.field = 'client_side' AND (lfev.value = 'required' OR lfev.value = 'optional') +AND lf2.field = 'server_side' AND (lfev2.value = 'optional' OR lfev2.value = 'unsupported' OR lfev2.value = 'unknown'); + +-- Set server_only to be true if server_side is 'required' or 'optional', and client_side is 'optional', 'unsupported', or 'unknown' +UPDATE versions v SET server_only = true +FROM version_fields vf +INNER JOIN loader_fields lf ON vf.field_id = lf.id +INNER JOIN loader_field_enum_values lfev ON lf.enum_type = lfev.enum_id AND vf.enum_value = lfev.id +CROSS JOIN version_fields vf2 +INNER JOIN loader_fields lf2 ON vf2.field_id = lf2.id +INNER JOIN loader_field_enum_values lfev2 ON lf2.enum_type = lfev2.enum_id AND vf2.enum_value = lfev2.id +WHERE v.id = vf.version_id AND v.id = vf2.version_id +AND lf.field = 'server_side' AND (lfev.value = 'required' OR lfev.value = 'optional') +AND lf2.field = 'client_side' AND (lfev2.value = 'optional' OR lfev2.value = 'unsupported' OR lfev2.value = 'unknown'); + +-- Insert the values into the version_fields table +INSERT INTO version_fields (version_id, field_id, int_value) +SELECT v.id, lf.id, CASE WHEN v.singleplayer THEN 1 ELSE 0 END +FROM versions v +INNER JOIN loader_fields lf ON lf.field = 'singleplayer'; + +INSERT INTO version_fields (version_id, field_id, int_value) +SELECT v.id, lf.id, CASE WHEN v.client_and_server THEN 1 ELSE 0 END +FROM versions v +INNER JOIN loader_fields lf ON lf.field = 'client_and_server'; + +INSERT INTO version_fields (version_id, field_id, int_value) +SELECT v.id, lf.id, CASE WHEN v.client_only THEN 1 ELSE 0 END +FROM versions v +INNER JOIN loader_fields lf ON lf.field = 'client_only'; + +INSERT INTO version_fields (version_id, field_id, int_value) +SELECT v.id, lf.id, CASE WHEN v.server_only THEN 1 ELSE 0 END +FROM versions v +INNER JOIN loader_fields lf ON lf.field = 'server_only'; + +-- Drop the temporary columns +ALTER TABLE versions DROP COLUMN singleplayer; +ALTER TABLE versions DROP COLUMN client_and_server; +ALTER TABLE versions DROP COLUMN client_only; +ALTER TABLE versions DROP COLUMN server_only; + +-- For each loader where loader_fields_loaders is 'client_side' or 'server_side', add the new fields +INSERT INTO loader_fields_loaders (loader_id, loader_field_id) +SELECT lfl.loader_id, lf.id +FROM loader_fields_loaders lfl +CROSS JOIN loader_fields lf +WHERE lfl.loader_field_id IN (SELECT id FROM loader_fields WHERE field = 'client_side' OR field = 'server_side') +AND lf.field IN ('singleplayer', 'client_and_server', 'client_only', 'server_only') +ON CONFLICT DO NOTHING; + +-- Drop the old loader_fields_loaders entries +DELETE FROM loader_fields_loaders WHERE loader_field_id IN (SELECT id FROM loader_fields WHERE field = 'client_side' OR field = 'server_side'); + +-- Drop client_side and server_side loader fields +DELETE FROM version_fields WHERE field_id IN (SELECT id FROM loader_fields WHERE field = 'client_side' OR field = 'server_side'); +DELETE FROM loader_field_enum_values WHERE id IN (SELECT enum_type FROM loader_fields WHERE field = 'client_side' OR field = 'server_side'); +DELETE FROM loader_fields WHERE field = 'client_side' OR field = 'server_side'; +DELETE FROM loader_field_enums WHERE id IN (SELECT enum_type FROM loader_fields WHERE field = 'side_types'); diff --git a/migrations/20231122111700_adds_missing_loader_field_loaders.sql b/migrations/20231122111700_adds_missing_loader_field_loaders.sql deleted file mode 100644 index 8747a2cb..00000000 --- a/migrations/20231122111700_adds_missing_loader_field_loaders.sql +++ /dev/null @@ -1,45 +0,0 @@ - --- Adds missing fields to loader_fields_loaders -INSERT INTO loader_fields_loaders (loader_id, loader_field_id) -SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'game_versions' -AND l.loader = ANY( ARRAY['forge', 'fabric', 'quilt', 'modloader','rift','liteloader', 'neoforge']) -ON CONFLICT (loader_id, loader_field_id) DO NOTHING; - --- Fixes mrpack variants being added to the wrong enum --- Luckily, mrpack variants are the only ones set to 2 without metadata -UPDATE loader_field_enum_values SET enum_id = 3 WHERE enum_id = 2 AND metadata IS NULL; - --- Because it was mislabeled, version_fields for mrpack_loaders were set to null. --- 1) Update version_fields corresponding to mrpack_loaders to the correct enum_value -UPDATE version_fields vf -SET enum_value = subquery.lfev_id -FROM ( - SELECT vf.version_id, vf.field_id, lfev.id AS lfev_id - FROM version_fields vf - LEFT JOIN versions v ON v.id = vf.version_id - LEFT JOIN loaders_versions lv ON v.id = lv.version_id - LEFT JOIN loaders l ON l.id = lv.loader_id - LEFT JOIN loader_fields lf ON lf.id = vf.field_id - LEFT JOIN loader_field_enum_values lfev ON lfev.value = l.loader AND lf.enum_type = lfev.enum_id - WHERE lf.field = 'mrpack_loaders' AND vf.enum_value IS NULL -) AS subquery -WHERE vf.version_id = subquery.version_id AND vf.field_id = subquery.field_id; - --- 2) Set those versions to mrpack as their version -INSERT INTO loaders_versions (version_id, loader_id) -SELECT DISTINCT vf.version_id, l.id -FROM version_fields vf -LEFT JOIN loader_fields lf ON lf.id = vf.field_id -CROSS JOIN loaders l -WHERE lf.field = 'mrpack_loaders' -AND l.loader = 'mrpack' -ON CONFLICT DO NOTHING; - --- 3) Delete the old versions that had mrpack added to them -DELETE FROM loaders_versions lv -WHERE lv.loader_id != (SELECT id FROM loaders WHERE loader = 'mrpack') -AND lv.version_id IN ( - SELECT version_id - FROM loaders_versions - WHERE loader_id = (SELECT id FROM loaders WHERE loader = 'mrpack') -); diff --git a/src/database/models/loader_fields.rs b/src/database/models/loader_fields.rs index 22ceb316..744ea02a 100644 --- a/src/database/models/loader_fields.rs +++ b/src/database/models/loader_fields.rs @@ -320,6 +320,23 @@ impl LoaderField { exec: E, redis: &RedisPool, ) -> Result, 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>, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -336,11 +353,11 @@ impl LoaderField { .filter_map(|x: String| serde_json::from_str::(&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) diff --git a/src/models/v2/projects.rs b/src/models/v2/projects.rs index 5df1ecdb..af28b26b 100644 --- a/src/models/v2/projects.rs +++ b/src/models/v2/projects.rs @@ -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::>(); + (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, diff --git a/src/models/v3/pack.rs b/src/models/v3/pack.rs index 682d40dc..78b5931d 100644 --- a/src/models/v3/pack.rs +++ b/src/models/v3/pack.rs @@ -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, - pub env: Option>, + pub env: Option>, // 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, pub file_size: u32, diff --git a/src/models/v3/projects.rs b/src/models/v3/projects.rs index 63c6593b..5ed5541e 100644 --- a/src/models/v3/projects.rs +++ b/src/models/v3/projects.rs @@ -216,42 +216,6 @@ pub struct ModeratorMessage { pub body: Option, } -#[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)] diff --git a/src/routes/v2/project_creation.rs b/src/routes/v2/project_creation.rs index 6e36de5c..6108cd47 100644 --- a/src/routes/v2/project_creation.rs +++ b/src/routes/v2/project_creation.rs @@ -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. diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index debb7a95..f17c1cd1 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -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>> = if let Some(facets) = info.facets { - let facets = serde_json::from_str::>>(&facets)?; + let facets: Option>>> = if let Some(facets) = info.facets { + let facets = serde_json::from_str::>>(&facets)?; + // Search can now *optionally* have a third inner array: So 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>> = facets + .into_iter() + .map(|facets| { + facets + .into_iter() + .map(|facet| { + if facet.is_array() { + serde_json::from_value::>(facet).unwrap_or_default() + } else { + vec![serde_json::from_value::(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(), - }; - - if facet.starts_with("versions:") { - format!("game_versions:{}", val) - } else if facet.starts_with("project_type:") { - format!("project_types:{}", val) - } else { - 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() + } + }) + .collect::>() }) .collect::>() }) @@ -279,8 +306,8 @@ pub struct EditProject { #[validate] pub donation_urls: Option>, pub license_id: Option, - pub client_side: Option, - pub server_side: Option, + pub client_side: Option, + pub server_side: Option, #[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, ) -> Result { 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 { diff --git a/src/routes/v2/tags.rs b/src/routes/v2/tags.rs index 2f4075ea..38081fdb 100644 --- a/src/routes/v2/tags.rs +++ b/src/routes/v2/tags.rs @@ -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, - redis: web::Data, -) -> Result { - 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::>(response).await { - Ok(fields) => { - let fields = fields.into_iter().map(|f| f.value).collect::>(); - HttpResponse::Ok().json(fields) - } - Err(response) => response, - }, - ) +pub async fn side_type_list() -> Result { + // 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)) } diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index d55d851e..997189dc 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -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), - ); - - // 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; + 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), + ); + + // 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 + } + })); + } + + // 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 + _ => {} } - // No other type matters - _ => {} + break; } - 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)); - } + // 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, - }) + 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, + pool: Data, + redis: &RedisPool, +) -> Result>, 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( diff --git a/src/routes/v2_reroute.rs b/src/routes/v2_reroute.rs index 7e2c16cf..6672425b 100644 --- a/src/routes/v2_reroute.rs +++ b/src/routes/v2_reroute.rs @@ -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(response: HttpResponse) -> Result where @@ -29,14 +33,15 @@ where } } -pub async fn alter_actix_multipart( +pub async fn alter_actix_multipart( mut multipart: Multipart, mut headers: HeaderMap, - mut closure: impl FnMut(T) -> Result, + mut closure: impl FnMut(T) -> Fut, ) -> Result where T: serde::de::DeserializeOwned, U: serde::Serialize, + Fut: Future>, { let mut segments: Vec = 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 { + 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>> { + 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::>() + }) + .collect::>(), + (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::>() + }) + .collect::>(), + }; + + // 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, +) -> (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::>() + }) + .collect::>(); + + 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::>() + }) + .collect::>(); + + assert_eq!(converted_facets, post_facets); + } +} diff --git a/src/routes/v3/oauth_clients.rs b/src/routes/v3/oauth_clients.rs index 881cd040..87d7222d 100644 --- a/src/routes/v3/oauth_clients.rs +++ b/src/routes/v3/oauth_clients.rs @@ -511,7 +511,6 @@ pub async fn revoke_oauth_authorization( redis: web::Data, session_queue: web::Data, ) -> Result { - println!("Inside revoke_oauth_authorization"); let current_user = get_user_from_headers( &req, &**pool, diff --git a/src/routes/v3/tags.rs b/src/routes/v3/tags.rs index 3e6940ae..3c0ecd20 100644 --- a/src/routes/v3/tags.rs +++ b/src/routes/v3/tags.rs @@ -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, pub supported_games: Vec, + pub supported_fields: Vec, // Available loader fields for this loader pub metadata: Value, } @@ -91,14 +93,26 @@ pub async fn loader_list( pool: web::Data, redis: web::Data, ) -> Result { - 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::>(); diff --git a/src/search/mod.rs b/src/search/mod.rs index 4c5648a7..6f9a655d 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -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::>>(facets)?) + Some(serde_json::from_str::>>(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)>> + // 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>> = facets + .into_iter() + .map(|facets| { + facets + .into_iter() + .map(|facet| { + if facet.is_array() { + serde_json::from_value::>(facet).unwrap_or_default() + } else { + vec![serde_json::from_value::(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 ") } } diff --git a/tests/common/api_v3/request_data.rs b/tests/common/api_v3/request_data.rs index 3110095b..fdbc5500 100644 --- a/tests/common/api_v3/request_data.rs +++ b/tests/common/api_v3/request_data.rs @@ -83,8 +83,10 @@ pub fn get_public_version_creation_data_json( // Loader fields "game_versions": ["1.20.1"], - "client_side": "required", - "server_side": "optional" + "singleplayer": true, + "client_and_server": true, + "client_only": true, + "server_only": false, }); if is_modpack { j["mrpack_loaders"] = json!(["fabric"]); diff --git a/tests/common/api_v3/tags.rs b/tests/common/api_v3/tags.rs index a7f75061..54d436cc 100644 --- a/tests/common/api_v3/tags.rs +++ b/tests/common/api_v3/tags.rs @@ -3,10 +3,8 @@ use actix_web::{ test::{self, TestRequest}, }; use async_trait::async_trait; -use labrinth::routes::v3::tags::GameData; -use labrinth::{ - database::models::loader_fields::LoaderFieldEnumValue, routes::v3::tags::LoaderData, -}; +use labrinth::database::models::loader_fields::LoaderFieldEnumValue; +use labrinth::routes::v3::tags::{GameData, LoaderData}; use crate::common::{ api_common::{ diff --git a/tests/common/dummy_data.rs b/tests/common/dummy_data.rs index 1d4988a4..ccb912a1 100644 --- a/tests/common/dummy_data.rs +++ b/tests/common/dummy_data.rs @@ -24,7 +24,7 @@ use super::{ use super::{asserts::assert_status, database::USER_USER_ID, get_json_val_str}; -pub const DUMMY_DATA_UPDATE: i64 = 5; +pub const DUMMY_DATA_UPDATE: i64 = 6; #[allow(dead_code)] pub const DUMMY_CATEGORIES: &[&str] = &[ @@ -340,8 +340,10 @@ pub async fn add_project_beta(api: &ApiV3) -> (CommonProject, CommonVersion) { "version_title": "start", "status": "unlisted", "dependencies": [], - "client_side": "required", - "server_side": "optional", + "singleplayer": true, + "client_and_server": true, + "client_only": true, + "server_only": false, "game_versions": ["1.20.1"] , "release_channel": "release", "loaders": ["fabric"], diff --git a/tests/files/dummy_data.sql b/tests/files/dummy_data.sql index e332dbfc..52577791 100644 --- a/tests/files/dummy_data.sql +++ b/tests/files/dummy_data.sql @@ -68,7 +68,7 @@ INSERT INTO loader_field_enum_values(enum_id, value, metadata, ordering) VALUES (2, 'Ordering_Positive100', '{"type":"release","major":false}', 100); INSERT INTO loader_fields_loaders(loader_id, loader_field_id) -SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'game_versions' OR lf.field = 'client_side' OR lf.field = 'server_side'; +SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field IN ('game_versions','singleplayer', 'client_and_server', 'client_only', 'server_only'); INSERT INTO categories (id, category, project_type) VALUES (51, 'combat', 1), diff --git a/tests/loader_fields.rs b/tests/loader_fields.rs index 14bb3812..03e0925c 100644 --- a/tests/loader_fields.rs +++ b/tests/loader_fields.rs @@ -112,7 +112,7 @@ async fn creating_loader_fields() { Some( serde_json::from_value(json!([{ "op": "remove", - "path": "/client_side" + "path": "/singleplayer" }])) .unwrap(), ), @@ -183,7 +183,7 @@ async fn creating_loader_fields() { json!(1), json!([1]), json!("1.20.1"), - json!(["client_side"]), + json!(["singleplayer"]), ] { // TODO: - Create project // - Create version @@ -271,12 +271,12 @@ async fn creating_loader_fields() { "value": ["1.20.1", "1.20.2"] }, { "op": "add", - "path": "/client_side", - "value": "optional" + "path": "/singleplayer", + "value": false }, { "op": "add", - "path": "/server_side", - "value": "required" + "path": "/server_only", + "value": true }])) .unwrap(), ), @@ -287,16 +287,16 @@ async fn creating_loader_fields() { v.fields.get("game_versions").unwrap(), &json!(["1.20.1", "1.20.2"]) ); - assert_eq!(v.fields.get("client_side").unwrap(), &json!("optional")); - assert_eq!(v.fields.get("server_side").unwrap(), &json!("required")); + assert_eq!(v.fields.get("singleplayer").unwrap(), &json!(false)); + assert_eq!(v.fields.get("server_only").unwrap(), &json!(true)); // - Patch let resp = api .edit_version( alpha_version_id, json!({ "game_versions": ["1.20.1", "1.20.2"], - "client_side": "optional", - "server_side": "required" + "singleplayer": false, + "server_only": true }), USER_USER_PAT, ) @@ -314,16 +314,13 @@ async fn creating_loader_fields() { } #[actix_rt::test] -async fn get_loader_fields() { +async fn get_loader_fields_variants() { with_test_environment(None, |test_env: TestEnvironment| async move { let api = &test_env.api; let game_versions = api .get_loader_field_variants_deserialized("game_versions") .await; - let side_types = api - .get_loader_field_variants_deserialized("client_side") - .await; // These tests match dummy data and will need to be updated if the dummy data changes // Versions should be ordered by: @@ -348,18 +345,64 @@ async fn get_loader_fields() { "1.20.1" ] ); + }) + .await +} - let side_type_names = side_types +#[actix_rt::test] +async fn get_available_loader_fields() { + // Get available loader fields for a given loader + // (ie: which fields are relevant for 'fabric', etc) + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + let loaders = api.get_loaders_deserialized().await; + + let fabric_loader_fields = loaders + .iter() + .find(|x| x.name == "fabric") + .unwrap() + .supported_fields + .clone() .into_iter() - .map(|x| x.value) .collect::>(); assert_eq!( - side_type_names, - ["unknown", "required", "optional", "unsupported"] - .iter() - .map(|s| s.to_string()) - .collect() + fabric_loader_fields, + [ + "game_versions", + "singleplayer", + "client_and_server", + "client_only", + "server_only", + "test_fabric_optional" // exists for testing + ] + .iter() + .map(|s| s.to_string()) + .collect() + ); + + let mrpack_loader_fields = loaders + .iter() + .find(|x| x.name == "mrpack") + .unwrap() + .supported_fields + .clone() + .into_iter() + .collect::>(); + assert_eq!( + mrpack_loader_fields, + [ + "game_versions", + "singleplayer", + "client_and_server", + "client_only", + "server_only", + // mrpack has all the general fields as well as this + "mrpack_loaders" + ] + .iter() + .map(|s| s.to_string()) + .collect() ); }) - .await + .await; } diff --git a/tests/project.rs b/tests/project.rs index 0c71bd56..07c1343d 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -2,19 +2,24 @@ use actix_http::StatusCode; use actix_web::test; use bytes::Bytes; use chrono::{Duration, Utc}; +use common::api_v3::request_data::get_public_version_creation_data; +use common::api_v3::ApiV3; use common::database::*; use common::dummy_data::DUMMY_CATEGORIES; -use common::environment::with_test_environment_all; +use common::environment::{with_test_environment, with_test_environment_all, TestEnvironment}; use common::permissions::{PermissionsTest, PermissionsTestContext}; use futures::StreamExt; use labrinth::database::models::project_item::{PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE}; use labrinth::models::ids::base62_impl::parse_base62; +use labrinth::models::projects::ProjectId; use labrinth::models::teams::ProjectPermissions; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use serde_json::json; use crate::common::api_common::{ApiProject, ApiVersion}; +use crate::common::api_v3::request_data::get_public_project_creation_data_json; +use crate::common::dummy_data::TestFile; mod common; @@ -101,32 +106,11 @@ async fn test_get_project() { #[actix_rt::test] async fn test_add_remove_project() { // Test setup and dummy data - with_test_environment_all(None, |test_env| async move { + with_test_environment(None, |test_env: TestEnvironment| async move { let api = &test_env.api; - // Generate test project data. - let mut json_data = json!( - { - "title": "Test_Add_Project project", - "slug": "demo", - "description": "Example description.", - "body": "Example body.", - "initial_versions": [{ - "file_parts": ["basic-mod.jar"], - "version_number": "1.2.3", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - }], - "categories": [], - "license_id": "MIT" - } - ); + let mut json_data = + get_public_project_creation_data_json("demo", Some(&TestFile::BasicMod)); // Basic json let json_segment = MultipartSegment { @@ -730,48 +714,27 @@ async fn permissions_edit_details() { #[actix_rt::test] async fn permissions_upload_version() { - with_test_environment_all(None, |test_env| async move { + with_test_environment(None, |test_env: TestEnvironment| async move { let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; let alpha_file_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; let upload_version = ProjectPermissions::UPLOAD_VERSION; - // Upload version with basic-mod.jar let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post().uri("/v3/version").set_multipart([ - MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text( - serde_json::to_string(&json!({ - "project_id": ctx.project_id.unwrap(), - "file_parts": ["basic-mod.jar"], - "version_number": "1.0.0", - "version_title": "1.0.0", - "version_type": "release", - "client_side": "required", - "server_side": "optional", - "dependencies": [], - "game_versions": ["1.20.1"], - "loaders": ["fabric"], - "featured": false, - - })) - .unwrap(), - ), - }, - MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/basic-mod.jar").to_vec(), - ), - }, - ]) + let project_id = ctx.project_id.unwrap(); + let project_id = ProjectId(parse_base62(project_id).unwrap()); + let multipart = get_public_version_creation_data( + project_id, + "1.0.0", + TestFile::BasicMod, + None, + None, + ); + test::TestRequest::post() + .uri("/v3/version") + .set_multipart(multipart.segment_data) }; PermissionsTest::new(&test_env) .simple_project_permissions_test(upload_version, req_gen) diff --git a/tests/scopes.rs b/tests/scopes.rs index 4b692058..880d16f7 100644 --- a/tests/scopes.rs +++ b/tests/scopes.rs @@ -2,9 +2,16 @@ use actix_web::test::{self, TestRequest}; use bytes::Bytes; use chrono::{Duration, Utc}; -use common::environment::with_test_environment_all; +use common::api_v3::request_data::{ + get_public_project_creation_data, get_public_version_creation_data, +}; +use common::api_v3::ApiV3; +use common::dummy_data::TestFile; +use common::environment::{with_test_environment, with_test_environment_all, TestEnvironment}; use common::{database::*, scopes::ScopeTest}; +use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::pats::Scopes; +use labrinth::models::projects::ProjectId; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use serde_json::json; @@ -201,93 +208,37 @@ pub async fn notifications_scopes() { // Project version creation scopes #[actix_rt::test] pub async fn project_version_create_scopes() { - with_test_environment_all(None, |test_env| async move { + with_test_environment(None, |test_env: TestEnvironment| async move { // Create project let create_project = Scopes::PROJECT_CREATE; - let json_data = json!( - { - "title": "Test_Add_Project project", - "slug": "demo", - "description": "Example description.", - "body": "Example body.", - "initial_versions": [{ - "file_parts": ["basic-mod.jar"], - "version_number": "1.2.3", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - }], - "categories": [], - "license_id": "MIT" - } - ); - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - let file_segment = MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/basic-mod.jar").to_vec(), - ), - }; - let req_gen = || { + let creation_data = + get_public_project_creation_data("demo", Some(TestFile::BasicMod), None); test::TestRequest::post() .uri("/v3/project") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + .set_multipart(creation_data.segment_data) }; let (_, success) = ScopeTest::new(&test_env) .test(req_gen, create_project) .await .unwrap(); let project_id = success["id"].as_str().unwrap(); + let project_id = ProjectId(parse_base62(project_id).unwrap()); // Add version to project let create_version = Scopes::VERSION_CREATE; - let json_data = json!( - { - "project_id": project_id, - "file_parts": ["basic-mod-different.jar"], - "version_number": "1.2.3.4", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - } - ); - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - let file_segment = MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/basic-mod-different.jar").to_vec(), - ), - }; - let req_gen = || { + let creation_data = get_public_version_creation_data( + project_id, + "1.2.3.4", + TestFile::BasicModDifferent, + None, + None, + ); + test::TestRequest::post() .uri("/v3/version") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + .set_multipart(creation_data.segment_data) }; ScopeTest::new(&test_env) .test(req_gen, create_version) diff --git a/tests/search.rs b/tests/search.rs index 4a3b6b73..76698bee 100644 --- a/tests/search.rs +++ b/tests/search.rs @@ -66,7 +66,7 @@ async fn search_projects() { let id = 0; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[4..6] }, - { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, ])) .unwrap(); @@ -81,7 +81,7 @@ async fn search_projects() { let id = 1; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, - { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, + { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, ])) .unwrap(); project_creation_futures.push(create_async_future( @@ -95,7 +95,7 @@ async fn search_projects() { let id = 2; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, - { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, { "op": "add", "path": "/title", "value": "Mysterious Project" }, ])) .unwrap(); @@ -110,7 +110,7 @@ async fn search_projects() { let id = 3; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, - { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.4"] }, { "op": "add", "path": "/title", "value": "Mysterious Project" }, { "op": "add", "path": "/license_id", "value": "LicenseRef-All-Rights-Reserved" }, @@ -127,7 +127,7 @@ async fn search_projects() { let id = 4; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, - { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, + { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] }, ])) .unwrap(); @@ -142,7 +142,7 @@ async fn search_projects() { let id = 5; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, - { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, + { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, ])) @@ -158,8 +158,8 @@ async fn search_projects() { let id = 6; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, - { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, - { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, + { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, ])) .unwrap(); @@ -176,8 +176,8 @@ async fn search_projects() { let id = 7; let modify_json = serde_json::from_value(json!([ { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, - { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, - { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/initial_versions/0/client_only", "value": false }, + { "op": "add", "path": "/initial_versions/0/server_only", "value": true }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, { "op": "add", "path": "/initial_versions/0/loaders", "value": ["forge"] }, { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.2"] }, @@ -236,8 +236,8 @@ async fn search_projects() { vec![1, 2, 3, 4], ), (json!([["project_types:modpack"]]), vec![4]), - (json!([["client_side:required"]]), vec![0, 2, 3, 7]), - (json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]), + (json!([["client_only:true"]]), vec![0, 2, 3, 7]), + (json!([["server_only:true"]]), vec![0, 2, 3, 6, 7]), (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), (json!([["license:MIT"]]), vec![1, 2, 4]), (json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]), diff --git a/tests/v2/project.rs b/tests/v2/project.rs index 2bb4c757..fe01e991 100644 --- a/tests/v2/project.rs +++ b/tests/v2/project.rs @@ -1,6 +1,9 @@ use crate::common::{ api_common::ApiProject, - api_v2::ApiV2, + api_v2::{ + request_data::{get_public_project_creation_data_json, get_public_version_creation_data}, + ApiV2, + }, database::{ENEMY_USER_PAT, FRIEND_USER_ID, FRIEND_USER_PAT, MOD_USER_PAT, USER_USER_PAT}, dummy_data::{TestFile, DUMMY_CATEGORIES}, environment::{with_test_environment, TestEnvironment}, @@ -10,7 +13,7 @@ use actix_web::test; use itertools::Itertools; use labrinth::{ database::models::project_item::PROJECTS_SLUGS_NAMESPACE, - models::teams::ProjectPermissions, + models::{ids::base62_impl::parse_base62, projects::ProjectId, teams::ProjectPermissions}, util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}, }; use serde_json::json; @@ -63,28 +66,8 @@ async fn test_add_remove_project() { let api = &test_env.api; // Generate test project data. - let mut json_data = json!( - { - "title": "Test_Add_Project project", - "slug": "demo", - "description": "Example description.", - "body": "Example body.", - "client_side": "required", - "server_side": "optional", - "initial_versions": [{ - "file_parts": ["basic-mod.jar"], - "version_number": "1.2.3", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - }], - "categories": [], - "license_id": "MIT" - } - ); + let mut json_data = + get_public_project_creation_data_json("demo", Some(&TestFile::BasicMod)); // Basic json let json_segment = MultipartSegment { @@ -251,36 +234,18 @@ async fn permissions_upload_version() { // Upload version with basic-mod.jar let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post().uri("/v2/version").set_multipart([ - MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text( - serde_json::to_string(&json!({ - "project_id": ctx.project_id.unwrap(), - "file_parts": ["basic-mod.jar"], - "version_number": "1.0.0", - "version_title": "1.0.0", - "version_type": "release", - "dependencies": [], - "game_versions": ["1.20.1"], - "loaders": ["fabric"], - "featured": false, - - })) - .unwrap(), - ), - }, - MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod.jar").to_vec(), - ), - }, - ]) + let project_id = ctx.project_id.unwrap(); + let project_id = ProjectId(parse_base62(project_id).unwrap()); + let multipart = get_public_version_creation_data( + project_id, + "1.0.0", + TestFile::BasicMod, + None, + None, + ); + test::TestRequest::post() + .uri("/v2/version") + .set_multipart(multipart.segment_data) }; PermissionsTest::new(&test_env) .simple_project_permissions_test(upload_version, req_gen) @@ -491,7 +456,7 @@ pub async fn test_patch_project() { "issues_url": "https://github.com", "discord_url": "https://discord.gg", "wiki_url": "https://wiki.com", - "client_side": "optional", + "client_side": "unsupported", "server_side": "required", "donation_urls": [{ "id": "patreon", @@ -520,7 +485,11 @@ pub async fn test_patch_project() { assert_eq!(project.issues_url, Some("https://github.com".to_string())); assert_eq!(project.discord_url, Some("https://discord.gg".to_string())); assert_eq!(project.wiki_url, Some("https://wiki.com".to_string())); - assert_eq!(project.client_side.as_str(), "optional"); + // Note: the original V2 value of this was "optional", + // but Required/Optional is no longer a carried combination in v3, as the changes made were lossy. + // Now, the test Required/Unsupported combination is tested instead. + // Setting Required/Optional in v2 will not work, this is known and accepteed. + assert_eq!(project.client_side.as_str(), "unsupported"); assert_eq!(project.server_side.as_str(), "required"); assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); }) diff --git a/tests/v2/scopes.rs b/tests/v2/scopes.rs index 1648b90e..ff77670d 100644 --- a/tests/v2/scopes.rs +++ b/tests/v2/scopes.rs @@ -1,104 +1,50 @@ +use crate::common::api_v2::request_data::get_public_project_creation_data; +use crate::common::api_v2::request_data::get_public_version_creation_data; use crate::common::api_v2::ApiV2; +use crate::common::dummy_data::TestFile; use crate::common::environment::with_test_environment; use crate::common::environment::TestEnvironment; use crate::common::scopes::ScopeTest; use actix_web::test; +use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::pats::Scopes; +use labrinth::models::projects::ProjectId; use labrinth::util::actix::AppendsMultipart; -use labrinth::util::actix::MultipartSegment; -use labrinth::util::actix::MultipartSegmentData; -use serde_json::json; // Project version creation scopes #[actix_rt::test] pub async fn project_version_create_scopes() { with_test_environment(None, |test_env: TestEnvironment| async move { // Create project let create_project = Scopes::PROJECT_CREATE; - let json_data = json!( - { - "title": "Test_Add_Project project", - "slug": "demo", - "description": "Example description.", - "body": "Example body.", - "initial_versions": [{ - "file_parts": ["basic-mod.jar"], - "version_number": "1.2.3", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - }], - "categories": [], - "license_id": "MIT" - } - ); - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - let file_segment = MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod.jar").to_vec(), - ), - }; let req_gen = || { + let creation_data = + get_public_project_creation_data("demo", Some(TestFile::BasicMod), None); test::TestRequest::post() - .uri("/v3/project") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + .uri("/v2/project") + .set_multipart(creation_data.segment_data) }; let (_, success) = ScopeTest::new(&test_env) .test(req_gen, create_project) .await .unwrap(); let project_id = success["id"].as_str().unwrap(); + let project_id = ProjectId(parse_base62(project_id).unwrap()); // Add version to project let create_version = Scopes::VERSION_CREATE; - let json_data = json!( - { - "project_id": project_id, - "file_parts": ["basic-mod-different.jar"], - "version_number": "1.2.3.4", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - } - ); - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - let file_segment = MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), - ), - }; - let req_gen = || { + let creation_data = get_public_version_creation_data( + project_id, + "1.2.3.4", + TestFile::BasicModDifferent, + None, + None, + ); test::TestRequest::post() - .uri("/v3/version") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + .uri("/v2/version") + .set_multipart(creation_data.segment_data) }; ScopeTest::new(&test_env) .test(req_gen, create_version) diff --git a/tests/v2/search.rs b/tests/v2/search.rs index 944a4dd5..22e7ffa4 100644 --- a/tests/v2/search.rs +++ b/tests/v2/search.rs @@ -214,6 +214,15 @@ async fn search_projects() { // 1. vec of search facets // 2. expected project ids to be returned by this search let pairs = vec![ + // For testing: remove me + ( + json!([ + ["client_side:required"], + ["versions:1.20.5"], + [&format!("categories:{}", DUMMY_CATEGORIES[5])] + ]), + vec![], + ), (json!([["categories:fabric"]]), vec![0, 1, 2, 3, 4, 5, 6, 7]), (json!([["categories:forge"]]), vec![7]), ( @@ -229,7 +238,9 @@ async fn search_projects() { vec![1, 2, 3, 4], ), (json!([["project_types:modpack"]]), vec![4]), - (json!([["client_side:required"]]), vec![0, 2, 3, 7]), + // Formerly included 7, but with v2 changes, this is no longer the case. + // This is because we assume client_side/server_side with subsequent versions. + (json!([["client_side:required"]]), vec![0, 2, 3]), (json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]), (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), (json!([["license:MIT"]]), vec![1, 2, 4]), diff --git a/tests/v2/version.rs b/tests/v2/version.rs index ba8f0c35..07e40021 100644 --- a/tests/v2/version.rs +++ b/tests/v2/version.rs @@ -428,9 +428,9 @@ async fn add_version_project_types_v2() { .get_project_deserialized(&test_project.slug.unwrap(), USER_USER_PAT) .await; assert_eq!(test_project.project_type, "unknown"); // No project_type set, as no versions are set - // This is a known difference between older v2 ,but is acceptable. - // This would be the appropriate test on older v2: - // assert_eq!(test_project.project_type, "modpack"); + // This is a known difference between older v2 ,but is acceptable. + // This would be the appropriate test on older v2: + // assert_eq!(test_project.project_type, "modpack"); // Create a version with a modpack file attached let test_version = api