snap-validate 0.4.2 → 0.4.3

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/src/index.js CHANGED
@@ -1,957 +1,17 @@
1
1
  /**
2
2
  * Snap Validate - Enhanced Lightweight validator library
3
- * @version 0.4.0 - Phase 1: Arrays, Nested Objects, Transforms
3
+ * @version 0.4.3 - Security Fixes and Modularisation
4
4
  */
5
5
 
6
- // Utility function to safely test regex with timeout protection
7
- const safeRegexTest = (regex, str, timeoutMs = 1000) => {
8
- return new Promise((resolve, reject) => {
9
- if (str.length > 10000) {
10
- reject(new Error('Input too long for regex validation'));
11
- return;
12
- }
13
-
14
- if (!isRegexSafe(regex)) {
15
- reject(new Error('Unsafe regex pattern detected'));
16
- return;
17
- }
18
-
19
- const timeout = setTimeout(() => {
20
- reject(new Error('Regex execution timeout - potential ReDoS attack'));
21
- }, timeoutMs);
22
-
23
- try {
24
- const result = regex.test(str);
25
- clearTimeout(timeout);
26
- resolve(result);
27
- } catch (error) {
28
- clearTimeout(timeout);
29
- reject(error);
30
- }
31
- });
32
- };
33
-
34
- // Synchronous safe regex test with input length protection
35
- const safeRegexTestSync = (regex, str, maxLength = 10000) => {
36
- if (str.length > maxLength) {
37
- throw new Error('Input too long for pattern validation');
38
- }
39
- return regex.test(str);
40
- };
41
-
42
- // Function to detect potentially dangerous regex patterns
43
- const isRegexSafe = (regex) => {
44
- const regexStr = regex.toString();
45
-
46
- const dangerousPatterns = [
47
- /\([^)]*[+*?][^)]*\)[+*?]/,
48
- /\([^)]*\|[^)]*\)[+*]/,
49
- /\([^)]*\.\*[^)]*\)\*/,
50
- /[+*?]{2,}/,
51
- /\([^)]*\|[^)]*\)\+.*\([^)]*\|[^)]*\)\+/
52
- ];
53
-
54
- const isDangerous = dangerousPatterns.some((pattern) =>
55
- pattern.test(regexStr)
56
- );
57
-
58
- return !isDangerous;
59
- };
60
-
61
- // Core validation class
62
- class ValidationResult {
63
- constructor(isValid, errors = []) {
64
- this.isValid = isValid;
65
- this.errors = errors;
66
- }
67
-
68
- addError(message) {
69
- this.errors.push(message);
70
- this.isValid = false;
71
- return this;
72
- }
73
- }
74
-
75
- // Base validator class
76
- class BaseValidator {
77
- constructor(value) {
78
- this.value = value;
79
- this.rules = [];
80
- this.asyncRules = [];
81
- this.isOptional = false;
82
- this.regexTimeout = 1000;
83
- this.fieldName = null; // Track field name for better error messages
84
- }
85
-
86
- // Set field name for contextual error messages
87
- setFieldName(name) {
88
- this.fieldName = name;
89
- return this;
90
- }
91
-
92
- // Helper to format error messages with field context
93
- _formatError(message) {
94
- if (
95
- this.fieldName &&
96
- !message.toLowerCase().includes(this.fieldName.toLowerCase())
97
- ) {
98
- return `${this.fieldName}: ${message}`;
99
- }
100
- return message;
101
- }
102
-
103
- required(message = 'This field is required') {
104
- this.rules.push(() => {
105
- if (
106
- this.isOptional &&
107
- (this.value === null || this.value === undefined || this.value === '')
108
- ) {
109
- return new ValidationResult(true);
110
- }
111
-
112
- if (
113
- this.value === null ||
114
- this.value === undefined ||
115
- this.value === ''
116
- ) {
117
- return new ValidationResult(false, [this._formatError(message)]);
118
- }
119
- return new ValidationResult(true);
120
- });
121
- return this;
122
- }
123
-
124
- optional() {
125
- this.isOptional = true;
126
- return this;
127
- }
128
-
129
- setRegexTimeout(timeoutMs) {
130
- this.regexTimeout = timeoutMs;
131
- return this;
132
- }
133
-
134
- // Transform/sanitize data before validation
135
- transform(fn, errorMessage = 'Transform function failed') {
136
- this.rules.push(() => {
137
- if (this.value != null && this.value !== '') {
138
- try {
139
- this.value = fn(this.value);
140
- } catch (error) {
141
- return new ValidationResult(false, [
142
- this._formatError(`${errorMessage}: ${error.message}`)
143
- ]);
144
- }
145
- }
146
- return new ValidationResult(true);
147
- });
148
- return this;
149
- }
150
-
151
- // Check if value equals another value
152
- equals(compareValue, message) {
153
- const defaultMessage = `Must equal ${compareValue}`;
154
- this.rules.push(() => {
155
- if (
156
- this.isOptional &&
157
- (this.value === null || this.value === undefined || this.value === '')
158
- ) {
159
- return new ValidationResult(true);
160
- }
161
-
162
- if (this.value !== compareValue) {
163
- return new ValidationResult(false, [
164
- this._formatError(message || defaultMessage)
165
- ]);
166
- }
167
- return new ValidationResult(true);
168
- });
169
- return this;
170
- }
171
-
172
- // Check if value is one of allowed values
173
- oneOf(allowedValues, message) {
174
- const defaultMessage = `Must be one of: ${allowedValues.join(', ')}`;
175
- this.rules.push(() => {
176
- if (
177
- this.isOptional &&
178
- (this.value === null || this.value === undefined || this.value === '')
179
- ) {
180
- return new ValidationResult(true);
181
- }
182
-
183
- if (!allowedValues.includes(this.value)) {
184
- return new ValidationResult(false, [
185
- this._formatError(message || defaultMessage)
186
- ]);
187
- }
188
- return new ValidationResult(true);
189
- });
190
- return this;
191
- }
192
-
193
- // Check if value is between min and max (for numbers)
194
- between(min, max, message) {
195
- const defaultMessage = `Must be between ${min} and ${max}`;
196
- this.rules.push(() => {
197
- if (
198
- this.isOptional &&
199
- (this.value === null || this.value === undefined || this.value === '')
200
- ) {
201
- return new ValidationResult(true);
202
- }
203
-
204
- const numValue =
205
- typeof this.value === 'number' ? this.value : parseFloat(this.value);
206
-
207
- if (isNaN(numValue) || numValue < min || numValue > max) {
208
- return new ValidationResult(false, [
209
- this._formatError(message || defaultMessage)
210
- ]);
211
- }
212
- return new ValidationResult(true);
213
- });
214
- return this;
215
- }
216
-
217
- min(length, message = `Minimum length is ${length}`) {
218
- this.rules.push(() => {
219
- if (
220
- this.isOptional &&
221
- (this.value === null || this.value === undefined || this.value === '')
222
- ) {
223
- return new ValidationResult(true);
224
- }
225
-
226
- if (this.value != null && this.value !== '') {
227
- if (typeof this.value === 'string' || Array.isArray(this.value)) {
228
- if (this.value.length < length) {
229
- return new ValidationResult(false, [this._formatError(message)]);
230
- }
231
- } else if (typeof this.value === 'number') {
232
- if (this.value < length) {
233
- return new ValidationResult(false, [this._formatError(message)]);
234
- }
235
- } else {
236
- return new ValidationResult(false, [
237
- this._formatError('Value must be a string, array, or number')
238
- ]);
239
- }
240
- }
241
- return new ValidationResult(true);
242
- });
243
- return this;
244
- }
245
-
246
- max(length, message = `Maximum length is ${length}`) {
247
- this.rules.push(() => {
248
- if (
249
- this.isOptional &&
250
- (this.value === null || this.value === undefined || this.value === '')
251
- ) {
252
- return new ValidationResult(true);
253
- }
254
-
255
- if (this.value != null && this.value !== '') {
256
- if (typeof this.value === 'string' || Array.isArray(this.value)) {
257
- if (this.value.length > length) {
258
- return new ValidationResult(false, [this._formatError(message)]);
259
- }
260
- } else if (typeof this.value === 'number') {
261
- if (this.value > length) {
262
- return new ValidationResult(false, [this._formatError(message)]);
263
- }
264
- } else {
265
- return new ValidationResult(false, [
266
- this._formatError('Value must be a string, array or number')
267
- ]);
268
- }
269
- }
270
- return new ValidationResult(true);
271
- });
272
- return this;
273
- }
274
-
275
- // Validate that value is an array
276
- array(message = 'Must be an array') {
277
- this.rules.push(() => {
278
- if (
279
- this.isOptional &&
280
- (this.value === null || this.value === undefined)
281
- ) {
282
- return new ValidationResult(true);
283
- }
284
-
285
- if (!Array.isArray(this.value)) {
286
- return new ValidationResult(false, [this._formatError(message)]);
287
- }
288
- return new ValidationResult(true);
289
- });
290
- return this;
291
- }
292
-
293
- // Validate each item in an array
294
- arrayOf(validator, message = 'Invalid array items') {
295
- this.rules.push(() => {
296
- if (
297
- this.isOptional &&
298
- (this.value === null || this.value === undefined)
299
- ) {
300
- return new ValidationResult(true);
301
- }
302
-
303
- if (!Array.isArray(this.value)) {
304
- return new ValidationResult(false, [
305
- this._formatError('Value must be an array')
306
- ]);
307
- }
308
-
309
- const errors = [];
310
- this.value.forEach((item, index) => {
311
- try {
312
- const itemValidator =
313
- typeof validator === 'function' ? validator(item) : validator;
314
- const result = itemValidator.validate();
315
-
316
- if (!result.isValid) {
317
- errors.push(`[${index}]: ${result.errors.join(', ')}`);
318
- }
319
- } catch (error) {
320
- errors.push(`[${index}]: Validation error - ${error.message}`);
321
- }
322
- });
323
-
324
- if (errors.length > 0) {
325
- return new ValidationResult(false, [
326
- this._formatError(`${message}: ${errors.join('; ')}`)
327
- ]);
328
- }
329
-
330
- return new ValidationResult(true);
331
- });
332
- return this;
333
- }
334
-
335
- // Async array validation
336
- arrayOfAsync(validator, message = 'Invalid array items') {
337
- this.asyncRules.push(async () => {
338
- if (
339
- this.isOptional &&
340
- (this.value === null || this.value === undefined)
341
- ) {
342
- return new ValidationResult(true);
343
- }
344
-
345
- if (!Array.isArray(this.value)) {
346
- return new ValidationResult(false, [
347
- this._formatError('Value must be an array')
348
- ]);
349
- }
350
-
351
- const errors = [];
352
- for (let index = 0; index < this.value.length; index++) {
353
- try {
354
- const item = this.value[index];
355
- const itemValidator =
356
- typeof validator === 'function' ? validator(item) : validator;
357
-
358
- const result =
359
- itemValidator.asyncRules && itemValidator.asyncRules.length > 0
360
- ? await itemValidator.validateAsync()
361
- : itemValidator.validate();
362
-
363
- if (!result.isValid) {
364
- errors.push(`[${index}]: ${result.errors.join(', ')}`);
365
- }
366
- } catch (error) {
367
- errors.push(`[${index}]: Validation error - ${error.message}`);
368
- }
369
- }
370
-
371
- if (errors.length > 0) {
372
- return new ValidationResult(false, [
373
- this._formatError(`${message}: ${errors.join('; ')}`)
374
- ]);
375
- }
376
-
377
- return new ValidationResult(true);
378
- });
379
- return this;
380
- }
381
-
382
- // Validate nested objects
383
- object(schema, message = 'Invalid object structure') {
384
- this.rules.push(() => {
385
- if (
386
- this.isOptional &&
387
- (this.value === null || this.value === undefined)
388
- ) {
389
- return new ValidationResult(true);
390
- }
391
-
392
- if (
393
- typeof this.value !== 'object' ||
394
- this.value === null ||
395
- Array.isArray(this.value)
396
- ) {
397
- return new ValidationResult(false, [this._formatError(message)]);
398
- }
399
-
400
- try {
401
- const result = validate(schema, this.value);
402
-
403
- if (!result.isValid) {
404
- const fieldErrors = result.getErrors();
405
- const errorMessages = Object.entries(fieldErrors)
406
- .map(([field, errs]) => `${field}: ${errs.join(', ')}`)
407
- .join('; ');
408
-
409
- return new ValidationResult(false, [
410
- this._formatError(`Object validation failed - ${errorMessages}`)
411
- ]);
412
- }
413
-
414
- return new ValidationResult(true);
415
- } catch (error) {
416
- return new ValidationResult(false, [
417
- this._formatError(`Object validation error: ${error.message}`)
418
- ]);
419
- }
420
- });
421
- return this;
422
- }
423
-
424
- // Async nested object validation
425
- objectAsync(schema, message = 'Invalid object structure') {
426
- this.asyncRules.push(async () => {
427
- if (
428
- this.isOptional &&
429
- (this.value === null || this.value === undefined)
430
- ) {
431
- return new ValidationResult(true);
432
- }
433
-
434
- if (
435
- typeof this.value !== 'object' ||
436
- this.value === null ||
437
- Array.isArray(this.value)
438
- ) {
439
- return new ValidationResult(false, [this._formatError(message)]);
440
- }
441
-
442
- try {
443
- const result = await validateAsync(schema, this.value);
444
-
445
- if (!result.isValid) {
446
- const fieldErrors = result.getErrors();
447
- const errorMessages = Object.entries(fieldErrors)
448
- .map(([field, errs]) => `${field}: ${errs.join(', ')}`)
449
- .join('; ');
450
-
451
- return new ValidationResult(false, [
452
- this._formatError(`Object validation failed - ${errorMessages}`)
453
- ]);
454
- }
455
-
456
- return new ValidationResult(true);
457
- } catch (error) {
458
- return new ValidationResult(false, [
459
- this._formatError(`Object validation error: ${error.message}`)
460
- ]);
461
- }
462
- });
463
- return this;
464
- }
465
-
466
- pattern(regex, message = 'Invalid format') {
467
- if (!isRegexSafe(regex)) {
468
- throw new Error(
469
- 'Potentially unsafe regex pattern detected. Please use a simple pattern.'
470
- );
471
- }
472
-
473
- this.rules.push(() => {
474
- if (
475
- this.isOptional &&
476
- (this.value === null || this.value === undefined || this.value === '')
477
- ) {
478
- return new ValidationResult(true);
479
- }
480
-
481
- if (this.value != null && this.value !== '') {
482
- const stringValue = String(this.value);
483
-
484
- try {
485
- if (!safeRegexTestSync(regex, stringValue)) {
486
- return new ValidationResult(false, [this._formatError(message)]);
487
- }
488
- } catch (error) {
489
- if (error.message.includes('Input too long')) {
490
- return new ValidationResult(false, [
491
- this._formatError('Input too long for pattern validation')
492
- ]);
493
- }
494
- return new ValidationResult(false, [
495
- this._formatError('Pattern validation failed')
496
- ]);
497
- }
498
- }
499
- return new ValidationResult(true);
500
- });
501
- return this;
502
- }
503
-
504
- patternAsync(regex, message = 'Invalid format') {
505
- if (!isRegexSafe(regex)) {
506
- throw new Error(
507
- 'Potentially unsafe regex pattern detected. Please use a simple pattern.'
508
- );
509
- }
510
-
511
- this.asyncRules.push(async () => {
512
- if (
513
- this.isOptional &&
514
- (this.value === null || this.value === undefined || this.value === '')
515
- ) {
516
- return new ValidationResult(true);
517
- }
518
-
519
- if (this.value != null && this.value !== '') {
520
- const stringValue = String(this.value);
521
-
522
- if (stringValue.length > 10000) {
523
- return new ValidationResult(false, [
524
- this._formatError('Input too long for pattern validation')
525
- ]);
526
- }
527
-
528
- try {
529
- const result = await safeRegexTest(
530
- regex,
531
- stringValue,
532
- this.regexTimeout
533
- );
534
- if (!result) {
535
- return new ValidationResult(false, [this._formatError(message)]);
536
- }
537
- } catch (error) {
538
- if (error.message.includes('timeout')) {
539
- return new ValidationResult(false, [
540
- this._formatError(
541
- 'Pattern validation timeout - pattern too complex'
542
- )
543
- ]);
544
- }
545
- return new ValidationResult(false, [
546
- this._formatError('Pattern validation failed')
547
- ]);
548
- }
549
- }
550
- return new ValidationResult(true);
551
- });
552
- return this;
553
- }
554
-
555
- when(condition, validator) {
556
- this.rules.push(() => {
557
- const shouldValidate =
558
- typeof condition === 'function' ? condition(this.value) : condition;
559
-
560
- if (shouldValidate) {
561
- if (typeof validator === 'function') {
562
- const conditionalValidator = validator(this.value);
563
- return conditionalValidator.validate();
564
- } else {
565
- return validator.validate();
566
- }
567
- }
568
-
569
- return new ValidationResult(true);
570
- });
571
- return this;
572
- }
573
-
574
- custom(validatorFn, message = 'Custom validation failed') {
575
- this.rules.push(() => {
576
- if (
577
- this.isOptional &&
578
- (this.value === null || this.value === undefined || this.value === '')
579
- ) {
580
- return new ValidationResult(true);
581
- }
582
-
583
- try {
584
- const result = validatorFn(this.value);
585
-
586
- if (typeof result === 'boolean') {
587
- return result
588
- ? new ValidationResult(true)
589
- : new ValidationResult(false, [this._formatError(message)]);
590
- }
591
-
592
- if (result && typeof result === 'object' && 'isValid' in result) {
593
- return result;
594
- }
595
-
596
- if (typeof result === 'string') {
597
- return new ValidationResult(false, [this._formatError(result)]);
598
- }
599
-
600
- return new ValidationResult(true);
601
- } catch (error) {
602
- return new ValidationResult(false, [
603
- this._formatError(`Custom validation error: ${error.message}`)
604
- ]);
605
- }
606
- });
607
- return this;
608
- }
609
-
610
- customAsync(validatorFn, message = 'Async validation failed') {
611
- this.asyncRules.push(async () => {
612
- if (
613
- this.isOptional &&
614
- (this.value === null || this.value === undefined || this.value === '')
615
- ) {
616
- return new ValidationResult(true);
617
- }
618
-
619
- try {
620
- const result = await validatorFn(this.value);
621
-
622
- if (typeof result === 'boolean') {
623
- return result
624
- ? new ValidationResult(true)
625
- : new ValidationResult(false, [this._formatError(message)]);
626
- }
627
-
628
- if (result && typeof result === 'object' && 'isValid' in result) {
629
- return result;
630
- }
631
-
632
- if (typeof result === 'string') {
633
- return new ValidationResult(false, [this._formatError(result)]);
634
- }
635
-
636
- return new ValidationResult(true);
637
- } catch (error) {
638
- return new ValidationResult(false, [
639
- this._formatError(`Async validation error: ${error.message}`)
640
- ]);
641
- }
642
- });
643
- return this;
644
- }
645
-
646
- validate() {
647
- const result = new ValidationResult(true);
648
-
649
- for (const rule of this.rules) {
650
- try {
651
- const ruleResult = rule();
652
- if (!ruleResult.isValid) {
653
- result.isValid = false;
654
- result.errors.push(...ruleResult.errors);
655
- }
656
- } catch (error) {
657
- result.isValid = false;
658
- result.errors.push(
659
- this._formatError(`Validation error: ${error.message}`)
660
- );
661
- }
662
- }
663
-
664
- return result;
665
- }
666
-
667
- async validateAsync() {
668
- const syncResult = this.validate();
669
-
670
- if (!syncResult.isValid) {
671
- return syncResult;
672
- }
673
-
674
- const result = new ValidationResult(true, [...syncResult.errors]);
675
-
676
- for (const asyncRule of this.asyncRules) {
677
- try {
678
- const ruleResult = await asyncRule();
679
- if (!ruleResult.isValid) {
680
- result.isValid = false;
681
- result.errors.push(...ruleResult.errors);
682
- }
683
- } catch (error) {
684
- result.isValid = false;
685
- result.errors.push(
686
- this._formatError(`Async validation error: ${error.message}`)
687
- );
688
- }
689
- }
690
-
691
- return result;
692
- }
693
- }
694
-
695
- // Predefined validators
696
- const validators = {
697
- email: (value) => {
698
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
699
- return new BaseValidator(value)
700
- .transform((v) => (typeof v === 'string' ? v.trim().toLowerCase() : v))
701
- .required('Email is required')
702
- .pattern(emailRegex, 'Invalid email format');
703
- },
704
-
705
- phone: (value, format = 'us') => {
706
- const phoneRegex = {
707
- us: /^[+]?[1]?[0-9]{10}$/,
708
- international: /^[+][1-9][0-9]{7,14}$/,
709
- simple: /^[0-9]{10,15}$/
710
- };
711
-
712
- return new BaseValidator(value)
713
- .required('Phone number is required')
714
- .custom((val) => {
715
- const cleaned = String(val).replace(/[^+0-9]/g, '');
716
- const regex = phoneRegex[format] || phoneRegex.simple;
717
-
718
- if (!safeRegexTestSync(regex, cleaned)) {
719
- return 'Invalid phone number format';
720
- }
721
- return true;
722
- });
723
- },
724
-
725
- creditCard: (value) => {
726
- const luhnCheck = (num) => {
727
- let sum = 0;
728
- let isEven = false;
729
-
730
- const cleanNum = String(num).replace(/\s/g, '');
731
-
732
- for (let i = cleanNum.length - 1; i >= 0; i--) {
733
- let digit = parseInt(cleanNum[i]);
734
-
735
- if (isEven) {
736
- digit *= 2;
737
- if (digit > 9) digit -= 9;
738
- }
739
-
740
- sum += digit;
741
- isEven = !isEven;
742
- }
743
-
744
- return sum % 10 === 0;
745
- };
746
-
747
- const validator = new BaseValidator(value).required(
748
- 'Credit card number is required'
749
- );
750
-
751
- validator.rules.push(() => {
752
- if (
753
- validator.isOptional &&
754
- (validator.value === null ||
755
- validator.value === undefined ||
756
- validator.value === '')
757
- ) {
758
- return new ValidationResult(true);
759
- }
760
-
761
- if (validator.value) {
762
- const cleanValue = String(validator.value).replace(/\s/g, '');
763
-
764
- if (!safeRegexTestSync(/^\d{13,19}$/, cleanValue)) {
765
- return new ValidationResult(false, [
766
- 'Credit card must be 13-19 digits'
767
- ]);
768
- }
769
-
770
- if (!luhnCheck(cleanValue)) {
771
- return new ValidationResult(false, ['Invalid credit card number']);
772
- }
773
- }
774
- return new ValidationResult(true);
775
- });
776
-
777
- return validator;
778
- },
779
-
780
- url: (value) => {
781
- const urlRegex = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i;
782
- return new BaseValidator(value)
783
- .required('URL is required')
784
- .pattern(urlRegex, 'Invalid URL format');
785
- },
786
-
787
- password: (value, options = {}) => {
788
- const {
789
- minLength = 8,
790
- requireUppercase = true,
791
- requireLowercase = true,
792
- requireNumbers = true,
793
- requireSpecialChars = false
794
- } = options;
795
-
796
- const validator = new BaseValidator(value)
797
- .required('Password is required')
798
- .min(minLength, `Password must be at least ${minLength} characters`);
799
-
800
- if (requireUppercase) {
801
- validator.pattern(
802
- /[A-Z]/,
803
- 'Password must contain at least one uppercase letter'
804
- );
805
- }
806
- if (requireLowercase) {
807
- validator.pattern(
808
- /[a-z]/,
809
- 'Password must contain at least one lowercase letter'
810
- );
811
- }
812
- if (requireNumbers) {
813
- validator.pattern(/\d/, 'Password must contain at least one number');
814
- }
815
- if (requireSpecialChars) {
816
- validator.pattern(
817
- /[!@#$%^&*(),.?":{}|<>]/,
818
- 'Password must contain at least one special character'
819
- );
820
- }
821
-
822
- return validator;
823
- },
824
-
825
- alphanumeric: (value) => {
826
- return new BaseValidator(value)
827
- .required('This field is required')
828
- .pattern(/^[a-zA-Z0-9]+$/, 'Only letters and numbers are allowed');
829
- },
830
-
831
- numeric: (value) => {
832
- return new BaseValidator(value)
833
- .required('This field is required')
834
- .pattern(/^\d+$/, 'Only numbers are allowed');
835
- },
836
-
837
- zipCode: (value, country = 'us') => {
838
- const zipRegex = {
839
- us: /^\d{5}(-\d{4})?$/,
840
- ca: /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/,
841
- uk: /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i
842
- };
843
-
844
- return new BaseValidator(value)
845
- .required('Zip code is required')
846
- .pattern(zipRegex[country] || zipRegex.us, 'Invalid zip code format');
847
- }
848
- };
849
-
850
- // Main validation function
851
- const validate = (schema, data) => {
852
- if (!schema || typeof schema !== 'object') {
853
- throw new Error('Schema must be a valid object');
854
- }
855
-
856
- if (!data || typeof data !== 'object') {
857
- throw new Error('Data must be a valid object');
858
- }
859
-
860
- const results = {};
861
- let isValid = true;
862
-
863
- for (const [field, validator] of Object.entries(schema)) {
864
- try {
865
- const fieldValue = data[field];
866
- const validatorInstance =
867
- typeof validator === 'function' ? validator(fieldValue) : validator;
868
-
869
- // Set field name for better error context
870
- validatorInstance.setFieldName(field);
871
-
872
- const result = validatorInstance.validate();
873
-
874
- results[field] = result;
875
- if (!result.isValid) {
876
- isValid = false;
877
- }
878
- } catch (error) {
879
- results[field] = new ValidationResult(false, [
880
- `${field}: Validation setup error - ${error.message}`
881
- ]);
882
- isValid = false;
883
- }
884
- }
885
-
886
- return {
887
- isValid,
888
- errors: results,
889
- getErrors: () => {
890
- const errors = {};
891
- for (const [field, result] of Object.entries(results)) {
892
- if (!result.isValid) {
893
- errors[field] = result.errors;
894
- }
895
- }
896
- return errors;
897
- }
898
- };
899
- };
900
-
901
- // Async validation function
902
- const validateAsync = async (schema, data) => {
903
- if (!schema || typeof schema !== 'object') {
904
- throw new Error('Schema must be a valid object');
905
- }
906
-
907
- if (!data || typeof data !== 'object') {
908
- throw new Error('Data must be a valid object');
909
- }
910
-
911
- const results = {};
912
- let isValid = true;
913
-
914
- for (const [field, validator] of Object.entries(schema)) {
915
- try {
916
- const fieldValue = data[field];
917
-
918
- const validatorInstance =
919
- typeof validator === 'function' ? validator(fieldValue) : validator;
920
-
921
- // Set field name for better error context
922
- validatorInstance.setFieldName(field);
923
-
924
- const result =
925
- validatorInstance.asyncRules && validatorInstance.asyncRules.length > 0
926
- ? await validatorInstance.validateAsync()
927
- : validatorInstance.validate();
928
-
929
- results[field] = result;
930
- if (!result.isValid) {
931
- isValid = false;
932
- }
933
- } catch (error) {
934
- results[field] = new ValidationResult(false, [
935
- `${field}: Validation setup error - ${error.message}`
936
- ]);
937
- isValid = false;
938
- }
939
- }
940
-
941
- return {
942
- isValid,
943
- errors: results,
944
- getErrors: () => {
945
- const errors = {};
946
- for (const [field, result] of Object.entries(results)) {
947
- if (!result.isValid) {
948
- errors[field] = result.errors;
949
- }
950
- }
951
- return errors;
952
- }
953
- };
954
- };
6
+ const { BaseValidator } = require('./core/BaseValidator');
7
+ const { ValidationResult } = require('./core/ValidationResult');
8
+ const validators = require('./validators');
9
+ const { validate, validateAsync } = require('./schema/validate');
10
+ const {
11
+ safeRegexTest,
12
+ safeRegexTestSync,
13
+ isRegexSafe
14
+ } = require('./utils/safeRegex');
955
15
 
956
16
  module.exports = {
957
17
  BaseValidator,