@hy_ong/zod-kit 0.0.6 → 0.1.1
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/.claude/settings.local.json +4 -1
- package/README.md +134 -68
- package/dist/index.cjs +93 -89
- package/dist/index.d.cts +235 -169
- package/dist/index.d.ts +235 -169
- package/dist/index.js +93 -89
- package/package.json +15 -5
- package/src/validators/common/boolean.ts +17 -14
- package/src/validators/common/date.ts +21 -14
- package/src/validators/common/datetime.ts +21 -14
- package/src/validators/common/email.ts +18 -15
- package/src/validators/common/file.ts +20 -13
- package/src/validators/common/id.ts +14 -14
- package/src/validators/common/number.ts +18 -15
- package/src/validators/common/password.ts +21 -14
- package/src/validators/common/text.ts +21 -17
- package/src/validators/common/time.ts +21 -14
- package/src/validators/common/url.ts +22 -15
- package/src/validators/taiwan/business-id.ts +18 -11
- package/src/validators/taiwan/fax.ts +23 -14
- package/src/validators/taiwan/mobile.ts +23 -14
- package/src/validators/taiwan/national-id.ts +11 -12
- package/src/validators/taiwan/postal-code.ts +16 -17
- package/src/validators/taiwan/tel.ts +23 -14
- package/tests/common/boolean.test.ts +38 -38
- package/tests/common/date.test.ts +65 -65
- package/tests/common/datetime.test.ts +100 -118
- package/tests/common/email.test.ts +24 -28
- package/tests/common/file.test.ts +47 -51
- package/tests/common/id.test.ts +80 -113
- package/tests/common/number.test.ts +24 -25
- package/tests/common/password.test.ts +28 -35
- package/tests/common/text.test.ts +36 -37
- package/tests/common/time.test.ts +64 -82
- package/tests/common/url.test.ts +67 -67
- package/tests/taiwan/business-id.test.ts +22 -22
- package/tests/taiwan/fax.test.ts +33 -42
- package/tests/taiwan/mobile.test.ts +32 -41
- package/tests/taiwan/national-id.test.ts +31 -31
- package/tests/taiwan/postal-code.test.ts +142 -96
- package/tests/taiwan/tel.test.ts +33 -42
- package/debug.js +0 -21
- package/debug.ts +0 -16
|
@@ -69,7 +69,6 @@ export type NumberMessages = {
|
|
|
69
69
|
* @property {Record<Locale, NumberMessages>} [i18n] - Custom error messages for different locales
|
|
70
70
|
*/
|
|
71
71
|
export type NumberOptions<IsRequired extends boolean = true> = {
|
|
72
|
-
required?: IsRequired
|
|
73
72
|
min?: number
|
|
74
73
|
max?: number
|
|
75
74
|
defaultValue?: IsRequired extends true ? number : number | null
|
|
@@ -119,53 +118,55 @@ export type NumberSchema<IsRequired extends boolean> = IsRequired extends true ?
|
|
|
119
118
|
*
|
|
120
119
|
* @example
|
|
121
120
|
* ```typescript
|
|
122
|
-
* // Basic number validation
|
|
121
|
+
* // Basic number validation (optional by default)
|
|
123
122
|
* const basicSchema = number()
|
|
124
123
|
* basicSchema.parse(42) // ✓ Valid
|
|
125
124
|
* basicSchema.parse("42") // ✓ Valid (converted to number)
|
|
125
|
+
* basicSchema.parse(null) // ✓ Valid (optional)
|
|
126
|
+
*
|
|
127
|
+
* // Required number
|
|
128
|
+
* const requiredSchema = number(true)
|
|
129
|
+
* requiredSchema.parse(42) // ✓ Valid
|
|
130
|
+
* requiredSchema.parse(null) // ✗ Invalid (required)
|
|
126
131
|
*
|
|
127
132
|
* // Integer only
|
|
128
|
-
* const integerSchema = number({ type: "integer" })
|
|
133
|
+
* const integerSchema = number(false, { type: "integer" })
|
|
129
134
|
* integerSchema.parse(42) // ✓ Valid
|
|
130
135
|
* integerSchema.parse(42.5) // ✗ Invalid
|
|
131
136
|
*
|
|
132
137
|
* // Range validation
|
|
133
|
-
* const rangeSchema = number({ min: 0, max: 100 })
|
|
138
|
+
* const rangeSchema = number(true, { min: 0, max: 100 })
|
|
134
139
|
* rangeSchema.parse(50) // ✓ Valid
|
|
135
140
|
* rangeSchema.parse(150) // ✗ Invalid
|
|
136
141
|
*
|
|
137
142
|
* // Positive numbers only
|
|
138
|
-
* const positiveSchema = number({ positive: true })
|
|
143
|
+
* const positiveSchema = number(true, { positive: true })
|
|
139
144
|
* positiveSchema.parse(5) // ✓ Valid
|
|
140
145
|
* positiveSchema.parse(-5) // ✗ Invalid
|
|
141
146
|
*
|
|
142
147
|
* // Multiple of constraint
|
|
143
|
-
* const multipleSchema = number({ multipleOf: 5 })
|
|
148
|
+
* const multipleSchema = number(true, { multipleOf: 5 })
|
|
144
149
|
* multipleSchema.parse(10) // ✓ Valid
|
|
145
150
|
* multipleSchema.parse(7) // ✗ Invalid
|
|
146
151
|
*
|
|
147
152
|
* // Precision control
|
|
148
|
-
* const precisionSchema = number({ precision: 2 })
|
|
153
|
+
* const precisionSchema = number(true, { precision: 2 })
|
|
149
154
|
* precisionSchema.parse(3.14) // ✓ Valid
|
|
150
155
|
* precisionSchema.parse(3.14159) // ✗ Invalid
|
|
151
156
|
*
|
|
152
157
|
* // Comma-separated parsing
|
|
153
|
-
* const commaSchema = number({ parseCommas: true })
|
|
158
|
+
* const commaSchema = number(false, { parseCommas: true })
|
|
154
159
|
* commaSchema.parse("1,234.56") // ✓ Valid (parsed as 1234.56)
|
|
155
160
|
*
|
|
156
161
|
* // Optional with default
|
|
157
|
-
* const optionalSchema = number({
|
|
158
|
-
* required: false,
|
|
159
|
-
* defaultValue: 0
|
|
160
|
-
* })
|
|
162
|
+
* const optionalSchema = number(false, { defaultValue: 0 })
|
|
161
163
|
* ```
|
|
162
164
|
*
|
|
163
165
|
* @throws {z.ZodError} When validation fails with specific error messages
|
|
164
166
|
* @see {@link NumberOptions} for all available configuration options
|
|
165
167
|
*/
|
|
166
|
-
export function number<IsRequired extends boolean =
|
|
168
|
+
export function number<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<NumberOptions<IsRequired>, 'required'>): NumberSchema<IsRequired> {
|
|
167
169
|
const {
|
|
168
|
-
required = true,
|
|
169
170
|
min,
|
|
170
171
|
max,
|
|
171
172
|
defaultValue,
|
|
@@ -182,6 +183,8 @@ export function number<IsRequired extends boolean = true>(options?: NumberOption
|
|
|
182
183
|
i18n,
|
|
183
184
|
} = options ?? {}
|
|
184
185
|
|
|
186
|
+
const isRequired = required ?? false as IsRequired
|
|
187
|
+
|
|
185
188
|
// Helper function to get custom message or fallback to default i18n
|
|
186
189
|
const getMessage = (key: keyof NumberMessages, params?: Record<string, any>) => {
|
|
187
190
|
if (i18n) {
|
|
@@ -242,7 +245,7 @@ export function number<IsRequired extends boolean = true>(options?: NumberOption
|
|
|
242
245
|
)
|
|
243
246
|
.refine((val) => {
|
|
244
247
|
// Required check first
|
|
245
|
-
if (
|
|
248
|
+
if (isRequired && val === null) {
|
|
246
249
|
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
247
250
|
}
|
|
248
251
|
|
|
@@ -85,7 +85,6 @@ export type PasswordStrength = "weak" | "medium" | "strong" | "very-strong"
|
|
|
85
85
|
* @property {Record<Locale, PasswordMessages>} [i18n] - Custom error messages for different locales
|
|
86
86
|
*/
|
|
87
87
|
export type PasswordOptions<IsRequired extends boolean = true> = {
|
|
88
|
-
required?: IsRequired
|
|
89
88
|
min?: number
|
|
90
89
|
max?: number
|
|
91
90
|
uppercase?: boolean
|
|
@@ -191,7 +190,8 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
|
|
|
191
190
|
* Creates a Zod schema for password validation with comprehensive security checks
|
|
192
191
|
*
|
|
193
192
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
194
|
-
* @param {
|
|
193
|
+
* @param {IsRequired} [required=false] - Whether the field is required
|
|
194
|
+
* @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
|
|
195
195
|
* @returns {PasswordSchema<IsRequired>} Zod schema for password validation
|
|
196
196
|
*
|
|
197
197
|
* @description
|
|
@@ -212,11 +212,18 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
|
|
|
212
212
|
* @example
|
|
213
213
|
* ```typescript
|
|
214
214
|
* // Basic password validation
|
|
215
|
-
* const basicSchema = password()
|
|
215
|
+
* const basicSchema = password() // optional by default
|
|
216
216
|
* basicSchema.parse("MyPassword123!") // ✓ Valid
|
|
217
|
+
* basicSchema.parse(null) // ✓ Valid (optional)
|
|
218
|
+
*
|
|
219
|
+
* // Required validation
|
|
220
|
+
* const requiredSchema = parse("MyPassword123!") // ✓ Valid
|
|
221
|
+
(true)
|
|
222
|
+
* requiredSchema.parse(null) // ✗ Invalid (required)
|
|
223
|
+
*
|
|
217
224
|
*
|
|
218
225
|
* // Strong password requirements
|
|
219
|
-
* const strongSchema = password({
|
|
226
|
+
* const strongSchema = password(false, {
|
|
220
227
|
* min: 12,
|
|
221
228
|
* uppercase: true,
|
|
222
229
|
* lowercase: true,
|
|
@@ -226,7 +233,7 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
|
|
|
226
233
|
* })
|
|
227
234
|
*
|
|
228
235
|
* // No common passwords
|
|
229
|
-
* const secureSchema = password({
|
|
236
|
+
* const secureSchema = password(false, {
|
|
230
237
|
* noCommonWords: true,
|
|
231
238
|
* noRepeating: true,
|
|
232
239
|
* noSequential: true
|
|
@@ -236,7 +243,7 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
|
|
|
236
243
|
* secureSchema.parse("abc123") // ✗ Invalid (sequential characters)
|
|
237
244
|
*
|
|
238
245
|
* // Custom requirements
|
|
239
|
-
* const customSchema = password({
|
|
246
|
+
* const customSchema = password(false, {
|
|
240
247
|
* min: 8,
|
|
241
248
|
* includes: "@", // Must contain @
|
|
242
249
|
* excludes: ["admin", "user"], // Cannot contain these words
|
|
@@ -244,13 +251,12 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
|
|
|
244
251
|
* })
|
|
245
252
|
*
|
|
246
253
|
* // Minimum strength requirement
|
|
247
|
-
* const strengthSchema = password({ minStrength: "very-strong" })
|
|
254
|
+
* const strengthSchema = password(false, { minStrength: "very-strong" })
|
|
248
255
|
* strengthSchema.parse("weak") // ✗ Invalid (insufficient strength)
|
|
249
256
|
* strengthSchema.parse("MyVeryStr0ng!P@ssw0rd2024") // ✓ Valid
|
|
250
257
|
*
|
|
251
258
|
* // Optional with default
|
|
252
|
-
* const optionalSchema = password({
|
|
253
|
-
* required: false,
|
|
259
|
+
* const optionalSchema = password(false, {
|
|
254
260
|
* defaultValue: null
|
|
255
261
|
* })
|
|
256
262
|
* ```
|
|
@@ -260,9 +266,8 @@ const calculatePasswordStrength = (password: string): PasswordStrength => {
|
|
|
260
266
|
* @see {@link PasswordStrength} for strength level definitions
|
|
261
267
|
* @see {@link calculatePasswordStrength} for strength calculation logic
|
|
262
268
|
*/
|
|
263
|
-
export function password<IsRequired extends boolean =
|
|
269
|
+
export function password<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<PasswordOptions<IsRequired>, 'required'>): PasswordSchema<IsRequired> {
|
|
264
270
|
const {
|
|
265
|
-
required = true,
|
|
266
271
|
min,
|
|
267
272
|
max,
|
|
268
273
|
uppercase,
|
|
@@ -281,8 +286,10 @@ export function password<IsRequired extends boolean = true>(options?: PasswordOp
|
|
|
281
286
|
i18n,
|
|
282
287
|
} = options ?? {}
|
|
283
288
|
|
|
289
|
+
const isRequired = required ?? false as IsRequired
|
|
290
|
+
|
|
284
291
|
// Set appropriate default value based on required flag
|
|
285
|
-
const actualDefaultValue = defaultValue ?? (
|
|
292
|
+
const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
|
|
286
293
|
|
|
287
294
|
// Helper function to get custom message or fallback to default i18n
|
|
288
295
|
const getMessage = (key: keyof PasswordMessages, params?: Record<string, any>) => {
|
|
@@ -311,13 +318,13 @@ export function password<IsRequired extends boolean = true>(options?: PasswordOp
|
|
|
311
318
|
return processed
|
|
312
319
|
}
|
|
313
320
|
|
|
314
|
-
const baseSchema =
|
|
321
|
+
const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
315
322
|
|
|
316
323
|
const schema = baseSchema.refine((val) => {
|
|
317
324
|
if (val === null) return true
|
|
318
325
|
|
|
319
326
|
// Required check
|
|
320
|
-
if (
|
|
327
|
+
if (isRequired && (val === "" || val === "null" || val === "undefined")) {
|
|
321
328
|
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
322
329
|
}
|
|
323
330
|
|
|
@@ -60,7 +60,6 @@ export type TextMessages = {
|
|
|
60
60
|
* @property {Record<Locale, TextMessages>} [i18n] - Custom error messages for different locales
|
|
61
61
|
*/
|
|
62
62
|
export type TextOptions<IsRequired extends boolean = true> = {
|
|
63
|
-
required?: IsRequired
|
|
64
63
|
minLength?: number
|
|
65
64
|
maxLength?: number
|
|
66
65
|
startsWith?: string
|
|
@@ -108,17 +107,23 @@ export type TextSchema<IsRequired extends boolean> = IsRequired extends true ? Z
|
|
|
108
107
|
*
|
|
109
108
|
* @example
|
|
110
109
|
* ```typescript
|
|
111
|
-
* // Basic text validation
|
|
110
|
+
* // Basic text validation (optional by default)
|
|
112
111
|
* const basicSchema = text()
|
|
113
112
|
* basicSchema.parse("Hello World") // ✓ Valid
|
|
113
|
+
* basicSchema.parse(null) // ✓ Valid (optional)
|
|
114
|
+
*
|
|
115
|
+
* // Required text
|
|
116
|
+
* const requiredSchema = text(true)
|
|
117
|
+
* requiredSchema.parse("Hello") // ✓ Valid
|
|
118
|
+
* requiredSchema.parse(null) // ✗ Invalid (required)
|
|
114
119
|
*
|
|
115
120
|
* // Length constraints
|
|
116
|
-
* const lengthSchema = text({ minLength: 3, maxLength: 50 })
|
|
121
|
+
* const lengthSchema = text(true, { minLength: 3, maxLength: 50 })
|
|
117
122
|
* lengthSchema.parse("Hello") // ✓ Valid
|
|
118
123
|
* lengthSchema.parse("Hi") // ✗ Invalid (too short)
|
|
119
124
|
*
|
|
120
125
|
* // Content validation
|
|
121
|
-
* const contentSchema = text({
|
|
126
|
+
* const contentSchema = text(true, {
|
|
122
127
|
* startsWith: "Hello",
|
|
123
128
|
* endsWith: "!",
|
|
124
129
|
* includes: "World"
|
|
@@ -126,38 +131,37 @@ export type TextSchema<IsRequired extends boolean> = IsRequired extends true ? Z
|
|
|
126
131
|
* contentSchema.parse("Hello World!") // ✓ Valid
|
|
127
132
|
*
|
|
128
133
|
* // Case transformation
|
|
129
|
-
* const upperSchema = text({ casing: "upper" })
|
|
134
|
+
* const upperSchema = text(false, { casing: "upper" })
|
|
130
135
|
* upperSchema.parse("hello") // ✓ Valid (converted to "HELLO")
|
|
131
136
|
*
|
|
132
137
|
* // Trim modes
|
|
133
|
-
* const trimStartSchema = text({ trimMode: "trimStart" })
|
|
138
|
+
* const trimStartSchema = text(false, { trimMode: "trimStart" })
|
|
134
139
|
* trimStartSchema.parse(" hello ") // ✓ Valid (result: "hello ")
|
|
135
140
|
*
|
|
136
141
|
* // Regex validation
|
|
137
|
-
* const regexSchema = text({ regex: /^[a-zA-Z]+$/ })
|
|
142
|
+
* const regexSchema = text(true, { regex: /^[a-zA-Z]+$/ })
|
|
138
143
|
* regexSchema.parse("hello") // ✓ Valid
|
|
139
144
|
* regexSchema.parse("hello123") // ✗ Invalid
|
|
140
145
|
*
|
|
141
146
|
* // Not empty (rejects whitespace-only)
|
|
142
|
-
* const notEmptySchema = text({ notEmpty: true })
|
|
147
|
+
* const notEmptySchema = text(true, { notEmpty: true })
|
|
143
148
|
* notEmptySchema.parse("hello") // ✓ Valid
|
|
144
149
|
* notEmptySchema.parse(" ") // ✗ Invalid
|
|
145
150
|
*
|
|
146
151
|
* // Optional with default
|
|
147
|
-
* const optionalSchema = text({
|
|
148
|
-
* required: false,
|
|
149
|
-
* defaultValue: "default text"
|
|
150
|
-
* })
|
|
152
|
+
* const optionalSchema = text(false, { defaultValue: "default text" })
|
|
151
153
|
* ```
|
|
152
154
|
*
|
|
153
155
|
* @throws {z.ZodError} When validation fails with specific error messages
|
|
154
156
|
* @see {@link TextOptions} for all available configuration options
|
|
155
157
|
*/
|
|
156
|
-
export function text<IsRequired extends boolean =
|
|
157
|
-
const {
|
|
158
|
+
export function text<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TextOptions<IsRequired>, 'required'>): TextSchema<IsRequired> {
|
|
159
|
+
const { minLength, maxLength, startsWith, endsWith, includes, excludes, regex, trimMode = "trim", casing = "none", transform, notEmpty, defaultValue, i18n } = options ?? {}
|
|
160
|
+
|
|
161
|
+
const isRequired = required ?? false as IsRequired
|
|
158
162
|
|
|
159
163
|
// Set appropriate default value based on required flag
|
|
160
|
-
const actualDefaultValue = defaultValue ?? (
|
|
164
|
+
const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
|
|
161
165
|
|
|
162
166
|
// Helper function to get custom message or fallback to default i18n
|
|
163
167
|
const getMessage = (key: keyof TextMessages, params?: Record<string, any>) => {
|
|
@@ -217,14 +221,14 @@ export function text<IsRequired extends boolean = true>(options?: TextOptions<Is
|
|
|
217
221
|
return processed
|
|
218
222
|
}
|
|
219
223
|
|
|
220
|
-
const baseSchema =
|
|
224
|
+
const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
221
225
|
|
|
222
226
|
// Single refine with all validations for better performance
|
|
223
227
|
const schema = baseSchema.refine((val) => {
|
|
224
228
|
if (val === null) return true
|
|
225
229
|
|
|
226
230
|
// Required check
|
|
227
|
-
if (
|
|
231
|
+
if (isRequired && (val === "" || val === "null" || val === "undefined")) {
|
|
228
232
|
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
229
233
|
}
|
|
230
234
|
|
|
@@ -92,7 +92,6 @@ export type TimeFormat =
|
|
|
92
92
|
* @property {Record<Locale, TimeMessages>} [i18n] - Custom error messages for different locales
|
|
93
93
|
*/
|
|
94
94
|
export type TimeOptions<IsRequired extends boolean = true> = {
|
|
95
|
-
required?: IsRequired
|
|
96
95
|
format?: TimeFormat
|
|
97
96
|
min?: string // Minimum time (e.g., "09:00")
|
|
98
97
|
max?: string // Maximum time (e.g., "17:00")
|
|
@@ -284,7 +283,8 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
|
|
|
284
283
|
* Creates a Zod schema for time validation with comprehensive options
|
|
285
284
|
*
|
|
286
285
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
287
|
-
* @param {
|
|
286
|
+
* @param {IsRequired} [required=false] - Whether the field is required
|
|
287
|
+
* @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
|
|
288
288
|
* @returns {TimeSchema<IsRequired>} Zod schema for time validation
|
|
289
289
|
*
|
|
290
290
|
* @description
|
|
@@ -306,10 +306,17 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
|
|
|
306
306
|
* // Basic time validation (24-hour format)
|
|
307
307
|
* const basicSchema = time()
|
|
308
308
|
* basicSchema.parse("14:30") // ✓ Valid
|
|
309
|
+
* basicSchema.parse(null) // ✓ Valid (optional)
|
|
310
|
+
*
|
|
311
|
+
* // Required validation
|
|
312
|
+
* const requiredSchema = parse("14:30") // ✓ Valid
|
|
313
|
+
(true)
|
|
314
|
+
* requiredSchema.parse(null) // ✗ Invalid (required)
|
|
315
|
+
*
|
|
309
316
|
* basicSchema.parse("2:30 PM") // ✗ Invalid (wrong format)
|
|
310
317
|
*
|
|
311
318
|
* // 12-hour format with AM/PM
|
|
312
|
-
* const ampmSchema = time({ format: "hh:mm A" })
|
|
319
|
+
* const ampmSchema = time(false, { format: "hh:mm A" })
|
|
313
320
|
* ampmSchema.parse("02:30 PM") // ✓ Valid
|
|
314
321
|
* ampmSchema.parse("14:30") // ✗ Invalid (wrong format)
|
|
315
322
|
*
|
|
@@ -325,7 +332,7 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
|
|
|
325
332
|
* businessHours.parse("09:05") // ✗ Invalid (not 15-minute step)
|
|
326
333
|
*
|
|
327
334
|
* // Time range validation
|
|
328
|
-
* const timeRangeSchema = time({
|
|
335
|
+
* const timeRangeSchema = time(false, {
|
|
329
336
|
* min: "09:00",
|
|
330
337
|
* max: "17:00"
|
|
331
338
|
* })
|
|
@@ -340,7 +347,7 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
|
|
|
340
347
|
* specificHours.parse("11:30") // ✗ Invalid (hour not allowed)
|
|
341
348
|
*
|
|
342
349
|
* // Whitelist specific times
|
|
343
|
-
* const whitelistSchema = time({
|
|
350
|
+
* const whitelistSchema = time(false, {
|
|
344
351
|
* whitelist: ["09:00", "12:00", "17:00"],
|
|
345
352
|
* whitelistOnly: true
|
|
346
353
|
* })
|
|
@@ -348,8 +355,7 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
|
|
|
348
355
|
* whitelistSchema.parse("13:00") // ✗ Invalid (not in whitelist)
|
|
349
356
|
*
|
|
350
357
|
* // Optional with default
|
|
351
|
-
* const optionalSchema = time({
|
|
352
|
-
* required: false,
|
|
358
|
+
* const optionalSchema = time(false, {
|
|
353
359
|
* defaultValue: "09:00"
|
|
354
360
|
* })
|
|
355
361
|
* optionalSchema.parse("") // ✓ Valid (returns "09:00")
|
|
@@ -359,9 +365,8 @@ const normalizeTime = (timeStr: string, format: TimeFormat): string | null => {
|
|
|
359
365
|
* @see {@link TimeOptions} for all available configuration options
|
|
360
366
|
* @see {@link TimeFormat} for supported time formats
|
|
361
367
|
*/
|
|
362
|
-
export function time<IsRequired extends boolean =
|
|
368
|
+
export function time<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TimeOptions<IsRequired>, 'required'>): TimeSchema<IsRequired> {
|
|
363
369
|
const {
|
|
364
|
-
required = true,
|
|
365
370
|
format = "HH:mm",
|
|
366
371
|
min,
|
|
367
372
|
max,
|
|
@@ -382,8 +387,10 @@ export function time<IsRequired extends boolean = true>(options?: TimeOptions<Is
|
|
|
382
387
|
i18n,
|
|
383
388
|
} = options ?? {}
|
|
384
389
|
|
|
390
|
+
const isRequired = required ?? false as IsRequired
|
|
391
|
+
|
|
385
392
|
// Set appropriate default value based on required flag
|
|
386
|
-
const actualDefaultValue = defaultValue ?? (
|
|
393
|
+
const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
|
|
387
394
|
|
|
388
395
|
// Helper function to get custom message or fallback to default i18n
|
|
389
396
|
const getMessage = (key: keyof TimeMessages, params?: Record<string, any>) => {
|
|
@@ -429,7 +436,7 @@ export function time<IsRequired extends boolean = true>(options?: TimeOptions<Is
|
|
|
429
436
|
return ""
|
|
430
437
|
}
|
|
431
438
|
// If the field is optional and empty string not in whitelist, return default value
|
|
432
|
-
if (!
|
|
439
|
+
if (!isRequired) {
|
|
433
440
|
return actualDefaultValue
|
|
434
441
|
}
|
|
435
442
|
// If a field is required, return the default value (will be validated later)
|
|
@@ -456,18 +463,18 @@ export function time<IsRequired extends boolean = true>(options?: TimeOptions<Is
|
|
|
456
463
|
return processed
|
|
457
464
|
}
|
|
458
465
|
|
|
459
|
-
const baseSchema =
|
|
466
|
+
const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
460
467
|
|
|
461
468
|
const schema = baseSchema.refine((val) => {
|
|
462
469
|
if (val === null) return true
|
|
463
470
|
|
|
464
471
|
// Required check
|
|
465
|
-
if (
|
|
472
|
+
if (isRequired && (val === "" || val === "null" || val === "undefined")) {
|
|
466
473
|
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
467
474
|
}
|
|
468
475
|
|
|
469
476
|
if (val === null) return true
|
|
470
|
-
if (!
|
|
477
|
+
if (!isRequired && val === "") return true
|
|
471
478
|
|
|
472
479
|
// Whitelist check
|
|
473
480
|
if (whitelist && whitelist.length > 0) {
|
|
@@ -85,7 +85,6 @@ export type UrlMessages = {
|
|
|
85
85
|
* @property {Record<Locale, UrlMessages>} [i18n] - Custom error messages for different locales
|
|
86
86
|
*/
|
|
87
87
|
export type UrlOptions<IsRequired extends boolean = true> = {
|
|
88
|
-
required?: IsRequired
|
|
89
88
|
min?: number
|
|
90
89
|
max?: number
|
|
91
90
|
includes?: string
|
|
@@ -121,7 +120,8 @@ export type UrlSchema<IsRequired extends boolean> = IsRequired extends true ? Zo
|
|
|
121
120
|
* Creates a Zod schema for URL validation with comprehensive constraints
|
|
122
121
|
*
|
|
123
122
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
124
|
-
* @param {
|
|
123
|
+
* @param {IsRequired} [required=false] - Whether the field is required
|
|
124
|
+
* @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
|
|
125
125
|
* @returns {UrlSchema<IsRequired>} Zod schema for URL validation
|
|
126
126
|
*
|
|
127
127
|
* @description
|
|
@@ -145,43 +145,49 @@ export type UrlSchema<IsRequired extends boolean> = IsRequired extends true ? Zo
|
|
|
145
145
|
* @example
|
|
146
146
|
* ```typescript
|
|
147
147
|
* // Basic URL validation
|
|
148
|
-
* const basicSchema = url()
|
|
148
|
+
* const basicSchema = url() // optional by default
|
|
149
149
|
* basicSchema.parse("https://example.com") // ✓ Valid
|
|
150
|
+
* basicSchema.parse(null) // ✓ Valid (optional)
|
|
151
|
+
*
|
|
152
|
+
* // Required validation
|
|
153
|
+
* const requiredSchema = parse("https://example.com") // ✓ Valid
|
|
154
|
+
(true)
|
|
155
|
+
* requiredSchema.parse(null) // ✗ Invalid (required)
|
|
156
|
+
*
|
|
150
157
|
*
|
|
151
158
|
* // HTTPS only
|
|
152
|
-
* const httpsSchema = url({ protocols: ["https"] })
|
|
159
|
+
* const httpsSchema = url(false, { protocols: ["https"] })
|
|
153
160
|
* httpsSchema.parse("https://example.com") // ✓ Valid
|
|
154
161
|
* httpsSchema.parse("http://example.com") // ✗ Invalid
|
|
155
162
|
*
|
|
156
163
|
* // Domain restriction
|
|
157
|
-
* const domainSchema = url({
|
|
164
|
+
* const domainSchema = url(false, {
|
|
158
165
|
* allowedDomains: ["company.com", "trusted.org"]
|
|
159
166
|
* })
|
|
160
167
|
* domainSchema.parse("https://app.company.com") // ✓ Valid (subdomain)
|
|
161
168
|
* domainSchema.parse("https://example.com") // ✗ Invalid
|
|
162
169
|
*
|
|
163
170
|
* // Block localhost
|
|
164
|
-
* const noLocalhostSchema = url({ blockLocalhost: true })
|
|
171
|
+
* const noLocalhostSchema = url(false, { blockLocalhost: true })
|
|
165
172
|
* noLocalhostSchema.parse("https://example.com") // ✓ Valid
|
|
166
173
|
* noLocalhostSchema.parse("http://localhost:3000") // ✗ Invalid
|
|
167
174
|
*
|
|
168
175
|
* // API endpoints with path requirements
|
|
169
|
-
* const apiSchema = url({
|
|
176
|
+
* const apiSchema = url(false, {
|
|
170
177
|
* pathStartsWith: "/api/",
|
|
171
178
|
* mustHaveQuery: true
|
|
172
179
|
* })
|
|
173
180
|
* apiSchema.parse("https://api.com/api/users?page=1") // ✓ Valid
|
|
174
181
|
*
|
|
175
182
|
* // Port restrictions
|
|
176
|
-
* const portSchema = url({
|
|
183
|
+
* const portSchema = url(false, {
|
|
177
184
|
* allowedPorts: [80, 443, 8080]
|
|
178
185
|
* })
|
|
179
186
|
* portSchema.parse("https://example.com:443") // ✓ Valid
|
|
180
187
|
* portSchema.parse("https://example.com:3000") // ✗ Invalid
|
|
181
188
|
*
|
|
182
189
|
* // Optional with default
|
|
183
|
-
* const optionalSchema = url({
|
|
184
|
-
* required: false,
|
|
190
|
+
* const optionalSchema = url(false, {
|
|
185
191
|
* defaultValue: null
|
|
186
192
|
* })
|
|
187
193
|
* ```
|
|
@@ -189,9 +195,8 @@ export type UrlSchema<IsRequired extends boolean> = IsRequired extends true ? Zo
|
|
|
189
195
|
* @throws {z.ZodError} When validation fails with specific error messages
|
|
190
196
|
* @see {@link UrlOptions} for all available configuration options
|
|
191
197
|
*/
|
|
192
|
-
export function url<IsRequired extends boolean =
|
|
198
|
+
export function url<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<UrlOptions<IsRequired>, 'required'>): UrlSchema<IsRequired> {
|
|
193
199
|
const {
|
|
194
|
-
required = true,
|
|
195
200
|
min,
|
|
196
201
|
max,
|
|
197
202
|
includes,
|
|
@@ -214,7 +219,9 @@ export function url<IsRequired extends boolean = true>(options?: UrlOptions<IsRe
|
|
|
214
219
|
i18n,
|
|
215
220
|
} = options ?? {}
|
|
216
221
|
|
|
217
|
-
const
|
|
222
|
+
const isRequired = required ?? false as IsRequired
|
|
223
|
+
|
|
224
|
+
const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
|
|
218
225
|
|
|
219
226
|
// Helper function to get custom message or fallback to default i18n
|
|
220
227
|
const getMessage = (key: keyof UrlMessages, params?: Record<string, any>) => {
|
|
@@ -244,13 +251,13 @@ export function url<IsRequired extends boolean = true>(options?: UrlOptions<IsRe
|
|
|
244
251
|
return processed
|
|
245
252
|
}
|
|
246
253
|
|
|
247
|
-
const baseSchema =
|
|
254
|
+
const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
248
255
|
|
|
249
256
|
const schema = baseSchema.refine((val) => {
|
|
250
257
|
if (val === null) return true
|
|
251
258
|
|
|
252
259
|
// Required check
|
|
253
|
-
if (
|
|
260
|
+
if (isRequired && (val === "" || val === "null" || val === "undefined")) {
|
|
254
261
|
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
255
262
|
}
|
|
256
263
|
|
|
@@ -36,7 +36,6 @@ export type BusinessIdMessages = {
|
|
|
36
36
|
* @property {Record<Locale, BusinessIdMessages>} [i18n] - Custom error messages for different locales
|
|
37
37
|
*/
|
|
38
38
|
export type BusinessIdOptions<IsRequired extends boolean = true> = {
|
|
39
|
-
required?: IsRequired
|
|
40
39
|
transform?: (value: string) => string
|
|
41
40
|
defaultValue?: IsRequired extends true ? string : string | null
|
|
42
41
|
i18n?: Record<Locale, BusinessIdMessages>
|
|
@@ -148,23 +147,30 @@ const validateTaiwanBusinessId = (value: string): boolean => {
|
|
|
148
147
|
* @example
|
|
149
148
|
* ```typescript
|
|
150
149
|
* // Basic business ID validation
|
|
151
|
-
* const basicSchema = businessId()
|
|
150
|
+
* const basicSchema = businessId() // optional by default
|
|
152
151
|
* basicSchema.parse("12345675") // ✓ Valid (if checksum correct)
|
|
152
|
+
* basicSchema.parse(null) // ✓ Valid (optional)
|
|
153
|
+
*
|
|
154
|
+
* // Required validation
|
|
155
|
+
* const requiredSchema = parse("12345675") // ✓ Valid (if checksum correct)
|
|
156
|
+
(true)
|
|
157
|
+
* requiredSchema.parse(null) // ✗ Invalid (required)
|
|
158
|
+
*
|
|
153
159
|
* basicSchema.parse("1234567") // ✗ Invalid (not 8 digits)
|
|
154
160
|
*
|
|
155
161
|
* // Optional business ID
|
|
156
|
-
* const optionalSchema = businessId(
|
|
162
|
+
* const optionalSchema = businessId(false)
|
|
157
163
|
* optionalSchema.parse("") // ✓ Valid (returns null)
|
|
158
164
|
* optionalSchema.parse("12345675") // ✓ Valid (if checksum correct)
|
|
159
165
|
*
|
|
160
166
|
* // With custom transformation
|
|
161
|
-
* const transformSchema = businessId({
|
|
167
|
+
* const transformSchema = businessId(false, {
|
|
162
168
|
* transform: (value) => value.replace(/[^0-9]/g, '') // Remove non-digits
|
|
163
169
|
* })
|
|
164
170
|
* transformSchema.parse("1234-5675") // ✓ Valid (if checksum correct after cleaning)
|
|
165
171
|
*
|
|
166
172
|
* // With custom error messages
|
|
167
|
-
* const customSchema = businessId({
|
|
173
|
+
* const customSchema = businessId(false, {
|
|
168
174
|
* i18n: {
|
|
169
175
|
* en: { invalid: "Please enter a valid Taiwan Business ID" },
|
|
170
176
|
* 'zh-TW': { invalid: "請輸入有效的統一編號" }
|
|
@@ -176,16 +182,17 @@ const validateTaiwanBusinessId = (value: string): boolean => {
|
|
|
176
182
|
* @see {@link BusinessIdOptions} for all available configuration options
|
|
177
183
|
* @see {@link validateTaiwanBusinessId} for validation logic details
|
|
178
184
|
*/
|
|
179
|
-
export function businessId<IsRequired extends boolean =
|
|
185
|
+
export function businessId<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<BusinessIdOptions<IsRequired>, 'required'>): BusinessIdSchema<IsRequired> {
|
|
180
186
|
const {
|
|
181
|
-
required = true,
|
|
182
187
|
transform,
|
|
183
188
|
defaultValue,
|
|
184
189
|
i18n
|
|
185
190
|
} = options ?? {}
|
|
186
191
|
|
|
192
|
+
const isRequired = required ?? false as IsRequired
|
|
193
|
+
|
|
187
194
|
// Set appropriate default value based on required flag
|
|
188
|
-
const actualDefaultValue = defaultValue ?? (
|
|
195
|
+
const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
|
|
189
196
|
|
|
190
197
|
// Helper function to get custom message or fallback to default i18n
|
|
191
198
|
const getMessage = (key: keyof BusinessIdMessages, params?: Record<string, any>) => {
|
|
@@ -220,18 +227,18 @@ export function businessId<IsRequired extends boolean = true>(options?: Business
|
|
|
220
227
|
return processed
|
|
221
228
|
}
|
|
222
229
|
|
|
223
|
-
const baseSchema =
|
|
230
|
+
const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
224
231
|
|
|
225
232
|
const schema = baseSchema.refine((val) => {
|
|
226
233
|
if (val === null) return true
|
|
227
234
|
|
|
228
235
|
// Required check
|
|
229
|
-
if (
|
|
236
|
+
if (isRequired && (val === "" || val === "null" || val === "undefined")) {
|
|
230
237
|
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
231
238
|
}
|
|
232
239
|
|
|
233
240
|
if (val === null) return true
|
|
234
|
-
if (!
|
|
241
|
+
if (!isRequired && val === "") return true
|
|
235
242
|
|
|
236
243
|
// Taiwan Business ID format validation (8 digits + checksum)
|
|
237
244
|
if (!validateTaiwanBusinessId(val)) {
|