@objectql/core 4.0.2 → 4.0.4

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 (98) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +4 -4
  4. package/dist/app.d.ts +9 -6
  5. package/dist/app.js +151 -29
  6. package/dist/app.js.map +1 -1
  7. package/dist/index.d.ts +6 -9
  8. package/dist/index.js +2 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/optimizations/CompiledHookManager.d.ts +55 -0
  11. package/dist/optimizations/CompiledHookManager.js +164 -0
  12. package/dist/optimizations/CompiledHookManager.js.map +1 -0
  13. package/dist/optimizations/DependencyGraph.d.ts +82 -0
  14. package/dist/optimizations/DependencyGraph.js +211 -0
  15. package/dist/optimizations/DependencyGraph.js.map +1 -0
  16. package/dist/optimizations/GlobalConnectionPool.d.ts +89 -0
  17. package/dist/optimizations/GlobalConnectionPool.js +193 -0
  18. package/dist/optimizations/GlobalConnectionPool.js.map +1 -0
  19. package/dist/optimizations/LazyMetadataLoader.d.ts +75 -0
  20. package/dist/optimizations/LazyMetadataLoader.js +149 -0
  21. package/dist/optimizations/LazyMetadataLoader.js.map +1 -0
  22. package/dist/optimizations/OptimizedMetadataRegistry.d.ts +26 -0
  23. package/dist/optimizations/OptimizedMetadataRegistry.js +117 -0
  24. package/dist/optimizations/OptimizedMetadataRegistry.js.map +1 -0
  25. package/dist/optimizations/OptimizedValidationEngine.d.ts +73 -0
  26. package/dist/optimizations/OptimizedValidationEngine.js +141 -0
  27. package/dist/optimizations/OptimizedValidationEngine.js.map +1 -0
  28. package/dist/optimizations/QueryCompiler.d.ts +51 -0
  29. package/dist/optimizations/QueryCompiler.js +216 -0
  30. package/dist/optimizations/QueryCompiler.js.map +1 -0
  31. package/dist/optimizations/SQLQueryOptimizer.d.ts +96 -0
  32. package/dist/optimizations/SQLQueryOptimizer.js +265 -0
  33. package/dist/optimizations/SQLQueryOptimizer.js.map +1 -0
  34. package/dist/optimizations/index.d.ts +32 -0
  35. package/dist/optimizations/index.js +44 -0
  36. package/dist/optimizations/index.js.map +1 -0
  37. package/dist/plugin.d.ts +8 -7
  38. package/dist/plugin.js +57 -22
  39. package/dist/plugin.js.map +1 -1
  40. package/dist/query/query-analyzer.js.map +1 -1
  41. package/dist/query/query-builder.d.ts +6 -1
  42. package/dist/query/query-builder.js +21 -5
  43. package/dist/query/query-builder.js.map +1 -1
  44. package/dist/query/query-service.js.map +1 -1
  45. package/dist/repository.d.ts +2 -0
  46. package/dist/repository.js +15 -9
  47. package/dist/repository.js.map +1 -1
  48. package/jest.config.js +3 -3
  49. package/package.json +8 -5
  50. package/src/app.ts +173 -47
  51. package/src/index.ts +8 -9
  52. package/src/optimizations/CompiledHookManager.ts +185 -0
  53. package/src/optimizations/DependencyGraph.ts +255 -0
  54. package/src/optimizations/GlobalConnectionPool.ts +251 -0
  55. package/src/optimizations/LazyMetadataLoader.ts +180 -0
  56. package/src/optimizations/OptimizedMetadataRegistry.ts +132 -0
  57. package/src/optimizations/OptimizedValidationEngine.ts +172 -0
  58. package/src/optimizations/QueryCompiler.ts +242 -0
  59. package/src/optimizations/SQLQueryOptimizer.ts +329 -0
  60. package/src/optimizations/index.ts +34 -0
  61. package/src/plugin.ts +71 -28
  62. package/src/query/query-analyzer.ts +1 -1
  63. package/src/query/query-builder.ts +21 -7
  64. package/src/query/query-service.ts +1 -1
  65. package/src/repository.ts +25 -13
  66. package/test/__mocks__/@objectstack/runtime.ts +8 -8
  67. package/test/app.test.ts +9 -7
  68. package/test/optimizations.test.ts +440 -0
  69. package/test/plugin-integration.test.ts +30 -19
  70. package/tsconfig.json +4 -6
  71. package/tsconfig.tsbuildinfo +1 -1
  72. package/dist/ai-agent.d.ts +0 -176
  73. package/dist/ai-agent.js +0 -722
  74. package/dist/ai-agent.js.map +0 -1
  75. package/dist/formula-engine.d.ts +0 -102
  76. package/dist/formula-engine.js +0 -433
  77. package/dist/formula-engine.js.map +0 -1
  78. package/dist/formula-plugin.d.ts +0 -52
  79. package/dist/formula-plugin.js +0 -107
  80. package/dist/formula-plugin.js.map +0 -1
  81. package/dist/validator-plugin.d.ts +0 -56
  82. package/dist/validator-plugin.js +0 -106
  83. package/dist/validator-plugin.js.map +0 -1
  84. package/dist/validator.d.ts +0 -80
  85. package/dist/validator.js +0 -625
  86. package/dist/validator.js.map +0 -1
  87. package/src/ai-agent.ts +0 -868
  88. package/src/formula-engine.ts +0 -572
  89. package/src/formula-plugin.ts +0 -141
  90. package/src/validator-plugin.ts +0 -140
  91. package/src/validator.ts +0 -743
  92. package/test/formula-engine.test.ts +0 -725
  93. package/test/formula-integration.test.ts +0 -286
  94. package/test/formula-plugin.test.ts +0 -197
  95. package/test/formula-spec-compliance.test.ts +0 -258
  96. package/test/validation-spec-compliance.test.ts +0 -440
  97. package/test/validator-plugin.test.ts +0 -126
  98. package/test/validator.test.ts +0 -440
