@object-ui/core 3.1.5 → 3.3.1

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 (110) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +20 -1
  3. package/dist/actions/ActionRunner.d.ts +9 -0
  4. package/dist/actions/ActionRunner.js +41 -4
  5. package/dist/adapters/ValueDataSource.d.ts +5 -1
  6. package/dist/adapters/ValueDataSource.js +30 -1
  7. package/dist/errors/index.js +2 -3
  8. package/dist/evaluator/ExpressionCache.d.ts +9 -10
  9. package/dist/evaluator/ExpressionCache.js +29 -8
  10. package/dist/evaluator/SafeExpressionParser.d.ts +131 -0
  11. package/dist/evaluator/SafeExpressionParser.js +851 -0
  12. package/dist/evaluator/index.d.ts +1 -0
  13. package/dist/evaluator/index.js +1 -0
  14. package/dist/protocols/DndProtocol.js +2 -14
  15. package/dist/protocols/KeyboardProtocol.js +1 -4
  16. package/dist/protocols/NotificationProtocol.js +3 -13
  17. package/dist/utils/debug.js +2 -1
  18. package/dist/utils/filter-converter.js +25 -5
  19. package/package.json +33 -9
  20. package/.turbo/turbo-build.log +0 -4
  21. package/src/__benchmarks__/core.bench.ts +0 -64
  22. package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
  23. package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
  24. package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
  25. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
  26. package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
  27. package/src/actions/ActionEngine.ts +0 -268
  28. package/src/actions/ActionRunner.ts +0 -717
  29. package/src/actions/TransactionManager.ts +0 -521
  30. package/src/actions/UndoManager.ts +0 -215
  31. package/src/actions/__tests__/ActionEngine.test.ts +0 -206
  32. package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
  33. package/src/actions/__tests__/ActionRunner.test.ts +0 -711
  34. package/src/actions/__tests__/TransactionManager.test.ts +0 -447
  35. package/src/actions/__tests__/UndoManager.test.ts +0 -320
  36. package/src/actions/index.ts +0 -12
  37. package/src/adapters/ApiDataSource.ts +0 -376
  38. package/src/adapters/README.md +0 -180
  39. package/src/adapters/ValueDataSource.ts +0 -438
  40. package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
  41. package/src/adapters/__tests__/ValueDataSource.test.ts +0 -472
  42. package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
  43. package/src/adapters/index.ts +0 -15
  44. package/src/adapters/resolveDataSource.ts +0 -79
  45. package/src/builder/__tests__/schema-builder.test.ts +0 -235
  46. package/src/builder/schema-builder.ts +0 -584
  47. package/src/data-scope/DataScopeManager.ts +0 -269
  48. package/src/data-scope/ViewDataProvider.ts +0 -282
  49. package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
  50. package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
  51. package/src/data-scope/index.ts +0 -24
  52. package/src/errors/__tests__/errors.test.ts +0 -292
  53. package/src/errors/index.ts +0 -270
  54. package/src/evaluator/ExpressionCache.ts +0 -192
  55. package/src/evaluator/ExpressionContext.ts +0 -118
  56. package/src/evaluator/ExpressionEvaluator.ts +0 -315
  57. package/src/evaluator/FormulaFunctions.ts +0 -398
  58. package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
  59. package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
  60. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -131
  61. package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
  62. package/src/evaluator/index.ts +0 -12
  63. package/src/index.ts +0 -38
  64. package/src/protocols/DndProtocol.ts +0 -184
  65. package/src/protocols/KeyboardProtocol.ts +0 -185
  66. package/src/protocols/NotificationProtocol.ts +0 -159
  67. package/src/protocols/ResponsiveProtocol.ts +0 -210
  68. package/src/protocols/SharingProtocol.ts +0 -185
  69. package/src/protocols/index.ts +0 -13
  70. package/src/query/__tests__/query-ast.test.ts +0 -211
  71. package/src/query/__tests__/window-functions.test.ts +0 -275
  72. package/src/query/index.ts +0 -7
  73. package/src/query/query-ast.ts +0 -341
  74. package/src/registry/PluginScopeImpl.ts +0 -259
  75. package/src/registry/PluginSystem.ts +0 -206
  76. package/src/registry/Registry.ts +0 -219
  77. package/src/registry/WidgetRegistry.ts +0 -316
  78. package/src/registry/__tests__/PluginSystem.test.ts +0 -309
  79. package/src/registry/__tests__/Registry.test.ts +0 -293
  80. package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
  81. package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
  82. package/src/theme/ThemeEngine.ts +0 -530
  83. package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
  84. package/src/theme/index.ts +0 -24
  85. package/src/types/index.ts +0 -21
  86. package/src/utils/__tests__/debug-collector.test.ts +0 -102
  87. package/src/utils/__tests__/debug.test.ts +0 -134
  88. package/src/utils/__tests__/expand-fields.test.ts +0 -120
  89. package/src/utils/__tests__/extract-records.test.ts +0 -50
  90. package/src/utils/__tests__/filter-converter.test.ts +0 -118
  91. package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
  92. package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
  93. package/src/utils/debug-collector.ts +0 -100
  94. package/src/utils/debug.ts +0 -147
  95. package/src/utils/expand-fields.ts +0 -76
  96. package/src/utils/extract-records.ts +0 -33
  97. package/src/utils/filter-converter.ts +0 -133
  98. package/src/utils/merge-views-into-objects.ts +0 -36
  99. package/src/utils/normalize-quick-filter.ts +0 -78
  100. package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
  101. package/src/validation/__tests__/schema-validator.test.ts +0 -118
  102. package/src/validation/__tests__/validation-engine.test.ts +0 -102
  103. package/src/validation/index.ts +0 -10
  104. package/src/validation/schema-validator.ts +0 -344
  105. package/src/validation/validation-engine.ts +0 -528
  106. package/src/validation/validators/index.ts +0 -25
  107. package/src/validation/validators/object-validation-engine.ts +0 -722
  108. package/tsconfig.json +0 -15
  109. package/tsconfig.tsbuildinfo +0 -1
  110. package/vitest.config.ts +0 -2
@@ -1,722 +0,0 @@
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 v2.0.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 v2.0.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
- }