unischema 1.0.1 → 1.1.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/README.md +634 -231
- package/dist/adapters/backend/index.d.mts +2 -1
- package/dist/adapters/backend/index.d.ts +2 -1
- package/dist/adapters/backend/index.js +17 -441
- package/dist/adapters/backend/index.mjs +9 -433
- package/dist/adapters/frontend/index.d.mts +2 -1
- package/dist/adapters/frontend/index.d.ts +2 -1
- package/dist/adapters/frontend/index.js +10 -421
- package/dist/adapters/frontend/index.mjs +8 -419
- package/dist/chunk-2JYFKT3R.js +103 -0
- package/dist/chunk-3FANCMEF.js +206 -0
- package/dist/chunk-3TS35CVJ.mjs +478 -0
- package/dist/chunk-ASKTY6EG.js +131 -0
- package/dist/chunk-BJLVOIAP.js +491 -0
- package/dist/chunk-BNIB23NQ.js +90 -0
- package/dist/chunk-BVRXGZLS.js +17 -0
- package/dist/chunk-CQYXR2LZ.js +353 -0
- package/dist/chunk-ELL7U7IC.mjs +237 -0
- package/dist/chunk-FKDWSZIV.mjs +39 -0
- package/dist/chunk-FRBZHN4K.mjs +335 -0
- package/dist/chunk-FZ7K2PC7.js +248 -0
- package/dist/chunk-KHHJD6QK.mjs +85 -0
- package/dist/chunk-NUW55QTO.js +48 -0
- package/dist/chunk-TTK77YBI.mjs +15 -0
- package/dist/chunk-VWP24NYS.mjs +194 -0
- package/dist/chunk-XC4DKEXP.mjs +97 -0
- package/dist/chunk-XGTUU27F.mjs +124 -0
- package/dist/index-BQR7OrY7.d.mts +80 -0
- package/dist/index-BQR7OrY7.d.ts +80 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +527 -494
- package/dist/index.mjs +476 -482
- package/dist/{schema-D9DGC9E_.d.mts → schema-CpAjXgEF.d.ts} +182 -79
- package/dist/{schema-D9DGC9E_.d.ts → schema-DYU1zGVm.d.mts} +182 -79
- package/dist/validators/array.d.mts +15 -0
- package/dist/validators/array.d.ts +15 -0
- package/dist/validators/array.js +31 -0
- package/dist/validators/array.mjs +2 -0
- package/dist/validators/common.d.mts +13 -0
- package/dist/validators/common.d.ts +13 -0
- package/dist/validators/common.js +27 -0
- package/dist/validators/common.mjs +2 -0
- package/dist/validators/date.d.mts +23 -0
- package/dist/validators/date.d.ts +23 -0
- package/dist/validators/date.js +47 -0
- package/dist/validators/date.mjs +2 -0
- package/dist/validators/index.d.mts +46 -0
- package/dist/validators/index.d.ts +46 -0
- package/dist/validators/index.js +256 -0
- package/dist/validators/index.mjs +7 -0
- package/dist/validators/number.d.mts +25 -0
- package/dist/validators/number.d.ts +25 -0
- package/dist/validators/number.js +51 -0
- package/dist/validators/number.mjs +2 -0
- package/dist/validators/object.d.mts +11 -0
- package/dist/validators/object.d.ts +11 -0
- package/dist/validators/object.js +23 -0
- package/dist/validators/object.mjs +2 -0
- package/dist/validators/string.d.mts +37 -0
- package/dist/validators/string.d.ts +37 -0
- package/dist/validators/string.js +75 -0
- package/dist/validators/string.mjs +2 -0
- package/package.json +36 -1
- package/dist/adapters/backend/index.js.map +0 -1
- package/dist/adapters/backend/index.mjs.map +0 -1
- package/dist/adapters/frontend/index.js.map +0 -1
- package/dist/adapters/frontend/index.mjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,368 +1,771 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Unischema
|
|
2
2
|
|
|
3
|
-
**Schema-Driven
|
|
3
|
+
**The Universal Schema-Driven Validation Library**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/unischema)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Unischema is a TypeScript-first validation library that provides **one schema, everywhere**. Define your validation once, use it on frontend, backend, and get automatic TypeScript types.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
- Frontend has its own validation (vee-validate, Yup, Zod)
|
|
11
|
-
- Backend has its own validation (Joi, AJV, class-validator)
|
|
12
|
-
- They often drift apart, causing bugs
|
|
13
|
-
- No type safety between the two
|
|
14
|
-
- Duplicate code, duplicate bugs
|
|
10
|
+
## 🚀 Why Unischema?
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
**The Problem:**
|
|
13
|
+
- Frontend has validation (Yup, Zod, vee-validate)
|
|
14
|
+
- Backend has validation (Joi, AJV, class-validator)
|
|
15
|
+
- They drift apart → bugs
|
|
16
|
+
- No type safety between them
|
|
17
|
+
- Code duplication everywhere
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
**The Solution:**
|
|
20
|
+
```typescript
|
|
21
|
+
// ✅ Define once
|
|
22
|
+
const UserSchema = schema({
|
|
23
|
+
email: field.string().email().required(),
|
|
24
|
+
age: field.number().min(18)
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ✅ Use on backend (Express)
|
|
28
|
+
app.post('/users', validateBody(UserSchema), handler);
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
// ✅ Use on frontend (any framework)
|
|
31
|
+
const form = createForm(UserSchema, { onSubmit });
|
|
32
|
+
|
|
33
|
+
// ✅ Get TypeScript types automatically
|
|
34
|
+
type User = InferInput<typeof UserSchema>;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
26
38
|
|
|
27
39
|
```bash
|
|
28
|
-
npm install
|
|
40
|
+
npm install unischema
|
|
29
41
|
```
|
|
30
42
|
|
|
31
|
-
## Quick Start
|
|
43
|
+
## ⚡ Quick Start
|
|
32
44
|
|
|
33
|
-
### 1
|
|
45
|
+
### 1️⃣ Define Your Schema
|
|
34
46
|
|
|
35
47
|
```typescript
|
|
36
|
-
|
|
37
|
-
import { schema, field, type InferInput } from 'formschema';
|
|
48
|
+
import { schema, field } from 'unischema';
|
|
38
49
|
|
|
39
50
|
export const UserSchema = schema({
|
|
40
51
|
email: field.string()
|
|
41
|
-
.email('Invalid email
|
|
52
|
+
.email('Invalid email')
|
|
42
53
|
.required('Email is required'),
|
|
43
54
|
|
|
44
55
|
password: field.string()
|
|
45
|
-
.min(8, '
|
|
46
|
-
.required(
|
|
56
|
+
.min(8, 'At least 8 characters')
|
|
57
|
+
.required(),
|
|
47
58
|
|
|
48
59
|
age: field.number()
|
|
49
|
-
.min(
|
|
50
|
-
.
|
|
60
|
+
.min(18, 'Must be 18+')
|
|
61
|
+
.max(120),
|
|
51
62
|
});
|
|
52
|
-
|
|
53
|
-
// TypeScript type is automatically inferred
|
|
54
|
-
export type UserInput = InferInput<typeof UserSchema>;
|
|
55
|
-
// { email: string; password: string; age: number }
|
|
56
63
|
```
|
|
57
64
|
|
|
58
|
-
### 2
|
|
65
|
+
### 2️⃣ Use on Backend
|
|
59
66
|
|
|
60
67
|
```typescript
|
|
61
|
-
// server.ts
|
|
62
68
|
import express from 'express';
|
|
63
|
-
import { validateBody
|
|
64
|
-
import { UserSchema
|
|
69
|
+
import { validateBody } from 'unischema/backend';
|
|
70
|
+
import { UserSchema } from './schemas';
|
|
65
71
|
|
|
66
72
|
const app = express();
|
|
67
|
-
app.use(express.json());
|
|
68
|
-
|
|
69
|
-
app.post('/api/users',
|
|
70
|
-
validateBody(UserSchema),
|
|
71
|
-
(req: ValidatedRequest<UserInput>, res) => {
|
|
72
|
-
// req.validatedData is typed and validated!
|
|
73
|
-
const { email, password, age } = req.validatedData;
|
|
74
|
-
|
|
75
|
-
// Check for warnings
|
|
76
|
-
if (req.validationResult.softErrors.length > 0) {
|
|
77
|
-
console.log('Warnings:', req.validationResult.softErrors);
|
|
78
|
-
}
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
);
|
|
74
|
+
app.post('/register', validateBody(UserSchema), (req, res) => {
|
|
75
|
+
const { email, password, age } = req.validatedData; // ✅ Typed & validated
|
|
76
|
+
res.json({ success: true });
|
|
77
|
+
});
|
|
83
78
|
```
|
|
84
79
|
|
|
85
|
-
### 3
|
|
80
|
+
### 3️⃣ Use on Frontend
|
|
86
81
|
|
|
87
82
|
```typescript
|
|
88
|
-
|
|
89
|
-
import {
|
|
90
|
-
import { UserSchema } from './schemas/user';
|
|
83
|
+
import { createForm } from 'unischema/frontend';
|
|
84
|
+
import { UserSchema } from './schemas';
|
|
91
85
|
|
|
92
86
|
const form = createForm(UserSchema, {
|
|
93
87
|
onSubmit: async (values) => {
|
|
94
|
-
|
|
88
|
+
await fetch('/register', {
|
|
95
89
|
method: 'POST',
|
|
96
|
-
body: JSON.stringify(values)
|
|
90
|
+
body: JSON.stringify(values)
|
|
97
91
|
});
|
|
98
|
-
|
|
99
|
-
},
|
|
92
|
+
}
|
|
100
93
|
});
|
|
101
94
|
|
|
102
|
-
// Get field props for your UI
|
|
95
|
+
// Get field props for your UI framework
|
|
103
96
|
const emailProps = form.getFieldProps('email');
|
|
104
|
-
// { name, value, onChange, onBlur, error, hasError, ... }
|
|
105
97
|
```
|
|
106
98
|
|
|
107
|
-
##
|
|
99
|
+
## 🎯 Features
|
|
100
|
+
|
|
101
|
+
- ✅ **50+ Built-in Validators** - Email, URL, IPv4/IPv6, phone, coordinates, and more
|
|
102
|
+
- ✅ **Isomorphic** - Same code runs in browser and Node.js
|
|
103
|
+
- ✅ **TypeScript First** - Automatic type inference
|
|
104
|
+
- ✅ **Hard & Soft Validation** - Errors vs warnings for enterprise apps
|
|
105
|
+
- ✅ **Tree-Shakeable** - Only bundle what you use (~2KB min+gzip)
|
|
106
|
+
- ✅ **Framework Agnostic** - Works with React, Vue, Svelte, Angular, etc.
|
|
107
|
+
- ✅ **Zero Dependencies** - Lightweight and fast
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
## 📚 All Validators (v1.1.0)
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
### String Validators (17)
|
|
112
112
|
|
|
113
113
|
```typescript
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
field.string()
|
|
115
|
+
// Basic
|
|
116
|
+
.required() // Required field
|
|
117
|
+
.min(5) // Min length
|
|
118
|
+
.max(100) // Max length
|
|
119
|
+
.length(10) // Exact length
|
|
120
|
+
|
|
121
|
+
// Format validation
|
|
122
|
+
.email() // Valid email
|
|
123
|
+
.url() // Valid URL
|
|
124
|
+
.ipAddress() // IPv4 (validates 0-255)
|
|
125
|
+
.ipv6() // IPv6 address
|
|
126
|
+
|
|
127
|
+
// Character validation
|
|
128
|
+
.alpha() // Only letters (a-zA-Z)
|
|
129
|
+
.alphanumeric() // Letters + numbers
|
|
130
|
+
.numeric() // Only digits
|
|
131
|
+
.lowercase() // Must be lowercase
|
|
132
|
+
.uppercase() // Must be UPPERCASE
|
|
133
|
+
|
|
134
|
+
// Pattern validation
|
|
135
|
+
.slug() // URL-friendly slug
|
|
136
|
+
.hex() // Hexadecimal
|
|
137
|
+
.base64() // Base64 encoded
|
|
138
|
+
.json() // Valid JSON string
|
|
139
|
+
.pattern(/regex/) // Custom regex
|
|
140
|
+
|
|
141
|
+
// Content validation
|
|
142
|
+
.contains('substring') // Must contain text
|
|
143
|
+
.startsWith('prefix') // Must start with
|
|
144
|
+
.endsWith('suffix') // Must end with
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Examples:**
|
|
148
|
+
```typescript
|
|
149
|
+
// Email with custom message
|
|
150
|
+
email: field.string().email('Please enter a valid email')
|
|
151
|
+
|
|
152
|
+
// Alphanumeric username
|
|
153
|
+
username: field.string()
|
|
154
|
+
.alphanumeric('Only letters and numbers')
|
|
155
|
+
.min(3)
|
|
156
|
+
.max(20)
|
|
157
|
+
|
|
158
|
+
// URL slug
|
|
159
|
+
slug: field.string()
|
|
160
|
+
.slug('Must be URL-friendly')
|
|
161
|
+
.lowercase()
|
|
162
|
+
|
|
163
|
+
// Hex color
|
|
164
|
+
color: field.string()
|
|
165
|
+
.hex('Invalid color code')
|
|
166
|
+
.length(6)
|
|
167
|
+
```
|
|
119
168
|
|
|
120
|
-
|
|
169
|
+
### Number Validators (11)
|
|
121
170
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
171
|
+
```typescript
|
|
172
|
+
field.number()
|
|
173
|
+
// Range validation
|
|
174
|
+
.min(0) // Minimum value
|
|
175
|
+
.max(100) // Maximum value
|
|
176
|
+
.between(10, 20) // Between range
|
|
177
|
+
|
|
178
|
+
// Type validation
|
|
179
|
+
.integer() // Must be integer
|
|
180
|
+
.positive() // Must be > 0
|
|
181
|
+
.negative() // Must be < 0
|
|
182
|
+
.even() // Even number
|
|
183
|
+
.odd() // Odd number
|
|
184
|
+
.safe() // Safe integer
|
|
185
|
+
.finite() // Not Infinity/NaN
|
|
186
|
+
|
|
187
|
+
// Special formats
|
|
188
|
+
.port() // Port (0-65535)
|
|
189
|
+
.latitude() // Latitude (-90 to 90)
|
|
190
|
+
.longitude() // Longitude (-180 to 180)
|
|
191
|
+
.percentage() // Percentage (0-100)
|
|
192
|
+
|
|
193
|
+
// Mathematical
|
|
194
|
+
.divisibleBy(5) // Divisible by N
|
|
195
|
+
.multipleOf(3) // Multiple of N
|
|
125
196
|
```
|
|
126
197
|
|
|
127
|
-
**
|
|
198
|
+
**Examples:**
|
|
199
|
+
```typescript
|
|
200
|
+
// Port number
|
|
201
|
+
port: field.number()
|
|
202
|
+
.port('Invalid port number')
|
|
203
|
+
|
|
204
|
+
// GPS coordinates
|
|
205
|
+
location: schema({
|
|
206
|
+
latitude: field.number().latitude(),
|
|
207
|
+
longitude: field.number().longitude()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// Age with soft warning
|
|
211
|
+
age: field.number()
|
|
212
|
+
.min(13, 'Must be 13+') // Hard error
|
|
213
|
+
.minSoft(18, 'Parental consent') // Soft warning
|
|
214
|
+
|
|
215
|
+
// Even page count
|
|
216
|
+
pages: field.number()
|
|
217
|
+
.integer()
|
|
218
|
+
.even('Must be even number')
|
|
219
|
+
```
|
|
128
220
|
|
|
129
|
-
###
|
|
221
|
+
### Date Validators (10)
|
|
130
222
|
|
|
131
|
-
|
|
223
|
+
```typescript
|
|
224
|
+
field.date()
|
|
225
|
+
// Basic
|
|
226
|
+
.after(date) // After date
|
|
227
|
+
.before(date) // Before date
|
|
228
|
+
.past() // Must be in past
|
|
229
|
+
.future() // Must be in future
|
|
230
|
+
|
|
231
|
+
// Relative validation
|
|
232
|
+
.today() // Must be today
|
|
233
|
+
.yesterday() // Must be yesterday
|
|
234
|
+
.tomorrow() // Must be tomorrow
|
|
235
|
+
.thisWeek() // This week
|
|
236
|
+
.thisMonth() // This month
|
|
237
|
+
.thisYear() // This year
|
|
238
|
+
|
|
239
|
+
// Day validation
|
|
240
|
+
.weekday() // Monday-Friday
|
|
241
|
+
.weekend() // Saturday-Sunday
|
|
242
|
+
|
|
243
|
+
// Age validation
|
|
244
|
+
.age(min, max) // Age range from birthdate
|
|
245
|
+
.between(start, end) // Between two dates
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Examples:**
|
|
249
|
+
```typescript
|
|
250
|
+
// Birth date (18-65 years old)
|
|
251
|
+
birthDate: field.date()
|
|
252
|
+
.age(18, 65, 'Must be 18-65 years old')
|
|
253
|
+
.past('Cannot be in future')
|
|
254
|
+
|
|
255
|
+
// Event must be in future
|
|
256
|
+
eventDate: field.date()
|
|
257
|
+
.future('Event must be scheduled ahead')
|
|
258
|
+
.weekday('Events only on weekdays')
|
|
259
|
+
|
|
260
|
+
// Today's attendance
|
|
261
|
+
checkIn: field.date()
|
|
262
|
+
.today('Must check in today')
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Array Validators (6)
|
|
132
266
|
|
|
133
267
|
```typescript
|
|
268
|
+
field.array()
|
|
269
|
+
// Size validation
|
|
270
|
+
.min(2) // Min items
|
|
271
|
+
.max(10) // Max items
|
|
272
|
+
.unique() // All items unique
|
|
273
|
+
|
|
274
|
+
// Content validation
|
|
275
|
+
.includes(item) // Must include item
|
|
276
|
+
.excludes(item) // Must not include item
|
|
277
|
+
.notEmpty() // At least 1 item
|
|
278
|
+
.empty() // Must be empty
|
|
279
|
+
|
|
280
|
+
// Order validation
|
|
281
|
+
.sorted('asc') // Sorted ascending
|
|
282
|
+
.sorted('desc') // Sorted descending
|
|
283
|
+
|
|
284
|
+
// Quality validation
|
|
285
|
+
.compact() // No falsy values
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Examples:**
|
|
289
|
+
```typescript
|
|
290
|
+
// Tags (1-5 unique items)
|
|
291
|
+
tags: field.array(field.string())
|
|
292
|
+
.min(1, 'At least one tag')
|
|
293
|
+
.max(5, 'Max 5 tags')
|
|
294
|
+
.unique('Tags must be unique')
|
|
295
|
+
|
|
296
|
+
// Must include required item
|
|
297
|
+
permissions: field.array()
|
|
298
|
+
.includes('read', 'Read permission required')
|
|
299
|
+
|
|
300
|
+
// Sorted numbers
|
|
301
|
+
scores: field.array(field.number())
|
|
302
|
+
.sorted('desc', 'Must be sorted highest first')
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Boolean Validators
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
field.boolean()
|
|
309
|
+
.isTrue() // Must be true
|
|
310
|
+
.isFalse() // Must be false
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Examples:**
|
|
314
|
+
```typescript
|
|
315
|
+
// Terms acceptance
|
|
316
|
+
acceptTerms: field.boolean()
|
|
317
|
+
.isTrue('You must accept terms')
|
|
318
|
+
.required()
|
|
319
|
+
|
|
320
|
+
// Optional newsletter
|
|
321
|
+
newsletter: field.boolean()
|
|
322
|
+
.optional()
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Object Validators (Nested)
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
field.object(schema) // Nested schema validation
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Examples:**
|
|
332
|
+
```typescript
|
|
333
|
+
// Nested address
|
|
134
334
|
const AddressSchema = schema({
|
|
135
335
|
street: field.string().required(),
|
|
136
336
|
city: field.string().required(),
|
|
137
|
-
zipCode: field.string().pattern(/^\d{5}$/)
|
|
337
|
+
zipCode: field.string().pattern(/^\d{5}$/)
|
|
138
338
|
});
|
|
139
339
|
|
|
140
340
|
const UserSchema = schema({
|
|
141
341
|
name: field.string().required(),
|
|
142
|
-
address: field.object(AddressSchema).required()
|
|
342
|
+
address: field.object(AddressSchema).required()
|
|
143
343
|
});
|
|
344
|
+
```
|
|
144
345
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
346
|
+
### Cross-Field Validators (5)
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
field.string()
|
|
350
|
+
.matches('password') // Must match field
|
|
351
|
+
.notMatches('oldPassword') // Must NOT match field
|
|
352
|
+
|
|
353
|
+
field.number()
|
|
354
|
+
.greaterThan('minValue') // > another field
|
|
355
|
+
.lessThan('maxValue') // < another field
|
|
356
|
+
|
|
357
|
+
field.string()
|
|
358
|
+
.dependsOn('country') // Required if field exists
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Examples:**
|
|
362
|
+
```typescript
|
|
363
|
+
// Password confirmation
|
|
364
|
+
const schema = schema({
|
|
365
|
+
password: field.string().min(8),
|
|
366
|
+
confirmPassword: field.string()
|
|
367
|
+
.matches('password', 'Passwords must match')
|
|
148
368
|
});
|
|
149
369
|
|
|
150
|
-
//
|
|
151
|
-
const
|
|
152
|
-
|
|
370
|
+
// New password must differ
|
|
371
|
+
const changePasswordSchema = schema({
|
|
372
|
+
currentPassword: field.string(),
|
|
373
|
+
newPassword: field.string()
|
|
374
|
+
.notMatches('currentPassword', 'Must be different')
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Range validation
|
|
378
|
+
const rangeSchema = schema({
|
|
379
|
+
minPrice: field.number(),
|
|
380
|
+
maxPrice: field.number()
|
|
381
|
+
.greaterThan('minPrice', 'Max must be > min')
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Conditional requirement
|
|
385
|
+
const locationSchema = schema({
|
|
386
|
+
country: field.string(),
|
|
387
|
+
state: field.string()
|
|
388
|
+
.dependsOn('country', 'State requires country')
|
|
389
|
+
});
|
|
153
390
|
```
|
|
154
391
|
|
|
155
|
-
|
|
392
|
+
## 💡 Hard vs Soft Validation
|
|
156
393
|
|
|
157
|
-
|
|
394
|
+
Unischema supports two-tier validation for enterprise applications:
|
|
158
395
|
|
|
159
396
|
```typescript
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
397
|
+
const TransactionSchema = schema({
|
|
398
|
+
amount: field.number()
|
|
399
|
+
.min(0.01, 'Amount must be positive') // ❌ Hard: blocks submission
|
|
400
|
+
.maxSoft(10000, 'Review required for $10k+') // ⚠️ Soft: warning only
|
|
164
401
|
});
|
|
165
402
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// }
|
|
403
|
+
const result = validateSchema(TransactionSchema.definition, { amount: 15000 });
|
|
404
|
+
|
|
405
|
+
console.log(result.valid); // true (no hard errors)
|
|
406
|
+
console.log(result.hardErrors); // []
|
|
407
|
+
console.log(result.softErrors); // [{ field: 'amount', message: 'Review required...', severity: 'soft' }]
|
|
172
408
|
```
|
|
173
409
|
|
|
174
|
-
|
|
410
|
+
**Use cases:**
|
|
411
|
+
- Warnings that don't block submission
|
|
412
|
+
- Age warnings (13+ required, 18+ recommended)
|
|
413
|
+
- Security score suggestions
|
|
414
|
+
- Large transaction reviews
|
|
175
415
|
|
|
176
|
-
|
|
416
|
+
## 🔧 Advanced Usage
|
|
177
417
|
|
|
178
|
-
|
|
418
|
+
### Schema Composition
|
|
179
419
|
|
|
180
420
|
```typescript
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
.enum(
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
421
|
+
// Extend schemas
|
|
422
|
+
const BaseUser = schema({
|
|
423
|
+
email: field.string().email(),
|
|
424
|
+
name: field.string()
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const AdminUser = extend(BaseUser, {
|
|
428
|
+
role: field.string().enum(['admin', 'superadmin']),
|
|
429
|
+
permissions: field.array(field.string())
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Pick specific fields
|
|
433
|
+
const LoginSchema = pick(BaseUser, ['email']);
|
|
434
|
+
|
|
435
|
+
// Omit fields
|
|
436
|
+
const PublicUser = omit(BaseUser, ['password']);
|
|
437
|
+
|
|
438
|
+
// Merge schemas
|
|
439
|
+
const FullSchema = merge(ProfileSchema, SettingsSchema);
|
|
195
440
|
```
|
|
196
441
|
|
|
197
|
-
|
|
442
|
+
### TypeScript Integration
|
|
198
443
|
|
|
199
444
|
```typescript
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
445
|
+
import { type InferInput, type InferOutput } from 'unischema';
|
|
446
|
+
|
|
447
|
+
const UserSchema = schema({
|
|
448
|
+
email: field.string().email().required(),
|
|
449
|
+
age: field.number().min(0)
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Input type (what you pass in)
|
|
453
|
+
type UserInput = InferInput<typeof UserSchema>;
|
|
454
|
+
// { email: string; age: number }
|
|
455
|
+
|
|
456
|
+
// Output type (after validation)
|
|
457
|
+
type UserOutput = InferOutput<typeof UserSchema>;
|
|
210
458
|
```
|
|
211
459
|
|
|
212
|
-
|
|
460
|
+
### Custom Validation
|
|
213
461
|
|
|
214
462
|
```typescript
|
|
215
|
-
|
|
216
|
-
.
|
|
217
|
-
|
|
218
|
-
|
|
463
|
+
const schema = schema({
|
|
464
|
+
password: field.string()
|
|
465
|
+
.custom((value, context) => {
|
|
466
|
+
if (!/[A-Z]/.test(value)) {
|
|
467
|
+
return { valid: false, message: 'Need uppercase letter' };
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
})
|
|
471
|
+
});
|
|
219
472
|
```
|
|
220
473
|
|
|
221
|
-
|
|
474
|
+
### Granular Imports (Tree-Shaking)
|
|
222
475
|
|
|
223
476
|
```typescript
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
477
|
+
// Import only what you need
|
|
478
|
+
import { emailValidator } from 'unischema/validators/string';
|
|
479
|
+
import { portValidator } from 'unischema/validators/number';
|
|
480
|
+
import { todayValidator } from 'unischema/validators/date';
|
|
481
|
+
|
|
482
|
+
// Or import by category
|
|
483
|
+
import * as stringValidators from 'unischema/validators/string';
|
|
484
|
+
import * as numberValidators from 'unischema/validators/number';
|
|
230
485
|
```
|
|
231
486
|
|
|
232
|
-
|
|
487
|
+
## 🌐 Framework Examples
|
|
233
488
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
489
|
+
### React
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
import { createForm } from 'unischema/frontend';
|
|
493
|
+
import { UserSchema } from './schemas';
|
|
494
|
+
|
|
495
|
+
function RegisterForm() {
|
|
496
|
+
const form = createForm(UserSchema, {
|
|
497
|
+
initialValues: { email: '', password: '' },
|
|
498
|
+
onSubmit: async (values) => {
|
|
499
|
+
await api.register(values);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const emailProps = form.getFieldProps('email');
|
|
504
|
+
|
|
505
|
+
return (
|
|
506
|
+
<form onSubmit={form.handleSubmit}>
|
|
507
|
+
<input {...emailProps} />
|
|
508
|
+
{emailProps.hasError && <span>{emailProps.error}</span>}
|
|
509
|
+
|
|
510
|
+
<button type="submit">Register</button>
|
|
511
|
+
</form>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
241
514
|
```
|
|
242
515
|
|
|
243
|
-
|
|
516
|
+
### Vue
|
|
244
517
|
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
|
|
518
|
+
```vue
|
|
519
|
+
<script setup>
|
|
520
|
+
import { createForm } from 'unischema/frontend';
|
|
521
|
+
import { UserSchema } from './schemas';
|
|
522
|
+
|
|
523
|
+
const form = createForm(UserSchema, {
|
|
524
|
+
onSubmit: async (values) => {
|
|
525
|
+
await api.register(values);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const emailProps = form.getFieldProps('email');
|
|
530
|
+
</script>
|
|
531
|
+
|
|
532
|
+
<template>
|
|
533
|
+
<form @submit.prevent="form.handleSubmit">
|
|
534
|
+
<input v-bind="emailProps" />
|
|
535
|
+
<span v-if="emailProps.hasError">{{ emailProps.error }}</span>
|
|
536
|
+
</form>
|
|
537
|
+
</template>
|
|
248
538
|
```
|
|
249
539
|
|
|
250
|
-
###
|
|
540
|
+
### Express.js
|
|
251
541
|
|
|
252
542
|
```typescript
|
|
253
|
-
import
|
|
543
|
+
import express from 'express';
|
|
544
|
+
import { validateBody, validateQuery, validateParams } from 'unischema/backend';
|
|
254
545
|
|
|
255
|
-
|
|
256
|
-
const result = validate(schema.definition, data);
|
|
257
|
-
// { valid: boolean, hardErrors: [], softErrors: [] }
|
|
546
|
+
const app = express();
|
|
258
547
|
|
|
259
|
-
//
|
|
260
|
-
|
|
548
|
+
// Body validation
|
|
549
|
+
app.post('/users', validateBody(UserSchema), (req, res) => {
|
|
550
|
+
const user = req.validatedData; // ✅ Typed and validated
|
|
551
|
+
res.json(user);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Query validation
|
|
555
|
+
app.get('/search', validateQuery(SearchSchema), (req, res) => {
|
|
556
|
+
const { query } = req.validatedData;
|
|
557
|
+
res.json(results);
|
|
558
|
+
});
|
|
261
559
|
|
|
262
|
-
//
|
|
263
|
-
|
|
560
|
+
// Params validation
|
|
561
|
+
app.get('/users/:id', validateParams(IdSchema), (req, res) => {
|
|
562
|
+
const { id } = req.validatedData;
|
|
563
|
+
res.json(user);
|
|
564
|
+
});
|
|
264
565
|
```
|
|
265
566
|
|
|
266
|
-
|
|
567
|
+
## 📊 Real-World Examples
|
|
568
|
+
|
|
569
|
+
### User Registration
|
|
267
570
|
|
|
268
571
|
```typescript
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
app.get('/users', validateQuery(QuerySchema), handler);
|
|
280
|
-
app.get('/users/:id', validateParams(ParamsSchema), handler);
|
|
572
|
+
const RegisterSchema = schema({
|
|
573
|
+
email: field.string()
|
|
574
|
+
.email('Invalid email address')
|
|
575
|
+
.required('Email is required'),
|
|
576
|
+
|
|
577
|
+
username: field.string()
|
|
578
|
+
.alphanumeric('Only letters and numbers')
|
|
579
|
+
.min(3, 'At least 3 characters')
|
|
580
|
+
.max(20, 'Max 20 characters')
|
|
581
|
+
.required(),
|
|
281
582
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
583
|
+
password: field.string()
|
|
584
|
+
.min(8, 'At least 8 characters')
|
|
585
|
+
.pattern(/[A-Z]/, 'Need uppercase letter')
|
|
586
|
+
.pattern(/[0-9]/, 'Need a number')
|
|
587
|
+
.required(),
|
|
588
|
+
|
|
589
|
+
confirmPassword: field.string()
|
|
590
|
+
.matches('password', 'Passwords must match')
|
|
591
|
+
.required(),
|
|
286
592
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
593
|
+
age: field.number()
|
|
594
|
+
.min(13, 'Must be 13+')
|
|
595
|
+
.minSoft(18, 'Parental consent required under 18')
|
|
596
|
+
.max(120, 'Invalid age')
|
|
597
|
+
.integer()
|
|
598
|
+
.required(),
|
|
599
|
+
|
|
600
|
+
acceptTerms: field.boolean()
|
|
601
|
+
.isTrue('You must accept the terms')
|
|
602
|
+
.required()
|
|
290
603
|
});
|
|
291
604
|
```
|
|
292
605
|
|
|
293
|
-
###
|
|
606
|
+
### E-Commerce Order
|
|
294
607
|
|
|
295
608
|
```typescript
|
|
296
|
-
|
|
609
|
+
const OrderSchema = schema({
|
|
610
|
+
customerId: field.string()
|
|
611
|
+
.alphanumeric()
|
|
612
|
+
.length(10)
|
|
613
|
+
.required(),
|
|
614
|
+
|
|
615
|
+
items: field.array(field.object(schema({
|
|
616
|
+
productId: field.string().required(),
|
|
617
|
+
quantity: field.number().min(1).integer(),
|
|
618
|
+
price: field.number().positive()
|
|
619
|
+
})))
|
|
620
|
+
.min(1, 'At least one item required')
|
|
621
|
+
.max(50, 'Maximum 50 items per order'),
|
|
622
|
+
|
|
623
|
+
total: field.number()
|
|
624
|
+
.positive()
|
|
625
|
+
.required(),
|
|
626
|
+
|
|
627
|
+
shippingAddress: field.object(schema({
|
|
628
|
+
street: field.string().required(),
|
|
629
|
+
city: field.string().required(),
|
|
630
|
+
state: field.string().uppercase().length(2),
|
|
631
|
+
zipCode: field.string().pattern(/^\d{5}$/)
|
|
632
|
+
})).required(),
|
|
633
|
+
|
|
634
|
+
shippingDate: field.date()
|
|
635
|
+
.future('Must be a future date')
|
|
636
|
+
.weekday('No weekend shipping')
|
|
637
|
+
});
|
|
638
|
+
```
|
|
297
639
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
640
|
+
### API Configuration
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
const ServerConfigSchema = schema({
|
|
644
|
+
host: field.string()
|
|
645
|
+
.ipAddress('Invalid IP address')
|
|
646
|
+
.required(),
|
|
647
|
+
|
|
648
|
+
port: field.number()
|
|
649
|
+
.port('Invalid port number')
|
|
650
|
+
.required(),
|
|
651
|
+
|
|
652
|
+
ssl: field.boolean()
|
|
653
|
+
.required(),
|
|
654
|
+
|
|
655
|
+
maxConnections: field.number()
|
|
656
|
+
.integer()
|
|
657
|
+
.positive()
|
|
658
|
+
.between(1, 10000),
|
|
659
|
+
|
|
660
|
+
timeout: field.number()
|
|
661
|
+
.integer()
|
|
662
|
+
.positive()
|
|
663
|
+
.multipleOf(1000, 'Must be in seconds (1000ms)')
|
|
305
664
|
});
|
|
665
|
+
```
|
|
306
666
|
|
|
307
|
-
|
|
308
|
-
form.setFieldValue('email', 'test@example.com');
|
|
309
|
-
form.touchField('email');
|
|
310
|
-
form.validate();
|
|
311
|
-
form.validateField('email');
|
|
312
|
-
form.reset();
|
|
313
|
-
form.handleSubmit();
|
|
667
|
+
## 🚀 Migration Guide
|
|
314
668
|
|
|
315
|
-
|
|
316
|
-
const props = form.getFieldProps('email');
|
|
317
|
-
// { name, value, onChange, onBlur, error, hasError, warning, hasWarning, ... }
|
|
669
|
+
### From Yup
|
|
318
670
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
671
|
+
```typescript
|
|
672
|
+
// Yup
|
|
673
|
+
const schema = yup.object({
|
|
674
|
+
email: yup.string().email().required(),
|
|
675
|
+
age: yup.number().min(18)
|
|
676
|
+
});
|
|
323
677
|
|
|
324
|
-
|
|
678
|
+
// Unischema
|
|
679
|
+
const schema = schema({
|
|
680
|
+
email: field.string().email().required(),
|
|
681
|
+
age: field.number().min(18)
|
|
682
|
+
});
|
|
683
|
+
```
|
|
325
684
|
|
|
326
|
-
|
|
685
|
+
### From Zod
|
|
327
686
|
|
|
328
687
|
```typescript
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
validation: {
|
|
335
|
-
hard_validations: ValidationError[];
|
|
336
|
-
soft_validations: ValidationError[];
|
|
337
|
-
};
|
|
338
|
-
}
|
|
688
|
+
// Zod
|
|
689
|
+
const schema = z.object({
|
|
690
|
+
email: z.string().email(),
|
|
691
|
+
age: z.number().min(18)
|
|
692
|
+
});
|
|
339
693
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
694
|
+
// Unischema
|
|
695
|
+
const schema = schema({
|
|
696
|
+
email: field.string().email(),
|
|
697
|
+
age: field.number().min(18)
|
|
698
|
+
});
|
|
346
699
|
```
|
|
347
700
|
|
|
348
|
-
##
|
|
701
|
+
## 🎨 Bundle Size
|
|
702
|
+
|
|
703
|
+
Unischema is optimized for tree-shaking:
|
|
704
|
+
|
|
705
|
+
- **Full library**: ~15KB min+gzip
|
|
706
|
+
- **Core only**: ~5KB min+gzip
|
|
707
|
+
- **Single validator**: ~2KB min+gzip
|
|
349
708
|
|
|
350
|
-
|
|
709
|
+
Import only what you use for minimal bundle size.
|
|
710
|
+
|
|
711
|
+
## 📖 API Reference
|
|
712
|
+
|
|
713
|
+
### Core Functions
|
|
351
714
|
|
|
352
715
|
```typescript
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
716
|
+
import {
|
|
717
|
+
schema, // Create schema
|
|
718
|
+
field, // Field builders
|
|
719
|
+
validate, // Validate data
|
|
720
|
+
validateSchema, // Validate with schema
|
|
721
|
+
isValid, // Boolean validation
|
|
722
|
+
assertValid, // Throws if invalid
|
|
723
|
+
extend, // Extend schema
|
|
724
|
+
pick, // Pick fields
|
|
725
|
+
omit, // Omit fields
|
|
726
|
+
merge, // Merge schemas
|
|
727
|
+
partial, // Make all optional
|
|
728
|
+
type InferInput, // Input type
|
|
729
|
+
type InferOutput // Output type
|
|
730
|
+
} from 'unischema';
|
|
731
|
+
```
|
|
356
732
|
|
|
357
|
-
|
|
358
|
-
import { type InferInput } from 'formschema';
|
|
359
|
-
type User = InferInput<typeof UserSchema>;
|
|
733
|
+
### Backend
|
|
360
734
|
|
|
361
|
-
|
|
362
|
-
import {
|
|
363
|
-
|
|
735
|
+
```typescript
|
|
736
|
+
import {
|
|
737
|
+
validateBody, // Validate request body
|
|
738
|
+
validateQuery, // Validate query params
|
|
739
|
+
validateParams, // Validate route params
|
|
740
|
+
withValidation, // Wrapper with validation
|
|
741
|
+
createHandler // Serverless handler
|
|
742
|
+
} from 'unischema/backend';
|
|
364
743
|
```
|
|
365
744
|
|
|
366
|
-
|
|
745
|
+
### Frontend
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
import {
|
|
749
|
+
createForm, // Create form helper
|
|
750
|
+
parseApiErrors, // Parse server errors
|
|
751
|
+
focusFirstError // Focus first error field
|
|
752
|
+
} from 'unischema/frontend';
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
## 🤝 Contributing
|
|
756
|
+
|
|
757
|
+
Contributions are welcome! Please check out the [GitHub repository](https://github.com/Gaurav-pasi/unischema).
|
|
758
|
+
|
|
759
|
+
## 📄 License
|
|
760
|
+
|
|
761
|
+
MIT © [Gaurav Pasi](https://github.com/Gaurav-pasi)
|
|
762
|
+
|
|
763
|
+
## 🔗 Links
|
|
764
|
+
|
|
765
|
+
- [npm package](https://www.npmjs.com/package/unischema)
|
|
766
|
+
- [GitHub repository](https://github.com/Gaurav-pasi/unischema)
|
|
767
|
+
- [Issue tracker](https://github.com/Gaurav-pasi/unischema/issues)
|
|
768
|
+
|
|
769
|
+
---
|
|
367
770
|
|
|
368
|
-
|
|
771
|
+
**Made with ❤️ for developers who value type safety and code reusability**
|