@objectql/core 4.0.2 → 4.0.3

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 +6 -7
  38. package/dist/plugin.js +39 -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 +51 -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
package/src/repository.ts CHANGED
@@ -7,25 +7,33 @@
7
7
  */
8
8
 
9
9
  import { ObjectQLContext, IObjectQL, ObjectConfig, Driver, UnifiedQuery, ActionContext, HookAPI, RetrievalHookContext, MutationHookContext, UpdateHookContext, ValidationContext, ValidationError, ValidationRuleResult, FormulaContext, Filter } from '@objectql/types';
10
- import type { ObjectStackKernel } from '@objectql/runtime';
10
+ import type { ObjectKernel } from '@objectstack/runtime';
11
11
  import { Data } from '@objectstack/spec';
12
12
  type QueryAST = Data.QueryAST;
13
13
  type SortNode = Data.SortNode;
14
- import { Validator } from './validator';
15
- import { FormulaEngine } from './formula-engine';
14
+ import { Validator } from '@objectql/plugin-validator';
15
+ import { FormulaEngine } from '@objectql/plugin-formula';
16
16
  import { QueryBuilder } from './query';
17
+ import { QueryCompiler } from './optimizations/QueryCompiler';
17
18
 
18
19
  /**
19
20
  * Extended ObjectStack Kernel with optional ObjectQL plugin capabilities.
20
21
  * These properties are attached by ValidatorPlugin and FormulaPlugin during installation.
21
22
  */
