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
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snap Validate - BaseValidator
|
|
3
|
+
* The chainable validator. Each method pushes a rule (sync) or async rule
|
|
4
|
+
* onto the instance and returns `this` for fluent chaining.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { ValidationResult } = require('./ValidationResult');
|
|
8
|
+
const {
|
|
9
|
+
isRegexSafe,
|
|
10
|
+
safeRegexTest,
|
|
11
|
+
safeRegexTestSync
|
|
12
|
+
} = require('../utils/safeRegex');
|
|
13
|
+
const { validate, validateAsync } = require('../schema/validate');
|
|
14
|
+
|
|
15
|
+
class BaseValidator {
|
|
16
|
+
constructor(value) {
|
|
17
|
+
this.value = value;
|
|
18
|
+
this.rules = [];
|
|
19
|
+
this.asyncRules = [];
|
|
20
|
+
this.isOptional = false;
|
|
21
|
+
// Deprecated no-op: a timer cannot interrupt a synchronous regex, so this
|
|
22
|
+
// value is no longer consulted. Retained only for backward compatibility.
|
|
23
|
+
this.regexTimeout = 1000;
|
|
24
|
+
this.fieldName = null; // Track field name for better error messages
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Set field name for contextual error messages
|
|
28
|
+
setFieldName(name) {
|
|
29
|
+
this.fieldName = name;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Helper to format error messages with field context
|
|
34
|
+
_formatError(message) {
|
|
35
|
+
if (
|
|
36
|
+
this.fieldName &&
|
|
37
|
+
!message.toLowerCase().includes(this.fieldName.toLowerCase())
|
|
38
|
+
) {
|
|
39
|
+
return `${this.fieldName}: ${message}`;
|
|
40
|
+
}
|
|
41
|
+
return message;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Returns true when the field is optional and effectively empty, so a rule
|
|
45
|
+
// can short-circuit as valid. Pass `false` to NOT treat '' as empty
|
|
46
|
+
// (array/object rules, where an empty string is a real, invalid value).
|
|
47
|
+
_shouldSkipOptional(emptyStringCounts = true) {
|
|
48
|
+
if (!this.isOptional) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const v = this.value;
|
|
52
|
+
return v === null || v === undefined || (emptyStringCounts && v === '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
required(message = 'This field is required') {
|
|
56
|
+
this.rules.push(() => {
|
|
57
|
+
if (this._shouldSkipOptional()) {
|
|
58
|
+
return new ValidationResult(true);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
this.value === null ||
|
|
63
|
+
this.value === undefined ||
|
|
64
|
+
this.value === ''
|
|
65
|
+
) {
|
|
66
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
67
|
+
}
|
|
68
|
+
return new ValidationResult(true);
|
|
69
|
+
});
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
optional() {
|
|
74
|
+
this.isOptional = true;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Deprecated: retained for backward compatibility and chainability. Regex
|
|
79
|
+
// execution cannot be interrupted by a timeout on a single thread, so this
|
|
80
|
+
// value is no longer used. Safe to remove in a future major version.
|
|
81
|
+
setRegexTimeout(timeoutMs) {
|
|
82
|
+
this.regexTimeout = timeoutMs;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Transform/sanitize data before validation
|
|
87
|
+
transform(fn, errorMessage = 'Transform function failed') {
|
|
88
|
+
this.rules.push(() => {
|
|
89
|
+
if (this.value != null && this.value !== '') {
|
|
90
|
+
try {
|
|
91
|
+
this.value = fn(this.value);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return new ValidationResult(false, [
|
|
94
|
+
this._formatError(`${errorMessage}: ${error.message}`)
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return new ValidationResult(true);
|
|
99
|
+
});
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if value equals another value
|
|
104
|
+
equals(compareValue, message) {
|
|
105
|
+
const defaultMessage = `Must equal ${compareValue}`;
|
|
106
|
+
this.rules.push(() => {
|
|
107
|
+
if (this._shouldSkipOptional()) {
|
|
108
|
+
return new ValidationResult(true);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.value !== compareValue) {
|
|
112
|
+
return new ValidationResult(false, [
|
|
113
|
+
this._formatError(message || defaultMessage)
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
return new ValidationResult(true);
|
|
117
|
+
});
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if value is one of allowed values
|
|
122
|
+
oneOf(allowedValues, message) {
|
|
123
|
+
const defaultMessage = `Must be one of: ${allowedValues.join(', ')}`;
|
|
124
|
+
this.rules.push(() => {
|
|
125
|
+
if (this._shouldSkipOptional()) {
|
|
126
|
+
return new ValidationResult(true);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!allowedValues.includes(this.value)) {
|
|
130
|
+
return new ValidationResult(false, [
|
|
131
|
+
this._formatError(message || defaultMessage)
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
return new ValidationResult(true);
|
|
135
|
+
});
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if value is between min and max (for numbers)
|
|
140
|
+
between(min, max, message) {
|
|
141
|
+
const defaultMessage = `Must be between ${min} and ${max}`;
|
|
142
|
+
this.rules.push(() => {
|
|
143
|
+
if (this._shouldSkipOptional()) {
|
|
144
|
+
return new ValidationResult(true);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const numValue =
|
|
148
|
+
typeof this.value === 'number' ? this.value : parseFloat(this.value);
|
|
149
|
+
|
|
150
|
+
if (isNaN(numValue) || numValue < min || numValue > max) {
|
|
151
|
+
return new ValidationResult(false, [
|
|
152
|
+
this._formatError(message || defaultMessage)
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
return new ValidationResult(true);
|
|
156
|
+
});
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
min(length, message = `Minimum length is ${length}`) {
|
|
161
|
+
this.rules.push(() => {
|
|
162
|
+
if (this._shouldSkipOptional()) {
|
|
163
|
+
return new ValidationResult(true);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.value != null && this.value !== '') {
|
|
167
|
+
if (typeof this.value === 'string' || Array.isArray(this.value)) {
|
|
168
|
+
if (this.value.length < length) {
|
|
169
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
170
|
+
}
|
|
171
|
+
} else if (typeof this.value === 'number') {
|
|
172
|
+
if (this.value < length) {
|
|
173
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
return new ValidationResult(false, [
|
|
177
|
+
this._formatError('Value must be a string, array, or number')
|
|
178
|
+
]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return new ValidationResult(true);
|
|
182
|
+
});
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
max(length, message = `Maximum length is ${length}`) {
|
|
187
|
+
this.rules.push(() => {
|
|
188
|
+
if (this._shouldSkipOptional()) {
|
|
189
|
+
return new ValidationResult(true);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.value != null && this.value !== '') {
|
|
193
|
+
if (typeof this.value === 'string' || Array.isArray(this.value)) {
|
|
194
|
+
if (this.value.length > length) {
|
|
195
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
196
|
+
}
|
|
197
|
+
} else if (typeof this.value === 'number') {
|
|
198
|
+
if (this.value > length) {
|
|
199
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
return new ValidationResult(false, [
|
|
203
|
+
this._formatError('Value must be a string, array or number')
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return new ValidationResult(true);
|
|
208
|
+
});
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Validate that value is an array
|
|
213
|
+
array(message = 'Must be an array') {
|
|
214
|
+
this.rules.push(() => {
|
|
215
|
+
if (this._shouldSkipOptional(false)) {
|
|
216
|
+
return new ValidationResult(true);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!Array.isArray(this.value)) {
|
|
220
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
221
|
+
}
|
|
222
|
+
return new ValidationResult(true);
|
|
223
|
+
});
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Validate each item in an array
|
|
228
|
+
arrayOf(validator, message = 'Invalid array items') {
|
|
229
|
+
this.rules.push(() => {
|
|
230
|
+
if (this._shouldSkipOptional(false)) {
|
|
231
|
+
return new ValidationResult(true);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!Array.isArray(this.value)) {
|
|
235
|
+
return new ValidationResult(false, [
|
|
236
|
+
this._formatError('Value must be an array')
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const errors = [];
|
|
241
|
+
this.value.forEach((item, index) => {
|
|
242
|
+
try {
|
|
243
|
+
const itemValidator =
|
|
244
|
+
typeof validator === 'function' ? validator(item) : validator;
|
|
245
|
+
const result = itemValidator.validate();
|
|
246
|
+
|
|
247
|
+
if (!result.isValid) {
|
|
248
|
+
errors.push(`[${index}]: ${result.errors.join(', ')}`);
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
errors.push(`[${index}]: Validation error - ${error.message}`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (errors.length > 0) {
|
|
256
|
+
return new ValidationResult(false, [
|
|
257
|
+
this._formatError(`${message}: ${errors.join('; ')}`)
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return new ValidationResult(true);
|
|
262
|
+
});
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Async array validation
|
|
267
|
+
arrayOfAsync(validator, message = 'Invalid array items') {
|
|
268
|
+
this.asyncRules.push(async () => {
|
|
269
|
+
if (this._shouldSkipOptional(false)) {
|
|
270
|
+
return new ValidationResult(true);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!Array.isArray(this.value)) {
|
|
274
|
+
return new ValidationResult(false, [
|
|
275
|
+
this._formatError('Value must be an array')
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const errors = [];
|
|
280
|
+
for (let index = 0; index < this.value.length; index++) {
|
|
281
|
+
try {
|
|
282
|
+
const item = this.value[index];
|
|
283
|
+
const itemValidator =
|
|
284
|
+
typeof validator === 'function' ? validator(item) : validator;
|
|
285
|
+
|
|
286
|
+
const result =
|
|
287
|
+
itemValidator.asyncRules && itemValidator.asyncRules.length > 0
|
|
288
|
+
? await itemValidator.validateAsync()
|
|
289
|
+
: itemValidator.validate();
|
|
290
|
+
|
|
291
|
+
if (!result.isValid) {
|
|
292
|
+
errors.push(`[${index}]: ${result.errors.join(', ')}`);
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
errors.push(`[${index}]: Validation error - ${error.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (errors.length > 0) {
|
|
300
|
+
return new ValidationResult(false, [
|
|
301
|
+
this._formatError(`${message}: ${errors.join('; ')}`)
|
|
302
|
+
]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return new ValidationResult(true);
|
|
306
|
+
});
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Validate nested objects
|
|
311
|
+
object(schema, message = 'Invalid object structure') {
|
|
312
|
+
this.rules.push(() => {
|
|
313
|
+
if (this._shouldSkipOptional(false)) {
|
|
314
|
+
return new ValidationResult(true);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (
|
|
318
|
+
typeof this.value !== 'object' ||
|
|
319
|
+
this.value === null ||
|
|
320
|
+
Array.isArray(this.value)
|
|
321
|
+
) {
|
|
322
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const result = validate(schema, this.value);
|
|
327
|
+
|
|
328
|
+
if (!result.isValid) {
|
|
329
|
+
const fieldErrors = result.getErrors();
|
|
330
|
+
const errorMessages = Object.entries(fieldErrors)
|
|
331
|
+
.map(([field, errs]) => `${field}: ${errs.join(', ')}`)
|
|
332
|
+
.join('; ');
|
|
333
|
+
|
|
334
|
+
return new ValidationResult(false, [
|
|
335
|
+
this._formatError(`Object validation failed - ${errorMessages}`)
|
|
336
|
+
]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return new ValidationResult(true);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
return new ValidationResult(false, [
|
|
342
|
+
this._formatError(`Object validation error: ${error.message}`)
|
|
343
|
+
]);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Async nested object validation
|
|
350
|
+
objectAsync(schema, message = 'Invalid object structure') {
|
|
351
|
+
this.asyncRules.push(async () => {
|
|
352
|
+
if (this._shouldSkipOptional(false)) {
|
|
353
|
+
return new ValidationResult(true);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (
|
|
357
|
+
typeof this.value !== 'object' ||
|
|
358
|
+
this.value === null ||
|
|
359
|
+
Array.isArray(this.value)
|
|
360
|
+
) {
|
|
361
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const result = await validateAsync(schema, this.value);
|
|
366
|
+
|
|
367
|
+
if (!result.isValid) {
|
|
368
|
+
const fieldErrors = result.getErrors();
|
|
369
|
+
const errorMessages = Object.entries(fieldErrors)
|
|
370
|
+
.map(([field, errs]) => `${field}: ${errs.join(', ')}`)
|
|
371
|
+
.join('; ');
|
|
372
|
+
|
|
373
|
+
return new ValidationResult(false, [
|
|
374
|
+
this._formatError(`Object validation failed - ${errorMessages}`)
|
|
375
|
+
]);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return new ValidationResult(true);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return new ValidationResult(false, [
|
|
381
|
+
this._formatError(`Object validation error: ${error.message}`)
|
|
382
|
+
]);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
return this;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
pattern(regex, message = 'Invalid format') {
|
|
389
|
+
if (!isRegexSafe(regex)) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
'Potentially unsafe regex pattern detected. Please use a simple pattern.'
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this.rules.push(() => {
|
|
396
|
+
if (this._shouldSkipOptional()) {
|
|
397
|
+
return new ValidationResult(true);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (this.value != null && this.value !== '') {
|
|
401
|
+
const stringValue = String(this.value);
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
if (!safeRegexTestSync(regex, stringValue)) {
|
|
405
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error.message.includes('Input too long')) {
|
|
409
|
+
return new ValidationResult(false, [
|
|
410
|
+
this._formatError('Input too long for pattern validation')
|
|
411
|
+
]);
|
|
412
|
+
}
|
|
413
|
+
return new ValidationResult(false, [
|
|
414
|
+
this._formatError('Pattern validation failed')
|
|
415
|
+
]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return new ValidationResult(true);
|
|
419
|
+
});
|
|
420
|
+
return this;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
patternAsync(regex, message = 'Invalid format') {
|
|
424
|
+
if (!isRegexSafe(regex)) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
'Potentially unsafe regex pattern detected. Please use a simple pattern.'
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
this.asyncRules.push(async () => {
|
|
431
|
+
if (this._shouldSkipOptional()) {
|
|
432
|
+
return new ValidationResult(true);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (this.value != null && this.value !== '') {
|
|
436
|
+
const stringValue = String(this.value);
|
|
437
|
+
|
|
438
|
+
if (stringValue.length > 10000) {
|
|
439
|
+
return new ValidationResult(false, [
|
|
440
|
+
this._formatError('Input too long for pattern validation')
|
|
441
|
+
]);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const result = await safeRegexTest(regex, stringValue);
|
|
446
|
+
if (!result) {
|
|
447
|
+
return new ValidationResult(false, [this._formatError(message)]);
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
return new ValidationResult(false, [
|
|
451
|
+
this._formatError('Pattern validation failed')
|
|
452
|
+
]);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return new ValidationResult(true);
|
|
456
|
+
});
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
when(condition, validator) {
|
|
461
|
+
this.rules.push(() => {
|
|
462
|
+
const shouldValidate =
|
|
463
|
+
typeof condition === 'function' ? condition(this.value) : condition;
|
|
464
|
+
|
|
465
|
+
if (shouldValidate) {
|
|
466
|
+
if (typeof validator === 'function') {
|
|
467
|
+
const conditionalValidator = validator(this.value);
|
|
468
|
+
return conditionalValidator.validate();
|
|
469
|
+
} else {
|
|
470
|
+
return validator.validate();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return new ValidationResult(true);
|
|
475
|
+
});
|
|
476
|
+
return this;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
custom(validatorFn, message = 'Custom validation failed') {
|
|
480
|
+
this.rules.push(() => {
|
|
481
|
+
if (this._shouldSkipOptional()) {
|
|
482
|
+
return new ValidationResult(true);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const result = validatorFn(this.value);
|
|
487
|
+
|
|
488
|
+
if (typeof result === 'boolean') {
|
|
489
|
+
return result
|
|
490
|
+
? new ValidationResult(true)
|
|
491
|
+
: new ValidationResult(false, [this._formatError(message)]);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (result && typeof result === 'object' && 'isValid' in result) {
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (typeof result === 'string') {
|
|
499
|
+
return new ValidationResult(false, [this._formatError(result)]);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return new ValidationResult(true);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
return new ValidationResult(false, [
|
|
505
|
+
this._formatError(`Custom validation error: ${error.message}`)
|
|
506
|
+
]);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
return this;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
customAsync(validatorFn, message = 'Async validation failed') {
|
|
513
|
+
this.asyncRules.push(async () => {
|
|
514
|
+
if (this._shouldSkipOptional()) {
|
|
515
|
+
return new ValidationResult(true);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const result = await validatorFn(this.value);
|
|
520
|
+
|
|
521
|
+
if (typeof result === 'boolean') {
|
|
522
|
+
return result
|
|
523
|
+
? new ValidationResult(true)
|
|
524
|
+
: new ValidationResult(false, [this._formatError(message)]);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (result && typeof result === 'object' && 'isValid' in result) {
|
|
528
|
+
return result;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (typeof result === 'string') {
|
|
532
|
+
return new ValidationResult(false, [this._formatError(result)]);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return new ValidationResult(true);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return new ValidationResult(false, [
|
|
538
|
+
this._formatError(`Async validation error: ${error.message}`)
|
|
539
|
+
]);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
return this;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
validate() {
|
|
546
|
+
const result = new ValidationResult(true);
|
|
547
|
+
|
|
548
|
+
for (const rule of this.rules) {
|
|
549
|
+
try {
|
|
550
|
+
const ruleResult = rule();
|
|
551
|
+
if (!ruleResult.isValid) {
|
|
552
|
+
result.isValid = false;
|
|
553
|
+
result.errors.push(...ruleResult.errors);
|
|
554
|
+
}
|
|
555
|
+
} catch (error) {
|
|
556
|
+
result.isValid = false;
|
|
557
|
+
result.errors.push(
|
|
558
|
+
this._formatError(`Validation error: ${error.message}`)
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async validateAsync() {
|
|
567
|
+
const syncResult = this.validate();
|
|
568
|
+
|
|
569
|
+
if (!syncResult.isValid) {
|
|
570
|
+
return syncResult;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const result = new ValidationResult(true, [...syncResult.errors]);
|
|
574
|
+
|
|
575
|
+
for (const asyncRule of this.asyncRules) {
|
|
576
|
+
try {
|
|
577
|
+
const ruleResult = await asyncRule();
|
|
578
|
+
if (!ruleResult.isValid) {
|
|
579
|
+
result.isValid = false;
|
|
580
|
+
result.errors.push(...ruleResult.errors);
|
|
581
|
+
}
|
|
582
|
+
} catch (error) {
|
|
583
|
+
result.isValid = false;
|
|
584
|
+
result.errors.push(
|
|
585
|
+
this._formatError(`Async validation error: ${error.message}`)
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
module.exports = { BaseValidator };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snap Validate - ValidationResult
|
|
3
|
+
* The result type returned by every validation rule.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ValidationResult {
|
|
7
|
+
constructor(isValid, errors = []) {
|
|
8
|
+
this.isValid = isValid;
|
|
9
|
+
this.errors = errors;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
addError(message) {
|
|
13
|
+
this.errors.push(message);
|
|
14
|
+
this.isValid = false;
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { ValidationResult };
|