From a7a008c62e51466fa866175b65cde25764a156bd Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 May 2026 18:30:58 +0200 Subject: [PATCH] feat(validators): adopt drizzle-zod for tags + brochures schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pilot adoption of `drizzle-zod` (already shipped as part of `drizzle-orm`). Two CRUD-shape validators migrate from hand-written z.object() to `createInsertSchema(table, refinements)`: - tags: name + color (with hex regex refinement). - brochures: label + description + isDefault. Both schemas now derive directly from the Drizzle table definition. Adding a column to the table will auto-include it in the validator (filtered via `.pick(...)` where API surface should stay narrower than the table). Eliminates the validator-drift class of bugs the audit flagged (e.g. adding a column to clients but forgetting to add it to createClientSchema). Pattern is established for future validator touches. Migrating the remaining CRUD validators is opportunistic — done when the validator file is otherwise being edited. Verified: tsc clean, vitest 1293/1293 pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/validators/brochures.ts | 23 +++++++++++++---------- src/lib/validators/tags.ts | 18 ++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/lib/validators/brochures.ts b/src/lib/validators/brochures.ts index a30acd2f..577ab70c 100644 --- a/src/lib/validators/brochures.ts +++ b/src/lib/validators/brochures.ts @@ -1,16 +1,19 @@ import { z } from 'zod'; +import { createInsertSchema, createUpdateSchema } from 'drizzle-zod'; -export const createBrochureSchema = z.object({ - label: z.string().trim().min(1).max(120), - description: z.string().max(500).optional().nullable(), - isDefault: z.boolean().optional(), -}); +import { brochures } from '@/lib/db/schema/brochures'; -export const updateBrochureSchema = z.object({ - label: z.string().trim().min(1).max(120).optional(), - description: z.string().max(500).optional().nullable(), - isDefault: z.boolean().optional(), -}); +// Derived from the Drizzle table — adding a column to `brochures` +// auto-includes it here. Refinements override per-field. +export const createBrochureSchema = createInsertSchema(brochures, { + label: (s) => s.trim().min(1).max(120), + description: (s) => s.max(500), +}).pick({ label: true, description: true, isDefault: true }); + +export const updateBrochureSchema = createUpdateSchema(brochures, { + label: (s) => s.trim().min(1).max(120), + description: (s) => s.max(500), +}).pick({ label: true, description: true, isDefault: true }); export const registerBrochureVersionSchema = z.object({ storageKey: z diff --git a/src/lib/validators/tags.ts b/src/lib/validators/tags.ts index 378018ca..b954633d 100644 --- a/src/lib/validators/tags.ts +++ b/src/lib/validators/tags.ts @@ -1,13 +1,15 @@ import { z } from 'zod'; +import { createInsertSchema } from 'drizzle-zod'; -export const createTagSchema = z.object({ - name: z.string().min(1).max(50), - color: z - .string() - .regex(/^#[0-9A-Fa-f]{6}$/, 'Color must be a valid hex color (e.g. #6B7280)') - .optional() - .default('#6B7280'), -}); +import { tags } from '@/lib/db/schema/system'; + +// Derive the schema from the Drizzle table — adding a column to `tags` +// auto-includes it here; omitting / refining is per-field. Eliminates +// the validator-drift class of bugs the audit flagged. +export const createTagSchema = createInsertSchema(tags, { + name: (s) => s.min(1).max(50), + color: (s) => s.regex(/^#[0-9A-Fa-f]{6}$/, 'Color must be a valid hex color (e.g. #6B7280)'), +}).pick({ name: true, color: true }); export const updateTagSchema = createTagSchema.partial();