@@ -1,258 +0,0 @@
1
- /**
2
- * ObjectQL
3
- * Copyright (c) 2026-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
- * Formula Specification Compliance Tests
11
- *
12
- * Ensures that formula fields work according to the latest specification,
13
- * supporting both 'expression' (spec-compliant) and 'formula' (legacy) properties.
14
- */
15
-
16
- import { FormulaEngine } from '../src/formula-engine';
17
- import { Repository } from '../src/repository';
18
- import type { ObjectConfig, FormulaContext } from '@objectql/types';
19
-
20
- describe('Formula Specification Compliance', () => {
21
- let engine: FormulaEngine;
22
- let baseContext: FormulaContext;
23
-
24
- beforeEach(() => {
25
- engine = new FormulaEngine();
26
-
27
- const now = new Date('2026-01-25T10:00:00Z');
28
- baseContext = {
29
- record: {
30
- first_name: 'John',
31
- last_name: 'Doe',
32
- quantity: 10,
33
- unit_price: 25.5,
34
- },
35
- system: {
36
- today: new Date('2026-01-25'),
37
- now: now,
38
- year: 2026,
39
- month: 1,
40
- day: 25,
41
- hour: 10,
42
- minute: 0,
43
- second: 0,
44
- },
45
- current_user: {
46
- id: 'user-123',
47
- name: 'Admin User',
48
- email: 'admin@example.com',
49
- role: 'admin',
50
- },
51
- is_new: false,
52
- record_id: 'rec-456',
53
- };
54
- });
55
-
56
- describe('Specification-compliant property: expression', () => {
57
- it('should evaluate formula using "expression" property', () => {
58
- const result = engine.evaluate(
59
- 'first_name + " " + last_name',
60
- baseContext,
61
- 'text'
62
- );
63
-
64
- expect(result.success).toBe(true);
65
- expect(result.value).toBe('John Doe');
66
- });
67
-
68
- it('should calculate numeric formulas using "expression"', () => {
69
- const result = engine.evaluate(
70
- 'quantity * unit_price',
71
- baseContext,
72
- 'currency'
73
- );
74
-
75
- expect(result.success).toBe(true);
76
- expect(result.value).toBe(255);
77
- });
78
-
79
- it('should support system variables in expression', () => {
80
- const result = engine.evaluate(
81
- '$year',
82
- baseContext,
83
- 'number'
84
- );
85
-
86
- expect(result.success).toBe(true);
87
- expect(result.value).toBe(2026);
88
- });
89
-
90
- it('should support conditional expressions', () => {
91
- const result = engine.evaluate(
92
- 'quantity > 5 ? "High" : "Low"',
93
- baseContext,
94
- 'text'
95
- );
96
-
97
- expect(result.success).toBe(true);
98
- expect(result.value).toBe('High');
99
- });
100
- });
101
-
102
- describe('Specification examples from formula.mdx', () => {
103
- it('should calculate full_name as per spec example', () => {
104
- // From spec line 105-110
105
- const result = engine.evaluate(
106
- 'first_name + " " + last_name',
107
- baseContext,
108
- 'text'
109
- );
110
-
111
- expect(result.success).toBe(true);
112
- expect(result.value).toBe('John Doe');
113
- });
114
-
115
- it('should calculate total_amount with formula fields', () => {
116
- // From spec: total_amount: expression: "quantity * unit_price"
117
- const result = engine.evaluate(
118
- 'quantity * unit_price',
119
- baseContext,
120
- 'currency'
121
- );
122
-
123
- expect(result.success).toBe(true);
124
- expect(result.value).toBe(255);
125
- });
126
-
127
- it('should handle template literals', () => {
128
- // From spec line 216-218
129
- const result = engine.evaluate(
130
- '`Hello, ${first_name}!`',
131
- baseContext,
132
- 'text'
133
- );
134
-
135
- expect(result.success).toBe(true);
136
- expect(result.value).toBe('Hello, John!');
137
- });
138
-
139
- it('should support Math functions', () => {
140
- // From spec line 435-438
141
- const context = {
142
- ...baseContext,
143
- record: { ...baseContext.record, price: 99.7 }
144
- };
145
-
146
- const result = engine.evaluate(
147
- 'Math.round(price)',
148
- context,
149
- 'number'
150
- );
151
-
152
- expect(result.success).toBe(true);
153
- expect(result.value).toBe(100);
154
- });
155
- });
156
-
157
- describe('Error handling per specification', () => {
158
- it('should handle division by zero safely', () => {
159
- // From spec section 7.1
160
- const context = {
161
- ...baseContext,
162
- record: { total: 100, count: 0 }
163
- };
164
-
165
- const result = engine.evaluate(
166
- 'count !== 0 ? total / count : 0',
167
- context,
168
- 'number'
169
- );
170
-
171
- expect(result.success).toBe(true);
172
- expect(result.value).toBe(0);
173
- });
174
-
175
- it('should detect division by zero without guard', () => {
176
- const context = {
177
- ...baseContext,
178
- record: { total: 100, count: 0 }
179
- };
180
-
181
- const result = engine.evaluate(
182
- 'total / count',
183
- context,
184
- 'number'
185
- );
186
-
187
- expect(result.success).toBe(false);
188
- expect(result.error).toContain('Infinity');
189
- });
190
-
191
- it('should handle null values with optional chaining', () => {
192
- // From spec section 7.2
193
- const context = {
194
- ...baseContext,
195
- record: { account: null }
196
- };
197
-
198
- const result = engine.evaluate(
199
- 'account?.name ?? "No Account"',
200
- context,
201
- 'text'
202
- );
203
-
204
- expect(result.success).toBe(true);
205
- expect(result.value).toBe('No Account');
206
- });
207
- });
208
-
209
- describe('Data type coercion per specification', () => {
210
- it('should coerce to number type', () => {
211
- const context = {
212
- ...baseContext,
213
- record: { text_value: '42' }
214
- };
215
-
216
- const result = engine.evaluate(
217
- 'Number(text_value) || 0',
218
- context,
219
- 'number'
220
- );
221
-
222
- expect(result.success).toBe(true);
223
- expect(result.value).toBe(42);
224
- });
225
-
226
- it('should coerce to text type', () => {
227
- const context = {
228
- ...baseContext,
229
- record: { numeric_value: 123 }
230
- };
231
-
232
- const result = engine.evaluate(
233
- 'String(numeric_value)',
234
- context,
235
- 'text'
236
- );
237
-
238
- expect(result.success).toBe(true);
239
- expect(result.value).toBe('123');
240
- });
241
-
242
- it('should coerce to boolean type', () => {
243
- const context = {
244
- ...baseContext,
245
- record: { value: 1 }
246
- };
247
-
248
- const result = engine.evaluate(
249
- 'Boolean(value)',
250
- context,
251
- 'boolean'
252
- );
253
-
254
- expect(result.success).toBe(true);
255
- expect(result.value).toBe(true);
256
- });
257
- });
258
- });
@@ -1,440 +0,0 @@
1
- /**
2
- * ObjectQL
3
- * Copyright (c) 2026-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
- * Validation Specification Compliance Tests
11
- *
12
- * Ensures that validation rules work according to the latest specification.
13
- */
14
-
15
- import { Validator } from '../src/validator';
16
- import type {
17
- ValidationContext,
18
- CrossFieldValidationRule,
19
- StateMachineValidationRule,
20
- FieldConfig,
21
- } from '@objectql/types';
22
-
23
- describe('Validation Specification Compliance', () => {
24
- let validator: Validator;
25
-
26
- beforeEach(() => {
27
- validator = new Validator();
28
- });
29
-
30
- describe('Cross-field validation per specification', () => {
31
- it('should validate date range as per spec example', async () => {
32
- // From spec lines 219-235
33
- const rule: CrossFieldValidationRule = {
34
- name: 'end_after_start',
35
- type: 'cross_field',
36
- rule: {
37
- field: 'end_date',
38
- operator: '>=',
39
- compare_to: 'start_date',
40
- },
41
- message: 'End date must be on or after start date',
42
- error_code: 'INVALID_DATE_RANGE',
43
- severity: 'error',
44
- };
45
-
46
- const validContext: ValidationContext = {
47
- operation: 'create',
48
- record: {
49
- start_date: new Date('2026-01-01'),
50
- end_date: new Date('2026-12-31'),
51
- },
52
- };
53
-
54
- const result = await validator.validate([rule], validContext);
55
- expect(result.valid).toBe(true);
56
- });
57
-
58
- it('should reject invalid date range', async () => {
59
- const rule: CrossFieldValidationRule = {
60
- name: 'end_after_start',
61
- type: 'cross_field',
62
- rule: {
63
- field: 'end_date',
64
- operator: '>=',
65
- compare_to: 'start_date',
66
- },
67
- message: 'End date must be on or after start date',
68
- error_code: 'INVALID_DATE_RANGE',
69
- };
70
-
71
- const invalidContext: ValidationContext = {
72
- operation: 'create',
73
- record: {
74
- start_date: new Date('2026-12-31'),
75
- end_date: new Date('2026-01-01'),
76
- },
77
- };
78
-
79
- const result = await validator.validate([rule], invalidContext);
80
- expect(result.valid).toBe(false);
81
- expect(result.errors[0].error_code).toBe('INVALID_DATE_RANGE');
82
- });
83
-
84
- it('should validate conditional requirements as per spec', async () => {
85
- // From spec lines 238-258
86
- const rule: CrossFieldValidationRule = {
87
- name: 'reason_required_for_rejection',
88
- type: 'cross_field',
89
- rule: {
90
- if: {
91
- field: 'status',
92
- operator: '=',
93
- value: 'rejected',
94
- },
95
- then: {
96
- field: 'rejection_reason',
97
- operator: '!=',
98
- value: null,
99
- },
100
- },
101
- message: 'Rejection reason is required when status is rejected',
102
- error_code: 'REJECTION_REASON_REQUIRED',
103
- };
104
-
105
- const validContext: ValidationContext = {
106
- operation: 'create',
107
- record: {
108
- status: 'rejected',
109
- rejection_reason: 'Does not meet requirements',
110
- },
111
- };
112
-
113
- const result = await validator.validate([rule], validContext);
114
- expect(result.valid).toBe(true);
115
- });
116
- });
117
-
118
- describe('State machine validation per specification', () => {
119
- it('should validate state transitions as per spec example', async () => {
120
- // From spec lines 446-497
121
- const rule: StateMachineValidationRule = {
122
- name: 'order_status_flow',
123
- type: 'state_machine',
124
- field: 'status',
125
- message: 'Invalid status transition',
126
- transitions: {
127
- draft: {
128
- allowed_next: ['submitted', 'cancelled'],
129
- },
130
- submitted: {
131
- allowed_next: ['approved', 'rejected'],
132
- },
133
- approved: {
134
- allowed_next: ['processing', 'cancelled'],
135
- },
136
- },
137
- };
138
-
139
- const validContext: ValidationContext = {
140
- operation: 'update',
141
- record: { status: 'submitted' },
142
- previousRecord: { status: 'draft' },
143
- };
144
-
145
- const result = await validator.validate([rule], validContext);
146
- expect(result.valid).toBe(true);
147
- });
148
-
149
- it('should reject invalid state transitions', async () => {
150
- const rule: StateMachineValidationRule = {
151
- name: 'order_status_flow',
152
- type: 'state_machine',
153
- field: 'status',
154
- message: 'Invalid status transition from {{old_status}} to {{new_status}}',
155
- transitions: {
156
- draft: {
157
- allowed_next: ['submitted', 'cancelled'],
158
- },
159
- submitted: {
160
- allowed_next: ['approved', 'rejected'],
161
- },
162
- },
163
- };
164
-
165
- const invalidContext: ValidationContext = {
166
- operation: 'update',
167
- record: { status: 'approved' },
168
- previousRecord: { status: 'draft' },
169
- };
170
-
171
- const result = await validator.validate([rule], invalidContext);
172
- expect(result.valid).toBe(false);
173
- expect(result.errors[0].message).toContain('draft');
174
- expect(result.errors[0].message).toContain('approved');
175
- });
176
-
177
- it('should handle terminal states correctly', async () => {
178
- const rule: StateMachineValidationRule = {
179
- name: 'status_transition',
180
- type: 'state_machine',
181
- field: 'status',
182
- message: 'Invalid status transition',
183
- transitions: {
184
- completed: {
185
- allowed_next: [],
186
- is_terminal: true,
187
- },
188
- },
189
- };
190
-
191
- const invalidContext: ValidationContext = {
192
- operation: 'update',
193
- record: { status: 'active' },
194
- previousRecord: { status: 'completed' },
195
- };
196
-
197
- const result = await validator.validate([rule], invalidContext);
198
- expect(result.valid).toBe(false);
199
- });
200
- });
201
-
202
- describe('Field-level validation per specification', () => {
203
- it('should validate email format as per spec', async () => {
204
- // From spec lines 152-159
205
- const fieldConfig: FieldConfig = {
206
- type: 'email',
207
- required: true,
208
- validation: {
209
- format: 'email',
210
- message: 'Please enter a valid email address',
211
- },
212
- };
213
-
214
- const context: ValidationContext = {
215
- operation: 'create',
216
- record: { email: 'test@example.com' },
217
- };
218
-
219
- const results = await validator.validateField('email', fieldConfig, 'test@example.com', context);
220
- expect(results.length).toBe(0);
221
- });
222
-
223
- it('should reject invalid email format', async () => {
224
- const fieldConfig: FieldConfig = {
225
- type: 'email',
226
- required: true,
227
- validation: {
228
- format: 'email',
229
- message: 'Please enter a valid email address',
230
- },
231
- };
232
-
233
- const context: ValidationContext = {
234
- operation: 'create',
235
- record: { email: 'invalid-email' },
236
- };
237
-
238
- const results = await validator.validateField('email', fieldConfig, 'invalid-email', context);
239
- expect(results.length).toBeGreaterThan(0);
240
- expect(results[0].valid).toBe(false);
241
- });
242
-
243
- it('should validate URL format with protocol restrictions', async () => {
244
- // From spec lines 190-199
245
- const fieldConfig: FieldConfig = {
246
- type: 'url',
247
- validation: {
248
- format: 'url',
249
- protocols: ['http', 'https'],
250
- message: 'Please enter a valid URL',
251
- },
252
- };
253
-
254
- const context: ValidationContext = {
255
- operation: 'create',
256
- record: { website: 'https://example.com' },
257
- };
258
-
259
- const results = await validator.validateField('website', fieldConfig, 'https://example.com', context);
260
- expect(results.length).toBe(0);
261
- });
262
-
263
- it('should reject URL with invalid protocol', async () => {
264
- const fieldConfig: FieldConfig = {
265
- type: 'url',
266
- validation: {
267
- format: 'url',
268
- protocols: ['http', 'https'],
269
- message: 'Please enter a valid URL',
270
- },
271
- };
272
-
273
- const context: ValidationContext = {
274
- operation: 'create',
275
- record: { website: 'ftp://example.com' },
276
- };
277
-
278
- const results = await validator.validateField('website', fieldConfig, 'ftp://example.com', context);
279
- expect(results.length).toBeGreaterThan(0);
280
- expect(results[0].valid).toBe(false);
281
- });
282
-
283
- it('should validate min/max range as per spec', async () => {
284
- // From spec lines 160-170
285
- const fieldConfig: FieldConfig = {
286
- type: 'number',
287
- validation: {
288
- min: 0,
289
- max: 150,
290
- message: 'Age must be between 0 and 150',
291
- },
292
- };
293
-
294
- const context: ValidationContext = {
295
- operation: 'create',
296
- record: { age: 25 },
297
- };
298
-
299
- const results = await validator.validateField('age', fieldConfig, 25, context);
300
- expect(results.length).toBe(0);
301
- });
302
-
303
- it('should validate string length constraints', async () => {
304
- // From spec lines 172-185
305
- const fieldConfig: FieldConfig = {
306
- type: 'text',
307
- required: true,
308
- validation: {
309
- min_length: 3,
310
- max_length: 20,
311
- message: 'Username must be 3-20 characters',
312
- },
313
- };
314
-
315
- const context: ValidationContext = {
316
- operation: 'create',
317
- record: { username: 'john_doe' },
318
- };
319
-
320
- const results = await validator.validateField('username', fieldConfig, 'john_doe', context);
321
- expect(results.length).toBe(0);
322
- });
323
-
324
- it('should validate pattern matching', async () => {
325
- const fieldConfig: FieldConfig = {
326
- type: 'text',
327
- validation: {
328
- pattern: '^[a-zA-Z0-9_]+$',
329
- message: 'Username must be alphanumeric',
330
- },
331
- };
332
-
333
- const validContext: ValidationContext = {
334
- operation: 'create',
335
- record: { username: 'john_doe_123' },
336
- };
337
-
338
- const results = await validator.validateField('username', fieldConfig, 'john_doe_123', validContext);
339
- expect(results.length).toBe(0);
340
- });
341
-
342
- it('should reject invalid pattern', async () => {
343
- const fieldConfig: FieldConfig = {
344
- type: 'text',
345
- validation: {
346
- pattern: '^[a-zA-Z0-9_]+$',
347
- message: 'Username must be alphanumeric',
348
- },
349
- };
350
-
351
- const invalidContext: ValidationContext = {
352
- operation: 'create',
353
- record: { username: 'john@doe!' },
354
- };
355
-
356
- const results = await validator.validateField('username', fieldConfig, 'john@doe!', invalidContext);
357
- expect(results.length).toBeGreaterThan(0);
358
- expect(results[0].valid).toBe(false);
359
- });
360
- });
361
-
362
- describe('Validation triggers and conditions', () => {
363
- it('should apply rule only on specified operations', async () => {
364
- const rule: CrossFieldValidationRule = {
365
- name: 'budget_check',
366
- type: 'cross_field',
367
- trigger: ['create', 'update'],
368
- rule: {
369
- field: 'budget',
370
- operator: '>=',
371
- value: 0,
372
- },
373
- message: 'Budget must be positive',
374
- };
375
-
376
- const deleteContext: ValidationContext = {
377
- operation: 'delete',
378
- record: { budget: -100 },
379
- };
380
-
381
- // Rule should be skipped on delete operation
382
- const result = await validator.validate([rule], deleteContext);
383
- expect(result.valid).toBe(true);
384
- });
385
-
386
- it('should apply conditional validation', async () => {
387
- const rule: CrossFieldValidationRule = {
388
- name: 'high_value_approval',
389
- type: 'cross_field',
390
- apply_when: {
391
- field: 'total_amount',
392
- operator: '>',
393
- value: 10000,
394
- },
395
- rule: {
396
- field: 'manager_approval_id',
397
- operator: '!=',
398
- value: null,
399
- },
400
- message: 'Manager approval required for orders over $10,000',
401
- };
402
-
403
- // Rule should not apply for low-value orders
404
- const lowValueContext: ValidationContext = {
405
- operation: 'create',
406
- record: {
407
- total_amount: 5000,
408
- manager_approval_id: null,
409
- },
410
- };
411
-
412
- const result = await validator.validate([rule], lowValueContext);
413
- expect(result.valid).toBe(true);
414
- });
415
- });
416
-
417
- describe('Message formatting with templates', () => {
418
- it('should format messages with template variables', async () => {
419
- const rule: CrossFieldValidationRule = {
420
- name: 'budget_limit',
421
- type: 'cross_field',
422
- rule: {
423
- field: 'budget',
424
- operator: '<=',
425
- value: 100000,
426
- },
427
- message: 'Budget {{budget}} exceeds limit',
428
- };
429
-
430
- const context: ValidationContext = {
431
- operation: 'create',
432
- record: { budget: 150000 },
433
- };
434
-
435
- const result = await validator.validate([rule], context);
436
- expect(result.valid).toBe(false);
437
- expect(result.errors[0].message).toContain('150000');
438
- });
439
- });
440
- });