Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions crates/api/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use axum::{
};
use common::{
AgenticThreatSummary, BulkLookupRequest, CveSummary, MaintainerInfo, PackageCapabilities,
PackageListItem, PackageListResponse, PackageResponse, PaginationParams, PublisherInfo,
Registry, ScanJob, ScanPriority, ScanRequest, ScanRequestResponse,
PackageListItem, PackageListResponse, PackageResponse, PackageSortBy, PaginationParams,
PublisherInfo, Registry, ScanJob, ScanPriority, ScanRequest, ScanRequestResponse,
};
use serde_json::json;
use std::io::Write;
Expand All @@ -29,6 +29,17 @@ pub async fn list_packages(
let limit = params.limit.unwrap_or(50).min(100); // Default 50, max 100
let offset = params.offset.unwrap_or(0);

let sort_by = match params.sort_by.as_deref() {
None | Some("weekly_downloads") => PackageSortBy::WeeklyDownloads,
Some("scanned_at") => PackageSortBy::ScannedAt,
Some(_) => {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({ "error": "invalid sort_by value" })),
));
}
};

let latest = params.latest.unwrap_or(false);
let registry = params.registry;
let risk_level = params.risk_level;
Expand All @@ -37,23 +48,23 @@ pub async fn list_packages(
if latest {
state
.db
.search_packages_latest(q, limit, offset, registry, risk_level)
.search_packages_latest(q, limit, offset, registry, risk_level, sort_by)
.await
} else {
state
.db
.search_packages(q, limit, offset, registry, risk_level)
.search_packages(q, limit, offset, registry, risk_level, sort_by)
.await
}
} else if latest {
state
.db
.get_packages_paginated_latest(limit, offset, registry, risk_level)
.get_packages_paginated_latest(limit, offset, registry, risk_level, sort_by)
.await
} else {
state
.db
.get_packages_paginated(limit, offset, registry, risk_level)
.get_packages_paginated(limit, offset, registry, risk_level, sort_by)
.await
}
.map_err(|e| {
Expand Down
142 changes: 88 additions & 54 deletions crates/common/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,16 @@ impl Database {
offset: i64,
registry: Option<Registry>,
risk_level: Option<RiskLevel>,
sort_by: PackageSortBy,
) -> Result<(Vec<PackageWithCounts>, i64)> {
let registry_str = registry.map(|r| r.to_string());
let risk_level_str = risk_level.map(|r| r.to_string());
let order_clause = match sort_by {
PackageSortBy::WeeklyDownloads => "p.weekly_downloads DESC NULLS LAST, p.name ASC",
PackageSortBy::ScannedAt => "p.scanned_at DESC",
};

let packages: Vec<PackageWithCounts> = sqlx::query_as(
let sql = format!(
r#"
SELECT
p.id, p.name, p.version, p.registry, p.risk_level, p.trust_score,
Expand All @@ -363,16 +368,19 @@ impl Database {
FROM packages p
WHERE ($3::text IS NULL OR p.registry = $3)
AND ($4::text IS NULL OR p.risk_level = $4)
ORDER BY p.weekly_downloads DESC NULLS LAST, p.name ASC
ORDER BY {}
LIMIT $1 OFFSET $2
"#,
)
.bind(limit)
.bind(offset)
.bind(&registry_str)
.bind(&risk_level_str)
.fetch_all(&self.pool)
.await?;
order_clause
);

let packages: Vec<PackageWithCounts> = sqlx::query_as(&sql)
.bind(limit)
.bind(offset)
.bind(&registry_str)
.bind(&risk_level_str)
.fetch_all(&self.pool)
.await?;

let total: (i64,) = sqlx::query_as(
r#"
Expand All @@ -398,12 +406,25 @@ impl Database {
offset: i64,
registry: Option<Registry>,
risk_level: Option<RiskLevel>,
sort_by: PackageSortBy,
) -> Result<(Vec<PackageWithCounts>, i64)> {
let pattern = format!("%{}%", query);
let registry_str = registry.map(|r| r.to_string());
let risk_level_str = risk_level.map(|r| r.to_string());
let order_clause = match sort_by {
PackageSortBy::WeeklyDownloads => {
"CASE \
WHEN LOWER(p.name) = LOWER($2) THEN 0 \
WHEN LOWER(p.name) LIKE LOWER($2) || '%' THEN 1 \
ELSE 2 \
END, \
p.weekly_downloads DESC NULLS LAST, \
p.name ASC"
}
PackageSortBy::ScannedAt => "p.scanned_at DESC",
};

let packages: Vec<PackageWithCounts> = sqlx::query_as(
let sql = format!(
r#"
SELECT
p.id, p.name, p.version, p.registry, p.risk_level, p.trust_score,
Expand All @@ -414,25 +435,21 @@ impl Database {
WHERE p.name ILIKE $1
AND ($5::text IS NULL OR p.registry = $5)
AND ($6::text IS NULL OR p.risk_level = $6)
ORDER BY
CASE
WHEN LOWER(p.name) = LOWER($2) THEN 0
WHEN LOWER(p.name) LIKE LOWER($2) || '%' THEN 1
ELSE 2
END,
p.weekly_downloads DESC NULLS LAST,
p.name ASC
ORDER BY {}
LIMIT $3 OFFSET $4
"#,
)
.bind(&pattern)
.bind(query)
.bind(limit)
.bind(offset)
.bind(&registry_str)
.bind(&risk_level_str)
.fetch_all(&self.pool)
.await?;
order_clause
);

let packages: Vec<PackageWithCounts> = sqlx::query_as(&sql)
.bind(&pattern)
.bind(query)
.bind(limit)
.bind(offset)
.bind(&registry_str)
.bind(&risk_level_str)
.fetch_all(&self.pool)
.await?;

let total: (i64,) = sqlx::query_as(
r#"
Expand All @@ -458,11 +475,16 @@ impl Database {
offset: i64,
registry: Option<Registry>,
risk_level: Option<RiskLevel>,
sort_by: PackageSortBy,
) -> Result<(Vec<PackageWithCounts>, i64)> {
let registry_str = registry.map(|r| r.to_string());
let risk_level_str = risk_level.map(|r| r.to_string());
let order_clause = match sort_by {
PackageSortBy::WeeklyDownloads => "p.weekly_downloads DESC NULLS LAST, p.name ASC",
PackageSortBy::ScannedAt => "p.scanned_at DESC",
};

let packages: Vec<PackageWithCounts> = sqlx::query_as(
let sql = format!(
r#"
WITH latest AS (
SELECT DISTINCT ON (name, registry) *
Expand All @@ -477,16 +499,19 @@ impl Database {
COALESCE((SELECT COUNT(*) FROM agentic_threats WHERE package_id = p.id AND verification_status = 'verified'), 0) as threat_count
FROM latest p
WHERE ($4::text IS NULL OR p.risk_level = $4)
ORDER BY p.weekly_downloads DESC NULLS LAST, p.name ASC
ORDER BY {}
LIMIT $1 OFFSET $2
"#,
)
.bind(limit)
.bind(offset)
.bind(&registry_str)
.bind(&risk_level_str)
.fetch_all(&self.pool)
.await?;
order_clause
);

let packages: Vec<PackageWithCounts> = sqlx::query_as(&sql)
.bind(limit)
.bind(offset)
.bind(&registry_str)
.bind(&risk_level_str)
.fetch_all(&self.pool)
.await?;

let total: (i64,) = sqlx::query_as(
r#"
Expand Down Expand Up @@ -515,12 +540,25 @@ impl Database {
offset: i64,
registry: Option<Registry>,
risk_level: Option<RiskLevel>,
sort_by: PackageSortBy,
) -> Result<(Vec<PackageWithCounts>, i64)> {
let pattern = format!("%{}%", query);
let registry_str = registry.map(|r| r.to_string());
let risk_level_str = risk_level.map(|r| r.to_string());
let order_clause = match sort_by {
PackageSortBy::WeeklyDownloads => {
"CASE \
WHEN LOWER(p.name) = LOWER($2) THEN 0 \
WHEN LOWER(p.name) LIKE LOWER($2) || '%' THEN 1 \
ELSE 2 \
END, \
p.weekly_downloads DESC NULLS LAST, \
p.name ASC"
}
PackageSortBy::ScannedAt => "p.scanned_at DESC",
};

let packages: Vec<PackageWithCounts> = sqlx::query_as(
let sql = format!(
r#"
WITH latest AS (
SELECT DISTINCT ON (name, registry) *
Expand All @@ -536,25 +574,21 @@ impl Database {
COALESCE((SELECT COUNT(*) FROM agentic_threats WHERE package_id = p.id AND verification_status = 'verified'), 0) as threat_count
FROM latest p
WHERE ($6::text IS NULL OR p.risk_level = $6)
ORDER BY
CASE
WHEN LOWER(p.name) = LOWER($2) THEN 0
WHEN LOWER(p.name) LIKE LOWER($2) || '%' THEN 1
ELSE 2
END,
p.weekly_downloads DESC NULLS LAST,
p.name ASC
ORDER BY {}
LIMIT $3 OFFSET $4
"#,
)
.bind(&pattern)
.bind(query)
.bind(limit)
.bind(offset)
.bind(&registry_str)
.bind(&risk_level_str)
.fetch_all(&self.pool)
.await?;
order_clause
);

let packages: Vec<PackageWithCounts> = sqlx::query_as(&sql)
.bind(&pattern)
.bind(query)
.bind(limit)
.bind(offset)
.bind(&registry_str)
.bind(&risk_level_str)
.fetch_all(&self.pool)
.await?;

let total: (i64,) = sqlx::query_as(
r#"
Expand Down
10 changes: 10 additions & 0 deletions crates/common/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,14 @@ pub struct PackageListResponse {
pub offset: i64,
}

/// Sort order for package list queries
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PackageSortBy {
#[default]
WeeklyDownloads,
ScannedAt,
}

/// Pagination query parameters
#[derive(Debug, Clone, Deserialize)]
pub struct PaginationParams {
Expand All @@ -445,6 +453,8 @@ pub struct PaginationParams {
pub registry: Option<Registry>,
/// Filter by risk level (clean, warning, critical)
pub risk_level: Option<RiskLevel>,
/// Sort order: "weekly_downloads" (default) or "scanned_at"
pub sort_by: Option<String>,
}

/// Full package response for API
Expand Down