From 430b5090d7c51587ceaabd828552cbfe78d4c8e2 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Fri, 1 Aug 2025 17:34:34 -0400 Subject: [PATCH 1/4] feat(labrinth): dependents api endpoint --- ...75b14b5b9edbb23ede6da63d996973d06bf6c.json | 28 ++++++ .../src/database/models/project_item.rs | 69 ++++++++++++++- .../src/database/models/version_item.rs | 1 + apps/labrinth/src/queue/moderation.rs | 1 + apps/labrinth/src/routes/v2/projects.rs | 49 ++++++++++- apps/labrinth/src/routes/v3/organizations.rs | 2 + apps/labrinth/src/routes/v3/projects.rs | 86 ++++++++++++++++++- .../src/routes/v3/version_creation.rs | 9 +- apps/labrinth/src/routes/v3/versions.rs | 3 + 9 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-7c418e474c911332e6425bbb42775b14b5b9edbb23ede6da63d996973d06bf6c.json diff --git a/apps/labrinth/.sqlx/query-7c418e474c911332e6425bbb42775b14b5b9edbb23ede6da63d996973d06bf6c.json b/apps/labrinth/.sqlx/query-7c418e474c911332e6425bbb42775b14b5b9edbb23ede6da63d996973d06bf6c.json new file mode 100644 index 0000000000..264e8b7862 --- /dev/null +++ b/apps/labrinth/.sqlx/query-7c418e474c911332e6425bbb42775b14b5b9edbb23ede6da63d996973d06bf6c.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT DISTINCT version.id version_id, mod.id FROM versions version\n INNER JOIN mods mod ON version.mod_id = mod.id\n INNER JOIN dependencies d ON version.id = d.dependent_id\n WHERE mod.status = 'approved' AND d.mod_dependency_id = $1\n ORDER BY mod.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "7c418e474c911332e6425bbb42775b14b5b9edbb23ede6da63d996973d06bf6c" +} diff --git a/apps/labrinth/src/database/models/project_item.rs b/apps/labrinth/src/database/models/project_item.rs index b22c4cb543..6bfe3320fa 100644 --- a/apps/labrinth/src/database/models/project_item.rs +++ b/apps/labrinth/src/database/models/project_item.rs @@ -19,6 +19,7 @@ use std::hash::Hash; pub const PROJECTS_NAMESPACE: &str = "projects"; pub const PROJECTS_SLUGS_NAMESPACE: &str = "projects_slugs"; const PROJECTS_DEPENDENCIES_NAMESPACE: &str = "projects_dependencies"; +const PROJECTS_DEPENDENTS_NAMESPACE: &str = "projects_dependents"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LinkUrl { @@ -343,8 +344,14 @@ impl DBProject { let project = Self::get_id(id, &mut **transaction, redis).await?; if let Some(project) = project { - DBProject::clear_cache(id, project.inner.slug, Some(true), redis) - .await?; + DBProject::clear_cache( + id, + project.inner.slug, + Some(true), + Some(true), + redis, + ) + .await?; sqlx::query!( " @@ -932,10 +939,60 @@ impl DBProject { Ok(dependencies) } + pub async fn get_dependents<'a, E>( + id: DBProjectId, + exec: E, + redis: &RedisPool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + type Dependents = Vec<(DBVersionId, DBProjectId)>; + + let mut redis = redis.connect().await?; + + let dependents = redis + .get_deserialized_from_json::( + PROJECTS_DEPENDENTS_NAMESPACE, + &id.0.to_string(), + ) + .await?; + if let Some(dependents) = dependents { + return Ok(dependents); + } + + // SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id + let dependents: Dependents = sqlx::query!( + " + SELECT DISTINCT version.id version_id, mod.id FROM versions version + INNER JOIN mods mod ON version.mod_id = mod.id + INNER JOIN dependencies d ON version.id = d.dependent_id + WHERE mod.status = 'approved' AND d.mod_dependency_id = $1 + ORDER BY mod.id; + ", + id as DBProjectId + ) + .fetch(exec) + .map_ok(|x| (DBVersionId(x.version_id), DBProjectId(x.id))) + .try_collect::() + .await?; + + redis + .set_serialized_to_json( + PROJECTS_DEPENDENTS_NAMESPACE, + id.0, + &dependents, + None, + ) + .await?; + Ok(dependents) + } + pub async fn clear_cache( id: DBProjectId, slug: Option, clear_dependencies: Option, + clear_dependents: Option, redis: &RedisPool, ) -> Result<(), DatabaseError> { let mut redis = redis.connect().await?; @@ -952,6 +1009,14 @@ impl DBProject { None }, ), + ( + PROJECTS_DEPENDENTS_NAMESPACE, + if clear_dependents.unwrap_or(false) { + Some(id.0.to_string()) + } else { + None + }, + ), ]) .await?; Ok(()) diff --git a/apps/labrinth/src/database/models/version_item.rs b/apps/labrinth/src/database/models/version_item.rs index 3a4edb765e..0a1271e261 100644 --- a/apps/labrinth/src/database/models/version_item.rs +++ b/apps/labrinth/src/database/models/version_item.rs @@ -451,6 +451,7 @@ impl DBVersion { DBProjectId(project_id.mod_id), None, None, + None, redis, ) .await?; diff --git a/apps/labrinth/src/queue/moderation.rs b/apps/labrinth/src/queue/moderation.rs index 49c2e38c22..107a9fba4a 100644 --- a/apps/labrinth/src/queue/moderation.rs +++ b/apps/labrinth/src/queue/moderation.rs @@ -704,6 +704,7 @@ impl AutomatedModerationQueue { project.inner.id, project.inner.slug.clone(), None, + None, &redis, ) .await?; diff --git a/apps/labrinth/src/routes/v2/projects.rs b/apps/labrinth/src/routes/v2/projects.rs index 4915b3ad02..a1275660d4 100644 --- a/apps/labrinth/src/routes/v2/projects.rs +++ b/apps/labrinth/src/routes/v2/projects.rs @@ -45,7 +45,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { web::scope("{project_id}") .service(super::versions::version_list) .service(super::versions::version_project_get) - .service(dependency_list), + .service(dependency_list) + .service(dependents_list), ), ); } @@ -306,6 +307,52 @@ pub async fn dependency_list( } } +#[get("dependents")] +pub async fn dependents_list( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + // TODO: tests, probably + let response = v3::projects::dependents_list( + req, + info, + pool.clone(), + redis.clone(), + session_queue, + ) + .await + .or_else(v2_reroute::flatten_404_error)?; + + match v2_reroute::extract_ok_json::< + crate::routes::v3::projects::DependencyInfo, + >(response) + .await + { + Ok(dependency_info) => { + let converted_projects = LegacyProject::from_many( + dependency_info.projects, + &**pool, + &redis, + ) + .await?; + let converted_versions = dependency_info + .versions + .into_iter() + .map(LegacyVersion::from) + .collect(); + + Ok(HttpResponse::Ok().json(DependencyInfo { + projects: converted_projects, + versions: converted_versions, + })) + } + Err(response) => Ok(response), + } +} + #[derive(Serialize, Deserialize, Validate)] pub struct EditProject { #[validate( diff --git a/apps/labrinth/src/routes/v3/organizations.rs b/apps/labrinth/src/routes/v3/organizations.rs index a1c38009b2..2a6388f925 100644 --- a/apps/labrinth/src/routes/v3/organizations.rs +++ b/apps/labrinth/src/routes/v3/organizations.rs @@ -830,6 +830,7 @@ pub async fn organization_projects_add( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1018,6 +1019,7 @@ pub async fn organization_projects_remove( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 766f4fc5a7..49110b91fc 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -73,7 +73,8 @@ pub fn config(cfg: &mut web::ServiceConfig) { "version/{slug}", web::get().to(super::versions::version_project_get), ) - .route("dependencies", web::get().to(dependency_list)), + .route("dependencies", web::get().to(dependency_list)) + .route("dependents", web::get().to(dependents_list)), ), ); } @@ -895,6 +896,7 @@ pub async fn project_edit( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1094,6 +1096,82 @@ pub async fn dependency_list( } } +pub async fn dependents_list( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + let string = info.into_inner().0; + + let result = db_models::DBProject::get(&string, &**pool, &redis).await?; + + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_READ, + ) + .await + .map(|x| x.1) + .ok(); + + if let Some(project) = result { + if !is_visible_project(&project.inner, &user_option, &pool, false) + .await? + { + return Err(ApiError::NotFound); + } + + let dependents = database::DBProject::get_dependents( + project.inner.id, + &**pool, + &redis, + ) + .await?; + let project_ids = + dependents.iter().map(|x| x.1).unique().collect::>(); + + let dep_version_ids = dependents + .iter() + .map(|x| x.0) + .unique() + .collect::>(); + let (projects_result, versions_result) = futures::future::try_join( + database::DBProject::get_many_ids(&project_ids, &**pool, &redis), + database::DBVersion::get_many(&dep_version_ids, &**pool, &redis), + ) + .await?; + + let mut projects = filter_visible_projects( + projects_result, + &user_option, + &pool, + false, + ) + .await?; + let mut versions = filter_visible_versions( + versions_result, + &user_option, + &pool, + &redis, + ) + .await?; + + projects.sort_by(|a, b| b.published.cmp(&a.published)); + projects.dedup_by(|a, b| a.id == b.id); + + versions.sort_by(|a, b| b.date_published.cmp(&a.date_published)); + versions.dedup_by(|a, b| a.id == b.id); + + Ok(HttpResponse::Ok().json(DependencyInfo { projects, versions })) + } else { + Err(ApiError::NotFound) + } +} + pub struct CategoryChanges<'a> { pub categories: &'a Option>, pub add_categories: &'a Option>, @@ -1328,6 +1406,7 @@ pub async fn projects_edit( project.inner.id, project.inner.slug, None, + None, &redis, ) .await?; @@ -1521,6 +1600,7 @@ pub async fn project_icon_edit( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1610,6 +1690,7 @@ pub async fn delete_project_icon( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1765,6 +1846,7 @@ pub async fn add_gallery_item( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1948,6 +2030,7 @@ pub async fn edit_gallery_item( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -2062,6 +2145,7 @@ pub async fn delete_gallery_item( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index ca9e25eae8..2a2fd222fb 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -513,7 +513,14 @@ async fn version_create_inner( } } - models::DBProject::clear_cache(project_id, None, Some(true), redis).await?; + models::DBProject::clear_cache( + project_id, + None, + Some(true), + Some(true), + redis, + ) + .await?; let project_status = sqlx::query!( "SELECT status FROM mods WHERE id = $1", diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index 22bd9b3ca4..680e302930 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -528,6 +528,7 @@ pub async fn version_edit_helper( version_item.inner.project_id, None, None, + None, &redis, ) .await?; @@ -688,6 +689,7 @@ pub async fn version_edit_helper( version_item.inner.project_id, None, Some(true), + Some(true), &redis, ) .await?; @@ -966,6 +968,7 @@ pub async fn version_delete( version.inner.project_id, None, Some(true), + Some(true), &redis, ) .await?; From 79df52c3d9b1bb82a339c6f60110c31c42a1831a Mon Sep 17 00:00:00 2001 From: IThundxr Date: Fri, 1 Aug 2025 17:45:56 -0400 Subject: [PATCH 2/4] feat(docs): document dependendents api endpoint --- apps/docs/public/openapi.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/apps/docs/public/openapi.yaml b/apps/docs/public/openapi.yaml index 73b2b05b7b..0f9fc3ca88 100644 --- a/apps/docs/public/openapi.yaml +++ b/apps/docs/public/openapi.yaml @@ -1053,6 +1053,19 @@ components: items: $ref: '#/components/schemas/Version' description: Versions that the project depends upon + ProjectDependentsList: + type: object + properties: + projects: + type: array + items: + $ref: '#/components/schemas/Project' + description: Projects that that depend on the project + versions: + type: array + items: + $ref: '#/components/schemas/Version' + description: Versions that depend on the project PatchProjectsBody: type: object properties: @@ -2396,6 +2409,23 @@ paths: $ref: '#/components/schemas/ProjectDependencyList' '404': description: The requested item(s) were not found or no authorization to access the requested item(s) + /project/{id|slug}/dependents: + parameters: + - $ref: '#/components/parameters/ProjectIdentifier' + get: + summary: Get all dependents for a project + operationId: getDependents + tags: + - projects + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectDependentsList' + '404': + description: The requested item(s) were not found or no authorization to access the requested item(s) /project/{id|slug}/follow: parameters: - $ref: '#/components/parameters/ProjectIdentifier' From 31f34d8096c2b5bda2a25cd59c4678b77e1e755c Mon Sep 17 00:00:00 2001 From: IThundxr Date: Tue, 5 Aug 2025 15:54:53 -0400 Subject: [PATCH 3/4] Run cargo fmt --- apps/labrinth/src/routes/v3/projects.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 09f252d1a7..ad3f5d9356 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -898,9 +898,9 @@ pub async fn project_edit( project_item.inner.slug, None, None, - &redis, - ) - .await?; + &redis, + ) + .await?; // Remove no longer searchable projects from search index if let (true, Some(false)) = ( From 0c211b79993955e2ce5e500bcba0a7b8cbca576c Mon Sep 17 00:00:00 2001 From: IThundxr Date: Sat, 27 Sep 2025 10:14:41 -0400 Subject: [PATCH 4/4] Remove comment Signed-off-by: IThundxr --- apps/labrinth/src/database/models/project_item.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/labrinth/src/database/models/project_item.rs b/apps/labrinth/src/database/models/project_item.rs index 3c432eb791..ff221b9a75 100644 --- a/apps/labrinth/src/database/models/project_item.rs +++ b/apps/labrinth/src/database/models/project_item.rs @@ -973,7 +973,6 @@ impl DBProject { return Ok(dependents); } - // SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id let dependents: Dependents = sqlx::query!( " SELECT DISTINCT version.id version_id, mod.id FROM versions version