@objectql/core 4.0.1 → 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 +32 -0
- package/README.md +14 -12
- 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/filter-translator.d.ts +6 -18
- package/dist/query/filter-translator.js +6 -103
- package/dist/query/filter-translator.js.map +1 -1
- package/dist/query/query-analyzer.js +24 -25
- package/dist/query/query-analyzer.js.map +1 -1
- package/dist/query/query-builder.d.ts +9 -3
- package/dist/query/query-builder.js +25 -35
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-service.d.ts +2 -2
- package/dist/query/query-service.js +5 -5
- package/dist/query/query-service.js.map +1 -1
- package/dist/repository.d.ts +2 -0
- package/dist/repository.js +24 -17
- 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 +7 -8
- 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/filter-translator.ts +8 -115
- package/src/query/query-analyzer.ts +25 -29
- package/src/query/query-builder.ts +26 -43
- package/src/query/query-service.ts +6 -6
- package/src/repository.ts +35 -22
- package/test/__mocks__/@objectstack/runtime.ts +8 -8
- package/test/app.test.ts +11 -8
- 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/validator-plugin.test.ts +0 -126
- package/test/validator.test.ts +0 -440
|
@@ -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
|
+
});
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ObjectQLPlugin } from '../src/plugin';
|
|
10
|
-
import { ValidatorPlugin } from '
|
|
11
|
-
import { FormulaPlugin } from '
|
|
10
|
+
import { ValidatorPlugin } from '@objectql/plugin-validator';
|
|
11
|
+
import { FormulaPlugin } from '@objectql/plugin-formula';
|
|
12
12
|
|
|
13
13
|
// Mock the sub-plugins
|
|
14
|
-
jest.mock('
|
|
15
|
-
jest.mock('
|
|
14
|
+
jest.mock('@objectql/plugin-validator');
|
|
15
|
+
jest.mock('@objectql/plugin-formula');
|
|
16
16
|
|
|
17
17
|
describe('ObjectQLPlugin Integration', () => {
|
|
18
18
|
let plugin: ObjectQLPlugin;
|
|
@@ -29,7 +29,7 @@ describe('ObjectQLPlugin Integration', () => {
|
|
|
29
29
|
registerFormulaProvider: jest.fn(),
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
mockContext = {
|
|
32
|
+
mockContext = { app: mockKernel };
|
|
33
33
|
|
|
34
34
|
// Setup mock implementations
|
|
35
35
|
(ValidatorPlugin as jest.Mock).mockImplementation(() => ({
|
|
@@ -45,7 +45,7 @@ describe('ObjectQLPlugin Integration', () => {
|
|
|
45
45
|
it('should have correct name and version', () => {
|
|
46
46
|
plugin = new ObjectQLPlugin();
|
|
47
47
|
expect(plugin.name).toBe('@objectql/core');
|
|
48
|
-
expect(plugin.version).toBe('4.0.
|
|
48
|
+
expect(plugin.version).toBe('4.0.2');
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -89,32 +89,36 @@ describe('ObjectQLPlugin Integration', () => {
|
|
|
89
89
|
describe('Installation - Conditional Plugin Loading', () => {
|
|
90
90
|
it('should install validator plugin when enabled', async () => {
|
|
91
91
|
plugin = new ObjectQLPlugin({ enableValidator: true });
|
|
92
|
-
|
|
92
|
+
const runtimeContext = { engine: mockContext.app };
|
|
93
|
+
await plugin.install(runtimeContext);
|
|
93
94
|
|
|
94
95
|
expect(ValidatorPlugin).toHaveBeenCalled();
|
|
95
96
|
const validatorInstance = (ValidatorPlugin as jest.Mock).mock.results[0].value;
|
|
96
|
-
expect(validatorInstance.install).toHaveBeenCalledWith(
|
|
97
|
+
expect(validatorInstance.install).toHaveBeenCalledWith(runtimeContext);
|
|
97
98
|
});
|
|
98
99
|
|
|
99
100
|
it('should not install validator plugin when disabled', async () => {
|
|
100
101
|
plugin = new ObjectQLPlugin({ enableValidator: false });
|
|
101
|
-
|
|
102
|
+
const runtimeContext = { engine: mockContext.app };
|
|
103
|
+
await plugin.install(runtimeContext);
|
|
102
104
|
|
|
103
105
|
expect(ValidatorPlugin).not.toHaveBeenCalled();
|
|
104
106
|
});
|
|
105
107
|
|
|
106
108
|
it('should install formula plugin when enabled', async () => {
|
|
107
109
|
plugin = new ObjectQLPlugin({ enableFormulas: true });
|
|
108
|
-
|
|
110
|
+
const runtimeContext = { engine: mockContext.app };
|
|
111
|
+
await plugin.install(runtimeContext);
|
|
109
112
|
|
|
110
113
|
expect(FormulaPlugin).toHaveBeenCalled();
|
|
111
114
|
const formulaInstance = (FormulaPlugin as jest.Mock).mock.results[0].value;
|
|
112
|
-
expect(formulaInstance.install).toHaveBeenCalledWith(
|
|
115
|
+
expect(formulaInstance.install).toHaveBeenCalledWith(runtimeContext);
|
|
113
116
|
});
|
|
114
117
|
|
|
115
118
|
it('should not install formula plugin when disabled', async () => {
|
|
116
119
|
plugin = new ObjectQLPlugin({ enableFormulas: false });
|
|
117
|
-
|
|
120
|
+
const runtimeContext = { engine: mockContext.app };
|
|
121
|
+
await plugin.install(runtimeContext);
|
|
118
122
|
|
|
119
123
|
expect(FormulaPlugin).not.toHaveBeenCalled();
|
|
120
124
|
});
|
|
@@ -130,7 +134,8 @@ describe('ObjectQLPlugin Integration', () => {
|
|
|
130
134
|
validatorConfig,
|
|
131
135
|
});
|
|
132
136
|
|
|
133
|
-
|
|
137
|
+
const runtimeContext = { engine: mockContext.app };
|
|
138
|
+
await plugin.install(runtimeContext);
|
|
134
139
|
|
|
135
140
|
expect(ValidatorPlugin).toHaveBeenCalledWith(validatorConfig);
|
|
136
141
|
});
|
|
@@ -146,7 +151,8 @@ describe('ObjectQLPlugin Integration', () => {
|
|
|
146
151
|
formulaConfig,
|
|
147
152
|
});
|
|
148
153
|
|
|
149
|
-
|
|
154
|
+
const runtimeContext = { engine: mockContext.app };
|
|
155
|
+
await plugin.install(runtimeContext);
|
|
150
156
|
|
|
151
157
|
expect(FormulaPlugin).toHaveBeenCalledWith(formulaConfig);
|
|
152
158
|
});
|
|
@@ -157,7 +163,8 @@ describe('ObjectQLPlugin Integration', () => {
|
|
|
157
163
|
enableFormulas: true,
|
|
158
164
|
});
|
|
159
165
|
|
|
160
|
-
|
|
166
|
+
const runtimeContext = { engine: mockContext.app };
|
|
167
|
+
await plugin.install(runtimeContext);
|
|
161
168
|
|
|
162
169
|
expect(ValidatorPlugin).toHaveBeenCalled();
|
|
163
170
|
expect(FormulaPlugin).toHaveBeenCalled();
|
|
@@ -171,7 +178,8 @@ describe('ObjectQLPlugin Integration', () => {
|
|
|
171
178
|
enableAI: false,
|
|
172
179
|
});
|
|
173
180
|
|
|
174
|
-
|
|
181
|
+
const runtimeContext = { engine: mockContext.app };
|
|
182
|
+
await plugin.install(runtimeContext);
|
|
175
183
|
|
|
176
184
|
expect(ValidatorPlugin).not.toHaveBeenCalled();
|
|
177
185
|
expect(FormulaPlugin).not.toHaveBeenCalled();
|
|
@@ -183,15 +191,17 @@ describe('ObjectQLPlugin Integration', () => {
|
|
|
183
191
|
plugin = new ObjectQLPlugin();
|
|
184
192
|
expect(typeof plugin.onStart).toBe('function');
|
|
185
193
|
|
|
194
|
+
const runtimeContext = { engine: mockContext.app };
|
|
186
195
|
// Should not throw when called
|
|
187
|
-
await expect(plugin.onStart(
|
|
196
|
+
await expect(plugin.onStart(runtimeContext)).resolves.not.toThrow();
|
|
188
197
|
});
|
|
189
198
|
});
|
|
190
199
|
|
|
191
200
|
describe('Default Configuration', () => {
|
|
192
201
|
it('should enable all features by default', async () => {
|
|
193
202
|
plugin = new ObjectQLPlugin();
|
|
194
|
-
|
|
203
|
+
const runtimeContext = { engine: mockContext.app };
|
|
204
|
+
await plugin.install(runtimeContext);
|
|
195
205
|
|
|
196
206
|
// Validator and Formula should be installed by default
|
|
197
207
|
expect(ValidatorPlugin).toHaveBeenCalled();
|
|
@@ -203,7 +213,8 @@ describe('ObjectQLPlugin Integration', () => {
|
|
|
203
213
|
// Explicitly not setting enableValidator or enableFormulas
|
|
204
214
|
});
|
|
205
215
|
|
|
206
|
-
|
|
216
|
+
const runtimeContext = { engine: mockContext.app };
|
|
217
|
+
await plugin.install(runtimeContext);
|
|
207
218
|
|
|
208
219
|
// Both should still be installed
|
|
209
220
|
expect(ValidatorPlugin).toHaveBeenCalled();
|
package/tsconfig.json
CHANGED
|
@@ -7,13 +7,11 @@
|
|
|
7
7
|
"include": ["src/**/*"],
|
|
8
8
|
"exclude": [
|
|
9
9
|
"node_modules",
|
|
10
|
-
"dist"
|
|
11
|
-
// Exclude external @objectstack/objectql package that has type incompatibilities
|
|
12
|
-
// with our stub packages during migration phase
|
|
13
|
-
"../../../node_modules/@objectstack+objectql"
|
|
10
|
+
"dist"
|
|
14
11
|
],
|
|
15
12
|
"references": [
|
|
16
|
-
{ "path": "
|
|
17
|
-
{ "path": "../
|
|
13
|
+
{ "path": "../types" },
|
|
14
|
+
{ "path": "../plugin-validator" },
|
|
15
|
+
{ "path": "../plugin-formula" }
|
|
18
16
|
]
|
|
19
17
|
}
|