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' 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 362907bda6..ff221b9a75 100644 --- a/apps/labrinth/src/database/models/project_item.rs +++ b/apps/labrinth/src/database/models/project_item.rs @@ -21,6 +21,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 { @@ -351,8 +352,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!( " @@ -944,10 +951,59 @@ 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); + } + + 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?; @@ -964,6 +1020,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 0aae95b29f..2631304944 100644 --- a/apps/labrinth/src/database/models/version_item.rs +++ b/apps/labrinth/src/database/models/version_item.rs @@ -449,6 +449,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 c7f27be2e7..6e50d24751 100644 --- a/apps/labrinth/src/queue/moderation.rs +++ b/apps/labrinth/src/queue/moderation.rs @@ -702,6 +702,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 c4a91b8779..8bf75fb41f 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 da726d8309..f943ccd7c6 100644 --- a/apps/labrinth/src/routes/v3/organizations.rs +++ b/apps/labrinth/src/routes/v3/organizations.rs @@ -831,6 +831,7 @@ pub async fn organization_projects_add( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1019,6 +1020,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 c6be6e5a66..ad3f5d9356 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -74,7 +74,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)), ), ); } @@ -896,6 +897,7 @@ pub async fn project_edit( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1103,6 +1105,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>, @@ -1337,6 +1415,7 @@ pub async fn projects_edit( project.inner.id, project.inner.slug, None, + None, &redis, ) .await?; @@ -1532,6 +1611,7 @@ pub async fn project_icon_edit( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1622,6 +1702,7 @@ pub async fn delete_project_icon( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1778,6 +1859,7 @@ pub async fn add_gallery_item( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -1961,6 +2043,7 @@ pub async fn edit_gallery_item( project_item.inner.id, project_item.inner.slug, None, + None, &redis, ) .await?; @@ -2076,6 +2159,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 d992fd6ca5..b3d9b06924 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 5a921a52fa..4dc839b517 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?; @@ -963,6 +965,7 @@ pub async fn version_delete( version.inner.project_id, None, Some(true), + Some(true), &redis, ) .await?;