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-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-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-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" -} 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..edad1d37db 100644 --- a/apps/labrinth/src/models/v3/projects.rs +++ b/apps/labrinth/src/models/v3/projects.rs @@ -928,8 +928,17 @@ 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..1b70701f21 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,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" }), + ), ("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 8f2a4bfa74..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, @@ -657,6 +669,9 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ "gallery", "featured_gallery", "color", + "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", @@ -696,6 +711,9 @@ const DEFAULT_SORTABLE_ATTRIBUTES: &[&str] = &[ "date_created", "date_modified", "version_published_timestamp", + "required_dependencies", + "optional_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 85c6408ec2..bece1f3e2a 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::{DependencyType, from_duplicate_version_fields}; 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,6 +486,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 @@ -507,6 +508,7 @@ struct PartialVersion { project_types: Vec, version_fields: Vec, date_published: DateTime, + dependencies: Dependencies, } async fn index_versions( @@ -614,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> = @@ -633,6 +689,11 @@ async fn index_versions( .map(|(_, version_fields)| version_fields) .unwrap_or_default(); + let dependencies = dependencies + .remove(version_id) + .map(|(_, dependencies)| dependencies) + .unwrap_or_default(); + res_versions .entry(*project_id) .or_default() @@ -642,6 +703,7 @@ async fn index_versions( project_types: version_loader_data.project_types, version_fields, date_published: *date_published, + dependencies, }); } } diff --git a/apps/labrinth/src/search/mod.rs b/apps/labrinth/src/search/mod.rs index d5be4fc592..28f7bd78b5 100644 --- a/apps/labrinth/src/search/mod.rs +++ b/apps/labrinth/src/search/mod.rs @@ -2,14 +2,17 @@ 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::{Deserialize, Serialize}; +use serde::de::{Error, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; +use std::fmt::Formatter; use std::{collections::HashMap, str::FromStr}; use thiserror::Error; use utoipa::ToSchema; @@ -192,6 +195,9 @@ pub enum SearchField { ProjectTypes, ProjectId, OpenSource, + RequiredDependencies, + OptionalDependencies, + Incompatibilities, Environment, GameVersions, ClientSide, @@ -220,6 +226,89 @@ 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,6 +346,9 @@ pub struct UploadSearchProject { pub open_source: bool, pub color: Option, + #[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. pub project_loader_fields: HashMap>, // Aggregation of loader_fields from all versions of the project, allowing for reconstruction of the Project model. @@ -304,6 +396,9 @@ pub struct ResultSearchProject { pub featured_gallery: Option, pub color: Option, + #[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. pub project_loader_fields: HashMap>, // Aggregation of loader_fields from all versions of the project, allowing for reconstruction of the Project model. @@ -340,6 +435,7 @@ impl From for ResultSearchProject { gallery: source.gallery, featured_gallery: source.featured_gallery, color: source.color, + dependencies: source.dependencies, loaders: source.loaders, project_loader_fields: source.project_loader_fields, components: source.components, @@ -364,3 +460,42 @@ 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); + } +}