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:
@@ -1,5 +1,10 @@
|
||||
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 BillingMode = z.enum(['consolidated', 'split'])
|
||||
export type BillingMode = z.infer<typeof BillingMode>
|
||||
|
||||
@@ -11,8 +16,8 @@ export type TaxExemptStatus = z.infer<typeof TaxExemptStatus>
|
||||
|
||||
export const AccountCreateSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
email: z.string().email().optional(),
|
||||
phone: z.string().max(50).optional(),
|
||||
email: opt(z.string().email()),
|
||||
phone: opt(z.string().max(50)),
|
||||
address: z
|
||||
.object({
|
||||
street: z.string().optional(),
|
||||
@@ -22,7 +27,7 @@ export const AccountCreateSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
billingMode: BillingMode.default('consolidated'),
|
||||
notes: z.string().optional(),
|
||||
notes: opt(z.string()),
|
||||
})
|
||||
export type AccountCreateInput = z.infer<typeof AccountCreateSchema>
|
||||
|
||||
@@ -33,11 +38,11 @@ export const MemberCreateSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
firstName: z.string().min(1).max(100),
|
||||
lastName: z.string().min(1).max(100),
|
||||
dateOfBirth: z.string().date().optional(),
|
||||
dateOfBirth: opt(z.string().date()),
|
||||
isMinor: z.boolean().optional(),
|
||||
email: z.string().email().optional(),
|
||||
phone: z.string().max(50).optional(),
|
||||
notes: z.string().optional(),
|
||||
email: opt(z.string().email()),
|
||||
phone: opt(z.string().max(50)),
|
||||
notes: opt(z.string()),
|
||||
})
|
||||
export type MemberCreateInput = z.infer<typeof MemberCreateSchema>
|
||||
|
||||
@@ -56,14 +61,14 @@ export type IdentifierType = z.infer<typeof IdentifierType>
|
||||
export const MemberIdentifierCreateSchema = z.object({
|
||||
memberId: z.string().uuid(),
|
||||
type: IdentifierType,
|
||||
label: z.string().max(100).optional(),
|
||||
label: opt(z.string().max(100)),
|
||||
value: z.string().min(1).max(255),
|
||||
issuingAuthority: z.string().max(255).optional(),
|
||||
issuedDate: z.string().date().optional(),
|
||||
expiresAt: z.string().date().optional(),
|
||||
imageFrontUrl: z.string().max(500).optional(),
|
||||
imageBackUrl: z.string().max(500).optional(),
|
||||
notes: z.string().optional(),
|
||||
issuingAuthority: opt(z.string().max(255)),
|
||||
issuedDate: opt(z.string().date()),
|
||||
expiresAt: opt(z.string().date()),
|
||||
imageFront: opt(z.string()),
|
||||
imageBack: opt(z.string()),
|
||||
notes: opt(z.string()),
|
||||
isPrimary: z.boolean().default(false),
|
||||
})
|
||||
export type MemberIdentifierCreateInput = z.infer<typeof MemberIdentifierCreateSchema>
|
||||
@@ -91,8 +96,8 @@ export const PaymentMethodCreateSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
processor: PaymentProcessor,
|
||||
processorPaymentMethodId: z.string().min(1).max(255),
|
||||
cardBrand: z.string().max(50).optional(),
|
||||
lastFour: z.string().length(4).optional(),
|
||||
cardBrand: opt(z.string().max(50)),
|
||||
lastFour: opt(z.string().length(4)),
|
||||
expMonth: z.number().int().min(1).max(12).optional(),
|
||||
expYear: z.number().int().min(2000).max(2100).optional(),
|
||||
isDefault: z.boolean().default(false),
|
||||
@@ -110,19 +115,19 @@ export type PaymentMethodUpdateInput = z.infer<typeof PaymentMethodUpdateSchema>
|
||||
export const TaxExemptionCreateSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
certificateNumber: z.string().min(1).max(255),
|
||||
certificateType: z.string().max(100).optional(),
|
||||
issuingState: z.string().length(2).optional(),
|
||||
expiresAt: z.string().date().optional(),
|
||||
notes: z.string().optional(),
|
||||
certificateType: opt(z.string().max(100)),
|
||||
issuingState: opt(z.string().length(2)),
|
||||
expiresAt: opt(z.string().date()),
|
||||
notes: opt(z.string()),
|
||||
})
|
||||
export type TaxExemptionCreateInput = z.infer<typeof TaxExemptionCreateSchema>
|
||||
|
||||
export const TaxExemptionUpdateSchema = z.object({
|
||||
certificateNumber: z.string().min(1).max(255).optional(),
|
||||
certificateType: z.string().max(100).optional(),
|
||||
issuingState: z.string().length(2).optional(),
|
||||
expiresAt: z.string().date().optional(),
|
||||
notes: z.string().optional(),
|
||||
certificateNumber: opt(z.string().min(1).max(255)),
|
||||
certificateType: opt(z.string().max(100)),
|
||||
issuingState: opt(z.string().length(2)),
|
||||
expiresAt: opt(z.string().date()),
|
||||
notes: opt(z.string()),
|
||||
})
|
||||
export type TaxExemptionUpdateInput = z.infer<typeof TaxExemptionUpdateSchema>
|
||||
|
||||
@@ -131,14 +136,14 @@ export type TaxExemptionUpdateInput = z.infer<typeof TaxExemptionUpdateSchema>
|
||||
export const LookupCreateSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
slug: z.string().min(1).max(100).regex(/^[a-z0-9_]+$/, 'Slug must be lowercase alphanumeric with underscores'),
|
||||
description: z.string().optional(),
|
||||
description: opt(z.string()),
|
||||
sortOrder: z.number().int().default(0),
|
||||
})
|
||||
export type LookupCreateInput = z.infer<typeof LookupCreateSchema>
|
||||
|
||||
export const LookupUpdateSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
description: z.string().optional(),
|
||||
name: opt(z.string().min(1).max(100)),
|
||||
description: opt(z.string()),
|
||||
sortOrder: z.number().int().optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user