@object-ui/core 3.3.0 → 3.3.2

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