* docs(ST-511): add backlog entry for entity codes feature Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ST-511): add createWithCodeRetry helper to handle P2002 race on auto codes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ST-511): retry on auto-code unique conflict in story and pbi create Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ST-511): surface field errors for code and title in PBI dialog Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ST-511): read create-state errors in Story dialog fieldError Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(ST-512): add backlog entry for REST API code/description/implementation_plan extensions; mark ST-511 done Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-512): extend REST API with code, description and implementation_plan - GET /api/products returns code, description and definition_of_done - GET /api/products/:id/next-story returns story.code and per-task code + implementation_plan - GET /api/sprints/:id/tasks returns description, implementation_plan, story_code and derived per-task code - POST /api/todos accepts and returns optional description (max 2000) All changes are additive — existing clients ignore unknown keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(ST-512): mark ST-512 as done Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(ST-513): add backlog entry for API hardening for Claude Code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-513): add task and story status mappers for API boundary Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-513): expose lowercase status on API and accept lowercase in PATCH /api/tasks Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-513): add metadata JSONB column to StoryLog Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-513): accept optional metadata in story log and switch validation errors to 422 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-513): add GET /api/health endpoint with optional db ping Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ST-513): add GET /api/products/:id/claude-context bundled endpoint Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(ST-513): add docs/API.md and link from CLAUDE.md specs table Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(ST-513): mark ST-513 as done Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ST-513): split 400 (malformed JSON) from 422 (validation), reject 'review' Codex review on PR #2: - P2.1: routes treated JSON parse failures as 422 instead of 400, breaking the contract in docs/API.md. Replace `request.json().catch(() => null)` with try/catch in 4 routes (tasks, reorder, todos, story-log) so a malformed body returns 400 and only well-formed-but-invalid bodies return 422. - P2.2: PATCH /api/tasks/:id accepted `status: "review"`, but the sprint task list UI does not render REVIEW (no label/color, the cycle helper falls back to TO_DO). Reject `review` at the API until the sprint UI is extended; document the subset in docs/API.md. Tests in __tests__/api updated for the new contract (29 assertions: zod-failures now expect 422, status payloads use lowercase API values, sprint-tasks mocks include the new story relation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
2.2 KiB
TypeScript
73 lines
2.2 KiB
TypeScript
import { Prisma } from '@prisma/client'
|
|
import { prisma } from '@/lib/prisma'
|
|
|
|
const MAX_AUTO_CODE_ATTEMPTS = 3
|
|
|
|
function isCodeUniqueConflict(error: unknown): boolean {
|
|
if (!(error instanceof Prisma.PrismaClientKnownRequestError)) return false
|
|
if (error.code !== 'P2002') return false
|
|
const target = (error.meta as { target?: string[] | string } | undefined)?.target
|
|
if (!target) return false
|
|
if (Array.isArray(target)) return target.includes('code')
|
|
return target.includes('code')
|
|
}
|
|
|
|
/**
|
|
* Generate an auto code, then run the create. If the insert collides with an
|
|
* existing code (P2002 on the code column), regenerate and retry up to a small
|
|
* number of attempts. Protects against the SELECT-MAX → INSERT race when two
|
|
* concurrent requests pick the same next number.
|
|
*/
|
|
export async function createWithCodeRetry<T>(
|
|
generate: () => Promise<string>,
|
|
create: (code: string) => Promise<T>,
|
|
): Promise<T> {
|
|
let lastError: unknown
|
|
for (let attempt = 0; attempt < MAX_AUTO_CODE_ATTEMPTS; attempt++) {
|
|
const code = await generate()
|
|
try {
|
|
return await create(code)
|
|
} catch (e) {
|
|
if (isCodeUniqueConflict(e)) {
|
|
lastError = e
|
|
continue
|
|
}
|
|
throw e
|
|
}
|
|
}
|
|
throw lastError ?? new Error('Kon geen unieke code genereren')
|
|
}
|
|
|
|
const STORY_AUTO_RE = /^ST-(\d+)$/
|
|
const PBI_AUTO_RE = /^PBI-(\d+)$/
|
|
|
|
function nextSequential(existing: (string | null)[], pattern: RegExp): number {
|
|
let max = 0
|
|
for (const c of existing) {
|
|
if (!c) continue
|
|
const m = c.match(pattern)
|
|
if (m) {
|
|
const n = Number.parseInt(m[1], 10)
|
|
if (!Number.isNaN(n) && n > max) max = n
|
|
}
|
|
}
|
|
return max + 1
|
|
}
|
|
|
|
export async function generateNextStoryCode(productId: string): Promise<string> {
|
|
const stories = await prisma.story.findMany({
|
|
where: { product_id: productId },
|
|
select: { code: true },
|
|
})
|
|
const next = nextSequential(stories.map((s) => s.code), STORY_AUTO_RE)
|
|
return `ST-${String(next).padStart(3, '0')}`
|
|
}
|
|
|
|
export async function generateNextPbiCode(productId: string): Promise<string> {
|
|
const pbis = await prisma.pbi.findMany({
|
|
where: { product_id: productId },
|
|
select: { code: true },
|
|
})
|
|
const next = nextSequential(pbis.map((p) => p.code), PBI_AUTO_RE)
|
|
return `PBI-${next}`
|
|
}
|