Scrum4Me/lib/insights/velocity.ts
Janpeter Visser a11b4709a7
fix(insights): narrow Sprint.completed_at to Date in velocity.ts (#56)
Vercel build was failing with TS18047 — `sprint.completed_at` is
possibly `null`. The earlier `.filter(s => s.completed_at != null)`
runtime-filtered the nulls out but did NOT narrow the element type;
TypeScript still saw `Date | null` on the result.

Add a user-defined type guard `(s): s is SprintWithCompletedAt =>` so
the narrowed array carries `completed_at: Date`. No runtime change.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 20:21:30 +02:00

62 lines
1.9 KiB
TypeScript

import { prisma } from '@/lib/prisma'
import { productAccessFilter } from '@/lib/product-access'
export interface VelocitySprint {
sprintId: string
sprintGoal: string
productId: string
productName: string
doneCount: number
completedAt: string
}
export interface VelocityData {
sprints: VelocitySprint[]
productNames: { id: string; name: string }[]
}
export async function getVelocity(userId: string, sprintsBack = 5): Promise<VelocityData> {
const sprints = await prisma.sprint.findMany({
where: {
status: 'COMPLETED',
product: productAccessFilter(userId),
},
orderBy: { completed_at: 'desc' },
take: sprintsBack,
select: {
id: true,
sprint_goal: true,
completed_at: true,
product: { select: { id: true, name: true } },
tasks: { select: { status: true } },
},
})
// Reverse to chronological order (oldest first, for x-axis)
// Type-guard so the narrowed array carries `completed_at: Date` (not Date | null).
// A `.filter(s => s.completed_at != null)` alone does NOT narrow the element type.
type SprintWithCompletedAt = (typeof sprints)[number] & { completed_at: Date }
const chronological = [...sprints]
.filter((s): s is SprintWithCompletedAt => s.completed_at != null)
.reverse()
const result: VelocitySprint[] = chronological.map(sprint => ({
sprintId: sprint.id,
sprintGoal: sprint.sprint_goal,
productId: sprint.product.id,
productName: sprint.product.name,
doneCount: sprint.tasks.filter(t => t.status === 'DONE').length,
completedAt: sprint.completed_at.toISOString(),
}))
const seen = new Set<string>()
const productNames: { id: string; name: string }[] = []
for (const s of result) {
if (!seen.has(s.productId)) {
seen.add(s.productId)
productNames.push({ id: s.productId, name: s.productName })
}
}
return { sprints: result, productNames }
}