fix input field requirement

This commit is contained in:
root
2026-06-11 15:49:31 -04:00
parent 37a6ed8a76
commit 6b97c895e0
7 changed files with 349 additions and 200 deletions
+98 -101
View File
@@ -1,145 +1,109 @@
/**
* Zod integration helpers for the input validation/formatting library.
*
* Provides pre-built Zod refinements and transforms that can be
* chained onto `.string()` schemas throughout the API.
*
* Usage:
* import { textField, emailField } from '../lib/zodValidation'
* const schema = z.object({
* firstName: textField('name'),
* email: emailField(),
* })
*/
import { z } from 'zod'
import { sanitizeAndFormat, getMaxLength } from './inputValidation'
import type { FieldType } from './inputValidation'
// ─── Character validation (inline refine) ──────────────────────
// ─── Regex constants ───────────────────────────────────────────
/** Global allowed character pattern from the validation plan */
const GLOBAL_ALLOWED = /^[a-zA-Z0-9@\-/\s]*$/
export const globalCharRefine = (val: string) =>
GLOBAL_ALLOWED.test(val) || { message: 'Only letters, numbers, @, -, and / are allowed' }
// ─── Email validation ──────────────────────────────────────────
const EMAIL_REGEX = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/
export const emailFormatRefine = (val: string) =>
EMAIL_REGEX.test(val) || { message: 'Please enter a valid email address' }
/** Morocco phone: (+212|00212|0) + 5/6/7 + 8 digits (optional spaces) */
const MA_PHONE_REGEX = /^(?:(?:\+|00)212|0)\s?[5-7](?:\s?\d){8}$/
// ─── Pre-built Zod string wrappers ─────────────────────────────
// ─── Phone sanitization ───────────────────────────────────────
/**
* Returns the base Zod chain: .string() → .min(1) → character refine → trim → transform
* If optional is true, wraps in .optional() at the end.
*/
function baseTextField(fieldType: FieldType, optional = false) {
function sanitizePhone(raw: string): string {
let out = ''
for (const ch of raw) {
if (/[\d\s+]/.test(ch)) out += ch
}
return out.trim()
}
// ─── Required fields ───────────────────────────────────────────
function applyFieldRules(fieldType: FieldType) {
const maxLength = getMaxLength(fieldType)
let schema = z
return z
.string()
.min(1, { message: 'This field is required' })
.refine(globalCharRefine)
.trim()
.transform((val) => sanitizeAndFormat(val, fieldType))
.refine(
(val) => val.length <= maxLength,
{ message: `Maximum ${maxLength} characters allowed` }
)
if (optional) {
// For optional fields: allow undefined input, but if a value is provided, apply all rules
return z
.string()
.optional()
.transform((val) => {
if (val === undefined || val === null) return undefined
return sanitizeAndFormat(val, fieldType)
})
.refine(
(val) => val === undefined || val.length === 0 || (globalCharRefine(val as string) === true),
{ message: 'Only letters, numbers, @, -, and / are allowed' }
.transform((val: string) => sanitizeAndFormat(val, fieldType))
.pipe(
z.string().refine(
(val: string) => val.length <= maxLength,
{ message: 'Maximum ' + maxLength + ' characters allowed' }
)
}
return schema
)
}
/**
* Text field with title case formatting.
* fieldType must be one of: name, nationality, streetAddress, city,
* pickupLocation, returnLocation, country
*/
// ─── Optional fields ───────────────────────────────────────────
function applyOptionalFieldRules(fieldType: FieldType) {
const maxLength = getMaxLength(fieldType)
return z
.string()
.optional()
.transform((val) => {
if (val === undefined || val === null || val.trim() === '') return undefined
return sanitizeAndFormat(val, fieldType)
})
.pipe(
z.string().optional().refine(
(val) => val === undefined || val.length <= maxLength,
{ message: 'Maximum ' + maxLength + ' characters allowed' }
)
)
}
// ─── Exported helpers ──────────────────────────────────────────
export function textField(fieldType: FieldType) {
return baseTextField(fieldType, false)
return applyFieldRules(fieldType)
}
/** Optional version of textField */
export function optionalTextField(fieldType: FieldType) {
return baseTextField(fieldType, true)
return applyOptionalFieldRules(fieldType)
}
/**
* Title-case-all fields: fullAddress, commercialName, legalCompanyName
*/
export function titleCaseAllField(fieldType: FieldType) {
return baseTextField(fieldType, false)
return applyFieldRules(fieldType)
}
export function optionalTitleCaseAllField(fieldType: FieldType) {
return baseTextField(fieldType, true)
return applyOptionalFieldRules(fieldType)
}
/**
* Uppercase fields (letters only in allowed chars): licensePlate, vin, cin,
* passportNumber, internationalPermitNumber, driverLicenseNumber,
* licenseCategory, iceNumber, commercialRegistry
*/
export function upperField(fieldType: FieldType) {
return baseTextField(fieldType, false)
return applyFieldRules(fieldType)
}
export function optionalUpperField(fieldType: FieldType) {
return baseTextField(fieldType, true)
return applyOptionalFieldRules(fieldType)
}
/**
* Car fields (title case, letters/numbers/spaces/hyphen allowed):
* carMark, carModel, carColor
*/
export function carTextField(fieldType: FieldType) {
return baseTextField(fieldType, false)
return applyFieldRules(fieldType)
}
export function optionalCarTextField(fieldType: FieldType) {
return baseTextField(fieldType, true)
return applyOptionalFieldRules(fieldType)
}
/**
* Preserved format field: zipCode
*/
export function zipField() {
return baseTextField('zipCode', false)
return applyFieldRules('zipCode')
}
export function optionalZipField() {
return baseTextField('zipCode', true)
return applyOptionalFieldRules('zipCode')
}
/**
* Email field with full validation chain
*/
// ─── Email fields ──────────────────────────────────────────────
export function emailField() {
return z
.string()
.min(1, { message: 'Email is required' })
.refine(globalCharRefine)
.trim()
.transform((val) => sanitizeAndFormat(val, 'email'))
.refine(emailFormatRefine)
.transform((val: string) => sanitizeAndFormat(val, 'email'))
.pipe(
z.string().refine(
(val: string) => EMAIL_REGEX.test(val),
{ message: 'Please enter a valid email address' }
)
)
}
export function optionalEmailField() {
@@ -150,9 +114,42 @@ export function optionalEmailField() {
if (val === undefined || val === null || val.trim() === '') return undefined
return sanitizeAndFormat(val, 'email')
})
.refine(
(val) => val === undefined || emailFormatRefine(val as string) === true,
{ message: 'Please enter a valid email address' }
.pipe(
z.string().optional().refine(
(val) => val === undefined || EMAIL_REGEX.test(val),
{ message: 'Please enter a valid email address' }
)
)
}
// ─── Phone fields ──────────────────────────────────────────────
export function phoneField() {
return z
.string()
.min(1, { message: 'Phone number is required' })
.trim()
.transform((val: string) => sanitizePhone(val))
.pipe(
z.string().refine(
(val: string) => MA_PHONE_REGEX.test(val),
{ message: 'Please enter a valid Morocco phone number' }
)
)
}
export function optionalPhoneField() {
return z
.string()
.optional()
.transform((val) => {
if (val === undefined || val === null || val.trim() === '') return undefined
return sanitizePhone(val)
})
.pipe(
z.string().optional().refine(
(val) => val === undefined || MA_PHONE_REGEX.test(val),
{ message: 'Please enter a valid Morocco phone number' }
)
)
}