@hy_ong/zod-kit 0.0.4 → 0.0.6
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 +28 -0
- package/LICENSE +21 -0
- package/README.md +465 -97
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +3127 -146
- package/dist/index.d.cts +3021 -25
- package/dist/index.d.ts +3021 -25
- package/dist/index.js +3081 -144
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +161 -25
- package/src/i18n/locales/zh-TW.json +165 -26
- package/src/index.ts +17 -7
- package/src/validators/common/boolean.ts +191 -0
- package/src/validators/common/date.ts +299 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +313 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +471 -0
- package/src/validators/common/number.ts +319 -0
- package/src/validators/common/password.ts +386 -0
- package/src/validators/common/text.ts +271 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +347 -0
- package/src/validators/taiwan/business-id.ts +262 -0
- package/src/validators/taiwan/fax.ts +327 -0
- package/src/validators/taiwan/mobile.ts +242 -0
- package/src/validators/taiwan/national-id.ts +425 -0
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +330 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/file.test.ts +479 -0
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +271 -44
- package/tests/common/text.test.ts +210 -13
- package/tests/common/time.test.ts +528 -0
- package/tests/common/url.test.ts +492 -67
- package/tests/taiwan/business-id.test.ts +240 -0
- package/tests/taiwan/fax.test.ts +463 -0
- package/tests/taiwan/mobile.test.ts +373 -0
- package/tests/taiwan/national-id.test.ts +435 -0
- package/tests/taiwan/postal-code.test.ts +705 -0
- package/tests/taiwan/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -36
- package/src/common/date.ts +0 -43
- package/src/common/email.ts +0 -44
- package/src/common/integer.ts +0 -46
- package/src/common/number.ts +0 -37
- package/src/common/password.ts +0 -33
- package/src/common/text.ts +0 -34
- package/src/common/url.ts +0 -37
- package/tests/common/integer.test.ts +0 -90
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ID validator for Zod Kit
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive ID validation with support for multiple ID formats,
|
|
5
|
+
* auto-detection, custom patterns, and flexible validation options.
|
|
6
|
+
*
|
|
7
|
+
* @author Ong Hoe Yuan
|
|
8
|
+
* @version 0.0.5
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
12
|
+
import { t } from "../../i18n"
|
|
13
|
+
import { getLocale, type Locale } from "../../config"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type definition for ID validation error messages
|
|
17
|
+
*
|
|
18
|
+
* @interface IdMessages
|
|
19
|
+
* @property {string} [required] - Message when field is required but empty
|
|
20
|
+
* @property {string} [invalid] - Message when ID format is invalid
|
|
21
|
+
* @property {string} [minLength] - Message when ID is too short
|
|
22
|
+
* @property {string} [maxLength] - Message when ID is too long
|
|
23
|
+
* @property {string} [numeric] - Message when numeric ID format is invalid
|
|
24
|
+
* @property {string} [uuid] - Message when UUID format is invalid
|
|
25
|
+
* @property {string} [objectId] - Message when MongoDB ObjectId format is invalid
|
|
26
|
+
* @property {string} [nanoid] - Message when Nano ID format is invalid
|
|
27
|
+
* @property {string} [snowflake] - Message when Snowflake ID format is invalid
|
|
28
|
+
* @property {string} [cuid] - Message when CUID format is invalid
|
|
29
|
+
* @property {string} [ulid] - Message when ULID format is invalid
|
|
30
|
+
* @property {string} [shortid] - Message when ShortId format is invalid
|
|
31
|
+
* @property {string} [customFormat] - Message when custom regex format is invalid
|
|
32
|
+
* @property {string} [includes] - Message when ID doesn't contain required string
|
|
33
|
+
* @property {string} [excludes] - Message when ID contains forbidden string
|
|
34
|
+
* @property {string} [startsWith] - Message when ID doesn't start with required string
|
|
35
|
+
* @property {string} [endsWith] - Message when ID doesn't end with required string
|
|
36
|
+
*/
|
|
37
|
+
export type IdMessages = {
|
|
38
|
+
required?: string
|
|
39
|
+
invalid?: string
|
|
40
|
+
minLength?: string
|
|
41
|
+
maxLength?: string
|
|
42
|
+
numeric?: string
|
|
43
|
+
uuid?: string
|
|
44
|
+
objectId?: string
|
|
45
|
+
nanoid?: string
|
|
46
|
+
snowflake?: string
|
|
47
|
+
cuid?: string
|
|
48
|
+
ulid?: string
|
|
49
|
+
shortid?: string
|
|
50
|
+
customFormat?: string
|
|
51
|
+
includes?: string
|
|
52
|
+
excludes?: string
|
|
53
|
+
startsWith?: string
|
|
54
|
+
endsWith?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Supported ID types for validation
|
|
59
|
+
*
|
|
60
|
+
* @typedef {string} IdType
|
|
61
|
+
*
|
|
62
|
+
* Available types:
|
|
63
|
+
* - numeric: Pure numeric IDs (1, 123, 999999)
|
|
64
|
+
* - uuid: UUID v4 format (xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
|
|
65
|
+
* - objectId: MongoDB ObjectId (24-character hexadecimal)
|
|
66
|
+
* - nanoid: Nano ID format (21-character URL-safe)
|
|
67
|
+
* - snowflake: Twitter Snowflake (19-digit number)
|
|
68
|
+
* - cuid: CUID format (25-character starting with 'c')
|
|
69
|
+
* - ulid: ULID format (26-character case-insensitive)
|
|
70
|
+
* - shortid: ShortId format (7-14 character URL-safe)
|
|
71
|
+
* - auto: Auto-detect format from the value
|
|
72
|
+
*/
|
|
73
|
+
export type IdType =
|
|
74
|
+
| "numeric" // Pure numeric IDs (1, 123, 999999)
|
|
75
|
+
| "uuid" // UUID v4 format
|
|
76
|
+
| "objectId" // MongoDB ObjectId (24-character hexadecimal)
|
|
77
|
+
| "nanoid" // Nano ID
|
|
78
|
+
| "snowflake" // Twitter Snowflake (19-digit number)
|
|
79
|
+
| "cuid" // CUID format
|
|
80
|
+
| "ulid" // ULID format
|
|
81
|
+
| "shortid" // ShortId format
|
|
82
|
+
| "auto" // Auto-detect format
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Configuration options for ID validation
|
|
86
|
+
*
|
|
87
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
88
|
+
*
|
|
89
|
+
* @interface IdOptions
|
|
90
|
+
* @property {IsRequired} [required=true] - Whether the field is required
|
|
91
|
+
* @property {IdType} [type="auto"] - Expected ID type or auto-detection
|
|
92
|
+
* @property {number} [minLength] - Minimum length of ID
|
|
93
|
+
* @property {number} [maxLength] - Maximum length of ID
|
|
94
|
+
* @property {IdType[]} [allowedTypes] - Multiple allowed ID types (overrides type)
|
|
95
|
+
* @property {RegExp} [customRegex] - Custom regex pattern (overrides type validation)
|
|
96
|
+
* @property {string} [includes] - String that must be included in ID
|
|
97
|
+
* @property {string | string[]} [excludes] - String(s) that must not be included
|
|
98
|
+
* @property {string} [startsWith] - String that ID must start with
|
|
99
|
+
* @property {string} [endsWith] - String that ID must end with
|
|
100
|
+
* @property {boolean} [caseSensitive=true] - Whether validation is case-sensitive
|
|
101
|
+
* @property {Function} [transform] - Custom transformation function for ID
|
|
102
|
+
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
103
|
+
* @property {Record<Locale, IdMessages>} [i18n] - Custom error messages for different locales
|
|
104
|
+
*/
|
|
105
|
+
export type IdOptions<IsRequired extends boolean = true> = {
|
|
106
|
+
required?: IsRequired
|
|
107
|
+
type?: IdType
|
|
108
|
+
minLength?: number
|
|
109
|
+
maxLength?: number
|
|
110
|
+
allowedTypes?: IdType[]
|
|
111
|
+
customRegex?: RegExp
|
|
112
|
+
includes?: string
|
|
113
|
+
excludes?: string | string[]
|
|
114
|
+
startsWith?: string
|
|
115
|
+
endsWith?: string
|
|
116
|
+
caseSensitive?: boolean
|
|
117
|
+
transform?: (value: string) => string
|
|
118
|
+
defaultValue?: IsRequired extends true ? string : string | null
|
|
119
|
+
i18n?: Record<Locale, IdMessages>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Type alias for ID validation schema based on required flag
|
|
124
|
+
*
|
|
125
|
+
* @template IsRequired - Whether the field is required
|
|
126
|
+
* @typedef IdSchema
|
|
127
|
+
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
128
|
+
*/
|
|
129
|
+
export type IdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Regular expression patterns for different ID formats
|
|
133
|
+
*
|
|
134
|
+
* @constant {Record<string, RegExp>} ID_PATTERNS
|
|
135
|
+
* @description Maps each ID type to its corresponding regex pattern
|
|
136
|
+
*/
|
|
137
|
+
const ID_PATTERNS = {
|
|
138
|
+
numeric: /^\d+$/,
|
|
139
|
+
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
140
|
+
objectId: /^[0-9a-f]{24}$/i,
|
|
141
|
+
nanoid: /^[A-Za-z0-9_-]{21}$/,
|
|
142
|
+
snowflake: /^\d{19}$/,
|
|
143
|
+
cuid: /^c[a-z0-9]{24}$/,
|
|
144
|
+
ulid: /^[0-9A-HJKMNP-TV-Z]{26}$/,
|
|
145
|
+
shortid: /^[A-Za-z0-9_-]{7,14}$/,
|
|
146
|
+
} as const
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Detects the ID type of a given value using pattern matching
|
|
150
|
+
*
|
|
151
|
+
* @param {string} value - The ID value to analyze
|
|
152
|
+
* @returns {IdType | null} The detected ID type or null if no pattern matches
|
|
153
|
+
*
|
|
154
|
+
* @description
|
|
155
|
+
* Attempts to identify the ID type by testing against known patterns.
|
|
156
|
+
* Patterns are ordered by specificity to avoid false positives.
|
|
157
|
+
* More specific patterns (UUID, ObjectId) are tested before generic ones (numeric, shortid).
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* detectIdType("550e8400-e29b-41d4-a716-446655440000") // "uuid"
|
|
162
|
+
* detectIdType("507f1f77bcf86cd799439011") // "objectId"
|
|
163
|
+
* detectIdType("V1StGXR8_Z5jdHi6B-myT") // "nanoid"
|
|
164
|
+
* detectIdType("123456789") // "numeric"
|
|
165
|
+
* detectIdType("invalid-id") // null
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
const detectIdType = (value: string): IdType | null => {
|
|
169
|
+
// 按優先順序檢查(從最具體到最通用)
|
|
170
|
+
const orderedTypes: Array<[IdType, RegExp]> = [
|
|
171
|
+
["uuid", ID_PATTERNS.uuid],
|
|
172
|
+
["objectId", ID_PATTERNS.objectId],
|
|
173
|
+
["snowflake", ID_PATTERNS.snowflake],
|
|
174
|
+
["cuid", ID_PATTERNS.cuid],
|
|
175
|
+
["ulid", ID_PATTERNS.ulid],
|
|
176
|
+
["nanoid", ID_PATTERNS.nanoid],
|
|
177
|
+
["numeric", ID_PATTERNS.numeric],
|
|
178
|
+
["shortid", ID_PATTERNS.shortid], // 放最後,因為最通用
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
for (const [type, pattern] of orderedTypes) {
|
|
182
|
+
if (pattern.test(value)) {
|
|
183
|
+
return type
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Validates if a value matches the specified ID type
|
|
191
|
+
*
|
|
192
|
+
* @param {string} value - The ID value to validate
|
|
193
|
+
* @param {IdType} type - The expected ID type
|
|
194
|
+
* @returns {boolean} True if the value matches the specified type
|
|
195
|
+
*
|
|
196
|
+
* @description
|
|
197
|
+
* Validates a specific ID type using regex patterns or auto-detection.
|
|
198
|
+
* For "auto" type, uses detectIdType to check if any known pattern matches.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* validateIdType("123456", "numeric") // true
|
|
203
|
+
* validateIdType("abc123", "numeric") // false
|
|
204
|
+
* validateIdType("550e8400-e29b-41d4-a716-446655440000", "uuid") // true
|
|
205
|
+
* validateIdType("invalid-uuid", "uuid") // false
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
const validateIdType = (value: string, type: IdType): boolean => {
|
|
209
|
+
if (type === "auto") {
|
|
210
|
+
return detectIdType(value) !== null
|
|
211
|
+
}
|
|
212
|
+
const pattern = ID_PATTERNS[type as keyof typeof ID_PATTERNS]
|
|
213
|
+
return pattern ? pattern.test(value) : false
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Creates a Zod schema for ID validation with comprehensive format support
|
|
218
|
+
*
|
|
219
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
220
|
+
* @param {IdOptions<IsRequired>} [options] - Configuration options for ID validation
|
|
221
|
+
* @returns {IdSchema<IsRequired>} Zod schema for ID validation
|
|
222
|
+
*
|
|
223
|
+
* @description
|
|
224
|
+
* Creates a comprehensive ID validator with support for multiple ID formats,
|
|
225
|
+
* auto-detection, custom patterns, and flexible validation options.
|
|
226
|
+
*
|
|
227
|
+
* Features:
|
|
228
|
+
* - Multiple ID format support (UUID, ObjectId, Snowflake, etc.)
|
|
229
|
+
* - Auto-detection of ID types
|
|
230
|
+
* - Custom regex pattern support
|
|
231
|
+
* - Length validation
|
|
232
|
+
* - Content validation (includes, excludes, startsWith, endsWith)
|
|
233
|
+
* - Case sensitivity control
|
|
234
|
+
* - Multiple allowed types
|
|
235
|
+
* - Custom transformation functions
|
|
236
|
+
* - Comprehensive internationalization
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* // Auto-detect ID format
|
|
241
|
+
* const autoSchema = id()
|
|
242
|
+
* autoSchema.parse("550e8400-e29b-41d4-a716-446655440000") // ✓ Valid (UUID)
|
|
243
|
+
* autoSchema.parse("507f1f77bcf86cd799439011") // ✓ Valid (ObjectId)
|
|
244
|
+
* autoSchema.parse("123456") // ✓ Valid (numeric)
|
|
245
|
+
*
|
|
246
|
+
* // Specific ID type
|
|
247
|
+
* const uuidSchema = id({ type: "uuid" })
|
|
248
|
+
* uuidSchema.parse("550e8400-e29b-41d4-a716-446655440000") // ✓ Valid
|
|
249
|
+
* uuidSchema.parse("invalid-uuid") // ✗ Invalid
|
|
250
|
+
*
|
|
251
|
+
* // Multiple allowed types
|
|
252
|
+
* const multiSchema = id({ allowedTypes: ["uuid", "objectId"] })
|
|
253
|
+
* multiSchema.parse("550e8400-e29b-41d4-a716-446655440000") // ✓ Valid (UUID)
|
|
254
|
+
* multiSchema.parse("507f1f77bcf86cd799439011") // ✓ Valid (ObjectId)
|
|
255
|
+
* multiSchema.parse("123456") // ✗ Invalid (numeric not allowed)
|
|
256
|
+
*
|
|
257
|
+
* // Custom regex pattern
|
|
258
|
+
* const customSchema = id({ customRegex: /^CUST_\d{6}$/ })
|
|
259
|
+
* customSchema.parse("CUST_123456") // ✓ Valid
|
|
260
|
+
* customSchema.parse("invalid") // ✗ Invalid
|
|
261
|
+
*
|
|
262
|
+
* // Content validation
|
|
263
|
+
* const prefixSchema = id({
|
|
264
|
+
* type: "auto",
|
|
265
|
+
* startsWith: "user_",
|
|
266
|
+
* minLength: 10
|
|
267
|
+
* })
|
|
268
|
+
* prefixSchema.parse("user_123456") // ✓ Valid
|
|
269
|
+
*
|
|
270
|
+
* // Case insensitive
|
|
271
|
+
* const caseInsensitiveSchema = id({
|
|
272
|
+
* type: "uuid",
|
|
273
|
+
* caseSensitive: false
|
|
274
|
+
* })
|
|
275
|
+
* caseInsensitiveSchema.parse("550E8400-E29B-41D4-A716-446655440000") // ✓ Valid
|
|
276
|
+
*
|
|
277
|
+
* // Optional with default
|
|
278
|
+
* const optionalSchema = id({
|
|
279
|
+
* required: false,
|
|
280
|
+
* defaultValue: null
|
|
281
|
+
* })
|
|
282
|
+
* ```
|
|
283
|
+
*
|
|
284
|
+
* @throws {z.ZodError} When validation fails with specific error messages
|
|
285
|
+
* @see {@link IdOptions} for all available configuration options
|
|
286
|
+
* @see {@link IdType} for supported ID types
|
|
287
|
+
* @see {@link detectIdType} for auto-detection logic
|
|
288
|
+
* @see {@link validateIdType} for type-specific validation
|
|
289
|
+
*/
|
|
290
|
+
export function id<IsRequired extends boolean = true>(options?: IdOptions<IsRequired>): IdSchema<IsRequired> {
|
|
291
|
+
const {
|
|
292
|
+
required = true,
|
|
293
|
+
type = "auto",
|
|
294
|
+
minLength,
|
|
295
|
+
maxLength,
|
|
296
|
+
allowedTypes,
|
|
297
|
+
customRegex,
|
|
298
|
+
includes,
|
|
299
|
+
excludes,
|
|
300
|
+
startsWith,
|
|
301
|
+
endsWith,
|
|
302
|
+
caseSensitive = true,
|
|
303
|
+
transform,
|
|
304
|
+
defaultValue,
|
|
305
|
+
i18n,
|
|
306
|
+
} = options ?? {}
|
|
307
|
+
|
|
308
|
+
// Set appropriate default value based on required flag
|
|
309
|
+
const actualDefaultValue = defaultValue ?? (required ? "" : null)
|
|
310
|
+
|
|
311
|
+
// Helper function to get custom message or fallback to default i18n
|
|
312
|
+
const getMessage = (key: keyof IdMessages, params?: Record<string, any>) => {
|
|
313
|
+
if (i18n) {
|
|
314
|
+
const currentLocale = getLocale()
|
|
315
|
+
const customMessages = i18n[currentLocale]
|
|
316
|
+
if (customMessages && customMessages[key]) {
|
|
317
|
+
const template = customMessages[key]!
|
|
318
|
+
return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return t(`common.id.${key}`, params)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Preprocessing function
|
|
325
|
+
const preprocessFn = (val: unknown) => {
|
|
326
|
+
if (val === "" || val === null || val === undefined) {
|
|
327
|
+
return actualDefaultValue
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let processed = String(val)
|
|
331
|
+
|
|
332
|
+
if (transform) {
|
|
333
|
+
processed = transform(processed)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return processed
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
340
|
+
|
|
341
|
+
const schema = baseSchema
|
|
342
|
+
.refine((val) => {
|
|
343
|
+
if (val === null) return true
|
|
344
|
+
|
|
345
|
+
// Required check
|
|
346
|
+
if (required && (val === "" || val === "null" || val === "undefined")) {
|
|
347
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Create comparison value for case-insensitive checks
|
|
351
|
+
const comparisonVal = !caseSensitive ? val.toLowerCase() : val
|
|
352
|
+
|
|
353
|
+
// Length checks
|
|
354
|
+
if (val !== null && minLength !== undefined && val.length < minLength) {
|
|
355
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("minLength", { minLength }), path: [] }])
|
|
356
|
+
}
|
|
357
|
+
if (val !== null && maxLength !== undefined && val.length > maxLength) {
|
|
358
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("maxLength", { maxLength }), path: [] }])
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check if we have content-based validations that override format checking
|
|
362
|
+
const hasContentValidations = customRegex !== undefined || startsWith !== undefined || endsWith !== undefined || includes !== undefined || excludes !== undefined
|
|
363
|
+
|
|
364
|
+
// Custom regex validation (overrides ID format validation)
|
|
365
|
+
if (val !== null && customRegex !== undefined) {
|
|
366
|
+
if (!customRegex.test(val)) {
|
|
367
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("customFormat"), path: [] }])
|
|
368
|
+
}
|
|
369
|
+
} else if (val !== null && !hasContentValidations) {
|
|
370
|
+
// ID type validation (only if no custom regex or content validations)
|
|
371
|
+
let isValidId: boolean
|
|
372
|
+
|
|
373
|
+
if (allowedTypes && allowedTypes.length > 0) {
|
|
374
|
+
// Check if ID matches any of the allowed types
|
|
375
|
+
isValidId = allowedTypes.some((allowedType) => validateIdType(val, allowedType))
|
|
376
|
+
if (!isValidId) {
|
|
377
|
+
const typeNames = allowedTypes.join(", ")
|
|
378
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }])
|
|
379
|
+
}
|
|
380
|
+
} else if (type !== "auto") {
|
|
381
|
+
// Validate specific type
|
|
382
|
+
isValidId = validateIdType(val, type)
|
|
383
|
+
if (!isValidId) {
|
|
384
|
+
throw new z.ZodError([{ code: "custom", message: getMessage(type as keyof IdMessages) || getMessage("invalid"), path: [] }])
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// Auto-detection - must match at least one known pattern
|
|
388
|
+
isValidId = detectIdType(val) !== null
|
|
389
|
+
if (!isValidId) {
|
|
390
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} else if (val !== null && hasContentValidations && type !== "auto" && !customRegex) {
|
|
394
|
+
// Still validate specific types even with content validations (but not auto)
|
|
395
|
+
if (allowedTypes && allowedTypes.length > 0) {
|
|
396
|
+
const isValidType = allowedTypes.some((allowedType) => validateIdType(val, allowedType))
|
|
397
|
+
if (!isValidType) {
|
|
398
|
+
const typeNames = allowedTypes.join(", ")
|
|
399
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }])
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
if (!validateIdType(val, type)) {
|
|
403
|
+
throw new z.ZodError([{ code: "custom", message: getMessage(type as keyof IdMessages) || getMessage("invalid"), path: [] }])
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// String content checks (using comparisonVal for case sensitivity)
|
|
409
|
+
const searchStartsWith = !caseSensitive && startsWith ? startsWith.toLowerCase() : startsWith
|
|
410
|
+
const searchEndsWith = !caseSensitive && endsWith ? endsWith.toLowerCase() : endsWith
|
|
411
|
+
const searchIncludes = !caseSensitive && includes ? includes.toLowerCase() : includes
|
|
412
|
+
|
|
413
|
+
if (val !== null && startsWith !== undefined && !comparisonVal.startsWith(searchStartsWith!)) {
|
|
414
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("startsWith", { startsWith }), path: [] }])
|
|
415
|
+
}
|
|
416
|
+
if (val !== null && endsWith !== undefined && !comparisonVal.endsWith(searchEndsWith!)) {
|
|
417
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("endsWith", { endsWith }), path: [] }])
|
|
418
|
+
}
|
|
419
|
+
if (val !== null && includes !== undefined && !comparisonVal.includes(searchIncludes!)) {
|
|
420
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("includes", { includes }), path: [] }])
|
|
421
|
+
}
|
|
422
|
+
if (val !== null && excludes !== undefined) {
|
|
423
|
+
const excludeList = Array.isArray(excludes) ? excludes : [excludes]
|
|
424
|
+
for (const exclude of excludeList) {
|
|
425
|
+
const searchExclude = !caseSensitive ? exclude.toLowerCase() : exclude
|
|
426
|
+
if (comparisonVal.includes(searchExclude)) {
|
|
427
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("excludes", { excludes: exclude }), path: [] }])
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return true
|
|
433
|
+
})
|
|
434
|
+
.transform((val) => {
|
|
435
|
+
if (val === null) return val
|
|
436
|
+
|
|
437
|
+
// Handle case transformations
|
|
438
|
+
const shouldPreserveCase = type === "uuid" || type === "objectId"
|
|
439
|
+
|
|
440
|
+
if (!caseSensitive && !shouldPreserveCase) {
|
|
441
|
+
return val.toLowerCase()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return val // preserve the original case for UUID/ObjectId or when case-sensitive
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
return schema as unknown as IdSchema<IsRequired>
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Utility functions and constants exported for external use
|
|
452
|
+
*
|
|
453
|
+
* @description
|
|
454
|
+
* These utilities can be used independently for ID validation, type detection,
|
|
455
|
+
* and pattern matching without creating a full Zod schema.
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* ```typescript
|
|
459
|
+
* import { detectIdType, validateIdType, ID_PATTERNS } from './id'
|
|
460
|
+
*
|
|
461
|
+
* // Detect ID type
|
|
462
|
+
* const type = detectIdType("550e8400-e29b-41d4-a716-446655440000") // "uuid"
|
|
463
|
+
*
|
|
464
|
+
* // Validate specific type
|
|
465
|
+
* const isValid = validateIdType("123456", "numeric") // true
|
|
466
|
+
*
|
|
467
|
+
* // Access regex patterns
|
|
468
|
+
* const uuidPattern = ID_PATTERNS.uuid
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
export { detectIdType, validateIdType, ID_PATTERNS }
|