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/README.md +76 -51
- package/package.json +12 -6
- package/src/core/BaseValidator.js +594 -0
- package/src/core/ValidationResult.js +19 -0
- package/src/index.js +10 -950
- package/src/schema/runner.js +32 -0
- package/src/schema/validate.js +78 -0
- package/src/utils/safeRegex.js +72 -0
- package/src/validators/alphanumeric.js +9 -0
- package/src/validators/creditCard.js +49 -0
- package/src/validators/email.js +11 -0
- package/src/validators/index.js +19 -0
- package/src/validators/numeric.js +9 -0
- package/src/validators/password.js +41 -0
- package/src/validators/phone.js +24 -0
- package/src/validators/url.js +10 -0
- package/src/validators/zipCode.js +15 -0
- package/types/index.d.ts +85 -7
package/src/index.js
CHANGED
|
@@ -1,957 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Snap Validate - Enhanced Lightweight validator library
|
|
3
|
-
* @version 0.4.
|
|
3
|
+
* @version 0.4.3 - Security Fixes and Modularisation
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,
|