Fix empty string validation on all optional form fields across all schemas

Add opt() preprocessor that coerces empty strings to undefined before Zod
validation. Applied to every optional string field in account, member,
identifier, supplier, product, inventory unit, tax exemption, payment
method, and lookup schemas. Fixes forms rejecting blank optional fields.
This commit is contained in:
Ryan Moon
2026-03-28 11:40:48 -05:00
parent 6ca38e2105
commit ce2a61ced9
2 changed files with 58 additions and 48 deletions

View File

@@ -1,9 +1,14 @@
import { z } from 'zod'
/** Coerce empty strings to undefined — solves HTML form inputs sending '' for blank optional fields */
function opt<T extends z.ZodTypeAny>(schema: T) {
return z.preprocess((v) => (v === '' ? undefined : v), schema.optional()) as z.ZodEffects<z.ZodOptional<T>>
}
export const CategoryCreateSchema = z.object({
name: z.string().min(1).max(255),
description: z.string().optional(),
parentId: z.string().uuid().optional(),
description: opt(z.string()),
parentId: opt(z.string().uuid()),
sortOrder: z.number().int().default(0),
})
export type CategoryCreateInput = z.infer<typeof CategoryCreateSchema>
@@ -13,13 +18,13 @@ export type CategoryUpdateInput = z.infer<typeof CategoryUpdateSchema>
export const SupplierCreateSchema = z.object({
name: z.string().min(1).max(255),
contactName: z.string().max(255).optional(),
email: z.string().email().optional(),
phone: z.string().max(50).optional(),
website: z.string().max(255).optional(),
accountNumber: z.string().max(100).optional(),
paymentTerms: z.string().max(100).optional(),
notes: z.string().optional(),
contactName: opt(z.string().max(255)),
email: opt(z.string().email()),
phone: opt(z.string().max(50)),
website: opt(z.string().max(255)),
accountNumber: opt(z.string().max(100)),
paymentTerms: opt(z.string().max(100)),
notes: opt(z.string()),
})
export type SupplierCreateInput = z.infer<typeof SupplierCreateSchema>
@@ -45,14 +50,14 @@ export const ItemCondition = z.string().min(1).max(100)
export const UnitStatus = z.string().min(1).max(100)
export const ProductCreateSchema = z.object({
sku: z.string().max(100).optional(),
upc: z.string().max(100).optional(),
sku: opt(z.string().max(100)),
upc: opt(z.string().max(100)),
name: z.string().min(1).max(255),
description: z.string().optional(),
brand: z.string().max(255).optional(),
model: z.string().max(255).optional(),
categoryId: z.string().uuid().optional(),
locationId: z.string().uuid().optional(),
description: opt(z.string()),
brand: opt(z.string().max(255)),
model: opt(z.string().max(255)),
categoryId: opt(z.string().uuid()),
locationId: opt(z.string().uuid()),
isSerialized: z.boolean().default(false),
isRental: z.boolean().default(false),
isDualUseRepair: z.boolean().default(false),
@@ -73,13 +78,13 @@ export const ProductSearchSchema = z.object({
export const InventoryUnitCreateSchema = z.object({
productId: z.string().uuid(),
locationId: z.string().uuid().optional(),
serialNumber: z.string().max(255).optional(),
locationId: opt(z.string().uuid()),
serialNumber: opt(z.string().max(255)),
condition: ItemCondition.default('new'),
status: UnitStatus.default('available'),
purchaseDate: z.string().date().optional(),
purchaseDate: opt(z.string().date()),
purchaseCost: z.number().min(0).optional(),
notes: z.string().optional(),
notes: opt(z.string()),
})
export type InventoryUnitCreateInput = z.infer<typeof InventoryUnitCreateSchema>