@@ -154,6 +154,7 @@ export interface ListPackagesOptions {
154154 ecosystem ?: string
155155 lifecycle ?: string
156156 name ?: string
157+ purl ?: string
157158 status ?: string
158159 healthBand ?: HealthBand
159160 vulnSeverity ?: VulnSeverityFilter
@@ -226,6 +227,11 @@ export async function getPackageStatusCounts(
226227 params . name = `%${ opts . name } %`
227228 }
228229
230+ if ( opts . purl ) {
231+ conditions . push ( 'p.purl ILIKE $(purl)' )
232+ params . purl = `%${ opts . purl } %`
233+ }
234+
229235 if ( opts . lifecycle ) {
230236 conditions . push ( 'p.status IS NOT NULL' )
231237 }
@@ -344,6 +350,11 @@ export async function listPackagesForApi(
344350 params . name = `%${ opts . name } %`
345351 }
346352
353+ if ( opts . purl ) {
354+ conditions . push ( 'p.purl ILIKE $(purl)' )
355+ params . purl = `%${ opts . purl } %`
356+ }
357+
347358 // Exclude packages with no registry status when a lifecycle filter is active.
348359 // Full lifecycle column support is pending; this prevents null-lifecycle rows
349360 // from leaking into filtered results.
@@ -429,7 +440,24 @@ export async function listPackagesForApi(
429440 const sortDir = opts . sortDir === 'desc' ? 'DESC' : 'ASC'
430441
431442 // Separate paginated params from filter-only params used by the fallback COUNT query
432- const queryParams = { ...params , limit : opts . pageSize , offset : ( opts . page - 1 ) * opts . pageSize }
443+ const queryParams : Record < string , unknown > = {
444+ ...params ,
445+ limit : opts . pageSize ,
446+ offset : ( opts . page - 1 ) * opts . pageSize ,
447+ }
448+
449+ // Float exact name/purl matches to the top while still returning all partial matches via ILIKE.
450+ const exactParts : string [ ] = [ ]
451+ if ( opts . name ) {
452+ exactParts . push ( 'p.name ILIKE $(name_exact)' )
453+ queryParams . name_exact = opts . name
454+ }
455+ if ( opts . purl ) {
456+ exactParts . push ( 'p.purl ILIKE $(purl_exact)' )
457+ queryParams . purl_exact = opts . purl
458+ }
459+ const exactSort =
460+ exactParts . length > 0 ? `CASE WHEN ${ exactParts . join ( ' OR ' ) } THEN 0 ELSE 1 END` : ''
433461
434462 // Laterals needed for WHERE filter conditions — included in both the main query and the COUNT fallback.
435463 const filterLaterals = `
@@ -522,7 +550,7 @@ export async function listPackagesForApi(
522550 FROM packages p
523551 ${ laterals }
524552 ${ where }
525- ORDER BY ${ sortExpr } ${ sortDir } NULLS LAST, p.purl ${ sortDir }
553+ ORDER BY ${ [ exactSort , ` ${ sortExpr } ${ sortDir } NULLS LAST` , ` p.purl ${ sortDir } ` ] . filter ( Boolean ) . join ( ', ' ) }
526554 LIMIT $(limit) OFFSET $(offset)
527555 ` ,
528556 queryParams ,
0 commit comments