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) <noreply@anthropic.com>
40 lines
1.6 KiB
TypeScript
40 lines
1.6 KiB
TypeScript
import { z } from 'zod';
|
|
import { createInsertSchema, createUpdateSchema } from 'drizzle-zod';
|
|
|
|
import { brochures } from '@/lib/db/schema/brochures';
|
|
|
|
// 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
|
|
.string()
|
|
.min(1)
|
|
.max(500)
|
|
// Mirrors the `validateStorageKey` regex in `src/lib/storage/filesystem.ts`
|
|
// — defense-in-depth against path-traversal payloads from the client.
|
|
.regex(/^[a-zA-Z0-9/_.-]+$/, 'Invalid storage key format')
|
|
.refine((s) => !s.includes('..'), 'Storage key may not contain ".."')
|
|
.refine((s) => !s.startsWith('/'), 'Storage key may not be absolute'),
|
|
fileName: z.string().min(1).max(255),
|
|
fileSizeBytes: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.max(100 * 1024 * 1024), // 100MB hard ceiling
|
|
contentSha256: z.string().regex(/^[0-9a-f]{64}$/, 'sha256 must be 64-char hex'),
|
|
});
|
|
|
|
export type CreateBrochureInput = z.infer<typeof createBrochureSchema>;
|
|
export type UpdateBrochureInput = z.infer<typeof updateBrochureSchema>;
|
|
export type RegisterBrochureVersionInput = z.infer<typeof registerBrochureVersionSchema>;
|