Side types overhaul (#762)

* side types overhaul

* fixes, fmt clippy

* migration fix for v3 bug

* fixed migration issues

* more tested migration changes

* fmt, clippy

* bump cicd

---------

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

View File

@@ -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 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) 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 FROM versions v
INNER JOIN mods m ON v.mod_id = m.id INNER JOIN mods m ON v.mod_id = m.id
INNER JOIN loader_field_enum_values lfev ON m.client_side = lfev.original_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) 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 FROM versions v
INNER JOIN mods m ON v.mod_id = m.id INNER JOIN mods m ON v.mod_id = m.id
INNER JOIN loader_field_enum_values lfev ON m.client_side = lfev.original_id INNER JOIN loader_field_enum_values lfev ON m.server_side = lfev.original_id
WHERE server_side IS NOT NULL AND lfev.enum_id = 1; 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 client_side;
ALTER TABLE mods DROP COLUMN server_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; 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 (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) 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 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 loaders;
ALTER TABLE mods DROP COLUMN game_versions; ALTER TABLE mods DROP COLUMN game_versions;
@@ -108,12 +112,13 @@ DROP TABLE game_versions;
-- Convert project types -- Convert project types
-- we are creating a new loader type- 'mrpack'- for minecraft modpacks -- 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'); INSERT INTO loaders (loader) VALUES ('mrpack');
-- For the loader 'mrpack', we create loader fields for every loader -- For the loader 'mrpack', we create loader fields for every loader
-- That way we keep information like "this modpack is a fabric modpack" -- 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_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 (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) 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'; 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_versions lv ON v.id = lv.version_id
INNER JOIN loaders l ON lv.loader_id = l.id INNER JOIN loaders l ON lv.loader_id = l.id
CROSS JOIN loader_fields lf 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'; 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'; 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 --- 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'); 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');

View File

@@ -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');

View File

@@ -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')
);

View File

@@ -320,6 +320,23 @@ impl LoaderField {
exec: E, exec: E,
redis: &RedisPool, redis: &RedisPool,
) -> Result<Vec<LoaderField>, DatabaseError> ) -> 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 where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -336,11 +353,11 @@ impl LoaderField {
.filter_map(|x: String| serde_json::from_str::<RedisLoaderFieldTuple>(&x).ok()) .filter_map(|x: String| serde_json::from_str::<RedisLoaderFieldTuple>(&x).ok())
.collect(); .collect();
let mut found_loader_fields = vec![]; let mut found_loader_fields = HashMap::new();
if !cached_fields.is_empty() { if !cached_fields.is_empty() {
for (loader_id, fields) in cached_fields { for (loader_id, fields) in cached_fields {
if loader_ids.contains(&loader_id) { 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); loader_ids.retain(|x| x != &loader_id);
} }
} }
@@ -388,14 +405,10 @@ impl LoaderField {
redis redis
.set_serialized_to_json(LOADER_FIELDS_NAMESPACE, k.0, (k, &v), None) .set_serialized_to_json(LOADER_FIELDS_NAMESPACE, k.0, (k, &v), None)
.await?; .await?;
found_loader_fields.extend(v); found_loader_fields.insert(k, v);
} }
} }
let result = found_loader_fields Ok(found_loader_fields)
.into_iter()
.unique_by(|x| x.id)
.collect();
Ok(result)
} }
// Gets all fields for a given loader(s) // Gets all fields for a given loader(s)

View File

