@validex/core 1.0.0
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/LICENSE +21 -0
- package/README.md +583 -0
- package/dist/checks/index.cjs +51 -0
- package/dist/checks/index.d.cts +207 -0
- package/dist/checks/index.d.ts +207 -0
- package/dist/checks/index.js +51 -0
- package/dist/chunk-2OTGUKO7.cjs +26 -0
- package/dist/chunk-4HUGF4EZ.js +0 -0
- package/dist/chunk-5CYBSNNG.cjs +33 -0
- package/dist/chunk-5DOFRLSB.js +30 -0
- package/dist/chunk-64X6D23X.cjs +24 -0
- package/dist/chunk-6MGS4JWP.js +152 -0
- package/dist/chunk-AGJWIOFF.js +24 -0
- package/dist/chunk-BAFEYOPS.js +43 -0
- package/dist/chunk-BYMZTDYD.js +56 -0
- package/dist/chunk-D232V332.cjs +30 -0
- package/dist/chunk-FD77PZXB.js +9 -0
- package/dist/chunk-H3XHQLZF.cjs +43 -0
- package/dist/chunk-ISY3F7TI.cjs +239 -0
- package/dist/chunk-JEKRBIPN.cjs +1 -0
- package/dist/chunk-KTN4NQQL.js +33 -0
- package/dist/chunk-LSQNDPFQ.cjs +9 -0
- package/dist/chunk-NDLUDRZJ.js +24 -0
- package/dist/chunk-OCTLUBGT.cjs +24 -0
- package/dist/chunk-OFT3FQPJ.cjs +152 -0
- package/dist/chunk-OOFMB7K5.js +34 -0
- package/dist/chunk-OTPQTLPM.js +50 -0
- package/dist/chunk-P3FRVJ3U.cjs +50 -0
- package/dist/chunk-PFPNNQGJ.js +30 -0
- package/dist/chunk-PQ4TUE2Q.cjs +2688 -0
- package/dist/chunk-SMDC2EAD.js +26 -0
- package/dist/chunk-TB6J73U7.js +239 -0
- package/dist/chunk-TBVAKZA5.js +2688 -0
- package/dist/chunk-TSPTIW3V.cjs +34 -0
- package/dist/chunk-WE2OD5XD.cjs +30 -0
- package/dist/chunk-WKVMDEA3.js +26 -0
- package/dist/chunk-ZAUX2RGL.cjs +56 -0
- package/dist/chunk-ZWIO2MJX.cjs +26 -0
- package/dist/cli/index.cjs +120 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +120 -0
- package/dist/commonPasswords-3BYUBARZ.cjs +10 -0
- package/dist/commonPasswords-ZYOEI6PG.js +10 -0
- package/dist/countryCodes-EKJKVHR5.cjs +10 -0
- package/dist/countryCodes-RTJZVCLB.js +10 -0
- package/dist/countryCodes-VY56VZPT.cjs +255 -0
- package/dist/countryCodes-YRY75MQP.js +255 -0
- package/dist/creditCardPrefixes-EXMJZGE7.cjs +10 -0
- package/dist/creditCardPrefixes-EZK7T4IZ.js +10 -0
- package/dist/creditCardPrefixes-HKWKCHNU.cjs +48 -0
- package/dist/creditCardPrefixes-QP3S4ZAU.js +48 -0
- package/dist/currencyCodes-GU6W3HSN.cjs +171 -0
- package/dist/currencyCodes-P67AASLW.js +171 -0
- package/dist/currencyCodes-RMRLGDME.cjs +10 -0
- package/dist/currencyCodes-U6TSAWDR.js +10 -0
- package/dist/disposableDomains-DCXSV422.js +10 -0
- package/dist/disposableDomains-USU2JQSF.cjs +10 -0
- package/dist/ibanPatterns-2PM32RIY.cjs +85 -0
- package/dist/ibanPatterns-BSQUWKLY.js +85 -0
- package/dist/ibanPatterns-KTLY6TZY.cjs +10 -0
- package/dist/ibanPatterns-LJRPR7FV.js +10 -0
- package/dist/index-Cid7Ygr_.d.cts +950 -0
- package/dist/index-Cid7Ygr_.d.ts +950 -0
- package/dist/index.cjs +361 -0
- package/dist/index.d.cts +132 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.js +361 -0
- package/dist/locales/en.cjs +10 -0
- package/dist/locales/en.d.cts +234 -0
- package/dist/locales/en.d.ts +234 -0
- package/dist/locales/en.js +10 -0
- package/dist/passwordsTier1-NAZLSHKW.cjs +105 -0
- package/dist/passwordsTier1-OYRMLOWD.js +105 -0
- package/dist/passwordsTier2-GYJTYGY6.cjs +906 -0
- package/dist/passwordsTier2-JAEO5AYY.js +906 -0
- package/dist/passwordsTier3-BAPUFHZM.cjs +9006 -0
- package/dist/passwordsTier3-E6WBK5OB.js +9006 -0
- package/dist/phoneDetection-AFSSD4IB.cjs +6 -0
- package/dist/phoneDetection-G23LZ6MU.js +6 -0
- package/dist/phoneParser-2RTXDB6H.js +10 -0
- package/dist/phoneParser-CGRP2OUN.cjs +10 -0
- package/dist/postalCodes-4EZVDT2N.cjs +10 -0
- package/dist/postalCodes-ZPAJB3P5.js +10 -0
- package/dist/reservedUsernames-3QPPKUXR.cjs +246 -0
- package/dist/reservedUsernames-GIK6NX3J.js +246 -0
- package/dist/reservedUsernames-QR4ONXSL.js +10 -0
- package/dist/reservedUsernames-W65FGT6A.cjs +10 -0
- package/dist/rules/index.cjs +66 -0
- package/dist/rules/index.d.cts +2 -0
- package/dist/rules/index.d.ts +2 -0
- package/dist/rules/index.js +66 -0
- package/dist/utilities/index.cjs +8 -0
- package/dist/utilities/index.d.cts +47 -0
- package/dist/utilities/index.d.ts +47 -0
- package/dist/utilities/index.js +8 -0
- package/dist/vatPatterns-BLRXHNCP.js +36 -0
- package/dist/vatPatterns-DNVZJPTW.js +10 -0
- package/dist/vatPatterns-NPN6SV2Y.cjs +36 -0
- package/dist/vatPatterns-RRHUTA3U.cjs +10 -0
- package/package.json +133 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 validex contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
# @validex/core
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@validex/core)
|
|
4
|
+
[](https://www.npmjs.com/package/@validex/core)
|
|
5
|
+
[](https://bundlephobia.com/package/@validex/core)
|
|
6
|
+
[](https://github.com/chiptoma/validex/actions)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://github.com/chiptoma/validex/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
**Type-safe validation rules built on Zod** — tree-shakeable, so you only ship what you use.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
- [Why validex?](#why-validex)
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Quick Start](#quick-start)
|
|
17
|
+
- [Rules](#rules) — all 25 rules
|
|
18
|
+
- [Bundle Size](#bundle-size)
|
|
19
|
+
- [Configuration](#configuration) — global defaults, three-tier merge, preloading
|
|
20
|
+
- [Cross-Field Validation](#cross-field-validation) — sameAs, requiredWhen
|
|
21
|
+
- [Chainable Methods](#chainable-methods) — checks and transforms
|
|
22
|
+
- [Check Functions](#check-functions) — standalone pure functions
|
|
23
|
+
- [Error Handling](#error-handling) — structured errors, getParams
|
|
24
|
+
- [i18n](#i18n) — translations, CLI
|
|
25
|
+
- [Custom Rules](#custom-rules) — createRule, customFn
|
|
26
|
+
- [Framework Adapters](#framework-adapters) — Nuxt, Fastify
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Why validex?
|
|
31
|
+
|
|
32
|
+
I built validex because I was **fed up** writing the same validation rules over and over again in every project.
|
|
33
|
+
|
|
34
|
+
Different teams, different defaults, forgetting what I had configured last time, and ending up with inconsistent behavior across the codebase. Sound familiar?
|
|
35
|
+
|
|
36
|
+
Validex was created to solve that pain once and for all:
|
|
37
|
+
|
|
38
|
+
- **One config system** — `setup()` lets you define your defaults globally, per-rule, or per-call. Three-tier merge (built-in defaults → global config → per-call options) so you never repeat yourself again.
|
|
39
|
+
- **One consistent error surface** — `validate()` always returns the same clean shape — flat errors, nested errors, first-per-field, raw issues. One function, one result, every time.
|
|
40
|
+
- **Every error is validex-owned** — no raw Zod messages leak to your users. Every error carries a namespace, code, and label for precise routing.
|
|
41
|
+
- **25 production-ready rules** covering the fields you actually use: identity, auth, networking, finance, and text.
|
|
42
|
+
- **Tree-shakeable & lightweight** — 5–6 kB Brotli per rule (shared core included). All 25 rules together = 13 kB. Heavy data loads on demand.
|
|
43
|
+
- **i18n-ready out of the box** — key mode, `t()` function support, label/message transforms, and a CLI that generates ready-to-translate locale files.
|
|
44
|
+
- **First-class framework adapters** — Nuxt and Fastify integrations that feel native.
|
|
45
|
+
|
|
46
|
+
Stop copy-pasting rules. Get consistent, maintainable validation with sensible defaults — and only ship what you actually use.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm add @validex/core zod
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
### Single rule
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { Email } from '@validex/core'
|
|
60
|
+
|
|
61
|
+
const schema = Email()
|
|
62
|
+
schema.parse('hello@example.com') // OK
|
|
63
|
+
schema.parse('not-an-email') // throws ZodError
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Rule with options
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { Password } from '@validex/core'
|
|
70
|
+
|
|
71
|
+
const schema = Password({
|
|
72
|
+
length: { min: 10 },
|
|
73
|
+
uppercase: { min: 2 },
|
|
74
|
+
blockCommon: 'basic',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
schema.parse('ABcdefgh1!') // OK — 10+ chars, 2 uppercase, 1 digit, 1 special
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Composed schema with `validate()`
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { z } from 'zod'
|
|
84
|
+
import { Email, Password, validate } from '@validex/core'
|
|
85
|
+
|
|
86
|
+
const schema = z.object({
|
|
87
|
+
email: Email(),
|
|
88
|
+
password: Password(),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const result = await validate(schema, {
|
|
92
|
+
email: 'user@example.com',
|
|
93
|
+
password: 'Str0ng!Pass',
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (result.success) {
|
|
97
|
+
console.log(result.data) // typed as { email: string; password: string }
|
|
98
|
+
} else {
|
|
99
|
+
console.log(result.errors) // { email: ['...'], password: ['...'] }
|
|
100
|
+
console.log(result.firstErrors) // { email: '...', password: '...' }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Rules
|
|
105
|
+
|
|
106
|
+
| Rule | Description |
|
|
107
|
+
|------|-------------|
|
|
108
|
+
| Email | Email address with domain filtering, plus-alias blocking, and disposable detection |
|
|
109
|
+
| Password | Strength rules: length, casing, digits, specials, consecutive limits, common-password ban |
|
|
110
|
+
| PasswordConfirmation | Confirms two password fields match |
|
|
111
|
+
| PersonName | Human name with unicode support, word count, and boundary rules |
|
|
112
|
+
| BusinessName | Company/organization name with boundary and consecutive limits |
|
|
113
|
+
| Phone | International phone via libphonenumber-js |
|
|
114
|
+
| Website | URL restricted to http/https with optional www and domain filtering |
|
|
115
|
+
| Url | General URL with protocol, TLD, and domain validation |
|
|
116
|
+
| Username | Alphanumeric with configurable separators and reserved-word ban |
|
|
117
|
+
| Slug | URL-safe slug (lowercase, hyphens, length limits) |
|
|
118
|
+
| PostalCode | Country-aware postal/ZIP code |
|
|
119
|
+
| LicenseKey | Software license key format (segments, separators, charset) |
|
|
120
|
+
| Uuid | UUID v1-v7 validation |
|
|
121
|
+
| Jwt | JSON Web Token structure with optional expiry checks |
|
|
122
|
+
| DateTime | Date/time string with format and range constraints |
|
|
123
|
+
| Token | Generic token validation (hex, base64, nanoid, etc.) |
|
|
124
|
+
| Text | Free text with length, word count, content detection, and regex override |
|
|
125
|
+
| Country | ISO 3166 country code (alpha-2, alpha-3) |
|
|
126
|
+
| Currency | ISO 4217 currency code |
|
|
127
|
+
| Color | Hex, RGB, HSL, and named CSS color formats |
|
|
128
|
+
| CreditCard | Card number with Luhn check and issuer detection |
|
|
129
|
+
| Iban | International Bank Account Number with country patterns |
|
|
130
|
+
| VatNumber | EU VAT identification number |
|
|
131
|
+
| MacAddress | MAC address (colon, hyphen, and dot notations) |
|
|
132
|
+
| IpAddress | IPv4 and IPv6 with optional CIDR notation |
|
|
133
|
+
|
|
134
|
+
## Bundle Size
|
|
135
|
+
|
|
136
|
+
Every rule shares a ~5 kB core (Brotli). Each additional rule adds 0.1-0.8 kB. Measured with esbuild `--splitting` + Brotli, excluding `zod` peer dependency.
|
|
137
|
+
|
|
138
|
+
| Rule | Initial (Brotli) | On-demand data | Trigger |
|
|
139
|
+
|------|-----------------|----------------|---------|
|
|
140
|
+
| Email | 5.7 kB | — | — |
|
|
141
|
+
| Password | 5.6 kB | +0.5 kB / +3.8 kB / +35.5 kB | `blockCommon: 'basic'` / `'moderate'` / `'strict'` |
|
|
142
|
+
| PasswordConfirmation | 5.7 kB | — | — |
|
|
143
|
+
| PersonName | 5.7 kB | — | — |
|
|
144
|
+
| BusinessName | 5.7 kB | — | — |
|
|
145
|
+
| Phone | 5.7 kB | external | libphonenumber-js peer dep |
|
|
146
|
+
| Website | 5.7 kB | — | — |
|
|
147
|
+
| Url | 5.6 kB | — | — |
|
|
148
|
+
| Username | 5.9 kB | +0.8 kB | `blockReserved: true` |
|
|
149
|
+
| Slug | 5.5 kB | — | — |
|
|
150
|
+
| PostalCode | 5.4 kB | external | postcode-validator peer dep |
|
|
151
|
+
| LicenseKey | 5.5 kB | — | — |
|
|
152
|
+
| Uuid | 5.3 kB | — | — |
|
|
153
|
+
| Jwt | 5.6 kB | — | — |
|
|
154
|
+
| DateTime | 5.6 kB | — | — |
|
|
155
|
+
| Token | 5.5 kB | — | — |
|
|
156
|
+
| Text | 5.5 kB | — | — |
|
|
157
|
+
| Country | 5.4 kB | +2.4 kB | First use |
|
|
158
|
+
| Currency | 5.4 kB | +0.3 kB | First use |
|
|
159
|
+
| Color | 5.5 kB | — | — |
|
|
160
|
+
| CreditCard | 5.6 kB | +0.3 kB | First use |
|
|
161
|
+
| Iban | 5.5 kB | +0.7 kB | First use |
|
|
162
|
+
| VatNumber | 5.5 kB | +0.3 kB | First use |
|
|
163
|
+
| MacAddress | 5.3 kB | — | — |
|
|
164
|
+
| IpAddress | 5.6 kB | — | — |
|
|
165
|
+
|
|
166
|
+
| Combination | Initial (Brotli) |
|
|
167
|
+
|-------------|-----------------|
|
|
168
|
+
| Email + Password | 6.0 kB |
|
|
169
|
+
| Form (Email + Password + PersonName + Phone) | 6.9 kB |
|
|
170
|
+
| All 25 rules | 13.0 kB |
|
|
171
|
+
|
|
172
|
+
"On-demand data" loads asynchronously on first use or when the listed option is enabled. Not included in the initial bundle.
|
|
173
|
+
|
|
174
|
+
## Configuration
|
|
175
|
+
|
|
176
|
+
### Global defaults with `setup()`
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { setup, Email, Password } from '@validex/core'
|
|
180
|
+
|
|
181
|
+
setup({
|
|
182
|
+
rules: {
|
|
183
|
+
email: { blockDisposable: true },
|
|
184
|
+
password: { length: { min: 10 }, special: { min: 2 } },
|
|
185
|
+
},
|
|
186
|
+
i18n: {
|
|
187
|
+
enabled: true,
|
|
188
|
+
t: (key, params) => translate(key, params),
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// Rules now use your defaults — no need to pass options every time
|
|
193
|
+
const emailSchema = Email()
|
|
194
|
+
const passwordSchema = Password()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Three-tier merge
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
built-in defaults < setup() config < per-call options
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Per-call options override `setup()` config, which overrides built-in defaults. Passing `undefined` for a per-call option removes the global setting for that field.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { setup, Email } from '@validex/core'
|
|
207
|
+
|
|
208
|
+
setup({ rules: { email: { blockDisposable: true } } })
|
|
209
|
+
|
|
210
|
+
Email() // blockDisposable: true (from setup)
|
|
211
|
+
Email({ blockPlusAlias: true }) // blockDisposable: true + blockPlusAlias: true
|
|
212
|
+
Email({ blockDisposable: undefined }) // blockDisposable removed for this call
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `resetConfig()`
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { resetConfig } from '@validex/core'
|
|
219
|
+
|
|
220
|
+
resetConfig() // resets to built-in defaults
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### `preloadData()`
|
|
224
|
+
|
|
225
|
+
Preload async data files at startup so first validation has no delay:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { preloadData } from '@validex/core'
|
|
229
|
+
|
|
230
|
+
await preloadData({
|
|
231
|
+
disposable: true,
|
|
232
|
+
passwords: 'moderate',
|
|
233
|
+
reserved: true,
|
|
234
|
+
phone: 'mobile',
|
|
235
|
+
countryCodes: true,
|
|
236
|
+
currencyCodes: true,
|
|
237
|
+
ibanPatterns: true,
|
|
238
|
+
vatPatterns: true,
|
|
239
|
+
creditCardPrefixes: true,
|
|
240
|
+
postalCodes: true,
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Cross-Field Validation
|
|
245
|
+
|
|
246
|
+
### `sameAs`
|
|
247
|
+
|
|
248
|
+
Creates a `superRefine` callback that verifies two fields hold the same value:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import { z } from 'zod'
|
|
252
|
+
import { Password, sameAs } from '@validex/core'
|
|
253
|
+
|
|
254
|
+
const schema = z.object({
|
|
255
|
+
password: Password(),
|
|
256
|
+
confirmPassword: z.string(),
|
|
257
|
+
}).superRefine(sameAs('confirmPassword', 'password', {
|
|
258
|
+
message: 'Passwords do not match',
|
|
259
|
+
}))
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`PasswordConfirmation` auto-wires this — it registers a `sameAs: 'password'` constraint automatically:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { z } from 'zod'
|
|
266
|
+
import { Password, PasswordConfirmation, validate } from '@validex/core'
|
|
267
|
+
|
|
268
|
+
const schema = z.object({
|
|
269
|
+
password: Password(),
|
|
270
|
+
confirmPassword: PasswordConfirmation(),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const result = await validate(schema, {
|
|
274
|
+
password: 'Str0ng!Pass',
|
|
275
|
+
confirmPassword: 'different',
|
|
276
|
+
})
|
|
277
|
+
// result.firstErrors.confirmPassword → "Password Confirmation must match Password"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `requiredWhen`
|
|
281
|
+
|
|
282
|
+
Creates a `superRefine` callback that marks a field as required when a condition is met:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { z } from 'zod'
|
|
286
|
+
import { requiredWhen } from '@validex/core'
|
|
287
|
+
|
|
288
|
+
const schema = z.object({
|
|
289
|
+
accountType: z.string(),
|
|
290
|
+
companyName: z.string().optional(),
|
|
291
|
+
}).superRefine(requiredWhen(
|
|
292
|
+
'companyName',
|
|
293
|
+
(data) => data['accountType'] === 'business',
|
|
294
|
+
{ message: 'Company name is required for business accounts' },
|
|
295
|
+
))
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### `validate()` resolves cross-field
|
|
299
|
+
|
|
300
|
+
`schema.safeParse()` only runs field-level validation. `validate()` adds cross-field checks (`sameAs`, `requiredWhen`) after Zod parsing:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// safeParse — field-level only, no cross-field
|
|
304
|
+
const zodResult = schema.safeParse(data)
|
|
305
|
+
|
|
306
|
+
// validate — runs field-level + cross-field
|
|
307
|
+
const result = await validate(schema, data)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Chainable Methods
|
|
311
|
+
|
|
312
|
+
Import `@validex/core` and all Zod string schemas get these methods:
|
|
313
|
+
|
|
314
|
+
### Checks (return same type, add refinement)
|
|
315
|
+
|
|
316
|
+
| Method | Options | Description |
|
|
317
|
+
|--------|---------|-------------|
|
|
318
|
+
| `.hasUppercase(opts?)` | `min?, max?` | Requires uppercase letters |
|
|
319
|
+
| `.hasLowercase(opts?)` | `min?, max?` | Requires lowercase letters |
|
|
320
|
+
| `.hasDigits(opts?)` | `min?, max?` | Requires digits |
|
|
321
|
+
| `.hasSpecial(opts?)` | `min?, max?` | Requires special characters |
|
|
322
|
+
| `.noEmails(opts?)` | — | Blocks email addresses |
|
|
323
|
+
| `.noUrls(opts?)` | — | Blocks URLs |
|
|
324
|
+
| `.noHtml(opts?)` | — | Blocks HTML tags |
|
|
325
|
+
| `.noPhoneNumbers(opts?)` | — | Blocks phone numbers |
|
|
326
|
+
| `.noSpaces(opts?)` | — | Blocks whitespace |
|
|
327
|
+
| `.onlyAlpha(opts?)` | — | Letters only |
|
|
328
|
+
| `.onlyNumeric(opts?)` | — | Digits only |
|
|
329
|
+
| `.onlyAlphanumeric(opts?)` | — | Letters + digits |
|
|
330
|
+
| `.onlyAlphaSpaceHyphen(opts?)` | — | Letters, spaces, hyphens |
|
|
331
|
+
| `.onlyAlphanumericSpaceHyphen(opts?)` | — | Letters, digits, spaces, hyphens |
|
|
332
|
+
| `.maxWords(opts)` | `max` | Maximum word count |
|
|
333
|
+
| `.minWords(opts)` | `min` | Minimum word count |
|
|
334
|
+
| `.maxConsecutive(opts)` | `max` | Max consecutive identical chars |
|
|
335
|
+
|
|
336
|
+
### Transforms (return ZodPipe)
|
|
337
|
+
|
|
338
|
+
| Method | Description |
|
|
339
|
+
|--------|-------------|
|
|
340
|
+
| `.toTitleCase()` | Converts to Title Case |
|
|
341
|
+
| `.toSlug()` | Converts to url-safe-slug |
|
|
342
|
+
| `.stripHtml()` | Removes HTML tags |
|
|
343
|
+
| `.collapseWhitespace()` | Collapses multiple spaces to single |
|
|
344
|
+
| `.emptyToUndefined()` | Converts `""` to `undefined` |
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import { z } from 'zod'
|
|
348
|
+
import '@validex/core'
|
|
349
|
+
|
|
350
|
+
const schema = z.string().hasUppercase({ min: 2 }).noSpaces().toSlug()
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Check Functions
|
|
354
|
+
|
|
355
|
+
Pure functions, no Zod dependency. Import from `@validex/core/checks`.
|
|
356
|
+
|
|
357
|
+
### Composition
|
|
358
|
+
|
|
359
|
+
| Function | Signature | Description |
|
|
360
|
+
|----------|-----------|-------------|
|
|
361
|
+
| `hasUppercase` | `(value: string, min: number, max?: number) => boolean` | Uppercase letter count within `[min, max]` |
|
|
362
|
+
| `hasLowercase` | `(value: string, min: number, max?: number) => boolean` | Lowercase letter count within `[min, max]` |
|
|
363
|
+
| `hasDigits` | `(value: string, min: number, max?: number) => boolean` | Digit count within `[min, max]` |
|
|
364
|
+
| `hasSpecial` | `(value: string, min: number, max?: number) => boolean` | Special character count within `[min, max]` |
|
|
365
|
+
|
|
366
|
+
### Detection
|
|
367
|
+
|
|
368
|
+
| Function | Signature | Description |
|
|
369
|
+
|----------|-----------|-------------|
|
|
370
|
+
| `containsEmail` | `(value: string) => boolean` | Detects email-like patterns |
|
|
371
|
+
| `containsUrl` | `(value: string) => boolean` | Detects URL-like patterns |
|
|
372
|
+
| `containsHtml` | `(value: string) => boolean` | Detects HTML tags |
|
|
373
|
+
| `containsPhoneNumber` | `(value: string) => Promise<boolean>` | Detects phone numbers (async, uses libphonenumber-js) |
|
|
374
|
+
|
|
375
|
+
### Restriction
|
|
376
|
+
|
|
377
|
+
| Function | Signature | Description |
|
|
378
|
+
|----------|-----------|-------------|
|
|
379
|
+
| `onlyAlpha` | `(value: string) => boolean` | Every character is a unicode letter |
|
|
380
|
+
| `onlyNumeric` | `(value: string) => boolean` | Every character is a digit |
|
|
381
|
+
| `onlyAlphanumeric` | `(value: string) => boolean` | Every character is a letter or digit |
|
|
382
|
+
| `onlyAlphaSpaceHyphen` | `(value: string) => boolean` | Letters, spaces, hyphens only |
|
|
383
|
+
| `onlyAlphanumericSpaceHyphen` | `(value: string) => boolean` | Letters, digits, spaces, hyphens only |
|
|
384
|
+
|
|
385
|
+
### Limits
|
|
386
|
+
|
|
387
|
+
| Function | Signature | Description |
|
|
388
|
+
|----------|-----------|-------------|
|
|
389
|
+
| `maxWords` | `(value: string, max: number) => boolean` | At most `max` words |
|
|
390
|
+
| `minWords` | `(value: string, min: number) => boolean` | At least `min` words |
|
|
391
|
+
| `maxConsecutive` | `(value: string, max: number) => boolean` | No character repeats more than `max` times |
|
|
392
|
+
| `noSpaces` | `(value: string) => boolean` | No whitespace characters |
|
|
393
|
+
|
|
394
|
+
### Transforms
|
|
395
|
+
|
|
396
|
+
| Function | Signature | Description |
|
|
397
|
+
|----------|-----------|-------------|
|
|
398
|
+
| `emptyToUndefined` | `(value: unknown) => unknown` | `""` and `null` to `undefined` |
|
|
399
|
+
| `toTitleCase` | `(value: string) => string` | Title Case with hyphen/apostrophe handling |
|
|
400
|
+
| `toSlug` | `(value: string) => string` | URL-safe slug |
|
|
401
|
+
| `stripHtml` | `(value: string) => string` | Removes HTML tags |
|
|
402
|
+
| `collapseWhitespace` | `(value: string) => string` | Collapses whitespace, trims |
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import { hasUppercase, containsEmail, toSlug } from '@validex/core/checks'
|
|
406
|
+
|
|
407
|
+
hasUppercase('Hello', 1) // true
|
|
408
|
+
containsEmail('hi@test.com') // true
|
|
409
|
+
toSlug('Hello World!') // 'hello-world'
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Error Handling
|
|
413
|
+
|
|
414
|
+
### Error structure
|
|
415
|
+
|
|
416
|
+
Every validex error carries structured metadata via Zod's custom error params:
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
ctx.addIssue({
|
|
420
|
+
code: 'custom',
|
|
421
|
+
params: {
|
|
422
|
+
code: 'disposableBlocked',
|
|
423
|
+
namespace: 'email',
|
|
424
|
+
label: 'Email',
|
|
425
|
+
domain: 'tempmail.com',
|
|
426
|
+
},
|
|
427
|
+
})
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### `getParams(issue)`
|
|
431
|
+
|
|
432
|
+
Extract structured metadata from any Zod issue:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import { Email, getParams } from '@validex/core'
|
|
436
|
+
|
|
437
|
+
const schema = Email()
|
|
438
|
+
const result = schema.safeParse('user@tempmail.com')
|
|
439
|
+
|
|
440
|
+
if (!result.success) {
|
|
441
|
+
const params = getParams(result.error.issues[0])
|
|
442
|
+
// { code: 'disposableBlocked', namespace: 'email', label: 'Email',
|
|
443
|
+
// key: 'validation.messages.email.disposableBlocked', path: [], ... }
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Error code pattern
|
|
448
|
+
|
|
449
|
+
Keys follow: `validation.messages.{namespace}.{code}`
|
|
450
|
+
|
|
451
|
+
- `validation.messages.email.disposableBlocked`
|
|
452
|
+
- `validation.messages.password.commonBlocked`
|
|
453
|
+
- `validation.messages.username.reservedBlocked`
|
|
454
|
+
|
|
455
|
+
### `validate()` result
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
interface ValidationResult<T> {
|
|
459
|
+
success: boolean
|
|
460
|
+
data?: T // typed parsed data (when success)
|
|
461
|
+
errors: Record<string, readonly string[]> // dot-path to all messages
|
|
462
|
+
firstErrors: Record<string, string> // dot-path to first message
|
|
463
|
+
nestedErrors: NestedErrors // nested object matching schema shape
|
|
464
|
+
issues: ReadonlyArray<unknown> // raw Zod issues (escape hatch)
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## i18n
|
|
469
|
+
|
|
470
|
+
### Setup
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
import { setup } from '@validex/core'
|
|
474
|
+
|
|
475
|
+
setup({
|
|
476
|
+
i18n: {
|
|
477
|
+
enabled: true,
|
|
478
|
+
prefix: 'validation', // default
|
|
479
|
+
separator: '.', // default
|
|
480
|
+
t: (key, params) => i18next.t(key, params),
|
|
481
|
+
},
|
|
482
|
+
})
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Key pattern
|
|
486
|
+
|
|
487
|
+
`validation.messages.{namespace}.{code}`
|
|
488
|
+
|
|
489
|
+
When `i18n.enabled` is `true` and `t()` is provided, validex calls `t()` automatically for every error message and field label.
|
|
490
|
+
|
|
491
|
+
### Label transforms
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
setup({
|
|
495
|
+
label: {
|
|
496
|
+
fallback: 'derived', // 'derived' | 'generic' | 'none'
|
|
497
|
+
transform: ({ path, fieldName, defaultLabel }) => {
|
|
498
|
+
return myLabelLookup(fieldName) ?? defaultLabel
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
})
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### CLI
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
npx validex fr de --output ./locales
|
|
508
|
+
npx validex ja --empty --output ./locales
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Full guide with all 141 error codes: [Translation Guide](https://github.com/chiptoma/validex/blob/main/docs/I18N.md)
|
|
512
|
+
|
|
513
|
+
## Custom Rules
|
|
514
|
+
|
|
515
|
+
### `createRule()`
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
import { createRule } from '@validex/core'
|
|
519
|
+
import { z } from 'zod'
|
|
520
|
+
|
|
521
|
+
interface HexColorOptions {
|
|
522
|
+
label?: string
|
|
523
|
+
emptyToUndefined?: boolean
|
|
524
|
+
normalize?: boolean
|
|
525
|
+
customFn?: (value: string) => true | string | Promise<true | string>
|
|
526
|
+
allowAlpha?: boolean
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const HexColor = createRule<HexColorOptions>({
|
|
530
|
+
name: 'hexColor',
|
|
531
|
+
defaults: { allowAlpha: false },
|
|
532
|
+
build: (opts) => {
|
|
533
|
+
const pattern = opts.allowAlpha
|
|
534
|
+
? /^#[\da-f]{6,8}$/i
|
|
535
|
+
: /^#[\da-f]{6}$/i
|
|
536
|
+
return z.string().regex(pattern)
|
|
537
|
+
},
|
|
538
|
+
messages: {
|
|
539
|
+
invalid: '{{label}} is not a valid hex color',
|
|
540
|
+
},
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
const schema = HexColor({ allowAlpha: true })
|
|
544
|
+
schema.parse('#ff00aacc') // OK
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### `customFn`
|
|
548
|
+
|
|
549
|
+
Every rule accepts a `customFn` that runs after built-in checks. Return `true` to pass or a string to fail:
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
import { Email } from '@validex/core'
|
|
553
|
+
|
|
554
|
+
const schema = Email({
|
|
555
|
+
customFn: (value) => value.endsWith('.org') || 'Must be a .org domain',
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
schema.parse('info@example.org') // OK
|
|
559
|
+
schema.parse('info@example.com') // throws — "Must be a .org domain"
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Custom regex
|
|
563
|
+
|
|
564
|
+
Rules that extend `FormatRuleOptions` (like `Text`) accept a `regex` property:
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
import { Text } from '@validex/core'
|
|
568
|
+
|
|
569
|
+
const schema = Text({
|
|
570
|
+
regex: /^[^<>]+$/,
|
|
571
|
+
})
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Full reference: [API Reference](https://github.com/chiptoma/validex/blob/main/docs/API.md)
|
|
575
|
+
|
|
576
|
+
## Framework Adapters
|
|
577
|
+
|
|
578
|
+
- [@validex/nuxt](https://github.com/chiptoma/validex/tree/main/packages/nuxt) — Nuxt module with auto-imports and `useValidation` composable
|
|
579
|
+
- [@validex/fastify](https://github.com/chiptoma/validex/tree/main/packages/fastify) — Fastify plugin with request validation decorators
|
|
580
|
+
|
|
581
|
+
## License
|
|
582
|
+
|
|
583
|
+
[MIT](https://github.com/chiptoma/validex/blob/main/LICENSE)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});require('../chunk-JEKRBIPN.cjs');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
var _chunkOFT3FQPJcjs = require('../chunk-OFT3FQPJ.cjs');
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
var _chunkLSQNDPFQcjs = require('../chunk-LSQNDPFQ.cjs');
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
exports.collapseWhitespace = _chunkOFT3FQPJcjs.collapseWhitespace; exports.containsEmail = _chunkOFT3FQPJcjs.containsEmail; exports.containsHtml = _chunkOFT3FQPJcjs.containsHtml; exports.containsPhoneNumber = _chunkLSQNDPFQcjs.containsPhoneNumber; exports.containsUrl = _chunkOFT3FQPJcjs.containsUrl; exports.emptyToUndefined = _chunkOFT3FQPJcjs.emptyToUndefined; exports.hasDigits = _chunkOFT3FQPJcjs.hasDigits; exports.hasLowercase = _chunkOFT3FQPJcjs.hasLowercase; exports.hasSpecial = _chunkOFT3FQPJcjs.hasSpecial; exports.hasUppercase = _chunkOFT3FQPJcjs.hasUppercase; exports.maxConsecutive = _chunkOFT3FQPJcjs.maxConsecutive; exports.maxWords = _chunkOFT3FQPJcjs.maxWords; exports.minWords = _chunkOFT3FQPJcjs.minWords; exports.noSpaces = _chunkOFT3FQPJcjs.noSpaces; exports.onlyAlpha = _chunkOFT3FQPJcjs.onlyAlpha; exports.onlyAlphaSpaceHyphen = _chunkOFT3FQPJcjs.onlyAlphaSpaceHyphen; exports.onlyAlphanumeric = _chunkOFT3FQPJcjs.onlyAlphanumeric; exports.onlyAlphanumericSpaceHyphen = _chunkOFT3FQPJcjs.onlyAlphanumericSpaceHyphen; exports.onlyNumeric = _chunkOFT3FQPJcjs.onlyNumeric; exports.stripHtml = _chunkOFT3FQPJcjs.stripHtml; exports.toSlug = _chunkOFT3FQPJcjs.toSlug; exports.toTitleCase = _chunkOFT3FQPJcjs.toTitleCase;
|