sehawq.db 4.0.2 → 4.0.5
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/.github/workflows/npm-publish.yml +30 -30
- package/LICENSE +21 -21
- package/index.js +1 -1
- package/package.json +36 -36
- package/readme.md +413 -413
- package/src/core/Database.js +294 -294
- package/src/core/Events.js +285 -285
- package/src/core/IndexManager.js +813 -813
- package/src/core/Persistence.js +375 -375
- package/src/core/QueryEngine.js +447 -447
- package/src/core/Storage.js +321 -321
- package/src/core/Validator.js +324 -324
- package/src/index.js +115 -115
- package/src/performance/Cache.js +338 -338
- package/src/performance/LazyLoader.js +354 -354
- package/src/performance/MemoryManager.js +495 -495
- package/src/server/api.js +687 -687
- package/src/server/websocket.js +527 -527
- package/src/utils/benchmark.js +51 -51
- package/src/utils/dot-notation.js +247 -247
- package/src/utils/helpers.js +275 -275
- package/src/utils/profiler.js +70 -70
- package/src/version.js +37 -37
package/src/core/Validator.js
CHANGED
|
@@ -1,325 +1,325 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Data Validator - Keeps your data clean and proper 🧼
|
|
3
|
-
*
|
|
4
|
-
* Because garbage in, garbage out is a real thing
|
|
5
|
-
* Validates everything from emails to custom business rules
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
class Validator {
|
|
9
|
-
constructor() {
|
|
10
|
-
this.rules = new Map();
|
|
11
|
-
this.customValidators = new Map();
|
|
12
|
-
|
|
13
|
-
this._setupBuiltinRules();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Setup built-in validation rules
|
|
18
|
-
*/
|
|
19
|
-
_setupBuiltinRules() {
|
|
20
|
-
// Type validators
|
|
21
|
-
this.rules.set('string', value => typeof value === 'string');
|
|
22
|
-
this.rules.set('number', value => typeof value === 'number' && !isNaN(value));
|
|
23
|
-
this.rules.set('boolean', value => typeof value === 'boolean');
|
|
24
|
-
this.rules.set('array', value => Array.isArray(value));
|
|
25
|
-
this.rules.set('object', value => value && typeof value === 'object' && !Array.isArray(value));
|
|
26
|
-
this.rules.set('function', value => typeof value === 'function');
|
|
27
|
-
this.rules.set('null', value => value === null);
|
|
28
|
-
this.rules.set('undefined', value => value === undefined);
|
|
29
|
-
|
|
30
|
-
// Common format validators
|
|
31
|
-
this.rules.set('email', value =>
|
|
32
|
-
typeof value === 'string' &&
|
|
33
|
-
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
this.rules.set('url', value => {
|
|
37
|
-
if (typeof value !== 'string') return false;
|
|
38
|
-
try {
|
|
39
|
-
new URL(value);
|
|
40
|
-
return true;
|
|
41
|
-
} catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
this.rules.set('uuid', value =>
|
|
47
|
-
typeof value === 'string' &&
|
|
48
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
this.rules.set('date', value =>
|
|
52
|
-
value instanceof Date && !isNaN(value.getTime())
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
this.rules.set('hexColor', value =>
|
|
56
|
-
typeof value === 'string' &&
|
|
57
|
-
/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i.test(value)
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
// Comparison validators
|
|
61
|
-
this.rules.set('min', (value, min) => {
|
|
62
|
-
if (typeof value === 'number') return value >= min;
|
|
63
|
-
if (typeof value === 'string' || Array.isArray(value)) return value.length >= min;
|
|
64
|
-
return false;
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
this.rules.set('max', (value, max) => {
|
|
68
|
-
if (typeof value === 'number') return value <= max;
|
|
69
|
-
if (typeof value === 'string' || Array.isArray(value)) return value.length <= max;
|
|
70
|
-
return false;
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
this.rules.set('range', (value, [min, max]) => {
|
|
74
|
-
if (typeof value !== 'number') return false;
|
|
75
|
-
return value >= min && value <= max;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
this.rules.set('minLength', (value, min) =>
|
|
79
|
-
(typeof value === 'string' || Array.isArray(value)) && value.length >= min
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
this.rules.set('maxLength', (value, max) =>
|
|
83
|
-
(typeof value === 'string' || Array.isArray(value)) && value.length <= max
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
this.rules.set('length', (value, length) =>
|
|
87
|
-
(typeof value === 'string' || Array.isArray(value)) && value.length === length
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
// Pattern validators
|
|
91
|
-
this.rules.set('pattern', (value, pattern) =>
|
|
92
|
-
typeof value === 'string' && pattern.test(value)
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
this.rules.set('alphanumeric', value =>
|
|
96
|
-
typeof value === 'string' && /^[a-zA-Z0-9]+$/.test(value)
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
this.rules.set('numeric', value =>
|
|
100
|
-
typeof value === 'string' && /^\d+$/.test(value)
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
// Collection validators
|
|
104
|
-
this.rules.set('in', (value, allowed) =>
|
|
105
|
-
Array.isArray(allowed) && allowed.includes(value)
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
this.rules.set('notIn', (value, disallowed) =>
|
|
109
|
-
Array.isArray(disallowed) && !disallowed.includes(value)
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
// Special validators
|
|
113
|
-
this.rules.set('required', value =>
|
|
114
|
-
value !== null && value !== undefined && value !== ''
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
this.rules.set('optional', () => true);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Add custom validator
|
|
122
|
-
*/
|
|
123
|
-
addRule(name, validatorFn) {
|
|
124
|
-
if (this.rules.has(name) || this.customValidators.has(name)) {
|
|
125
|
-
throw new Error(`Validator '${name}' already exists`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
this.customValidators.set(name, validatorFn);
|
|
129
|
-
return this;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Remove validator
|
|
134
|
-
*/
|
|
135
|
-
removeRule(name) {
|
|
136
|
-
this.rules.delete(name);
|
|
137
|
-
this.customValidators.delete(name);
|
|
138
|
-
return this;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Validate single value against rules
|
|
143
|
-
*/
|
|
144
|
-
validateValue(value, rules) {
|
|
145
|
-
const errors = [];
|
|
146
|
-
|
|
147
|
-
for (const rule of rules) {
|
|
148
|
-
const [ruleName, ...ruleArgs] = Array.isArray(rule) ? rule : [rule];
|
|
149
|
-
|
|
150
|
-
let isValid = false;
|
|
151
|
-
|
|
152
|
-
// Check built-in rules first
|
|
153
|
-
if (this.rules.has(ruleName)) {
|
|
154
|
-
const validator = this.rules.get(ruleName);
|
|
155
|
-
isValid = validator(value, ...ruleArgs);
|
|
156
|
-
}
|
|
157
|
-
// Check custom rules
|
|
158
|
-
else if (this.customValidators.has(ruleName)) {
|
|
159
|
-
const validator = this.customValidators.get(ruleName);
|
|
160
|
-
isValid = validator(value, ...ruleArgs);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
errors.push(`Unknown validation rule: ${ruleName}`);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!isValid) {
|
|
168
|
-
errors.push(this._formatError(ruleName, ruleArgs, value));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
isValid: errors.length === 0,
|
|
174
|
-
errors,
|
|
175
|
-
value
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Validate object against schema
|
|
181
|
-
*/
|
|
182
|
-
validateObject(obj, schema) {
|
|
183
|
-
const errors = {};
|
|
184
|
-
let isValid = true;
|
|
185
|
-
|
|
186
|
-
for (const [field, fieldRules] of Object.entries(schema)) {
|
|
187
|
-
const value = obj[field];
|
|
188
|
-
const result = this.validateValue(value, fieldRules);
|
|
189
|
-
|
|
190
|
-
if (!result.isValid) {
|
|
191
|
-
errors[field] = result.errors;
|
|
192
|
-
isValid = false;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
isValid,
|
|
198
|
-
errors,
|
|
199
|
-
data: obj
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Create schema validator
|
|
205
|
-
*/
|
|
206
|
-
schema(schemaDef) {
|
|
207
|
-
return (data) => this.validateObject(data, schemaDef);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Format validation error
|
|
212
|
-
*/
|
|
213
|
-
_formatError(rule, args, value) {
|
|
214
|
-
const valueStr = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
|
|
215
|
-
|
|
216
|
-
switch (rule) {
|
|
217
|
-
case 'required':
|
|
218
|
-
return 'Field is required';
|
|
219
|
-
case 'min':
|
|
220
|
-
return `Value must be at least ${args[0]}`;
|
|
221
|
-
case 'max':
|
|
222
|
-
return `Value must be at most ${args[0]}`;
|
|
223
|
-
case 'minLength':
|
|
224
|
-
return `Length must be at least ${args[0]} characters`;
|
|
225
|
-
case 'maxLength':
|
|
226
|
-
return `Length must be at most ${args[0]} characters`;
|
|
227
|
-
case 'length':
|
|
228
|
-
return `Length must be exactly ${args[0]} characters`;
|
|
229
|
-
case 'range':
|
|
230
|
-
return `Value must be between ${args[0][0]} and ${args[0][1]}`;
|
|
231
|
-
case 'email':
|
|
232
|
-
return 'Must be a valid email address';
|
|
233
|
-
case 'url':
|
|
234
|
-
return 'Must be a valid URL';
|
|
235
|
-
case 'uuid':
|
|
236
|
-
return 'Must be a valid UUID';
|
|
237
|
-
case 'pattern':
|
|
238
|
-
return 'Value does not match required pattern';
|
|
239
|
-
case 'in':
|
|
240
|
-
return `Value must be one of: ${args[0].join(', ')}`;
|
|
241
|
-
case 'notIn':
|
|
242
|
-
return `Value must not be one of: ${args[0].join(', ')}`;
|
|
243
|
-
default:
|
|
244
|
-
return `Failed validation: ${rule}`;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Quick validators (static methods)
|
|
250
|
-
*/
|
|
251
|
-
static isEmail(value) {
|
|
252
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
static isURL(value) {
|
|
256
|
-
try {
|
|
257
|
-
new URL(value);
|
|
258
|
-
return true;
|
|
259
|
-
} catch {
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
static isDate(value) {
|
|
265
|
-
return value instanceof Date && !isNaN(value.getTime());
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
static isNumber(value) {
|
|
269
|
-
return typeof value === 'number' && !isNaN(value);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
static isString(value) {
|
|
273
|
-
return typeof value === 'string';
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
static isArray(value) {
|
|
277
|
-
return Array.isArray(value);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
static isObject(value) {
|
|
281
|
-
return value && typeof value === 'object' && !Array.isArray(value);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
static isEmpty(value) {
|
|
285
|
-
if (value === null || value === undefined) return true;
|
|
286
|
-
if (typeof value === 'string') return value.trim().length === 0;
|
|
287
|
-
if (Array.isArray(value)) return value.length === 0;
|
|
288
|
-
if (typeof value === 'object') return Object.keys(value).length === 0;
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Sanitize functions
|
|
294
|
-
*/
|
|
295
|
-
static trim(value) {
|
|
296
|
-
return typeof value === 'string' ? value.trim() : value;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
static toLowerCase(value) {
|
|
300
|
-
return typeof value === 'string' ? value.toLowerCase() : value;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
static toUpperCase(value) {
|
|
304
|
-
return typeof value === 'string' ? value.toUpperCase() : value;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
static toNumber(value) {
|
|
308
|
-
if (typeof value === 'number') return value;
|
|
309
|
-
const num = parseFloat(value);
|
|
310
|
-
return isNaN(num) ? value : num;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
static toBoolean(value) {
|
|
314
|
-
if (typeof value === 'boolean') return value;
|
|
315
|
-
if (typeof value === 'string') {
|
|
316
|
-
return value.toLowerCase() === 'true' || value === '1';
|
|
317
|
-
}
|
|
318
|
-
return !!value;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Export singleton instance
|
|
323
|
-
const validator = new Validator();
|
|
324
|
-
module.exports = validator;
|
|
1
|
+
/**
|
|
2
|
+
* Data Validator - Keeps your data clean and proper 🧼
|
|
3
|
+
*
|
|
4
|
+
* Because garbage in, garbage out is a real thing
|
|
5
|
+
* Validates everything from emails to custom business rules
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class Validator {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.rules = new Map();
|
|
11
|
+
this.customValidators = new Map();
|
|
12
|
+
|
|
13
|
+
this._setupBuiltinRules();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Setup built-in validation rules
|
|
18
|
+
*/
|
|
19
|
+
_setupBuiltinRules() {
|
|
20
|
+
// Type validators
|
|
21
|
+
this.rules.set('string', value => typeof value === 'string');
|
|
22
|
+
this.rules.set('number', value => typeof value === 'number' && !isNaN(value));
|
|
23
|
+
this.rules.set('boolean', value => typeof value === 'boolean');
|
|
24
|
+
this.rules.set('array', value => Array.isArray(value));
|
|
25
|
+
this.rules.set('object', value => value && typeof value === 'object' && !Array.isArray(value));
|
|
26
|
+
this.rules.set('function', value => typeof value === 'function');
|
|
27
|
+
this.rules.set('null', value => value === null);
|
|
28
|
+
this.rules.set('undefined', value => value === undefined);
|
|
29
|
+
|
|
30
|
+
// Common format validators
|
|
31
|
+
this.rules.set('email', value =>
|
|
32
|
+
typeof value === 'string' &&
|
|
33
|
+
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
this.rules.set('url', value => {
|
|
37
|
+
if (typeof value !== 'string') return false;
|
|
38
|
+
try {
|
|
39
|
+
new URL(value);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.rules.set('uuid', value =>
|
|
47
|
+
typeof value === 'string' &&
|
|
48
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
this.rules.set('date', value =>
|
|
52
|
+
value instanceof Date && !isNaN(value.getTime())
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
this.rules.set('hexColor', value =>
|
|
56
|
+
typeof value === 'string' &&
|
|
57
|
+
/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i.test(value)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Comparison validators
|
|
61
|
+
this.rules.set('min', (value, min) => {
|
|
62
|
+
if (typeof value === 'number') return value >= min;
|
|
63
|
+
if (typeof value === 'string' || Array.isArray(value)) return value.length >= min;
|
|
64
|
+
return false;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.rules.set('max', (value, max) => {
|
|
68
|
+
if (typeof value === 'number') return value <= max;
|
|
69
|
+
if (typeof value === 'string' || Array.isArray(value)) return value.length <= max;
|
|
70
|
+
return false;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.rules.set('range', (value, [min, max]) => {
|
|
74
|
+
if (typeof value !== 'number') return false;
|
|
75
|
+
return value >= min && value <= max;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.rules.set('minLength', (value, min) =>
|
|
79
|
+
(typeof value === 'string' || Array.isArray(value)) && value.length >= min
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
this.rules.set('maxLength', (value, max) =>
|
|
83
|
+
(typeof value === 'string' || Array.isArray(value)) && value.length <= max
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
this.rules.set('length', (value, length) =>
|
|
87
|
+
(typeof value === 'string' || Array.isArray(value)) && value.length === length
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Pattern validators
|
|
91
|
+
this.rules.set('pattern', (value, pattern) =>
|
|
92
|
+
typeof value === 'string' && pattern.test(value)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
this.rules.set('alphanumeric', value =>
|
|
96
|
+
typeof value === 'string' && /^[a-zA-Z0-9]+$/.test(value)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
this.rules.set('numeric', value =>
|
|
100
|
+
typeof value === 'string' && /^\d+$/.test(value)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Collection validators
|
|
104
|
+
this.rules.set('in', (value, allowed) =>
|
|
105
|
+
Array.isArray(allowed) && allowed.includes(value)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
this.rules.set('notIn', (value, disallowed) =>
|
|
109
|
+
Array.isArray(disallowed) && !disallowed.includes(value)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Special validators
|
|
113
|
+
this.rules.set('required', value =>
|
|
114
|
+
value !== null && value !== undefined && value !== ''
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
this.rules.set('optional', () => true);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Add custom validator
|
|
122
|
+
*/
|
|
123
|
+
addRule(name, validatorFn) {
|
|
124
|
+
if (this.rules.has(name) || this.customValidators.has(name)) {
|
|
125
|
+
throw new Error(`Validator '${name}' already exists`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.customValidators.set(name, validatorFn);
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Remove validator
|
|
134
|
+
*/
|
|
135
|
+
removeRule(name) {
|
|
136
|
+
this.rules.delete(name);
|
|
137
|
+
this.customValidators.delete(name);
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validate single value against rules
|
|
143
|
+
*/
|
|
144
|
+
validateValue(value, rules) {
|
|
145
|
+
const errors = [];
|
|
146
|
+
|
|
147
|
+
for (const rule of rules) {
|
|
148
|
+
const [ruleName, ...ruleArgs] = Array.isArray(rule) ? rule : [rule];
|
|
149
|
+
|
|
150
|
+
let isValid = false;
|
|
151
|
+
|
|
152
|
+
// Check built-in rules first
|
|
153
|
+
if (this.rules.has(ruleName)) {
|
|
154
|
+
const validator = this.rules.get(ruleName);
|
|
155
|
+
isValid = validator(value, ...ruleArgs);
|
|
156
|
+
}
|
|
157
|
+
// Check custom rules
|
|
158
|
+
else if (this.customValidators.has(ruleName)) {
|
|
159
|
+
const validator = this.customValidators.get(ruleName);
|
|
160
|
+
isValid = validator(value, ...ruleArgs);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
errors.push(`Unknown validation rule: ${ruleName}`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!isValid) {
|
|
168
|
+
errors.push(this._formatError(ruleName, ruleArgs, value));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
isValid: errors.length === 0,
|
|
174
|
+
errors,
|
|
175
|
+
value
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Validate object against schema
|
|
181
|
+
*/
|
|
182
|
+
validateObject(obj, schema) {
|
|
183
|
+
const errors = {};
|
|
184
|
+
let isValid = true;
|
|
185
|
+
|
|
186
|
+
for (const [field, fieldRules] of Object.entries(schema)) {
|
|
187
|
+
const value = obj[field];
|
|
188
|
+
const result = this.validateValue(value, fieldRules);
|
|
189
|
+
|
|
190
|
+
if (!result.isValid) {
|
|
191
|
+
errors[field] = result.errors;
|
|
192
|
+
isValid = false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
isValid,
|
|
198
|
+
errors,
|
|
199
|
+
data: obj
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create schema validator
|
|
205
|
+
*/
|
|
206
|
+
schema(schemaDef) {
|
|
207
|
+
return (data) => this.validateObject(data, schemaDef);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Format validation error
|
|
212
|
+
*/
|
|
213
|
+
_formatError(rule, args, value) {
|
|
214
|
+
const valueStr = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
|
|
215
|
+
|
|
216
|
+
switch (rule) {
|
|
217
|
+
case 'required':
|
|
218
|
+
return 'Field is required';
|
|
219
|
+
case 'min':
|
|
220
|
+
return `Value must be at least ${args[0]}`;
|
|
221
|
+
case 'max':
|
|
222
|
+
return `Value must be at most ${args[0]}`;
|
|
223
|
+
case 'minLength':
|
|
224
|
+
return `Length must be at least ${args[0]} characters`;
|
|
225
|
+
case 'maxLength':
|
|
226
|
+
return `Length must be at most ${args[0]} characters`;
|
|
227
|
+
case 'length':
|
|
228
|
+
return `Length must be exactly ${args[0]} characters`;
|
|
229
|
+
case 'range':
|
|
230
|
+
return `Value must be between ${args[0][0]} and ${args[0][1]}`;
|
|
231
|
+
case 'email':
|
|
232
|
+
return 'Must be a valid email address';
|
|
233
|
+
case 'url':
|
|
234
|
+
return 'Must be a valid URL';
|
|
235
|
+
case 'uuid':
|
|
236
|
+
return 'Must be a valid UUID';
|
|
237
|
+
case 'pattern':
|
|
238
|
+
return 'Value does not match required pattern';
|
|
239
|
+
case 'in':
|
|
240
|
+
return `Value must be one of: ${args[0].join(', ')}`;
|
|
241
|
+
case 'notIn':
|
|
242
|
+
return `Value must not be one of: ${args[0].join(', ')}`;
|
|
243
|
+
default:
|
|
244
|
+
return `Failed validation: ${rule}`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Quick validators (static methods)
|
|
250
|
+
*/
|
|
251
|
+
static isEmail(value) {
|
|
252
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
static isURL(value) {
|
|
256
|
+
try {
|
|
257
|
+
new URL(value);
|
|
258
|
+
return true;
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
static isDate(value) {
|
|
265
|
+
return value instanceof Date && !isNaN(value.getTime());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static isNumber(value) {
|
|
269
|
+
return typeof value === 'number' && !isNaN(value);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
static isString(value) {
|
|
273
|
+
return typeof value === 'string';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
static isArray(value) {
|
|
277
|
+
return Array.isArray(value);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
static isObject(value) {
|
|
281
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
static isEmpty(value) {
|
|
285
|
+
if (value === null || value === undefined) return true;
|
|
286
|
+
if (typeof value === 'string') return value.trim().length === 0;
|
|
287
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
288
|
+
if (typeof value === 'object') return Object.keys(value).length === 0;
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Sanitize functions
|
|
294
|
+
*/
|
|
295
|
+
static trim(value) {
|
|
296
|
+
return typeof value === 'string' ? value.trim() : value;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
static toLowerCase(value) {
|
|
300
|
+
return typeof value === 'string' ? value.toLowerCase() : value;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
static toUpperCase(value) {
|
|
304
|
+
return typeof value === 'string' ? value.toUpperCase() : value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
static toNumber(value) {
|
|
308
|
+
if (typeof value === 'number') return value;
|
|
309
|
+
const num = parseFloat(value);
|
|
310
|
+
return isNaN(num) ? value : num;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
static toBoolean(value) {
|
|
314
|
+
if (typeof value === 'boolean') return value;
|
|
315
|
+
if (typeof value === 'string') {
|
|
316
|
+
return value.toLowerCase() === 'true' || value === '1';
|
|
317
|
+
}
|
|
318
|
+
return !!value;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Export singleton instance
|
|
323
|
+
const validator = new Validator();
|
|
324
|
+
module.exports = validator;
|
|
325
325
|
module.exports.Validator = Validator;
|