22
- interface ExtendedKernel extends ObjectStackKernel {
23
+ interface ExtendedKernel extends ObjectKernel {
23
24
  validator?: Validator;
24
25
  formulaEngine?: FormulaEngine;
26
+ create?: (objectName: string, data: any) => Promise<any>;
27
+ update?: (objectName: string, id: string, data: any) => Promise<any>;
28
+ delete?: (objectName: string, id: string) => Promise<any>;
29
+ find?: (objectName: string, query: any) => Promise<any>;
30
+ get?: (objectName: string, id: string) => Promise<any>;
25
31
  }
26
32
 
27
33
  export class ObjectRepository {
28
34
  private queryBuilder: QueryBuilder;
35
+ // Shared query compiler for caching compiled queries
36
+ private static queryCompiler = new QueryCompiler(1000);
29
37
 
30
38
  constructor(
31
39
  private objectName: string,
@@ -67,7 +75,7 @@ export class ObjectRepository {
67
75
  return this.app.datasource(datasourceName);
68
76
  }
69
77
 
70
- private getKernel(): ObjectStackKernel {
78
+ private getKernel(): ObjectKernel {
71
79
  return this.app.getKernel();
72
80
  }
73
81
 
@@ -80,9 +88,13 @@ export class ObjectRepository {
80
88
 
81
89
  /**
82
90
  * Translates ObjectQL UnifiedQuery to ObjectStack QueryAST format
91
+ * Uses query compiler for caching and optimization
83
92
  */
84
93
  private buildQueryAST(query: UnifiedQuery): QueryAST {
85
- return this.queryBuilder.build(this.objectName, query);
94
+ const ast = this.queryBuilder.build(this.objectName, query);
95
+ // Use query compiler to cache and optimize the AST
96
+ const compiled = ObjectRepository.queryCompiler.compile(this.objectName, ast);
97
+ return compiled.ast;
86
98
  }
87
99
 
88
100
  getSchema(): ObjectConfig {
@@ -241,7 +253,7 @@ export class ObjectRepository {
241
253
  record[fieldName] = null;
242
254
  // Formula evaluation should not throw here, but we need observability
243
255
  // This logging is intentionally minimal and side-effect free
244
- // eslint-disable-next-line no-console
256
+
245
257
  console.error(
246
258
  '[ObjectQL][FormulaEngine] Formula evaluation failed',
247
259
  {
@@ -274,7 +286,7 @@ export class ObjectRepository {
274
286
 
275
287
  // Build QueryAST and execute via kernel
276
288
  const ast = this.buildQueryAST(hookCtx.query || {});
277
- const kernelResult = await this.getKernel().find(this.objectName, ast);
289
+ const kernelResult = await (this.getKernel() as any).find(this.objectName, ast);
278
290
  const results = kernelResult.value;
279
291
 
280
292
  // Evaluate formulas for each result
@@ -300,7 +312,7 @@ export class ObjectRepository {
300
312
  await this.app.triggerHook('beforeFind', this.objectName, hookCtx);
301
313
 
302
314
  // Use kernel.get() for direct ID lookup
303
- const result = await this.getKernel().get(this.objectName, String(idOrQuery));
315
+ const result = await (this.getKernel() as any).get(this.objectName, String(idOrQuery));
304
316
 
305
317
  // Evaluate formulas if result exists
306
318
  const resultWithFormulas = result ? this.evaluateFormulas(result) : result;
@@ -344,7 +356,7 @@ export class ObjectRepository {
344
356
 
345
357
  // Build QueryAST and execute via kernel to get count
346
358
  const ast = this.buildQueryAST(hookCtx.query || {});
347
- const kernelResult = await this.getKernel().find(this.objectName, ast);
359
+ const kernelResult = await (this.getKernel() as any).find(this.objectName, ast);
348
360
  const result = kernelResult.count;
349
361
 
350
362
  hookCtx.result = result;
@@ -372,7 +384,7 @@ export class ObjectRepository {
372
384
  await this.validateRecord('create', finalDoc);
373
385
 
374
386
  // Execute via kernel
375
- const result = await this.getKernel().create(this.objectName, finalDoc);
387
+ const result = await (this.getKernel() as any).create(this.objectName, finalDoc, this.getOptions());
376
388
 
377
389
  hookCtx.result = result;
378
390
  await this.app.triggerHook('afterCreate', this.objectName, hookCtx);
@@ -399,7 +411,7 @@ export class ObjectRepository {
399
411
  await this.validateRecord('update', hookCtx.data, previousData);
400
412
 
401
413
  // Execute via kernel
402
- const result = await this.getKernel().update(this.objectName, String(id), hookCtx.data);
414
+ const result = await (this.getKernel() as any).update(this.objectName, String(id), hookCtx.data, this.getOptions());
403
415
 
404
416
  hookCtx.result = result;
405
417
  await this.app.triggerHook('afterUpdate', this.objectName, hookCtx);
@@ -421,7 +433,7 @@ export class ObjectRepository {
421
433
  await this.app.triggerHook('beforeDelete', this.objectName, hookCtx);
422
434
 
423
435
  // Execute via kernel
424
- const result = await this.getKernel().delete(this.objectName, String(id));
436
+ const result = await (this.getKernel() as any).delete(this.objectName, String(id), this.getOptions());
425
437
 
426
438
  hookCtx.result = result;
427
439
  await this.app.triggerHook('afterDelete', this.objectName, hookCtx);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Mock for @objectql/runtime
2
+ * Mock for @objectstack/runtime
3
3
  * This mock is needed because the npm package has issues with Jest
4
4
  * and we want to focus on testing ObjectQL's logic, not the kernel integration.
5
5
  *
@@ -123,7 +123,7 @@ class MockActionManager {
123
123
  }
124
124
  }
125
125
 
126
- export class ObjectStackKernel {
126
+ export class ObjectKernel {
127
127
  public ql: unknown = null;
128
128
  public metadata: MockMetadataRegistry;
129
129
  public hooks: MockHookManager;
@@ -242,14 +242,14 @@ export class ObjectStackKernel {
242
242
  }
243
243
  }
244
244
 
245
- export class ObjectStackRuntimeProtocol {}
245
+ export class ObjectStackProtocolImplementation {}
246
246
 
247
- export interface RuntimeContext {
248
- engine: ObjectStackKernel;
247
+ export interface any {
248
+ engine: ObjectKernel;
249
249
  }
250
250
 
251
- export interface RuntimePlugin {
251
+ export interface ObjectQLPlugin {
252
252
  name: string;
253
- install?: (ctx: RuntimeContext) => void | Promise<void>;
254
- onStart?: (ctx: RuntimeContext) => void | Promise<void>;
253
+ install?: (ctx: any) => void | Promise<void>;
254
+ onStart?: (ctx: any) => void | Promise<void>;
255
255
  }
package/test/app.test.ts CHANGED
@@ -269,6 +269,8 @@ describe('ObjectQL App', () => {
269
269
  const entry: Metadata = {
270
270
  type: 'object',
271
271
  id: 'todo',
272
+ // @ts-expect-error - SchemaRegistry typing limitation
273
+ name: 'todo', // Ensure SchemaRegistry keys it correctly
272
274
  package: 'test-package',
273
275
  content: obj
274
276
  };
@@ -293,12 +295,12 @@ describe('ObjectQL App', () => {
293
295
 
294
296
  describe('Plugin System', () => {
295
297
  it('should initialize runtime plugins on init', async () => {
296
- const installFn = jest.fn();
297
- const onStartFn = jest.fn();
298
+ const initFn = jest.fn();
299
+ const startFn = jest.fn();
298
300
  const mockPlugin = {
299
301
  name: 'test-plugin',
300
- install: installFn,
301
- onStart: onStartFn
302
+ init: initFn,
303
+ start: startFn
302
304
  };
303
305
 
304
306
  const app = new ObjectQL({
@@ -307,14 +309,14 @@ describe('ObjectQL App', () => {
307
309
  });
308
310
 
309
311
  await app.init();
310
- expect(installFn).toHaveBeenCalled();
311
- expect(onStartFn).toHaveBeenCalled();
312
+ expect(initFn).toHaveBeenCalled();
313
+ expect(startFn).toHaveBeenCalled();
312
314
  });
313
315
 
314
316
  it('should use plugin method', () => {
315
317
  const mockPlugin = {
316
318
  name: 'test-plugin',
317
- install: jest.fn()
319
+ init: jest.fn()
318
320
  };
319
321
 
320
322
  const app = new ObjectQL({ datasources: {} });
@@ -0,0 +1,440 @@
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
+ import { describe, it, expect, beforeEach } from '@jest/globals';
10
+ import {
11
+ OptimizedMetadataRegistry,
12
+ QueryCompiler,
13
+ CompiledHookManager,
14
+ GlobalConnectionPool,
15
+ OptimizedValidationEngine,
16
+ LazyMetadataLoader,
17
+ DependencyGraph,
18
+ SQLQueryOptimizer
19
+ } from '../src/optimizations';
20
+
21
+ describe('Kernel Optimizations', () => {
22
+ describe('OptimizedMetadataRegistry', () => {
23
+ let registry: OptimizedMetadataRegistry;
24
+
25
+ beforeEach(() => {
26
+ registry = new OptimizedMetadataRegistry();
27
+ });
28
+
29
+ it('should register and retrieve metadata', () => {
30
+ const item = { name: 'user', package: 'crm', fields: {} };
31
+ registry.register('object', item);
32
+
33
+ const retrieved = registry.get('object', 'user');
34
+ expect(retrieved).toEqual(item);
35
+ });
36
+
37
+ it('should unregister package with O(k) complexity', () => {
38
+ // Register multiple items in the same package
39
+ registry.register('object', { name: 'user', package: 'crm' });
40
+ registry.register('object', { name: 'account', package: 'crm' });
41
+ registry.register('object', { name: 'task', package: 'todo' });
42
+
43
+ // Unregister the 'crm' package
44
+ registry.unregisterPackage('crm');
45
+
46
+ // Verify items are removed
47
+ expect(registry.get('object', 'user')).toBeUndefined();
48
+ expect(registry.get('object', 'account')).toBeUndefined();
49
+
50
+ // Verify other package items remain
51
+ expect(registry.get('object', 'task')).toBeDefined();
52
+ });
53
+
54
+ it('should list items by type', () => {
55
+ registry.register('object', { name: 'user', package: 'crm' });
56
+ registry.register('object', { name: 'account', package: 'crm' });
57
+
58
+ const objects = registry.list('object');
59
+ expect(objects.length).toBe(2);
60
+ });
61
+ });
62
+
63
+ describe('QueryCompiler', () => {
64
+ let compiler: QueryCompiler;
65
+
66
+ beforeEach(() => {
67
+ compiler = new QueryCompiler(10);
68
+ });
69
+
70
+ it('should compile and cache queries', () => {
71
+ const ast = {
72
+ filters: { status: 'active' },
73
+ sort: [{ field: 'created', order: 'desc' as const }]
74
+ };
75
+
76
+ const compiled1 = compiler.compile('user', ast);
77
+ const compiled2 = compiler.compile('user', ast);
78
+
79
+ // Should return same cached instance
80
+ expect(compiled1).toBe(compiled2);
81
+ });
82
+
83
+ it('should detect indexable fields', () => {
84
+ const ast = {
85
+ filters: { status: 'active', email: 'test@example.com' }
86
+ };
87
+
88
+ const compiled = compiler.compile('user', ast);
89
+ expect(compiled.plan.useIndex).toContain('status');
90
+ expect(compiled.plan.useIndex).toContain('email');
91
+ });
92
+
93
+ it('should cache different queries separately', () => {
94
+ const ast1 = { filters: { status: 'active' } };
95
+ const ast2 = { filters: { status: 'inactive' } };
96
+
97
+ const compiled1 = compiler.compile('user', ast1);
98
+ const compiled2 = compiler.compile('user', ast2);
99
+
100
+ // Should be different instances
101
+ expect(compiled1).not.toBe(compiled2);
102
+ });
103
+
104
+ it('should clear cache', () => {
105
+ const ast = { filters: { status: 'active' } };
106
+ compiler.compile('user', ast);
107
+
108
+ compiler.clearCache();
109
+
110
+ const compiled = compiler.compile('user', ast);
111
+ expect(compiled).toBeDefined();
112
+ });
113
+ });
114
+
115
+ describe('CompiledHookManager', () => {
116
+ let hookManager: CompiledHookManager;
117
+
118
+ beforeEach(() => {
119
+ hookManager = new CompiledHookManager();
120
+ });
121
+
122
+ it('should register and run hooks', async () => {
123
+ let executed = false;
124
+ const handler = async () => { executed = true; };
125
+
126
+ hookManager.registerHook('beforeCreate', 'user', handler);
127
+ await hookManager.runHooks('beforeCreate', 'user', {});
128
+
129
+ expect(executed).toBe(true);
130
+ });
131
+
132
+ it('should expand wildcard patterns', async () => {
133
+ let count = 0;
134
+ const handler = async () => { count++; };
135
+
136
+ hookManager.registerHook('before*', 'user', handler);
137
+
138
+ await hookManager.runHooks('beforeCreate', 'user', {});
139
+ await hookManager.runHooks('beforeUpdate', 'user', {});
140
+
141
+ expect(count).toBe(2);
142
+ });
143
+
144
+ it('should handle wildcard object names', async () => {
145
+ let count = 0;
146
+ const handler = async () => { count++; };
147
+
148
+ hookManager.registerHook('beforeCreate', '*', handler);
149
+
150
+ await hookManager.runHooks('beforeCreate', 'user', {});
151
+ await hookManager.runHooks('beforeCreate', 'account', {});
152
+
153
+ expect(count).toBe(2);
154
+ });
155
+
156
+ it('should remove package hooks', async () => {
157
+ let executed = false;
158
+ const handler = async () => { executed = true; };
159
+
160
+ hookManager.registerHook('beforeCreate', 'user', handler, 'crm');
161
+ hookManager.removePackage('crm');
162
+
163
+ await hookManager.runHooks('beforeCreate', 'user', {});
164
+
165
+ expect(executed).toBe(false);
166
+ });
167
+ });
168
+
169
+ describe('GlobalConnectionPool', () => {
170
+ let pool: GlobalConnectionPool;
171
+
172
+ beforeEach(() => {
173
+ pool = new GlobalConnectionPool({ total: 5, perDriver: 3 });
174
+ });
175
+
176
+ it('should acquire and release connections', async () => {
177
+ const conn = await pool.acquire('postgres');
178
+ expect(conn).toBeDefined();
179
+ expect(conn.driverName).toBe('postgres');
180
+
181
+ await pool.release(conn);
182
+ const stats = pool.getStats();
183
+ expect(stats.driverStats.postgres.idle).toBe(1);
184
+ });
185
+
186
+ it('should enforce per-driver limits', async () => {
187
+ const conns = [];
188
+ for (let i = 0; i < 3; i++) {
189
+ conns.push(await pool.acquire('postgres'));
190
+ }
191
+
192
+ // Should queue the next request
193
+ const promise = pool.acquire('postgres');
194
+ const stats = pool.getStats();
195
+ expect(stats.waitQueueSize).toBe(1);
196
+
197
+ // Release one connection
198
+ await pool.release(conns[0]);
199
+
200
+ // Now the queued request should complete
201
+ const conn = await promise;
202
+ expect(conn).toBeDefined();
203
+ });
204
+
205
+ it('should track connection statistics', async () => {
206
+ await pool.acquire('postgres');
207
+ await pool.acquire('postgres');
208
+
209
+ const stats = pool.getStats();
210
+ expect(stats.totalConnections).toBe(2);
211
+ expect(stats.driverStats.postgres.active).toBe(2);
212
+ });
213
+ });
214
+
215
+ describe('OptimizedValidationEngine', () => {
216
+ let engine: OptimizedValidationEngine;
217
+
218
+ beforeEach(() => {
219
+ engine = new OptimizedValidationEngine();
220
+ });
221
+
222
+ it('should compile and validate schemas', () => {
223
+ const schema = {
224
+ type: 'object',
225
+ properties: {
226
+ name: { type: 'string', required: true, minLength: 3 },
227
+ age: { type: 'number', minimum: 0, maximum: 120 }
228
+ }
229
+ };
230
+
231
+ engine.compile('user', schema);
232
+
233
+ const result = engine.validate('user', {
234
+ name: 'John',
235
+ age: 30
236
+ });
237
+
238
+ expect(result.valid).toBe(true);
239
+ });
240
+
241
+ it('should detect validation errors', () => {
242
+ const schema = {
243
+ type: 'object',
244
+ properties: {
245
+ email: { type: 'string', pattern: '^[^@]+@[^@]+\\.[^@]+$' }
246
+ }
247
+ };
248
+
249
+ engine.compile('user', schema);
250
+
251
+ const result = engine.validate('user', {
252
+ email: 'invalid-email'
253
+ });
254
+
255
+ expect(result.valid).toBe(false);
256
+ expect(result.errors).toBeDefined();
257
+ });
258
+
259
+ it('should validate enum values', () => {
260
+ const schema = {
261
+ type: 'string',
262
+ enum: ['active', 'inactive', 'pending']
263
+ };
264
+
265
+ engine.compile('status', schema);
266
+
267
+ const validResult = engine.validate('status', 'active');
268
+ expect(validResult.valid).toBe(true);
269
+
270
+ const invalidResult = engine.validate('status', 'unknown');
271
+ expect(invalidResult.valid).toBe(false);
272
+ });
273
+ });
274
+
275
+ describe('LazyMetadataLoader', () => {
276
+ let loader: LazyMetadataLoader;
277
+ let loadCount: number;
278
+
279
+ beforeEach(() => {
280
+ loadCount = 0;
281
+ loader = new LazyMetadataLoader(async (objectName) => {
282
+ loadCount++;
283
+ return {
284
+ name: objectName,
285
+ fields: { id: { type: 'string' } },
286
+ relatedObjects: objectName === 'user' ? ['account'] : []
287
+ };
288
+ });
289
+ });
290
+
291
+ it('should load metadata on-demand', async () => {
292
+ const metadata = await loader.get('user');
293
+ expect(metadata.name).toBe('user');
294
+ expect(loadCount).toBe(1);
295
+ });
296
+
297
+ it('should cache loaded metadata', async () => {
298
+ await loader.get('user');
299
+ await loader.get('user');
300
+
301
+ // Should only load once
302
+ expect(loadCount).toBe(1);
303
+ });
304
+
305
+ it('should predictively preload related objects', async () => {
306
+ await loader.get('user');
307
+
308
+ // Give time for async preload
309
+ await new Promise(resolve => setTimeout(resolve, 100));
310
+
311
+ // Account should have been preloaded
312
+ expect(loader.isLoaded('account')).toBe(true);
313
+ });
314
+
315
+ it('should invalidate cache', async () => {
316
+ await loader.get('user');
317
+ loader.invalidate('user');
318
+
319
+ await loader.get('user');
320
+ expect(loadCount).toBe(2);
321
+ });
322
+ });
323
+
324
+ describe('DependencyGraph', () => {
325
+ let graph: DependencyGraph;
326
+
327
+ beforeEach(() => {
328
+ graph = new DependencyGraph();
329
+ });
330
+
331
+ it('should build dependency graph', () => {
332
+ graph.addDependency('account', 'contact', 'lookup', 'account_id');
333
+ graph.addDependency('account', 'opportunity', 'lookup', 'account_id');
334
+
335
+ const dependents = graph.getDependents('account');
336
+ expect(dependents).toContain('contact');
337
+ expect(dependents).toContain('opportunity');
338
+ });
339
+
340
+ it('should compute topological sort', () => {
341
+ graph.addDependency('account', 'contact', 'lookup', 'account_id');
342
+ graph.addDependency('contact', 'task', 'lookup', 'contact_id');
343
+
344
+ const sorted = graph.topologicalSort(['account', 'contact', 'task']);
345
+
346
+ // Task should come before contact, contact before account
347
+ expect(sorted.indexOf('task')).toBeLessThan(sorted.indexOf('contact'));
348
+ expect(sorted.indexOf('contact')).toBeLessThan(sorted.indexOf('account'));
349
+ });
350
+
351
+ it('should detect circular dependencies', () => {
352
+ graph.addDependency('a', 'b', 'lookup', 'a_id');
353
+ graph.addDependency('b', 'c', 'lookup', 'b_id');
354
+ graph.addDependency('c', 'a', 'lookup', 'c_id');
355
+
356
+ expect(graph.hasCircularDependency()).toBe(true);
357
+ });
358
+
359
+ it('should get cascade delete order', () => {
360
+ graph.addDependency('account', 'contact', 'master_detail', 'account_id');
361
+ graph.addDependency('contact', 'task', 'master_detail', 'contact_id');
362
+
363
+ const deleteOrder = graph.getCascadeDeleteOrder('account');
364
+
365
+ // Should delete in order: task -> contact -> account
366
+ expect(deleteOrder).toContain('task');
367
+ expect(deleteOrder).toContain('contact');
368
+ expect(deleteOrder).toContain('account');
369
+ });
370
+ });
371
+
372
+ describe('SQLQueryOptimizer', () => {
373
+ let optimizer: SQLQueryOptimizer;
374
+
375
+ beforeEach(() => {
376
+ optimizer = new SQLQueryOptimizer();
377
+
378
+ // Register schema with indexes
379
+ optimizer.registerSchema({
380
+ name: 'users',
381
+ fields: {
382
+ id: { type: 'string' },
383
+ email: { type: 'string' },
384
+ status: { type: 'string' }
385
+ },
386
+ indexes: [
387
+ { name: 'idx_email', fields: ['email'], unique: true },
388
+ { name: 'idx_status', fields: ['status'], unique: false }
389
+ ]
390
+ });
391
+ });
392
+
393
+ it('should generate basic SQL', () => {
394
+ const sql = optimizer.optimize({
395
+ object: 'users',
396
+ fields: ['id', 'email'],
397
+ filters: { status: 'active' }
398
+ });
399
+
400
+ expect(sql).toContain('SELECT id, email');
401
+ expect(sql).toContain('FROM users');
402
+ expect(sql).toContain('WHERE');
403
+ });
404
+
405
+ it('should add index hints', () => {
406
+ const sql = optimizer.optimize({
407
+ object: 'users',
408
+ filters: { status: 'active' }
409
+ });
410
+
411
+ expect(sql).toContain('USE INDEX (idx_status)');
412
+ });
413
+
414
+ it('should optimize join types', () => {
415
+ const sql = optimizer.optimize({
416
+ object: 'users',
417
+ joins: [
418
+ { type: 'left', table: 'accounts', on: 'users.account_id = accounts.id' }
419
+ ],
420
+ filters: { 'accounts.type': 'premium' }
421
+ });
422
+
423
+ // Should convert LEFT to INNER when filtering on joined table
424
+ expect(sql).toContain('INNER JOIN');
425
+ });
426
+
427
+ it('should handle sorting and limits', () => {
428
+ const sql = optimizer.optimize({
429
+ object: 'users',
430
+ sort: [{ field: 'created_at', order: 'desc' }],
431
+ limit: 10,
432
+ offset: 20
433
+ });
434
+
435
+ expect(sql).toContain('ORDER BY created_at DESC');
436
+ expect(sql).toContain('LIMIT 10');
437
+ expect(sql).toContain('OFFSET 20');
438
+ });
439
+ });
440
+ });