@@ -1,3 +1,5 @@
use std::collections::HashMap;
use super::super::ids::OrganizationId; use super::super::ids::OrganizationId;
use super::super::teams::TeamId; use super::super::teams::TeamId;
use super::super::users::UserId; use super::super::users::UserId;
@@ -10,6 +12,7 @@ use crate::models::projects::{
Project, ProjectStatus, Version, VersionFile, VersionStatus, VersionType, Project, ProjectStatus, Version, VersionFile, VersionStatus, VersionType,
}; };
use crate::models::threads::ThreadId; use crate::models::threads::ThreadId;
use crate::routes::v2_reroute;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -85,26 +88,6 @@ impl LegacyProject {
let mut loaders = data.loaders; let mut loaders = data.loaders;
if let Some(versions_item) = versions_item { 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 game_versions = versions_item
.version_fields .version_fields
.iter() .iter()
@@ -113,6 +96,14 @@ impl LegacyProject {
.map(|v| v.into_iter().map(|v| v.version).collect()) .map(|v| v.into_iter().map(|v| v.version).collect())
.unwrap_or(Vec::new()); .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 // - if loader is mrpack, this is a modpack
// the loaders are whatever the corresponding loader fields are // the loaders are whatever the corresponding loader fields are
if versions_item.loaders == vec!["mrpack".to_string()] { 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")] #[serde(rename_all = "kebab-case")]
pub enum LegacySideType { pub enum LegacySideType {
Required, Required,

View File

@@ -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 serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
@@ -23,7 +23,7 @@ pub struct PackFormat {
pub struct PackFile { pub struct PackFile {
pub path: String, pub path: String,
pub hashes: std::collections::HashMap<PackFileHash, 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"))] #[validate(custom(function = "validate_download_url"))]
pub downloads: Vec<String>, pub downloads: Vec<String>,
pub file_size: u32, pub file_size: u32,

View File

@@ -216,42 +216,6 @@ pub struct ModeratorMessage {
pub body: Option<String>, 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"; pub const DEFAULT_LICENSE_ID: &str = "LicenseRef-All-Rights-Reserved";
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]

View File

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

View File

@@ -3,9 +3,9 @@ use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost; use crate::file_hosting::FileHost;
use crate::models; use crate::models;
use crate::models::projects::{ 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::models::v2::search::LegacySearchResults;
use crate::queue::session::AuthQueue; use crate::queue::session::AuthQueue;
use crate::routes::v3::projects::ProjectIds; 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 crate::search::{search_for_project, SearchConfig, SearchError};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::PgPool; use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use validator::Validate; 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 // 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 // While the backend for this has changed, it doesnt affect much
// in the API calls except that 'versions:x' is now 'game_versions:x' // 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: Option<Vec<Vec<Vec<String>>>> = if let Some(facets) = info.facets {
let facets = serde_json::from_str::<Vec<Vec<&str>>>(&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( Some(
facets facets
.into_iter() .into_iter()
.map(|facet| { .map(|facet| {
facet facet
.into_iter() .into_iter()
.map(|facet| { .map(|facets| {
let val = match facet.split(':').nth(1) { facets
Some(val) => val, .into_iter()
None => return facet.to_string(), .map(|facet| {
}; let val = match facet.split(':').nth(1) {
Some(val) => val,
None => return facet.to_string(),
};
if facet.starts_with("versions:") { if facet.starts_with("versions:") {
format!("game_versions:{}", val) format!("game_versions:{}", val)
} else if facet.starts_with("project_type:") { } else if facet.starts_with("project_type:") {
format!("project_types:{}", val) format!("project_types:{}", val)
} else { } else {
facet.to_string() facet.to_string()
} }
})
.collect::<Vec<_>>()
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })
@@ -279,8 +306,8 @@ pub struct EditProject {
#[validate] #[validate]
pub donation_urls: Option<Vec<DonationLink>>, pub donation_urls: Option<Vec<DonationLink>>,
pub license_id: Option<String>, pub license_id: Option<String>,
pub client_side: Option<SideType>, pub client_side: Option<LegacySideType>,
pub server_side: Option<SideType>, pub server_side: Option<LegacySideType>,
#[validate( #[validate(
length(min = 3, max = 64), length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE" regex = "crate::util::validate::RE_URL_SAFE"
@@ -321,8 +348,8 @@ pub async fn project_edit(
session_queue: web::Data<AuthQueue>, session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let v2_new_project = new_project.into_inner(); let v2_new_project = new_project.into_inner();
let client_side = v2_new_project.client_side.clone(); let client_side = v2_new_project.client_side;
let server_side = v2_new_project.server_side.clone(); let server_side = v2_new_project.server_side;
let new_slug = v2_new_project.slug.clone(); let new_slug = v2_new_project.slug.clone();
// TODO: Some kind of handling here to ensure project type is fine. // 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 version_ids = project_item.map(|x| x.versions).unwrap_or_default();
let versions = version_item::Version::get_many(&version_ids, &**pool, &redis).await?; let versions = version_item::Version::get_many(&version_ids, &**pool, &redis).await?;
for version in versions { for version in versions {
let mut fields = HashMap::new(); let version = Version::from(version);
fields.insert("client_side".to_string(), json!(client_side)); let mut fields = version.fields;
fields.insert("server_side".to_string(), json!(server_side)); 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( response = v3::versions::version_edit_helper(
req.clone(), req.clone(),
(version.inner.id.into(),), (version.id,),
pool.clone(), pool.clone(),
redis.clone(), redis.clone(),
v3::versions::EditVersion { v3::versions::EditVersion {

View File

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

View File

@@ -1,3 +1,5 @@
use crate::database::models::loader_fields::VersionField;
use crate::database::models::{project_item, version_item};
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost; use crate::file_hosting::FileHost;
use crate::models::ids::ImageId; use crate::models::ids::ImageId;
@@ -88,63 +90,90 @@ pub async fn version_create(
payload, payload,
req.headers().clone(), req.headers().clone(),
|legacy_create: InitialVersionData| { |legacy_create: InitialVersionData| {
// Convert input data to V3 format let client = client.clone();
let mut fields = HashMap::new(); let redis = redis.clone();
fields.insert( async move {
"game_versions".to_string(), // Convert input data to V3 format
json!(legacy_create.game_versions), let mut fields = HashMap::new();
); fields.insert(
"game_versions".to_string(),
json!(legacy_create.game_versions),
);
// TODO: will be overhauled with side-types overhaul // Copies side types of another version of the project.
// TODO: if not, should default to previous version // If no version exists, defaults to all false.
fields.insert("client_side".to_string(), json!("required")); // TODO: write test for this to ensure predictible unchanging behaviour
fields.insert("server_side".to_string(), json!("optional")); // 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.
// Handle project type via file extension prediction let side_type_loader_field_names = [
let mut project_type = None; "singleplayer",
for file_part in &legacy_create.file_parts { "client_and_server",
if let Some(ext) = file_part.split('.').last() { "client_only",
match ext { "server_only",
"mrpack" | "mrpack-primary" => { ];
project_type = Some("modpack"); fields.extend(
break; 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?; .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} // under /api/v1/version/{version_id}
#[post("{version_id}/file")] #[post("{version_id}/file")]
pub async fn upload_file_to_version( pub async fn upload_file_to_version(

View File

@@ -1,10 +1,14 @@
use std::collections::HashMap;
use super::v3::project_creation::CreateError; use super::v3::project_creation::CreateError;
use crate::models::v2::projects::LegacySideType;
use crate::util::actix::{generate_multipart, MultipartSegment, MultipartSegmentData}; use crate::util::actix::{generate_multipart, MultipartSegment, MultipartSegmentData};
use actix_multipart::Multipart; use actix_multipart::Multipart;
use actix_web::http::header::{HeaderMap, TryIntoHeaderPair}; use actix_web::http::header::{HeaderMap, TryIntoHeaderPair};
use actix_web::HttpResponse; use actix_web::HttpResponse;
use futures::{stream, StreamExt}; use futures::{stream, Future, StreamExt};
use serde_json::json; use itertools::Itertools;
use serde_json::{json, Value};
pub async fn extract_ok_json<T>(response: HttpResponse) -> Result<T, HttpResponse> pub async fn extract_ok_json<T>(response: HttpResponse) -> Result<T, HttpResponse>
where 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 multipart: Multipart,
mut headers: HeaderMap, mut headers: HeaderMap,
mut closure: impl FnMut(T) -> Result<U, CreateError>, mut closure: impl FnMut(T) -> Fut,
) -> Result<Multipart, CreateError> ) -> Result<Multipart, CreateError>
where where
T: serde::de::DeserializeOwned, T: serde::de::DeserializeOwned,
U: serde::Serialize, U: serde::Serialize,
Fut: Future<Output = Result<U, CreateError>>,
{ {
let mut segments: Vec<MultipartSegment> = Vec::new(); let mut segments: Vec<MultipartSegment> = Vec::new();
@@ -56,7 +61,7 @@ where
{ {
let json_value: T = serde_json::from_slice(&buffer)?; 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)?; buffer = serde_json::to_vec(&json_value)?;
} }
@@ -110,3 +115,353 @@ where
Ok(new_multipart) 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);
}
}

View File

@@ -511,7 +511,6 @@ pub async fn revoke_oauth_authorization(
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>, session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
println!("Inside revoke_oauth_authorization");
let current_user = get_user_from_headers( let current_user = get_user_from_headers(
&req, &req,
&**pool, &**pool,

View File

@@ -8,6 +8,7 @@ use crate::database::models::loader_fields::{
use crate::database::redis::RedisPool; use crate::database::redis::RedisPool;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use itertools::Itertools;
use serde_json::Value; use serde_json::Value;
use sqlx::PgPool; use sqlx::PgPool;
@@ -84,6 +85,7 @@ pub struct LoaderData {
pub name: String, pub name: String,
pub supported_project_types: Vec<String>, pub supported_project_types: Vec<String>,
pub supported_games: Vec<String>, pub supported_games: Vec<String>,
pub supported_fields: Vec<String>, // Available loader fields for this loader
pub metadata: Value, pub metadata: Value,
} }
@@ -91,14 +93,26 @@ pub async fn loader_list(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let mut results = Loader::list(&**pool, &redis) let loaders = Loader::list(&**pool, &redis).await?;
.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() .into_iter()
.map(|x| LoaderData { .map(|x| LoaderData {
icon: x.icon, icon: x.icon,
name: x.loader, name: x.loader,
supported_project_types: x.supported_project_types, supported_project_types: x.supported_project_types,
supported_games: x.supported_games, 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, metadata: x.metadata,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View File

@@ -3,8 +3,10 @@ use crate::models::projects::SearchRequest;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::HttpResponse; use actix_web::HttpResponse;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use itertools::Itertools;
use meilisearch_sdk::client::Client; use meilisearch_sdk::client::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::min; use std::cmp::min;
use std::collections::HashMap; use std::collections::HashMap;
@@ -177,7 +179,7 @@ pub async fn search_for_project(
query.with_filter(new_filters); query.with_filter(new_filters);
} else { } else {
let facets = if let Some(facets) = &info.facets { 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 { } else {
None None
}; };
@@ -190,14 +192,42 @@ pub async fn search_for_project(
}; };
if let Some(facets) = facets { 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('('); filter_string.push('(');
for (index, facet_list) in facets.iter().enumerate() { for (index, facet_outer_list) in facets.iter().enumerate() {
filter_string.push('('); filter_string.push('(');
for (facet_index, facet) in facet_list.iter().enumerate() { for (facet_outer_index, facet_inner_list) in facet_outer_list.iter().enumerate()
filter_string.push_str(&facet.replace(':', " = ")); {
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 ") filter_string.push_str(" OR ")
} }
} }

View File

@@ -83,8 +83,10 @@ pub fn get_public_version_creation_data_json(
// Loader fields // Loader fields
"game_versions": ["1.20.1"], "game_versions": ["1.20.1"],
"client_side": "required", "singleplayer": true,
"server_side": "optional" "client_and_server": true,
"client_only": true,
"server_only": false,
}); });
if is_modpack { if is_modpack {
j["mrpack_loaders"] = json!(["fabric"]); j["mrpack_loaders"] = json!(["fabric"]);

View File

@@ -3,10 +3,8 @@ use actix_web::{
test::{self, TestRequest}, test::{self, TestRequest},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use labrinth::routes::v3::tags::GameData; use labrinth::database::models::loader_fields::LoaderFieldEnumValue;
use labrinth::{ use labrinth::routes::v3::tags::{GameData, LoaderData};
database::models::loader_fields::LoaderFieldEnumValue, routes::v3::tags::LoaderData,
};
use crate::common::{ use crate::common::{
api_common::{ api_common::{

View File

@@ -24,7 +24,7 @@ use super::{
use super::{asserts::assert_status, database::USER_USER_ID, get_json_val_str}; 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)] #[allow(dead_code)]
pub const DUMMY_CATEGORIES: &[&str] = &[ pub const DUMMY_CATEGORIES: &[&str] = &[
@@ -340,8 +340,10 @@ pub async fn add_project_beta(api: &ApiV3) -> (CommonProject, CommonVersion) {
"version_title": "start", "version_title": "start",
"status": "unlisted", "status": "unlisted",
"dependencies": [], "dependencies": [],
"client_side": "required", "singleplayer": true,
"server_side": "optional", "client_and_server": true,
"client_only": true,
"server_only": false,
"game_versions": ["1.20.1"] , "game_versions": ["1.20.1"] ,
"release_channel": "release", "release_channel": "release",
"loaders": ["fabric"], "loaders": ["fabric"],

View File

@@ -68,7 +68,7 @@ INSERT INTO loader_field_enum_values(enum_id, value, metadata, ordering)
VALUES (2, 'Ordering_Positive100', '{"type":"release","major":false}', 100); VALUES (2, 'Ordering_Positive100', '{"type":"release","major":false}', 100);
INSERT INTO loader_fields_loaders(loader_id, loader_field_id) 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 INSERT INTO categories (id, category, project_type) VALUES
(51, 'combat', 1), (51, 'combat', 1),

View File

@@ -112,7 +112,7 @@ async fn creating_loader_fields() {
Some( Some(
serde_json::from_value(json!([{ serde_json::from_value(json!([{
"op": "remove", "op": "remove",
"path": "/client_side" "path": "/singleplayer"
}])) }]))
.unwrap(), .unwrap(),
), ),
@@ -183,7 +183,7 @@ async fn creating_loader_fields() {
json!(1), json!(1),
json!([1]), json!([1]),
json!("1.20.1"), json!("1.20.1"),
json!(["client_side"]), json!(["singleplayer"]),
] { ] {
// TODO: - Create project // TODO: - Create project
// - Create version // - Create version
@@ -271,12 +271,12 @@ async fn creating_loader_fields() {
"value": ["1.20.1", "1.20.2"] "value": ["1.20.1", "1.20.2"]
}, { }, {
"op": "add", "op": "add",
"path": "/client_side", "path": "/singleplayer",
"value": "optional" "value": false
}, { }, {
"op": "add", "op": "add",
"path": "/server_side", "path": "/server_only",
"value": "required" "value": true
}])) }]))
.unwrap(), .unwrap(),
), ),
@@ -287,16 +287,16 @@ async fn creating_loader_fields() {
v.fields.get("game_versions").unwrap(), v.fields.get("game_versions").unwrap(),
&json!(["1.20.1", "1.20.2"]) &json!(["1.20.1", "1.20.2"])
); );
assert_eq!(v.fields.get("client_side").unwrap(), &json!("optional")); assert_eq!(v.fields.get("singleplayer").unwrap(), &json!(false));
assert_eq!(v.fields.get("server_side").unwrap(), &json!("required")); assert_eq!(v.fields.get("server_only").unwrap(), &json!(true));
// - Patch // - Patch
let resp = api let resp = api
.edit_version( .edit_version(
alpha_version_id, alpha_version_id,
json!({ json!({
"game_versions": ["1.20.1", "1.20.2"], "game_versions": ["1.20.1", "1.20.2"],
"client_side": "optional", "singleplayer": false,
"server_side": "required" "server_only": true
}), }),
USER_USER_PAT, USER_USER_PAT,
) )
@@ -314,16 +314,13 @@ async fn creating_loader_fields() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn get_loader_fields() { async fn get_loader_fields_variants() {
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move { with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
let api = &test_env.api; let api = &test_env.api;
let game_versions = api let game_versions = api
.get_loader_field_variants_deserialized("game_versions") .get_loader_field_variants_deserialized("game_versions")
.await; .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 // These tests match dummy data and will need to be updated if the dummy data changes
// Versions should be ordered by: // Versions should be ordered by:
@@ -348,18 +345,64 @@ async fn get_loader_fields() {
"1.20.1" "1.20.1"
] ]
); );
let side_type_names = side_types
.into_iter()
.map(|x| x.value)
.collect::<HashSet<_>>();
assert_eq!(
side_type_names,
["unknown", "required", "optional", "unsupported"]
.iter()
.map(|s| s.to_string())
.collect()
);
}) })
.await .await
} }
#[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<ApiV3>| 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()
.collect::<HashSet<_>>();
assert_eq!(
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::<HashSet<_>>();
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;
}

View File

@@ -2,19 +2,24 @@ use actix_http::StatusCode;
use actix_web::test; use actix_web::test;
use bytes::Bytes; use bytes::Bytes;
use chrono::{Duration, Utc}; 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::database::*;
use common::dummy_data::DUMMY_CATEGORIES; 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 common::permissions::{PermissionsTest, PermissionsTestContext};
use futures::StreamExt; use futures::StreamExt;
use labrinth::database::models::project_item::{PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE}; use labrinth::database::models::project_item::{PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE};
use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::ids::base62_impl::parse_base62;
use labrinth::models::projects::ProjectId;
use labrinth::models::teams::ProjectPermissions; use labrinth::models::teams::ProjectPermissions;
use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData};
use serde_json::json; use serde_json::json;
use crate::common::api_common::{ApiProject, ApiVersion}; 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; mod common;
@@ -101,32 +106,11 @@ async fn test_get_project() {
#[actix_rt::test] #[actix_rt::test]
async fn test_add_remove_project() { async fn test_add_remove_project() {
// Test setup and dummy data // Test setup and dummy data
with_test_environment_all(None, |test_env| async move { with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
let api = &test_env.api; let api = &test_env.api;
// Generate test project data. let mut json_data =
let mut json_data = json!( get_public_project_creation_data_json("demo", Some(&TestFile::BasicMod));
{
"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"
}
);
// Basic json // Basic json
let json_segment = MultipartSegment { let json_segment = MultipartSegment {
@@ -730,48 +714,27 @@ async fn permissions_edit_details() {
#[actix_rt::test] #[actix_rt::test]
async fn permissions_upload_version() { async fn permissions_upload_version() {
with_test_environment_all(None, |test_env| async move { with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; 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_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_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 alpha_file_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash;
let upload_version = ProjectPermissions::UPLOAD_VERSION; let upload_version = ProjectPermissions::UPLOAD_VERSION;
// Upload version with basic-mod.jar // Upload version with basic-mod.jar
let req_gen = |ctx: &PermissionsTestContext| { let req_gen = |ctx: &PermissionsTestContext| {
test::TestRequest::post().uri("/v3/version").set_multipart([ let project_id = ctx.project_id.unwrap();
MultipartSegment { let project_id = ProjectId(parse_base62(project_id).unwrap());
name: "data".to_string(), let multipart = get_public_version_creation_data(
filename: None, project_id,
content_type: Some("application/json".to_string()), "1.0.0",
data: MultipartSegmentData::Text( TestFile::BasicMod,
serde_json::to_string(&json!({ None,
"project_id": ctx.project_id.unwrap(), None,
"file_parts": ["basic-mod.jar"], );
"version_number": "1.0.0", test::TestRequest::post()
"version_title": "1.0.0", .uri("/v3/version")
"version_type": "release", .set_multipart(multipart.segment_data)
"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(),
),
},
])
}; };
PermissionsTest::new(&test_env) PermissionsTest::new(&test_env)
.simple_project_permissions_test(upload_version, req_gen) .simple_project_permissions_test(upload_version, req_gen)

View File

@@ -2,9 +2,16 @@ use actix_web::test::{self, TestRequest};
use bytes::Bytes; use bytes::Bytes;
use chrono::{Duration, Utc}; 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 common::{database::*, scopes::ScopeTest};
use labrinth::models::ids::base62_impl::parse_base62;
use labrinth::models::pats::Scopes; use labrinth::models::pats::Scopes;
use labrinth::models::projects::ProjectId;
use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData};
use serde_json::json; use serde_json::json;
@@ -201,93 +208,37 @@ pub async fn notifications_scopes() {
// Project version creation scopes // Project version creation scopes
#[actix_rt::test] #[actix_rt::test]
pub async fn project_version_create_scopes() { pub async fn project_version_create_scopes() {
with_test_environment_all(None, |test_env| async move { with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
// Create project // Create project
let create_project = Scopes::PROJECT_CREATE; 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 req_gen = || {
let creation_data =
get_public_project_creation_data("demo", Some(TestFile::BasicMod), None);
test::TestRequest::post() test::TestRequest::post()
.uri("/v3/project") .uri("/v3/project")
.set_multipart(vec![json_segment.clone(), file_segment.clone()]) .set_multipart(creation_data.segment_data)
}; };
let (_, success) = ScopeTest::new(&test_env) let (_, success) = ScopeTest::new(&test_env)
.test(req_gen, create_project) .test(req_gen, create_project)
.await .await
.unwrap(); .unwrap();
let project_id = success["id"].as_str().unwrap(); let project_id = success["id"].as_str().unwrap();
let project_id = ProjectId(parse_base62(project_id).unwrap());
// Add version to project // Add version to project
let create_version = Scopes::VERSION_CREATE; 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 req_gen = || {
let creation_data = get_public_version_creation_data(
project_id,
"1.2.3.4",
TestFile::BasicModDifferent,
None,
None,
);
test::TestRequest::post() test::TestRequest::post()
.uri("/v3/version") .uri("/v3/version")
.set_multipart(vec![json_segment.clone(), file_segment.clone()]) .set_multipart(creation_data.segment_data)
}; };
ScopeTest::new(&test_env) ScopeTest::new(&test_env)
.test(req_gen, create_version) .test(req_gen, create_version)

View File

@@ -66,7 +66,7 @@ async fn search_projects() {
let id = 0; let id = 0;
let modify_json = serde_json::from_value(json!([ let modify_json = serde_json::from_value(json!([
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[4..6] }, { "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" }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" },
])) ]))
.unwrap(); .unwrap();
@@ -81,7 +81,7 @@ async fn search_projects() {
let id = 1; let id = 1;
let modify_json = serde_json::from_value(json!([ let modify_json = serde_json::from_value(json!([
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, { "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(); .unwrap();
project_creation_futures.push(create_async_future( project_creation_futures.push(create_async_future(
@@ -95,7 +95,7 @@ async fn search_projects() {
let id = 2; let id = 2;
let modify_json = serde_json::from_value(json!([ let modify_json = serde_json::from_value(json!([
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, { "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" }, { "op": "add", "path": "/title", "value": "Mysterious Project" },
])) ]))
.unwrap(); .unwrap();
@@ -110,7 +110,7 @@ async fn search_projects() {
let id = 3; let id = 3;
let modify_json = serde_json::from_value(json!([ let modify_json = serde_json::from_value(json!([
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, { "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": "/initial_versions/0/game_versions", "value": ["1.20.4"] },
{ "op": "add", "path": "/title", "value": "Mysterious Project" }, { "op": "add", "path": "/title", "value": "Mysterious Project" },
{ "op": "add", "path": "/license_id", "value": "LicenseRef-All-Rights-Reserved" }, { "op": "add", "path": "/license_id", "value": "LicenseRef-All-Rights-Reserved" },
@@ -127,7 +127,7 @@ async fn search_projects() {
let id = 4; let id = 4;
let modify_json = serde_json::from_value(json!([ let modify_json = serde_json::from_value(json!([
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, { "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"] }, { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] },
])) ]))
.unwrap(); .unwrap();
@@ -142,7 +142,7 @@ async fn search_projects() {
let id = 5; let id = 5;
let modify_json = serde_json::from_value(json!([ let modify_json = serde_json::from_value(json!([
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, { "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": "/initial_versions/0/game_versions", "value": ["1.20.5"] },
{ "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" },
])) ]))
@@ -158,8 +158,8 @@ async fn search_projects() {
let id = 6; let id = 6;
let modify_json = serde_json::from_value(json!([ let modify_json = serde_json::from_value(json!([
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, { "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/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" }, { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" },
])) ]))
.unwrap(); .unwrap();
@@ -176,8 +176,8 @@ async fn search_projects() {
let id = 7; let id = 7;
let modify_json = serde_json::from_value(json!([ let modify_json = serde_json::from_value(json!([
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, { "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/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" }, { "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/loaders", "value": ["forge"] },
{ "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.2"] }, { "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], vec![1, 2, 3, 4],
), ),
(json!([["project_types:modpack"]]), vec![4]), (json!([["project_types:modpack"]]), vec![4]),
(json!([["client_side:required"]]), vec![0, 2, 3, 7]), (json!([["client_only:true"]]), vec![0, 2, 3, 7]),
(json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]), (json!([["server_only:true"]]), vec![0, 2, 3, 6, 7]),
(json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]),
(json!([["license:MIT"]]), vec![1, 2, 4]), (json!([["license:MIT"]]), vec![1, 2, 4]),
(json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]), (json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]),

View File

@@ -1,6 +1,9 @@
use crate::common::{ use crate::common::{
api_common::ApiProject, 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}, database::{ENEMY_USER_PAT, FRIEND_USER_ID, FRIEND_USER_PAT, MOD_USER_PAT, USER_USER_PAT},
dummy_data::{TestFile, DUMMY_CATEGORIES}, dummy_data::{TestFile, DUMMY_CATEGORIES},
environment::{with_test_environment, TestEnvironment}, environment::{with_test_environment, TestEnvironment},
@@ -10,7 +13,7 @@ use actix_web::test;
use itertools::Itertools; use itertools::Itertools;
use labrinth::{ use labrinth::{
database::models::project_item::PROJECTS_SLUGS_NAMESPACE, 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}, util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData},
}; };
use serde_json::json; use serde_json::json;
@@ -63,28 +66,8 @@ async fn test_add_remove_project() {
let api = &test_env.api; let api = &test_env.api;
// Generate test project data. // Generate test project data.
let mut json_data = json!( let mut json_data =
{ get_public_project_creation_data_json("demo", Some(&TestFile::BasicMod));
"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"
}
);
// Basic json // Basic json
let json_segment = MultipartSegment { let json_segment = MultipartSegment {
@@ -251,36 +234,18 @@ async fn permissions_upload_version() {
// Upload version with basic-mod.jar // Upload version with basic-mod.jar
let req_gen = |ctx: &PermissionsTestContext| { let req_gen = |ctx: &PermissionsTestContext| {
test::TestRequest::post().uri("/v2/version").set_multipart([ let project_id = ctx.project_id.unwrap();
MultipartSegment { let project_id = ProjectId(parse_base62(project_id).unwrap());
name: "data".to_string(), let multipart = get_public_version_creation_data(
filename: None, project_id,
content_type: Some("application/json".to_string()), "1.0.0",
data: MultipartSegmentData::Text( TestFile::BasicMod,
serde_json::to_string(&json!({ None,
"project_id": ctx.project_id.unwrap(), None,
"file_parts": ["basic-mod.jar"], );
"version_number": "1.0.0", test::TestRequest::post()
"version_title": "1.0.0", .uri("/v2/version")
"version_type": "release", .set_multipart(multipart.segment_data)
"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(),
),
},
])
}; };
PermissionsTest::new(&test_env) PermissionsTest::new(&test_env)
.simple_project_permissions_test(upload_version, req_gen) .simple_project_permissions_test(upload_version, req_gen)
@@ -491,7 +456,7 @@ pub async fn test_patch_project() {
"issues_url": "https://github.com", "issues_url": "https://github.com",
"discord_url": "https://discord.gg", "discord_url": "https://discord.gg",
"wiki_url": "https://wiki.com", "wiki_url": "https://wiki.com",
"client_side": "optional", "client_side": "unsupported",
"server_side": "required", "server_side": "required",
"donation_urls": [{ "donation_urls": [{
"id": "patreon", "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.issues_url, Some("https://github.com".to_string()));
assert_eq!(project.discord_url, Some("https://discord.gg".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.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.server_side.as_str(), "required");
assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com");
}) })

View File

@@ -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::api_v2::ApiV2;
use crate::common::dummy_data::TestFile;
use crate::common::environment::with_test_environment; use crate::common::environment::with_test_environment;
use crate::common::environment::TestEnvironment; use crate::common::environment::TestEnvironment;
use crate::common::scopes::ScopeTest; use crate::common::scopes::ScopeTest;
use actix_web::test; use actix_web::test;
use labrinth::models::ids::base62_impl::parse_base62;
use labrinth::models::pats::Scopes; use labrinth::models::pats::Scopes;
use labrinth::models::projects::ProjectId;
use labrinth::util::actix::AppendsMultipart; use labrinth::util::actix::AppendsMultipart;
use labrinth::util::actix::MultipartSegment;
use labrinth::util::actix::MultipartSegmentData;
use serde_json::json;
// Project version creation scopes // Project version creation scopes
#[actix_rt::test] #[actix_rt::test]
pub async fn project_version_create_scopes() { pub async fn project_version_create_scopes() {
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move { with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
// Create project // Create project
let create_project = Scopes::PROJECT_CREATE; 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 req_gen = || {
let creation_data =
get_public_project_creation_data("demo", Some(TestFile::BasicMod), None);
test::TestRequest::post() test::TestRequest::post()
.uri("/v3/project") .uri("/v2/project")
.set_multipart(vec![json_segment.clone(), file_segment.clone()]) .set_multipart(creation_data.segment_data)
}; };
let (_, success) = ScopeTest::new(&test_env) let (_, success) = ScopeTest::new(&test_env)
.test(req_gen, create_project) .test(req_gen, create_project)
.await .await
.unwrap(); .unwrap();
let project_id = success["id"].as_str().unwrap(); let project_id = success["id"].as_str().unwrap();
let project_id = ProjectId(parse_base62(project_id).unwrap());
// Add version to project // Add version to project
let create_version = Scopes::VERSION_CREATE; 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 req_gen = || {
let creation_data = get_public_version_creation_data(
project_id,
"1.2.3.4",
TestFile::BasicModDifferent,
None,
None,
);
test::TestRequest::post() test::TestRequest::post()
.uri("/v3/version") .uri("/v2/version")
.set_multipart(vec![json_segment.clone(), file_segment.clone()]) .set_multipart(creation_data.segment_data)
}; };
ScopeTest::new(&test_env) ScopeTest::new(&test_env)
.test(req_gen, create_version) .test(req_gen, create_version)

View File

@@ -214,6 +214,15 @@ async fn search_projects() {
// 1. vec of search facets // 1. vec of search facets
// 2. expected project ids to be returned by this search // 2. expected project ids to be returned by this search
let pairs = vec![ 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:fabric"]]), vec![0, 1, 2, 3, 4, 5, 6, 7]),
(json!([["categories:forge"]]), vec![7]), (json!([["categories:forge"]]), vec![7]),
( (
@@ -229,7 +238,9 @@ async fn search_projects() {
vec![1, 2, 3, 4], vec![1, 2, 3, 4],
), ),
(json!([["project_types:modpack"]]), vec![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!([["server_side:required"]]), vec![0, 2, 3, 6, 7]),
(json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]),
(json!([["license:MIT"]]), vec![1, 2, 4]), (json!([["license:MIT"]]), vec![1, 2, 4]),

View File

@@ -428,9 +428,9 @@ async fn add_version_project_types_v2() {
.get_project_deserialized(&test_project.slug.unwrap(), USER_USER_PAT) .get_project_deserialized(&test_project.slug.unwrap(), USER_USER_PAT)
.await; .await;
assert_eq!(test_project.project_type, "unknown"); // No project_type set, as no versions are set 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 is a known difference between older v2 ,but is acceptable.
// This would be the appropriate test on older v2: // This would be the appropriate test on older v2:
// assert_eq!(test_project.project_type, "modpack"); // assert_eq!(test_project.project_type, "modpack");
// Create a version with a modpack file attached // Create a version with a modpack file attached
let test_version = api let test_version = api