@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.
@@ -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 {IdType} [type="auto"] - Expected ID type or auto-detection
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 {string | null} [defaultValue] - Default value when input is empty
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<IsRequired extends boolean = true> = {
106
- type?: IdType
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?: IsRequired extends true ? string : string | null
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 ZodString if required, ZodNullable<ZodString> if optional
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> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
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
- * @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
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
- export function id<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<IdOptions<IsRequired>, 'required'>): IdSchema<IsRequired> {
290
- const {
291
- type = "auto",
292
- minLength,
293
- maxLength,
294
- allowedTypes,
295
- customRegex,
296
- includes,
297
- excludes,
298
- startsWith,
299
- endsWith,
300
- caseSensitive = true,
301
- transform,
302
- defaultValue,
303
- i18n,
304
- } = options ?? {}
305
-
306
- const isRequired = required ?? false as IsRequired
307
-
308
- // Set appropriate default value based on required flag
309
- const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
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, (_, k) => params?.[k] ?? "")
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 preprocessFn = (val: unknown) => {
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
- const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
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
- .refine((val) => {
343
- if (val === null) return true
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
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("minLength", { minLength }), path: [] }])
426
+ ctx.addIssue({ code: "custom", message: getMessage("minLength", { minLength }) })
427
+ return
356
428
  }
357
429
  if (val !== null && maxLength !== undefined && val.length > maxLength) {
358
- throw new z.ZodError([{ code: "custom", message: getMessage("maxLength", { maxLength }), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("customFormat"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage(type as keyof IdMessages) || getMessage("invalid"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage(type as keyof IdMessages) || getMessage("invalid"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("startsWith", { startsWith }), path: [] }])
493
+ ctx.addIssue({ code: "custom", message: getMessage("startsWith", { startsWith }) })
494
+ return
415
495
  }
416
496
  if (val !== null && endsWith !== undefined && !comparisonVal.endsWith(searchEndsWith!)) {
417
- throw new z.ZodError([{ code: "custom", message: getMessage("endsWith", { endsWith }), path: [] }])
497
+ ctx.addIssue({ code: "custom", message: getMessage("endsWith", { endsWith }) })
498
+ return
418
499
  }
419
500
  if (val !== null && includes !== undefined && !comparisonVal.includes(searchIncludes!)) {
420
- throw new z.ZodError([{ code: "custom", message: getMessage("includes", { includes }), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("excludes", { excludes: exclude }), path: [] }])
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>, 'required'>): NumberSchema<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
- .refine((val) => {
232
+ .superRefine((val, ctx) => {
247
233
  // Required check first
248
234
  if (isRequired && val === null) {
249
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
235
+ ctx.addIssue({
236
+ code: "custom",
237
+ message: getMessage("required"),
238
+ })
239
+ return
250
240
  }
251
241
 
252
- if (val === null) return true
242
+ if (val === null) return
253
243
 
254
244
  // Type validation for invalid inputs (NaN)
255
- if (typeof val === "number" && isNaN(val)) {
245
+ if (isNaN(val)) {
256
246
  if (type === "integer") {
257
- throw new z.ZodError([{ code: "custom", message: getMessage("integer"), path: [] }])
247
+ ctx.addIssue({
248
+ code: "custom",
249
+ message: getMessage("integer"),
250
+ })
258
251
  } else if (type === "float") {
259
- throw new z.ZodError([{ code: "custom", message: getMessage("float"), path: [] }])
252
+ ctx.addIssue({
253
+ code: "custom",
254
+ message: getMessage("float"),
255
+ })
260
256
  } else {
261
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("finite"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("integer"), path: [] }])
276
+ ctx.addIssue({
277
+ code: "custom",
278
+ message: getMessage("integer"),
279
+ })
280
+ return
278
281
  }
279
282
  if (type === "float" && Number.isInteger(val)) {
280
- throw new z.ZodError([{ code: "custom", message: getMessage("float"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("positive"), path: [] }])
292
+ ctx.addIssue({
293
+ code: "custom",
294
+ message: getMessage("positive"),
295
+ })
296
+ return
286
297
  }
287
298
  if (negative && val >= 0) {
288
- throw new z.ZodError([{ code: "custom", message: getMessage("negative"), path: [] }])
299
+ ctx.addIssue({
300
+ code: "custom",
301
+ message: getMessage("negative"),
302
+ })
303
+ return
289
304
  }
290
305
  if (nonNegative && val < 0) {
291
- throw new z.ZodError([{ code: "custom", message: getMessage("nonNegative"), path: [] }])
306
+ ctx.addIssue({
307
+ code: "custom",
308
+ message: getMessage("nonNegative"),
309
+ })
310
+ return
292
311
  }
293
312
  if (nonPositive && val > 0) {
294
- throw new z.ZodError([{ code: "custom", message: getMessage("nonPositive"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("min", { min }), path: [] }])
322
+ ctx.addIssue({
323
+ code: "custom",
324
+ message: getMessage("min", { min }),
325
+ })
326
+ return
300
327
  }
301
328
  if (max !== undefined && val > max) {
302
- throw new z.ZodError([{ code: "custom", message: getMessage("max", { max }), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("multipleOf", { multipleOf }), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("precision", { precision }), path: [] }])
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.refine((val) => {
324
- if (val === null) return true
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
- throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("min", { min }), path: [] }])
334
+ ctx.addIssue({ code: "custom", message: getMessage("min", { min }) })
335
+ return
334
336
  }
335
337
  if (val !== null && max !== undefined && val.length > max) {
336
- throw new z.ZodError([{ code: "custom", message: getMessage("max", { max }), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("uppercase"), path: [] }])
344
+ ctx.addIssue({ code: "custom", message: getMessage("uppercase") })
345
+ return
342
346
  }
343
347
  if (val !== null && lowercase && !/[a-z]/.test(val)) {
344
- throw new z.ZodError([{ code: "custom", message: getMessage("lowercase"), path: [] }])
348
+ ctx.addIssue({ code: "custom", message: getMessage("lowercase") })
349
+ return
345
350
  }
346
351
  if (val !== null && digits && !/[0-9]/.test(val)) {
347
- throw new z.ZodError([{ code: "custom", message: getMessage("digits"), path: [] }])
352
+ ctx.addIssue({ code: "custom", message: getMessage("digits") })
353
+ return
348
354
  }
349
355
  if (val !== null && special && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(val)) {
350
- throw new z.ZodError([{ code: "custom", message: getMessage("special"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("noRepeating"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("noSequential"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("noCommonWords"), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("minStrength", { minStrength }), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("includes", { includes }), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("excludes", { excludes: exclude }), path: [] }])
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
- throw new z.ZodError([{ code: "custom", message: getMessage("invalid", { regex }), path: [] }])
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>