unischema 1.0.1 → 1.2.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 +780 -228
- 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-5A4ITJVD.mjs +124 -0
- package/dist/chunk-66RFUBVU.js +131 -0
- package/dist/chunk-75YSYC4K.mjs +85 -0
- package/dist/chunk-76BBWQDH.js +90 -0
- package/dist/chunk-7XES4A3M.mjs +237 -0
- package/dist/chunk-BVRXGZLS.js +17 -0
- package/dist/chunk-COMVAVFU.mjs +335 -0
- package/dist/chunk-DT2TQZU7.js +796 -0
- package/dist/chunk-FPCCH55A.js +103 -0
- package/dist/chunk-IUXRLMET.js +206 -0
- package/dist/chunk-JEW6U6CB.js +353 -0
- package/dist/chunk-KZCV5IW4.mjs +97 -0
- package/dist/chunk-KZZ7NVU3.mjs +41 -0
- package/dist/chunk-MFEBMQAU.mjs +779 -0
- package/dist/chunk-OIYG5D2I.js +50 -0
- package/dist/chunk-RW6HDA5H.mjs +194 -0
- package/dist/chunk-TTK77YBI.mjs +15 -0
- package/dist/chunk-TXT36BCE.js +248 -0
- package/dist/index-C17xs-fU.d.mts +140 -0
- package/dist/index-C17xs-fU.d.ts +140 -0
- package/dist/index.d.mts +26 -7
- package/dist/index.d.ts +26 -7
- package/dist/index.js +769 -499
- package/dist/index.mjs +695 -487
- package/dist/{schema-D9DGC9E_.d.ts → schema-DYE8Wz8X.d.mts} +264 -79
- package/dist/{schema-D9DGC9E_.d.mts → schema-Dtp-joeT.d.ts} +264 -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 +82 -5
- 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,920 @@
|
|
|
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);
|
|
29
|
+
|
|
30
|
+
// ✅ Use on frontend (any framework)
|
|
31
|
+
const form = createForm(UserSchema, { onSubmit });
|
|
24
32
|
|
|
25
|
-
|
|
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
|
+
- ✅ **53+ Built-in Validators** - Email, URL, IPv4/IPv6, phone, coordinates, and more
|
|
102
|
+
- ✅ **Async Validation** - Database checks, API validation with debouncing
|
|
103
|
+
- ✅ **Data Transformation** - Transform & coerce values before validation
|
|
104
|
+
- ✅ **Advanced Schema Composition** - deepPartial, passthrough, strict, catchall
|
|
105
|
+
- ✅ **Isomorphic** - Same code runs in browser and Node.js
|
|
106
|
+
- ✅ **TypeScript First** - Automatic type inference
|
|
107
|
+
- ✅ **Hard & Soft Validation** - Errors vs warnings for enterprise apps
|
|
108
|
+
- ✅ **Nullable/Nullish Support** - Proper null/undefined handling
|
|
109
|
+
- ✅ **Tree-Shakeable** - Only bundle what you use (~2KB min+gzip)
|
|
110
|
+
- ✅ **Framework Agnostic** - Works with React, Vue, Svelte, Angular, etc.
|
|
111
|
+
- ✅ **Zero Dependencies** - Lightweight and fast
|
|
112
|
+
|
|
113
|
+
## 🆕 What's New in v1.2.0 - Production Ready!
|
|
114
|
+
|
|
115
|
+
Phase 1 is complete! Unischema now includes powerful features for production applications:
|
|
108
116
|
|
|
109
|
-
###
|
|
117
|
+
### Async Validation
|
|
110
118
|
|
|
111
|
-
|
|
119
|
+
Validate against external APIs, databases, or async operations with built-in debouncing:
|
|
112
120
|
|
|
113
121
|
```typescript
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
.
|
|
117
|
-
.
|
|
122
|
+
const UserSchema = schema({
|
|
123
|
+
email: field.string()
|
|
124
|
+
.email()
|
|
125
|
+
.refineAsync(async (email) => {
|
|
126
|
+
const exists = await checkEmailExists(email);
|
|
127
|
+
return !exists || { message: 'Email already registered' };
|
|
128
|
+
}, { debounce: 500, timeout: 5000 }),
|
|
129
|
+
|
|
130
|
+
username: field.string()
|
|
131
|
+
.refineAsync(async (name) => {
|
|
132
|
+
const available = await api.checkUsername(name);
|
|
133
|
+
return available;
|
|
134
|
+
}, { debounce: 300, message: 'Username taken' })
|
|
118
135
|
});
|
|
119
136
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
result.valid; // true - no hard errors
|
|
123
|
-
result.hardErrors; // []
|
|
124
|
-
result.softErrors; // [{ field: 'amount', message: 'Large transaction...', severity: 'soft' }]
|
|
137
|
+
// Use async validation
|
|
138
|
+
const result = await validateAsync(UserSchema.definition, data);
|
|
125
139
|
```
|
|
126
140
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
### Schema Composition
|
|
141
|
+
### Data Transformation & Coercion
|
|
130
142
|
|
|
131
|
-
|
|
143
|
+
Transform and coerce values before validation:
|
|
132
144
|
|
|
133
145
|
```typescript
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
// Transform strings
|
|
147
|
+
const LoginSchema = schema({
|
|
148
|
+
email: field.string()
|
|
149
|
+
.transform(s => s.trim())
|
|
150
|
+
.transform(s => s.toLowerCase())
|
|
151
|
+
.email(),
|
|
152
|
+
|
|
153
|
+
name: field.string()
|
|
154
|
+
.transform(s => s.trim())
|
|
155
|
+
.transform(s => s.replace(/\s+/g, ' ')) // Normalize whitespace
|
|
138
156
|
});
|
|
139
157
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
// Type coercion from form inputs
|
|
159
|
+
const FormSchema = schema({
|
|
160
|
+
age: coerce.number().min(18), // "25" → 25
|
|
161
|
+
active: coerce.boolean(), // "true" → true
|
|
162
|
+
startDate: coerce.date(), // "2024-01-01" → Date
|
|
163
|
+
tags: coerce.array(field.string()), // "javascript" → ["javascript"]
|
|
143
164
|
});
|
|
144
165
|
|
|
145
|
-
//
|
|
146
|
-
const
|
|
147
|
-
|
|
166
|
+
// Preprocessing for nullable values
|
|
167
|
+
const ProfileSchema = schema({
|
|
168
|
+
bio: field.string()
|
|
169
|
+
.preprocess(s => s?.trim()) // Handle null/undefined safely
|
|
170
|
+
.nullable()
|
|
148
171
|
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Advanced Schema Composition
|
|
149
175
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
176
|
+
More flexible schema manipulation:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const BaseSchema = schema({
|
|
180
|
+
id: field.string(),
|
|
181
|
+
name: field.string().required(),
|
|
182
|
+
email: field.string().email().required(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Deep partial - make all fields optional recursively
|
|
186
|
+
const PartialSchema = deepPartial(BaseSchema);
|
|
187
|
+
|
|
188
|
+
// Passthrough - allow unknown keys
|
|
189
|
+
const FlexibleSchema = passthrough(BaseSchema);
|
|
190
|
+
|
|
191
|
+
// Strict mode - reject unknown keys
|
|
192
|
+
const StrictSchema = strict(BaseSchema);
|
|
193
|
+
|
|
194
|
+
// Catchall - handle unknown keys with validation
|
|
195
|
+
const CatchAllSchema = catchall(BaseSchema, field.string());
|
|
196
|
+
|
|
197
|
+
// Make specific fields required/optional
|
|
198
|
+
const RequiredFields = required(BaseSchema, ['name', 'email']);
|
|
199
|
+
const OptionalFields = optional(BaseSchema, ['email']);
|
|
153
200
|
```
|
|
154
201
|
|
|
155
|
-
###
|
|
202
|
+
### Nullable & Nullish Handling
|
|
156
203
|
|
|
157
|
-
|
|
204
|
+
Better null/undefined value handling:
|
|
158
205
|
|
|
159
206
|
```typescript
|
|
160
207
|
const UserSchema = schema({
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
208
|
+
// Allow null
|
|
209
|
+
middleName: field.string().nullable(), // string | null
|
|
210
|
+
|
|
211
|
+
// Allow null or undefined
|
|
212
|
+
bio: field.string().nullish(), // string | null | undefined
|
|
213
|
+
|
|
214
|
+
// Required but nullable
|
|
215
|
+
avatar: field.string().nullable().required(),
|
|
164
216
|
});
|
|
217
|
+
```
|
|
165
218
|
|
|
166
|
-
|
|
219
|
+
### Enhanced Error Context
|
|
220
|
+
|
|
221
|
+
Get detailed error information:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const result = validate(schema({ age: field.number().min(18) }), { age: 15 });
|
|
225
|
+
|
|
226
|
+
result.hardErrors[0];
|
|
167
227
|
// {
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
228
|
+
// field: "age",
|
|
229
|
+
// code: "MIN_VALUE",
|
|
230
|
+
// message: "Value must be at least 18",
|
|
231
|
+
// severity: "hard",
|
|
232
|
+
// received: 15, // ✨ The actual value
|
|
233
|
+
// expected: { min: 18 } // ✨ The constraint that failed
|
|
234
|
+
// path: ["age"] // ✨ Path as array
|
|
171
235
|
// }
|
|
172
236
|
```
|
|
173
237
|
|
|
174
|
-
##
|
|
175
|
-
|
|
176
|
-
### Schema Builders
|
|
238
|
+
## 📚 All Validators (v1.2.0)
|
|
177
239
|
|
|
178
|
-
|
|
240
|
+
### String Validators (17)
|
|
179
241
|
|
|
180
242
|
```typescript
|
|
181
243
|
field.string()
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
.
|
|
185
|
-
.
|
|
186
|
-
.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
.
|
|
191
|
-
.
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
244
|
+
// Basic
|
|
245
|
+
.required() // Required field
|
|
246
|
+
.min(5) // Min length
|
|
247
|
+
.max(100) // Max length
|
|
248
|
+
.length(10) // Exact length
|
|
249
|
+
|
|
250
|
+
// Format validation
|
|
251
|
+
.email() // Valid email
|
|
252
|
+
.url() // Valid URL
|
|
253
|
+
.ipAddress() // IPv4 (validates 0-255)
|
|
254
|
+
.ipv6() // IPv6 address
|
|
255
|
+
|
|
256
|
+
// Character validation
|
|
257
|
+
.alpha() // Only letters (a-zA-Z)
|
|
258
|
+
.alphanumeric() // Letters + numbers
|
|
259
|
+
.numeric() // Only digits
|
|
260
|
+
.lowercase() // Must be lowercase
|
|
261
|
+
.uppercase() // Must be UPPERCASE
|
|
262
|
+
|
|
263
|
+
// Pattern validation
|
|
264
|
+
.slug() // URL-friendly slug
|
|
265
|
+
.hex() // Hexadecimal
|
|
266
|
+
.base64() // Base64 encoded
|
|
267
|
+
.json() // Valid JSON string
|
|
268
|
+
.pattern(/regex/) // Custom regex
|
|
269
|
+
|
|
270
|
+
// Content validation
|
|
271
|
+
.contains('substring') // Must contain text
|
|
272
|
+
.startsWith('prefix') // Must start with
|
|
273
|
+
.endsWith('suffix') // Must end with
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Examples:**
|
|
277
|
+
```typescript
|
|
278
|
+
// Email with custom message
|
|
279
|
+
email: field.string().email('Please enter a valid email')
|
|
280
|
+
|
|
281
|
+
// Alphanumeric username
|
|
282
|
+
username: field.string()
|
|
283
|
+
.alphanumeric('Only letters and numbers')
|
|
284
|
+
.min(3)
|
|
285
|
+
.max(20)
|
|
286
|
+
|
|
287
|
+
// URL slug
|
|
288
|
+
slug: field.string()
|
|
289
|
+
.slug('Must be URL-friendly')
|
|
290
|
+
.lowercase()
|
|
291
|
+
|
|
292
|
+
// Hex color
|
|
293
|
+
color: field.string()
|
|
294
|
+
.hex('Invalid color code')
|
|
295
|
+
.length(6)
|
|
195
296
|
```
|
|
196
297
|
|
|
197
|
-
|
|
298
|
+
### Number Validators (11)
|
|
198
299
|
|
|
199
300
|
```typescript
|
|
200
301
|
field.number()
|
|
201
|
-
|
|
202
|
-
.
|
|
203
|
-
.
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
.
|
|
209
|
-
.
|
|
302
|
+
// Range validation
|
|
303
|
+
.min(0) // Minimum value
|
|
304
|
+
.max(100) // Maximum value
|
|
305
|
+
.between(10, 20) // Between range
|
|
306
|
+
|
|
307
|
+
// Type validation
|
|
308
|
+
.integer() // Must be integer
|
|
309
|
+
.positive() // Must be > 0
|
|
310
|
+
.negative() // Must be < 0
|
|
311
|
+
.even() // Even number
|
|
312
|
+
.odd() // Odd number
|
|
313
|
+
.safe() // Safe integer
|
|
314
|
+
.finite() // Not Infinity/NaN
|
|
315
|
+
|
|
316
|
+
// Special formats
|
|
317
|
+
.port() // Port (0-65535)
|
|
318
|
+
.latitude() // Latitude (-90 to 90)
|
|
319
|
+
.longitude() // Longitude (-180 to 180)
|
|
320
|
+
.percentage() // Percentage (0-100)
|
|
321
|
+
|
|
322
|
+
// Mathematical
|
|
323
|
+
.divisibleBy(5) // Divisible by N
|
|
324
|
+
.multipleOf(3) // Multiple of N
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Examples:**
|
|
328
|
+
```typescript
|
|
329
|
+
// Port number
|
|
330
|
+
port: field.number()
|
|
331
|
+
.port('Invalid port number')
|
|
332
|
+
|
|
333
|
+
// GPS coordinates
|
|
334
|
+
location: schema({
|
|
335
|
+
latitude: field.number().latitude(),
|
|
336
|
+
longitude: field.number().longitude()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
// Age with soft warning
|
|
340
|
+
age: field.number()
|
|
341
|
+
.min(13, 'Must be 13+') // Hard error
|
|
342
|
+
.minSoft(18, 'Parental consent') // Soft warning
|
|
343
|
+
|
|
344
|
+
// Even page count
|
|
345
|
+
pages: field.number()
|
|
346
|
+
.integer()
|
|
347
|
+
.even('Must be even number')
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Date Validators (10)
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
field.date()
|
|
354
|
+
// Basic
|
|
355
|
+
.after(date) // After date
|
|
356
|
+
.before(date) // Before date
|
|
357
|
+
.past() // Must be in past
|
|
358
|
+
.future() // Must be in future
|
|
359
|
+
|
|
360
|
+
// Relative validation
|
|
361
|
+
.today() // Must be today
|
|
362
|
+
.yesterday() // Must be yesterday
|
|
363
|
+
.tomorrow() // Must be tomorrow
|
|
364
|
+
.thisWeek() // This week
|
|
365
|
+
.thisMonth() // This month
|
|
366
|
+
.thisYear() // This year
|
|
367
|
+
|
|
368
|
+
// Day validation
|
|
369
|
+
.weekday() // Monday-Friday
|
|
370
|
+
.weekend() // Saturday-Sunday
|
|
371
|
+
|
|
372
|
+
// Age validation
|
|
373
|
+
.age(min, max) // Age range from birthdate
|
|
374
|
+
.between(start, end) // Between two dates
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Examples:**
|
|
378
|
+
```typescript
|
|
379
|
+
// Birth date (18-65 years old)
|
|
380
|
+
birthDate: field.date()
|
|
381
|
+
.age(18, 65, 'Must be 18-65 years old')
|
|
382
|
+
.past('Cannot be in future')
|
|
383
|
+
|
|
384
|
+
// Event must be in future
|
|
385
|
+
eventDate: field.date()
|
|
386
|
+
.future('Event must be scheduled ahead')
|
|
387
|
+
.weekday('Events only on weekdays')
|
|
388
|
+
|
|
389
|
+
// Today's attendance
|
|
390
|
+
checkIn: field.date()
|
|
391
|
+
.today('Must check in today')
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Array Validators (6)
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
field.array()
|
|
398
|
+
// Size validation
|
|
399
|
+
.min(2) // Min items
|
|
400
|
+
.max(10) // Max items
|
|
401
|
+
.unique() // All items unique
|
|
402
|
+
|
|
403
|
+
// Content validation
|
|
404
|
+
.includes(item) // Must include item
|
|
405
|
+
.excludes(item) // Must not include item
|
|
406
|
+
.notEmpty() // At least 1 item
|
|
407
|
+
.empty() // Must be empty
|
|
408
|
+
|
|
409
|
+
// Order validation
|
|
410
|
+
.sorted('asc') // Sorted ascending
|
|
411
|
+
.sorted('desc') // Sorted descending
|
|
412
|
+
|
|
413
|
+
// Quality validation
|
|
414
|
+
.compact() // No falsy values
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Examples:**
|
|
418
|
+
```typescript
|
|
419
|
+
// Tags (1-5 unique items)
|
|
420
|
+
tags: field.array(field.string())
|
|
421
|
+
.min(1, 'At least one tag')
|
|
422
|
+
.max(5, 'Max 5 tags')
|
|
423
|
+
.unique('Tags must be unique')
|
|
424
|
+
|
|
425
|
+
// Must include required item
|
|
426
|
+
permissions: field.array()
|
|
427
|
+
.includes('read', 'Read permission required')
|
|
428
|
+
|
|
429
|
+
// Sorted numbers
|
|
430
|
+
scores: field.array(field.number())
|
|
431
|
+
.sorted('desc', 'Must be sorted highest first')
|
|
210
432
|
```
|
|
211
433
|
|
|
212
|
-
|
|
434
|
+
### Boolean Validators
|
|
213
435
|
|
|
214
436
|
```typescript
|
|
215
437
|
field.boolean()
|
|
216
|
-
.isTrue(
|
|
217
|
-
.isFalse(
|
|
218
|
-
|
|
438
|
+
.isTrue() // Must be true
|
|
439
|
+
.isFalse() // Must be false
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Examples:**
|
|
443
|
+
```typescript
|
|
444
|
+
// Terms acceptance
|
|
445
|
+
acceptTerms: field.boolean()
|
|
446
|
+
.isTrue('You must accept terms')
|
|
447
|
+
.required()
|
|
448
|
+
|
|
449
|
+
// Optional newsletter
|
|
450
|
+
newsletter: field.boolean()
|
|
451
|
+
.optional()
|
|
219
452
|
```
|
|
220
453
|
|
|
221
|
-
|
|
454
|
+
### Object Validators (Nested)
|
|
222
455
|
|
|
223
456
|
```typescript
|
|
224
|
-
field.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
457
|
+
field.object(schema) // Nested schema validation
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Examples:**
|
|
461
|
+
```typescript
|
|
462
|
+
// Nested address
|
|
463
|
+
const AddressSchema = schema({
|
|
464
|
+
street: field.string().required(),
|
|
465
|
+
city: field.string().required(),
|
|
466
|
+
zipCode: field.string().pattern(/^\d{5}$/)
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const UserSchema = schema({
|
|
470
|
+
name: field.string().required(),
|
|
471
|
+
address: field.object(AddressSchema).required()
|
|
472
|
+
});
|
|
230
473
|
```
|
|
231
474
|
|
|
232
|
-
|
|
475
|
+
### Cross-Field Validators (5)
|
|
233
476
|
|
|
234
477
|
```typescript
|
|
235
|
-
field.
|
|
236
|
-
.
|
|
237
|
-
.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
.
|
|
478
|
+
field.string()
|
|
479
|
+
.matches('password') // Must match field
|
|
480
|
+
.notMatches('oldPassword') // Must NOT match field
|
|
481
|
+
|
|
482
|
+
field.number()
|
|
483
|
+
.greaterThan('minValue') // > another field
|
|
484
|
+
.lessThan('maxValue') // < another field
|
|
485
|
+
|
|
486
|
+
field.string()
|
|
487
|
+
.dependsOn('country') // Required if field exists
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Examples:**
|
|
491
|
+
```typescript
|
|
492
|
+
// Password confirmation
|
|
493
|
+
const schema = schema({
|
|
494
|
+
password: field.string().min(8),
|
|
495
|
+
confirmPassword: field.string()
|
|
496
|
+
.matches('password', 'Passwords must match')
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// New password must differ
|
|
500
|
+
const changePasswordSchema = schema({
|
|
501
|
+
currentPassword: field.string(),
|
|
502
|
+
newPassword: field.string()
|
|
503
|
+
.notMatches('currentPassword', 'Must be different')
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Range validation
|
|
507
|
+
const rangeSchema = schema({
|
|
508
|
+
minPrice: field.number(),
|
|
509
|
+
maxPrice: field.number()
|
|
510
|
+
.greaterThan('minPrice', 'Max must be > min')
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Conditional requirement
|
|
514
|
+
const locationSchema = schema({
|
|
515
|
+
country: field.string(),
|
|
516
|
+
state: field.string()
|
|
517
|
+
.dependsOn('country', 'State requires country')
|
|
518
|
+
});
|
|
241
519
|
```
|
|
242
520
|
|
|
243
|
-
|
|
521
|
+
## 💡 Hard vs Soft Validation
|
|
522
|
+
|
|
523
|
+
Unischema supports two-tier validation for enterprise applications:
|
|
244
524
|
|
|
245
525
|
```typescript
|
|
246
|
-
|
|
247
|
-
.
|
|
526
|
+
const TransactionSchema = schema({
|
|
527
|
+
amount: field.number()
|
|
528
|
+
.min(0.01, 'Amount must be positive') // ❌ Hard: blocks submission
|
|
529
|
+
.maxSoft(10000, 'Review required for $10k+') // ⚠️ Soft: warning only
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const result = validateSchema(TransactionSchema.definition, { amount: 15000 });
|
|
533
|
+
|
|
534
|
+
console.log(result.valid); // true (no hard errors)
|
|
535
|
+
console.log(result.hardErrors); // []
|
|
536
|
+
console.log(result.softErrors); // [{ field: 'amount', message: 'Review required...', severity: 'soft' }]
|
|
248
537
|
```
|
|
249
538
|
|
|
250
|
-
|
|
539
|
+
**Use cases:**
|
|
540
|
+
- Warnings that don't block submission
|
|
541
|
+
- Age warnings (13+ required, 18+ recommended)
|
|
542
|
+
- Security score suggestions
|
|
543
|
+
- Large transaction reviews
|
|
544
|
+
|
|
545
|
+
## 🔧 Advanced Usage
|
|
546
|
+
|
|
547
|
+
### Schema Composition
|
|
251
548
|
|
|
252
549
|
```typescript
|
|
253
|
-
|
|
550
|
+
// Extend schemas
|
|
551
|
+
const BaseUser = schema({
|
|
552
|
+
email: field.string().email(),
|
|
553
|
+
name: field.string()
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const AdminUser = extend(BaseUser, {
|
|
557
|
+
role: field.string().enum(['admin', 'superadmin']),
|
|
558
|
+
permissions: field.array(field.string())
|
|
559
|
+
});
|
|
254
560
|
|
|
255
|
-
//
|
|
256
|
-
const
|
|
257
|
-
// { valid: boolean, hardErrors: [], softErrors: [] }
|
|
561
|
+
// Pick specific fields
|
|
562
|
+
const LoginSchema = pick(BaseUser, ['email']);
|
|
258
563
|
|
|
259
|
-
//
|
|
260
|
-
const
|
|
564
|
+
// Omit fields
|
|
565
|
+
const PublicUser = omit(BaseUser, ['password']);
|
|
261
566
|
|
|
262
|
-
//
|
|
263
|
-
const
|
|
567
|
+
// Merge schemas
|
|
568
|
+
const FullSchema = merge(ProfileSchema, SettingsSchema);
|
|
264
569
|
```
|
|
265
570
|
|
|
266
|
-
###
|
|
571
|
+
### TypeScript Integration
|
|
267
572
|
|
|
268
573
|
```typescript
|
|
269
|
-
import {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
app.get('/users', validateQuery(QuerySchema), handler);
|
|
280
|
-
app.get('/users/:id', validateParams(ParamsSchema), handler);
|
|
574
|
+
import { type InferInput, type InferOutput } from 'unischema';
|
|
575
|
+
|
|
576
|
+
const UserSchema = schema({
|
|
577
|
+
email: field.string().email().required(),
|
|
578
|
+
age: field.number().min(0)
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Input type (what you pass in)
|
|
582
|
+
type UserInput = InferInput<typeof UserSchema>;
|
|
583
|
+
// { email: string; age: number }
|
|
281
584
|
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
585
|
+
// Output type (after validation)
|
|
586
|
+
type UserOutput = InferOutput<typeof UserSchema>;
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Custom Validation
|
|
286
590
|
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
591
|
+
```typescript
|
|
592
|
+
const schema = schema({
|
|
593
|
+
password: field.string()
|
|
594
|
+
.custom((value, context) => {
|
|
595
|
+
if (!/[A-Z]/.test(value)) {
|
|
596
|
+
return { valid: false, message: 'Need uppercase letter' };
|
|
597
|
+
}
|
|
598
|
+
return true;
|
|
599
|
+
})
|
|
290
600
|
});
|
|
291
601
|
```
|
|
292
602
|
|
|
293
|
-
###
|
|
603
|
+
### Granular Imports (Tree-Shaking)
|
|
294
604
|
|
|
295
605
|
```typescript
|
|
296
|
-
|
|
606
|
+
// Import only what you need
|
|
607
|
+
import { emailValidator } from 'unischema/validators/string';
|
|
608
|
+
import { portValidator } from 'unischema/validators/number';
|
|
609
|
+
import { todayValidator } from 'unischema/validators/date';
|
|
610
|
+
|
|
611
|
+
// Or import by category
|
|
612
|
+
import * as stringValidators from 'unischema/validators/string';
|
|
613
|
+
import * as numberValidators from 'unischema/validators/number';
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
## 🌐 Framework Examples
|
|
617
|
+
|
|
618
|
+
### React
|
|
619
|
+
|
|
620
|
+
```tsx
|
|
621
|
+
import { createForm } from 'unischema/frontend';
|
|
622
|
+
import { UserSchema } from './schemas';
|
|
623
|
+
|
|
624
|
+
function RegisterForm() {
|
|
625
|
+
const form = createForm(UserSchema, {
|
|
626
|
+
initialValues: { email: '', password: '' },
|
|
627
|
+
onSubmit: async (values) => {
|
|
628
|
+
await api.register(values);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const emailProps = form.getFieldProps('email');
|
|
633
|
+
|
|
634
|
+
return (
|
|
635
|
+
<form onSubmit={form.handleSubmit}>
|
|
636
|
+
<input {...emailProps} />
|
|
637
|
+
{emailProps.hasError && <span>{emailProps.error}</span>}
|
|
638
|
+
|
|
639
|
+
<button type="submit">Register</button>
|
|
640
|
+
</form>
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Vue
|
|
646
|
+
|
|
647
|
+
```vue
|
|
648
|
+
<script setup>
|
|
649
|
+
import { createForm } from 'unischema/frontend';
|
|
650
|
+
import { UserSchema } from './schemas';
|
|
297
651
|
|
|
298
652
|
const form = createForm(UserSchema, {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
onSubmit: async (values, helpers) => {
|
|
303
|
-
// Submit logic
|
|
304
|
-
},
|
|
653
|
+
onSubmit: async (values) => {
|
|
654
|
+
await api.register(values);
|
|
655
|
+
}
|
|
305
656
|
});
|
|
306
657
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
form.
|
|
312
|
-
|
|
313
|
-
|
|
658
|
+
const emailProps = form.getFieldProps('email');
|
|
659
|
+
</script>
|
|
660
|
+
|
|
661
|
+
<template>
|
|
662
|
+
<form @submit.prevent="form.handleSubmit">
|
|
663
|
+
<input v-bind="emailProps" />
|
|
664
|
+
<span v-if="emailProps.hasError">{{ emailProps.error }}</span>
|
|
665
|
+
</form>
|
|
666
|
+
</template>
|
|
667
|
+
```
|
|
314
668
|
|
|
315
|
-
|
|
316
|
-
const props = form.getFieldProps('email');
|
|
317
|
-
// { name, value, onChange, onBlur, error, hasError, warning, hasWarning, ... }
|
|
669
|
+
### Express.js
|
|
318
670
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
671
|
+
```typescript
|
|
672
|
+
import express from 'express';
|
|
673
|
+
import { validateBody, validateQuery, validateParams } from 'unischema/backend';
|
|
674
|
+
|
|
675
|
+
const app = express();
|
|
676
|
+
|
|
677
|
+
// Body validation
|
|
678
|
+
app.post('/users', validateBody(UserSchema), (req, res) => {
|
|
679
|
+
const user = req.validatedData; // ✅ Typed and validated
|
|
680
|
+
res.json(user);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// Query validation
|
|
684
|
+
app.get('/search', validateQuery(SearchSchema), (req, res) => {
|
|
685
|
+
const { query } = req.validatedData;
|
|
686
|
+
res.json(results);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Params validation
|
|
690
|
+
app.get('/users/:id', validateParams(IdSchema), (req, res) => {
|
|
691
|
+
const { id } = req.validatedData;
|
|
692
|
+
res.json(user);
|
|
693
|
+
});
|
|
322
694
|
```
|
|
323
695
|
|
|
324
|
-
##
|
|
696
|
+
## 📊 Real-World Examples
|
|
325
697
|
|
|
326
|
-
|
|
698
|
+
### User Registration
|
|
327
699
|
|
|
328
700
|
```typescript
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
msg: string;
|
|
334
|
-
validation: {
|
|
335
|
-
hard_validations: ValidationError[];
|
|
336
|
-
soft_validations: ValidationError[];
|
|
337
|
-
};
|
|
338
|
-
}
|
|
701
|
+
const RegisterSchema = schema({
|
|
702
|
+
email: field.string()
|
|
703
|
+
.email('Invalid email address')
|
|
704
|
+
.required('Email is required'),
|
|
339
705
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
706
|
+
username: field.string()
|
|
707
|
+
.alphanumeric('Only letters and numbers')
|
|
708
|
+
.min(3, 'At least 3 characters')
|
|
709
|
+
.max(20, 'Max 20 characters')
|
|
710
|
+
.required(),
|
|
711
|
+
|
|
712
|
+
password: field.string()
|
|
713
|
+
.min(8, 'At least 8 characters')
|
|
714
|
+
.pattern(/[A-Z]/, 'Need uppercase letter')
|
|
715
|
+
.pattern(/[0-9]/, 'Need a number')
|
|
716
|
+
.required(),
|
|
717
|
+
|
|
718
|
+
confirmPassword: field.string()
|
|
719
|
+
.matches('password', 'Passwords must match')
|
|
720
|
+
.required(),
|
|
721
|
+
|
|
722
|
+
age: field.number()
|
|
723
|
+
.min(13, 'Must be 13+')
|
|
724
|
+
.minSoft(18, 'Parental consent required under 18')
|
|
725
|
+
.max(120, 'Invalid age')
|
|
726
|
+
.integer()
|
|
727
|
+
.required(),
|
|
728
|
+
|
|
729
|
+
acceptTerms: field.boolean()
|
|
730
|
+
.isTrue('You must accept the terms')
|
|
731
|
+
.required()
|
|
732
|
+
});
|
|
346
733
|
```
|
|
347
734
|
|
|
348
|
-
|
|
735
|
+
### E-Commerce Order
|
|
349
736
|
|
|
350
|
-
|
|
737
|
+
```typescript
|
|
738
|
+
const OrderSchema = schema({
|
|
739
|
+
customerId: field.string()
|
|
740
|
+
.alphanumeric()
|
|
741
|
+
.length(10)
|
|
742
|
+
.required(),
|
|
743
|
+
|
|
744
|
+
items: field.array(field.object(schema({
|
|
745
|
+
productId: field.string().required(),
|
|
746
|
+
quantity: field.number().min(1).integer(),
|
|
747
|
+
price: field.number().positive()
|
|
748
|
+
})))
|
|
749
|
+
.min(1, 'At least one item required')
|
|
750
|
+
.max(50, 'Maximum 50 items per order'),
|
|
751
|
+
|
|
752
|
+
total: field.number()
|
|
753
|
+
.positive()
|
|
754
|
+
.required(),
|
|
755
|
+
|
|
756
|
+
shippingAddress: field.object(schema({
|
|
757
|
+
street: field.string().required(),
|
|
758
|
+
city: field.string().required(),
|
|
759
|
+
state: field.string().uppercase().length(2),
|
|
760
|
+
zipCode: field.string().pattern(/^\d{5}$/)
|
|
761
|
+
})).required(),
|
|
762
|
+
|
|
763
|
+
shippingDate: field.date()
|
|
764
|
+
.future('Must be a future date')
|
|
765
|
+
.weekday('No weekend shipping')
|
|
766
|
+
});
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### API Configuration
|
|
351
770
|
|
|
352
771
|
```typescript
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
772
|
+
const ServerConfigSchema = schema({
|
|
773
|
+
host: field.string()
|
|
774
|
+
.ipAddress('Invalid IP address')
|
|
775
|
+
.required(),
|
|
776
|
+
|
|
777
|
+
port: field.number()
|
|
778
|
+
.port('Invalid port number')
|
|
779
|
+
.required(),
|
|
780
|
+
|
|
781
|
+
ssl: field.boolean()
|
|
782
|
+
.required(),
|
|
783
|
+
|
|
784
|
+
maxConnections: field.number()
|
|
785
|
+
.integer()
|
|
786
|
+
.positive()
|
|
787
|
+
.between(1, 10000),
|
|
788
|
+
|
|
789
|
+
timeout: field.number()
|
|
790
|
+
.integer()
|
|
791
|
+
.positive()
|
|
792
|
+
.multipleOf(1000, 'Must be in seconds (1000ms)')
|
|
793
|
+
});
|
|
794
|
+
```
|
|
356
795
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
796
|
+
## 🚀 Migration Guide
|
|
797
|
+
|
|
798
|
+
### From Yup
|
|
799
|
+
|
|
800
|
+
```typescript
|
|
801
|
+
// Yup
|
|
802
|
+
const schema = yup.object({
|
|
803
|
+
email: yup.string().email().required(),
|
|
804
|
+
age: yup.number().min(18)
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// Unischema
|
|
808
|
+
const schema = schema({
|
|
809
|
+
email: field.string().email().required(),
|
|
810
|
+
age: field.number().min(18)
|
|
811
|
+
});
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### From Zod
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
// Zod
|
|
818
|
+
const schema = z.object({
|
|
819
|
+
email: z.string().email(),
|
|
820
|
+
age: z.number().min(18)
|
|
821
|
+
});
|
|
360
822
|
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
823
|
+
// Unischema
|
|
824
|
+
const schema = schema({
|
|
825
|
+
email: field.string().email(),
|
|
826
|
+
age: field.number().min(18)
|
|
827
|
+
});
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
## 🎨 Bundle Size
|
|
831
|
+
|
|
832
|
+
Unischema is optimized for tree-shaking:
|
|
833
|
+
|
|
834
|
+
- **Full library**: ~15KB min+gzip
|
|
835
|
+
- **Core only**: ~5KB min+gzip
|
|
836
|
+
- **Single validator**: ~2KB min+gzip
|
|
837
|
+
|
|
838
|
+
Import only what you use for minimal bundle size.
|
|
839
|
+
|
|
840
|
+
## 📖 API Reference
|
|
841
|
+
|
|
842
|
+
### Core Functions
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
import {
|
|
846
|
+
// Schema creation
|
|
847
|
+
schema, // Create schema
|
|
848
|
+
field, // Field builders
|
|
849
|
+
coerce, // Type coercion builders
|
|
850
|
+
|
|
851
|
+
// Sync validation
|
|
852
|
+
validate, // Validate data
|
|
853
|
+
validateSchema, // Validate with schema
|
|
854
|
+
isValid, // Boolean validation
|
|
855
|
+
assertValid, // Throws if invalid
|
|
856
|
+
|
|
857
|
+
// Async validation (v1.2.0)
|
|
858
|
+
validateAsync, // Async validate data
|
|
859
|
+
validateSchemaAsync, // Async validate with schema
|
|
860
|
+
isValidAsync, // Async boolean validation
|
|
861
|
+
assertValidAsync, // Async throws if invalid
|
|
862
|
+
|
|
863
|
+
// Schema composition
|
|
864
|
+
extend, // Extend schema
|
|
865
|
+
pick, // Pick fields
|
|
866
|
+
omit, // Omit fields
|
|
867
|
+
merge, // Merge schemas
|
|
868
|
+
partial, // Make all optional
|
|
869
|
+
deepPartial, // Make all optional recursively (v1.2.0)
|
|
870
|
+
passthrough, // Allow unknown keys (v1.2.0)
|
|
871
|
+
strict, // Reject unknown keys (v1.2.0)
|
|
872
|
+
catchall, // Validate unknown keys (v1.2.0)
|
|
873
|
+
required, // Make specific fields required (v1.2.0)
|
|
874
|
+
optional, // Make specific fields optional (v1.2.0)
|
|
875
|
+
|
|
876
|
+
// Type inference
|
|
877
|
+
type InferInput, // Input type
|
|
878
|
+
type InferOutput // Output type
|
|
879
|
+
} from 'unischema';
|
|
364
880
|
```
|
|
365
881
|
|
|
366
|
-
|
|
882
|
+
### Backend
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
import {
|
|
886
|
+
validateBody, // Validate request body
|
|
887
|
+
validateQuery, // Validate query params
|
|
888
|
+
validateParams, // Validate route params
|
|
889
|
+
withValidation, // Wrapper with validation
|
|
890
|
+
createHandler // Serverless handler
|
|
891
|
+
} from 'unischema/backend';
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Frontend
|
|
895
|
+
|
|
896
|
+
```typescript
|
|
897
|
+
import {
|
|
898
|
+
createForm, // Create form helper
|
|
899
|
+
parseApiErrors, // Parse server errors
|
|
900
|
+
focusFirstError // Focus first error field
|
|
901
|
+
} from 'unischema/frontend';
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
## 🤝 Contributing
|
|
905
|
+
|
|
906
|
+
Contributions are welcome! Please check out the [GitHub repository](https://github.com/Gaurav-pasi/unischema).
|
|
907
|
+
|
|
908
|
+
## 📄 License
|
|
909
|
+
|
|
910
|
+
MIT © [Gaurav Pasi](https://github.com/Gaurav-pasi)
|
|
911
|
+
|
|
912
|
+
## 🔗 Links
|
|
913
|
+
|
|
914
|
+
- [npm package](https://www.npmjs.com/package/unischema)
|
|
915
|
+
- [GitHub repository](https://github.com/Gaurav-pasi/unischema)
|
|
916
|
+
- [Issue tracker](https://github.com/Gaurav-pasi/unischema/issues)
|
|
917
|
+
|
|
918
|
+
---
|
|
367
919
|
|
|
368
|
-
|
|
920
|
+
**Made with ❤️ for developers who value type safety and code reusability**
|