snap-validate 0.2.0 โ 0.3.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.
- package/LICENSE +2 -2
- package/README.md +110 -11
- package/package.json +1 -1
- package/src/index.js +296 -25
- package/types/index.d.ts +129 -9
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2025
|
|
3
|
+
Copyright (c) 2025 Snap Validate Contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Snap Validate โก
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/snap-validate)
|
|
4
4
|
[](https://github.com/aniru-dh21/snap-validate/actions)
|
|
5
|
-
[](https://codecov.io/gh/aniru-dh21/snap-validate)
|
|
6
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://npm-stat.com/charts.html?package=snap-validate)
|
|
7
7
|
|
|
8
8
|
A lightning-fast, lightweight validation library for common patterns without heavy dependencies. Perfect for client-side and server-side validation with zero external dependencies.
|
|
9
9
|
|
|
@@ -14,6 +14,9 @@ A lightning-fast, lightweight validation library for common patterns without hea
|
|
|
14
14
|
- ๐ง **Flexible**: Chainable validation rules and custom validators
|
|
15
15
|
- ๐ง **Common Patterns**: Email, phone, credit card, URL, password validation
|
|
16
16
|
- ๐ **International**: Support for different formats (US/International phone, postal codes)
|
|
17
|
+
- ๐ **Async Support**: Full async validation support for database checks and API calls
|
|
18
|
+
- ๐ฏ **Conditional**: Advanced conditional validation with `when()` and `optional()`
|
|
19
|
+
- ๐ ๏ธ **Custom Validators**: Add your own sync and async validation logic
|
|
17
20
|
- ๐งช **Well Tested**: Comprehensive test suite with high coverage
|
|
18
21
|
- ๐ฆ **Easy Integration**: Works in Node.js and browsers
|
|
19
22
|
- ๐ **Chainable API**: Intuitive fluent interface
|
|
@@ -126,6 +129,89 @@ validators.zipCode('K1A 0A6', 'ca').validate();
|
|
|
126
129
|
validators.zipCode('SW1A 1AA', 'uk').validate();
|
|
127
130
|
```
|
|
128
131
|
|
|
132
|
+
## Advanced Validation Features
|
|
133
|
+
|
|
134
|
+
### Conditional Validation
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
const { BaseValidator } = require('snap-validate');
|
|
138
|
+
|
|
139
|
+
// Validate only when condition is met
|
|
140
|
+
const validator = new BaseValidator(value)
|
|
141
|
+
.when(user.isAdmin, validators.required('Admin field required'))
|
|
142
|
+
.min(5, 'Must be at least 5 characters');
|
|
143
|
+
|
|
144
|
+
// Optional validation - skip if empty/null/undefined
|
|
145
|
+
const optionalValidator = new BaseValidator(value)
|
|
146
|
+
.optional()
|
|
147
|
+
.email('Must be a valid email if provided');
|
|
148
|
+
|
|
149
|
+
// Function-based conditions
|
|
150
|
+
const conditionalValidator = new BaseValidator(value)
|
|
151
|
+
.when(() => user.role === 'admin', validators.required())
|
|
152
|
+
.max(100);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Custom Validators
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
const { BaseValidator } = require('snap-validate');
|
|
159
|
+
|
|
160
|
+
// Synchronous custom validation
|
|
161
|
+
const customValidator = new BaseValidator(value)
|
|
162
|
+
.custom((val) => val !== 'forbidden', 'Value cannot be forbidden')
|
|
163
|
+
.custom((val) => {
|
|
164
|
+
if (val.includes('admin') && !user.isAdmin) {
|
|
165
|
+
return 'Only admins can use this value';
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Asynchronous custom validation
|
|
171
|
+
const asyncValidator = new BaseValidator(email)
|
|
172
|
+
.email()
|
|
173
|
+
.customAsync(async (email) => {
|
|
174
|
+
const exists = await checkEmailExists(email);
|
|
175
|
+
return !exists || 'Email already exists';
|
|
176
|
+
}, 'Email validation failed');
|
|
177
|
+
|
|
178
|
+
// Use async validation
|
|
179
|
+
const result = await asyncValidator.validateAsync();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Async Validation
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
// Async validation for single field
|
|
186
|
+
const validator = new BaseValidator(username)
|
|
187
|
+
.required()
|
|
188
|
+
.min(3)
|
|
189
|
+
.customAsync(async (username) => {
|
|
190
|
+
const available = await checkUsernameAvailable(username);
|
|
191
|
+
return available || 'Username is already taken';
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const result = await validator.validateAsync();
|
|
195
|
+
|
|
196
|
+
// Async schema validation
|
|
197
|
+
const asyncSchema = {
|
|
198
|
+
username: (value) => new BaseValidator(value)
|
|
199
|
+
.required()
|
|
200
|
+
.customAsync(async (val) => {
|
|
201
|
+
const available = await checkUsernameAvailable(val);
|
|
202
|
+
return available || 'Username taken';
|
|
203
|
+
}),
|
|
204
|
+
|
|
205
|
+
email: (value) => validators.email(value)
|
|
206
|
+
.customAsync(async (val) => {
|
|
207
|
+
const exists = await checkEmailExists(val);
|
|
208
|
+
return !exists || 'Email already registered';
|
|
209
|
+
})
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const asyncResult = await validate.async(asyncSchema, userData);
|
|
213
|
+
```
|
|
214
|
+
|
|
129
215
|
## Custom Validation
|
|
130
216
|
|
|
131
217
|
### Using BaseValidator
|
|
@@ -157,6 +243,7 @@ const schema = {
|
|
|
157
243
|
age: (value) => new BaseValidator(value)
|
|
158
244
|
.required()
|
|
159
245
|
.pattern(/^\d+$/, 'Age must be a number')
|
|
246
|
+
.custom((val) => parseInt(val) >= 18, 'Must be 18 or older')
|
|
160
247
|
};
|
|
161
248
|
|
|
162
249
|
const userData = {
|
|
@@ -185,6 +272,16 @@ if (!schemaResult.isValid) {
|
|
|
185
272
|
console.log('Field errors:', errors);
|
|
186
273
|
// Output: { email: ['Invalid email format'], password: ['Password too weak'] }
|
|
187
274
|
}
|
|
275
|
+
|
|
276
|
+
// Async error handling
|
|
277
|
+
try {
|
|
278
|
+
const asyncResult = await validator.validateAsync();
|
|
279
|
+
if (!asyncResult.isValid) {
|
|
280
|
+
console.log('Async validation errors:', asyncResult.errors);
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.log('Validation exception:', error.message);
|
|
284
|
+
}
|
|
188
285
|
```
|
|
189
286
|
|
|
190
287
|
## Browser Usage
|
|
@@ -212,7 +309,12 @@ if (!schemaResult.isValid) {
|
|
|
212
309
|
- `min(length, message?)` - Minimum length validation
|
|
213
310
|
- `max(length, message?)` - Maximum length validation
|
|
214
311
|
- `pattern(regex, message?)` - Pattern matching validation
|
|
215
|
-
- `
|
|
312
|
+
- `when(condition, validator)` - Conditional validation
|
|
313
|
+
- `optional()` - Skip validation if empty/null/undefined
|
|
314
|
+
- `custom(fn, message?)` - Custom synchronous validation
|
|
315
|
+
- `customAsync(fn, message?)` - Custom asynchronous validation
|
|
316
|
+
- `validate()` - Execute synchronous validation
|
|
317
|
+
- `validateAsync()` - Execute asynchronous validation
|
|
216
318
|
|
|
217
319
|
### Available Validators
|
|
218
320
|
|
|
@@ -225,6 +327,10 @@ if (!schemaResult.isValid) {
|
|
|
225
327
|
- `validators.numeric(value)`
|
|
226
328
|
- `validators.zipCode(value, country?)`
|
|
227
329
|
|
|
330
|
+
### Validation Functions
|
|
331
|
+
|
|
332
|
+
- `validate(schema, data)` - Synchronous schema validation
|
|
333
|
+
- `validate.async(schema, data)` - Asynchronous schema validation
|
|
228
334
|
|
|
229
335
|
## Contributing
|
|
230
336
|
|
|
@@ -264,14 +370,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
264
370
|
|
|
265
371
|
## Changelog
|
|
266
372
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
- Initial release
|
|
270
|
-
- Basic validation patterns (email, phone, credit card, URL, password)
|
|
271
|
-
- Schema validation support
|
|
272
|
-
- Comprehensive test suite
|
|
273
|
-
- CI/CD pipeline setup
|
|
274
|
-
- Lightning-fast performance optimizations
|
|
373
|
+
See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes.
|
|
275
374
|
|
|
276
375
|
---
|
|
277
376
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Snap Validate - Lightweight validator library
|
|
3
|
-
* @version 0.0
|
|
2
|
+
* Snap Validate - Enhanced Lightweight validator library
|
|
3
|
+
* @version 0.3.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Core validation class
|
|
@@ -22,10 +22,17 @@ class BaseValidator {
|
|
|
22
22
|
constructor(value) {
|
|
23
23
|
this.value = value;
|
|
24
24
|
this.rules = [];
|
|
25
|
+
this.asyncRules = [];
|
|
26
|
+
this.isOptional = false;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
required(message = 'This field is required') {
|
|
28
30
|
this.rules.push(() => {
|
|
31
|
+
// Skip validation if optional and empty
|
|
32
|
+
if (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
|
|
33
|
+
return new ValidationResult(true);
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
if (this.value === null || this.value === undefined || this.value === '') {
|
|
30
37
|
return new ValidationResult(false, [message]);
|
|
31
38
|
}
|
|
@@ -34,10 +41,33 @@ class BaseValidator {
|
|
|
34
41
|
return this;
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
optional() {
|
|
45
|
+
this.isOptional = true;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
min(length, message = `Minimum length is ${length}`) {
|
|
38
50
|
this.rules.push(() => {
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
// Skip validation if optional and empty
|
|
52
|
+
if (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
|
|
53
|
+
return new ValidationResult(true);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Only validate if value exists and a length property
|
|
57
|
+
if (this.value != null && this.value !== '') {
|
|
58
|
+
// Check if value has length property (string, array)
|
|
59
|
+
if (typeof this.value === 'string' || Array.isArray(this.value)) {
|
|
60
|
+
if (this.value.length < length) {
|
|
61
|
+
return new ValidationResult(false, [message]);
|
|
62
|
+
}
|
|
63
|
+
} else if (typeof this.value === 'number') {
|
|
64
|
+
// For numbers, compare the value itself
|
|
65
|
+
if (this.value < length) {
|
|
66
|
+
return new ValidationResult(false, [message]);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
return new ValidationResult(false, ['Value must be a string, array, or number']);
|
|
70
|
+
}
|
|
41
71
|
}
|
|
42
72
|
return new ValidationResult(true);
|
|
43
73
|
});
|
|
@@ -46,8 +76,26 @@ class BaseValidator {
|
|
|
46
76
|
|
|
47
77
|
max(length, message = `Maximum length is ${length}`) {
|
|
48
78
|
this.rules.push(() => {
|
|
49
|
-
|
|
50
|
-
|
|
79
|
+
// Skip validation if optional and empty
|
|
80
|
+
if (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
|
|
81
|
+
return new ValidationResult(true);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Only validate if value exists and has a length property
|
|
85
|
+
if (this.value != null && this.value !== '') {
|
|
86
|
+
// Check if value has length property (string, array)
|
|
87
|
+
if (typeof this.value === 'string' || Array.isArray(this.value)) {
|
|
88
|
+
if (this.value.length > length) {
|
|
89
|
+
return new ValidationResult(false, [message]);
|
|
90
|
+
}
|
|
91
|
+
} else if (typeof this.value === 'number') {
|
|
92
|
+
// For numbers, compare the value itself
|
|
93
|
+
if (this.value > length) {
|
|
94
|
+
return new ValidationResult(false, [message]);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
return new ValidationResult(false, ['Value must be a string, array or number']);
|
|
98
|
+
}
|
|
51
99
|
}
|
|
52
100
|
return new ValidationResult(true);
|
|
53
101
|
});
|
|
@@ -56,22 +104,155 @@ class BaseValidator {
|
|
|
56
104
|
|
|
57
105
|
pattern(regex, message = 'Invalid format') {
|
|
58
106
|
this.rules.push(() => {
|
|
59
|
-
if
|
|
60
|
-
|
|
107
|
+
// Skip validation if optional and empty
|
|
108
|
+
if (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
|
|
109
|
+
return new ValidationResult(true);
|
|
61
110
|
}
|
|
111
|
+
|
|
112
|
+
// Only test pattern if value exists and is not empty
|
|
113
|
+
if (this.value != null && this.value !== '') {
|
|
114
|
+
// Ensure value is a string before testing regex
|
|
115
|
+
const stringValue = String(this.value);
|
|
116
|
+
if (!regex.test(stringValue)) {
|
|
117
|
+
return new ValidationResult(false, [message]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return new ValidationResult(true);
|
|
121
|
+
});
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
when(condition, validator) {
|
|
126
|
+
this.rules.push(() => {
|
|
127
|
+
// Evaluate condition
|
|
128
|
+
const shouldValidate = typeof condition === 'function' ? condition(this.value) : condition;
|
|
129
|
+
|
|
130
|
+
if (shouldValidate) {
|
|
131
|
+
// Apply the conditional validator
|
|
132
|
+
if (typeof validator === 'function') {
|
|
133
|
+
const conditionalValidator = validator(this.value);
|
|
134
|
+
return conditionalValidator.validate();
|
|
135
|
+
} else {
|
|
136
|
+
// If validator is already a BaseValidator instance
|
|
137
|
+
return validator.validate();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
62
141
|
return new ValidationResult(true);
|
|
63
142
|
});
|
|
64
143
|
return this;
|
|
65
144
|
}
|
|
66
145
|
|
|
146
|
+
custom(validatorFn, message = 'Custom validation failed') {
|
|
147
|
+
this.rules.push(() => {
|
|
148
|
+
// Skip validation if optional and empty
|
|
149
|
+
if (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
|
|
150
|
+
return new ValidationResult(true);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const result = validatorFn(this.value);
|
|
155
|
+
|
|
156
|
+
// Handle boolean result
|
|
157
|
+
if (typeof result === 'boolean') {
|
|
158
|
+
return result ? new ValidationResult(true) : new ValidationResult(false, [message]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle ValidationResult object
|
|
162
|
+
if (result && typeof result === 'object' && 'isValid' in result) {
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle string result (error message)
|
|
167
|
+
if (typeof result === 'string') {
|
|
168
|
+
return new ValidationResult(false, [result]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Default to true if no clear result
|
|
172
|
+
return new ValidationResult(true);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return new ValidationResult(false, [`Custom validation error: ${error.message}`]);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
customAsync(validatorFn, message = 'Async validation failed') {
|
|
181
|
+
this.asyncRules.push(async () => {
|
|
182
|
+
// Skip validation if optional and empty
|
|
183
|
+
if (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
|
|
184
|
+
return new ValidationResult(true);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const result = await validatorFn(this.value);
|
|
189
|
+
|
|
190
|
+
// Handle boolean result
|
|
191
|
+
if (typeof result === 'boolean') {
|
|
192
|
+
return result ? new ValidationResult(true) : new ValidationResult(false, [message]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Handle ValidationResult object
|
|
196
|
+
if (result && typeof result === 'object' && 'isValid' in result) {
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Handle string result (error message)
|
|
201
|
+
if (typeof result === 'string') {
|
|
202
|
+
return new ValidationResult(false, [result]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Default to true if no clear result
|
|
206
|
+
return new ValidationResult(true);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return new ValidationResult(false, [`Async validation error: ${error.message}`]);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
67
214
|
validate() {
|
|
68
215
|
const result = new ValidationResult(true);
|
|
69
216
|
|
|
70
217
|
for (const rule of this.rules) {
|
|
71
|
-
|
|
72
|
-
|
|
218
|
+
try {
|
|
219
|
+
const ruleResult = rule();
|
|
220
|
+
if (!ruleResult.isValid) {
|
|
221
|
+
result.isValid = false;
|
|
222
|
+
result.errors.push(...ruleResult.errors);
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// Handle any unexpected errors during validation
|
|
226
|
+
result.isValid = false;
|
|
227
|
+
result.errors.push(`Validation error: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async validateAsync() {
|
|
235
|
+
// First run synchronous validations
|
|
236
|
+
const syncResult = this.validate();
|
|
237
|
+
|
|
238
|
+
if (!syncResult.isValid) {
|
|
239
|
+
return syncResult;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Then run asynchronous validations
|
|
243
|
+
const result = new ValidationResult(true, [...syncResult.errors]);
|
|
244
|
+
|
|
245
|
+
for (const asyncRule of this.asyncRules) {
|
|
246
|
+
try {
|
|
247
|
+
const ruleResult = await asyncRule();
|
|
248
|
+
if (!ruleResult.isValid) {
|
|
249
|
+
result.isValid = false;
|
|
250
|
+
result.errors.push(...ruleResult.errors);
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
// Handle any unexpected errors during async validation
|
|
73
254
|
result.isValid = false;
|
|
74
|
-
result.errors.push(
|
|
255
|
+
result.errors.push(`Async validation error: ${error.message}`);
|
|
75
256
|
}
|
|
76
257
|
}
|
|
77
258
|
|
|
@@ -106,8 +287,11 @@ const validators = {
|
|
|
106
287
|
let sum = 0;
|
|
107
288
|
let isEven = false;
|
|
108
289
|
|
|
109
|
-
|
|
110
|
-
|
|
290
|
+
// Remove spaces and ensure we have a string
|
|
291
|
+
const cleanNum = String(num).replace(/\s/g, '');
|
|
292
|
+
|
|
293
|
+
for (let i = cleanNum.length - 1; i >= 0; i--) {
|
|
294
|
+
let digit = parseInt(cleanNum[i]);
|
|
111
295
|
|
|
112
296
|
if (isEven) {
|
|
113
297
|
digit *= 2;
|
|
@@ -122,12 +306,27 @@ const validators = {
|
|
|
122
306
|
};
|
|
123
307
|
|
|
124
308
|
const validator = new BaseValidator(value)
|
|
125
|
-
.required('Credit card number is required')
|
|
126
|
-
.pattern(/^\d{13,19}$/, 'Credit card must be 13-19 digits');
|
|
309
|
+
.required('Credit card number is required');
|
|
127
310
|
|
|
311
|
+
// Add custom validation for credit card format and Luhn check
|
|
128
312
|
validator.rules.push(() => {
|
|
129
|
-
|
|
130
|
-
|
|
313
|
+
// Skip validation if optional and empty
|
|
314
|
+
if (validator.isOptional && (validator.value === null || validator.value === undefined || validator.value === '')) {
|
|
315
|
+
return new ValidationResult(true);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (validator.value) {
|
|
319
|
+
const cleanValue = String(validator.value).replace(/\s/g, '');
|
|
320
|
+
|
|
321
|
+
// Check length (13-19 digits)
|
|
322
|
+
if (!/^\d{13,19}$/.test(cleanValue)) {
|
|
323
|
+
return new ValidationResult(false, ['Credit card must be 13-19 digits']);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check Luhn algorithm
|
|
327
|
+
if (!luhnCheck(cleanValue)) {
|
|
328
|
+
return new ValidationResult(false, ['Invalid credit card number']);
|
|
329
|
+
}
|
|
131
330
|
}
|
|
132
331
|
return new ValidationResult(true);
|
|
133
332
|
});
|
|
@@ -174,7 +373,7 @@ const validators = {
|
|
|
174
373
|
alphanumeric: (value) => {
|
|
175
374
|
return new BaseValidator(value)
|
|
176
375
|
.required('This field is required')
|
|
177
|
-
.pattern(/^[a-zA-Z0-9]+$/, 'Only letters and
|
|
376
|
+
.pattern(/^[a-zA-Z0-9]+$/, 'Only letters and numbers are allowed');
|
|
178
377
|
},
|
|
179
378
|
|
|
180
379
|
numeric: (value) => {
|
|
@@ -198,17 +397,88 @@ const validators = {
|
|
|
198
397
|
|
|
199
398
|
// Main validation function
|
|
200
399
|
const validate = (schema, data) => {
|
|
400
|
+
// Input validation
|
|
401
|
+
if (!schema || typeof schema !== 'object') {
|
|
402
|
+
throw new Error('Schema must be a valid object');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!data || typeof data !== 'object') {
|
|
406
|
+
throw new Error('Data must be a valid object');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const results = {};
|
|
410
|
+
let isValid = true;
|
|
411
|
+
|
|
412
|
+
for (const [field, validator] of Object.entries(schema)) {
|
|
413
|
+
try {
|
|
414
|
+
const fieldValue = data[field];
|
|
415
|
+
const result = typeof validator === 'function'
|
|
416
|
+
? validator(fieldValue).validate()
|
|
417
|
+
: validator.validate();
|
|
418
|
+
|
|
419
|
+
results[field] = result;
|
|
420
|
+
if (!result.isValid) {
|
|
421
|
+
isValid = false;
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
// Handle validation setup errors
|
|
425
|
+
results[field] = new ValidationResult(false, [`Validation setup error: ${error.message}`]);
|
|
426
|
+
isValid = false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
isValid,
|
|
432
|
+
errors: results,
|
|
433
|
+
getErrors: () => {
|
|
434
|
+
const errors = {};
|
|
435
|
+
for (const [field, result] of Object.entries(results)) {
|
|
436
|
+
if (!result.isValid) {
|
|
437
|
+
errors[field] = result.errors;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return errors;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Async validation function
|
|
446
|
+
const validateAsync = async (schema, data) => {
|
|
447
|
+
// Input validation
|
|
448
|
+
if (!schema || typeof schema !== 'object') {
|
|
449
|
+
throw new Error('Schema must be a valid object');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!data || typeof data !== 'object') {
|
|
453
|
+
throw new Error('Data must be a valid object');
|
|
454
|
+
}
|
|
455
|
+
|
|
201
456
|
const results = {};
|
|
202
457
|
let isValid = true;
|
|
203
458
|
|
|
204
459
|
for (const [field, validator] of Object.entries(schema)) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
460
|
+
try {
|
|
461
|
+
const fieldValue = data[field];
|
|
462
|
+
|
|
463
|
+
let result;
|
|
464
|
+
if (typeof validator === 'function') {
|
|
465
|
+
const validatorInstance = validator(fieldValue);
|
|
466
|
+
result = validatorInstance.asyncRules.length > 0
|
|
467
|
+
? await validatorInstance.validateAsync()
|
|
468
|
+
: validatorInstance.validate();
|
|
469
|
+
} else {
|
|
470
|
+
result = validator.asyncRules && validator.asyncRules.length > 0
|
|
471
|
+
? await validator.validateAsync()
|
|
472
|
+
: validator.validate();
|
|
473
|
+
}
|
|
209
474
|
|
|
210
|
-
|
|
211
|
-
|
|
475
|
+
results[field] = result;
|
|
476
|
+
if (!result.isValid) {
|
|
477
|
+
isValid = false;
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
// Handle validation setup errors
|
|
481
|
+
results[field] = new ValidationResult(false, [`Validation setup error: ${error.message}`]);
|
|
212
482
|
isValid = false;
|
|
213
483
|
}
|
|
214
484
|
}
|
|
@@ -232,5 +502,6 @@ module.exports = {
|
|
|
232
502
|
BaseValidator,
|
|
233
503
|
ValidationResult,
|
|
234
504
|
validators,
|
|
235
|
-
validate
|
|
505
|
+
validate,
|
|
506
|
+
validateAsync
|
|
236
507
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
declare module '
|
|
2
|
-
|
|
1
|
+
declare module 'snap-validate' {
|
|
2
|
+
/**
|
|
3
|
+
* Result of a validation operation
|
|
4
|
+
*/
|
|
5
|
+
export class ValidationResult {
|
|
6
|
+
constructor(isValid: boolean, errors?: string[]);
|
|
3
7
|
isValid: boolean;
|
|
4
8
|
errors: string[];
|
|
5
9
|
addError(message: string): ValidationResult;
|
|
6
10
|
}
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Password validation options
|
|
14
|
+
*/
|
|
8
15
|
export interface PasswordOptions {
|
|
9
16
|
minLength?: number;
|
|
10
17
|
requireUppercase?: boolean;
|
|
@@ -13,25 +20,113 @@ declare module 'mini-validator' {
|
|
|
13
20
|
requireSpecialChars?: boolean;
|
|
14
21
|
}
|
|
15
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Phone number format types
|
|
25
|
+
*/
|
|
16
26
|
export type PhoneFormat = 'us' | 'international' | 'simple';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Country codes for zip code validation
|
|
30
|
+
*/
|
|
17
31
|
export type CountryCode = 'us' | 'ca' | 'uk';
|
|
18
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Custom validation function that returns boolean
|
|
35
|
+
*/
|
|
36
|
+
export type CustomValidatorFunction = (
|
|
37
|
+
value: any
|
|
38
|
+
) => boolean | string | ValidationResult;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Async custom validation function
|
|
42
|
+
*/
|
|
43
|
+
export type AsyncValidatorFunction = (
|
|
44
|
+
value: any
|
|
45
|
+
) => Promise<boolean | string | ValidationResult>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Conditional validation condition
|
|
49
|
+
*/
|
|
50
|
+
export type ConditionalFunction = (value: any) => boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Conditional validator function
|
|
54
|
+
*/
|
|
55
|
+
export type ConditionalValidatorFunction = (value: any) => BaseValidator;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Base validator class with chainable validation methods
|
|
59
|
+
*/
|
|
19
60
|
export class BaseValidator {
|
|
20
61
|
constructor(value: any);
|
|
62
|
+
value: any;
|
|
63
|
+
rules: Array<() => ValidationResult>;
|
|
64
|
+
asyncRules: Array<() => Promise<ValidationResult>>;
|
|
65
|
+
isOptional: boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Make field required
|
|
69
|
+
*/
|
|
21
70
|
required(message?: string): BaseValidator;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Make field optional (skips validation if empty)
|
|
74
|
+
*/
|
|
75
|
+
optional(): BaseValidator;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Set minimum length/value
|
|
79
|
+
*/
|
|
22
80
|
min(length: number, message?: string): BaseValidator;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Set maximum length/value
|
|
84
|
+
*/
|
|
23
85
|
max(length: number, message?: string): BaseValidator;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate against regex pattern
|
|
89
|
+
*/
|
|
24
90
|
pattern(regex: RegExp, message?: string): BaseValidator;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Conditional validation
|
|
94
|
+
*/
|
|
95
|
+
when(
|
|
96
|
+
condition: boolean | ConditionalFunction,
|
|
97
|
+
validator: BaseValidator | ConditionalValidatorFunction
|
|
98
|
+
): BaseValidator;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Custom synchronous validation
|
|
102
|
+
*/
|
|
103
|
+
custom(
|
|
104
|
+
validatorFn: CustomValidatorFunction,
|
|
105
|
+
message?: string
|
|
106
|
+
): BaseValidator;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Custom asynchronous validation
|
|
110
|
+
*/
|
|
111
|
+
customAsync(
|
|
112
|
+
validatorFn: AsyncValidatorFunction,
|
|
113
|
+
message?: string
|
|
114
|
+
): BaseValidator;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Execute synchronous validation
|
|
118
|
+
*/
|
|
25
119
|
validate(): ValidationResult;
|
|
26
|
-
}
|
|
27
120
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
addError(message: string): ValidationResult;
|
|
121
|
+
/**
|
|
122
|
+
* Execute asynchronous validation (includes sync rules)
|
|
123
|
+
*/
|
|
124
|
+
validateAsync(): Promise<ValidationResult>;
|
|
33
125
|
}
|
|
34
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Predefined validators
|
|
129
|
+
*/
|
|
35
130
|
export interface Validators {
|
|
36
131
|
email(value: string): BaseValidator;
|
|
37
132
|
phone(value: string, format?: PhoneFormat): BaseValidator;
|
|
@@ -43,18 +138,43 @@ declare module 'mini-validator' {
|
|
|
43
138
|
zipCode(value: string, country?: CountryCode): BaseValidator;
|
|
44
139
|
}
|
|
45
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Result of schema validation
|
|
143
|
+
*/
|
|
46
144
|
export interface SchemaValidationResult {
|
|
47
145
|
isValid: boolean;
|
|
48
146
|
errors: { [field: string]: ValidationResult };
|
|
49
147
|
getErrors(): { [field: string]: string[] };
|
|
50
148
|
}
|
|
51
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Validation function type for schema
|
|
152
|
+
*/
|
|
52
153
|
export type ValidationFunction = (value: any) => BaseValidator;
|
|
53
|
-
export type Schema = { [field: string]: ValidationFunction };
|
|
54
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Schema definition type
|
|
157
|
+
*/
|
|
158
|
+
export type Schema = { [field: string]: ValidationFunction | BaseValidator };
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Predefined validator instances
|
|
162
|
+
*/
|
|
55
163
|
export const validators: Validators;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validate data against schema synchronously
|
|
167
|
+
*/
|
|
56
168
|
export function validate(
|
|
57
169
|
schema: Schema,
|
|
58
170
|
data: { [key: string]: any }
|
|
59
171
|
): SchemaValidationResult;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Validate data against schema asynchronously
|
|
175
|
+
*/
|
|
176
|
+
export function validateAsync(
|
|
177
|
+
schema: Schema,
|
|
178
|
+
data: { [key: string]: any }
|
|
179
|
+
): Promise<SchemaValidationResult>;
|
|
60
180
|
}
|