@object-ui/core 0.3.1 → 0.5.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.
Files changed (68) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/dist/actions/index.d.ts +1 -1
  3. package/dist/actions/index.js +1 -1
  4. package/dist/evaluator/ExpressionCache.d.ts +101 -0
  5. package/dist/evaluator/ExpressionCache.js +135 -0
  6. package/dist/evaluator/ExpressionEvaluator.d.ts +20 -2
  7. package/dist/evaluator/ExpressionEvaluator.js +34 -14
  8. package/dist/evaluator/index.d.ts +3 -2
  9. package/dist/evaluator/index.js +3 -2
  10. package/dist/index.d.ts +10 -7
  11. package/dist/index.js +9 -7
  12. package/dist/query/index.d.ts +6 -0
  13. package/dist/query/index.js +6 -0
  14. package/dist/query/query-ast.d.ts +32 -0
  15. package/dist/query/query-ast.js +268 -0
  16. package/dist/registry/PluginScopeImpl.d.ts +80 -0
  17. package/dist/registry/PluginScopeImpl.js +243 -0
  18. package/dist/registry/PluginSystem.d.ts +66 -0
  19. package/dist/registry/PluginSystem.js +142 -0
  20. package/dist/registry/Registry.d.ts +73 -4
  21. package/dist/registry/Registry.js +112 -7
  22. package/dist/validation/index.d.ts +9 -0
  23. package/dist/validation/index.js +9 -0
  24. package/dist/validation/validation-engine.d.ts +70 -0
  25. package/dist/validation/validation-engine.js +363 -0
  26. package/dist/validation/validators/index.d.ts +16 -0
  27. package/dist/validation/validators/index.js +16 -0
  28. package/dist/validation/validators/object-validation-engine.d.ts +118 -0
  29. package/dist/validation/validators/object-validation-engine.js +538 -0
  30. package/package.json +13 -5
  31. package/src/actions/index.ts +1 -1
  32. package/src/evaluator/ExpressionCache.ts +192 -0
  33. package/src/evaluator/ExpressionEvaluator.ts +33 -14
  34. package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
  35. package/src/evaluator/index.ts +3 -2
  36. package/src/index.ts +10 -7
  37. package/src/query/__tests__/query-ast.test.ts +211 -0
  38. package/src/query/__tests__/window-functions.test.ts +275 -0
  39. package/src/query/index.ts +7 -0
  40. package/src/query/query-ast.ts +341 -0
  41. package/src/registry/PluginScopeImpl.ts +259 -0
  42. package/src/registry/PluginSystem.ts +161 -0
  43. package/src/registry/Registry.ts +125 -8
  44. package/src/registry/__tests__/PluginSystem.test.ts +226 -0
  45. package/src/registry/__tests__/Registry.test.ts +293 -0
  46. package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
  47. package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
  48. package/src/validation/__tests__/validation-engine.test.ts +102 -0
  49. package/src/validation/index.ts +10 -0
  50. package/src/validation/validation-engine.ts +461 -0
  51. package/src/validation/validators/index.ts +25 -0
  52. package/src/validation/validators/object-validation-engine.ts +722 -0
  53. package/tsconfig.tsbuildinfo +1 -1
  54. package/vitest.config.ts +2 -0
  55. package/src/adapters/index.d.ts +0 -8
  56. package/src/adapters/index.js +0 -10
  57. package/src/builder/schema-builder.d.ts +0 -294
  58. package/src/builder/schema-builder.js +0 -503
  59. package/src/index.d.ts +0 -13
  60. package/src/index.js +0 -16
  61. package/src/registry/Registry.d.ts +0 -56
  62. package/src/registry/Registry.js +0 -43
  63. package/src/types/index.d.ts +0 -19
  64. package/src/types/index.js +0 -8
  65. package/src/utils/filter-converter.d.ts +0 -57
  66. package/src/utils/filter-converter.js +0 -100
  67. package/src/validation/schema-validator.d.ts +0 -94
  68. package/src/validation/schema-validator.js +0 -278
