Scrum4Me/lib/code-server.ts
Madhura68 829122d437 feat(codes): generateNextTaskCode + CODE_REGEX export + Zod regex op alle 3 schemas
- lib/code.ts: rename VALID_CODE_RE naar geexporteerde CODE_REGEX,
  verwijder ongebruikte deriveTaskCode
- lib/code-server.ts: generateNextTaskCode(productId) — flat per-product
  T-N teller, hergebruikt nextSequential helper. Export
  isCodeUniqueConflict zodat callers P2002 op code-veld kunnen detecteren
- Zod schemas (pbi/story/task): codeField met trim + max-length + regex,
  optional input (server vult bij ontbreken). Task krijgt voor het
  eerst een codeField

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 08:36:28 +02:00

84 lines
2.6 KiB
TypeScript

import { Prisma } from '@prisma/client'
import { prisma } from '@/lib/prisma'
const MAX_AUTO_CODE_ATTEMPTS = 3
export 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+)$/
const TASK_AUTO_RE = /^T-(\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}`
}
export async function generateNextTaskCode(productId: string): Promise<string> {
const tasks = await prisma.task.findMany({
where: { product_id: productId },
select: { code: true },
})
const next = nextSequential(tasks.map((t) => t.code), TASK_AUTO_RE)
return `T-${next}`
}