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>
This commit is contained in:
parent
611b621d75
commit
829122d437
5 changed files with 38 additions and 12 deletions
|
|
@ -3,7 +3,7 @@ import { prisma } from '@/lib/prisma'
|
|||
|
||||
const MAX_AUTO_CODE_ATTEMPTS = 3
|
||||
|
||||
function isCodeUniqueConflict(error: unknown): boolean {
|
||||
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
|
||||
|
|
@ -40,6 +40,7 @@ export async function createWithCodeRetry<T>(
|
|||
|
||||
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
|
||||
|
|
@ -71,3 +72,13 @@ export async function generateNextPbiCode(productId: string): Promise<string> {
|
|||
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}`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// Pure helpers — safe to import from client components.
|
||||
// DB-backed helpers (generateNextStoryCode/PbiCode) live in lib/code-server.ts.
|
||||
|
||||
const VALID_CODE_RE = /^[A-Za-z0-9._-]+$/
|
||||
export const CODE_REGEX = /^[A-Za-z0-9._-]+$/
|
||||
|
||||
export const MAX_CODE_LENGTH = 30
|
||||
|
||||
export function isValidCode(code: string): boolean {
|
||||
return code.length > 0 && code.length <= MAX_CODE_LENGTH && VALID_CODE_RE.test(code)
|
||||
return code.length > 0 && code.length <= MAX_CODE_LENGTH && CODE_REGEX.test(code)
|
||||
}
|
||||
|
||||
export function normalizeCode(input: string | null | undefined): string | null {
|
||||
|
|
@ -14,8 +14,3 @@ export function normalizeCode(input: string | null | undefined): string | null {
|
|||
const trimmed = input.trim()
|
||||
return trimmed === '' ? null : trimmed
|
||||
}
|
||||
|
||||
export function deriveTaskCode(storyCode: string | null, indexOneBased: number): string | null {
|
||||
if (!storyCode) return null
|
||||
return `${storyCode}.${indexOneBased}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { z } from 'zod'
|
||||
import { MAX_CODE_LENGTH } from '@/lib/code'
|
||||
import { CODE_REGEX, MAX_CODE_LENGTH } from '@/lib/code'
|
||||
|
||||
const codeField = z.string().max(MAX_CODE_LENGTH).optional()
|
||||
const codeField = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(MAX_CODE_LENGTH)
|
||||
.regex(CODE_REGEX, 'Ongeldige code')
|
||||
.optional()
|
||||
.or(z.literal(''))
|
||||
const statusField = z.enum(['ready', 'blocked', 'done']).optional()
|
||||
|
||||
export const createPbiSchema = z.object({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { z } from 'zod'
|
||||
import { MAX_CODE_LENGTH } from '@/lib/code'
|
||||
import { CODE_REGEX, MAX_CODE_LENGTH } from '@/lib/code'
|
||||
|
||||
const codeField = z.string().max(MAX_CODE_LENGTH).optional()
|
||||
const codeField = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(MAX_CODE_LENGTH)
|
||||
.regex(CODE_REGEX, 'Ongeldige code')
|
||||
.optional()
|
||||
.or(z.literal(''))
|
||||
|
||||
export const createStorySchema = z.object({
|
||||
pbiId: z.string(),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
import { z } from 'zod'
|
||||
import { TaskStatus } from '@prisma/client'
|
||||
import { CODE_REGEX, MAX_CODE_LENGTH } from '@/lib/code'
|
||||
|
||||
export const taskSchema = z.object({
|
||||
code: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(MAX_CODE_LENGTH)
|
||||
.regex(CODE_REGEX, 'Ongeldige code')
|
||||
.optional()
|
||||
.or(z.literal('')),
|
||||
title: z.string().trim().min(1, 'Verplicht').max(120),
|
||||
description: z.string().max(2000).optional(),
|
||||
implementation_plan: z.string().max(10000).optional(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue