voltjs-framework 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1265 -0
  3. package/bin/volt.js +139 -0
  4. package/package.json +56 -0
  5. package/src/api/graphql.js +399 -0
  6. package/src/api/rest.js +204 -0
  7. package/src/api/websocket.js +285 -0
  8. package/src/cli/build.js +111 -0
  9. package/src/cli/create.js +371 -0
  10. package/src/cli/db.js +106 -0
  11. package/src/cli/dev.js +114 -0
  12. package/src/cli/generate.js +278 -0
  13. package/src/cli/lint.js +172 -0
  14. package/src/cli/routes.js +118 -0
  15. package/src/cli/start.js +42 -0
  16. package/src/cli/test.js +138 -0
  17. package/src/core/app.js +701 -0
  18. package/src/core/config.js +232 -0
  19. package/src/core/middleware.js +133 -0
  20. package/src/core/plugins.js +88 -0
  21. package/src/core/react-renderer.js +244 -0
  22. package/src/core/renderer.js +337 -0
  23. package/src/core/router.js +183 -0
  24. package/src/database/index.js +461 -0
  25. package/src/database/migration.js +192 -0
  26. package/src/database/model.js +285 -0
  27. package/src/database/query.js +394 -0
  28. package/src/database/seeder.js +89 -0
  29. package/src/index.js +156 -0
  30. package/src/security/auth.js +425 -0
  31. package/src/security/cors.js +80 -0
  32. package/src/security/csrf.js +125 -0
  33. package/src/security/encryption.js +110 -0
  34. package/src/security/helmet.js +103 -0
  35. package/src/security/index.js +75 -0
  36. package/src/security/rateLimit.js +119 -0
  37. package/src/security/sanitizer.js +113 -0
  38. package/src/security/xss.js +110 -0
  39. package/src/ui/component.js +224 -0
  40. package/src/ui/reactive.js +503 -0
  41. package/src/ui/template.js +448 -0
  42. package/src/utils/cache.js +216 -0
  43. package/src/utils/collection.js +772 -0
  44. package/src/utils/cron.js +213 -0
  45. package/src/utils/date.js +223 -0
  46. package/src/utils/events.js +181 -0
  47. package/src/utils/excel.js +482 -0
  48. package/src/utils/form.js +547 -0
  49. package/src/utils/hash.js +121 -0
  50. package/src/utils/http.js +461 -0
  51. package/src/utils/logger.js +186 -0
  52. package/src/utils/mail.js +347 -0
  53. package/src/utils/paginator.js +179 -0
  54. package/src/utils/pdf.js +417 -0
  55. package/src/utils/queue.js +199 -0
  56. package/src/utils/schema.js +985 -0
  57. package/src/utils/sms.js +243 -0
  58. package/src/utils/storage.js +348 -0
  59. package/src/utils/string.js +236 -0
  60. package/src/utils/validation.js +318 -0
@@ -0,0 +1,985 @@
1
+ /**
2
+ * VoltJS Schema Builder (Zod/Yup Alternative)
3
+ *
4
+ * Type-safe, chainable, composable schema validation with transforms.
5
+ *
6
+ * @example
7
+ * const { Schema } = require('voltjs');
8
+ *
9
+ * const userSchema = Schema.object({
10
+ * name: Schema.string().min(2).max(50).required(),
11
+ * email: Schema.string().email().required(),
12
+ * age: Schema.number().int().min(18).max(120).optional(),
13
+ * role: Schema.enum(['admin', 'user', 'editor']).default('user'),
14
+ * tags: Schema.array(Schema.string()).min(1),
15
+ * address: Schema.object({
16
+ * street: Schema.string(),
17
+ * city: Schema.string().required(),
18
+ * zip: Schema.string().regex(/^\d{5}$/),
19
+ * }).optional(),
20
+ * });
21
+ *
22
+ * const result = userSchema.validate(data);
23
+ * // { valid: true, data: { ... } } or { valid: false, errors: { ... } }
24
+ *
25
+ * const parsed = userSchema.parse(data); // throws on invalid
26
+ */
27
+
28
+ 'use strict';
29
+
30
+ // ============================================
31
+ // BASE SCHEMA
32
+ // ============================================
33
+
34
+ class BaseSchema {
35
+ constructor() {
36
+ this._checks = [];
37
+ this._required = false;
38
+ this._optional = false;
39
+ this._nullable = false;
40
+ this._default = undefined;
41
+ this._hasDefault = false;
42
+ this._transforms = [];
43
+ this._label = null;
44
+ this._messages = {};
45
+ }
46
+
47
+ /** Mark as required */
48
+ required(message) {
49
+ this._required = true;
50
+ this._optional = false;
51
+ if (message) this._messages.required = message;
52
+ return this;
53
+ }
54
+
55
+ /** Mark as optional */
56
+ optional() {
57
+ this._optional = true;
58
+ this._required = false;
59
+ return this;
60
+ }
61
+
62
+ /** Allow null */
63
+ nullable() {
64
+ this._nullable = true;
65
+ return this;
66
+ }
67
+
68
+ /** Set default value */
69
+ default(value) {
70
+ this._default = value;
71
+ this._hasDefault = true;
72
+ return this;
73
+ }
74
+
75
+ /** Set label for error messages */
76
+ label(name) {
77
+ this._label = name;
78
+ return this;
79
+ }
80
+
81
+ /** Custom error message override */
82
+ message(key, msg) {
83
+ this._messages[key] = msg;
84
+ return this;
85
+ }
86
+
87
+ /** Add a transform (applied before validation) */
88
+ transform(fn) {
89
+ this._transforms.push(fn);
90
+ return this;
91
+ }
92
+
93
+ /** Add a custom validation check */
94
+ refine(fn, message = 'Validation failed') {
95
+ this._checks.push({
96
+ name: 'custom',
97
+ fn,
98
+ message: typeof message === 'string' ? message : message.message || 'Validation failed',
99
+ });
100
+ return this;
101
+ }
102
+
103
+ /** Alias for refine */
104
+ test(name, message, fn) {
105
+ this._checks.push({ name, fn, message });
106
+ return this;
107
+ }
108
+
109
+ /** Internal: get label for error messages */
110
+ _getLabel(fallback = 'Value') {
111
+ return this._label || fallback;
112
+ }
113
+
114
+ /** Internal: get custom message or default */
115
+ _msg(key, defaultMsg) {
116
+ return this._messages[key] || defaultMsg;
117
+ }
118
+
119
+ /** Apply transforms */
120
+ _applyTransforms(value) {
121
+ let result = value;
122
+ for (const fn of this._transforms) {
123
+ result = fn(result);
124
+ }
125
+ return result;
126
+ }
127
+
128
+ /** Core validate logic (override in subclasses) */
129
+ _validate(value, path = '') {
130
+ const label = this._getLabel(path || 'Value');
131
+ const errors = [];
132
+
133
+ // Handle undefined/null
134
+ if (value === undefined || value === null || value === '') {
135
+ if (this._nullable && value === null) return { valid: true, value: null };
136
+ if (this._hasDefault) return { valid: true, value: this._default };
137
+ if (this._required) {
138
+ errors.push(this._msg('required', `${label} is required`));
139
+ return { valid: false, errors };
140
+ }
141
+ if (this._optional) return { valid: true, value: undefined };
142
+ // Default: treat as optional
143
+ return { valid: true, value: undefined };
144
+ }
145
+
146
+ // Apply transforms
147
+ value = this._applyTransforms(value);
148
+
149
+ // Type-specific validation
150
+ const typeResult = this._validateType(value, label);
151
+ if (typeResult) {
152
+ if (Array.isArray(typeResult)) {
153
+ errors.push(...typeResult);
154
+ } else {
155
+ errors.push(typeResult);
156
+ }
157
+ }
158
+
159
+ // Run custom checks
160
+ for (const check of this._checks) {
161
+ try {
162
+ const result = check.fn(value);
163
+ if (result === false) {
164
+ errors.push(check.message.replace('{label}', label));
165
+ }
166
+ } catch (err) {
167
+ errors.push(err.message || check.message);
168
+ }
169
+ }
170
+
171
+ return errors.length > 0
172
+ ? { valid: false, errors }
173
+ : { valid: true, value };
174
+ }
175
+
176
+ /** Override in subclasses for type-specific validation */
177
+ _validateType(value, label) {
178
+ return null;
179
+ }
180
+
181
+ /** Validate and return result */
182
+ validate(value, path = '') {
183
+ return this._validate(value, path);
184
+ }
185
+
186
+ /** Validate — throw if invalid */
187
+ parse(value) {
188
+ const result = this._validate(value);
189
+ if (!result.valid) {
190
+ const err = new Error('Schema validation failed');
191
+ err.errors = result.errors;
192
+ throw err;
193
+ }
194
+ return result.value;
195
+ }
196
+
197
+ /** Validate — return undefined instead of throwing */
198
+ safeParse(value) {
199
+ return this._validate(value);
200
+ }
201
+ }
202
+
203
+ // ============================================
204
+ // STRING SCHEMA
205
+ // ============================================
206
+
207
+ class StringSchema extends BaseSchema {
208
+ constructor() {
209
+ super();
210
+ this._coerce = false;
211
+ }
212
+
213
+ /** Coerce value to string */
214
+ coerce() {
215
+ this._coerce = true;
216
+ return this;
217
+ }
218
+
219
+ min(n, message) {
220
+ this._checks.push({ name: 'min', fn: v => v.length >= n, message: message || `Must be at least ${n} characters` });
221
+ return this;
222
+ }
223
+
224
+ max(n, message) {
225
+ this._checks.push({ name: 'max', fn: v => v.length <= n, message: message || `Must be at most ${n} characters` });
226
+ return this;
227
+ }
228
+
229
+ length(n, message) {
230
+ this._checks.push({ name: 'length', fn: v => v.length === n, message: message || `Must be exactly ${n} characters` });
231
+ return this;
232
+ }
233
+
234
+ email(message) {
235
+ this._checks.push({ name: 'email', fn: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: message || 'Must be a valid email' });
236
+ return this;
237
+ }
238
+
239
+ url(message) {
240
+ this._checks.push({ name: 'url', fn: v => { try { new URL(v); return true; } catch { return false; } }, message: message || 'Must be a valid URL' });
241
+ return this;
242
+ }
243
+
244
+ uuid(message) {
245
+ this._checks.push({ name: 'uuid', fn: v => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v), message: message || 'Must be a valid UUID' });
246
+ return this;
247
+ }
248
+
249
+ regex(pattern, message) {
250
+ this._checks.push({ name: 'regex', fn: v => pattern.test(v), message: message || `Must match pattern ${pattern}` });
251
+ return this;
252
+ }
253
+
254
+ includes(substr, message) {
255
+ this._checks.push({ name: 'includes', fn: v => v.includes(substr), message: message || `Must include "${substr}"` });
256
+ return this;
257
+ }
258
+
259
+ startsWith(prefix, message) {
260
+ this._checks.push({ name: 'startsWith', fn: v => v.startsWith(prefix), message: message || `Must start with "${prefix}"` });
261
+ return this;
262
+ }
263
+
264
+ endsWith(suffix, message) {
265
+ this._checks.push({ name: 'endsWith', fn: v => v.endsWith(suffix), message: message || `Must end with "${suffix}"` });
266
+ return this;
267
+ }
268
+
269
+ ip(message) {
270
+ this._checks.push({ name: 'ip', fn: v => /^(\d{1,3}\.){3}\d{1,3}$/.test(v), message: message || 'Must be a valid IP address' });
271
+ return this;
272
+ }
273
+
274
+ phone(message) {
275
+ this._checks.push({ name: 'phone', fn: v => /^\+?[\d\s()-]{7,15}$/.test(v), message: message || 'Must be a valid phone number' });
276
+ return this;
277
+ }
278
+
279
+ creditCard(message) {
280
+ this._checks.push({
281
+ name: 'creditCard',
282
+ fn: v => {
283
+ const cleaned = v.replace(/[\s-]/g, '');
284
+ if (!/^\d{13,19}$/.test(cleaned)) return false;
285
+ let sum = 0, alt = false;
286
+ for (let i = cleaned.length - 1; i >= 0; i--) {
287
+ let n = parseInt(cleaned[i]);
288
+ if (alt) { n *= 2; if (n > 9) n -= 9; }
289
+ sum += n;
290
+ alt = !alt;
291
+ }
292
+ return sum % 10 === 0;
293
+ },
294
+ message: message || 'Must be a valid credit card number',
295
+ });
296
+ return this;
297
+ }
298
+
299
+ /** Trim whitespace (transform) */
300
+ trim() {
301
+ return this.transform(v => typeof v === 'string' ? v.trim() : v);
302
+ }
303
+
304
+ /** Convert to lowercase (transform) */
305
+ toLowerCase() {
306
+ return this.transform(v => typeof v === 'string' ? v.toLowerCase() : v);
307
+ }
308
+
309
+ /** Convert to uppercase (transform) */
310
+ toUpperCase() {
311
+ return this.transform(v => typeof v === 'string' ? v.toUpperCase() : v);
312
+ }
313
+
314
+ /** Allow only alphanumeric */
315
+ alphanumeric(message) {
316
+ this._checks.push({ name: 'alphanumeric', fn: v => /^[a-zA-Z0-9]+$/.test(v), message: message || 'Must be alphanumeric' });
317
+ return this;
318
+ }
319
+
320
+ /** Slug format */
321
+ slug(message) {
322
+ this._checks.push({ name: 'slug', fn: v => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(v), message: message || 'Must be a valid slug' });
323
+ return this;
324
+ }
325
+
326
+ /** Non-empty (at least one non-whitespace character) */
327
+ nonempty(message) {
328
+ this._checks.push({ name: 'nonempty', fn: v => v.trim().length > 0, message: message || 'Must not be empty' });
329
+ return this;
330
+ }
331
+
332
+ _validateType(value, label) {
333
+ if (this._coerce) value = String(value);
334
+ if (typeof value !== 'string') {
335
+ return this._msg('type', `${label} must be a string`);
336
+ }
337
+ return null;
338
+ }
339
+
340
+ _validate(value, path = '') {
341
+ if (this._coerce && value !== undefined && value !== null) {
342
+ value = String(value);
343
+ }
344
+ return super._validate(value, path);
345
+ }
346
+ }
347
+
348
+ // ============================================
349
+ // NUMBER SCHEMA
350
+ // ============================================
351
+
352
+ class NumberSchema extends BaseSchema {
353
+ constructor() {
354
+ super();
355
+ this._coerce = false;
356
+ }
357
+
358
+ coerce() {
359
+ this._coerce = true;
360
+ return this;
361
+ }
362
+
363
+ min(n, message) {
364
+ this._checks.push({ name: 'min', fn: v => v >= n, message: message || `Must be at least ${n}` });
365
+ return this;
366
+ }
367
+
368
+ max(n, message) {
369
+ this._checks.push({ name: 'max', fn: v => v <= n, message: message || `Must be at most ${n}` });
370
+ return this;
371
+ }
372
+
373
+ int(message) {
374
+ this._checks.push({ name: 'int', fn: v => Number.isInteger(v), message: message || 'Must be an integer' });
375
+ return this;
376
+ }
377
+
378
+ positive(message) {
379
+ this._checks.push({ name: 'positive', fn: v => v > 0, message: message || 'Must be positive' });
380
+ return this;
381
+ }
382
+
383
+ negative(message) {
384
+ this._checks.push({ name: 'negative', fn: v => v < 0, message: message || 'Must be negative' });
385
+ return this;
386
+ }
387
+
388
+ nonnegative(message) {
389
+ this._checks.push({ name: 'nonnegative', fn: v => v >= 0, message: message || 'Must be non-negative' });
390
+ return this;
391
+ }
392
+
393
+ finite(message) {
394
+ this._checks.push({ name: 'finite', fn: v => Number.isFinite(v), message: message || 'Must be finite' });
395
+ return this;
396
+ }
397
+
398
+ multipleOf(n, message) {
399
+ this._checks.push({ name: 'multipleOf', fn: v => v % n === 0, message: message || `Must be a multiple of ${n}` });
400
+ return this;
401
+ }
402
+
403
+ port(message) {
404
+ return this.int().min(0).max(65535);
405
+ }
406
+
407
+ _validateType(value, label) {
408
+ if (typeof value !== 'number' || isNaN(value)) {
409
+ return this._msg('type', `${label} must be a number`);
410
+ }
411
+ return null;
412
+ }
413
+
414
+ _validate(value, path = '') {
415
+ if (this._coerce && value !== undefined && value !== null) {
416
+ const num = Number(value);
417
+ if (!isNaN(num)) value = num;
418
+ }
419
+ return super._validate(value, path);
420
+ }
421
+ }
422
+
423
+ // ============================================
424
+ // BOOLEAN SCHEMA
425
+ // ============================================
426
+
427
+ class BooleanSchema extends BaseSchema {
428
+ constructor() {
429
+ super();
430
+ this._coerce = false;
431
+ }
432
+
433
+ coerce() {
434
+ this._coerce = true;
435
+ return this;
436
+ }
437
+
438
+ _validateType(value, label) {
439
+ if (typeof value !== 'boolean') {
440
+ return this._msg('type', `${label} must be a boolean`);
441
+ }
442
+ return null;
443
+ }
444
+
445
+ _validate(value, path = '') {
446
+ if (this._coerce && value !== undefined && value !== null) {
447
+ if (value === 'true' || value === '1' || value === 1) value = true;
448
+ else if (value === 'false' || value === '0' || value === 0) value = false;
449
+ }
450
+ return super._validate(value, path);
451
+ }
452
+ }
453
+
454
+ // ============================================
455
+ // DATE SCHEMA
456
+ // ============================================
457
+
458
+ class DateSchema extends BaseSchema {
459
+ constructor() {
460
+ super();
461
+ this._coerce = false;
462
+ }
463
+
464
+ coerce() {
465
+ this._coerce = true;
466
+ return this;
467
+ }
468
+
469
+ min(date, message) {
470
+ const d = new Date(date);
471
+ this._checks.push({ name: 'min', fn: v => new Date(v) >= d, message: message || `Must be after ${d.toISOString()}` });
472
+ return this;
473
+ }
474
+
475
+ max(date, message) {
476
+ const d = new Date(date);
477
+ this._checks.push({ name: 'max', fn: v => new Date(v) <= d, message: message || `Must be before ${d.toISOString()}` });
478
+ return this;
479
+ }
480
+
481
+ past(message) {
482
+ this._checks.push({ name: 'past', fn: v => new Date(v) < new Date(), message: message || 'Must be in the past' });
483
+ return this;
484
+ }
485
+
486
+ future(message) {
487
+ this._checks.push({ name: 'future', fn: v => new Date(v) > new Date(), message: message || 'Must be in the future' });
488
+ return this;
489
+ }
490
+
491
+ _validateType(value, label) {
492
+ const d = value instanceof Date ? value : new Date(value);
493
+ if (isNaN(d.getTime())) {
494
+ return this._msg('type', `${label} must be a valid date`);
495
+ }
496
+ return null;
497
+ }
498
+
499
+ _validate(value, path = '') {
500
+ if (this._coerce && typeof value === 'string') {
501
+ const d = new Date(value);
502
+ if (!isNaN(d.getTime())) value = d;
503
+ }
504
+ return super._validate(value, path);
505
+ }
506
+ }
507
+
508
+ // ============================================
509
+ // ARRAY SCHEMA
510
+ // ============================================
511
+
512
+ class ArraySchema extends BaseSchema {
513
+ constructor(itemSchema = null) {
514
+ super();
515
+ this._itemSchema = itemSchema;
516
+ }
517
+
518
+ min(n, message) {
519
+ this._checks.push({ name: 'min', fn: v => v.length >= n, message: message || `Must have at least ${n} items` });
520
+ return this;
521
+ }
522
+
523
+ max(n, message) {
524
+ this._checks.push({ name: 'max', fn: v => v.length <= n, message: message || `Must have at most ${n} items` });
525
+ return this;
526
+ }
527
+
528
+ length(n, message) {
529
+ this._checks.push({ name: 'length', fn: v => v.length === n, message: message || `Must have exactly ${n} items` });
530
+ return this;
531
+ }
532
+
533
+ nonempty(message) {
534
+ return this.min(1, message || 'Must not be empty');
535
+ }
536
+
537
+ unique(message) {
538
+ this._checks.push({
539
+ name: 'unique',
540
+ fn: v => new Set(v.map(i => JSON.stringify(i))).size === v.length,
541
+ message: message || 'Must contain unique items',
542
+ });
543
+ return this;
544
+ }
545
+
546
+ _validateType(value, label) {
547
+ if (!Array.isArray(value)) {
548
+ return this._msg('type', `${label} must be an array`);
549
+ }
550
+ return null;
551
+ }
552
+
553
+ _validate(value, path = '') {
554
+ const baseResult = super._validate(value, path);
555
+ if (!baseResult.valid) return baseResult;
556
+ if (baseResult.value === undefined) return baseResult;
557
+
558
+ // Validate each item if item schema provided
559
+ if (this._itemSchema && Array.isArray(baseResult.value)) {
560
+ const errors = [];
561
+ const transformed = [];
562
+
563
+ for (let i = 0; i < baseResult.value.length; i++) {
564
+ const itemResult = this._itemSchema.validate(baseResult.value[i], `${path || 'Array'}[${i}]`);
565
+ if (!itemResult.valid) {
566
+ errors.push(...itemResult.errors.map(e => `[${i}]: ${e}`));
567
+ } else {
568
+ transformed.push(itemResult.value);
569
+ }
570
+ }
571
+
572
+ if (errors.length > 0) {
573
+ return { valid: false, errors };
574
+ }
575
+ return { valid: true, value: transformed };
576
+ }
577
+
578
+ return baseResult;
579
+ }
580
+ }
581
+
582
+ // ============================================
583
+ // OBJECT SCHEMA
584
+ // ============================================
585
+
586
+ class ObjectSchema extends BaseSchema {
587
+ constructor(shape = {}) {
588
+ super();
589
+ this._shape = shape;
590
+ this._strict = false;
591
+ this._passthrough = false;
592
+ }
593
+
594
+ /** Reject unknown keys */
595
+ strict() {
596
+ this._strict = true;
597
+ this._passthrough = false;
598
+ return this;
599
+ }
600
+
601
+ /** Allow and pass through unknown keys */
602
+ passthrough() {
603
+ this._passthrough = true;
604
+ this._strict = false;
605
+ return this;
606
+ }
607
+
608
+ /** Add or override schema for a key */
609
+ extend(shape) {
610
+ return new ObjectSchema({ ...this._shape, ...shape });
611
+ }
612
+
613
+ /** Pick specific keys (returns new schema) */
614
+ pick(keys) {
615
+ const newShape = {};
616
+ for (const key of keys) {
617
+ if (this._shape[key]) newShape[key] = this._shape[key];
618
+ }
619
+ return new ObjectSchema(newShape);
620
+ }
621
+
622
+ /** Omit specific keys (returns new schema) */
623
+ omit(keys) {
624
+ const keySet = new Set(keys);
625
+ const newShape = {};
626
+ for (const [key, schema] of Object.entries(this._shape)) {
627
+ if (!keySet.has(key)) newShape[key] = schema;
628
+ }
629
+ return new ObjectSchema(newShape);
630
+ }
631
+
632
+ /** Make all fields partial/optional (returns new schema) */
633
+ partial() {
634
+ const newShape = {};
635
+ for (const [key, schema] of Object.entries(this._shape)) {
636
+ // Clone-ish: create new schema that's optional
637
+ const clone = Object.create(Object.getPrototypeOf(schema));
638
+ Object.assign(clone, schema);
639
+ clone._required = false;
640
+ clone._optional = true;
641
+ newShape[key] = clone;
642
+ }
643
+ return new ObjectSchema(newShape);
644
+ }
645
+
646
+ /** Make all fields required (returns new schema) */
647
+ deepRequired() {
648
+ const newShape = {};
649
+ for (const [key, schema] of Object.entries(this._shape)) {
650
+ const clone = Object.create(Object.getPrototypeOf(schema));
651
+ Object.assign(clone, schema);
652
+ clone._required = true;
653
+ clone._optional = false;
654
+ newShape[key] = clone;
655
+ }
656
+ return new ObjectSchema(newShape);
657
+ }
658
+
659
+ /** Validate a single field from the schema */
660
+ validateField(field, value) {
661
+ const fieldSchema = this._shape[field];
662
+ if (!fieldSchema) return { valid: true, value };
663
+ return fieldSchema.validate(value, field);
664
+ }
665
+
666
+ _validateType(value, label) {
667
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
668
+ return this._msg('type', `${label} must be an object`);
669
+ }
670
+ return null;
671
+ }
672
+
673
+ _validate(value, path = '') {
674
+ const baseResult = super._validate(value, path);
675
+ if (!baseResult.valid) return baseResult;
676
+ if (baseResult.value === undefined) return baseResult;
677
+
678
+ const obj = baseResult.value;
679
+ const errors = {};
680
+ const result = {};
681
+
682
+ // Validate shape
683
+ for (const [key, schema] of Object.entries(this._shape)) {
684
+ const fieldResult = schema.validate(obj[key], key);
685
+ if (!fieldResult.valid) {
686
+ errors[key] = fieldResult.errors;
687
+ } else if (fieldResult.value !== undefined) {
688
+ result[key] = fieldResult.value;
689
+ }
690
+ }
691
+
692
+ // Handle unknown keys
693
+ if (this._strict) {
694
+ for (const key of Object.keys(obj)) {
695
+ if (!this._shape[key]) {
696
+ errors[key] = [`Unknown field: ${key}`];
697
+ }
698
+ }
699
+ } else if (this._passthrough) {
700
+ for (const key of Object.keys(obj)) {
701
+ if (!this._shape[key]) {
702
+ result[key] = obj[key];
703
+ }
704
+ }
705
+ }
706
+
707
+ if (Object.keys(errors).length > 0) {
708
+ return { valid: false, errors };
709
+ }
710
+ return { valid: true, value: result, data: result };
711
+ }
712
+ }
713
+
714
+ // ============================================
715
+ // ENUM SCHEMA
716
+ // ============================================
717
+
718
+ class EnumSchema extends BaseSchema {
719
+ constructor(values = []) {
720
+ super();
721
+ this._values = values;
722
+ }
723
+
724
+ _validateType(value, label) {
725
+ if (!this._values.includes(value)) {
726
+ return this._msg('type', `${label} must be one of: ${this._values.join(', ')}`);
727
+ }
728
+ return null;
729
+ }
730
+ }
731
+
732
+ // ============================================
733
+ // UNION SCHEMA (oneOf)
734
+ // ============================================
735
+
736
+ class UnionSchema extends BaseSchema {
737
+ constructor(schemas = []) {
738
+ super();
739
+ this._schemas = schemas;
740
+ }
741
+
742
+ _validate(value, path = '') {
743
+ // If any schema passes, succeed
744
+ for (const schema of this._schemas) {
745
+ const result = schema.validate(value, path);
746
+ if (result.valid) return result;
747
+ }
748
+ return {
749
+ valid: false,
750
+ errors: [`${this._getLabel(path)} does not match any of the allowed types`],
751
+ };
752
+ }
753
+ }
754
+
755
+ // ============================================
756
+ // LITERAL SCHEMA
757
+ // ============================================
758
+
759
+ class LiteralSchema extends BaseSchema {
760
+ constructor(expected) {
761
+ super();
762
+ this._expected = expected;
763
+ }
764
+
765
+ _validateType(value, label) {
766
+ if (value !== this._expected) {
767
+ return `${label} must be exactly ${JSON.stringify(this._expected)}`;
768
+ }
769
+ return null;
770
+ }
771
+ }
772
+
773
+ // ============================================
774
+ // ANY SCHEMA
775
+ // ============================================
776
+
777
+ class AnySchema extends BaseSchema {
778
+ _validateType() { return null; }
779
+ }
780
+
781
+ // ============================================
782
+ // TUPLE SCHEMA
783
+ // ============================================
784
+
785
+ class TupleSchema extends BaseSchema {
786
+ constructor(schemas = []) {
787
+ super();
788
+ this._schemas = schemas;
789
+ }
790
+
791
+ _validateType(value, label) {
792
+ if (!Array.isArray(value)) return `${label} must be a tuple (array)`;
793
+ if (value.length !== this._schemas.length) {
794
+ return `${label} must have exactly ${this._schemas.length} elements`;
795
+ }
796
+ return null;
797
+ }
798
+
799
+ _validate(value, path = '') {
800
+ const baseResult = super._validate(value, path);
801
+ if (!baseResult.valid) return baseResult;
802
+ if (baseResult.value === undefined) return baseResult;
803
+
804
+ const errors = [];
805
+ const result = [];
806
+
807
+ for (let i = 0; i < this._schemas.length; i++) {
808
+ const itemResult = this._schemas[i].validate(value[i], `${path || 'Tuple'}[${i}]`);
809
+ if (!itemResult.valid) {
810
+ errors.push(...itemResult.errors.map(e => `[${i}]: ${e}`));
811
+ } else {
812
+ result.push(itemResult.value);
813
+ }
814
+ }
815
+
816
+ if (errors.length > 0) return { valid: false, errors };
817
+ return { valid: true, value: result };
818
+ }
819
+ }
820
+
821
+ // ============================================
822
+ // RECORD SCHEMA (Map-like: { [key: string]: ValueSchema })
823
+ // ============================================
824
+
825
+ class RecordSchema extends BaseSchema {
826
+ constructor(keySchema, valueSchema) {
827
+ super();
828
+ this._keySchema = keySchema;
829
+ this._valueSchema = valueSchema;
830
+ }
831
+
832
+ _validateType(value, label) {
833
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
834
+ return `${label} must be an object`;
835
+ }
836
+ return null;
837
+ }
838
+
839
+ _validate(value, path = '') {
840
+ const baseResult = super._validate(value, path);
841
+ if (!baseResult.valid) return baseResult;
842
+ if (baseResult.value === undefined) return baseResult;
843
+
844
+ const errors = [];
845
+ const result = {};
846
+
847
+ for (const [k, v] of Object.entries(baseResult.value)) {
848
+ if (this._keySchema) {
849
+ const keyResult = this._keySchema.validate(k, `key(${k})`);
850
+ if (!keyResult.valid) {
851
+ errors.push(...keyResult.errors);
852
+ continue;
853
+ }
854
+ }
855
+ if (this._valueSchema) {
856
+ const valResult = this._valueSchema.validate(v, k);
857
+ if (!valResult.valid) {
858
+ errors.push(...valResult.errors.map(e => `${k}: ${e}`));
859
+ } else {
860
+ result[k] = valResult.value;
861
+ }
862
+ } else {
863
+ result[k] = v;
864
+ }
865
+ }
866
+
867
+ if (errors.length > 0) return { valid: false, errors };
868
+ return { valid: true, value: result };
869
+ }
870
+ }
871
+
872
+ // ============================================
873
+ // SCHEMA FACTORY
874
+ // ============================================
875
+
876
+ class Schema {
877
+ static string() { return new StringSchema(); }
878
+ static number() { return new NumberSchema(); }
879
+ static boolean() { return new BooleanSchema(); }
880
+ static date() { return new DateSchema(); }
881
+ static array(itemSchema) { return new ArraySchema(itemSchema); }
882
+ static object(shape) { return new ObjectSchema(shape); }
883
+ static enum(values) { return new EnumSchema(values); }
884
+ static union(schemas) { return new UnionSchema(schemas); }
885
+ static literal(value) { return new LiteralSchema(value); }
886
+ static any() { return new AnySchema(); }
887
+ static tuple(schemas) { return new TupleSchema(schemas); }
888
+ static record(keySchema, valueSchema) { return new RecordSchema(keySchema, valueSchema); }
889
+
890
+ // Convenience shortcuts
891
+ static email() { return new StringSchema().email().required(); }
892
+ static url() { return new StringSchema().url().required(); }
893
+ static uuid() { return new StringSchema().uuid().required(); }
894
+ static port() { return new NumberSchema().int().min(0).max(65535).required(); }
895
+ static id() { return new NumberSchema().int().positive().required(); }
896
+
897
+ /**
898
+ * Combine schemas with .and() — all must pass
899
+ *
900
+ * @example
901
+ * const schema = Schema.intersection(schemaA, schemaB);
902
+ */
903
+ static intersection(...schemas) {
904
+ return {
905
+ validate(value, path = '') {
906
+ let mergedResult = {};
907
+ for (const schema of schemas) {
908
+ const result = schema.validate(value, path);
909
+ if (!result.valid) return result;
910
+ if (result.value && typeof result.value === 'object') {
911
+ mergedResult = { ...mergedResult, ...result.value };
912
+ } else {
913
+ mergedResult = result.value;
914
+ }
915
+ }
916
+ return { valid: true, value: mergedResult };
917
+ },
918
+ parse(value) {
919
+ const result = this.validate(value);
920
+ if (!result.valid) {
921
+ const err = new Error('Schema validation failed');
922
+ err.errors = result.errors;
923
+ throw err;
924
+ }
925
+ return result.value;
926
+ },
927
+ };
928
+ }
929
+
930
+ /**
931
+ * Preprocess value before schema validation
932
+ *
933
+ * @example
934
+ * const schema = Schema.preprocess(
935
+ * (val) => typeof val === 'string' ? parseInt(val) : val,
936
+ * Schema.number().min(0)
937
+ * );
938
+ */
939
+ static preprocess(transform, schema) {
940
+ return {
941
+ validate(value, path = '') {
942
+ return schema.validate(transform(value), path);
943
+ },
944
+ parse(value) {
945
+ return schema.parse(transform(value));
946
+ },
947
+ };
948
+ }
949
+
950
+ /**
951
+ * Lazy schema for recursive types
952
+ *
953
+ * @example
954
+ * const categorySchema = Schema.object({
955
+ * name: Schema.string(),
956
+ * children: Schema.lazy(() => Schema.array(categorySchema)),
957
+ * });
958
+ */
959
+ static lazy(fn) {
960
+ return {
961
+ validate(value, path = '') {
962
+ return fn().validate(value, path);
963
+ },
964
+ parse(value) {
965
+ return fn().parse(value);
966
+ },
967
+ };
968
+ }
969
+ }
970
+
971
+ module.exports = {
972
+ Schema,
973
+ StringSchema,
974
+ NumberSchema,
975
+ BooleanSchema,
976
+ DateSchema,
977
+ ArraySchema,
978
+ ObjectSchema,
979
+ EnumSchema,
980
+ UnionSchema,
981
+ LiteralSchema,
982
+ AnySchema,
983
+ TupleSchema,
984
+ RecordSchema,
985
+ };