Skip to content

Commit b7d97d5

Browse files
authored
feat: add per-status package counts to list endpoint (CM-1245) (#4216)
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
1 parent 84a58c7 commit b7d97d5

3 files changed

Lines changed: 160 additions & 10 deletions

File tree

backend/.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ dist
22
node_modules
33
admin
44
venv-*
5-
.serverless
5+
.serverless
6+
.claude

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Request, Response } from 'express'
22
import { z } from 'zod'
33

4-
import { listPackagesForApi } from '@crowd/data-access-layer'
4+
import { getPackageStatusCounts, listPackagesForApi } from '@crowd/data-access-layer'
55

66
import { getPackagesQx } from '@/db/packagesDb'
77
import { ok } from '@/utils/api'
@@ -61,22 +61,22 @@ export async function listPackages(req: Request, res: Response): Promise<void> {
6161
sortDir,
6262
} = validateOrThrow(querySchema, req.query)
6363

64-
const qx = await getPackagesQx()
65-
const { rows, total } = await listPackagesForApi(qx, {
66-
page,
67-
pageSize,
64+
const filterOpts = {
6865
ecosystem,
6966
lifecycle,
7067
name,
71-
status,
7268
healthBand,
7369
vulnSeverity,
7470
staleOnly,
7571
unstewardedOnly,
7672
busFactor1Only,
77-
sortBy,
78-
sortDir,
79-
})
73+
}
74+
75+
const qx = await getPackagesQx()
76+
const [{ rows, total }, statusCounts] = await Promise.all([
77+
listPackagesForApi(qx, { page, pageSize, status, sortBy, sortDir, ...filterOpts }),
78+
getPackageStatusCounts(qx, filterOpts),
79+
])
8080

