From 62e1268e0e85a06192e749b0a2a7406a73d1acc1 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Mon, 16 Feb 2026 19:16:03 -0500 Subject: [PATCH 1/9] Add support for dependencies in search --- .../src/search/indexing/local_import.rs | 17 +++++++++++++++++ apps/labrinth/src/search/indexing/mod.rs | 2 ++ apps/labrinth/src/search/mod.rs | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/apps/labrinth/src/search/indexing/local_import.rs b/apps/labrinth/src/search/indexing/local_import.rs index 6c2549fef1..958c0eb293 100644 --- a/apps/labrinth/src/search/indexing/local_import.rs +++ b/apps/labrinth/src/search/indexing/local_import.rs @@ -414,6 +414,7 @@ pub async fn index_local( featured_gallery: featured_gallery.clone(), open_source, color: project.color.map(|x| x as u32), + dependencies: version.dependencies, loader_fields, project_loader_fields: project_loader_fields.clone(), // 'loaders' is aggregate of all versions' loaders @@ -433,6 +434,7 @@ struct PartialVersion { loaders: Vec, project_types: Vec, version_fields: Vec, + dependencies: Vec, } async fn index_versions( @@ -549,6 +551,20 @@ async fn index_versions( .map(|(_, version_fields)| version_fields) .unwrap_or_default(); + let dependencies: Vec = sqlx::query!( + " + SELECT mod_dependency_id as \"mod_dependency_id: DBProjectId\" + FROM dependencies + WHERE dependent_id = $1", + version_id.0 + ) + .fetch_all(pool) + .await? + .iter() + .flat_map(|r| r.mod_dependency_id) + .map(|id| crate::models::ids::ProjectId::from(id).to_string()) + .collect(); + res_versions .entry(*project_id) .or_default() @@ -557,6 +573,7 @@ async fn index_versions( loaders: version_loader_data.loaders, project_types: version_loader_data.project_types, version_fields, + dependencies, }); } } diff --git a/apps/labrinth/src/search/indexing/mod.rs b/apps/labrinth/src/search/indexing/mod.rs index 2da327ce14..557f2d1fcd 100644 --- a/apps/labrinth/src/search/indexing/mod.rs +++ b/apps/labrinth/src/search/indexing/mod.rs @@ -565,6 +565,7 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ "gallery", "featured_gallery", "color", + "dependencies", // Note: loader fields are not here, but are added on as they are needed (so they can be dynamically added depending on which exist). // TODO: remove these- as they should be automatically populated. This is a band-aid fix. "environment", @@ -609,6 +610,7 @@ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[ "project_id", "open_source", "color", + "dependencies", // Note: loader fields are not here, but are added on as they are needed (so they can be dynamically added depending on which exist). // TODO: remove these- as they should be automatically populated. This is a band-aid fix. "environment", diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index e64aa07071..7e0f5d3ff9 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -216,6 +216,8 @@ pub struct UploadSearchProject { pub open_source: bool, pub color: Option, + pub dependencies: Vec, + // Hidden fields to get the Project model out of the search results. pub loaders: Vec, // Search uses loaders as categories- this is purely for the Project model. pub project_loader_fields: HashMap>, // Aggregation of loader_fields from all versions of the project, allowing for reconstruction of the Project model. @@ -255,6 +257,8 @@ pub struct ResultSearchProject { pub featured_gallery: Option, pub color: Option, + pub dependencies: Vec, + // Hidden fields to get the Project model out of the search results. pub loaders: Vec, // Search uses loaders as categories- this is purely for the Project model. pub project_loader_fields: HashMap>, // Aggregation of loader_fields from all versions of the project, allowing for reconstruction of the Project model. From c14016bc850156efdc7175d947aeaf9c55be87b1 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Mon, 16 Feb 2026 20:28:15 -0500 Subject: [PATCH 2/9] Add dependency slug to project search index --- .../src/search/indexing/local_import.rs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/labrinth/src/search/indexing/local_import.rs b/apps/labrinth/src/search/indexing/local_import.rs index 958c0eb293..f2712b71d3 100644 --- a/apps/labrinth/src/search/indexing/local_import.rs +++ b/apps/labrinth/src/search/indexing/local_import.rs @@ -553,17 +553,28 @@ async fn index_versions( let dependencies: Vec = sqlx::query!( " - SELECT mod_dependency_id as \"mod_dependency_id: DBProjectId\" - FROM dependencies + SELECT d.mod_dependency_id as \"mod_dependency_id: DBProjectId\", m.slug as \"slug\" FROM dependencies d + INNER JOIN mods m ON m.id = d.mod_dependency_id WHERE dependent_id = $1", version_id.0 ) - .fetch_all(pool) - .await? - .iter() - .flat_map(|r| r.mod_dependency_id) - .map(|id| crate::models::ids::ProjectId::from(id).to_string()) - .collect(); + .fetch_all(pool) + .await? + .iter() + .flat_map(|r| { + let mut v = Vec::new(); + + if let Some(id) = r.mod_dependency_id { + v.push(crate::models::ids::ProjectId::from(id).to_string()) + } + + if let Some(slug) = &r.slug { + v.push(slug.to_string()) + } + + v + }) + .collect(); res_versions .entry(*project_id) From e604d7e884498bfcd01834a836e4a24f5aabf9db Mon Sep 17 00:00:00 2001 From: IThundxr Date: Mon, 16 Feb 2026 22:02:27 -0500 Subject: [PATCH 3/9] Run sqlx prepare --- ...f4eeff66ab4165a9f4980032e114db4dc1286.json | 26 ----------------- ...e789420301ac35f23276229f39a8de8e8a558.json | 28 +++++++++++++++++++ ...d2402f52fea71e27b08e7926fcc2a9e62c0f3.json | 20 ------------- ...afedb074492b4ec7f2457c14113f5fd13aa02.json | 18 ------------ ...e5c93783c7641b019fdb698a1ec0be1393606.json | 17 ----------- 5 files changed, 28 insertions(+), 81 deletions(-) delete mode 100644 apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json create mode 100644 apps/labrinth/.sqlx/query-678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558.json delete mode 100644 apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json delete mode 100644 apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json delete mode 100644 apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json diff --git a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json b/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json deleted file mode 100644 index 921f7f92d9..0000000000 --- a/apps/labrinth/.sqlx/query-1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n id,\n status AS \"status: PayoutStatus\"\n FROM payouts\n ORDER BY id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "status: PayoutStatus", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false - ] - }, - "hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286" -} diff --git a/apps/labrinth/.sqlx/query-678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558.json b/apps/labrinth/.sqlx/query-678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558.json new file mode 100644 index 0000000000..bd0481fc9b --- /dev/null +++ b/apps/labrinth/.sqlx/query-678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT d.mod_dependency_id as \"mod_dependency_id: DBProjectId\", m.slug as \"slug\" FROM dependencies d\n INNER JOIN mods m ON m.id = d.mod_dependency_id\n WHERE dependent_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_dependency_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "slug", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + true, + true + ] + }, + "hash": "678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558" +} diff --git a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json b/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json deleted file mode 100644 index 89bd8147dc..0000000000 --- a/apps/labrinth/.sqlx/query-b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "status: PayoutStatus", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3" -} diff --git a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json b/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json deleted file mode 100644 index 469c30168a..0000000000 --- a/apps/labrinth/.sqlx/query-cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text", - "Text", - "Varchar", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02" -} diff --git a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json b/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json deleted file mode 100644 index 52e020ebf2..0000000000 --- a/apps/labrinth/.sqlx/query-cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text", - "Varchar", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606" -} From 4d42a2eb41347edbbed2e2d95c6bde8bc07e16c7 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Thu, 19 Feb 2026 18:30:17 -0500 Subject: [PATCH 4/9] Switch dependencies to use a map --- .../src/search/indexing/local_import.rs | 31 +++++++++---------- apps/labrinth/src/search/mod.rs | 4 +-- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/apps/labrinth/src/search/indexing/local_import.rs b/apps/labrinth/src/search/indexing/local_import.rs index f2712b71d3..dc64fbeafa 100644 --- a/apps/labrinth/src/search/indexing/local_import.rs +++ b/apps/labrinth/src/search/indexing/local_import.rs @@ -434,7 +434,7 @@ struct PartialVersion { loaders: Vec, project_types: Vec, version_fields: Vec, - dependencies: Vec, + dependencies: HashMap>, } async fn index_versions( @@ -551,30 +551,29 @@ async fn index_versions( .map(|(_, version_fields)| version_fields) .unwrap_or_default(); - let dependencies: Vec = sqlx::query!( + let mut dependencies: HashMap> = HashMap::new(); + + let records = sqlx::query!( " - SELECT d.mod_dependency_id as \"mod_dependency_id: DBProjectId\", m.slug as \"slug\" FROM dependencies d + SELECT d.mod_dependency_id as \"mod_dependency_id: DBProjectId\", d.dependency_type, m.slug as \"slug\" FROM dependencies d INNER JOIN mods m ON m.id = d.mod_dependency_id WHERE dependent_id = $1", version_id.0 ) .fetch_all(pool) - .await? - .iter() - .flat_map(|r| { - let mut v = Vec::new(); + .await?; - if let Some(id) = r.mod_dependency_id { - v.push(crate::models::ids::ProjectId::from(id).to_string()) - } + for r in records { + let v = dependencies.entry(r.dependency_type.clone()).or_default(); - if let Some(slug) = &r.slug { - v.push(slug.to_string()) - } + if let Some(id) = r.mod_dependency_id { + v.push(crate::models::ids::ProjectId::from(id).to_string()) + } - v - }) - .collect(); + if let Some(slug) = &r.slug { + v.push(slug.to_string()) + } + } res_versions .entry(*project_id) diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index 7e0f5d3ff9..bf923ea76e 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -216,7 +216,7 @@ pub struct UploadSearchProject { pub open_source: bool, pub color: Option, - pub dependencies: Vec, + pub dependencies: HashMap>, // Hidden fields to get the Project model out of the search results. pub loaders: Vec, // Search uses loaders as categories- this is purely for the Project model. @@ -257,7 +257,7 @@ pub struct ResultSearchProject { pub featured_gallery: Option, pub color: Option, - pub dependencies: Vec, + pub dependencies: HashMap>, // Hidden fields to get the Project model out of the search results. pub loaders: Vec, // Search uses loaders as categories- this is purely for the Project model. From 9c7514344500ecd38d3ccffeb95227ec529fe14c Mon Sep 17 00:00:00 2001 From: IThundxr Date: Thu, 19 Feb 2026 19:01:19 -0500 Subject: [PATCH 5/9] Use separate vecs --- .../src/search/indexing/local_import.rs | 23 +++++++++++++++---- apps/labrinth/src/search/indexing/mod.rs | 8 +++++-- apps/labrinth/src/search/mod.rs | 8 +++++-- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/labrinth/src/search/indexing/local_import.rs b/apps/labrinth/src/search/indexing/local_import.rs index dc64fbeafa..2bff516cd3 100644 --- a/apps/labrinth/src/search/indexing/local_import.rs +++ b/apps/labrinth/src/search/indexing/local_import.rs @@ -414,7 +414,9 @@ pub async fn index_local( featured_gallery: featured_gallery.clone(), open_source, color: project.color.map(|x| x as u32), - dependencies: version.dependencies, + required_dependencies: version.required_dependencies, + optional_dependencies: version.optional_dependencies, + incompatibilities: version.incompatibilities, loader_fields, project_loader_fields: project_loader_fields.clone(), // 'loaders' is aggregate of all versions' loaders @@ -434,7 +436,9 @@ struct PartialVersion { loaders: Vec, project_types: Vec, version_fields: Vec, - dependencies: HashMap>, + pub required_dependencies: Vec, + pub optional_dependencies: Vec, + pub incompatibilities: Vec, } async fn index_versions( @@ -551,7 +555,9 @@ async fn index_versions( .map(|(_, version_fields)| version_fields) .unwrap_or_default(); - let mut dependencies: HashMap> = HashMap::new(); + let mut required_dependencies: Vec = Vec::new(); + let mut optional_dependencies: Vec = Vec::new(); + let mut incompatibilities: Vec = Vec::new(); let records = sqlx::query!( " @@ -564,7 +570,12 @@ async fn index_versions( .await?; for r in records { - let v = dependencies.entry(r.dependency_type.clone()).or_default(); + let v = match r.dependency_type.as_str() { + "required" => &mut required_dependencies, + "optional" => &mut optional_dependencies, + "incompatible" => &mut incompatibilities, + _ => continue, + }; if let Some(id) = r.mod_dependency_id { v.push(crate::models::ids::ProjectId::from(id).to_string()) @@ -583,7 +594,9 @@ async fn index_versions( loaders: version_loader_data.loaders, project_types: version_loader_data.project_types, version_fields, - dependencies, + required_dependencies, + optional_dependencies, + incompatibilities, }); } } diff --git a/apps/labrinth/src/search/indexing/mod.rs b/apps/labrinth/src/search/indexing/mod.rs index 557f2d1fcd..ddf4cd16a5 100644 --- a/apps/labrinth/src/search/indexing/mod.rs +++ b/apps/labrinth/src/search/indexing/mod.rs @@ -565,7 +565,9 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ "gallery", "featured_gallery", "color", - "dependencies", + "required_dependencies", + "optional_dependencies", + "incompatibilities", // Note: loader fields are not here, but are added on as they are needed (so they can be dynamically added depending on which exist). // TODO: remove these- as they should be automatically populated. This is a band-aid fix. "environment", @@ -610,7 +612,9 @@ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[ "project_id", "open_source", "color", - "dependencies", + "required_dependencies", + "optional_dependencies", + "incompatibilities", // Note: loader fields are not here, but are added on as they are needed (so they can be dynamically added depending on which exist). // TODO: remove these- as they should be automatically populated. This is a band-aid fix. "environment", diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index bf923ea76e..2f2f639c08 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -216,7 +216,9 @@ pub struct UploadSearchProject { pub open_source: bool, pub color: Option, - pub dependencies: HashMap>, + pub required_dependencies: Vec, + pub optional_dependencies: Vec, + pub incompatibilities: Vec, // Hidden fields to get the Project model out of the search results. pub loaders: Vec, // Search uses loaders as categories- this is purely for the Project model. @@ -257,7 +259,9 @@ pub struct ResultSearchProject { pub featured_gallery: Option, pub color: Option, - pub dependencies: HashMap>, + pub required_dependencies: Vec, + pub optional_dependencies: Vec, + pub incompatibilities: Vec, // Hidden fields to get the Project model out of the search results. pub loaders: Vec, // Search uses loaders as categories- this is purely for the Project model. From 2400c107dc9b9c7fe6982bed2989b1f39a8b0bab Mon Sep 17 00:00:00 2001 From: IThundxr Date: Thu, 19 Feb 2026 21:04:44 -0500 Subject: [PATCH 6/9] Add support for embedded dependencies --- ...92f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json} | 10 ++++++++-- apps/labrinth/src/search/indexing/local_import.rs | 11 ++++++++--- apps/labrinth/src/search/indexing/mod.rs | 2 ++ apps/labrinth/src/search/mod.rs | 2 ++ 4 files changed, 20 insertions(+), 5 deletions(-) rename apps/labrinth/.sqlx/{query-678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558.json => query-823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json} (53%) diff --git a/apps/labrinth/.sqlx/query-678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558.json b/apps/labrinth/.sqlx/query-823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json similarity index 53% rename from apps/labrinth/.sqlx/query-678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558.json rename to apps/labrinth/.sqlx/query-823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json index bd0481fc9b..656cc73da4 100644 --- a/apps/labrinth/.sqlx/query-678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558.json +++ b/apps/labrinth/.sqlx/query-823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT d.mod_dependency_id as \"mod_dependency_id: DBProjectId\", m.slug as \"slug\" FROM dependencies d\n INNER JOIN mods m ON m.id = d.mod_dependency_id\n WHERE dependent_id = $1", + "query": "\n SELECT d.mod_dependency_id as \"mod_dependency_id: DBProjectId\", d.dependency_type, m.slug as \"slug\" FROM dependencies d\n INNER JOIN mods m ON m.id = d.mod_dependency_id\n WHERE dependent_id = $1", "describe": { "columns": [ { @@ -10,6 +10,11 @@ }, { "ordinal": 1, + "name": "dependency_type", + "type_info": "Varchar" + }, + { + "ordinal": 2, "name": "slug", "type_info": "Varchar" } @@ -21,8 +26,9 @@ }, "nullable": [ true, + false, true ] }, - "hash": "678e51366948f5a41de74182e5de789420301ac35f23276229f39a8de8e8a558" + "hash": "823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84" } diff --git a/apps/labrinth/src/search/indexing/local_import.rs b/apps/labrinth/src/search/indexing/local_import.rs index 2bff516cd3..4cdf156217 100644 --- a/apps/labrinth/src/search/indexing/local_import.rs +++ b/apps/labrinth/src/search/indexing/local_import.rs @@ -416,6 +416,7 @@ pub async fn index_local( color: project.color.map(|x| x as u32), required_dependencies: version.required_dependencies, optional_dependencies: version.optional_dependencies, + embedded_dependencies: version.embedded_dependencies, incompatibilities: version.incompatibilities, loader_fields, project_loader_fields: project_loader_fields.clone(), @@ -436,9 +437,10 @@ struct PartialVersion { loaders: Vec, project_types: Vec, version_fields: Vec, - pub required_dependencies: Vec, - pub optional_dependencies: Vec, - pub incompatibilities: Vec, + required_dependencies: Vec, + optional_dependencies: Vec, + embedded_dependencies: Vec, + incompatibilities: Vec, } async fn index_versions( @@ -557,6 +559,7 @@ async fn index_versions( let mut required_dependencies: Vec = Vec::new(); let mut optional_dependencies: Vec = Vec::new(); + let mut embedded_dependencies: Vec = Vec::new(); let mut incompatibilities: Vec = Vec::new(); let records = sqlx::query!( @@ -573,6 +576,7 @@ async fn index_versions( let v = match r.dependency_type.as_str() { "required" => &mut required_dependencies, "optional" => &mut optional_dependencies, + "embedded" => &mut embedded_dependencies, "incompatible" => &mut incompatibilities, _ => continue, }; @@ -596,6 +600,7 @@ async fn index_versions( version_fields, required_dependencies, optional_dependencies, + embedded_dependencies, incompatibilities, }); } diff --git a/apps/labrinth/src/search/indexing/mod.rs b/apps/labrinth/src/search/indexing/mod.rs index ddf4cd16a5..82d1ae1773 100644 --- a/apps/labrinth/src/search/indexing/mod.rs +++ b/apps/labrinth/src/search/indexing/mod.rs @@ -567,6 +567,7 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ "color", "required_dependencies", "optional_dependencies", + "embedded_dependencies", "incompatibilities", // Note: loader fields are not here, but are added on as they are needed (so they can be dynamically added depending on which exist). // TODO: remove these- as they should be automatically populated. This is a band-aid fix. @@ -614,6 +615,7 @@ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[ "color", "required_dependencies", "optional_dependencies", + "embedded_dependencies", "incompatibilities", // Note: loader fields are not here, but are added on as they are needed (so they can be dynamically added depending on which exist). // TODO: remove these- as they should be automatically populated. This is a band-aid fix. diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index 2f2f639c08..3e71d5ad00 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -218,6 +218,7 @@ pub struct UploadSearchProject { pub required_dependencies: Vec, pub optional_dependencies: Vec, + pub embedded_dependencies: Vec, pub incompatibilities: Vec, // Hidden fields to get the Project model out of the search results. @@ -261,6 +262,7 @@ pub struct ResultSearchProject { pub required_dependencies: Vec, pub optional_dependencies: Vec, + pub embedded_dependencies: Vec, pub incompatibilities: Vec, // Hidden fields to get the Project model out of the search results. From cb7ae0d6f24d084c1fda54cd8bfa5cbcaaab3cc8 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Wed, 25 Mar 2026 15:08:24 -0400 Subject: [PATCH 7/9] post merge --- apps/labrinth/src/search/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index 029bfc0289..7026e5464f 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -338,6 +338,10 @@ impl From for ResultSearchProject { gallery: source.gallery, featured_gallery: source.featured_gallery, color: source.color, + required_dependencies: source.required_dependencies, + optional_dependencies: source.optional_dependencies, + embedded_dependencies: source.embedded_dependencies, + incompatibilities: source.incompatibilities, loaders: source.loaders, project_loader_fields: source.project_loader_fields, components: source.components, From d7c0f28153b0101019fe14dc01da76fe71536421 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Fri, 24 Apr 2026 19:16:05 -0400 Subject: [PATCH 8/9] Update dependencies in search - Add support for elasticsearch and typesense - New structs for some types - Add version of dependency to search - Address reviews --- ...36eb1dd626e77331d8278c406df952691be4c.json | 22 --- ...63ce02ca16781e32232ae0fa7a0af1973d3a4.json | 20 --- ...66908ea41a06ba858dc5b523aef6aae27b850.json | 15 -- ...58541fc78e910111c09ba3d50a12d9ca4a9f8.json | 16 -- ...8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json | 34 ----- ...ed2a23b88bae6b06c218849fc4063625b5f58.json | 40 +++++ ...86e8917cfab9679d5064074681663ba467e41.json | 22 --- apps/labrinth/src/models/v3/projects.rs | 3 +- .../src/search/backend/elasticsearch/mod.rs | 15 ++ .../search/backend/meilisearch/indexing.rs | 14 +- .../src/search/backend/typesense/mod.rs | 21 +++ apps/labrinth/src/search/indexing.rs | 113 ++++++++------ apps/labrinth/src/search/mod.rs | 138 ++++++++++++++++-- 13 files changed, 280 insertions(+), 193 deletions(-) delete mode 100644 apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json delete mode 100644 apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json delete mode 100644 apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json delete mode 100644 apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json delete mode 100644 apps/labrinth/.sqlx/query-823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json create mode 100644 apps/labrinth/.sqlx/query-eae9030e35687c348c2aa9e7363ed2a23b88bae6b06c218849fc4063625b5f58.json delete mode 100644 apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json diff --git a/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json b/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json deleted file mode 100644 index 4bc87e73a5..0000000000 --- a/apps/labrinth/.sqlx/query-0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c" -} diff --git a/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json b/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json deleted file mode 100644 index 3c99ff3fed..0000000000 --- a/apps/labrinth/.sqlx/query-20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - null - ] - }, - "hash": "20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4" -} diff --git a/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json b/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json deleted file mode 100644 index b4c2e5a56e..0000000000 --- a/apps/labrinth/.sqlx/query-6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO payouts_values_notifications (date_available, user_id, notified)\n VALUES ($1, $2, FALSE)\n ON CONFLICT (date_available, user_id) DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Timestamptz", - "Int8" - ] - }, - "nullable": [] - }, - "hash": "6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850" -} diff --git a/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json b/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json deleted file mode 100644 index fc7d2ac98d..0000000000 --- a/apps/labrinth/.sqlx/query-69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)\n VALUES ($1, NULL, $2, NOW(), $3)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Numeric", - "Timestamptz" - ] - }, - "nullable": [] - }, - "hash": "69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8" -} diff --git a/apps/labrinth/.sqlx/query-823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json b/apps/labrinth/.sqlx/query-823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json deleted file mode 100644 index 656cc73da4..0000000000 --- a/apps/labrinth/.sqlx/query-823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT d.mod_dependency_id as \"mod_dependency_id: DBProjectId\", d.dependency_type, m.slug as \"slug\" FROM dependencies d\n INNER JOIN mods m ON m.id = d.mod_dependency_id\n WHERE dependent_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_dependency_id: DBProjectId", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "dependency_type", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "slug", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - true, - false, - true - ] - }, - "hash": "823776dbfefe3061b88492f5cba8ae2a58e3b8ff46a1e29b7f4a57bf68f8da84" -} diff --git a/apps/labrinth/.sqlx/query-eae9030e35687c348c2aa9e7363ed2a23b88bae6b06c218849fc4063625b5f58.json b/apps/labrinth/.sqlx/query-eae9030e35687c348c2aa9e7363ed2a23b88bae6b06c218849fc4063625b5f58.json new file mode 100644 index 0000000000..7c446a9aaf --- /dev/null +++ b/apps/labrinth/.sqlx/query-eae9030e35687c348c2aa9e7363ed2a23b88bae6b06c218849fc4063625b5f58.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n d.dependent_id as \"dependent_version_id: DBVersionId\",\n d.mod_dependency_id as \"dependency_project_id: DBProjectId\",\n d.dependency_id as \"dependency_version_id: DBVersionId\",\n d.dependency_type as \"dependency_type: DependencyType\"\n FROM dependencies d\n INNER JOIN mods m ON m.id = d.mod_dependency_id\n WHERE dependent_id = ANY($1) AND dependency_type != 'embedded'\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "dependent_version_id: DBVersionId", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "dependency_project_id: DBProjectId", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "dependency_version_id: DBVersionId", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "dependency_type: DependencyType", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + true, + true, + false + ] + }, + "hash": "eae9030e35687c348c2aa9e7363ed2a23b88bae6b06c218849fc4063625b5f58" +} diff --git a/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json b/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json deleted file mode 100644 index d3e3520bcc..0000000000 --- a/apps/labrinth/.sqlx/query-fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41" -} diff --git a/apps/labrinth/src/models/v3/projects.rs b/apps/labrinth/src/models/v3/projects.rs index 92abe3fddb..56057cefc7 100644 --- a/apps/labrinth/src/models/v3/projects.rs +++ b/apps/labrinth/src/models/v3/projects.rs @@ -928,8 +928,9 @@ impl VersionType { } #[derive( - Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, utoipa::ToSchema, + Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, sqlx::Type, utoipa::ToSchema, )] +#[sqlx(type_name = "varchar", rename_all = "lowercase")] #[serde(rename_all = "lowercase")] pub enum DependencyType { Required, diff --git a/apps/labrinth/src/search/backend/elasticsearch/mod.rs b/apps/labrinth/src/search/backend/elasticsearch/mod.rs index 5c8cb8612e..94f1ea73fd 100644 --- a/apps/labrinth/src/search/backend/elasticsearch/mod.rs +++ b/apps/labrinth/src/search/backend/elasticsearch/mod.rs @@ -163,6 +163,18 @@ impl SearchField { path: "open_source", mapping: json!({ "type": "boolean" }), }, + SearchField::RequiredDependencies => ElasticsearchFieldSpec { + path: "required_dependencies", + mapping: json!({ "type": "keyword" }), + }, + SearchField::OptionalDependencies => ElasticsearchFieldSpec { + path: "optional_dependencies", + mapping: json!({ "type": "keyword" }), + }, + SearchField::Incompatibilities => ElasticsearchFieldSpec { + path: "incompatibilities", + mapping: json!({ "type": "keyword" }), + }, SearchField::Environment => ElasticsearchFieldSpec { path: "environment", mapping: json!({ "type": "keyword" }), @@ -251,6 +263,9 @@ static ELASTICSEARCH_PROPERTIES: LazyLock> = ("license".to_string(), json!({ "type": "keyword" })), ("loaders".to_string(), json!({ "type": "keyword" })), ("color".to_string(), json!({ "type": "long" })), + ("required_dependencies".to_string(), json!({ "type": "keyword" })), + ("optional_dependencies".to_string(), json!({ "type": "keyword" })), + ("incompatibilities".to_string(), json!({ "type": "keyword" })), ("environment".to_string(), json!({ "type": "keyword" })), ("mrpack_loaders".to_string(), json!({ "type": "keyword" })), ( diff --git a/apps/labrinth/src/search/backend/meilisearch/indexing.rs b/apps/labrinth/src/search/backend/meilisearch/indexing.rs index 1d9b00bcfa..7bb6c3e0ef 100644 --- a/apps/labrinth/src/search/backend/meilisearch/indexing.rs +++ b/apps/labrinth/src/search/backend/meilisearch/indexing.rs @@ -581,6 +581,18 @@ impl SearchField { path: "open_source", filterable: true, }, + SearchField::RequiredDependencies => MeilisearchFieldSpec { + path: "required_dependencies", + filterable: true, + }, + SearchField::OptionalDependencies => MeilisearchFieldSpec { + path: "optional_dependencies", + filterable: true, + }, + SearchField::Incompatibilities => MeilisearchFieldSpec { + path: "incompatibilities", + filterable: true, + }, SearchField::Environment => MeilisearchFieldSpec { path: "environment", filterable: true, @@ -659,7 +671,6 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ "color", "required_dependencies", "optional_dependencies", - "embedded_dependencies", "incompatibilities", // Note: loader fields are not here, but are added on as they are needed (so they can be dynamically added depending on which exist). // TODO: remove these- as they should be automatically populated. This is a band-aid fix. @@ -702,7 +713,6 @@ const DEFAULT_SORTABLE_ATTRIBUTES: &[&str] = &[ "version_published_timestamp", "required_dependencies", "optional_dependencies", - "embedded_dependencies", "incompatibilities", "minecraft_java_server.verified_plays_2w", "minecraft_java_server.ping.data.players_online", diff --git a/apps/labrinth/src/search/backend/typesense/mod.rs b/apps/labrinth/src/search/backend/typesense/mod.rs index e7f302b8c7..11b34de331 100644 --- a/apps/labrinth/src/search/backend/typesense/mod.rs +++ b/apps/labrinth/src/search/backend/typesense/mod.rs @@ -421,6 +421,27 @@ impl SearchField { sort: false, optional: true, }, + SearchField::RequiredDependencies => TypesenseFieldSpec { + path: "required_dependencies", + ty: "string[]", + facet: true, + sort: false, + optional: true, + }, + SearchField::OptionalDependencies => TypesenseFieldSpec { + path: "optional_dependencies", + ty: "string[]", + facet: true, + sort: false, + optional: true, + }, + SearchField::Incompatibilities => TypesenseFieldSpec { + path: "incompatibilities", + ty: "string[]", + facet: true, + sort: false, + optional: true, + }, SearchField::Environment => TypesenseFieldSpec { path: "environment", ty: "string[]", diff --git a/apps/labrinth/src/search/indexing.rs b/apps/labrinth/src/search/indexing.rs index 806844beb3..a9f39e55f4 100644 --- a/apps/labrinth/src/search/indexing.rs +++ b/apps/labrinth/src/search/indexing.rs @@ -20,11 +20,11 @@ use crate::database::models::{ }; use crate::database::redis::RedisPool; use crate::models::exp; -use crate::models::ids::ProjectId; -use crate::models::projects::from_duplicate_version_fields; +use crate::models::ids::{ProjectId, VersionId}; +use crate::models::projects::{from_duplicate_version_fields, DependencyType}; use crate::models::v2::projects::LegacyProject; use crate::routes::v2_reroute; -use crate::search::UploadSearchProject; +use crate::search::{Dependencies, Dependency, UploadSearchProject}; use crate::util::error::Context; fn normalize_for_search(s: &str) -> String { @@ -486,10 +486,7 @@ pub async fn index_local( featured_gallery: featured_gallery.clone(), open_source, color: project.color.map(|x| x as u32), - required_dependencies: version.required_dependencies, - optional_dependencies: version.optional_dependencies, - embedded_dependencies: version.embedded_dependencies, - incompatibilities: version.incompatibilities, + dependencies: version.dependencies, loader_fields, project_loader_fields: project_loader_fields.clone(), // 'loaders' is aggregate of all versions' loaders @@ -511,10 +508,7 @@ struct PartialVersion { project_types: Vec, version_fields: Vec, date_published: DateTime, - required_dependencies: Vec, - optional_dependencies: Vec, - embedded_dependencies: Vec, - incompatibilities: Vec, + dependencies: Dependencies, } async fn index_versions( @@ -622,7 +616,61 @@ async fn index_versions( .await .wrap_err("failed to fetch version fields")?; - // Get version fields + // Get dependencies + + // This can get a bit confusing to understand, so here's some documentation on it: + // pID = Project ID, vID = Version ID + // + // Projects: + // Project A (pID: 1) -> The Dependent + // Project B (pID: 2) -> The Dependency + // + // Versions: + // Project A (pID: 1) Version v1.0.0 (vID: 400) + // Project B (pID: 2) Version v2.0.0 (vID: 500) + // + // Returned Data: + // dependent_version_id (vID: 400) = Project A v1.0.0 (This is the version which is declaring a dependency aka The Dependent) + // dependency_project_id (pID: 2) = Project B (The Dependency) (this is technically nullable, but I'm not sure if there's any cases in which it'd be null) + // dependency_version_id (vID: 500) = Project B v2.0.0 (The version required by the dependency) (nullable) + // dependency_type = "required" + // + // And the returned data ends up saying in plain words: + // Version (Project A v1.0.0, vID: 400) has a (required) dependency on the version (Project B v2.0.0, vID: 500) from the Project (Project B, pID: 2) + // ~ @ithundxr + let dependencies: DashMap = sqlx::query!( + " + SELECT + d.dependent_id as \"dependent_version_id: DBVersionId\", + d.mod_dependency_id as \"dependency_project_id: DBProjectId\", + d.dependency_id as \"dependency_version_id: DBVersionId\", + d.dependency_type as \"dependency_type: DependencyType\" + FROM dependencies d + INNER JOIN mods m ON m.id = d.mod_dependency_id + WHERE dependent_id = ANY($1) AND dependency_type != 'embedded' + ", + &all_version_ids + ) + .fetch(pool) + .try_fold( + DashMap::new(), + |acc: DashMap, m| { + if let Some(dependency_project_id) = m.dependency_project_id { + let dependency = Dependency { + project: ProjectId::from(dependency_project_id), + version: m.dependency_version_id.map(VersionId::from), + }; + + acc.entry(m.dependent_version_id) + .or_default() + .add_dependency(m.dependency_type, dependency); + } + + async move { Ok(acc) } + } + ) + .await + .wrap_err("failed to fetch dependencies")?; // Convert to partial versions let mut res_versions: HashMap> = @@ -641,38 +689,10 @@ async fn index_versions( .map(|(_, version_fields)| version_fields) .unwrap_or_default(); - let mut required_dependencies: Vec = Vec::new(); - let mut optional_dependencies: Vec = Vec::new(); - let mut embedded_dependencies: Vec = Vec::new(); - let mut incompatibilities: Vec = Vec::new(); - - let records = sqlx::query!( - " - SELECT d.mod_dependency_id as \"mod_dependency_id: DBProjectId\", d.dependency_type, m.slug as \"slug\" FROM dependencies d - INNER JOIN mods m ON m.id = d.mod_dependency_id - WHERE dependent_id = $1", - version_id.0 - ) - .fetch_all(pool) - .await?; - - for r in records { - let v = match r.dependency_type.as_str() { - "required" => &mut required_dependencies, - "optional" => &mut optional_dependencies, - "embedded" => &mut embedded_dependencies, - "incompatible" => &mut incompatibilities, - _ => continue, - }; - - if let Some(id) = r.mod_dependency_id { - v.push(crate::models::ids::ProjectId::from(id).to_string()) - } - - if let Some(slug) = &r.slug { - v.push(slug.to_string()) - } - } + let dependencies = dependencies + .remove(version_id) + .map(|(_, dependencies)| dependencies) + .unwrap_or_default(); res_versions .entry(*project_id) @@ -683,10 +703,7 @@ async fn index_versions( project_types: version_loader_data.project_types, version_fields, date_published: *date_published, - required_dependencies, - optional_dependencies, - embedded_dependencies, - incompatibilities, + dependencies, }); } } diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index 758bb964bc..f606551b52 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -8,11 +8,14 @@ use crate::{database::PgPool, env::ENV}; use ariadne::ids::base62_impl::parse_base62; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use std::{collections::HashMap, str::FromStr}; +use std::fmt::Formatter; +use serde::de::{Error, Visitor}; use thiserror::Error; use utoipa::ToSchema; +use crate::models::projects::DependencyType; pub mod backend; pub mod indexing; @@ -192,6 +195,9 @@ pub enum SearchField { ProjectTypes, ProjectId, OpenSource, + RequiredDependencies, + OptionalDependencies, + Incompatibilities, Environment, GameVersions, ClientSide, @@ -220,6 +226,82 @@ impl FromStr for SearchBackendKind { } } +#[derive(Debug, Clone)] +pub struct Dependency { + project: ProjectId, + version: Option, +} + +impl Serialize for Dependency { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer + { + let s = match self.version { + Some(version) => format!("{}:{}", self.project, version), + None => self.project.to_string(), + }; + serializer.serialize_str(&s) + } +} + +impl<'de> Deserialize<'de> for Dependency { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de> + { + struct DependencyVisitor; + + impl<'de> Visitor<'de> for DependencyVisitor { + type Value = Dependency; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("a string in the format 'project[:version]'") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + let mut parts = v.splitn(2, ":"); + + let project = parts.next() + .and_then(|x| parse_base62(x).ok()) + .map(ProjectId) + .ok_or_else(|| E::custom("ProjectId is missing"))?; + let version = parts.next() + .and_then(|x| parse_base62(x).ok()) + .map(VersionId); + + Ok(Dependency { project, version }) + } + } + + deserializer.deserialize_str(DependencyVisitor) + } +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +pub struct Dependencies { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub required_dependencies: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub optional_dependencies: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub incompatibilities: Vec, +} + +impl Dependencies { + pub fn add_dependency(&mut self, dependency_type: DependencyType, dep: Dependency) { + match dependency_type { + DependencyType::Required => self.required_dependencies.push(dep), + DependencyType::Optional => self.optional_dependencies.push(dep), + DependencyType::Incompatible => self.incompatibilities.push(dep), + _ => {} + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UploadSearchProject { pub version_id: String, @@ -257,10 +339,8 @@ pub struct UploadSearchProject { pub open_source: bool, pub color: Option, - pub required_dependencies: Vec, - pub optional_dependencies: Vec, - pub embedded_dependencies: Vec, - pub incompatibilities: Vec, + #[serde(flatten)] + pub dependencies: Dependencies, // Hidden fields to get the Project model out of the search results. pub loaders: Vec, // Search uses loaders as categories- this is purely for the Project model. @@ -309,10 +389,8 @@ pub struct ResultSearchProject { pub featured_gallery: Option, pub color: Option, - pub required_dependencies: Vec, - pub optional_dependencies: Vec, - pub embedded_dependencies: Vec, - pub incompatibilities: Vec, + #[serde(flatten)] + pub dependencies: Dependencies, // Hidden fields to get the Project model out of the search results. pub loaders: Vec, // Search uses loaders as categories- this is purely for the Project model. @@ -350,10 +428,7 @@ impl From for ResultSearchProject { gallery: source.gallery, featured_gallery: source.featured_gallery, color: source.color, - required_dependencies: source.required_dependencies, - optional_dependencies: source.optional_dependencies, - embedded_dependencies: source.embedded_dependencies, - incompatibilities: source.incompatibilities, + dependencies: source.dependencies, loaders: source.loaders, project_loader_fields: source.project_loader_fields, components: source.components, @@ -378,3 +453,40 @@ pub fn backend(meta_namespace: Option) -> Box { } } } + +#[cfg(test)] +mod tests { + use super::*; + + // Dummy data, no real point in randomly generating them for tests + const PROJECT_ID: ProjectId = ProjectId(123456789); + const VERSION_ID: VersionId = VersionId(987654321); + + #[test] + fn test_dependency_serialize_then_deserialize_with_version() { + let dependency = Dependency { + project: PROJECT_ID, + version: Some(VERSION_ID), + }; + + let serialized = serde_json::to_string(&dependency).unwrap(); + let deserialized: Dependency = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(dependency.project, deserialized.project); + assert_eq!(dependency.version, deserialized.version); + } + + #[test] + fn test_dependency_serialize_then_deserialize_without_version() { + let dependency = Dependency { + project: PROJECT_ID, + version: None, + }; + + let serialized = serde_json::to_string(&dependency).unwrap(); + let deserialized: Dependency = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(dependency.project, deserialized.project); + assert_eq!(dependency.version, deserialized.version); + } +} \ No newline at end of file From a1181c9d6fa4b668439494ee4091194223c834a3 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Sat, 25 Apr 2026 12:53:15 +0000 Subject: [PATCH 9/9] Run cargo fmt --- apps/labrinth/src/models/v3/projects.rs | 10 ++++- .../src/search/backend/elasticsearch/mod.rs | 15 +++++-- apps/labrinth/src/search/indexing.rs | 44 +++++++++---------- apps/labrinth/src/search/mod.rs | 37 ++++++++++------ 4 files changed, 66 insertions(+), 40 deletions(-) diff --git a/apps/labrinth/src/models/v3/projects.rs b/apps/labrinth/src/models/v3/projects.rs index 56057cefc7..edad1d37db 100644 --- a/apps/labrinth/src/models/v3/projects.rs +++ b/apps/labrinth/src/models/v3/projects.rs @@ -928,7 +928,15 @@ impl VersionType { } #[derive( - Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, sqlx::Type, utoipa::ToSchema, + Serialize, + Deserialize, + Copy, + Clone, + Debug, + PartialEq, + Eq, + sqlx::Type, + utoipa::ToSchema, )] #[sqlx(type_name = "varchar", rename_all = "lowercase")] #[serde(rename_all = "lowercase")] diff --git a/apps/labrinth/src/search/backend/elasticsearch/mod.rs b/apps/labrinth/src/search/backend/elasticsearch/mod.rs index 94f1ea73fd..1b70701f21 100644 --- a/apps/labrinth/src/search/backend/elasticsearch/mod.rs +++ b/apps/labrinth/src/search/backend/elasticsearch/mod.rs @@ -263,9 +263,18 @@ static ELASTICSEARCH_PROPERTIES: LazyLock> = ("license".to_string(), json!({ "type": "keyword" })), ("loaders".to_string(), json!({ "type": "keyword" })), ("color".to_string(), json!({ "type": "long" })), - ("required_dependencies".to_string(), json!({ "type": "keyword" })), - ("optional_dependencies".to_string(), json!({ "type": "keyword" })), - ("incompatibilities".to_string(), json!({ "type": "keyword" })), + ( + "required_dependencies".to_string(), + json!({ "type": "keyword" }), + ), + ( + "optional_dependencies".to_string(), + json!({ "type": "keyword" }), + ), + ( + "incompatibilities".to_string(), + json!({ "type": "keyword" }), + ), ("environment".to_string(), json!({ "type": "keyword" })), ("mrpack_loaders".to_string(), json!({ "type": "keyword" })), ( diff --git a/apps/labrinth/src/search/indexing.rs b/apps/labrinth/src/search/indexing.rs index a9f39e55f4..bece1f3e2a 100644 --- a/apps/labrinth/src/search/indexing.rs +++ b/apps/labrinth/src/search/indexing.rs @@ -21,7 +21,7 @@ use crate::database::models::{ use crate::database::redis::RedisPool; use crate::models::exp; use crate::models::ids::{ProjectId, VersionId}; -use crate::models::projects::{from_duplicate_version_fields, DependencyType}; +use crate::models::projects::{DependencyType, from_duplicate_version_fields}; use crate::models::v2::projects::LegacyProject; use crate::routes::v2_reroute; use crate::search::{Dependencies, Dependency, UploadSearchProject}; @@ -639,7 +639,7 @@ async fn index_versions( // Version (Project A v1.0.0, vID: 400) has a (required) dependency on the version (Project B v2.0.0, vID: 500) from the Project (Project B, pID: 2) // ~ @ithundxr let dependencies: DashMap = sqlx::query!( - " + " SELECT d.dependent_id as \"dependent_version_id: DBVersionId\", d.mod_dependency_id as \"dependency_project_id: DBProjectId\", @@ -649,28 +649,28 @@ async fn index_versions( INNER JOIN mods m ON m.id = d.mod_dependency_id WHERE dependent_id = ANY($1) AND dependency_type != 'embedded' ", - &all_version_ids - ) - .fetch(pool) - .try_fold( - DashMap::new(), - |acc: DashMap, m| { - if let Some(dependency_project_id) = m.dependency_project_id { - let dependency = Dependency { - project: ProjectId::from(dependency_project_id), - version: m.dependency_version_id.map(VersionId::from), - }; - - acc.entry(m.dependent_version_id) - .or_default() - .add_dependency(m.dependency_type, dependency); - } + &all_version_ids + ) + .fetch(pool) + .try_fold( + DashMap::new(), + |acc: DashMap, m| { + if let Some(dependency_project_id) = m.dependency_project_id { + let dependency = Dependency { + project: ProjectId::from(dependency_project_id), + version: m.dependency_version_id.map(VersionId::from), + }; - async move { Ok(acc) } + acc.entry(m.dependent_version_id) + .or_default() + .add_dependency(m.dependency_type, dependency); } - ) - .await - .wrap_err("failed to fetch dependencies")?; + + async move { Ok(acc) } + }, + ) + .await + .wrap_err("failed to fetch dependencies")?; // Convert to partial versions let mut res_versions: HashMap> = diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index f606551b52..28f7bd78b5 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -2,20 +2,20 @@ use crate::database::redis::RedisPool; use crate::models::exp; use crate::models::exp::minecraft::JavaServerPing; use crate::models::ids::{ProjectId, VersionId}; +use crate::models::projects::DependencyType; use crate::queue::server_ping; use crate::routes::ApiError; use crate::{database::PgPool, env::ENV}; use ariadne::ids::base62_impl::parse_base62; use async_trait::async_trait; use chrono::{DateTime, Utc}; +use serde::de::{Error, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; -use std::{collections::HashMap, str::FromStr}; use std::fmt::Formatter; -use serde::de::{Error, Visitor}; +use std::{collections::HashMap, str::FromStr}; use thiserror::Error; use utoipa::ToSchema; -use crate::models::projects::DependencyType; pub mod backend; pub mod indexing; @@ -235,7 +235,7 @@ pub struct Dependency { impl Serialize for Dependency { fn serialize(&self, serializer: S) -> Result where - S: Serializer + S: Serializer, { let s = match self.version { Some(version) => format!("{}:{}", self.project, version), @@ -248,7 +248,7 @@ impl Serialize for Dependency { impl<'de> Deserialize<'de> for Dependency { fn deserialize(deserializer: D) -> Result where - D: Deserializer<'de> + D: Deserializer<'de>, { struct DependencyVisitor; @@ -256,7 +256,8 @@ impl<'de> Deserialize<'de> for Dependency { type Value = Dependency; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("a string in the format 'project[:version]'") + formatter + .write_str("a string in the format 'project[:version]'") } fn visit_str(self, v: &str) -> Result @@ -265,12 +266,14 @@ impl<'de> Deserialize<'de> for Dependency { { let mut parts = v.splitn(2, ":"); - let project = parts.next() - .and_then(|x| parse_base62(x).ok()) + let project = parts + .next() + .and_then(|x| parse_base62(x).ok()) .map(ProjectId) .ok_or_else(|| E::custom("ProjectId is missing"))?; - let version = parts.next() - .and_then(|x| parse_base62(x).ok()) + let version = parts + .next() + .and_then(|x| parse_base62(x).ok()) .map(VersionId); Ok(Dependency { project, version }) @@ -292,7 +295,11 @@ pub struct Dependencies { } impl Dependencies { - pub fn add_dependency(&mut self, dependency_type: DependencyType, dep: Dependency) { + pub fn add_dependency( + &mut self, + dependency_type: DependencyType, + dep: Dependency, + ) { match dependency_type { DependencyType::Required => self.required_dependencies.push(dep), DependencyType::Optional => self.optional_dependencies.push(dep), @@ -470,7 +477,8 @@ mod tests { }; let serialized = serde_json::to_string(&dependency).unwrap(); - let deserialized: Dependency = serde_json::from_str(&serialized).unwrap(); + let deserialized: Dependency = + serde_json::from_str(&serialized).unwrap(); assert_eq!(dependency.project, deserialized.project); assert_eq!(dependency.version, deserialized.version); @@ -484,9 +492,10 @@ mod tests { }; let serialized = serde_json::to_string(&dependency).unwrap(); - let deserialized: Dependency = serde_json::from_str(&serialized).unwrap(); + let deserialized: Dependency = + serde_json::from_str(&serialized).unwrap(); assert_eq!(dependency.project, deserialized.project); assert_eq!(dependency.version, deserialized.version); } -} \ No newline at end of file +}