From 829122d4375ae0a6a28b8ec3f243fc2dcb7b9e69 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Mon, 4 May 2026 08:36:28 +0200 Subject: [PATCH] feat(codes): generateNextTaskCode + CODE_REGEX export + Zod regex op alle 3 schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- lib/code-server.ts | 13 ++++++++++++- lib/code.ts | 9 ++------- lib/schemas/pbi.ts | 10 ++++++++-- lib/schemas/story.ts | 10 ++++++++-- lib/schemas/task.ts | 8 ++++++++ 5 files changed, 38 insertions(+), 12 deletions(-) diff --git a/lib/code-server.ts b/lib/code-server.ts index 5c09fcb..461c859 100644 --- a/lib/code-server.ts +++ b/lib/code-server.ts @@ -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( 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 { const next = nextSequential(pbis.map((p) => p.code), PBI_AUTO_RE) return `PBI-${next}` } + +export async function generateNextTaskCode(productId: string): Promise { + 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}` +} + diff --git a/lib/code.ts b/lib/code.ts index 1ce387a..9a247de 100644 --- a/lib/code.ts +++ b/lib/code.ts @@ -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}` -} diff --git a/lib/schemas/pbi.ts b/lib/schemas/pbi.ts index 50468a9..dc8b97d 100644 --- a/lib/schemas/pbi.ts +++ b/lib/schemas/pbi.ts @@ -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({ diff --git a/lib/schemas/story.ts b/lib/schemas/story.ts index 65802af..8de5811 100644 --- a/lib/schemas/story.ts +++ b/lib/schemas/story.ts @@ -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(), diff --git a/lib/schemas/task.ts b/lib/schemas/task.ts index b4c0c3e..9f32282 100644 --- a/lib/schemas/task.ts +++ b/lib/schemas/task.ts @@ -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(),