Skip to content

Commit 8239f9d

Browse files
authored
feat: add status filter (CM-1236) (#4233)
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent cf60c8f commit 8239f9d

4 files changed

Lines changed: 51 additions & 31 deletions

File tree

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import type { Request, Response } from 'express'
2+
import { z } from 'zod'
23

34
import { listPackagesForScatter } from '@crowd/data-access-layer'
45

56
import { getPackagesQx } from '@/db/packagesDb'
67
import { ok } from '@/utils/api'
8+
import { validateOrThrow } from '@/utils/validation'
9+
10+
import { STEWARDSHIP_STATUS_VALUES } from '../packages/types'
11+
12+
const scatterQuerySchema = z.object({
13+
status: z.enum(STEWARDSHIP_STATUS_VALUES).optional(),
14+
})
715

816
export async function packageScatterHandler(req: Request, res: Response): Promise<void> {
17+
const { status } = validateOrThrow(scatterQuerySchema, req.query)
918
const qx = await getPackagesQx()
10-
const points = await listPackagesForScatter(qx)
19+
const points = await listPackagesForScatter(qx, { status })
1120
ok(res, { points, total: points.length })
1221
}

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

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

10-
import type { StewardshipStatus } from './types'
10+
import { STEWARDSHIP_STATUS_VALUES, type StewardshipStatus } from './types'
1111

1212
const DEFAULT_PAGE_SIZE = 20
1313
const MAX_PAGE_SIZE = 100
1414

1515
const booleanQueryParam = z.preprocess((v) => v === 'true', z.boolean()).default(false)
1616

1717
const lifecycleValues = ['active', 'stable', 'declining', 'abandoned'] as const
18-
const stewardshipStatusValues = [
19-
'unassigned',
20-
'open',
21-
'assessing',
22-
'active',
23-
'needs_attention',
24-
'escalated',
25-
'blocked',
26-
'inactive',
27-
] as const
2818
const healthBandValues = ['healthy', 'fair', 'concerning', 'critical'] as const
2919
const vulnSeverityValues = ['any', 'high', 'critical', 'none'] as const
3020

@@ -34,7 +24,7 @@ const querySchema = z.object({
3424
ecosystem: z.string().trim().optional(),
3525
lifecycle: z.enum(lifecycleValues).optional(),
3626
name: z.string().trim().optional(),
37-
status: z.enum(stewardshipStatusValues).optional(),
27+
status: z.enum(STEWARDSHIP_STATUS_VALUES).optional(),
3828
healthBand: z.enum(healthBandValues).optional(),
3929
vulnSeverity: z.enum(vulnSeverityValues).optional(),
4030
busFactor1Only: booleanQueryParam,

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
export type StewardshipStatus =
2-
| 'unassigned'
3-
| 'open'
4-
| 'assessing'
5-
| 'active'
6-
| 'needs_attention'
7-
| 'escalated'
8-
| 'blocked'
9-
| 'inactive'
1+
export const STEWARDSHIP_STATUS_VALUES = [
2+
'unassigned',
3+
'open',
4+
'assessing',
5+
'active',
6+
'needs_attention',
7+
'escalated',
8+
'blocked',
9+
'inactive',
10+
] as const
11+
12+
export type StewardshipStatus = (typeof STEWARDSHIP_STATUS_VALUES)[number]
1013

1114
export type Lifecycle = 'active' | 'stable' | 'declining' | 'abandoned'
1215

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

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,21 @@ export interface ScatterPoint {
665665
advisoryCount: number
666666
}
667667

668-
export async function listPackagesForScatter(qx: QueryExecutor): Promise<ScatterPoint[]> {
668+
export async function listPackagesForScatter(
669+
qx: QueryExecutor,
670+
options: { status?: string } = {},
671+
): Promise<ScatterPoint[]> {
672+
const { status } = options
673+
674+
// 'unassigned' covers packages with no stewardship row (s.id IS NULL) in addition
675+
// to rows explicitly marked unassigned. All other statuses filter via s.status directly.
676+
// The query always uses LEFT JOIN — the filter is applied in the WHERE clause, not the join.
677+
const statusFilter = status
678+
? status === 'unassigned'
679+
? `AND (s.status = 'unassigned' OR s.id IS NULL)`
680+
: `AND s.status = $(status)`
681+
: ''
682+
669683
const rows: Array<{
670684
purl: string
671685
name: string
@@ -675,16 +689,17 @@ export async function listPackagesForScatter(qx: QueryExecutor): Promise<Scatter
675689
stewardshipId: string | null
676690
stewardshipStatus: string | null
677691
openVulns: number
678-
}> = await qx.select(`
692+
}> = await qx.select(
693+
`
679694
SELECT
680695
p.purl,
681696
p.name,
682-
ROUND(COALESCE(p.impact, 0) * 100)::int AS "criticalityScore",
683-
ROUND(COALESCE(r_sc.scorecard_score, 0) * 10)::int AS "healthScore",
684-
r_sc.scorecard_score AS "scorecardScoreRaw",
685-
s.id::text AS "stewardshipId",
686-
s.status AS "stewardshipStatus",
687-
COALESCE(ap_counts.cnt, 0) AS "openVulns"
697+
ROUND(COALESCE(p.impact, 0) * 100)::int AS "criticalityScore",
698+
ROUND(COALESCE(r_sc.scorecard_score, 0) * 10)::int AS "healthScore",
699+
r_sc.scorecard_score AS "scorecardScoreRaw",
700+
s.id::text AS "stewardshipId",
701+
s.status AS "stewardshipStatus",
702+
COALESCE(ap_counts.cnt, 0) AS "openVulns"
688703
FROM packages p
689704
LEFT JOIN stewardships s ON s.package_id = p.id
690705
LEFT JOIN LATERAL (
@@ -699,9 +714,12 @@ export async function listPackagesForScatter(qx: QueryExecutor): Promise<Scatter
699714
LIMIT 1
700715
) r_sc ON true
701716
WHERE p.is_critical = true
717+
${statusFilter}
702718
ORDER BY p.impact DESC NULLS LAST, p.purl ASC
703719
LIMIT 2000
704-
`)
720+
`,
721+
{ status },
722+
)
705723

706724
return rows.map((r) => ({
707725
purl: r.purl,

0 commit comments

Comments
 (0)