@hy_ong/zod-kit 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +40 -21
- package/dist/index.d.cts +24 -11
- package/dist/index.d.ts +24 -11
- package/dist/index.js +40 -21
- package/package.json +1 -1
- package/src/validators/common/date.ts +26 -16
- package/src/validators/common/datetime.ts +46 -28
- package/src/validators/common/email.ts +57 -15
- package/src/validators/common/id.ts +139 -58
- package/src/validators/common/number.ts +80 -43
- package/src/validators/common/password.ts +30 -18
- package/src/validators/common/text.ts +48 -14
- package/src/validators/common/time.ts +34 -22
- package/src/validators/common/url.ts +40 -23
- package/src/validators/taiwan/business-id.ts +19 -19
- package/src/validators/taiwan/fax.ts +29 -28
- package/src/validators/taiwan/mobile.ts +29 -28
- package/src/validators/taiwan/national-id.ts +27 -27
- package/src/validators/taiwan/postal-code.ts +51 -45
- package/src/validators/taiwan/tel.ts +29 -28
- package/tests/common/id.test.ts +62 -4
- package/tests/taiwan/business-id.test.ts +23 -23
- package/tests/taiwan/fax.test.ts +34 -34
- package/tests/taiwan/mobile.test.ts +33 -33
- package/tests/taiwan/national-id.test.ts +32 -32
- package/tests/taiwan/postal-code.test.ts +62 -62
- package/tests/taiwan/tel.test.ts +34 -34
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @version 0.0.5
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { z, ZodNullable, ZodString } from "zod"
|
|
11
|
+
import { z, ZodNullable, ZodString, ZodNumber } from "zod"
|
|
12
12
|
import { t } from "../../i18n"
|
|
13
13
|
import { getLocale, type Locale } from "../../config"
|
|
14
14
|
|
|
@@ -85,10 +85,11 @@ export type IdType =
|
|
|
85
85
|
* Configuration options for ID validation
|
|
86
86
|
*
|
|
87
87
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
88
|
+
* @template Type - The ID type being validated
|
|
88
89
|
*
|
|
89
90
|
* @interface IdOptions
|
|
90
91
|
* @property {IsRequired} [required=true] - Whether the field is required
|
|
91
|
-
* @property {
|
|
92
|
+
* @property {Type} [type="auto"] - Expected ID type or auto-detection
|
|
92
93
|
* @property {number} [minLength] - Minimum length of ID
|
|
93
94
|
* @property {number} [maxLength] - Maximum length of ID
|
|
94
95
|
* @property {IdType[]} [allowedTypes] - Multiple allowed ID types (overrides type)
|
|
@@ -99,11 +100,11 @@ export type IdType =
|
|
|
99
100
|
* @property {string} [endsWith] - String that ID must end with
|
|
100
101
|
* @property {boolean} [caseSensitive=true] - Whether validation is case-sensitive
|
|
101
102
|
* @property {Function} [transform] - Custom transformation function for ID
|
|
102
|
-
* @property {
|
|
103
|
+
* @property {any} [defaultValue] - Default value when input is empty (string for string types, number for numeric)
|
|
103
104
|
* @property {Record<Locale, IdMessages>} [i18n] - Custom error messages for different locales
|
|
104
105
|
*/
|
|
105
|
-
export type IdOptions<
|
|
106
|
-
type?:
|
|
106
|
+
export type IdOptions<Type extends IdType | undefined = undefined> = {
|
|
107
|
+
type?: Type
|
|
107
108
|
minLength?: number
|
|
108
109
|
maxLength?: number
|
|
109
110
|
allowedTypes?: IdType[]
|
|
@@ -114,18 +115,27 @@ export type IdOptions<IsRequired extends boolean = true> = {
|
|
|
114
115
|
endsWith?: string
|
|
115
116
|
caseSensitive?: boolean
|
|
116
117
|
transform?: (value: string) => string
|
|
117
|
-
defaultValue?:
|
|
118
|
+
defaultValue?: any // Simplified to avoid complex conditional types
|
|
118
119
|
i18n?: Record<Locale, IdMessages>
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
/**
|
|
122
|
-
* Type alias for ID validation schema based on required flag
|
|
123
|
+
* Type alias for ID validation schema based on required flag and ID type
|
|
123
124
|
*
|
|
124
125
|
* @template IsRequired - Whether the field is required
|
|
126
|
+
* @template IdType - The ID type being validated
|
|
125
127
|
* @typedef IdSchema
|
|
126
|
-
* @description Returns
|
|
128
|
+
* @description Returns appropriate Zod type based on required flag and ID type:
|
|
129
|
+
* - numeric type: ZodNumber or ZodNullable<ZodNumber>
|
|
130
|
+
* - other types: ZodString or ZodNullable<ZodString>
|
|
127
131
|
*/
|
|
128
|
-
export type IdSchema<IsRequired extends boolean
|
|
132
|
+
export type IdSchema<IsRequired extends boolean, Type extends IdType | undefined = undefined> = Type extends "numeric"
|
|
133
|
+
? IsRequired extends true
|
|
134
|
+
? ZodNumber
|
|
135
|
+
: ZodNullable<ZodNumber>
|
|
136
|
+
: IsRequired extends true
|
|
137
|
+
? ZodString
|
|
138
|
+
: ZodNullable<ZodString>
|
|
129
139
|
|
|
130
140
|
/**
|
|
131
141
|
* Regular expression patterns for different ID formats
|
|
@@ -216,9 +226,9 @@ const validateIdType = (value: string, type: IdType): boolean => {
|
|
|
216
226
|
* Creates a Zod schema for ID validation with comprehensive format support
|
|
217
227
|
*
|
|
218
228
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
229
|
+
* @template Type - The ID type being validated (affects return type for numeric)
|
|
219
230
|
* @param {IsRequired} [required=false] - Whether the field is required
|
|
220
|
-
* @
|
|
221
|
-
* @returns {IdSchema<IsRequired>} Zod schema for ID validation
|
|
231
|
+
* @returns {IdSchema<IsRequired, Type>} Zod schema for ID validation
|
|
222
232
|
*
|
|
223
233
|
* @description
|
|
224
234
|
* Creates a comprehensive ID validator with support for multiple ID formats,
|
|
@@ -286,27 +296,31 @@ const validateIdType = (value: string, type: IdType): boolean => {
|
|
|
286
296
|
* @see {@link detectIdType} for auto-detection logic
|
|
287
297
|
* @see {@link validateIdType} for type-specific validation
|
|
288
298
|
*/
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
299
|
+
// Overload: no options provided
|
|
300
|
+
export function id<IsRequired extends boolean = false>(required?: IsRequired): IdSchema<IsRequired, undefined>
|
|
301
|
+
|
|
302
|
+
// Overload: options with numeric type
|
|
303
|
+
export function id<IsRequired extends boolean = false>(required: IsRequired, options: Omit<IdOptions<"numeric">, "required"> & { type: "numeric" }): IdSchema<IsRequired, "numeric">
|
|
304
|
+
|
|
305
|
+
// Overload: options with other specific type
|
|
306
|
+
export function id<IsRequired extends boolean = false, Type extends Exclude<IdType, "numeric"> = Exclude<IdType, "numeric">>(
|
|
307
|
+
required: IsRequired,
|
|
308
|
+
options: Omit<IdOptions<Type>, "required"> & { type: Type }
|
|
309
|
+
): IdSchema<IsRequired, Type>
|
|
310
|
+
|
|
311
|
+
// Overload: options without type specified
|
|
312
|
+
export function id<IsRequired extends boolean = false>(required: IsRequired, options: Omit<IdOptions, "required"> & { type?: never }): IdSchema<IsRequired, undefined>
|
|
313
|
+
|
|
314
|
+
// Implementation
|
|
315
|
+
export function id<IsRequired extends boolean = false, Type extends IdType | undefined = undefined>(required?: IsRequired, options?: any): any {
|
|
316
|
+
const { type = "auto" as Type, minLength, maxLength, allowedTypes, customRegex, includes, excludes, startsWith, endsWith, caseSensitive = true, transform, defaultValue, i18n } = options ?? {}
|
|
317
|
+
|
|
318
|
+
const isRequired = (required ?? false) as IsRequired
|
|
319
|
+
const isNumericType = type === "numeric"
|
|
320
|
+
|
|
321
|
+
// Set appropriate default value based on required flag and type
|
|
322
|
+
// For required fields, we don't set a default unless explicitly provided
|
|
323
|
+
const actualDefaultValue = defaultValue !== undefined ? defaultValue : isRequired ? (isNumericType ? NaN : "") : null
|
|
310
324
|
|
|
311
325
|
// Helper function to get custom message or fallback to default i18n
|
|
312
326
|
const getMessage = (key: keyof IdMessages, params?: Record<string, any>) => {
|
|
@@ -315,14 +329,30 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
|
|
|
315
329
|
const customMessages = i18n[currentLocale]
|
|
316
330
|
if (customMessages && customMessages[key]) {
|
|
317
331
|
const template = customMessages[key]!
|
|
318
|
-
return template.replace(/\$\{(\w+)}/g, (
|
|
332
|
+
return template.replace(/\$\{(\w+)}/g, (_match: string, k: string) => params?.[k] ?? "")
|
|
319
333
|
}
|
|
320
334
|
}
|
|
321
335
|
return t(`common.id.${key}`, params)
|
|
322
336
|
}
|
|
323
337
|
|
|
324
|
-
// Preprocessing function
|
|
325
|
-
const
|
|
338
|
+
// Preprocessing function for numeric type
|
|
339
|
+
const preprocessNumericFn = (val: unknown) => {
|
|
340
|
+
// Handle empty/null values
|
|
341
|
+
if (val === "" || val === null || val === undefined) {
|
|
342
|
+
// If required and no default, return a special marker that will fail validation
|
|
343
|
+
if (isRequired && defaultValue === undefined) {
|
|
344
|
+
// Return undefined to trigger required error in refine
|
|
345
|
+
return undefined as any
|
|
346
|
+
}
|
|
347
|
+
return actualDefaultValue
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Try to convert to number and return (even if NaN) so it can be validated by the schema
|
|
351
|
+
return Number(val)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Preprocessing function for string type
|
|
355
|
+
const preprocessStringFn = (val: unknown) => {
|
|
326
356
|
if (val === "" || val === null || val === undefined) {
|
|
327
357
|
return actualDefaultValue
|
|
328
358
|
}
|
|
@@ -336,15 +366,56 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
|
|
|
336
366
|
return processed
|
|
337
367
|
}
|
|
338
368
|
|
|
339
|
-
|
|
369
|
+
// Create base schema based on type
|
|
370
|
+
if (isNumericType) {
|
|
371
|
+
// Use z.any() to avoid Zod's built-in type checking, then validate manually
|
|
372
|
+
const numericSchema = z.preprocess(preprocessNumericFn, z.any()).superRefine((val, ctx) => {
|
|
373
|
+
// Allow null for optional fields
|
|
374
|
+
if (!isRequired && val === null) return
|
|
375
|
+
|
|
376
|
+
// Required check for undefined/null/empty (empty string when required)
|
|
377
|
+
if (val === undefined || (isRequired && val === null)) {
|
|
378
|
+
ctx.addIssue({ code: "custom", message: getMessage("required") })
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Numeric validation - check if it's an actual number (not NaN)
|
|
383
|
+
if (typeof val !== "number" || isNaN(val)) {
|
|
384
|
+
ctx.addIssue({ code: "custom", message: getMessage("numeric") })
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Length checks on string representation
|
|
389
|
+
const strVal = String(val)
|
|
390
|
+
if (!ID_PATTERNS.numeric.test(strVal)) {
|
|
391
|
+
ctx.addIssue({ code: "custom", message: getMessage("numeric") })
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (minLength !== undefined && strVal.length < minLength) {
|
|
396
|
+
ctx.addIssue({ code: "custom", message: getMessage("minLength", { minLength }) })
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
if (maxLength !== undefined && strVal.length > maxLength) {
|
|
400
|
+
ctx.addIssue({ code: "custom", message: getMessage("maxLength", { maxLength }) })
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
return numericSchema as unknown as IdSchema<IsRequired, Type>
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// String-based ID validation
|
|
409
|
+
const baseSchema = isRequired ? z.preprocess(preprocessStringFn, z.string()) : z.preprocess(preprocessStringFn, z.string().nullable())
|
|
340
410
|
|
|
341
411
|
const schema = baseSchema
|
|
342
|
-
.
|
|
343
|
-
if (val === null) return
|
|
412
|
+
.superRefine((val, ctx) => {
|
|
413
|
+
if (val === null) return
|
|
344
414
|
|
|
345
415
|
// Required check
|
|
346
416
|
if (isRequired && (val === "" || val === "null" || val === "undefined")) {
|
|
347
|
-
|
|
417
|
+
ctx.addIssue({ code: "custom", message: getMessage("required") })
|
|
418
|
+
return
|
|
348
419
|
}
|
|
349
420
|
|
|
350
421
|
// Create comparison value for case-insensitive checks
|
|
@@ -352,10 +423,12 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
|
|
|
352
423
|
|
|
353
424
|
// Length checks
|
|
354
425
|
if (val !== null && minLength !== undefined && val.length < minLength) {
|
|
355
|
-
|
|
426
|
+
ctx.addIssue({ code: "custom", message: getMessage("minLength", { minLength }) })
|
|
427
|
+
return
|
|
356
428
|
}
|
|
357
429
|
if (val !== null && maxLength !== undefined && val.length > maxLength) {
|
|
358
|
-
|
|
430
|
+
ctx.addIssue({ code: "custom", message: getMessage("maxLength", { maxLength }) })
|
|
431
|
+
return
|
|
359
432
|
}
|
|
360
433
|
|
|
361
434
|
// Check if we have content-based validations that override format checking
|
|
@@ -364,7 +437,8 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
|
|
|
364
437
|
// Custom regex validation (overrides ID format validation)
|
|
365
438
|
if (val !== null && customRegex !== undefined) {
|
|
366
439
|
if (!customRegex.test(val)) {
|
|
367
|
-
|
|
440
|
+
ctx.addIssue({ code: "custom", message: getMessage("customFormat") })
|
|
441
|
+
return
|
|
368
442
|
}
|
|
369
443
|
} else if (val !== null && !hasContentValidations) {
|
|
370
444
|
// ID type validation (only if no custom regex or content validations)
|
|
@@ -372,35 +446,40 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
|
|
|
372
446
|
|
|
373
447
|
if (allowedTypes && allowedTypes.length > 0) {
|
|
374
448
|
// Check if ID matches any of the allowed types
|
|
375
|
-
isValidId = allowedTypes.some((allowedType) => validateIdType(val, allowedType))
|
|
449
|
+
isValidId = allowedTypes.some((allowedType: IdType) => validateIdType(val, allowedType))
|
|
376
450
|
if (!isValidId) {
|
|
377
451
|
const typeNames = allowedTypes.join(", ")
|
|
378
|
-
|
|
452
|
+
ctx.addIssue({ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})` })
|
|
453
|
+
return
|
|
379
454
|
}
|
|
380
|
-
} else if (type !== "auto") {
|
|
455
|
+
} else if (type && type !== "auto") {
|
|
381
456
|
// Validate specific type
|
|
382
457
|
isValidId = validateIdType(val, type)
|
|
383
458
|
if (!isValidId) {
|
|
384
|
-
|
|
459
|
+
ctx.addIssue({ code: "custom", message: getMessage(type as keyof IdMessages) || getMessage("invalid") })
|
|
460
|
+
return
|
|
385
461
|
}
|
|
386
462
|
} else {
|
|
387
463
|
// Auto-detection - must match at least one known pattern
|
|
388
464
|
isValidId = detectIdType(val) !== null
|
|
389
465
|
if (!isValidId) {
|
|
390
|
-
|
|
466
|
+
ctx.addIssue({ code: "custom", message: getMessage("invalid") })
|
|
467
|
+
return
|
|
391
468
|
}
|
|
392
469
|
}
|
|
393
|
-
} else if (val !== null && hasContentValidations && type !== "auto" && !customRegex) {
|
|
470
|
+
} else if (val !== null && hasContentValidations && type && type !== "auto" && !customRegex) {
|
|
394
471
|
// Still validate specific types even with content validations (but not auto)
|
|
395
472
|
if (allowedTypes && allowedTypes.length > 0) {
|
|
396
|
-
const isValidType = allowedTypes.some((allowedType) => validateIdType(val, allowedType))
|
|
473
|
+
const isValidType = allowedTypes.some((allowedType: IdType) => validateIdType(val, allowedType))
|
|
397
474
|
if (!isValidType) {
|
|
398
475
|
const typeNames = allowedTypes.join(", ")
|
|
399
|
-
|
|
476
|
+
ctx.addIssue({ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})` })
|
|
477
|
+
return
|
|
400
478
|
}
|
|
401
479
|
} else {
|
|
402
480
|
if (!validateIdType(val, type)) {
|
|
403
|
-
|
|
481
|
+
ctx.addIssue({ code: "custom", message: getMessage(type as keyof IdMessages) || getMessage("invalid") })
|
|
482
|
+
return
|
|
404
483
|
}
|
|
405
484
|
}
|
|
406
485
|
}
|
|
@@ -411,25 +490,27 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
|
|
|
411
490
|
const searchIncludes = !caseSensitive && includes ? includes.toLowerCase() : includes
|
|
412
491
|
|
|
413
492
|
if (val !== null && startsWith !== undefined && !comparisonVal.startsWith(searchStartsWith!)) {
|
|
414
|
-
|
|
493
|
+
ctx.addIssue({ code: "custom", message: getMessage("startsWith", { startsWith }) })
|
|
494
|
+
return
|
|
415
495
|
}
|
|
416
496
|
if (val !== null && endsWith !== undefined && !comparisonVal.endsWith(searchEndsWith!)) {
|
|
417
|
-
|
|
497
|
+
ctx.addIssue({ code: "custom", message: getMessage("endsWith", { endsWith }) })
|
|
498
|
+
return
|
|
418
499
|
}
|
|
419
500
|
if (val !== null && includes !== undefined && !comparisonVal.includes(searchIncludes!)) {
|
|
420
|
-
|
|
501
|
+
ctx.addIssue({ code: "custom", message: getMessage("includes", { includes }) })
|
|
502
|
+
return
|
|
421
503
|
}
|
|
422
504
|
if (val !== null && excludes !== undefined) {
|
|
423
505
|
const excludeList = Array.isArray(excludes) ? excludes : [excludes]
|
|
424
506
|
for (const exclude of excludeList) {
|
|
425
507
|
const searchExclude = !caseSensitive ? exclude.toLowerCase() : exclude
|
|
426
508
|
if (comparisonVal.includes(searchExclude)) {
|
|
427
|
-
|
|
509
|
+
ctx.addIssue({ code: "custom", message: getMessage("excludes", { excludes: exclude }) })
|
|
510
|
+
return
|
|
428
511
|
}
|
|
429
512
|
}
|
|
430
513
|
}
|
|
431
|
-
|
|
432
|
-
return true
|
|
433
514
|
})
|
|
434
515
|
.transform((val) => {
|
|
435
516
|
if (val === null) return val
|
|
@@ -444,7 +525,7 @@ export function id<IsRequired extends boolean = false>(required?: IsRequired, op
|
|
|
444
525
|
return val // preserve the original case for UUID/ObjectId or when case-sensitive
|
|
445
526
|
})
|
|
446
527
|
|
|
447
|
-
return schema as unknown as IdSchema<IsRequired>
|
|
528
|
+
return schema as unknown as IdSchema<IsRequired, Type>
|
|
448
529
|
}
|
|
449
530
|
|
|
450
531
|
/**
|
|
@@ -98,6 +98,7 @@ export type NumberSchema<IsRequired extends boolean> = IsRequired extends true ?
|
|
|
98
98
|
* Creates a Zod schema for number validation with comprehensive constraints
|
|
99
99
|
*
|
|
100
100
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
101
|
+
* @param required
|
|
101
102
|
* @param {NumberOptions<IsRequired>} [options] - Configuration options for number validation
|
|
102
103
|
* @returns {NumberSchema<IsRequired>} Zod schema for number validation
|
|
103
104
|
*
|
|
@@ -165,25 +166,10 @@ export type NumberSchema<IsRequired extends boolean> = IsRequired extends true ?
|
|
|
165
166
|
* @throws {z.ZodError} When validation fails with specific error messages
|
|
166
167
|
* @see {@link NumberOptions} for all available configuration options
|
|
167
168
|
*/
|
|
168
|
-
export function number<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<NumberOptions<IsRequired>,
|
|
169
|
-
const {
|
|
170
|
-
min,
|
|
171
|
-
max,
|
|
172
|
-
defaultValue,
|
|
173
|
-
type = "both",
|
|
174
|
-
positive,
|
|
175
|
-
negative,
|
|
176
|
-
nonNegative,
|
|
177
|
-
nonPositive,
|
|
178
|
-
multipleOf,
|
|
179
|
-
precision,
|
|
180
|
-
finite = true,
|
|
181
|
-
transform,
|
|
182
|
-
parseCommas = false,
|
|
183
|
-
i18n,
|
|
184
|
-
} = options ?? {}
|
|
169
|
+
export function number<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<NumberOptions<IsRequired>, "required">): NumberSchema<IsRequired> {
|
|
170
|
+
const { min, max, defaultValue, type = "both", positive, negative, nonNegative, nonPositive, multipleOf, precision, finite = true, transform, parseCommas = false, i18n } = options ?? {}
|
|
185
171
|
|
|
186
|
-
const isRequired = required ?? false as IsRequired
|
|
172
|
+
const isRequired = required ?? (false as IsRequired)
|
|
187
173
|
|
|
188
174
|
// Helper function to get custom message or fallback to default i18n
|
|
189
175
|
const getMessage = (key: keyof NumberMessages, params?: Record<string, any>) => {
|
|
@@ -243,79 +229,130 @@ export function number<IsRequired extends boolean = false>(required?: IsRequired
|
|
|
243
229
|
},
|
|
244
230
|
z.union([z.number(), z.null(), z.nan(), z.custom<number>((val) => val === Infinity || val === -Infinity)])
|
|
245
231
|
)
|
|
246
|
-
.
|
|
232
|
+
.superRefine((val, ctx) => {
|
|
247
233
|
// Required check first
|
|
248
234
|
if (isRequired && val === null) {
|
|
249
|
-
|
|
235
|
+
ctx.addIssue({
|
|
236
|
+
code: "custom",
|
|
237
|
+
message: getMessage("required"),
|
|
238
|
+
})
|
|
239
|
+
return
|
|
250
240
|
}
|
|
251
241
|
|
|
252
|
-
if (val === null) return
|
|
242
|
+
if (val === null) return
|
|
253
243
|
|
|
254
244
|
// Type validation for invalid inputs (NaN)
|
|
255
|
-
if (
|
|
245
|
+
if (isNaN(val)) {
|
|
256
246
|
if (type === "integer") {
|
|
257
|
-
|
|
247
|
+
ctx.addIssue({
|
|
248
|
+
code: "custom",
|
|
249
|
+
message: getMessage("integer"),
|
|
250
|
+
})
|
|
258
251
|
} else if (type === "float") {
|
|
259
|
-
|
|
252
|
+
ctx.addIssue({
|
|
253
|
+
code: "custom",
|
|
254
|
+
message: getMessage("float"),
|
|
255
|
+
})
|
|
260
256
|
} else {
|
|
261
|
-
|
|
257
|
+
ctx.addIssue({
|
|
258
|
+
code: "custom",
|
|
259
|
+
message: getMessage("invalid"),
|
|
260
|
+
})
|
|
262
261
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// Invalid number check for non-numbers
|
|
266
|
-
if (typeof val !== "number") {
|
|
267
|
-
throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
|
|
262
|
+
return
|
|
268
263
|
}
|
|
269
264
|
|
|
270
265
|
// Finite check
|
|
271
266
|
if (finite && !Number.isFinite(val)) {
|
|
272
|
-
|
|
267
|
+
ctx.addIssue({
|
|
268
|
+
code: "custom",
|
|
269
|
+
message: getMessage("finite"),
|
|
270
|
+
})
|
|
271
|
+
return
|
|
273
272
|
}
|
|
274
273
|
|
|
275
274
|
// Type validation for valid numbers
|
|
276
275
|
if (type === "integer" && !Number.isInteger(val)) {
|
|
277
|
-
|
|
276
|
+
ctx.addIssue({
|
|
277
|
+
code: "custom",
|
|
278
|
+
message: getMessage("integer"),
|
|
279
|
+
})
|
|
280
|
+
return
|
|
278
281
|
}
|
|
279
282
|
if (type === "float" && Number.isInteger(val)) {
|
|
280
|
-
|
|
283
|
+
ctx.addIssue({
|
|
284
|
+
code: "custom",
|
|
285
|
+
message: getMessage("float"),
|
|
286
|
+
})
|
|
287
|
+
return
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
// Sign checks (more specific, should come first)
|
|
284
291
|
if (positive && val <= 0) {
|
|
285
|
-
|
|
292
|
+
ctx.addIssue({
|
|
293
|
+
code: "custom",
|
|
294
|
+
message: getMessage("positive"),
|
|
295
|
+
})
|
|
296
|
+
return
|
|
286
297
|
}
|
|
287
298
|
if (negative && val >= 0) {
|
|
288
|
-
|
|
299
|
+
ctx.addIssue({
|
|
300
|
+
code: "custom",
|
|
301
|
+
message: getMessage("negative"),
|
|
302
|
+
})
|
|
303
|
+
return
|
|
289
304
|
}
|
|
290
305
|
if (nonNegative && val < 0) {
|
|
291
|
-
|
|
306
|
+
ctx.addIssue({
|
|
307
|
+
code: "custom",
|
|
308
|
+
message: getMessage("nonNegative"),
|
|
309
|
+
})
|
|
310
|
+
return
|
|
292
311
|
}
|
|
293
312
|
if (nonPositive && val > 0) {
|
|
294
|
-
|
|
313
|
+
ctx.addIssue({
|
|
314
|
+
code: "custom",
|
|
315
|
+
message: getMessage("nonPositive"),
|
|
316
|
+
})
|
|
317
|
+
return
|
|
295
318
|
}
|
|
296
319
|
|
|
297
320
|
// Range checks
|
|
298
321
|
if (min !== undefined && val < min) {
|
|
299
|
-
|
|
322
|
+
ctx.addIssue({
|
|
323
|
+
code: "custom",
|
|
324
|
+
message: getMessage("min", { min }),
|
|
325
|
+
})
|
|
326
|
+
return
|
|
300
327
|
}
|
|
301
328
|
if (max !== undefined && val > max) {
|
|
302
|
-
|
|
329
|
+
ctx.addIssue({
|
|
330
|
+
code: "custom",
|
|
331
|
+
message: getMessage("max", { max }),
|
|
332
|
+
})
|
|
333
|
+
return
|
|
303
334
|
}
|
|
304
335
|
|
|
305
336
|
// Multiple of check
|
|
306
337
|
if (multipleOf !== undefined && val % multipleOf !== 0) {
|
|
307
|
-
|
|
338
|
+
ctx.addIssue({
|
|
339
|
+
code: "custom",
|
|
340
|
+
message: getMessage("multipleOf", { multipleOf }),
|
|
341
|
+
})
|
|
342
|
+
return
|
|
308
343
|
}
|
|
309
344
|
|
|
310
345
|
// Precision check
|
|
311
346
|
if (precision !== undefined) {
|
|
312
347
|
const decimalPlaces = (val.toString().split(".")[1] || "").length
|
|
313
348
|
if (decimalPlaces > precision) {
|
|
314
|
-
|
|
349
|
+
ctx.addIssue({
|
|
350
|
+
code: "custom",
|
|
351
|
+
message: getMessage("precision", { precision }),
|
|
352
|
+
})
|
|
353
|
+
return
|
|
315
354
|
}
|
|
316
355
|
}
|
|
317
|
-
|
|
318
|
-
return true
|
|
319
356
|
})
|
|
320
357
|
|
|
321
358
|
return schema as unknown as NumberSchema<IsRequired>
|
|
@@ -320,45 +320,55 @@ export function password<IsRequired extends boolean = false>(required?: IsRequir
|
|
|
320
320
|
|
|
321
321
|
const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
322
322
|
|
|
323
|
-
const schema = baseSchema.
|
|
324
|
-
if (val === null) return
|
|
323
|
+
const schema = baseSchema.superRefine((val, ctx) => {
|
|
324
|
+
if (val === null) return
|
|
325
325
|
|
|
326
326
|
// Required check
|
|
327
327
|
if (isRequired && (val === "" || val === "null" || val === "undefined")) {
|
|
328
|
-
|
|
328
|
+
ctx.addIssue({ code: "custom", message: getMessage("required") })
|
|
329
|
+
return
|
|
329
330
|
}
|
|
330
331
|
|
|
331
332
|
// Length checks
|
|
332
333
|
if (val !== null && min !== undefined && val.length < min) {
|
|
333
|
-
|
|
334
|
+
ctx.addIssue({ code: "custom", message: getMessage("min", { min }) })
|
|
335
|
+
return
|
|
334
336
|
}
|
|
335
337
|
if (val !== null && max !== undefined && val.length > max) {
|
|
336
|
-
|
|
338
|
+
ctx.addIssue({ code: "custom", message: getMessage("max", { max }) })
|
|
339
|
+
return
|
|
337
340
|
}
|
|
338
341
|
|
|
339
342
|
// Character requirements
|
|
340
343
|
if (val !== null && uppercase && !/[A-Z]/.test(val)) {
|
|
341
|
-
|
|
344
|
+
ctx.addIssue({ code: "custom", message: getMessage("uppercase") })
|
|
345
|
+
return
|
|
342
346
|
}
|
|
343
347
|
if (val !== null && lowercase && !/[a-z]/.test(val)) {
|
|
344
|
-
|
|
348
|
+
ctx.addIssue({ code: "custom", message: getMessage("lowercase") })
|
|
349
|
+
return
|
|
345
350
|
}
|
|
346
351
|
if (val !== null && digits && !/[0-9]/.test(val)) {
|
|
347
|
-
|
|
352
|
+
ctx.addIssue({ code: "custom", message: getMessage("digits") })
|
|
353
|
+
return
|
|
348
354
|
}
|
|
349
355
|
if (val !== null && special && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(val)) {
|
|
350
|
-
|
|
356
|
+
ctx.addIssue({ code: "custom", message: getMessage("special") })
|
|
357
|
+
return
|
|
351
358
|
}
|
|
352
359
|
|
|
353
360
|
// Advanced security checks
|
|
354
361
|
if (val !== null && noRepeating && /(.)\1{2,}/.test(val)) {
|
|
355
|
-
|
|
362
|
+
ctx.addIssue({ code: "custom", message: getMessage("noRepeating") })
|
|
363
|
+
return
|
|
356
364
|
}
|
|
357
365
|
if (val !== null && noSequential && /(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(val)) {
|
|
358
|
-
|
|
366
|
+
ctx.addIssue({ code: "custom", message: getMessage("noSequential") })
|
|
367
|
+
return
|
|
359
368
|
}
|
|
360
369
|
if (val !== null && noCommonWords && COMMON_PASSWORDS.some((common) => val.toLowerCase().includes(common.toLowerCase()))) {
|
|
361
|
-
|
|
370
|
+
ctx.addIssue({ code: "custom", message: getMessage("noCommonWords") })
|
|
371
|
+
return
|
|
362
372
|
}
|
|
363
373
|
if (val !== null && minStrength) {
|
|
364
374
|
const strength = calculatePasswordStrength(val)
|
|
@@ -366,27 +376,29 @@ export function password<IsRequired extends boolean = false>(required?: IsRequir
|
|
|
366
376
|
const currentLevel = strengthLevels.indexOf(strength)
|
|
367
377
|
const requiredLevel = strengthLevels.indexOf(minStrength)
|
|
368
378
|
if (currentLevel < requiredLevel) {
|
|
369
|
-
|
|
379
|
+
ctx.addIssue({ code: "custom", message: getMessage("minStrength", { minStrength }) })
|
|
380
|
+
return
|
|
370
381
|
}
|
|
371
382
|
}
|
|
372
383
|
|
|
373
384
|
// Content checks
|
|
374
385
|
if (val !== null && includes !== undefined && !val.includes(includes)) {
|
|
375
|
-
|
|
386
|
+
ctx.addIssue({ code: "custom", message: getMessage("includes", { includes }) })
|
|
387
|
+
return
|
|
376
388
|
}
|
|
377
389
|
if (val !== null && excludes !== undefined) {
|
|
378
390
|
const excludeList = Array.isArray(excludes) ? excludes : [excludes]
|
|
379
391
|
for (const exclude of excludeList) {
|
|
380
392
|
if (val.includes(exclude)) {
|
|
381
|
-
|
|
393
|
+
ctx.addIssue({ code: "custom", message: getMessage("excludes", { excludes: exclude }) })
|
|
394
|
+
return
|
|
382
395
|
}
|
|
383
396
|
}
|
|
384
397
|
}
|
|
385
398
|
if (val !== null && regex !== undefined && !regex.test(val)) {
|
|
386
|
-
|
|
399
|
+
ctx.addIssue({ code: "custom", message: getMessage("invalid", { regex }) })
|
|
400
|
+
return
|
|
387
401
|
}
|
|
388
|
-
|
|
389
|
-
return true
|
|
390
402
|
})
|
|
391
403
|
|
|
392
404
|
return schema as unknown as PasswordSchema<IsRequired>
|