Skip to content

Commit 4bafe13

Browse files
authored
fix: filters (CM-1259) (#4249)
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 53fa333 commit 4bafe13

4 files changed

Lines changed: 48 additions & 7 deletions

File tree

backend/src/api/public/v1/ossprey/packageList.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { getPackagesQx } from '@/db/packagesDb'
1212
import { ok } from '@/utils/api'
1313
import { validateOrThrow } from '@/utils/validation'
1414

15+
import { purlFilterSchema } from '../packages/purl'
16+
1517
const MAX_PAGE_SIZE = 250
1618

1719
const boolParam = z.preprocess((v) => v === 'true', z.boolean()).default(false)
@@ -22,6 +24,7 @@ const querySchema = z.object({
2224
ecosystem: z.string().trim().optional(),
2325
lifecycle: z.enum(['active', 'stable', 'declining', 'abandoned']).optional(),
2426
name: z.string().trim().optional(),
27+
purl: purlFilterSchema,
2528
status: z
2629
.enum([
2730
'unassigned',
@@ -50,6 +53,7 @@ export async function packageListHandler(req: Request, res: Response): Promise<v
5053
ecosystem: params.ecosystem,
5154
lifecycle: params.lifecycle,
5255
name: params.name,
56+
purl: params.purl,
5357
healthBand: params.healthBand,
5458
vulnSeverity: params.vulnSeverity,
5559
staleOnly: params.staleOnly,

backend/src/api/public/v1/packages/listPackages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getPackagesQx } from '@/db/packagesDb'
77
import { ok } from '@/utils/api'
88
import { validateOrThrow } from '@/utils/validation'
99

10+
import { purlFilterSchema } from './purl'
1011
import { STEWARDSHIP_STATUS_VALUES, type StewardshipStatus } from './types'
1112

1213
const DEFAULT_PAGE_SIZE = 20
@@ -24,6 +25,7 @@ const querySchema = z.object({
2425
ecosystem: z.string().trim().optional(),
2526
lifecycle: z.enum(lifecycleValues).optional(),
2627
name: z.string().trim().optional(),
28+
purl: purlFilterSchema,
2729
status: z.enum(STEWARDSHIP_STATUS_VALUES).optional(),
2830
healthBand: z.enum(healthBandValues).optional(),
2931
vulnSeverity: z.enum(vulnSeverityValues).optional(),
@@ -41,6 +43,7 @@ export async function listPackages(req: Request, res: Response): Promise<void> {
4143
ecosystem,
4244
lifecycle,
4345
name,
46+
purl,
4447
status,
4548
healthBand,
4649
vulnSeverity,
@@ -55,6 +58,7 @@ export async function listPackages(req: Request, res: Response): Promise<void> {
5558
ecosystem,
5659
lifecycle,
5760
name,
61+
purl,
5862
healthBand,
5963
vulnSeverity,
6064
staleOnly,
@@ -91,6 +95,7 @@ export async function listPackages(req: Request, res: Response): Promise<void> {
9195
ecosystem: ecosystem ?? null,
9296
lifecycle: lifecycle ?? null,
9397
name: name ?? null,
98+
purl: purl ?? null,
9499
status: status ?? null,
95100
healthBand: healthBand ?? null,
96101
vulnSeverity: vulnSeverity ?? null,

backend/src/api/public/v1/packages/purl.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
*/
1616
import { z } from 'zod'
1717

18+
export function normalizePurl(purl: string): string {
19+
const withoutQualifiers = purl.replace(/[?#].*$/, '')
20+
const withoutVersion = withoutQualifiers.replace(/@[^/@]+$/, '')
21+
return withoutVersion.replace(/@/g, '%40')
22+
}
23+
1824
export const purlFieldSchema = z
1925
.string()
2026
.trim()
@@ -24,8 +30,6 @@ export const purlFieldSchema = z
2430

2531
export const purlQuerySchema = z.object({ purl: purlFieldSchema })
2632

27-
export function normalizePurl(purl: string): string {
28-
const withoutQualifiers = purl.replace(/[?#].*$/, '')
29-
const withoutVersion = withoutQualifiers.replace(/@[^/@]+$/, '')
30-
return withoutVersion.replace(/@/g, '%40')
31-
}
33+
// Loose schema for search filters: normalizes without requiring the pkg: prefix,
34+
// so partial inputs (e.g. "@babel/core" or "lodash") are accepted.
35+
export const purlFilterSchema = z.string().trim().transform(normalizePurl).optional()

services/libs/data-access-layer/src/osspckgs/api.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)