@@ -0,0 +1,722 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * @object-ui/core - Object-Level Validation Engine
11
+ *
12
+ * ObjectStack Spec v0.7.1 compliant validation engine for object-level validation rules.
13
+ * Supports all 9 validation types from the specification:
14
+ * - ScriptValidation
15
+ * - UniquenessValidation
16
+ * - StateMachineValidation
17
+ * - CrossFieldValidation
18
+ * - AsyncValidation
19
+ * - ConditionalValidation
20
+ * - FormatValidation
21
+ * - RangeValidation
22
+ *
23
+ * @module object-validation-engine
24
+ * @packageDocumentation
25
+ */
26
+
27
+ import type {
28
+ ScriptValidation,
29
+ UniquenessValidation,
30
+ StateMachineValidation,
31
+ CrossFieldValidation,
32
+ AsyncValidation,
33
+ ConditionalValidation,
34
+ FormatValidation,
35
+ RangeValidation,
36
+ ObjectValidationRule,
37
+ } from '@object-ui/types';
38
+
39
+ /**
40
+ * Validation context for object-level validations
41
+ */
42
+ export interface ObjectValidationContext {
43
+ /** Current record data */
44
+ record: Record<string, any>;
45
+
46
+ /** Previous record data (for updates) */
47
+ oldRecord?: Record<string, any>;
48
+
49
+ /** Current user */
50
+ user?: Record<string, any>;
51
+
52
+ /** Additional context data */
53
+ [key: string]: any;
54
+ }
55
+
56
+ /**
57
+ * Validation result
58
+ */
59
+ export interface ObjectValidationResult {
60
+ /** Whether validation passed */
61
+ valid: boolean;
62
+
63
+ /** Error message if validation failed */
64
+ message?: string;
65
+
66
+ /** Validation rule that failed */
67
+ rule?: string;
68
+
69
+ /** Severity */
70
+ severity?: 'error' | 'warning' | 'info';
71
+ }
72
+
73
+ /**
74
+ * Validation expression evaluator interface
75
+ */
76
+ export interface ValidationExpressionEvaluator {
77
+ evaluate(expression: string, context: Record<string, any>): any;
78
+ }
79
+
80
+ /**
81
+ * Simple expression evaluator using a simple parser (no dynamic code execution)
82
+ *
83
+ * SECURITY: This implementation parses expressions into an AST and evaluates them
84
+ * without using eval() or new Function(). It supports:
85
+ * - Comparison operators: ==, !=, >, <, >=, <=
86
+ * - Logical operators: &&, ||, !
87
+ * - Property access: record.field, record['field']
88
+ * - Literals: true, false, null, numbers, strings
89
+ *
90
+ * LIMITATIONS:
91
+ * - Single comparison operator per expression (no chaining like a > b > c)
92
+ * - Simple escape sequence handling (doesn't handle escaped backslashes)
93
+ * - Field names in bracket notation cannot contain escaped quotes
94
+ *
95
+ * For more complex expressions, integrate a dedicated library like:
96
+ * - JSONLogic (jsonlogic.com)
97
+ * - filtrex
98
+ *
99
+ * @see https://github.com/objectstack-ai/objectui/blob/main/SECURITY_FIX_SUMMARY.md
100
+ */
101
+ class SimpleExpressionEvaluator implements ValidationExpressionEvaluator {
102
+ evaluate(expression: string, context: Record<string, any>): any {
103
+ try {
104
+ return this.evaluateSafeExpression(expression.trim(), context);
105
+ } catch (error) {
106
+ console.error('Expression evaluation error:', error);
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Safely evaluate an expression without using dynamic code execution
113
+ */
114
+ private evaluateSafeExpression(expr: string, context: Record<string, any>): any {
115
+ // Handle boolean literals
116
+ if (expr === 'true') return true;
117
+ if (expr === 'false') return false;
118
+ if (expr === 'null') return null;
119
+
120
+ // Handle string literals
121
+ if ((expr.startsWith('"') && expr.endsWith('"')) ||
122
+ (expr.startsWith("'") && expr.endsWith("'"))) {
123
+ return expr.slice(1, -1);
124
+ }
125
+
126
+ // Handle numeric literals
127
+ if (/^-?\d+(\.\d+)?$/.test(expr)) {
128
+ return parseFloat(expr);
129
+ }
130
+
131
+ // Handle logical NOT
132
+ if (expr.startsWith('!')) {
133
+ return !this.evaluateSafeExpression(expr.slice(1).trim(), context);
134
+ }
135
+
136
+ // Handle logical AND
137
+ if (expr.includes('&&')) {
138
+ const parts = this.splitOnOperator(expr, '&&');
139
+ return parts.every(part => this.evaluateSafeExpression(part, context));
140
+ }
141
+
142
+ // Handle logical OR
143
+ if (expr.includes('||')) {
144
+ const parts = this.splitOnOperator(expr, '||');
145
+ return parts.some(part => this.evaluateSafeExpression(part, context));
146
+ }
147
+
148
+ // Handle comparison operators
149
+ const comparisonMatch = expr.match(/^(.+?)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/);
150
+ if (comparisonMatch) {
151
+ const [, left, op, right] = comparisonMatch;
152
+ const leftVal = this.evaluateSafeExpression(left.trim(), context);
153
+ const rightVal = this.evaluateSafeExpression(right.trim(), context);
154
+
155
+ switch (op) {
156
+ case '===':
157
+ return leftVal === rightVal;
158
+ case '==':
159
+ // Use loose equality for backward compatibility with existing expressions
160
+ // eslint-disable-next-line eqeqeq
161
+ return leftVal == rightVal;
162
+ case '!==':
163
+ return leftVal !== rightVal;
164
+ case '!=':
165
+ // Use loose inequality for backward compatibility with existing expressions
166
+ // eslint-disable-next-line eqeqeq
167
+ return leftVal != rightVal;
168
+ case '>': return leftVal > rightVal;
169
+ case '<': return leftVal < rightVal;
170
+ case '>=': return leftVal >= rightVal;
171
+ case '<=': return leftVal <= rightVal;
172
+ default: return false;
173
+ }
174
+ }
175
+
176
+ // Handle property access (e.g., record.field or context.field)
177
+ return this.getValueFromContext(expr, context);
178
+ }
179
+
180
+ /**
181
+ * Split expression on operator, respecting parentheses and quotes
182
+ */
183
+ private splitOnOperator(expr: string, operator: string): string[] {
184
+ const parts: string[] = [];
185
+ let current = '';
186
+ let depth = 0;
187
+ let inString = false;
188
+ let stringChar = '';
189
+
190
+ for (let i = 0; i < expr.length; i++) {
191
+ const char = expr[i];
192
+ const nextChar = expr[i + 1];
193
+ const prevChar = i > 0 ? expr[i - 1] : '';
194
+
195
+ // Handle string quotes, checking for escape sequences
196
+ if ((char === '"' || char === "'") && !inString) {
197
+ inString = true;
198
+ stringChar = char;
199
+ } else if (char === stringChar && inString && prevChar !== '\\') {
200
+ // Only close string if quote is not escaped
201
+ inString = false;
202
+ }
203
+
204
+ if (!inString) {
205
+ if (char === '(') depth++;
206
+ if (char === ')') depth--;
207
+
208
+ if (depth === 0 && char === operator[0] && nextChar === operator[1]) {
209
+ parts.push(current.trim());
210
+ current = '';
211
+ i++; // Skip next character
212
+ continue;
213
+ }
214
+ }
215
+
216
+ current += char;
217
+ }
218
+
219
+ if (current) {
220
+ parts.push(current.trim());
221
+ }
222
+
223
+ return parts;
224
+ }
225
+
226
+ /**
227
+ * Get value from context by path (e.g., "record.age" or "age")
228
+ */
229
+ private getValueFromContext(path: string, context: Record<string, any>): any {
230
+ // Handle bracket notation: record['field']
231
+ const bracketMatch = path.match(/^(\w+)\['([^']+)'\]$/);
232
+ if (bracketMatch) {
233
+ const [, obj, field] = bracketMatch;
234
+ return context[obj]?.[field];
235
+ }
236
+
237
+ // Handle dot notation: record.field or just field
238
+ const parts = path.split('.');
239
+ let value: any = context;
240
+
241
+ for (const part of parts) {
242
+ if (value && typeof value === 'object' && part in value) {
243
+ value = value[part];
244
+ } else {
245
+ // Try direct context access for simple identifiers
246
+ if (parts.length === 1 && part in context) {
247
+ return context[part];
248
+ }
249
+ return undefined;
250
+ }
251
+ }
252
+
253
+ return value;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Object-Level Validation Engine
259
+ * Implements ObjectStack Spec v0.7.1 validation framework
260
+ */
261
+ export class ObjectValidationEngine {
262
+ private expressionEvaluator: ValidationExpressionEvaluator;
263
+ private uniquenessChecker?: (
264
+ fields: string[],
265
+ values: Record<string, any>,
266
+ scope?: string,
267
+ context?: ObjectValidationContext
268
+ ) => Promise<boolean>;
269
+
270
+ constructor(
271
+ expressionEvaluator?: ValidationExpressionEvaluator,
272
+ uniquenessChecker?: (
273
+ fields: string[],
274
+ values: Record<string, any>,
275
+ scope?: string,
276
+ context?: ObjectValidationContext
277
+ ) => Promise<boolean>
278
+ ) {
279
+ this.expressionEvaluator = expressionEvaluator || new SimpleExpressionEvaluator();
280
+ this.uniquenessChecker = uniquenessChecker;
281
+ }
282
+
283
+ /**
284
+ * Validate a record against a set of validation rules
285
+ */
286
+ async validateRecord(
287
+ rules: ObjectValidationRule[],
288
+ context: ObjectValidationContext,
289
+ event: 'insert' | 'update' | 'delete' = 'insert'
290
+ ): Promise<ObjectValidationResult[]> {
291
+ const results: ObjectValidationResult[] = [];
292
+
293
+ for (const rule of rules) {
294
+ // Check if rule is active
295
+ if (!rule.active) {
296
+ continue;
297
+ }
298
+
299
+ // Check if rule applies to this event
300
+ if (!rule.events.includes(event)) {
301
+ continue;
302
+ }
303
+
304
+ const result = await this.validateRule(rule, context);
305
+ if (!result.valid) {
306
+ results.push(result);
307
+ }
308
+ }
309
+
310
+ return results;
311
+ }
312
+
313
+ /**
314
+ * Validate a single rule
315
+ */
316
+ private async validateRule(
317
+ rule: ObjectValidationRule,
318
+ context: ObjectValidationContext
319
+ ): Promise<ObjectValidationResult> {
320
+ switch (rule.type) {
321
+ case 'script':
322
+ return this.validateScript(rule, context);
323
+
324
+ case 'unique':
325
+ return this.validateUniqueness(rule, context);
326
+
327
+ case 'state_machine':
328
+ return this.validateStateMachine(rule, context);
329
+
330
+ case 'cross_field':
331
+ return this.validateCrossField(rule, context);
332
+
333
+ case 'async':
334
+ return this.validateAsync(rule, context);
335
+
336
+ case 'conditional':
337
+ return this.validateConditional(rule, context);
338
+
339
+ case 'format':
340
+ return this.validateFormat(rule, context);
341
+
342
+ case 'range':
343
+ return this.validateRange(rule, context);
344
+
345
+ default:
346
+ return {
347
+ valid: true,
348
+ message: `Unknown validation type: ${(rule as any).type}`,
349
+ };
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Validate script-based rule
355
+ */
356
+ private validateScript(
357
+ rule: ScriptValidation,
358
+ context: ObjectValidationContext
359
+ ): ObjectValidationResult {
360
+ try {
361
+ const result = this.expressionEvaluator.evaluate(rule.condition, context.record);
362
+
363
+ if (!result) {
364
+ return {
365
+ valid: false,
366
+ message: rule.message,
367
+ rule: rule.name,
368
+ severity: rule.severity,
369
+ };
370
+ }
371
+
372
+ return { valid: true };
373
+ } catch (error) {
374
+ return {
375
+ valid: false,
376
+ message: `Script evaluation error: ${error}`,
377
+ rule: rule.name,
378
+ severity: 'error',
379
+ };
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Validate uniqueness constraint
385
+ */
386
+ private async validateUniqueness(
387
+ rule: UniquenessValidation,
388
+ context: ObjectValidationContext
389
+ ): Promise<ObjectValidationResult> {
390
+ if (!this.uniquenessChecker) {
391
+ console.warn('Uniqueness checker not configured');
392
+ return { valid: true };
393
+ }
394
+
395
+ const values: Record<string, any> = {};
396
+ for (const field of rule.fields) {
397
+ values[field] = context.record[field];
398
+ }
399
+
400
+ const isUnique = await this.uniquenessChecker(
401
+ rule.fields,
402
+ values,
403
+ rule.scope,
404
+ context
405
+ );
406
+
407
+ if (!isUnique) {
408
+ return {
409
+ valid: false,
410
+ message: rule.message,
411
+ rule: rule.name,
412
+ severity: rule.severity,
413
+ };
414
+ }
415
+
416
+ return { valid: true };
417
+ }
418
+
419
+ /**
420
+ * Validate state machine transitions
421
+ */
422
+ private validateStateMachine(
423
+ rule: StateMachineValidation,
424
+ context: ObjectValidationContext
425
+ ): ObjectValidationResult {
426
+ const currentState = context.record[rule.stateField];
427
+ const previousState = context.oldRecord?.[rule.stateField];
428
+
429
+ // If no previous state (insert), allow any state
430
+ if (!previousState) {
431
+ return { valid: true };
432
+ }
433
+
434
+ // Check if transition is allowed
435
+ for (const transition of rule.transitions) {
436
+ const fromStates = Array.isArray(transition.from) ? transition.from : [transition.from];
437
+
438
+ if (!fromStates.includes(previousState)) {
439
+ continue;
440
+ }
441
+
442
+ if (transition.to !== currentState) {
443
+ continue;
444
+ }
445
+
446
+ // Check condition if specified
447
+ if (transition.condition) {
448
+ const conditionMet = this.expressionEvaluator.evaluate(
449
+ transition.condition,
450
+ context.record
451
+ );
452
+ if (!conditionMet) {
453
+ continue;
454
+ }
455
+ }
456
+
457
+ // Valid transition found
458
+ return { valid: true };
459
+ }
460
+
461
+ return {
462
+ valid: false,
463
+ message: rule.message || `Invalid state transition from ${previousState} to ${currentState}`,
464
+ rule: rule.name,
465
+ severity: rule.severity,
466
+ };
467
+ }
468
+
469
+ /**
470
+ * Validate cross-field constraints
471
+ */
472
+ private validateCrossField(
473
+ rule: CrossFieldValidation,
474
+ context: ObjectValidationContext
475
+ ): ObjectValidationResult {
476
+ try {
477
+ const result = this.expressionEvaluator.evaluate(rule.condition, context.record);
478
+
479
+ if (!result) {
480
+ return {
481
+ valid: false,
482
+ message: rule.message,
483
+ rule: rule.name,
484
+ severity: rule.severity,
485
+ };
486
+ }
487
+
488
+ return { valid: true };
489
+ } catch (error) {
490
+ return {
491
+ valid: false,
492
+ message: `Cross-field validation error: ${error}`,
493
+ rule: rule.name,
494
+ severity: 'error',
495
+ };
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Validate async/remote validation
501
+ */
502
+ private async validateAsync(
503
+ rule: AsyncValidation,
504
+ context: ObjectValidationContext
505
+ ): Promise<ObjectValidationResult> {
506
+ try {
507
+ const method = rule.method || 'POST';
508
+ const response = await fetch(rule.endpoint, {
509
+ method,
510
+ headers: {
511
+ 'Content-Type': 'application/json',
512
+ },
513
+ body: method !== 'GET' ? JSON.stringify(context.record) : undefined,
514
+ });
515
+
516
+ const data = await response.json();
517
+
518
+ if (!data.valid) {
519
+ return {
520
+ valid: false,
521
+ message: data.message || rule.message,
522
+ rule: rule.name,
523
+ severity: rule.severity,
524
+ };
525
+ }
526
+
527
+ return { valid: true };
528
+ } catch (error) {
529
+ return {
530
+ valid: false,
531
+ message: `Async validation error: ${error}`,
532
+ rule: rule.name,
533
+ severity: 'error',
534
+ };
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Validate conditional rules
540
+ */
541
+ private async validateConditional(
542
+ rule: ConditionalValidation,
543
+ context: ObjectValidationContext
544
+ ): Promise<ObjectValidationResult> {
545
+ try {
546
+ const conditionMet = this.expressionEvaluator.evaluate(rule.condition, context.record);
547
+
548
+ if (!conditionMet) {
549
+ // Condition not met, validation passes
550
+ return { valid: true };
551
+ }
552
+
553
+ // Condition met, validate nested rules
554
+ for (const nestedRule of rule.rules) {
555
+ const result = await this.validateRule(nestedRule, context);
556
+ if (!result.valid) {
557
+ return result;
558
+ }
559
+ }
560
+
561
+ return { valid: true };
562
+ } catch (error) {
563
+ return {
564
+ valid: false,
565
+ message: `Conditional validation error: ${error}`,
566
+ rule: rule.name,
567
+ severity: 'error',
568
+ };
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Validate format/pattern
574
+ */
575
+ private validateFormat(
576
+ rule: FormatValidation,
577
+ context: ObjectValidationContext
578
+ ): ObjectValidationResult {
579
+ const value = context.record[rule.field];
580
+
581
+ if (value === null || value === undefined || value === '') {
582
+ return { valid: true };
583
+ }
584
+
585
+ try {
586
+ let pattern: RegExp;
587
+
588
+ if (rule.format) {
589
+ // Use predefined format
590
+ pattern = this.getPredefinedPattern(rule.format);
591
+ } else if (typeof rule.pattern === 'string') {
592
+ pattern = new RegExp(rule.pattern, rule.flags);
593
+ } else {
594
+ pattern = rule.pattern as RegExp;
595
+ }
596
+
597
+ if (!pattern.test(String(value))) {
598
+ return {
599
+ valid: false,
600
+ message: rule.message || `Invalid format for ${rule.field}`,
601
+ rule: rule.name,
602
+ severity: rule.severity,
603
+ };
604
+ }
605
+
606
+ return { valid: true };
607
+ } catch (error) {
608
+ return {
609
+ valid: false,
610
+ message: `Format validation error: ${error}`,
611
+ rule: rule.name,
612
+ severity: 'error',
613
+ };
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Validate range constraints
619
+ */
620
+ private validateRange(
621
+ rule: RangeValidation,
622
+ context: ObjectValidationContext
623
+ ): ObjectValidationResult {
624
+ const value = context.record[rule.field];
625
+
626
+ if (value === null || value === undefined) {
627
+ return { valid: true };
628
+ }
629
+
630
+ try {
631
+ // Convert to comparable values
632
+ let compareValue: number | Date;
633
+ let minValue: number | Date | undefined;
634
+ let maxValue: number | Date | undefined;
635
+
636
+ if (value instanceof Date || typeof value === 'string') {
637
+ compareValue = value instanceof Date ? value : new Date(value);
638
+ minValue = rule.min ? (rule.min instanceof Date ? rule.min : new Date(rule.min)) : undefined;
639
+ maxValue = rule.max ? (rule.max instanceof Date ? rule.max : new Date(rule.max)) : undefined;
640
+ } else {
641
+ compareValue = Number(value);
642
+ minValue = rule.min !== undefined ? Number(rule.min) : undefined;
643
+ maxValue = rule.max !== undefined ? Number(rule.max) : undefined;
644
+ }
645
+
646
+ // Check minimum
647
+ if (minValue !== undefined) {
648
+ const fails = rule.minExclusive
649
+ ? compareValue <= minValue
650
+ : compareValue < minValue;
651
+
652
+ if (fails) {
653
+ return {
654
+ valid: false,
655
+ message: rule.message || `Value must be ${rule.minExclusive ? 'greater than' : 'at least'} ${rule.min}`,
656
+ rule: rule.name,
657
+ severity: rule.severity,
658
+ };
659
+ }
660
+ }
661
+
662
+ // Check maximum
663
+ if (maxValue !== undefined) {
664
+ const fails = rule.maxExclusive
665
+ ? compareValue >= maxValue
666
+ : compareValue > maxValue;
667
+
668
+ if (fails) {
669
+ return {
670
+ valid: false,
671
+ message: rule.message || `Value must be ${rule.maxExclusive ? 'less than' : 'at most'} ${rule.max}`,
672
+ rule: rule.name,
673
+ severity: rule.severity,
674
+ };
675
+ }
676
+ }
677
+
678
+ return { valid: true };
679
+ } catch (error) {
680
+ return {
681
+ valid: false,
682
+ message: `Range validation error: ${error}`,
683
+ rule: rule.name,
684
+ severity: 'error',
685
+ };
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Get predefined regex pattern
691
+ */
692
+ private getPredefinedPattern(format: string): RegExp {
693
+ const patterns: Record<string, RegExp> = {
694
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
695
+ url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)$/,
696
+ phone: /^[\d\s\-+()]+$/,
697
+ ipv4: /^(\d{1,3}\.){3}\d{1,3}$/,
698
+ ipv6: /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i,
699
+ uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
700
+ iso_date: /^\d{4}-\d{2}-\d{2}$/,
701
+ credit_card: /^\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}$/,
702
+ };
703
+
704
+ return patterns[format] || /.*/;
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Default instance
710
+ */
711
+ export const defaultObjectValidationEngine = new ObjectValidationEngine();
712
+
713
+ /**
714
+ * Convenience function to validate a record
715
+ */
716
+ export async function validateRecord(
717
+ rules: ObjectValidationRule[],
718
+ context: ObjectValidationContext,
719
+ event: 'insert' | 'update' | 'delete' = 'insert'
720
+ ): Promise<ObjectValidationResult[]> {
721
+ return defaultObjectValidationEngine.validateRecord(rules, context, event);
722
+ }