@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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +18 -0
- package/README.md +4 -4
- package/dist/app.d.ts +9 -6
- package/dist/app.js +151 -29
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.js +2 -5
- package/dist/index.js.map +1 -1
- package/dist/optimizations/CompiledHookManager.d.ts +55 -0
- package/dist/optimizations/CompiledHookManager.js +164 -0
- package/dist/optimizations/CompiledHookManager.js.map +1 -0
- package/dist/optimizations/DependencyGraph.d.ts +82 -0
- package/dist/optimizations/DependencyGraph.js +211 -0
- package/dist/optimizations/DependencyGraph.js.map +1 -0
- package/dist/optimizations/GlobalConnectionPool.d.ts +89 -0
- package/dist/optimizations/GlobalConnectionPool.js +193 -0
- package/dist/optimizations/GlobalConnectionPool.js.map +1 -0
- package/dist/optimizations/LazyMetadataLoader.d.ts +75 -0
- package/dist/optimizations/LazyMetadataLoader.js +149 -0
- package/dist/optimizations/LazyMetadataLoader.js.map +1 -0
- package/dist/optimizations/OptimizedMetadataRegistry.d.ts +26 -0
- package/dist/optimizations/OptimizedMetadataRegistry.js +117 -0
- package/dist/optimizations/OptimizedMetadataRegistry.js.map +1 -0
- package/dist/optimizations/OptimizedValidationEngine.d.ts +73 -0
- package/dist/optimizations/OptimizedValidationEngine.js +141 -0
- package/dist/optimizations/OptimizedValidationEngine.js.map +1 -0
- package/dist/optimizations/QueryCompiler.d.ts +51 -0
- package/dist/optimizations/QueryCompiler.js +216 -0
- package/dist/optimizations/QueryCompiler.js.map +1 -0
- package/dist/optimizations/SQLQueryOptimizer.d.ts +96 -0
- package/dist/optimizations/SQLQueryOptimizer.js +265 -0
- package/dist/optimizations/SQLQueryOptimizer.js.map +1 -0
- package/dist/optimizations/index.d.ts +32 -0
- package/dist/optimizations/index.js +44 -0
- package/dist/optimizations/index.js.map +1 -0
- package/dist/plugin.d.ts +6 -7
- package/dist/plugin.js +39 -22
- package/dist/plugin.js.map +1 -1
- package/dist/query/query-analyzer.js.map +1 -1
- package/dist/query/query-builder.d.ts +6 -1
- package/dist/query/query-builder.js +21 -5
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-service.js.map +1 -1
- package/dist/repository.d.ts +2 -0
- package/dist/repository.js +15 -9
- package/dist/repository.js.map +1 -1
- package/jest.config.js +3 -3
- package/package.json +8 -5
- package/src/app.ts +173 -47
- package/src/index.ts +8 -9
- package/src/optimizations/CompiledHookManager.ts +185 -0
- package/src/optimizations/DependencyGraph.ts +255 -0
- package/src/optimizations/GlobalConnectionPool.ts +251 -0
- package/src/optimizations/LazyMetadataLoader.ts +180 -0
- package/src/optimizations/OptimizedMetadataRegistry.ts +132 -0
- package/src/optimizations/OptimizedValidationEngine.ts +172 -0
- package/src/optimizations/QueryCompiler.ts +242 -0
- package/src/optimizations/SQLQueryOptimizer.ts +329 -0
- package/src/optimizations/index.ts +34 -0
- package/src/plugin.ts +51 -28
- package/src/query/query-analyzer.ts +1 -1
- package/src/query/query-builder.ts +21 -7
- package/src/query/query-service.ts +1 -1
- package/src/repository.ts +25 -13
- package/test/__mocks__/@objectstack/runtime.ts +8 -8
- package/test/app.test.ts +9 -7
- package/test/optimizations.test.ts +440 -0
- package/test/plugin-integration.test.ts +30 -19
- package/tsconfig.json +4 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/ai-agent.d.ts +0 -176
- package/dist/ai-agent.js +0 -722
- package/dist/ai-agent.js.map +0 -1
- package/dist/formula-engine.d.ts +0 -102
- package/dist/formula-engine.js +0 -433
- package/dist/formula-engine.js.map +0 -1
- package/dist/formula-plugin.d.ts +0 -52
- package/dist/formula-plugin.js +0 -107
- package/dist/formula-plugin.js.map +0 -1
- package/dist/validator-plugin.d.ts +0 -56
- package/dist/validator-plugin.js +0 -106
- package/dist/validator-plugin.js.map +0 -1
- package/dist/validator.d.ts +0 -80
- package/dist/validator.js +0 -625
- package/dist/validator.js.map +0 -1
- package/src/ai-agent.ts +0 -868
- package/src/formula-engine.ts +0 -572
- package/src/formula-plugin.ts +0 -141
- package/src/validator-plugin.ts +0 -140
- package/src/validator.ts +0 -743
- package/test/formula-engine.test.ts +0 -725
- package/test/formula-integration.test.ts +0 -286
- package/test/formula-plugin.test.ts +0 -197
- package/test/formula-spec-compliance.test.ts +0 -258
- package/test/validation-spec-compliance.test.ts +0 -440
- package/test/validator-plugin.test.ts +0 -126
- 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 {
|
|
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 '
|
|
15
|
-
import { FormulaEngine } from '
|
|
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
|
|
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():
|
|
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
|
-
|
|
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
|
-
|
|
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 @
|
|
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
|
|
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
|
|
245
|
+
export class ObjectStackProtocolImplementation {}
|
|
246
246
|
|
|
247
|
-
export interface
|
|
248
|
-
engine:
|
|
247
|
+
export interface any {
|
|
248
|
+
engine: ObjectKernel;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
export interface
|
|
251
|
+
export interface ObjectQLPlugin {
|
|
252
252
|
name: string;
|
|
253
|
-
install?: (ctx:
|
|
254
|
-
onStart?: (ctx:
|
|
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
|
|
297
|
-
const
|
|
298
|
+
const initFn = jest.fn();
|
|
299
|
+
const startFn = jest.fn();
|
|
298
300
|
const mockPlugin = {
|
|
299
301
|
name: 'test-plugin',
|
|
300
|
-
|
|
301
|
-
|
|
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(
|
|
311
|
-
expect(
|
|
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
|
-
|
|
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
|
+
});
|