@hy_ong/zod-kit 0.0.5 → 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 +6 -1
- package/README.md +465 -97
- package/dist/index.cjs +1628 -121
- package/dist/index.d.cts +2699 -2
- package/dist/index.d.ts +2699 -2
- package/dist/index.js +1610 -120
- package/package.json +1 -1
- package/src/i18n/locales/en.json +62 -0
- package/src/i18n/locales/zh-TW.json +62 -0
- package/src/index.ts +4 -0
- package/src/validators/common/boolean.ts +94 -0
- package/src/validators/common/date.ts +128 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +113 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +224 -12
- package/src/validators/common/number.ts +125 -0
- package/src/validators/common/password.ts +174 -2
- package/src/validators/common/text.ts +120 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +140 -0
- package/src/validators/taiwan/business-id.ts +124 -2
- package/src/validators/taiwan/fax.ts +147 -2
- package/src/validators/taiwan/mobile.ts +134 -2
- package/src/validators/taiwan/national-id.ts +227 -10
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +150 -2
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/file.test.ts +479 -0
- package/tests/common/time.test.ts +528 -0
- package/tests/taiwan/postal-code.test.ts +705 -0
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Email validator for Zod Kit
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive email validation with domain filtering, business email
|
|
5
|
+
* validation, disposable email detection, and extensive customization options.
|
|
6
|
+
*
|
|
7
|
+
* @author Ong Hoe Yuan
|
|
8
|
+
* @version 0.0.5
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
import { z, ZodNullable, ZodString } from "zod"
|
|
2
12
|
import { t } from "../../i18n"
|
|
3
13
|
import { getLocale, type Locale } from "../../config"
|
|
4
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Type definition for email validation error messages
|
|
17
|
+
*
|
|
18
|
+
* @interface EmailMessages
|
|
19
|
+
* @property {string} [required] - Message when field is required but empty
|
|
20
|
+
* @property {string} [invalid] - Message when email format is invalid
|
|
21
|
+
* @property {string} [minLength] - Message when email is too short
|
|
22
|
+
* @property {string} [maxLength] - Message when email is too long
|
|
23
|
+
* @property {string} [includes] - Message when email doesn't contain required string
|
|
24
|
+
* @property {string} [domain] - Message when email domain is not allowed
|
|
25
|
+
* @property {string} [domainBlacklist] - Message when email domain is blacklisted
|
|
26
|
+
* @property {string} [businessOnly] - Message when free email providers are not allowed
|
|
27
|
+
* @property {string} [noDisposable] - Message when disposable email addresses are not allowed
|
|
28
|
+
*/
|
|
5
29
|
export type EmailMessages = {
|
|
6
30
|
required?: string
|
|
7
31
|
invalid?: string
|
|
@@ -14,6 +38,27 @@ export type EmailMessages = {
|
|
|
14
38
|
noDisposable?: string
|
|
15
39
|
}
|
|
16
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Configuration options for email validation
|
|
43
|
+
*
|
|
44
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
45
|
+
*
|
|
46
|
+
* @interface EmailOptions
|
|
47
|
+
* @property {IsRequired} [required=true] - Whether the field is required
|
|
48
|
+
* @property {string | string[]} [domain] - Allowed domain(s) for email addresses
|
|
49
|
+
* @property {string[]} [domainBlacklist] - Domains that are not allowed
|
|
50
|
+
* @property {number} [minLength] - Minimum length of email address
|
|
51
|
+
* @property {number} [maxLength] - Maximum length of email address
|
|
52
|
+
* @property {string} [includes] - String that must be included in the email
|
|
53
|
+
* @property {string | string[]} [excludes] - String(s) that must not be included
|
|
54
|
+
* @property {boolean} [allowSubdomains=true] - Whether to allow subdomains in domain validation
|
|
55
|
+
* @property {boolean} [businessOnly=false] - If true, reject common free email providers
|
|
56
|
+
* @property {boolean} [noDisposable=false] - If true, reject disposable email addresses
|
|
57
|
+
* @property {boolean} [lowercase=true] - Whether to convert email to lowercase
|
|
58
|
+
* @property {Function} [transform] - Custom transformation function for email strings
|
|
59
|
+
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
60
|
+
* @property {Record<Locale, EmailMessages>} [i18n] - Custom error messages for different locales
|
|
61
|
+
*/
|
|
17
62
|
export type EmailOptions<IsRequired extends boolean = true> = {
|
|
18
63
|
required?: IsRequired
|
|
19
64
|
domain?: string | string[]
|
|
@@ -31,8 +76,76 @@ export type EmailOptions<IsRequired extends boolean = true> = {
|
|
|
31
76
|
i18n?: Record<Locale, EmailMessages>
|
|
32
77
|
}
|
|
33
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Type alias for email validation schema based on required flag
|
|
81
|
+
*
|
|
82
|
+
* @template IsRequired - Whether the field is required
|
|
83
|
+
* @typedef EmailSchema
|
|
84
|
+
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
85
|
+
*/
|
|
34
86
|
export type EmailSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
35
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Creates a Zod schema for email validation with comprehensive filtering options
|
|
90
|
+
*
|
|
91
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
92
|
+
* @param {EmailOptions<IsRequired>} [options] - Configuration options for email validation
|
|
93
|
+
* @returns {EmailSchema<IsRequired>} Zod schema for email validation
|
|
94
|
+
*
|
|
95
|
+
* @description
|
|
96
|
+
* Creates a comprehensive email validator with domain filtering, business email
|
|
97
|
+
* validation, disposable email detection, and extensive customization options.
|
|
98
|
+
*
|
|
99
|
+
* Features:
|
|
100
|
+
* - RFC-compliant email format validation
|
|
101
|
+
* - Domain whitelist/blacklist support
|
|
102
|
+
* - Business email validation (excludes free providers)
|
|
103
|
+
* - Disposable email detection
|
|
104
|
+
* - Subdomain support configuration
|
|
105
|
+
* - Length validation
|
|
106
|
+
* - Content inclusion/exclusion
|
|
107
|
+
* - Automatic lowercase conversion
|
|
108
|
+
* - Custom transformation functions
|
|
109
|
+
* - Comprehensive internationalization
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* // Basic email validation
|
|
114
|
+
* const basicSchema = email()
|
|
115
|
+
* basicSchema.parse("user@example.com") // ✓ Valid
|
|
116
|
+
*
|
|
117
|
+
* // Domain restriction
|
|
118
|
+
* const domainSchema = email({
|
|
119
|
+
* domain: ["company.com", "organization.org"]
|
|
120
|
+
* })
|
|
121
|
+
* domainSchema.parse("user@company.com") // ✓ Valid
|
|
122
|
+
* domainSchema.parse("user@gmail.com") // ✗ Invalid
|
|
123
|
+
*
|
|
124
|
+
* // Business emails only (no free providers)
|
|
125
|
+
* const businessSchema = email({ businessOnly: true })
|
|
126
|
+
* businessSchema.parse("user@company.com") // ✓ Valid
|
|
127
|
+
* businessSchema.parse("user@gmail.com") // ✗ Invalid
|
|
128
|
+
*
|
|
129
|
+
* // No disposable emails
|
|
130
|
+
* const noDisposableSchema = email({ noDisposable: true })
|
|
131
|
+
* noDisposableSchema.parse("user@company.com") // ✓ Valid
|
|
132
|
+
* noDisposableSchema.parse("user@10minutemail.com") // ✗ Invalid
|
|
133
|
+
*
|
|
134
|
+
* // Domain blacklist
|
|
135
|
+
* const blacklistSchema = email({
|
|
136
|
+
* domainBlacklist: ["spam.com", "blocked.org"]
|
|
137
|
+
* })
|
|
138
|
+
*
|
|
139
|
+
* // Optional with default
|
|
140
|
+
* const optionalSchema = email({
|
|
141
|
+
* required: false,
|
|
142
|
+
* defaultValue: null
|
|
143
|
+
* })
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* @throws {z.ZodError} When validation fails with specific error messages
|
|
147
|
+
* @see {@link EmailOptions} for all available configuration options
|
|
148
|
+
*/
|
|
36
149
|
export function email<IsRequired extends boolean = true>(options?: EmailOptions<IsRequired>): EmailSchema<IsRequired> {
|
|
37
150
|
const {
|
|
38
151
|
required = true,
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview File validator for Zod Kit
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive file validation with MIME type filtering, size validation,
|
|
5
|
+
* extension validation, and extensive customization options.
|
|
6
|
+
*
|
|
7
|
+
* @author Ong Hoe Yuan
|
|
8
|
+
* @version 0.0.5
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z, ZodNullable, ZodType } from "zod"
|
|
12
|
+
import { t } from "../../i18n"
|
|
13
|
+
import { getLocale, type Locale } from "../../config"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type definition for file validation error messages
|
|
17
|
+
*
|
|
18
|
+
* @interface FileMessages
|
|
19
|
+
* @property {string} [required] - Message when field is required but empty
|
|
20
|
+
* @property {string} [invalid] - Message when file format is invalid
|
|
21
|
+
* @property {string} [size] - Message when file size exceeds limit
|
|
22
|
+
* @property {string} [minSize] - Message when file size is too small
|
|
23
|
+
* @property {string} [maxSize] - Message when file size exceeds maximum
|
|
24
|
+
* @property {string} [type] - Message when file type is not allowed
|
|
25
|
+
* @property {string} [extension] - Message when file extension is not allowed
|
|
26
|
+
* @property {string} [extensionBlacklist] - Message when file extension is blacklisted
|
|
27
|
+
* @property {string} [name] - Message when file name doesn't match pattern
|
|
28
|
+
* @property {string} [nameBlacklist] - Message when file name matches blacklisted pattern
|
|
29
|
+
* @property {string} [imageOnly] - Message when only image files are allowed
|
|
30
|
+
* @property {string} [documentOnly] - Message when only document files are allowed
|
|
31
|
+
* @property {string} [videoOnly] - Message when only video files are allowed
|
|
32
|
+
* @property {string} [audioOnly] - Message when only audio files are allowed
|
|
33
|
+
* @property {string} [archiveOnly] - Message when only archive files are allowed
|
|
34
|
+
*/
|
|
35
|
+
export type FileMessages = {
|
|
36
|
+
required?: string
|
|
37
|
+
invalid?: string
|
|
38
|
+
size?: string
|
|
39
|
+
minSize?: string
|
|
40
|
+
maxSize?: string
|
|
41
|
+
type?: string
|
|
42
|
+
extension?: string
|
|
43
|
+
extensionBlacklist?: string
|
|
44
|
+
name?: string
|
|
45
|
+
nameBlacklist?: string
|
|
46
|
+
imageOnly?: string
|
|
47
|
+
documentOnly?: string
|
|
48
|
+
videoOnly?: string
|
|
49
|
+
audioOnly?: string
|
|
50
|
+
archiveOnly?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Configuration options for file validation
|
|
55
|
+
*
|
|
56
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
57
|
+
*
|
|
58
|
+
* @interface FileOptions
|
|
59
|
+
* @property {IsRequired} [required=true] - Whether the field is required
|
|
60
|
+
* @property {number} [maxSize] - Maximum file size in bytes
|
|
61
|
+
* @property {number} [minSize] - Minimum file size in bytes
|
|
62
|
+
* @property {string | string[]} [type] - Allowed MIME type(s)
|
|
63
|
+
* @property {string[]} [typeBlacklist] - MIME types that are not allowed
|
|
64
|
+
* @property {string | string[]} [extension] - Allowed file extension(s)
|
|
65
|
+
* @property {string[]} [extensionBlacklist] - File extensions that are not allowed
|
|
66
|
+
* @property {RegExp | string} [namePattern] - Pattern that file name must match
|
|
67
|
+
* @property {RegExp | string | Array<RegExp | string>} [nameBlacklist] - Pattern(s) that file name must not match
|
|
68
|
+
* @property {boolean} [imageOnly=false] - If true, only allow image files
|
|
69
|
+
* @property {boolean} [documentOnly=false] - If true, only allow document files
|
|
70
|
+
* @property {boolean} [videoOnly=false] - If true, only allow video files
|
|
71
|
+
* @property {boolean} [audioOnly=false] - If true, only allow audio files
|
|
72
|
+
* @property {boolean} [archiveOnly=false] - If true, only allow archive files
|
|
73
|
+
* @property {boolean} [caseSensitive=false] - Whether extension matching is case-sensitive
|
|
74
|
+
* @property {Function} [transform] - Custom transformation function for File objects
|
|
75
|
+
* @property {File | null} [defaultValue] - Default value when input is empty
|
|
76
|
+
* @property {Record<Locale, FileMessages>} [i18n] - Custom error messages for different locales
|
|
77
|
+
*/
|
|
78
|
+
export type FileOptions<IsRequired extends boolean = true> = {
|
|
79
|
+
required?: IsRequired
|
|
80
|
+
maxSize?: number
|
|
81
|
+
minSize?: number
|
|
82
|
+
type?: string | string[]
|
|
83
|
+
typeBlacklist?: string[]
|
|
84
|
+
extension?: string | string[]
|
|
85
|
+
extensionBlacklist?: string[]
|
|
86
|
+
namePattern?: RegExp | string
|
|
87
|
+
nameBlacklist?: RegExp | string | Array<RegExp | string>
|
|
88
|
+
imageOnly?: boolean
|
|
89
|
+
documentOnly?: boolean
|
|
90
|
+
videoOnly?: boolean
|
|
91
|
+
audioOnly?: boolean
|
|
92
|
+
archiveOnly?: boolean
|
|
93
|
+
caseSensitive?: boolean
|
|
94
|
+
transform?: (value: File) => File
|
|
95
|
+
defaultValue?: IsRequired extends true ? File : File | null
|
|
96
|
+
i18n?: Record<Locale, FileMessages>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Type alias for file validation schema based on required flag
|
|
101
|
+
*
|
|
102
|
+
* @template IsRequired - Whether the field is required
|
|
103
|
+
* @typedef FileSchema
|
|
104
|
+
* @description Returns ZodType<File> if required, ZodNullable<ZodType<File>> if optional
|
|
105
|
+
*/
|
|
106
|
+
export type FileSchema<IsRequired extends boolean> = IsRequired extends true ? ZodType<File> : ZodNullable<ZodType<File>>
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a Zod schema for file validation with comprehensive filtering options
|
|
110
|
+
*
|
|
111
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
112
|
+
* @param {FileOptions<IsRequired>} [options] - Configuration options for file validation
|
|
113
|
+
* @returns {FileSchema<IsRequired>} Zod schema for file validation
|
|
114
|
+
*
|
|
115
|
+
* @description
|
|
116
|
+
* Creates a comprehensive file validator with MIME type filtering, size validation,
|
|
117
|
+
* extension validation, and extensive customization options.
|
|
118
|
+
*
|
|
119
|
+
* Features:
|
|
120
|
+
* - File size validation (min/max)
|
|
121
|
+
* - MIME type whitelist/blacklist support
|
|
122
|
+
* - File extension whitelist/blacklist support
|
|
123
|
+
* - File name pattern validation
|
|
124
|
+
* - Predefined file category filters (image, document, video, audio, archive)
|
|
125
|
+
* - Case-sensitive/insensitive extension matching
|
|
126
|
+
* - Custom transformation functions
|
|
127
|
+
* - Comprehensive internationalization
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* // Basic file validation
|
|
132
|
+
* const basicSchema = file()
|
|
133
|
+
* basicSchema.parse(new File(["content"], "test.txt"))
|
|
134
|
+
*
|
|
135
|
+
* // Size restrictions
|
|
136
|
+
* const sizeSchema = file({
|
|
137
|
+
* maxSize: 1024 * 1024, // 1MB
|
|
138
|
+
* minSize: 1024 // 1KB
|
|
139
|
+
* })
|
|
140
|
+
*
|
|
141
|
+
* // Extension restrictions
|
|
142
|
+
* const imageSchema = file({
|
|
143
|
+
* extension: [".jpg", ".png", ".gif"],
|
|
144
|
+
* maxSize: 5 * 1024 * 1024 // 5MB
|
|
145
|
+
* })
|
|
146
|
+
*
|
|
147
|
+
* // MIME type restrictions
|
|
148
|
+
* const documentSchema = file({
|
|
149
|
+
* type: ["application/pdf", "application/msword"],
|
|
150
|
+
* maxSize: 10 * 1024 * 1024 // 10MB
|
|
151
|
+
* })
|
|
152
|
+
*
|
|
153
|
+
* // Image files only
|
|
154
|
+
* const imageOnlySchema = file({ imageOnly: true })
|
|
155
|
+
*
|
|
156
|
+
* // Document files only
|
|
157
|
+
* const docOnlySchema = file({ documentOnly: true })
|
|
158
|
+
*
|
|
159
|
+
* // Name pattern validation
|
|
160
|
+
* const patternSchema = file({
|
|
161
|
+
* namePattern: /^[a-zA-Z0-9_-]+\.(pdf|doc|docx)$/,
|
|
162
|
+
* maxSize: 5 * 1024 * 1024
|
|
163
|
+
* })
|
|
164
|
+
*
|
|
165
|
+
* // Optional with default
|
|
166
|
+
* const optionalSchema = file({
|
|
167
|
+
* required: false,
|
|
168
|
+
* defaultValue: null
|
|
169
|
+
* })
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @throws {z.ZodError} When validation fails with specific error messages
|
|
173
|
+
* @see {@link FileOptions} for all available configuration options
|
|
174
|
+
*/
|
|
175
|
+
export function file<IsRequired extends boolean = true>(options?: FileOptions<IsRequired>): FileSchema<IsRequired> {
|
|
176
|
+
const {
|
|
177
|
+
required = true,
|
|
178
|
+
maxSize,
|
|
179
|
+
minSize,
|
|
180
|
+
type,
|
|
181
|
+
typeBlacklist,
|
|
182
|
+
extension,
|
|
183
|
+
extensionBlacklist,
|
|
184
|
+
namePattern,
|
|
185
|
+
nameBlacklist,
|
|
186
|
+
imageOnly = false,
|
|
187
|
+
documentOnly = false,
|
|
188
|
+
videoOnly = false,
|
|
189
|
+
audioOnly = false,
|
|
190
|
+
archiveOnly = false,
|
|
191
|
+
caseSensitive = false,
|
|
192
|
+
transform,
|
|
193
|
+
defaultValue,
|
|
194
|
+
i18n,
|
|
195
|
+
} = options ?? {}
|
|
196
|
+
|
|
197
|
+
// Helper function to get custom message or fallback to default i18n
|
|
198
|
+
const getMessage = (key: keyof FileMessages, params?: Record<string, any>) => {
|
|
199
|
+
if (i18n) {
|
|
200
|
+
const currentLocale = getLocale()
|
|
201
|
+
const customMessages = i18n[currentLocale]
|
|
202
|
+
if (customMessages && customMessages[key]) {
|
|
203
|
+
const template = customMessages[key]!
|
|
204
|
+
return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return t(`common.file.${key}`, params)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Predefined file type categories
|
|
211
|
+
const imageTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/svg+xml", "image/bmp", "image/tiff"]
|
|
212
|
+
const documentTypes = [
|
|
213
|
+
"application/pdf",
|
|
214
|
+
"application/msword",
|
|
215
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
216
|
+
"application/vnd.ms-excel",
|
|
217
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
218
|
+
"application/vnd.ms-powerpoint",
|
|
219
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
220
|
+
"text/plain",
|
|
221
|
+
"text/csv",
|
|
222
|
+
]
|
|
223
|
+
const videoTypes = ["video/mp4", "video/mpeg", "video/quicktime", "video/x-msvideo", "video/x-ms-wmv", "video/webm", "video/ogg"]
|
|
224
|
+
const audioTypes = ["audio/mpeg", "audio/wav", "audio/ogg", "audio/aac", "audio/webm", "audio/mp3", "audio/x-wav"]
|
|
225
|
+
const archiveTypes = ["application/zip", "application/x-rar-compressed", "application/x-7z-compressed", "application/x-tar", "application/gzip"]
|
|
226
|
+
|
|
227
|
+
const actualDefaultValue = defaultValue ?? null
|
|
228
|
+
|
|
229
|
+
const baseSchema = z.preprocess(
|
|
230
|
+
(val) => {
|
|
231
|
+
if (val === "" || val === null || val === undefined) {
|
|
232
|
+
return actualDefaultValue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!(val instanceof File)) {
|
|
236
|
+
return val
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let processed = val
|
|
240
|
+
|
|
241
|
+
if (transform) {
|
|
242
|
+
processed = transform(processed)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return processed
|
|
246
|
+
},
|
|
247
|
+
z.union([z.instanceof(File).refine(() => true, { message: getMessage("invalid") }), z.null()])
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const schema = baseSchema
|
|
251
|
+
.refine((val) => required === false || val !== null, {
|
|
252
|
+
message: getMessage("required"),
|
|
253
|
+
})
|
|
254
|
+
.refine((val) => val === null || val instanceof File, {
|
|
255
|
+
message: getMessage("invalid"),
|
|
256
|
+
})
|
|
257
|
+
.refine((val) => val === null || minSize === undefined || val.size >= minSize, {
|
|
258
|
+
message: getMessage("minSize", { minSize: formatFileSize(minSize || 0) }),
|
|
259
|
+
})
|
|
260
|
+
.refine((val) => val === null || maxSize === undefined || val.size <= maxSize, {
|
|
261
|
+
message: getMessage("maxSize", { maxSize: formatFileSize(maxSize || 0) }),
|
|
262
|
+
})
|
|
263
|
+
.refine((val) => val === null || !imageOnly || imageTypes.includes(val.type), {
|
|
264
|
+
message: getMessage("imageOnly"),
|
|
265
|
+
})
|
|
266
|
+
.refine((val) => val === null || !documentOnly || documentTypes.includes(val.type), {
|
|
267
|
+
message: getMessage("documentOnly"),
|
|
268
|
+
})
|
|
269
|
+
.refine((val) => val === null || !videoOnly || videoTypes.includes(val.type), {
|
|
270
|
+
message: getMessage("videoOnly"),
|
|
271
|
+
})
|
|
272
|
+
.refine((val) => val === null || !audioOnly || audioTypes.includes(val.type), {
|
|
273
|
+
message: getMessage("audioOnly"),
|
|
274
|
+
})
|
|
275
|
+
.refine((val) => val === null || !archiveOnly || archiveTypes.includes(val.type), {
|
|
276
|
+
message: getMessage("archiveOnly"),
|
|
277
|
+
})
|
|
278
|
+
.refine(
|
|
279
|
+
(val) => {
|
|
280
|
+
if (val === null || !typeBlacklist || typeBlacklist.length === 0) return true
|
|
281
|
+
return !typeBlacklist.includes(val.type)
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
message: getMessage("type", { type: typeBlacklist?.join(", ") || "" }),
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
.refine(
|
|
288
|
+
(val) => {
|
|
289
|
+
if (val === null || type === undefined) return true
|
|
290
|
+
const allowedTypes = Array.isArray(type) ? type : [type]
|
|
291
|
+
return allowedTypes.includes(val.type)
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
message: getMessage("type", { type: Array.isArray(type) ? type.join(", ") : type || "" }),
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
.refine(
|
|
298
|
+
(val) => {
|
|
299
|
+
if (val === null || extensionBlacklist === undefined || extensionBlacklist.length === 0) return true
|
|
300
|
+
const fileExtension = getFileExtension(val.name, caseSensitive)
|
|
301
|
+
const normalizedBlacklist = extensionBlacklist.map((ext) => normalizeExtension(ext, caseSensitive))
|
|
302
|
+
return !normalizedBlacklist.includes(fileExtension)
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
message: getMessage("extensionBlacklist", { extension: extensionBlacklist?.join(", ") || "" }),
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
.refine(
|
|
309
|
+
(val) => {
|
|
310
|
+
if (val === null || extension === undefined) return true
|
|
311
|
+
const fileName = val.name
|
|
312
|
+
const fileExtension = getFileExtension(fileName, caseSensitive)
|
|
313
|
+
const allowedExtensions = Array.isArray(extension) ? extension : [extension]
|
|
314
|
+
const normalizedExtensions = allowedExtensions.map((ext) => normalizeExtension(ext, caseSensitive))
|
|
315
|
+
return normalizedExtensions.includes(fileExtension)
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
message: getMessage("extension", { extension: Array.isArray(extension) ? extension.join(", ") : extension || "" }),
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
.refine(
|
|
322
|
+
(val) => {
|
|
323
|
+
if (val === null || namePattern === undefined) return true
|
|
324
|
+
const pattern = typeof namePattern === "string" ? new RegExp(namePattern) : namePattern
|
|
325
|
+
return pattern.test(val.name)
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
message: getMessage("name", { pattern: namePattern?.toString() || "" }),
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
.refine(
|
|
332
|
+
(val) => {
|
|
333
|
+
if (val === null || nameBlacklist === undefined) return true
|
|
334
|
+
const blacklistPatterns = Array.isArray(nameBlacklist) ? nameBlacklist : [nameBlacklist]
|
|
335
|
+
for (const blacklistPattern of blacklistPatterns) {
|
|
336
|
+
const pattern = typeof blacklistPattern === "string" ? new RegExp(blacklistPattern) : blacklistPattern
|
|
337
|
+
if (pattern.test(val.name)) {
|
|
338
|
+
return false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return true
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
message: getMessage("nameBlacklist", { pattern: "" }),
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return schema as unknown as FileSchema<IsRequired>
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Helper function to get file extension
|
|
353
|
+
*/
|
|
354
|
+
function getFileExtension(fileName: string, caseSensitive: boolean): string {
|
|
355
|
+
const lastDotIndex = fileName.lastIndexOf(".")
|
|
356
|
+
if (lastDotIndex === -1) return ""
|
|
357
|
+
|
|
358
|
+
const extension = fileName.substring(lastDotIndex)
|
|
359
|
+
return caseSensitive ? extension : extension.toLowerCase()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Helper function to normalize extension for comparison
|
|
364
|
+
*/
|
|
365
|
+
function normalizeExtension(extension: string, caseSensitive: boolean): string {
|
|
366
|
+
const normalized = extension.startsWith(".") ? extension : `.${extension}`
|
|
367
|
+
return caseSensitive ? normalized : normalized.toLowerCase()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Helper function to format file size in human-readable format
|
|
372
|
+
*/
|
|
373
|
+
function formatFileSize(bytes: number): string {
|
|
374
|
+
const units = ["B", "KB", "MB", "GB", "TB"]
|
|
375
|
+
let size = bytes
|
|
376
|
+
let unitIndex = 0
|
|
377
|
+
|
|
378
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
379
|
+
size /= 1024
|
|
380
|
+
unitIndex++
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return `${Math.round(size * 100) / 100} ${units[unitIndex]}`
|
|
384
|
+
}
|