refactor: Adjust API to return project ids instead of direct count

This commit is contained in:
Blodhgarm
2026-08-25 18:49:54 -05:00
parent fba6ee994d
commit 6c3f09f39a
@@ -1680,22 +1680,23 @@ pub async fn delete_all_locks(
Ok(web::Json(DeleteAllLocksResponse { deleted_count })) Ok(web::Json(DeleteAllLocksResponse { deleted_count }))
} }
/// Get counts of each `ProjectStatus` across a user's projects. Only /// Get project id's for a given user with them grouped by their `ProjectStatus`.
/// statuses with at least one project are present in the map. ///
/// Only statuses with at least one project are present in the map.
#[utoipa::path( #[utoipa::path(
context_path = "/moderation", context_path = "/moderation",
tag = "moderation", tag = "moderation",
security(("bearer_auth" = [])), security(("bearer_auth" = [])),
responses((status = OK, body = HashMap<ProjectStatus, u32>)) responses((status = OK, body = HashMap<ProjectStatus, Vec<ProjectId>>))
)] )]
#[get("/user/{user_id}/project-status-counts")] #[get("/user/{user_id}/all-projects-grouped")]
pub async fn get_user_project_status_counts( pub async fn get_user_project_grouped(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>, session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<HashMap<ProjectStatus, u32>>, ApiError> { ) -> Result<web::Json<HashMap<ProjectStatus, Vec<ProjectId>>>, ApiError> {
check_is_moderator_from_headers( check_is_moderator_from_headers(
&req, &req,
&**pool, &**pool,
@@ -1713,17 +1714,13 @@ pub async fn get_user_project_status_counts(
.wrap_not_found_err("resource not found")?; .wrap_not_found_err("resource not found")?;
let counts = let counts =
user_project_status_counts(target_user.id, &**pool, &redis).await?; user_projects_status_grouped(target_user.id, &**pool, &redis).await?;
Ok(web::Json(counts)) Ok(web::Json(counts))
} }
#[derive(Debug, Deserialize)] /// Get project id's for a list of user's with them grouped by their `ProjectStatus`.
pub struct ProjectStatusCountsUserIds { ///
pub ids: String,
}
/// Get counts of each `ProjectStatus` across multiple users' projects.
/// Users that don't exist are silently omitted from the response; users /// Users that don't exist are silently omitted from the response; users
/// that exist but have no projects are included with an empty map. /// that exist but have no projects are included with an empty map.
#[utoipa::path( #[utoipa::path(
@@ -1731,16 +1728,16 @@ pub struct ProjectStatusCountsUserIds {
tag = "moderation", tag = "moderation",
security(("bearer_auth" = [])), security(("bearer_auth" = [])),
params(("ids" = String, Query)), params(("ids" = String, Query)),
responses((status = OK, body = HashMap<UserId, HashMap<ProjectStatus, u32>>)) responses((status = OK, body = HashMap<UserId, HashMap<ProjectStatus, Vec<ProjectId>>>))
)] )]
#[get("/users/project-status-counts")] #[get("/users/all-projects-grouped")]
pub async fn get_users_project_status_counts( pub async fn get_users_project_grouped(
req: HttpRequest, req: HttpRequest,
ids: web::Query<ProjectStatusCountsUserIds>, ids: web::Query<UserIds>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>, session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<HashMap<UserId, HashMap<ProjectStatus, u32>>>, ApiError> ) -> Result<web::Json<HashMap<UserId, HashMap<ProjectStatus, Vec<ProjectId>>>>, ApiError>
{ {
check_is_moderator_from_headers( check_is_moderator_from_headers(
&req, &req,
@@ -1767,9 +1764,9 @@ pub async fn get_users_project_status_counts(
let pool_ref = &**pool; let pool_ref = &**pool;
let redis_ref = &*redis; let redis_ref = &*redis;
let counts_by_user = try_join_all(target_users.into_iter().map( let grouped_projects_by_user = try_join_all(target_users.into_iter().map(
|target_user| async move { |target_user| async move {
let counts = user_project_status_counts( let counts = user_projects_status_grouped(
target_user.id, target_user.id,
pool_ref, pool_ref,
redis_ref, redis_ref,
@@ -1783,25 +1780,26 @@ pub async fn get_users_project_status_counts(
.into_iter() .into_iter()
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
Ok(web::Json(counts_by_user)) Ok(web::Json(grouped_projects_by_user))
} }
/// Get counts of each `ProjectStatus` across an organization's projects. /// Get project id's for a given organization with them grouped by their `ProjectStatus`.
///
/// Only statuses with at least one project are present in the map. /// Only statuses with at least one project are present in the map.
#[utoipa::path( #[utoipa::path(
context_path = "/moderation", context_path = "/moderation",
tag = "moderation", tag = "moderation",
security(("bearer_auth" = [])), security(("bearer_auth" = [])),
responses((status = OK, body = HashMap<ProjectStatus, u32>)) responses((status = OK, body = HashMap<ProjectStatus, Vec<ProjectId>>))
)] )]
#[get("/organization/{organization_id}/project-status-counts")] #[get("/organization/{organization_id}/all-projects-grouped")]
pub async fn get_organization_project_status_counts( pub async fn get_organization_project_grouped(
req: HttpRequest, req: HttpRequest,
info: web::Path<(String,)>, info: web::Path<(String,)>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>, session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<HashMap<ProjectStatus, u32>>, ApiError> { ) -> Result<web::Json<HashMap<ProjectStatus, Vec<ProjectId>>>, ApiError> {
check_is_moderator_from_headers( check_is_moderator_from_headers(
&req, &req,
&**pool, &**pool,
@@ -1821,18 +1819,19 @@ pub async fn get_organization_project_status_counts(
.wrap_internal_err("fetching organization from database")? .wrap_internal_err("fetching organization from database")?
.wrap_not_found_err("resource not found")?; .wrap_not_found_err("resource not found")?;
let counts = organization_project_status_counts( let grouped_projects = organization_projects_status_grouped(
target_org.id, target_org.id,
&**pool, &**pool,
&redis, &redis,
) )
.await?; .await?;
Ok(web::Json(counts)) Ok(web::Json(grouped_projects))
} }
/// Get counts of each `ProjectStatus` across multiple organizations' /// Get project id's for a list of organization's with them grouped by their `ProjectStatus`.
/// projects. Organizations that don't exist are silently omitted from the ///
/// Organizations that don't exist are silently omitted from the
/// response; organizations that exist but have no projects are included /// response; organizations that exist but have no projects are included
/// with an empty map. /// with an empty map.
#[utoipa::path( #[utoipa::path(
@@ -1840,17 +1839,17 @@ pub async fn get_organization_project_status_counts(
tag = "moderation", tag = "moderation",
security(("bearer_auth" = [])), security(("bearer_auth" = [])),
params(("ids" = String, Query)), params(("ids" = String, Query)),
responses((status = OK, body = HashMap<OrganizationId, HashMap<ProjectStatus, u32>>)) responses((status = OK, body = HashMap<OrganizationId, HashMap<ProjectStatus, Vec<ProjectId>>>))
)] )]
#[get("/organizations/project-status-counts")] #[get("/organizations/all-projects-grouped")]
pub async fn get_organizations_project_status_counts( pub async fn get_organizations_project_grouped(
req: HttpRequest, req: HttpRequest,
ids: web::Query<OrganizationIds>, ids: web::Query<OrganizationIds>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
redis: web::Data<RedisPool>, redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>, session_queue: web::Data<AuthQueue>,
) -> Result< ) -> Result<
web::Json<HashMap<OrganizationId, HashMap<ProjectStatus, u32>>>, web::Json<HashMap<OrganizationId, HashMap<ProjectStatus, Vec<ProjectId>>>>,
ApiError, ApiError,
> { > {
check_is_moderator_from_headers( check_is_moderator_from_headers(
@@ -1881,9 +1880,9 @@ pub async fn get_organizations_project_status_counts(
let pool_ref = &**pool; let pool_ref = &**pool;
let redis_ref = &*redis; let redis_ref = &*redis;
let counts_by_org = try_join_all(target_orgs.into_iter().map( let grouped_projects_by_org = try_join_all(target_orgs.into_iter().map(
|target_org| async move { |target_org| async move {
let counts = organization_project_status_counts( let counts = organization_projects_status_grouped(
target_org.id, target_org.id,
pool_ref, pool_ref,
redis_ref, redis_ref,
@@ -1897,37 +1896,35 @@ pub async fn get_organizations_project_status_counts(
.into_iter() .into_iter()
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
Ok(web::Json(counts_by_org)) Ok(web::Json(grouped_projects_by_org))
} }
/// Tallies a single user's projects by `ProjectStatus`. Shared by the /// Groups the given User projects by their `ProjectStatus`.
/// single- and multi-user project-status-count endpoints. async fn user_projects_status_grouped<'a, E>(
async fn user_project_status_counts<'a, E>(
user_id: database::models::DBUserId, user_id: database::models::DBUserId,
pool: E, pool: E,
redis: &RedisPool, redis: &RedisPool,
) -> Result<HashMap<ProjectStatus, u32>, ApiError> ) -> Result<HashMap<ProjectStatus, Vec<ProjectId>>, ApiError>
where where
E: database::Executor<'a, Database = sqlx::Postgres> E: database::Executor<'a, Database = sqlx::Postgres>
+ database::Acquire<'a, Database = sqlx::Postgres> + database::Acquire<'a, Database = sqlx::Postgres>
+ Copy, + Copy,
{ {
let project_ids = let project_ids =
models::DBUser::get_projects(user_id, pool, redis) database::models::DBUser::get_projects(user_id, pool, redis)
.await .await
.wrap_internal_err("fetching user's projects from database")?; .wrap_internal_err("fetching user's projects from database")?;
project_status_counts_for(&project_ids, pool, redis).await grouped_projects_for(&project_ids, pool, redis).await
} }
// Tallies a single organization's projects by `ProjectStatus`. Shared by /// Groups the given Organization projects by their `ProjectStatus`.
/// the single- and multi-organization project-status-count endpoints. async fn organization_projects_status_grouped<'a, E>(
async fn organization_project_status_counts<'a, E>(
organization_id: DBOrganizationId, organization_id: DBOrganizationId,
pool: E, pool: E,
redis: &RedisPool, redis: &RedisPool,
) -> Result<HashMap<ProjectStatus, u32>, ApiError> ) -> Result<HashMap<ProjectStatus, Vec<ProjectId>>, ApiError>
where where
E: database::Executor<'a, Database = sqlx::Postgres> E: database::Executor<'a, Database = sqlx::Postgres>
+ database::Acquire<'a, Database = sqlx::Postgres> + database::Acquire<'a, Database = sqlx::Postgres>
@@ -1937,16 +1934,15 @@ where
.await .await
.wrap_internal_err("fetching project IDs from database")?; .wrap_internal_err("fetching project IDs from database")?;
project_status_counts_for(&project_ids, pool, redis).await grouped_projects_for(&project_ids, pool, redis).await
} }
/// Tallies `ProjectStatus` counts across the given projects. Shared by the /// Groups the given input Projects by their `ProjectStatus`.
/// user- and organization-scoped project-status-count helpers above. async fn grouped_projects_for<'a, E>(
async fn project_status_counts_for<'a, E>(
project_ids: &[DBProjectId], project_ids: &[DBProjectId],
pool: E, pool: E,
redis: &RedisPool, redis: &RedisPool,
) -> Result<HashMap<ProjectStatus, u32>, ApiError> ) -> Result<HashMap<ProjectStatus, Vec<ProjectId>>, ApiError>
where where
E: database::Executor<'a, Database = sqlx::Postgres> E: database::Executor<'a, Database = sqlx::Postgres>
+ database::Acquire<'a, Database = sqlx::Postgres> + database::Acquire<'a, Database = sqlx::Postgres>
@@ -1961,10 +1957,10 @@ where
.await .await
.wrap_internal_err("fetching projects from database")?; .wrap_internal_err("fetching projects from database")?;
let mut counts = HashMap::new(); let mut grouped_projects = HashMap::new();
for project in &projects { for project in &projects {
*counts.entry(project.inner.status).or_insert(0u32) += 1; *grouped_projects.entry(project.inner.status).or_default().push(project.inner.id.into());
} }
Ok(counts) Ok(grouped_projects)
} }