feat(defects): read API — feed, per-section counts and history (T6)

GET /api/defects returns a newest-first feed joined to the project name,
bounded by limit; ?section=ID narrows to one section's history. GET
/api/defects/counts returns { sectionId: n } for the weak-point map. Adds a
(section_id, created_at) index and a db:seed script; ~2k rows query in ~20ms.
This commit is contained in:
Julien Calixte
2026-05-28 00:18:18 +02:00
parent fc958ba59f
commit 999593b426
11 changed files with 384 additions and 14 deletions

View File

@@ -1,7 +1,7 @@
import { desc } from 'drizzle-orm'
import { desc, eq, sql } from 'drizzle-orm'
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import type * as schema from '../schema'
import { defects } from '../schema'
import { defects, projects } from '../schema'
type Db = NodePgDatabase<typeof schema>
@@ -22,3 +22,48 @@ export async function createDefect(db: Db, input: NewDefect) {
export async function listRecentDefects(db: Db, limit = 100) {
return db.select().from(defects).orderBy(desc(defects.createdAt)).limit(limit)
}
export interface DefectView {
id: string
sectionId: string
projectId: string
projectName: string
verbatim: string
reporterEmail: string
createdAt: Date
}
/**
* Defects for the dashboard (F6), newest-first and joined to the project name.
* The whole feed by default; one section's history when `sectionId` is given
* (the lazy modal load). Always bounded by `limit`.
*/
export async function listDefects(
db: Db,
opts: { sectionId?: string; limit?: number } = {},
): Promise<DefectView[]> {
return db
.select({
id: defects.id,
sectionId: defects.sectionId,
projectId: defects.projectId,
projectName: projects.name,
verbatim: defects.verbatim,
reporterEmail: defects.reporterEmail,
createdAt: defects.createdAt,
})
.from(defects)
.innerJoin(projects, eq(projects.id, defects.projectId))
.where(opts.sectionId ? eq(defects.sectionId, opts.sectionId) : undefined)
.orderBy(desc(defects.createdAt))
.limit(opts.limit ?? 100)
}
/** Per-section defect counts `{ sectionId: n }` for the weak-point map (F4/F6). */
export async function countDefectsBySection(db: Db): Promise<Record<string, number>> {
const rows = await db
.select({ sectionId: defects.sectionId, count: sql<number>`count(*)::int` })
.from(defects)
.groupBy(defects.sectionId)
return Object.fromEntries(rows.map((r) => [r.sectionId, r.count]))
}