@vertz/schema 0.1.0 → 0.2.1
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 +543 -0
- package/dist/index.d.ts +27 -25
- package/dist/index.js +47 -12
- package/package.json +11 -4
package/README.md
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# @vertz/schema
|
|
2
|
+
|
|
3
|
+
Type-safe validation with end-to-end type inference. Define schemas with a fluent API, get full TypeScript types automatically.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @vertz/schema
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { s, type Infer } from '@vertz/schema';
|
|
15
|
+
|
|
16
|
+
// Define a schema
|
|
17
|
+
const userSchema = s.object({
|
|
18
|
+
name: s.string().min(1),
|
|
19
|
+
email: s.email(),
|
|
20
|
+
age: s.number().int().min(18),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Infer the type
|
|
24
|
+
type User = Infer<typeof userSchema>;
|
|
25
|
+
// { name: string; email: string; age: number }
|
|
26
|
+
|
|
27
|
+
// Parse (throws on invalid)
|
|
28
|
+
const user = userSchema.parse({
|
|
29
|
+
name: 'Alice',
|
|
30
|
+
email: 'alice@example.com',
|
|
31
|
+
age: 25,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Safe parse (never throws)
|
|
35
|
+
const result = userSchema.safeParse(data);
|
|
36
|
+
if (result.success) {
|
|
37
|
+
console.log(result.data);
|
|
38
|
+
} else {
|
|
39
|
+
console.log(result.error.issues);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Schema Types
|
|
44
|
+
|
|
45
|
+
### Primitives
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
s.string() // string
|
|
49
|
+
s.number() // number
|
|
50
|
+
s.boolean() // boolean
|
|
51
|
+
s.bigint() // bigint
|
|
52
|
+
s.date() // Date
|
|
53
|
+
s.symbol() // symbol
|
|
54
|
+
s.int() // number (integer-only)
|
|
55
|
+
s.nan() // NaN
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Special Types
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
s.any() // any
|
|
62
|
+
s.unknown() // unknown
|
|
63
|
+
s.null() // null
|
|
64
|
+
s.undefined() // undefined
|
|
65
|
+
s.void() // void
|
|
66
|
+
s.never() // never
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Composites
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Objects
|
|
73
|
+
s.object({
|
|
74
|
+
name: s.string(),
|
|
75
|
+
age: s.number(),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Arrays
|
|
79
|
+
s.array(s.number())
|
|
80
|
+
|
|
81
|
+
// Tuples
|
|
82
|
+
s.tuple([s.string(), s.number()])
|
|
83
|
+
|
|
84
|
+
// Enums
|
|
85
|
+
s.enum(['admin', 'user', 'guest'])
|
|
86
|
+
|
|
87
|
+
// Literals
|
|
88
|
+
s.literal('active')
|
|
89
|
+
|
|
90
|
+
// Unions
|
|
91
|
+
s.union([s.string(), s.number()])
|
|
92
|
+
|
|
93
|
+
// Discriminated unions
|
|
94
|
+
s.discriminatedUnion('type', [
|
|
95
|
+
s.object({ type: s.literal('text'), content: s.string() }),
|
|
96
|
+
s.object({ type: s.literal('image'), url: s.url() }),
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
// Intersections
|
|
100
|
+
s.intersection(
|
|
101
|
+
s.object({ id: s.string() }),
|
|
102
|
+
s.object({ name: s.string() }),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
// Records (dynamic keys)
|
|
106
|
+
s.record(s.string())
|
|
107
|
+
|
|
108
|
+
// Maps
|
|
109
|
+
s.map(s.string(), s.number())
|
|
110
|
+
|
|
111
|
+
// Sets
|
|
112
|
+
s.set(s.string())
|
|
113
|
+
|
|
114
|
+
// Files
|
|
115
|
+
s.file()
|
|
116
|
+
|
|
117
|
+
// Custom validators
|
|
118
|
+
s.custom<number>(
|
|
119
|
+
(val) => typeof val === 'number' && val % 2 === 0,
|
|
120
|
+
'Must be an even number',
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Instance checks
|
|
124
|
+
s.instanceof(Date)
|
|
125
|
+
|
|
126
|
+
// Recursive types
|
|
127
|
+
s.lazy(() => categorySchema)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Format Validators
|
|
131
|
+
|
|
132
|
+
Built-in validators for common formats:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
s.email() // Email address
|
|
136
|
+
s.uuid() // UUID
|
|
137
|
+
s.url() // HTTP(S) URL
|
|
138
|
+
s.hostname() // Valid hostname
|
|
139
|
+
s.ipv4() // IPv4 address
|
|
140
|
+
s.ipv6() // IPv6 address
|
|
141
|
+
s.base64() // Base64 string
|
|
142
|
+
s.hex() // Hexadecimal string
|
|
143
|
+
s.jwt() // JWT token (format only)
|
|
144
|
+
s.cuid() // CUID
|
|
145
|
+
s.ulid() // ULID
|
|
146
|
+
s.nanoid() // Nano ID
|
|
147
|
+
|
|
148
|
+
// ISO formats
|
|
149
|
+
s.iso.date() // YYYY-MM-DD
|
|
150
|
+
s.iso.time() // HH:MM:SS
|
|
151
|
+
s.iso.datetime() // ISO 8601 datetime
|
|
152
|
+
s.iso.duration() // ISO 8601 duration (P1Y2M3D)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Database Enum Bridge
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Convert a @vertz/db enum column to a schema
|
|
159
|
+
s.fromDbEnum(statusColumn)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Modifiers
|
|
163
|
+
|
|
164
|
+
### Optional & Nullable
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
s.string().optional() // string | undefined
|
|
168
|
+
s.string().nullable() // string | null
|
|
169
|
+
s.string().nullable().optional() // string | null | undefined
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Default Values
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
s.string().default('hello')
|
|
176
|
+
s.number().default(() => Math.random())
|
|
177
|
+
|
|
178
|
+
s.string().default('hello').parse(undefined) // 'hello'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Transformations
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
s.string().transform((val) => val.toUpperCase())
|
|
185
|
+
s.string().trim().transform((s) => s.split(','))
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Refinements
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Simple predicate
|
|
192
|
+
s.string().refine(
|
|
193
|
+
(val) => val.includes('@'),
|
|
194
|
+
{ message: 'Must contain @' },
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
// Multiple refinements
|
|
198
|
+
s.string()
|
|
199
|
+
.min(8)
|
|
200
|
+
.refine((val) => /[A-Z]/.test(val), { message: 'Need uppercase' })
|
|
201
|
+
.refine((val) => /[0-9]/.test(val), { message: 'Need digit' })
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Super Refine
|
|
205
|
+
|
|
206
|
+
Access the refinement context for cross-field validation:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
s.object({
|
|
210
|
+
password: s.string(),
|
|
211
|
+
confirm: s.string(),
|
|
212
|
+
}).superRefine((data, ctx) => {
|
|
213
|
+
if (data.password !== data.confirm) {
|
|
214
|
+
ctx.addIssue({
|
|
215
|
+
code: 'custom',
|
|
216
|
+
path: ['confirm'],
|
|
217
|
+
message: 'Passwords must match',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Branded Types
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
const UserId = s.string().uuid().brand('UserId');
|
|
227
|
+
const PostId = s.string().uuid().brand('PostId');
|
|
228
|
+
|
|
229
|
+
type UserId = Infer<typeof UserId>; // string & { __brand: 'UserId' }
|
|
230
|
+
type PostId = Infer<typeof PostId>; // string & { __brand: 'PostId' }
|
|
231
|
+
|
|
232
|
+
function getUser(id: UserId) { /* ... */ }
|
|
233
|
+
getUser(UserId.parse('...')); // OK
|
|
234
|
+
getUser(PostId.parse('...')); // Type error
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Catch (Error Recovery)
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
s.number().catch(0).parse('invalid') // 0
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Readonly
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
s.object({ tags: s.array(s.string()) }).readonly()
|
|
247
|
+
// Readonly<{ tags: readonly string[] }>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Pipe
|
|
251
|
+
|
|
252
|
+
Chain schemas sequentially:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
s.string().pipe(s.coerce.number())
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## String Validations
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
s.string()
|
|
262
|
+
.min(3) // min length
|
|
263
|
+
.max(20) // max length
|
|
264
|
+
.length(10) // exact length
|
|
265
|
+
.regex(/^[a-z]+$/) // pattern
|
|
266
|
+
.startsWith('hello')
|
|
267
|
+
.endsWith('world')
|
|
268
|
+
.includes('mid')
|
|
269
|
+
.uppercase() // must be uppercase
|
|
270
|
+
.lowercase() // must be lowercase
|
|
271
|
+
.trim() // trims whitespace (transform)
|
|
272
|
+
.toLowerCase() // converts to lowercase (transform)
|
|
273
|
+
.toUpperCase() // converts to uppercase (transform)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Number Validations
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
s.number()
|
|
280
|
+
.int() // integer only
|
|
281
|
+
.positive() // > 0
|
|
282
|
+
.negative() // < 0
|
|
283
|
+
.nonnegative() // >= 0
|
|
284
|
+
.nonpositive() // <= 0
|
|
285
|
+
.min(0) // >= n
|
|
286
|
+
.max(100) // <= n
|
|
287
|
+
.gt(0) // > n
|
|
288
|
+
.lt(100) // < n
|
|
289
|
+
.multipleOf(5) // divisible by n
|
|
290
|
+
.finite() // no Infinity
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Array Validations
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
s.array(s.string())
|
|
297
|
+
.min(1) // at least 1 element
|
|
298
|
+
.max(10) // at most 10 elements
|
|
299
|
+
.length(5) // exactly 5 elements
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Object Methods
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const base = s.object({ id: s.string(), name: s.string(), email: s.email() });
|
|
306
|
+
|
|
307
|
+
base.pick('id', 'name') // { id: string; name: string }
|
|
308
|
+
base.omit('email') // { id: string; name: string }
|
|
309
|
+
base.partial() // { id?: string; name?: string; email?: string }
|
|
310
|
+
base.required() // all fields required
|
|
311
|
+
base.extend({ age: s.number() }) // add fields
|
|
312
|
+
base.merge(otherSchema) // merge two object schemas
|
|
313
|
+
base.strict() // reject unknown keys
|
|
314
|
+
base.passthrough() // pass through unknown keys
|
|
315
|
+
base.catchall(s.string()) // validate unknown keys with schema
|
|
316
|
+
base.keyof() // ['id', 'name', 'email']
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Tuple Rest Elements
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
s.tuple([s.string(), s.number()]).rest(s.boolean())
|
|
323
|
+
// [string, number, ...boolean[]]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Parsing
|
|
327
|
+
|
|
328
|
+
### `.parse(data)`
|
|
329
|
+
|
|
330
|
+
Returns the parsed value. Throws `ParseError` on failure:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
try {
|
|
334
|
+
const value = schema.parse(data);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (error instanceof ParseError) {
|
|
337
|
+
console.log(error.issues);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### `.safeParse(data)`
|
|
343
|
+
|
|
344
|
+
Returns a result object. Never throws:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
const result = schema.safeParse(data);
|
|
348
|
+
|
|
349
|
+
if (result.success) {
|
|
350
|
+
result.data // parsed value
|
|
351
|
+
} else {
|
|
352
|
+
result.error // ParseError
|
|
353
|
+
result.error.issues // ValidationIssue[]
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Type Inference
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
import type { Infer, Input, Output } from '@vertz/schema';
|
|
361
|
+
|
|
362
|
+
const schema = s.string().transform((s) => s.length);
|
|
363
|
+
|
|
364
|
+
type In = Input<typeof schema>; // string
|
|
365
|
+
type Out = Output<typeof schema>; // number
|
|
366
|
+
type Out2 = Infer<typeof schema>; // number (alias for Output)
|
|
367
|
+
|
|
368
|
+
// Also available as instance properties:
|
|
369
|
+
type In3 = typeof schema._input;
|
|
370
|
+
type Out3 = typeof schema._output;
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Coercion
|
|
374
|
+
|
|
375
|
+
Convert values to the target type before validation:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
s.coerce.string() // String(value)
|
|
379
|
+
s.coerce.number() // Number(value)
|
|
380
|
+
s.coerce.boolean() // Boolean(value)
|
|
381
|
+
s.coerce.bigint() // BigInt(value)
|
|
382
|
+
s.coerce.date() // new Date(value)
|
|
383
|
+
|
|
384
|
+
s.coerce.number().parse('42') // 42
|
|
385
|
+
s.coerce.date().parse('2024-01-01') // Date object
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Error Handling
|
|
389
|
+
|
|
390
|
+
### ParseError
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import { ParseError } from '@vertz/schema';
|
|
394
|
+
|
|
395
|
+
const result = schema.safeParse(data);
|
|
396
|
+
|
|
397
|
+
if (!result.success) {
|
|
398
|
+
for (const issue of result.error.issues) {
|
|
399
|
+
console.log(issue.code); // 'invalid_type', 'too_small', etc.
|
|
400
|
+
console.log(issue.message); // human-readable message
|
|
401
|
+
console.log(issue.path); // ['address', 'street']
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Error Codes
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { ErrorCode } from '@vertz/schema';
|
|
410
|
+
|
|
411
|
+
ErrorCode.InvalidType // 'invalid_type'
|
|
412
|
+
ErrorCode.TooSmall // 'too_small'
|
|
413
|
+
ErrorCode.TooBig // 'too_big'
|
|
414
|
+
ErrorCode.InvalidString // 'invalid_string'
|
|
415
|
+
ErrorCode.InvalidEnumValue // 'invalid_enum_value'
|
|
416
|
+
ErrorCode.InvalidLiteral // 'invalid_literal'
|
|
417
|
+
ErrorCode.InvalidUnion // 'invalid_union'
|
|
418
|
+
ErrorCode.InvalidDate // 'invalid_date'
|
|
419
|
+
ErrorCode.MissingProperty // 'missing_property'
|
|
420
|
+
ErrorCode.UnrecognizedKeys // 'unrecognized_keys'
|
|
421
|
+
ErrorCode.Custom // 'custom'
|
|
422
|
+
ErrorCode.NotMultipleOf // 'not_multiple_of'
|
|
423
|
+
ErrorCode.NotFinite // 'not_finite'
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Result Type
|
|
427
|
+
|
|
428
|
+
Errors-as-values pattern for explicit error handling without try/catch:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { ok, err, unwrap, map, flatMap, match, matchErr } from '@vertz/schema';
|
|
432
|
+
import type { Result, Ok, Err } from '@vertz/schema';
|
|
433
|
+
|
|
434
|
+
// Create results
|
|
435
|
+
const success: Result<number, string> = ok(42);
|
|
436
|
+
const failure: Result<number, string> = err('not found');
|
|
437
|
+
|
|
438
|
+
// Check and extract
|
|
439
|
+
if (success.ok) {
|
|
440
|
+
success.data // 42
|
|
441
|
+
}
|
|
442
|
+
if (failure.ok === false) {
|
|
443
|
+
failure.error // 'not found'
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Unwrap (throws if Err)
|
|
447
|
+
const value = unwrap(success); // 42
|
|
448
|
+
|
|
449
|
+
// Map success value
|
|
450
|
+
const doubled = map(success, (n) => n * 2); // Ok(84)
|
|
451
|
+
|
|
452
|
+
// Chain Result-returning functions
|
|
453
|
+
const chained = flatMap(success, (n) =>
|
|
454
|
+
n > 0 ? ok(n.toString()) : err('must be positive')
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Pattern matching
|
|
458
|
+
const message = match(result, {
|
|
459
|
+
ok: (data) => `Got ${data}`,
|
|
460
|
+
err: (error) => `Failed: ${error}`,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Exhaustive error matching by code
|
|
464
|
+
const handled = matchErr(result, {
|
|
465
|
+
ok: (data) => data,
|
|
466
|
+
NOT_FOUND: (e) => fallback,
|
|
467
|
+
CONFLICT: (e) => retry(),
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The `Result` type is used throughout `@vertz/db` for all query methods.
|
|
472
|
+
|
|
473
|
+
## JSON Schema Generation
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
import { toJSONSchema } from '@vertz/schema';
|
|
477
|
+
|
|
478
|
+
const schema = s.object({
|
|
479
|
+
name: s.string().min(1),
|
|
480
|
+
age: s.number().int().min(0),
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const jsonSchema = toJSONSchema(schema);
|
|
484
|
+
// {
|
|
485
|
+
// type: 'object',
|
|
486
|
+
// properties: {
|
|
487
|
+
// name: { type: 'string', minLength: 1 },
|
|
488
|
+
// age: { type: 'integer', minimum: 0 }
|
|
489
|
+
// },
|
|
490
|
+
// required: ['name', 'age']
|
|
491
|
+
// }
|
|
492
|
+
|
|
493
|
+
// Also available as instance method:
|
|
494
|
+
schema.toJSONSchema()
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
## Schema Registry
|
|
498
|
+
|
|
499
|
+
Register and retrieve schemas by name:
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { SchemaRegistry } from '@vertz/schema';
|
|
503
|
+
|
|
504
|
+
// Register via .id()
|
|
505
|
+
const userSchema = s.object({ name: s.string() }).id('User');
|
|
506
|
+
|
|
507
|
+
// Retrieve
|
|
508
|
+
const schema = SchemaRegistry.get('User');
|
|
509
|
+
SchemaRegistry.has('User'); // true
|
|
510
|
+
SchemaRegistry.getAll(); // Map<string, Schema>
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Schema Metadata
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
const schema = s.string()
|
|
517
|
+
.id('Username')
|
|
518
|
+
.describe('The user display name')
|
|
519
|
+
.meta({ deprecated: true })
|
|
520
|
+
.example('alice');
|
|
521
|
+
|
|
522
|
+
schema.metadata.id // 'Username'
|
|
523
|
+
schema.metadata.description // 'The user display name'
|
|
524
|
+
schema.metadata.meta // { deprecated: true }
|
|
525
|
+
schema.metadata.examples // ['alice']
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
## Preprocessing
|
|
529
|
+
|
|
530
|
+
Transform raw input before schema validation:
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import { preprocess } from '@vertz/schema';
|
|
534
|
+
|
|
535
|
+
const schema = preprocess(
|
|
536
|
+
(val) => typeof val === 'string' ? val.trim() : val,
|
|
537
|
+
s.string().min(1),
|
|
538
|
+
);
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## License
|
|
542
|
+
|
|
543
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ declare class RefTracker {
|
|
|
56
56
|
getDefs(): Record<string, JSONSchemaObject>;
|
|
57
57
|
}
|
|
58
58
|
declare function toJSONSchema2(schema: SchemaAny): JSONSchemaObject;
|
|
59
|
+
import { Err, Ok, Result } from "@vertz/errors";
|
|
60
|
+
import { err, flatMap, isErr, isOk, map, match, matchErr, ok, unwrap } from "@vertz/errors";
|
|
59
61
|
declare enum SchemaType {
|
|
60
62
|
String = "string",
|
|
61
63
|
Number = "number",
|
|
@@ -93,13 +95,6 @@ interface SchemaMetadata {
|
|
|
93
95
|
meta: Record<string, unknown> | undefined;
|
|
94
96
|
examples: unknown[];
|
|
95
97
|
}
|
|
96
|
-
type SafeParseResult<T> = {
|
|
97
|
-
success: true;
|
|
98
|
-
data: T;
|
|
99
|
-
} | {
|
|
100
|
-
success: false;
|
|
101
|
-
error: ParseError;
|
|
102
|
-
};
|
|
103
98
|
type SchemaAny = Schema<any, any>;
|
|
104
99
|
/** Apply Readonly only to object types; leave primitives and `any` unchanged. */
|
|
105
100
|
type ReadonlyOutput<O> = 0 extends 1 & O ? O : O extends object ? Readonly<O> : O;
|
|
@@ -119,8 +114,8 @@ declare abstract class Schema<
|
|
|
119
114
|
abstract _schemaType(): SchemaType;
|
|
120
115
|
abstract _toJSONSchema(tracker: RefTracker): JSONSchemaObject;
|
|
121
116
|
abstract _clone(): Schema<O, I>;
|
|
122
|
-
parse(value: unknown): O
|
|
123
|
-
safeParse(value: unknown):
|
|
117
|
+
parse(value: unknown): Result<O, ParseError>;
|
|
118
|
+
safeParse(value: unknown): Result<O, ParseError>;
|
|
124
119
|
id(name: string): this;
|
|
125
120
|
describe(description: string): this;
|
|
126
121
|
meta(data: Record<string, unknown>): this;
|
|
@@ -380,26 +375,26 @@ declare class StringSchema extends Schema<string> {
|
|
|
380
375
|
private _toUpperCase;
|
|
381
376
|
private _normalize;
|
|
382
377
|
_parse(value: unknown, ctx: ParseContext): string;
|
|
383
|
-
min(n: number, message?: string):
|
|
384
|
-
max(n: number, message?: string):
|
|
385
|
-
length(n: number, message?: string):
|
|
386
|
-
regex(pattern: RegExp):
|
|
387
|
-
startsWith(prefix: string):
|
|
388
|
-
endsWith(suffix: string):
|
|
389
|
-
includes(substring: string):
|
|
390
|
-
uppercase():
|
|
391
|
-
lowercase():
|
|
392
|
-
trim():
|
|
393
|
-
toLowerCase():
|
|
394
|
-
toUpperCase():
|
|
395
|
-
normalize():
|
|
378
|
+
min(n: number, message?: string): this;
|
|
379
|
+
max(n: number, message?: string): this;
|
|
380
|
+
length(n: number, message?: string): this;
|
|
381
|
+
regex(pattern: RegExp): this;
|
|
382
|
+
startsWith(prefix: string): this;
|
|
383
|
+
endsWith(suffix: string): this;
|
|
384
|
+
includes(substring: string): this;
|
|
385
|
+
uppercase(): this;
|
|
386
|
+
lowercase(): this;
|
|
387
|
+
trim(): this;
|
|
388
|
+
toLowerCase(): this;
|
|
389
|
+
toUpperCase(): this;
|
|
390
|
+
normalize(): this;
|
|
396
391
|
_schemaType(): SchemaType;
|
|
397
392
|
_toJSONSchema(_tracker: RefTracker): JSONSchemaObject;
|
|
398
|
-
_clone():
|
|
393
|
+
_clone(): this;
|
|
399
394
|
}
|
|
400
395
|
declare class CoercedStringSchema extends StringSchema {
|
|
401
396
|
_parse(value: unknown, ctx: ParseContext): string;
|
|
402
|
-
_clone():
|
|
397
|
+
_clone(): this;
|
|
403
398
|
}
|
|
404
399
|
declare class CoercedNumberSchema extends NumberSchema {
|
|
405
400
|
_parse(value: unknown, ctx: ParseContext): number;
|
|
@@ -465,6 +460,8 @@ declare class DiscriminatedUnionSchema<T extends DiscriminatedOptions> extends S
|
|
|
465
460
|
declare class EnumSchema<T extends readonly [string, ...string[]]> extends Schema<T[number]> {
|
|
466
461
|
private readonly _values;
|
|
467
462
|
constructor(values: T);
|
|
463
|
+
/** Public accessor for the enum's allowed values. */
|
|
464
|
+
get values(): T;
|
|
468
465
|
_parse(value: unknown, ctx: ParseContext): T[number];
|
|
469
466
|
_schemaType(): SchemaType;
|
|
470
467
|
_toJSONSchema(_tracker: RefTracker): JSONSchemaObject;
|
|
@@ -770,6 +767,11 @@ declare const s: {
|
|
|
770
767
|
datetime: () => IsoDatetimeSchema;
|
|
771
768
|
duration: () => IsoDurationSchema;
|
|
772
769
|
};
|
|
770
|
+
fromDbEnum: <const TValues extends readonly [string, ...string[]]>(column: {
|
|
771
|
+
_meta: {
|
|
772
|
+
enumValues: TValues;
|
|
773
|
+
};
|
|
774
|
+
}) => EnumSchema<TValues>;
|
|
773
775
|
coerce: {
|
|
774
776
|
string: () => CoercedStringSchema;
|
|
775
777
|
number: () => CoercedNumberSchema;
|
|
@@ -779,4 +781,4 @@ declare const s: {
|
|
|
779
781
|
};
|
|
780
782
|
};
|
|
781
783
|
declare const schema: typeof s;
|
|
782
|
-
export { toJSONSchema2 as toJSONSchema, schema, s, preprocess, VoidSchema, ValidationIssue, UuidSchema, UrlSchema, UnknownSchema, UnionSchema, UndefinedSchema, UlidSchema, TupleSchema, TransformSchema, SymbolSchema, SuperRefinedSchema, StringSchema, SetSchema, SchemaType, SchemaRegistry, SchemaMetadata, SchemaAny, Schema,
|
|
784
|
+
export { unwrap, toJSONSchema2 as toJSONSchema, schema, s, preprocess, ok, matchErr, match, map, isOk, isErr, flatMap, err, VoidSchema, ValidationIssue, UuidSchema, UrlSchema, UnknownSchema, UnionSchema, UndefinedSchema, UlidSchema, TupleSchema, TransformSchema, SymbolSchema, SuperRefinedSchema, StringSchema, SetSchema, SchemaType, SchemaRegistry, SchemaMetadata, SchemaAny, Schema, Result, RefinementContext, RefinedSchema, RefTracker, RecordSchema, ReadonlySchema, ReadonlyOutput, PipeSchema, ParseError, ParseContext, Output, OptionalSchema, Ok, ObjectSchema, NumberSchema, NullableSchema, NullSchema, NeverSchema, NanoidSchema, NanSchema, MapSchema, LiteralSchema, LazySchema, JwtSchema, JSONSchemaObject, IsoTimeSchema, IsoDurationSchema, IsoDatetimeSchema, IsoDateSchema, Ipv6Schema, Ipv4Schema, IntersectionSchema, InstanceOfSchema, Input, Infer, HostnameSchema, HexSchema, FileSchema, ErrorCode, Err, EnumSchema, EmailSchema, DiscriminatedUnionSchema, DefaultSchema, DateSchema, CustomSchema, CuidSchema, CoercedStringSchema, CoercedNumberSchema, CoercedDateSchema, CoercedBooleanSchema, CoercedBigIntSchema, CatchSchema, BrandedSchema, BooleanSchema, BigIntSchema, Base64Schema, ArraySchema, AnySchema };
|
package/dist/index.js
CHANGED
|
@@ -102,6 +102,20 @@ function toJSONSchema(schema) {
|
|
|
102
102
|
return schema.toJSONSchema();
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// src/result.ts
|
|
106
|
+
import {
|
|
107
|
+
err,
|
|
108
|
+
flatMap,
|
|
109
|
+
isErr,
|
|
110
|
+
isOk,
|
|
111
|
+
map,
|
|
112
|
+
match,
|
|
113
|
+
matchErr,
|
|
114
|
+
ok,
|
|
115
|
+
unwrap,
|
|
116
|
+
unwrapOr
|
|
117
|
+
} from "@vertz/errors";
|
|
118
|
+
|
|
105
119
|
// src/core/schema.ts
|
|
106
120
|
class Schema {
|
|
107
121
|
_id;
|
|
@@ -115,21 +129,21 @@ class Schema {
|
|
|
115
129
|
const ctx = new ParseContext;
|
|
116
130
|
const result = this._runPipeline(value, ctx);
|
|
117
131
|
if (ctx.hasIssues()) {
|
|
118
|
-
|
|
132
|
+
return err(new ParseError(ctx.issues));
|
|
119
133
|
}
|
|
120
|
-
return result;
|
|
134
|
+
return ok(result);
|
|
121
135
|
}
|
|
122
136
|
safeParse(value) {
|
|
123
137
|
const ctx = new ParseContext;
|
|
124
138
|
try {
|
|
125
139
|
const data = this._runPipeline(value, ctx);
|
|
126
140
|
if (ctx.hasIssues()) {
|
|
127
|
-
return
|
|
141
|
+
return err(new ParseError(ctx.issues));
|
|
128
142
|
}
|
|
129
|
-
return
|
|
143
|
+
return ok(data);
|
|
130
144
|
} catch (e) {
|
|
131
145
|
if (e instanceof ParseError) {
|
|
132
|
-
return
|
|
146
|
+
return err(e);
|
|
133
147
|
}
|
|
134
148
|
throw e;
|
|
135
149
|
}
|
|
@@ -1095,7 +1109,8 @@ class StringSchema extends Schema {
|
|
|
1095
1109
|
return schema;
|
|
1096
1110
|
}
|
|
1097
1111
|
_clone() {
|
|
1098
|
-
const
|
|
1112
|
+
const Ctor = this.constructor;
|
|
1113
|
+
const clone = this._cloneBase(new Ctor);
|
|
1099
1114
|
clone._min = this._min;
|
|
1100
1115
|
clone._minMessage = this._minMessage;
|
|
1101
1116
|
clone._max = this._max;
|
|
@@ -1122,7 +1137,8 @@ class CoercedStringSchema extends StringSchema {
|
|
|
1122
1137
|
return super._parse(value == null ? "" : String(value), ctx);
|
|
1123
1138
|
}
|
|
1124
1139
|
_clone() {
|
|
1125
|
-
|
|
1140
|
+
const Ctor = this.constructor;
|
|
1141
|
+
return Object.assign(new Ctor, super._clone());
|
|
1126
1142
|
}
|
|
1127
1143
|
}
|
|
1128
1144
|
|
|
@@ -1299,6 +1315,9 @@ class EnumSchema extends Schema {
|
|
|
1299
1315
|
super();
|
|
1300
1316
|
this._values = values;
|
|
1301
1317
|
}
|
|
1318
|
+
get values() {
|
|
1319
|
+
return this._values;
|
|
1320
|
+
}
|
|
1302
1321
|
_parse(value, ctx) {
|
|
1303
1322
|
if (!this._values.includes(value)) {
|
|
1304
1323
|
ctx.addIssue({
|
|
@@ -1435,10 +1454,10 @@ var IPV4_RE = /^(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d
|
|
|
1435
1454
|
class Ipv4Schema extends FormatSchema {
|
|
1436
1455
|
_errorMessage = "Invalid IPv4 address";
|
|
1437
1456
|
_validate(value) {
|
|
1438
|
-
const
|
|
1439
|
-
if (!
|
|
1457
|
+
const match2 = IPV4_RE.exec(value);
|
|
1458
|
+
if (!match2)
|
|
1440
1459
|
return false;
|
|
1441
|
-
return [
|
|
1460
|
+
return [match2[1], match2[2], match2[3], match2[4]].every((o) => Number(o) <= 255);
|
|
1442
1461
|
}
|
|
1443
1462
|
_jsonSchemaExtra() {
|
|
1444
1463
|
return { format: "ipv4" };
|
|
@@ -1614,7 +1633,7 @@ class IntersectionSchema extends Schema {
|
|
|
1614
1633
|
_parse(value, ctx) {
|
|
1615
1634
|
const leftResult = this._left.safeParse(value);
|
|
1616
1635
|
const rightResult = this._right.safeParse(value);
|
|
1617
|
-
if (!leftResult.
|
|
1636
|
+
if (!leftResult.ok || !rightResult.ok) {
|
|
1618
1637
|
ctx.addIssue({
|
|
1619
1638
|
code: "invalid_intersection" /* InvalidIntersection */,
|
|
1620
1639
|
message: "Value does not satisfy intersection"
|
|
@@ -2249,7 +2268,7 @@ class UnionSchema extends Schema {
|
|
|
2249
2268
|
_parse(value, ctx) {
|
|
2250
2269
|
for (const option of this._options) {
|
|
2251
2270
|
const result = option.safeParse(value);
|
|
2252
|
-
if (result.
|
|
2271
|
+
if (result.ok) {
|
|
2253
2272
|
return result.data;
|
|
2254
2273
|
}
|
|
2255
2274
|
}
|
|
@@ -2355,6 +2374,13 @@ var s = {
|
|
|
2355
2374
|
datetime: () => new IsoDatetimeSchema,
|
|
2356
2375
|
duration: () => new IsoDurationSchema
|
|
2357
2376
|
},
|
|
2377
|
+
fromDbEnum: (column) => {
|
|
2378
|
+
const values = column._meta.enumValues;
|
|
2379
|
+
if (!values || values.length === 0) {
|
|
2380
|
+
throw new Error("s.fromDbEnum(): not an enum column — _meta.enumValues is missing or empty");
|
|
2381
|
+
}
|
|
2382
|
+
return new EnumSchema(values);
|
|
2383
|
+
},
|
|
2358
2384
|
coerce: {
|
|
2359
2385
|
string: () => new CoercedStringSchema,
|
|
2360
2386
|
number: () => new CoercedNumberSchema,
|
|
@@ -2365,10 +2391,19 @@ var s = {
|
|
|
2365
2391
|
};
|
|
2366
2392
|
var schema = s;
|
|
2367
2393
|
export {
|
|
2394
|
+
unwrap,
|
|
2368
2395
|
toJSONSchema,
|
|
2369
2396
|
schema,
|
|
2370
2397
|
s,
|
|
2371
2398
|
preprocess,
|
|
2399
|
+
ok,
|
|
2400
|
+
matchErr,
|
|
2401
|
+
match,
|
|
2402
|
+
map,
|
|
2403
|
+
isOk,
|
|
2404
|
+
isErr,
|
|
2405
|
+
flatMap,
|
|
2406
|
+
err,
|
|
2372
2407
|
VoidSchema,
|
|
2373
2408
|
UuidSchema,
|
|
2374
2409
|
UrlSchema,
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/schema",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"description": "Type-safe schema definitions for Vertz",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "https://github.com/vertz-dev/vertz.git",
|
|
@@ -25,16 +26,22 @@
|
|
|
25
26
|
],
|
|
26
27
|
"scripts": {
|
|
27
28
|
"build": "bunup",
|
|
28
|
-
"test": "
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"test:coverage": "vitest run --coverage",
|
|
29
31
|
"test:watch": "vitest",
|
|
30
32
|
"typecheck": "tsc --noEmit"
|
|
31
33
|
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@vertz/errors": "0.2.1"
|
|
36
|
+
},
|
|
32
37
|
"devDependencies": {
|
|
38
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
33
39
|
"bunup": "latest",
|
|
34
40
|
"typescript": "^5.7.0",
|
|
35
|
-
"vitest": "^
|
|
41
|
+
"vitest": "^4.0.18"
|
|
36
42
|
},
|
|
37
43
|
"engines": {
|
|
38
44
|
"node": ">=22"
|
|
39
|
-
}
|
|
45
|
+
},
|
|
46
|
+
"sideEffects": false
|
|
40
47
|
}
|