8181
const packages = rows.map((r) => ({
8282
purl: r.purl,
@@ -96,6 +96,7 @@ export async function listPackages(req: Request, res: Response): Promise<void> {
9696
page,
9797
pageSize,
9898
total,
99+
statusCounts,
99100
filters: {
100101
ecosystem: ecosystem ?? null,
101102
lifecycle: lifecycle ?? null,

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

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,154 @@ const SEVERITY_RANK_EXPR = `MAX(CASE a.severity
9292
WHEN 'LOW' THEN 1
9393
ELSE 0 END)::int`
9494

95+
export interface PackageStatusCounts {
96+
all: number
97+
unassigned: number
98+
open: number
99+
assessing: number
100+
active: number
101+
needs_attention: number
102+
escalated: number
103+
blocked: number
104+
inactive: number
105+
}
106+
107+
export type StatusCountsOptions = Omit<
108+
ListPackagesOptions,
109+
'status' | 'page' | 'pageSize' | 'sortBy' | 'sortDir'
110+
>
111+
112+
const ALL_STEWARDSHIP_STATUSES = [
113+
'unassigned',
114+
'open',
115+
'assessing',
116+
'active',
117+
'needs_attention',
118+
'escalated',
119+
'blocked',
120+
'inactive',
121+
] as const
122+
123+
/**
124+
* Returns per-status counts for the same filter set used by listPackagesForApi, but without
125+
* the status filter so the tab bar always shows all status breakdowns for the active filters.
126+
*/
127+
export async function getPackageStatusCounts(
128+
qx: QueryExecutor,
129+
opts: StatusCountsOptions,
130+
): Promise<PackageStatusCounts> {
131+
const conditions: string[] = ['p.is_critical = true']
132+
const params: Record<string, unknown> = {}
133+
134+
if (opts.ecosystem) {
135+
conditions.push('p.ecosystem = $(ecosystem)')
136+
params.ecosystem = opts.ecosystem
137+
}
138+
139+
if (opts.name) {
140+
conditions.push('p.name ILIKE $(name)')
141+
params.name = `%${opts.name}%`
142+
}
143+
144+
if (opts.lifecycle) {
145+
conditions.push('p.status IS NOT NULL')
146+
}
147+
148+
if (opts.healthBand) {
149+
if (opts.healthBand === 'healthy') {
150+
conditions.push('r_sc.scorecard_score >= 7.0')
151+
} else if (opts.healthBand === 'fair') {
152+
conditions.push('r_sc.scorecard_score >= 5.0 AND r_sc.scorecard_score < 7.0')
153+
} else if (opts.healthBand === 'concerning') {
154+
conditions.push('r_sc.scorecard_score >= 3.0 AND r_sc.scorecard_score < 5.0')
155+
} else {
156+
conditions.push('(r_sc.scorecard_score IS NULL OR r_sc.scorecard_score < 3.0)')
157+
}
158+
}
159+
160+
if (opts.vulnSeverity) {
161+
if (opts.vulnSeverity === 'any') {
162+
conditions.push('ap_counts.cnt > 0')
163+
} else if (opts.vulnSeverity === 'high') {
164+
conditions.push('ap_severity.max_rank >= 3')
165+
} else {
166+
conditions.push('ap_severity.max_rank >= 4')
167+
}
168+
}
169+
170+
if (opts.staleOnly) {
171+
conditions.push(
172+
`(p.latest_release_at IS NULL OR p.latest_release_at < NOW() - INTERVAL '${STALE_MONTHS} months')`,
173+
)
174+
}
175+
176+
if (opts.unstewardedOnly) {
177+
conditions.push(`(s.status = 'unassigned' OR s.id IS NULL)`)
178+
}
179+
180+
if (opts.busFactor1Only) {
181+
conditions.push(`pm_counts.cnt = 1`)
182+
}
183+
184+
const where = `WHERE ${conditions.join(' AND ')}`
185+
186+
const rows: { status: string; count: number }[] = await qx.select(
187+
`
188+
SELECT
189+
COALESCE(s.status, 'unassigned') AS status,
190+
COUNT(*)::int AS count
191+
FROM packages p
192+
LEFT JOIN stewardships s ON s.package_id = p.id
193+
LEFT JOIN LATERAL (
194+
SELECT COUNT(*)::int AS cnt FROM advisory_packages WHERE package_id = p.id
195+
) ap_counts ON true
196+
LEFT JOIN LATERAL (
197+
SELECT COUNT(*)::int AS cnt FROM package_maintainers pm WHERE pm.package_id = p.id
198+
) pm_counts ON true
199+
LEFT JOIN LATERAL (
200+
SELECT ${SEVERITY_RANK_EXPR} AS max_rank
201+
FROM advisory_packages ap
202+
JOIN advisories a ON a.id = ap.advisory_id
203+
WHERE ap.package_id = p.id
204+
) ap_severity ON true
205+
LEFT JOIN LATERAL (
206+
SELECT r.scorecard_score
207+
FROM package_repos pr
208+
JOIN repos r ON r.id = pr.repo_id
209+
WHERE pr.package_id = p.id
210+
ORDER BY pr.confidence DESC
211+
LIMIT 1
212+
) r_sc ON true
213+
${where}
214+
GROUP BY COALESCE(s.status, 'unassigned')
215+
`,
216+
params,
217+
)
218+
219+
const countsMap: Record<string, number> = {}
220+
let all = 0
221+
for (const row of rows) {
222+
countsMap[row.status] = row.count
223+
all += row.count
224+
}
225+
226+
const result: PackageStatusCounts = {
227+
all,
228+
unassigned: 0,
229+
open: 0,
230+
assessing: 0,
231+
active: 0,
232+
needs_attention: 0,
233+
escalated: 0,
234+
blocked: 0,
235+
inactive: 0,
236+
}
237+
for (const status of ALL_STEWARDSHIP_STATUSES) {
238+
result[status] = countsMap[status] ?? 0
239+
}
240+
return result
241+
}
242+
95243
export async function listPackagesForApi(
96244
qx: QueryExecutor,
97245
opts: ListPackagesOptions,

0 commit comments

Comments
 (0)