@hy_ong/zod-kit 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +40 -21
- package/dist/index.d.cts +24 -11
- package/dist/index.d.ts +24 -11
- package/dist/index.js +40 -21
- package/package.json +1 -1
- package/src/validators/common/date.ts +26 -16
- package/src/validators/common/datetime.ts +46 -28
- package/src/validators/common/email.ts +57 -15
- package/src/validators/common/id.ts +139 -58
- package/src/validators/common/number.ts +80 -43
- package/src/validators/common/password.ts +30 -18
- package/src/validators/common/text.ts +48 -14
- package/src/validators/common/time.ts +34 -22
- package/src/validators/common/url.ts +40 -23
- package/src/validators/taiwan/business-id.ts +19 -19
- package/src/validators/taiwan/fax.ts +29 -28
- package/src/validators/taiwan/mobile.ts +29 -28
- package/src/validators/taiwan/national-id.ts +27 -27
- package/src/validators/taiwan/postal-code.ts +51 -45
- package/src/validators/taiwan/tel.ts +29 -28
- package/tests/common/id.test.ts +62 -4
- package/tests/taiwan/business-id.test.ts +23 -23
- package/tests/taiwan/fax.test.ts +34 -34
- package/tests/taiwan/mobile.test.ts +33 -33
- package/tests/taiwan/national-id.test.ts +32 -32
- package/tests/taiwan/postal-code.test.ts +62 -62
- package/tests/taiwan/tel.test.ts +34 -34
|
@@ -16,7 +16,7 @@ import { getLocale, type Locale } from "../../config"
|
|
|
16
16
|
/**
|
|
17
17
|
* Type definition for postal code validation error messages
|
|
18
18
|
*
|
|
19
|
-
* @interface
|
|
19
|
+
* @interface TwPostalCodeMessages
|
|
20
20
|
* @property {string} [required] - Message when field is required but empty
|
|
21
21
|
* @property {string} [invalid] - Message when postal code format is invalid
|
|
22
22
|
* @property {string} [invalidFormat] - Message when format doesn't match expected pattern
|
|
@@ -26,7 +26,7 @@ import { getLocale, type Locale } from "../../config"
|
|
|
26
26
|
* @property {string} [format5Only] - Message when only 5-digit format is allowed
|
|
27
27
|
* @property {string} [format6Only] - Message when only 6-digit format is allowed
|
|
28
28
|
*/
|
|
29
|
-
export type
|
|
29
|
+
export type TwPostalCodeMessages = {
|
|
30
30
|
required?: string
|
|
31
31
|
invalid?: string
|
|
32
32
|
invalidFormat?: string
|
|
@@ -42,7 +42,7 @@ export type PostalCodeMessages = {
|
|
|
42
42
|
/**
|
|
43
43
|
* Postal code format types supported in Taiwan
|
|
44
44
|
*
|
|
45
|
-
* @typedef {"3" | "5" | "6" | "3+5" | "3+6" | "5+6" | "all"}
|
|
45
|
+
* @typedef {"3" | "5" | "6" | "3+5" | "3+6" | "5+6" | "all"} TwPostalCodeFormatType
|
|
46
46
|
*
|
|
47
47
|
* Available formats:
|
|
48
48
|
* - "3": 3-digit basic postal codes (100-982)
|
|
@@ -53,7 +53,7 @@ export type PostalCodeMessages = {
|
|
|
53
53
|
* - "5+6": Accept both 5-digit and 6-digit formats
|
|
54
54
|
* - "all": Accept all formats (3, 5, and 6 digits)
|
|
55
55
|
*/
|
|
56
|
-
export type
|
|
56
|
+
export type TwPostalCodeFormatType =
|
|
57
57
|
| "3" // 3-digit only
|
|
58
58
|
| "5" // 5-digit only (legacy)
|
|
59
59
|
| "6" // 6-digit only (current)
|
|
@@ -67,9 +67,9 @@ export type PostalCodeFormat =
|
|
|
67
67
|
*
|
|
68
68
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
69
69
|
*
|
|
70
|
-
* @interface
|
|
70
|
+
* @interface TwPostalCodeOptions
|
|
71
71
|
* @property {IsRequired} [required=true] - Whether the field is required
|
|
72
|
-
* @property {
|
|
72
|
+
* @property {TwPostalCodeFormatType} [format="3+6"] - Which postal code formats to accept
|
|
73
73
|
* @property {boolean} [strictValidation=true] - Enable strict validation against known postal code ranges
|
|
74
74
|
* @property {boolean} [allowDashes=true] - Whether to allow dashes in postal codes (e.g., "100-01" or "100-001")
|
|
75
75
|
* @property {boolean} [warn5Digit=true] - Whether to show warning for 5-digit legacy format
|
|
@@ -77,10 +77,10 @@ export type PostalCodeFormat =
|
|
|
77
77
|
* @property {string[]} [blockedPrefixes] - Specific 3-digit prefixes to block
|
|
78
78
|
* @property {Function} [transform] - Custom transformation function for postal codes
|
|
79
79
|
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
80
|
-
* @property {Record<Locale,
|
|
80
|
+
* @property {Record<Locale, TwPostalCodeMessages>} [i18n] - Custom error messages for different locales
|
|
81
81
|
*/
|
|
82
|
-
export type
|
|
83
|
-
format?:
|
|
82
|
+
export type TwPostalCodeOptions<IsRequired extends boolean = true> = {
|
|
83
|
+
format?: TwPostalCodeFormatType
|
|
84
84
|
strictValidation?: boolean
|
|
85
85
|
allowDashes?: boolean
|
|
86
86
|
warn5Digit?: boolean
|
|
@@ -88,7 +88,7 @@ export type PostalCodeOptions<IsRequired extends boolean = true> = {
|
|
|
88
88
|
blockedPrefixes?: string[]
|
|
89
89
|
transform?: (value: string) => string
|
|
90
90
|
defaultValue?: IsRequired extends true ? string : string | null
|
|
91
|
-
i18n?: Record<Locale,
|
|
91
|
+
i18n?: Record<Locale, TwPostalCodeMessages>
|
|
92
92
|
strictSuffixValidation?: boolean
|
|
93
93
|
deprecate5Digit?: boolean
|
|
94
94
|
}
|
|
@@ -97,10 +97,10 @@ export type PostalCodeOptions<IsRequired extends boolean = true> = {
|
|
|
97
97
|
* Type alias for postal code validation schema based on required flag
|
|
98
98
|
*
|
|
99
99
|
* @template IsRequired - Whether the field is required
|
|
100
|
-
* @typedef
|
|
100
|
+
* @typedef TwPostalCodeSchema
|
|
101
101
|
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
102
102
|
*/
|
|
103
|
-
export type
|
|
103
|
+
export type TwPostalCodeSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Valid 3-digit postal code prefixes for Taiwan
|
|
@@ -728,7 +728,7 @@ const validate6DigitPostalCode = (value: string, strictValidation: boolean = tru
|
|
|
728
728
|
* Main validation function for Taiwan postal codes
|
|
729
729
|
*
|
|
730
730
|
* @param {string} value - The postal code to validate
|
|
731
|
-
* @param {
|
|
731
|
+
* @param {TwPostalCodeFormatType} format - Which formats to accept
|
|
732
732
|
* @param {boolean} strictValidation - Whether to validate against known postal codes
|
|
733
733
|
* @param {boolean} strictSuffixValidation - Whether to validate suffix ranges
|
|
734
734
|
* @param {boolean} allowDashes - Whether dashes/spaces are allowed and should be removed
|
|
@@ -738,7 +738,7 @@ const validate6DigitPostalCode = (value: string, strictValidation: boolean = tru
|
|
|
738
738
|
*/
|
|
739
739
|
const validateTaiwanPostalCode = (
|
|
740
740
|
value: string,
|
|
741
|
-
format:
|
|
741
|
+
format: TwPostalCodeFormatType = "3+6",
|
|
742
742
|
strictValidation: boolean = true,
|
|
743
743
|
strictSuffixValidation: boolean = false,
|
|
744
744
|
allowDashes: boolean = true,
|
|
@@ -801,8 +801,8 @@ const validateTaiwanPostalCode = (
|
|
|
801
801
|
* Creates a Zod schema for Taiwan postal code validation
|
|
802
802
|
*
|
|
803
803
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
804
|
-
* @param {
|
|
805
|
-
* @returns {
|
|
804
|
+
* @param {TwPostalCodeOptions<IsRequired>} [options] - Configuration options for postal code validation
|
|
805
|
+
* @returns {TwPostalCodeSchema<IsRequired>} Zod schema for postal code validation
|
|
806
806
|
*
|
|
807
807
|
* @description
|
|
808
808
|
* Creates a comprehensive Taiwan postal code validator that supports multiple formats
|
|
@@ -822,53 +822,53 @@ const validateTaiwanPostalCode = (
|
|
|
822
822
|
* @example
|
|
823
823
|
* ```typescript
|
|
824
824
|
* // Accept 3-digit or 6-digit formats (recommended)
|
|
825
|
-
* const modernSchema =
|
|
825
|
+
* const modernSchema = twPostalCode()
|
|
826
826
|
* modernSchema.parse("100") // ✓ Valid 3-digit
|
|
827
827
|
* modernSchema.parse("100001") // ✓ Valid 6-digit
|
|
828
828
|
* modernSchema.parse("10001") // ✗ Invalid (5-digit not allowed)
|
|
829
829
|
*
|
|
830
830
|
* // Accept all formats
|
|
831
|
-
* const flexibleSchema =
|
|
831
|
+
* const flexibleSchema = twPostalCode(false, { format: "all" })
|
|
832
832
|
* flexibleSchema.parse("100") // ✓ Valid
|
|
833
833
|
* flexibleSchema.parse("10001") // ✓ Valid
|
|
834
834
|
* flexibleSchema.parse("100001") // ✓ Valid
|
|
835
835
|
*
|
|
836
836
|
* // Only 6-digit format (current standard)
|
|
837
|
-
* const modernOnlySchema =
|
|
837
|
+
* const modernOnlySchema = twPostalCode(false, { format: "6" })
|
|
838
838
|
* modernOnlySchema.parse("100001") // ✓ Valid
|
|
839
839
|
* modernOnlySchema.parse("100") // ✗ Invalid
|
|
840
840
|
*
|
|
841
841
|
* // With dashes allowed
|
|
842
|
-
* const dashSchema =
|
|
842
|
+
* const dashSchema = twPostalCode(false, { allowDashes: true })
|
|
843
843
|
* dashSchema.parse("100-001") // ✓ Valid (normalized to "100001")
|
|
844
844
|
* dashSchema.parse("100-01") // ✓ Valid if 5-digit format allowed
|
|
845
845
|
*
|
|
846
846
|
* // Specific areas only
|
|
847
|
-
* const taipeiSchema =
|
|
847
|
+
* const taipeiSchema = twPostalCode(false, {
|
|
848
848
|
* allowedPrefixes: ["100", "103", "104", "105", "106"]
|
|
849
849
|
* })
|
|
850
850
|
* taipeiSchema.parse("100001") // ✓ Valid (Taipei area)
|
|
851
851
|
* taipeiSchema.parse("200001") // ✗ Invalid (not in allowlist)
|
|
852
852
|
*
|
|
853
853
|
* // Block specific areas
|
|
854
|
-
* const blockedSchema =
|
|
854
|
+
* const blockedSchema = twPostalCode(false, {
|
|
855
855
|
* blockedPrefixes: ["999"] // Block test codes
|
|
856
856
|
* })
|
|
857
857
|
*
|
|
858
858
|
* // With warning for legacy format
|
|
859
|
-
* const warnSchema =
|
|
859
|
+
* const warnSchema = twPostalCode(false, {
|
|
860
860
|
* format: "all",
|
|
861
861
|
* warn5Digit: true
|
|
862
862
|
* })
|
|
863
863
|
* // Will validate but may show warning for 5-digit codes
|
|
864
864
|
*
|
|
865
865
|
* // Optional with custom transformation
|
|
866
|
-
* const optionalSchema =
|
|
866
|
+
* const optionalSchema = twPostalCode(false, {
|
|
867
867
|
* transform: (value) => value.replace(/\D/g, '') // Remove non-digits
|
|
868
868
|
* })
|
|
869
869
|
*
|
|
870
870
|
* // Strict suffix validation for real postal codes
|
|
871
|
-
* const strictSchema =
|
|
871
|
+
* const strictSchema = twPostalCode(false, {
|
|
872
872
|
* format: "6",
|
|
873
873
|
* strictSuffixValidation: true // Validates suffix range 001-999
|
|
874
874
|
* })
|
|
@@ -876,7 +876,7 @@ const validateTaiwanPostalCode = (
|
|
|
876
876
|
* strictSchema.parse("100000") // ✗ Invalid (suffix 000 not allowed)
|
|
877
877
|
*
|
|
878
878
|
* // Deprecate 5-digit codes entirely
|
|
879
|
-
* const modern2024Schema =
|
|
879
|
+
* const modern2024Schema = twPostalCode(false, {
|
|
880
880
|
* format: "all",
|
|
881
881
|
* deprecate5Digit: true // Throws error for any 5-digit code
|
|
882
882
|
* })
|
|
@@ -885,11 +885,11 @@ const validateTaiwanPostalCode = (
|
|
|
885
885
|
* ```
|
|
886
886
|
*
|
|
887
887
|
* @throws {z.ZodError} When validation fails with specific error messages
|
|
888
|
-
* @see {@link
|
|
889
|
-
* @see {@link
|
|
888
|
+
* @see {@link TwPostalCodeOptions} for all available configuration options
|
|
889
|
+
* @see {@link TwPostalCodeFormatType} for supported formats
|
|
890
890
|
* @see {@link validateTaiwanPostalCode} for validation logic details
|
|
891
891
|
*/
|
|
892
|
-
export function
|
|
892
|
+
export function twPostalCode<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TwPostalCodeOptions<IsRequired>, 'required'>): TwPostalCodeSchema<IsRequired> {
|
|
893
893
|
const {
|
|
894
894
|
format = "3+6",
|
|
895
895
|
strictValidation = true,
|
|
@@ -910,7 +910,7 @@ export function postalCode<IsRequired extends boolean = false>(required?: IsRequ
|
|
|
910
910
|
const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
|
|
911
911
|
|
|
912
912
|
// Helper function to get custom message or fallback to default i18n
|
|
913
|
-
const getMessage = (key: keyof
|
|
913
|
+
const getMessage = (key: keyof TwPostalCodeMessages, params?: Record<string, any>) => {
|
|
914
914
|
if (i18n) {
|
|
915
915
|
const currentLocale = getLocale()
|
|
916
916
|
const customMessages = i18n[currentLocale]
|
|
@@ -949,34 +949,39 @@ export function postalCode<IsRequired extends boolean = false>(required?: IsRequ
|
|
|
949
949
|
|
|
950
950
|
const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
951
951
|
|
|
952
|
-
const schema = baseSchema.
|
|
953
|
-
if (val === null) return
|
|
952
|
+
const schema = baseSchema.superRefine((val, ctx) => {
|
|
953
|
+
if (val === null) return
|
|
954
954
|
|
|
955
955
|
// Required check
|
|
956
956
|
if (isRequired && (val === "" || val === "null" || val === "undefined")) {
|
|
957
|
-
|
|
957
|
+
ctx.addIssue({ code: "custom", message: getMessage("required") })
|
|
958
|
+
return
|
|
958
959
|
}
|
|
959
960
|
|
|
960
|
-
if (val === null) return
|
|
961
|
-
if (!isRequired && val === "") return
|
|
961
|
+
if (val === null) return
|
|
962
|
+
if (!isRequired && val === "") return
|
|
962
963
|
|
|
963
964
|
// Format-specific validation
|
|
964
965
|
const cleanValue = val.replace(/[-\s]/g, "")
|
|
965
966
|
|
|
966
967
|
// Check if format matches expected pattern
|
|
967
968
|
if (format === "3" && cleanValue.length !== 3) {
|
|
968
|
-
|
|
969
|
+
ctx.addIssue({ code: "custom", message: getMessage("format3Only") })
|
|
970
|
+
return
|
|
969
971
|
}
|
|
970
972
|
if (format === "5" && cleanValue.length !== 5) {
|
|
971
|
-
|
|
973
|
+
ctx.addIssue({ code: "custom", message: getMessage("format5Only") })
|
|
974
|
+
return
|
|
972
975
|
}
|
|
973
976
|
if (format === "6" && cleanValue.length !== 6) {
|
|
974
|
-
|
|
977
|
+
ctx.addIssue({ code: "custom", message: getMessage("format6Only") })
|
|
978
|
+
return
|
|
975
979
|
}
|
|
976
980
|
|
|
977
981
|
// Check for deprecated 5-digit format
|
|
978
982
|
if (deprecate5Digit && cleanValue.length === 5) {
|
|
979
|
-
|
|
983
|
+
ctx.addIssue({ code: "custom", message: getMessage("deprecated5Digit") })
|
|
984
|
+
return
|
|
980
985
|
}
|
|
981
986
|
|
|
982
987
|
// Pre-validate suffix for better error messages before main validation
|
|
@@ -987,7 +992,8 @@ export function postalCode<IsRequired extends boolean = false>(required?: IsRequ
|
|
|
987
992
|
const suffixNum = parseInt(suffix, 10)
|
|
988
993
|
const ranges = getPostalCodeRanges(prefix)
|
|
989
994
|
if (suffixNum < ranges.range5[0] || suffixNum > ranges.range5[1]) {
|
|
990
|
-
|
|
995
|
+
ctx.addIssue({ code: "custom", message: getMessage("invalidSuffix") })
|
|
996
|
+
return
|
|
991
997
|
}
|
|
992
998
|
} else if (cleanValue.length === 6) {
|
|
993
999
|
const prefix = cleanValue.substring(0, 3)
|
|
@@ -995,25 +1001,25 @@ export function postalCode<IsRequired extends boolean = false>(required?: IsRequ
|
|
|
995
1001
|
const suffixNum = parseInt(suffix, 10)
|
|
996
1002
|
const ranges = getPostalCodeRanges(prefix)
|
|
997
1003
|
if (suffixNum < ranges.range6[0] || suffixNum > ranges.range6[1]) {
|
|
998
|
-
|
|
1004
|
+
ctx.addIssue({ code: "custom", message: getMessage("invalidSuffix") })
|
|
1005
|
+
return
|
|
999
1006
|
}
|
|
1000
1007
|
}
|
|
1001
1008
|
}
|
|
1002
1009
|
|
|
1003
1010
|
// Main postal code validation (only validates prefix if strictSuffixValidation already passed)
|
|
1004
1011
|
if (!validateTaiwanPostalCode(val, format, strictValidation, strictSuffixValidation, allowDashes, allowedPrefixes, blockedPrefixes)) {
|
|
1005
|
-
|
|
1012
|
+
ctx.addIssue({ code: "custom", message: getMessage("invalid") })
|
|
1013
|
+
return
|
|
1006
1014
|
}
|
|
1007
1015
|
|
|
1008
1016
|
// Warning for 5-digit legacy format (doesn't fail validation)
|
|
1009
1017
|
if (warn5Digit && cleanValue.length === 5 && format !== "5" && !deprecate5Digit) {
|
|
1010
1018
|
console.warn(getMessage("legacy5DigitWarning"))
|
|
1011
1019
|
}
|
|
1012
|
-
|
|
1013
|
-
return true
|
|
1014
1020
|
})
|
|
1015
1021
|
|
|
1016
|
-
return schema as unknown as
|
|
1022
|
+
return schema as unknown as TwPostalCodeSchema<IsRequired>
|
|
1017
1023
|
}
|
|
1018
1024
|
|
|
1019
1025
|
/**
|
|
@@ -15,12 +15,12 @@ import { getLocale, type Locale } from "../../config"
|
|
|
15
15
|
/**
|
|
16
16
|
* Type definition for telephone validation error messages
|
|
17
17
|
*
|
|
18
|
-
* @interface
|
|
18
|
+
* @interface TwTelMessages
|
|
19
19
|
* @property {string} [required] - Message when field is required but empty
|
|
20
20
|
* @property {string} [invalid] - Message when telephone number format is invalid
|
|
21
21
|
* @property {string} [notInWhitelist] - Message when telephone number is not in whitelist
|
|
22
22
|
*/
|
|
23
|
-
export type
|
|
23
|
+
export type TwTelMessages = {
|
|
24
24
|
required?: string
|
|
25
25
|
invalid?: string
|
|
26
26
|
notInWhitelist?: string
|
|
@@ -31,28 +31,28 @@ export type TelMessages = {
|
|
|
31
31
|
*
|
|
32
32
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
33
33
|
*
|
|
34
|
-
* @interface
|
|
34
|
+
* @interface TwTelOptions
|
|
35
35
|
* @property {IsRequired} [required=true] - Whether the field is required
|
|
36
36
|
* @property {string[]} [whitelist] - Array of specific telephone numbers that are always allowed
|
|
37
37
|
* @property {Function} [transform] - Custom transformation function for telephone number
|
|
38
38
|
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
39
|
-
* @property {Record<Locale,
|
|
39
|
+
* @property {Record<Locale, TwTelMessages>} [i18n] - Custom error messages for different locales
|
|
40
40
|
*/
|
|
41
|
-
export type
|
|
41
|
+
export type TwTelOptions<IsRequired extends boolean = true> = {
|
|
42
42
|
whitelist?: string[]
|
|
43
43
|
transform?: (value: string) => string
|
|
44
44
|
defaultValue?: IsRequired extends true ? string : string | null
|
|
45
|
-
i18n?: Record<Locale,
|
|
45
|
+
i18n?: Record<Locale, TwTelMessages>
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Type alias for telephone validation schema based on required flag
|
|
50
50
|
*
|
|
51
51
|
* @template IsRequired - Whether the field is required
|
|
52
|
-
* @typedef
|
|
52
|
+
* @typedef TwTelSchema
|
|
53
53
|
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
54
54
|
*/
|
|
55
|
-
export type
|
|
55
|
+
export type TwTelSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Validates Taiwan landline telephone number format (Official 2024 rules)
|
|
@@ -174,7 +174,7 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
174
174
|
* @template IsRequired - Whether the field is required (affects return type)
|
|
175
175
|
* @param {IsRequired} [required=false] - Whether the field is required
|
|
176
176
|
* @param {Omit<ValidatorOptions<IsRequired>, 'required'>} [options] - Configuration options for validation
|
|
177
|
-
* @returns {
|
|
177
|
+
* @returns {TwTelSchema<IsRequired>} Zod schema for telephone number validation
|
|
178
178
|
*
|
|
179
179
|
* @description
|
|
180
180
|
* Creates a comprehensive Taiwan landline telephone number validator with support for
|
|
@@ -193,7 +193,7 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
193
193
|
* @example
|
|
194
194
|
* ```typescript
|
|
195
195
|
* // Basic telephone number validation
|
|
196
|
-
* const basicSchema =
|
|
196
|
+
* const basicSchema = twTel() // optional by default
|
|
197
197
|
* basicSchema.parse("0223456789") // ✓ Valid (Taipei)
|
|
198
198
|
* basicSchema.parse(null) // ✓ Valid (optional)
|
|
199
199
|
*
|
|
@@ -207,26 +207,26 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
207
207
|
* basicSchema.parse("0812345678") // ✗ Invalid (wrong format for 08)
|
|
208
208
|
*
|
|
209
209
|
* // With whitelist (only specific numbers allowed)
|
|
210
|
-
* const whitelistSchema =
|
|
210
|
+
* const whitelistSchema = twTel(false, {
|
|
211
211
|
* whitelist: ["0223456789", "0312345678"]
|
|
212
212
|
* })
|
|
213
213
|
* whitelistSchema.parse("0223456789") // ✓ Valid (in whitelist)
|
|
214
214
|
* whitelistSchema.parse("0287654321") // ✗ Invalid (not in whitelist)
|
|
215
215
|
*
|
|
216
216
|
* // Optional telephone number
|
|
217
|
-
* const optionalSchema =
|
|
217
|
+
* const optionalSchema = twTel(false)
|
|
218
218
|
* optionalSchema.parse("") // ✓ Valid (returns null)
|
|
219
219
|
* optionalSchema.parse("0223456789") // ✓ Valid
|
|
220
220
|
*
|
|
221
221
|
* // With custom transformation (remove separators)
|
|
222
|
-
* const transformSchema =
|
|
222
|
+
* const transformSchema = twTel(false, {
|
|
223
223
|
* transform: (value) => value.replace(/[^0-9]/g, '') // Keep only digits
|
|
224
224
|
* })
|
|
225
225
|
* transformSchema.parse("02-2345-6789") // ✓ Valid (separators removed)
|
|
226
226
|
* transformSchema.parse("02 2345 6789") // ✓ Valid (spaces removed)
|
|
227
227
|
*
|
|
228
228
|
* // With custom error messages
|
|
229
|
-
* const customSchema =
|
|
229
|
+
* const customSchema = twTel(false, {
|
|
230
230
|
* i18n: {
|
|
231
231
|
* en: { invalid: "Please enter a valid Taiwan landline number" },
|
|
232
232
|
* 'zh-TW': { invalid: "請輸入有效的台灣市話號碼" }
|
|
@@ -235,10 +235,10 @@ const validateTaiwanTel = (value: string): boolean => {
|
|
|
235
235
|
* ```
|
|
236
236
|
*
|
|
237
237
|
* @throws {z.ZodError} When validation fails with specific error messages
|
|
238
|
-
* @see {@link
|
|
238
|
+
* @see {@link TwTelOptions} for all available configuration options
|
|
239
239
|
* @see {@link validateTaiwanTel} for validation logic details
|
|
240
240
|
*/
|
|
241
|
-
export function
|
|
241
|
+
export function twTel<IsRequired extends boolean = false>(required?: IsRequired, options?: Omit<TwTelOptions<IsRequired>, 'required'>): TwTelSchema<IsRequired> {
|
|
242
242
|
const { whitelist, transform, defaultValue, i18n } = options ?? {}
|
|
243
243
|
|
|
244
244
|
const isRequired = required ?? false as IsRequired
|
|
@@ -247,7 +247,7 @@ export function tel<IsRequired extends boolean = false>(required?: IsRequired, o
|
|
|
247
247
|
const actualDefaultValue = defaultValue ?? (isRequired ? "" : null)
|
|
248
248
|
|
|
249
249
|
// Helper function to get custom message or fallback to default i18n
|
|
250
|
-
const getMessage = (key: keyof
|
|
250
|
+
const getMessage = (key: keyof TwTelMessages, params?: Record<string, any>) => {
|
|
251
251
|
if (i18n) {
|
|
252
252
|
const currentLocale = getLocale()
|
|
253
253
|
const customMessages = i18n[currentLocale]
|
|
@@ -290,35 +290,36 @@ export function tel<IsRequired extends boolean = false>(required?: IsRequired, o
|
|
|
290
290
|
|
|
291
291
|
const baseSchema = isRequired ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
292
292
|
|
|
293
|
-
const schema = baseSchema.
|
|
294
|
-
if (val === null) return
|
|
293
|
+
const schema = baseSchema.superRefine((val, ctx) => {
|
|
294
|
+
if (val === null) return
|
|
295
295
|
|
|
296
296
|
// Required check
|
|
297
297
|
if (isRequired && (val === "" || val === "null" || val === "undefined")) {
|
|
298
|
-
|
|
298
|
+
ctx.addIssue({ code: "custom", message: getMessage("required") })
|
|
299
|
+
return
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
if (val === null) return
|
|
302
|
-
if (!isRequired && val === "") return
|
|
302
|
+
if (val === null) return
|
|
303
|
+
if (!isRequired && val === "") return
|
|
303
304
|
|
|
304
305
|
// Allowlist check (if an allowlist is provided, only allow values in the allowlist)
|
|
305
306
|
if (whitelist && whitelist.length > 0) {
|
|
306
307
|
if (whitelist.includes(val)) {
|
|
307
|
-
return
|
|
308
|
+
return
|
|
308
309
|
}
|
|
309
310
|
// If not in the allowlist, reject regardless of format
|
|
310
|
-
|
|
311
|
+
ctx.addIssue({ code: "custom", message: getMessage("notInWhitelist") })
|
|
312
|
+
return
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
// Taiwan telephone format validation (only if no allowlist or allowlist is empty)
|
|
314
316
|
if (!validateTaiwanTel(val)) {
|
|
315
|
-
|
|
317
|
+
ctx.addIssue({ code: "custom", message: getMessage("invalid") })
|
|
318
|
+
return
|
|
316
319
|
}
|
|
317
|
-
|
|
318
|
-
return true
|
|
319
320
|
})
|
|
320
321
|
|
|
321
|
-
return schema as unknown as
|
|
322
|
+
return schema as unknown as TwTelSchema<IsRequired>
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
/**
|
package/tests/common/id.test.ts
CHANGED
|
@@ -135,8 +135,8 @@ describe.each(locales)("id(true) locale: $locale", ({ locale, messages }) => {
|
|
|
135
135
|
})
|
|
136
136
|
|
|
137
137
|
it("should apply transform function", () => {
|
|
138
|
-
const schema = id(true, { type: "
|
|
139
|
-
expect(schema.parse("
|
|
138
|
+
const schema = id(true, { type: "uuid", transform: (val) => val.toUpperCase() })
|
|
139
|
+
expect(schema.parse("550e8400-e29b-41d4-a716-446655440000")).toBe("550E8400-E29B-41D4-A716-446655440000")
|
|
140
140
|
})
|
|
141
141
|
})
|
|
142
142
|
|
|
@@ -162,7 +162,9 @@ describe.each(locales)("id(true) locale: $locale", ({ locale, messages }) => {
|
|
|
162
162
|
it("should accept valid numeric IDs", () => {
|
|
163
163
|
const schema = id(true, { type: "numeric" })
|
|
164
164
|
validIds.numeric.forEach((validId) => {
|
|
165
|
-
|
|
165
|
+
const result = schema.parse(validId)
|
|
166
|
+
expect(typeof result).toBe("number")
|
|
167
|
+
expect(result).toBe(Number(validId))
|
|
166
168
|
})
|
|
167
169
|
})
|
|
168
170
|
|
|
@@ -461,7 +463,8 @@ describe.each(locales)("id(true) locale: $locale", ({ locale, messages }) => {
|
|
|
461
463
|
expect(schema.parse("")).toBe(null)
|
|
462
464
|
expect(() => schema.parse("ab")).toThrow() // not numeric
|
|
463
465
|
expect(() => schema.parse("12")).toThrow() // too short
|
|
464
|
-
expect(schema.parse("12345")).toBe(
|
|
466
|
+
expect(schema.parse("12345")).toBe(12345) // Returns number for numeric type
|
|
467
|
+
expect(typeof schema.parse("12345")).toBe("number")
|
|
465
468
|
})
|
|
466
469
|
|
|
467
470
|
it("should handle multiple allowed types with constraints", () => {
|
|
@@ -499,4 +502,59 @@ describe.each(locales)("id(true) locale: $locale", ({ locale, messages }) => {
|
|
|
499
502
|
expect(ID_PATTERNS.objectId.test("507f1f77bcf86cd799439011")).toBe(true)
|
|
500
503
|
})
|
|
501
504
|
})
|
|
505
|
+
|
|
506
|
+
describe("numeric type returns number", () => {
|
|
507
|
+
it("should return number type when type is numeric and required is true", () => {
|
|
508
|
+
const schema = id(true, { type: "numeric" })
|
|
509
|
+
const result = schema.parse("123")
|
|
510
|
+
expect(result).toBe(123)
|
|
511
|
+
expect(typeof result).toBe("number")
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it("should return number | null when type is numeric and required is false", () => {
|
|
515
|
+
const schema = id(false, { type: "numeric" })
|
|
516
|
+
const result = schema.parse("456")
|
|
517
|
+
expect(result).toBe(456)
|
|
518
|
+
expect(typeof result).toBe("number")
|
|
519
|
+
|
|
520
|
+
const nullResult = schema.parse(null)
|
|
521
|
+
expect(nullResult).toBe(null)
|
|
522
|
+
|
|
523
|
+
const emptyResult = schema.parse("")
|
|
524
|
+
expect(emptyResult).toBe(null)
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it("should use numeric default value for numeric type", () => {
|
|
528
|
+
const schema = id(true, { type: "numeric", defaultValue: 999 })
|
|
529
|
+
const result = schema.parse("")
|
|
530
|
+
expect(result).toBe(999)
|
|
531
|
+
expect(typeof result).toBe("number")
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it("should validate numeric ID constraints", () => {
|
|
535
|
+
const schema = id(true, { type: "numeric", minLength: 3, maxLength: 5 })
|
|
536
|
+
|
|
537
|
+
expect(schema.parse("123")).toBe(123)
|
|
538
|
+
expect(schema.parse("12345")).toBe(12345)
|
|
539
|
+
|
|
540
|
+
// Too short
|
|
541
|
+
expect(() => schema.parse("12")).toThrow()
|
|
542
|
+
|
|
543
|
+
// Too long
|
|
544
|
+
expect(() => schema.parse("123456")).toThrow()
|
|
545
|
+
|
|
546
|
+
// Not numeric
|
|
547
|
+
expect(() => schema.parse("abc")).toThrow()
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it("should convert string numbers to number type", () => {
|
|
551
|
+
const schema = id(true, { type: "numeric" })
|
|
552
|
+
|
|
553
|
+
expect(schema.parse("0")).toBe(0)
|
|
554
|
+
expect(schema.parse("999999")).toBe(999999)
|
|
555
|
+
expect(schema.parse(123)).toBe(123)
|
|
556
|
+
|
|
557
|
+
expect(typeof schema.parse("123")).toBe("number")
|
|
558
|
+
})
|
|
559
|
+
})
|
|
502
560
|
})
|