diff --git a/.sqlx/query-594ead968747529638ce41ebd3f7f5433df9b9d86a2a1a695933feb94b093d5d.json b/.sqlx/query-594ead968747529638ce41ebd3f7f5433df9b9d86a2a1a695933feb94b093d5d.json new file mode 100644 index 000000000..53f55bd32 --- /dev/null +++ b/.sqlx/query-594ead968747529638ce41ebd3f7f5433df9b9d86a2a1a695933feb94b093d5d.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT v.id id, m.id mod_id, COALESCE(u.username, ou.username) owner_username\n FROM versions v\n INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2)\n LEFT JOIN team_members tm ON tm.team_id = m.team_id AND tm.is_owner = TRUE AND tm.accepted = TRUE\n LEFT JOIN users u ON tm.user_id = u.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN team_members otm ON otm.team_id = o.team_id AND otm.is_owner = TRUE AND otm.accepted = TRUE\n LEFT JOIN users ou ON otm.user_id = ou.id\n WHERE v.status != ANY($1)\n GROUP BY v.id, m.id, u.username, ou.username\n ORDER BY m.id DESC;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "owner_username", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "TextArray", + "TextArray" + ] + }, + "nullable": [ + false, + false, + null + ] + }, + "hash": "594ead968747529638ce41ebd3f7f5433df9b9d86a2a1a695933feb94b093d5d" +} diff --git a/.sqlx/query-794b781594db938d7e0e53f957ee614066bd7f7b3f653f186f1262d448ef89a1.json b/.sqlx/query-794b781594db938d7e0e53f957ee614066bd7f7b3f653f186f1262d448ef89a1.json deleted file mode 100644 index 6e8e6d3a2..000000000 --- a/.sqlx/query-794b781594db938d7e0e53f957ee614066bd7f7b3f653f186f1262d448ef89a1.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT v.id id, m.id mod_id, u.username owner_username\n \n FROM versions v\n INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2)\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.is_owner = TRUE AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n WHERE v.status != ANY($1)\n GROUP BY v.id, m.id, u.id\n ORDER BY m.id DESC;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "owner_username", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "TextArray", - "TextArray" - ] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "794b781594db938d7e0e53f957ee614066bd7f7b3f653f186f1262d448ef89a1" -} diff --git a/src/models/v2/projects.rs b/src/models/v2/projects.rs index 382d513fa..881f81328 100644 --- a/src/models/v2/projects.rs +++ b/src/models/v2/projects.rs @@ -70,6 +70,33 @@ pub struct LegacyProject { } impl LegacyProject { + // Returns visible v2 project_type and also 'og' selected project type + // These are often identical, but we want to display 'mod' for datapacks and plugins + // The latter can be used for further processing, such as determining side types of plugins + pub fn get_project_type(project_types: &[String]) -> (String, String) { + // V2 versions only have one project type- v3 versions can rarely have multiple. + // We'll prioritize 'modpack' first, and if neither are found, use the first one. + // If there are no project types, default to 'project' + let mut project_types = project_types.to_vec(); + if project_types.contains(&"modpack".to_string()) { + project_types = vec!["modpack".to_string()]; + } + + let og_project_type = project_types + .first() + .cloned() + .unwrap_or("project".to_string()); // Default to 'project' if none are found + + let project_type = if og_project_type == "datapack" || og_project_type == "plugin" { + // These are not supported in V2, so we'll just use 'mod' instead + "mod".to_string() + } else { + og_project_type.clone() + }; + + (project_type, og_project_type) + } + // Convert from a standard V3 project to a V2 project // Requires any queried versions to be passed in, to get access to certain version fields contained within. // - This can be any version, because the fields are ones that used to be on the project itself. @@ -83,22 +110,8 @@ impl LegacyProject { // V2 versions only have one project type- v3 versions can rarely have multiple. // We'll prioritize 'modpack' first, and if neither are found, use the first one. // If there are no project types, default to 'project' - let mut project_types = data.project_types; - if project_types.contains(&"modpack".to_string()) { - project_types = vec!["modpack".to_string()]; - } - - let og_project_type = project_types - .first() - .cloned() - .unwrap_or("project".to_string()); // Default to 'project' if none are found - - let mut project_type = if og_project_type == "datapack" || og_project_type == "plugin" { - // These are not supported in V2, so we'll just use 'mod' instead - "mod".to_string() - } else { - og_project_type.clone() - }; + let project_types = data.project_types; + let (mut project_type, og_project_type) = Self::get_project_type(&project_types); let mut loaders = data.loaders; diff --git a/src/models/v2/search.rs b/src/models/v2/search.rs index db169eea6..6d8b3f8a9 100644 --- a/src/models/v2/search.rs +++ b/src/models/v2/search.rs @@ -156,14 +156,16 @@ impl LegacyResultSearchProject { impl LegacySearchResults { pub fn from(search_results: crate::search::SearchResults) -> Self { + let limit = search_results.hits_per_page; + let offset = (search_results.page - 1) * limit; Self { hits: search_results .hits .into_iter() .map(LegacyResultSearchProject::from) .collect(), - offset: search_results.offset, - limit: search_results.limit, + offset, + limit, total_hits: search_results.total_hits, } } diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index f6c2f41c9..f22102ce5 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -78,8 +78,10 @@ pub async fn project_search( }) .collect_vec(); - // We will now convert side_types to their new boolean format - let facets = v2_reroute::convert_side_type_facets_v3(facets); + // These loaders speciically used to be combined with 'mod' to be a plugin, but now + // they are their own loader type. We will convert 'mod' to 'mod' OR 'plugin' + // as it essentially was before. + let facets = v2_reroute::convert_plugin_loaders_v3(facets); Some( facets diff --git a/src/routes/v2_reroute.rs b/src/routes/v2_reroute.rs index b44c8a63a..39c2dcb44 100644 --- a/src/routes/v2_reroute.rs +++ b/src/routes/v2_reroute.rs @@ -8,7 +8,6 @@ use actix_multipart::Multipart; use actix_web::http::header::{HeaderMap, TryIntoHeaderPair}; use actix_web::HttpResponse; use futures::{stream, Future, StreamExt}; -use itertools::Itertools; use serde_json::{json, Value}; pub async fn extract_ok_json(response: HttpResponse) -> Result @@ -152,90 +151,24 @@ pub fn convert_side_types_v3( 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()); +// Converts plugin loaders from v2 to v3 +// Within every 1st and 2nd level (the ones allowed in v2), we convert every instance of: +// "project_type:mod" to "project_type:plugin" OR "project_type:mod" +pub fn convert_plugin_loaders_v3(facets: Vec>>) -> Vec>> { + facets + .into_iter() + .map(|inner_facets| { + if inner_facets == [["project_type:mod"]] { + vec![ + vec!["project_type:plugin".to_string()], + vec!["project_type:datapack".to_string()], + vec!["project_type:mod".to_string()], + ] + } else { + inner_facets } - new_inner_facets.extend(new_possibilities); - } - v3_facets.push(new_inner_facets); - } - v3_facets + }) + .collect::>() } // Convert search facets from V3 back to v2 @@ -347,169 +280,4 @@ mod tests { } } } - - #[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/projects.rs b/src/routes/v3/projects.rs index a7ab61fed..c4f82ca77 100644 --- a/src/routes/v3/projects.rs +++ b/src/routes/v3/projects.rs @@ -896,8 +896,8 @@ pub async fn edit_project_categories( #[derive(Serialize, Deserialize)] pub struct ReturnSearchResults { pub hits: Vec, - pub offset: usize, - pub limit: usize, + pub page: usize, + pub hits_per_page: usize, pub total_hits: usize, } @@ -913,8 +913,8 @@ pub async fn project_search( .into_iter() .filter_map(Project::from_search) .collect::>(), - offset: results.offset, - limit: results.limit, + page: results.page, + hits_per_page: results.hits_per_page, total_hits: results.total_hits, }; diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index d8bdd0d84..7508762b7 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -6,22 +6,29 @@ use super::IndexingError; use crate::database::models::{project_item, version_item, ProjectId, VersionId}; use crate::database::redis::RedisPool; use crate::models; +use crate::models::v2::projects::LegacyProject; +use crate::routes::v2_reroute; use crate::search::UploadSearchProject; use sqlx::postgres::PgPool; pub async fn get_all_ids( pool: PgPool, ) -> Result, IndexingError> { + // TODO: Currently org owner is set to be considered owner. It may be worth considering + // adding a new facetable 'organization' field to the search index, and using that instead, + // and making owner to be optional. let all_visible_ids: Vec<(VersionId, ProjectId, String)> = sqlx::query!( " - SELECT v.id id, m.id mod_id, u.username owner_username - + SELECT v.id id, m.id mod_id, COALESCE(u.username, ou.username) owner_username FROM versions v INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2) - INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.is_owner = TRUE AND tm.accepted = TRUE - INNER JOIN users u ON tm.user_id = u.id + LEFT JOIN team_members tm ON tm.team_id = m.team_id AND tm.is_owner = TRUE AND tm.accepted = TRUE + LEFT JOIN users u ON tm.user_id = u.id + LEFT JOIN organizations o ON o.id = m.organization_id + LEFT JOIN team_members otm ON otm.team_id = o.team_id AND otm.is_owner = TRUE AND otm.accepted = TRUE + LEFT JOIN users ou ON otm.user_id = ou.id WHERE v.status != ANY($1) - GROUP BY v.id, m.id, u.id + GROUP BY v.id, m.id, u.username, ou.username ORDER BY m.id DESC; ", &*crate::models::projects::VersionStatus::iterator() @@ -38,7 +45,8 @@ pub async fn get_all_ids( Ok(e.right().map(|m| { let project_id: ProjectId = ProjectId(m.mod_id); let version_id: VersionId = VersionId(m.id); - (version_id, project_id, m.owner_username) + let owner_username = m.owner_username.unwrap_or_default(); + (version_id, project_id, owner_username) })) }) .try_collect::>() @@ -114,7 +122,12 @@ pub async fn index_local( categories.append(&mut additional_categories); let version_fields = v.version_fields.clone(); - let loader_fields = models::projects::from_duplicate_version_fields(version_fields); + let unvectorized_loader_fields = v + .version_fields + .iter() + .map(|vf| (vf.field_name.clone(), vf.value.serialize_internal())) + .collect(); + let mut loader_fields = models::projects::from_duplicate_version_fields(version_fields); let license = match m.inner.license.split(' ').next() { Some(license) => license.to_string(), None => m.inner.license.clone(), @@ -158,6 +171,24 @@ pub async fn index_local( categories.retain(|x| *x != "mrpack"); } + // SPECIAL BEHAVIOUR: + // For consitency with v2 searching, we manually input the + // client_side and server_side fields from the loader fields into + // separate loader fields. + // 'client_side' and 'server_side' remain supported by meilisearch even though they are no longer v3 fields. + let (_, v2_og_project_type) = LegacyProject::get_project_type(&v.project_types); + let (client_side, server_side) = v2_reroute::convert_side_types_v2( + &unvectorized_loader_fields, + Some(&v2_og_project_type), + ); + + if let Ok(client_side) = serde_json::to_value(client_side) { + loader_fields.insert("client_side".to_string(), vec![client_side]); + } + if let Ok(server_side) = serde_json::to_value(server_side) { + loader_fields.insert("server_side".to_string(), vec![server_side]); + } + let gallery = m .gallery_items .iter() diff --git a/src/search/indexing/mod.rs b/src/search/indexing/mod.rs index c12a7d1c0..bf6a801d3 100644 --- a/src/search/indexing/mod.rs +++ b/src/search/indexing/mod.rs @@ -382,6 +382,9 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ "singleplayer", "client_and_server", "mrpack_loaders", + // V2 legacy fields for logical consistency + "client_side", + "server_side", // Non-searchable fields for filling out the Project model. "license_url", "monetization_status", @@ -424,6 +427,9 @@ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[ "singleplayer", "client_and_server", "mrpack_loaders", + // V2 legacy fields for logical consistency + "client_side", + "server_side", ]; const DEFAULT_SORTABLE_ATTRIBUTES: &[&str] = diff --git a/src/search/mod.rs b/src/search/mod.rs index fc01b70f5..928b69778 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -142,8 +142,8 @@ pub struct UploadSearchProject { #[derive(Serialize, Deserialize, Debug)] pub struct SearchResults { pub hits: Vec, - pub offset: usize, - pub limit: usize, + pub page: usize, + pub hits_per_page: usize, pub total_hits: usize, } @@ -212,7 +212,7 @@ pub async fn search_for_project( ) -> Result { let client = Client::new(&*config.address, Some(&*config.key)); - let offset = info.offset.as_deref().unwrap_or("0").parse()?; + let offset: usize = info.offset.as_deref().unwrap_or("0").parse()?; let index = info.index.as_deref().unwrap_or("relevance"); let limit = info.limit.as_deref().unwrap_or("10").parse()?; @@ -221,12 +221,15 @@ pub async fn search_for_project( let mut filter_string = String::new(); + // Convert offset and limit to page and hits_per_page + let hits_per_page = limit; + let page = offset / limit + 1; + let results = { let mut query = meilisearch_index.search(); - query - .with_limit(min(100, limit)) - .with_offset(offset) + .with_page(page) + .with_hits_per_page(hits_per_page) .with_query(info.query.as_deref().unwrap_or_default()) .with_sort(&sort.1); @@ -312,8 +315,8 @@ pub async fn search_for_project( Ok(SearchResults { hits: results.hits.into_iter().map(|r| r.result).collect(), - offset: results.offset.unwrap_or_default(), - limit: results.limit.unwrap_or_default(), - total_hits: results.estimated_total_hits.unwrap_or_default(), + page: results.page.unwrap_or_default(), + hits_per_page: results.hits_per_page.unwrap_or_default(), + total_hits: results.total_hits.unwrap_or_default(), }) } diff --git a/tests/common/search.rs b/tests/common/search.rs index 582a7dede..ee8730219 100644 --- a/tests/common/search.rs +++ b/tests/common/search.rs @@ -20,6 +20,7 @@ pub async fn setup_search_projects(test_env: &TestEnvironment) -> Arc) -> Arc test id diff --git a/tests/search.rs b/tests/search.rs index f8f95a62e..d607404ec 100644 --- a/tests/search.rs +++ b/tests/search.rs @@ -27,11 +27,14 @@ async fn search_projects() { // 1. vec of search facets // 2. expected project ids to be returned by this search let pairs = 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, 9], + ), (json!([["categories:forge"]]), vec![7]), ( json!([["categories:fabric", "categories:forge"]]), - vec![0, 1, 2, 3, 4, 5, 6, 7], + vec![0, 1, 2, 3, 4, 5, 6, 7, 9], ), (json!([["categories:fabric"], ["categories:forge"]]), vec![]), ( @@ -42,12 +45,12 @@ async fn search_projects() { vec![1, 2, 3, 4], ), (json!([["project_types:modpack"]]), vec![4]), - (json!([["client_only:true"]]), vec![0, 2, 3, 7]), + (json!([["client_only:true"]]), vec![0, 2, 3, 7, 9]), (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!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7, 9]), + (json!([["license:MIT"]]), vec![1, 2, 4, 9]), (json!([[r#"name:'Mysterious Project'"#]]), vec![2, 3]), - (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7]), + (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7, 9]), // Organization test '9' is included here as user is owner of org (json!([["game_versions:1.20.5"]]), vec![4, 5]), // bug fix ( @@ -98,10 +101,12 @@ async fn search_projects() { .into_iter() .map(|p| id_conversion[&p.id.0]) .collect(); + let num_hits = projects.total_hits; expected_project_ids.sort(); found_project_ids.sort(); println!("Facets: {:?}", facets); assert_eq!(found_project_ids, expected_project_ids); + assert_eq!(num_hits, expected_project_ids.len() as usize); } }) .await; diff --git a/tests/v2/search.rs b/tests/v2/search.rs index 6e9b594a0..61b36aef5 100644 --- a/tests/v2/search.rs +++ b/tests/v2/search.rs @@ -189,6 +189,20 @@ async fn search_projects() { Some(modify_json), )); + // Test project 8 + // Server side unsupported + let id = 8; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/server_side", "value": "unsupported" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); + // Await all project creation // Returns a mapping of: // project id -> test id @@ -226,11 +240,14 @@ async fn search_projects() { ]), 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, 8], + ), (json!([["categories:forge"]]), vec![7]), ( json!([["categories:fabric", "categories:forge"]]), - vec![0, 1, 2, 3, 4, 5, 6, 7], + vec![0, 1, 2, 3, 4, 5, 6, 7, 8], ), (json!([["categories:fabric"], ["categories:forge"]]), vec![]), ( @@ -243,12 +260,12 @@ async fn search_projects() { (json!([["project_types:modpack"]]), vec![4]), // 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!([["client_side:required"]]), vec![0, 2, 3, 8]), (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]), + (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7, 8]), + (json!([["license:MIT"]]), vec![1, 2, 4, 8]), (json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]), - (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7]), + (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7, 8]), (json!([["versions:1.20.5"]]), vec![4, 5]), // bug fix ( @@ -275,6 +292,12 @@ async fn search_projects() { ]), vec![4], ), + ( + json!([["client_side:optional"], ["server_side:optional"]]), + vec![1, 4, 5], + ), + (json!([["server_side:optional"]]), vec![1, 4, 5]), + (json!([["server_side:unsupported"]]), vec![8]), ]; // TODO: Untested: @@ -313,7 +336,7 @@ async fn search_projects() { }) .await; - // A couple additional tests for the saerch type returned, making sure it is properly translated back + // A couple additional tests for the search type returned, making sure it is properly translated back let client_side_required = api .search_deserialized( Some(&format!("\"&{test_name}\"")), @@ -347,6 +370,18 @@ async fn search_projects() { assert_eq!(hit.client_side, "unsupported".to_string()); } + let client_side_optional_server_side_optional = api + .search_deserialized( + Some(&format!("\"&{test_name}\"")), + Some(json!([["client_side:optional"], ["server_side:optional"]])), + USER_USER_PAT, + ) + .await; + for hit in client_side_optional_server_side_optional.hits { + assert_eq!(hit.client_side, "optional".to_string()); + assert_eq!(hit.server_side, "optional".to_string()); + } + let game_versions = api .search_deserialized( Some(&format!("\"&{test_name}\"")),