@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,329 @@
|
|
|
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
|
+
* Index metadata
|
|
11
|
+
*/
|
|
12
|
+
export interface IndexMetadata {
|
|
13
|
+
name: string;
|
|
14
|
+
fields: string[];
|
|
15
|
+
unique: boolean;
|
|
16
|
+
type?: 'btree' | 'hash' | 'fulltext';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Schema with index information
|
|
21
|
+
*/
|
|
22
|
+
export interface SchemaWithIndexes {
|
|
23
|
+
name: string;
|
|
24
|
+
fields: Record<string, any>;
|
|
25
|
+
indexes?: IndexMetadata[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Query AST for optimization
|
|
30
|
+
*/
|
|
31
|
+
export interface OptimizableQueryAST {
|
|
32
|
+
object: string;
|
|
33
|
+
fields?: string[];
|
|
34
|
+
filters?: any;
|
|
35
|
+
sort?: Array<{ field: string; order: 'asc' | 'desc' }>;
|
|
36
|
+
joins?: Array<{ type: 'left' | 'inner'; table: string; on: any }>;
|
|
37
|
+
limit?: number;
|
|
38
|
+
offset?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* SQL Query Optimizer
|
|
43
|
+
*
|
|
44
|
+
* Improvement: SQL-aware optimization with index hints and join reordering.
|
|
45
|
+
* Analyzes query patterns and schema to generate optimal SQL.
|
|
46
|
+
*
|
|
47
|
+
* Expected: 2-5x faster queries on large datasets
|
|
48
|
+
*/
|
|
49
|
+
export class SQLQueryOptimizer {
|
|
50
|
+
private schemas = new Map<string, SchemaWithIndexes>();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Register a schema with index information
|
|
54
|
+
*/
|
|
55
|
+
registerSchema(schema: SchemaWithIndexes): void {
|
|
56
|
+
this.schemas.set(schema.name, schema);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a field has an index
|
|
61
|
+
*/
|
|
62
|
+
private hasIndex(objectName: string, fieldName: string): boolean {
|
|
63
|
+
const schema = this.schemas.get(objectName);
|
|
64
|
+
if (!schema || !schema.indexes) return false;
|
|
65
|
+
|
|
66
|
+
return schema.indexes.some(index =>
|
|
67
|
+
index.fields.includes(fieldName)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the best index for given filter fields
|
|
73
|
+
*/
|
|
74
|
+
private getBestIndex(objectName: string, filterFields: string[]): IndexMetadata | null {
|
|
75
|
+
const schema = this.schemas.get(objectName);
|
|
76
|
+
if (!schema || !schema.indexes) return null;
|
|
77
|
+
|
|
78
|
+
// Find indexes that cover the filter fields
|
|
79
|
+
const candidateIndexes = schema.indexes.filter(index => {
|
|
80
|
+
// Check if index covers any of the filter fields
|
|
81
|
+
return filterFields.some(field => index.fields.includes(field));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (candidateIndexes.length === 0) return null;
|
|
85
|
+
|
|
86
|
+
// Prefer indexes that match more filter fields
|
|
87
|
+
candidateIndexes.sort((a, b) => {
|
|
88
|
+
const aMatches = a.fields.filter(f => filterFields.includes(f)).length;
|
|
89
|
+
const bMatches = b.fields.filter(f => filterFields.includes(f)).length;
|
|
90
|
+
return bMatches - aMatches;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return candidateIndexes[0];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract filter fields from filter conditions
|
|
98
|
+
*/
|
|
99
|
+
private extractFilterFields(filters: any): string[] {
|
|
100
|
+
const fields: string[] = [];
|
|
101
|
+
|
|
102
|
+
const extract = (obj: any) => {
|
|
103
|
+
if (!obj || typeof obj !== 'object') return;
|
|
104
|
+
|
|
105
|
+
for (const key of Object.keys(obj)) {
|
|
106
|
+
if (!key.startsWith('$')) {
|
|
107
|
+
fields.push(key);
|
|
108
|
+
}
|
|
109
|
+
if (typeof obj[key] === 'object') {
|
|
110
|
+
extract(obj[key]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
extract(filters);
|
|
116
|
+
return [...new Set(fields)]; // Remove duplicates
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Optimize join type based on query characteristics
|
|
121
|
+
*
|
|
122
|
+
* Note: This method expects filter fields in one of two formats:
|
|
123
|
+
* 1. Table-prefixed: "tableName.fieldName" (e.g., "accounts.type")
|
|
124
|
+
* 2. Simple field name: "fieldName" (checked against joined table schema)
|
|
125
|
+
*
|
|
126
|
+
* Multi-level qualification (e.g., "schema.table.field") is not currently supported.
|
|
127
|
+
*/
|
|
128
|
+
private optimizeJoinType(
|
|
129
|
+
join: { type: 'left' | 'inner'; table: string; on: any },
|
|
130
|
+
ast: OptimizableQueryAST
|
|
131
|
+
): 'left' | 'inner' {
|
|
132
|
+
// If we're filtering on the joined table, we can use INNER JOIN
|
|
133
|
+
if (ast.filters) {
|
|
134
|
+
const filterFields = this.extractFilterFields(ast.filters);
|
|
135
|
+
|
|
136
|
+
// Check if any filter field references the joined table
|
|
137
|
+
const hasFilterOnJoinTable = filterFields.some(field => {
|
|
138
|
+
// Handle table-prefixed fields like "accounts.type"
|
|
139
|
+
if (field.includes('.')) {
|
|
140
|
+
const parts = field.split('.');
|
|
141
|
+
// Only consider the first part as table name for simplicity
|
|
142
|
+
// This assumes format: "table.field" not "schema.table.field"
|
|
143
|
+
if (parts.length === 2 && parts[0] === join.table) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
// If more than 2 parts or table doesn't match, skip this field
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Also check if the field exists in the joined table schema (non-prefixed)
|
|
151
|
+
const schema = this.schemas.get(join.table);
|
|
152
|
+
if (schema) {
|
|
153
|
+
return schema.fields[field] !== undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return false;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (hasFilterOnJoinTable) {
|
|
160
|
+
return 'inner'; // Convert LEFT to INNER for better performance
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return join.type;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Optimize a query AST to SQL
|
|
169
|
+
*/
|
|
170
|
+
optimize(ast: OptimizableQueryAST): string {
|
|
171
|
+
const schema = this.schemas.get(ast.object);
|
|
172
|
+
if (!schema) {
|
|
173
|
+
// Fallback to basic SQL generation
|
|
174
|
+
return this.generateBasicSQL(ast);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let sql = 'SELECT ';
|
|
178
|
+
|
|
179
|
+
// Fields
|
|
180
|
+
if (ast.fields && ast.fields.length > 0) {
|
|
181
|
+
sql += ast.fields.join(', ');
|
|
182
|
+
} else {
|
|
183
|
+
sql += '*';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
sql += ` FROM ${ast.object}`;
|
|
187
|
+
|
|
188
|
+
// Index hints
|
|
189
|
+
if (ast.filters) {
|
|
190
|
+
const filterFields = this.extractFilterFields(ast.filters);
|
|
191
|
+
const bestIndex = this.getBestIndex(ast.object, filterFields);
|
|
192
|
+
|
|
193
|
+
if (bestIndex) {
|
|
194
|
+
// Add index hint for MySQL/MariaDB
|
|
195
|
+
sql += ` USE INDEX (${bestIndex.name})`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Optimized joins
|
|
200
|
+
if (ast.joins && ast.joins.length > 0) {
|
|
201
|
+
for (const join of ast.joins) {
|
|
202
|
+
const optimizedType = this.optimizeJoinType(join, ast);
|
|
203
|
+
sql += ` ${optimizedType.toUpperCase()} JOIN ${join.table}`;
|
|
204
|
+
|
|
205
|
+
// Simplified join condition
|
|
206
|
+
if (typeof join.on === 'string') {
|
|
207
|
+
sql += ` ON ${join.on}`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// WHERE clause
|
|
213
|
+
if (ast.filters) {
|
|
214
|
+
sql += ' WHERE ' + this.filtersToSQL(ast.filters);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ORDER BY
|
|
218
|
+
if (ast.sort && ast.sort.length > 0) {
|
|
219
|
+
sql += ' ORDER BY ';
|
|
220
|
+
sql += ast.sort.map(s => `${s.field} ${s.order.toUpperCase()}`).join(', ');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// LIMIT and OFFSET
|
|
224
|
+
if (ast.limit !== undefined) {
|
|
225
|
+
sql += ` LIMIT ${ast.limit}`;
|
|
226
|
+
}
|
|
227
|
+
if (ast.offset !== undefined) {
|
|
228
|
+
sql += ` OFFSET ${ast.offset}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return sql;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Convert filter object to SQL WHERE clause
|
|
236
|
+
*/
|
|
237
|
+
private filtersToSQL(filters: any): string {
|
|
238
|
+
if (typeof filters === 'string') {
|
|
239
|
+
return filters;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Simplified filter conversion
|
|
243
|
+
const conditions: string[] = [];
|
|
244
|
+
|
|
245
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
246
|
+
if (key === '$and') {
|
|
247
|
+
const subconditions = (value as any[]).map(f => this.filtersToSQL(f));
|
|
248
|
+
conditions.push(`(${subconditions.join(' AND ')})`);
|
|
249
|
+
} else if (key === '$or') {
|
|
250
|
+
const subconditions = (value as any[]).map(f => this.filtersToSQL(f));
|
|
251
|
+
conditions.push(`(${subconditions.join(' OR ')})`);
|
|
252
|
+
} else if (!key.startsWith('$')) {
|
|
253
|
+
// Field condition
|
|
254
|
+
if (typeof value === 'object' && value !== null) {
|
|
255
|
+
for (const [op, val] of Object.entries(value)) {
|
|
256
|
+
switch (op) {
|
|
257
|
+
case '$eq':
|
|
258
|
+
conditions.push(`${key} = '${val}'`);
|
|
259
|
+
break;
|
|
260
|
+
case '$ne':
|
|
261
|
+
conditions.push(`${key} != '${val}'`);
|
|
262
|
+
break;
|
|
263
|
+
case '$gt':
|
|
264
|
+
conditions.push(`${key} > '${val}'`);
|
|
265
|
+
break;
|
|
266
|
+
case '$gte':
|
|
267
|
+
conditions.push(`${key} >= '${val}'`);
|
|
268
|
+
break;
|
|
269
|
+
case '$lt':
|
|
270
|
+
conditions.push(`${key} < '${val}'`);
|
|
271
|
+
break;
|
|
272
|
+
case '$lte':
|
|
273
|
+
conditions.push(`${key} <= '${val}'`);
|
|
274
|
+
break;
|
|
275
|
+
case '$in':
|
|
276
|
+
const inValues = (val as any[]).map(v => `'${v}'`).join(', ');
|
|
277
|
+
conditions.push(`${key} IN (${inValues})`);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
conditions.push(`${key} = '${value}'`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return conditions.join(' AND ');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Fallback: generate basic SQL without optimizations
|
|
292
|
+
*/
|
|
293
|
+
private generateBasicSQL(ast: OptimizableQueryAST): string {
|
|
294
|
+
let sql = 'SELECT ';
|
|
295
|
+
|
|
296
|
+
if (ast.fields && ast.fields.length > 0) {
|
|
297
|
+
sql += ast.fields.join(', ');
|
|
298
|
+
} else {
|
|
299
|
+
sql += '*';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
sql += ` FROM ${ast.object}`;
|
|
303
|
+
|
|
304
|
+
if (ast.filters) {
|
|
305
|
+
sql += ' WHERE ' + this.filtersToSQL(ast.filters);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (ast.sort && ast.sort.length > 0) {
|
|
309
|
+
sql += ' ORDER BY ';
|
|
310
|
+
sql += ast.sort.map(s => `${s.field} ${s.order.toUpperCase()}`).join(', ');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (ast.limit !== undefined) {
|
|
314
|
+
sql += ` LIMIT ${ast.limit}`;
|
|
315
|
+
}
|
|
316
|
+
if (ast.offset !== undefined) {
|
|
317
|
+
sql += ` OFFSET ${ast.offset}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return sql;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Clear all registered schemas
|
|
325
|
+
*/
|
|
326
|
+
clearSchemas(): void {
|
|
327
|
+
this.schemas.clear();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
* Kernel Optimizations Module
|
|
11
|
+
*
|
|
12
|
+
* This module contains 10 key optimizations for the ObjectQL kernel:
|
|
13
|
+
*
|
|
14
|
+
* 1. OptimizedMetadataRegistry - O(k) package uninstall with secondary indexes
|
|
15
|
+
* 2. QueryCompiler - LRU cache for compiled query plans
|
|
16
|
+
* 3. CompiledHookManager - Pre-compiled hook pipelines
|
|
17
|
+
* 4. GlobalConnectionPool - Kernel-level connection pooling
|
|
18
|
+
* 5. OptimizedValidationEngine - Compiled validation rules
|
|
19
|
+
* 6. LazyMetadataLoader - On-demand metadata loading with predictive preload
|
|
20
|
+
* 7. DependencyGraph - DAG-based dependency resolution
|
|
21
|
+
* 8. SQLQueryOptimizer - SQL-aware query optimization with index hints
|
|
22
|
+
*
|
|
23
|
+
* Note: TypeScript type generation optimization (#7) and memory-mapped storage (#10)
|
|
24
|
+
* are not included in this release for compatibility and complexity reasons.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export { OptimizedMetadataRegistry } from './OptimizedMetadataRegistry';
|
|
28
|
+
export { QueryCompiler, type CompiledQuery } from './QueryCompiler';
|
|
29
|
+
export { CompiledHookManager, type Hook } from './CompiledHookManager';
|
|
30
|
+
export { GlobalConnectionPool, type Connection, type PoolLimits } from './GlobalConnectionPool';
|
|
31
|
+
export { OptimizedValidationEngine, type ValidatorFunction, type ValidationSchema } from './OptimizedValidationEngine';
|
|
32
|
+
export { LazyMetadataLoader, type ObjectMetadata, type MetadataLoader } from './LazyMetadataLoader';
|
|
33
|
+
export { DependencyGraph, type DependencyEdge, type DependencyType } from './DependencyGraph';
|
|
34
|
+
export { SQLQueryOptimizer, type IndexMetadata, type SchemaWithIndexes, type OptimizableQueryAST } from './SQLQueryOptimizer';
|
package/src/plugin.ts
CHANGED
|
@@ -6,18 +6,26 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { RuntimePlugin, RuntimeContext } from '@objectql/
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import { FormulaPlugin, FormulaPluginConfig } from './formula-plugin';
|
|
9
|
+
import type { RuntimePlugin, RuntimeContext } from '@objectql/types';
|
|
10
|
+
import { ValidatorPlugin, ValidatorPluginConfig } from '@objectql/plugin-validator';
|
|
11
|
+
import { FormulaPlugin, FormulaPluginConfig } from '@objectql/plugin-formula';
|
|
13
12
|
import { QueryService } from './query/query-service';
|
|
14
13
|
import { QueryAnalyzer } from './query/query-analyzer';
|
|
15
14
|
import type { Driver } from '@objectql/types';
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
|
-
* Extended
|
|
17
|
+
* Extended kernel with ObjectQL services
|
|
19
18
|
*/
|
|
20
|
-
interface ExtendedKernel
|
|
19
|
+
interface ExtendedKernel {
|
|
20
|
+
metadata?: any;
|
|
21
|
+
actions?: any;
|
|
22
|
+
hooks?: any;
|
|
23
|
+
getAllDrivers?: () => Driver[];
|
|
24
|
+
create?: (objectName: string, data: any) => Promise<any>;
|
|
25
|
+
update?: (objectName: string, id: string, data: any) => Promise<any>;
|
|
26
|
+
delete?: (objectName: string, id: string) => Promise<any>;
|
|
27
|
+
find?: (objectName: string, query: any) => Promise<any>;
|
|
28
|
+
get?: (objectName: string, id: string) => Promise<any>;
|
|
21
29
|
queryService?: QueryService;
|
|
22
30
|
queryAnalyzer?: QueryAnalyzer;
|
|
23
31
|
}
|
|
@@ -78,15 +86,14 @@ export interface ObjectQLPluginConfig {
|
|
|
78
86
|
/**
|
|
79
87
|
* ObjectQL Plugin
|
|
80
88
|
*
|
|
81
|
-
* Implements the RuntimePlugin interface
|
|
82
|
-
*
|
|
83
|
-
* on top of the ObjectStack kernel.
|
|
89
|
+
* Implements the RuntimePlugin interface to provide ObjectQL's enhanced features
|
|
90
|
+
* (Repository, Validator, Formula, AI) on top of the microkernel.
|
|
84
91
|
*/
|
|
85
92
|
export class ObjectQLPlugin implements RuntimePlugin {
|
|
86
93
|
name = '@objectql/core';
|
|
87
|
-
version = '4.0.
|
|
94
|
+
version = '4.0.2';
|
|
88
95
|
|
|
89
|
-
constructor(private config: ObjectQLPluginConfig = {}) {
|
|
96
|
+
constructor(private config: ObjectQLPluginConfig = {}, ql?: any) {
|
|
90
97
|
// Set defaults
|
|
91
98
|
this.config = {
|
|
92
99
|
enableRepository: true,
|
|
@@ -107,10 +114,28 @@ export class ObjectQLPlugin implements RuntimePlugin {
|
|
|
107
114
|
|
|
108
115
|
const kernel = ctx.engine as ExtendedKernel;
|
|
109
116
|
|
|
117
|
+
// Get datasources - either from config or from kernel drivers
|
|
118
|
+
let datasources = this.config.datasources;
|
|
119
|
+
if (!datasources) {
|
|
120
|
+
// Try to get drivers from kernel (micro-kernel pattern)
|
|
121
|
+
const drivers = kernel.getAllDrivers?.();
|
|
122
|
+
if (drivers && drivers.length > 0) {
|
|
123
|
+
datasources = {};
|
|
124
|
+
drivers.forEach((driver: any, index: number) => {
|
|
125
|
+
// Use driver name if available, otherwise use 'default' for first driver
|
|
126
|
+
const driverName = driver.name || (index === 0 ? 'default' : `driver_${index + 1}`);
|
|
127
|
+
datasources![driverName] = driver;
|
|
128
|
+
});
|
|
129
|
+
console.log(`[${this.name}] Using drivers from kernel:`, Object.keys(datasources));
|
|
130
|
+
} else {
|
|
131
|
+
console.warn(`[${this.name}] No datasources configured and no drivers found in kernel. Repository and QueryService will not be available.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
110
135
|
// Register QueryService and QueryAnalyzer if enabled
|
|
111
|
-
if (this.config.enableQueryService !== false &&
|
|
136
|
+
if (this.config.enableQueryService !== false && datasources) {
|
|
112
137
|
const queryService = new QueryService(
|
|
113
|
-
|
|
138
|
+
datasources,
|
|
114
139
|
kernel.metadata
|
|
115
140
|
);
|
|
116
141
|
kernel.queryService = queryService;
|
|
@@ -125,24 +150,24 @@ export class ObjectQLPlugin implements RuntimePlugin {
|
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
// Register components based on configuration
|
|
128
|
-
if (this.config.enableRepository !== false) {
|
|
129
|
-
await this.registerRepository(
|
|
153
|
+
if (this.config.enableRepository !== false && datasources) {
|
|
154
|
+
await this.registerRepository(kernel, datasources);
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
// Install validator plugin if enabled
|
|
133
158
|
if (this.config.enableValidator !== false) {
|
|
134
159
|
const validatorPlugin = new ValidatorPlugin(this.config.validatorConfig || {});
|
|
135
|
-
await validatorPlugin.install(ctx);
|
|
160
|
+
await validatorPlugin.install?.(ctx);
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
// Install formula plugin if enabled
|
|
139
164
|
if (this.config.enableFormulas !== false) {
|
|
140
165
|
const formulaPlugin = new FormulaPlugin(this.config.formulaConfig || {});
|
|
141
|
-
await formulaPlugin.install(ctx);
|
|
166
|
+
await formulaPlugin.install?.(ctx);
|
|
142
167
|
}
|
|
143
168
|
|
|
144
169
|
if (this.config.enableAI !== false) {
|
|
145
|
-
await this.registerAI(
|
|
170
|
+
await this.registerAI(kernel);
|
|
146
171
|
}
|
|
147
172
|
|
|
148
173
|
console.log(`[${this.name}] Plugin installed successfully`);
|
|
@@ -161,17 +186,10 @@ export class ObjectQLPlugin implements RuntimePlugin {
|
|
|
161
186
|
* Register the Repository pattern
|
|
162
187
|
* @private
|
|
163
188
|
*/
|
|
164
|
-
private async registerRepository(kernel:
|
|
165
|
-
if (!this.config.datasources) {
|
|
166
|
-
console.log(`[${this.name}] No datasources configured, skipping repository registration`);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const datasources = this.config.datasources;
|
|
171
|
-
|
|
189
|
+
private async registerRepository(kernel: any, datasources: Record<string, Driver>): Promise<void> {
|
|
172
190
|
// Helper function to get the driver for an object
|
|
173
191
|
const getDriver = (objectName: string): Driver => {
|
|
174
|
-
const objectConfig = kernel.metadata.get
|
|
192
|
+
const objectConfig = (kernel as any).metadata.get('object', objectName);
|
|
175
193
|
const datasourceName = objectConfig?.datasource || 'default';
|
|
176
194
|
const driver = datasources[datasourceName];
|
|
177
195
|
if (!driver) {
|
|
@@ -209,6 +227,11 @@ export class ObjectQLPlugin implements RuntimePlugin {
|
|
|
209
227
|
return await driver.findOne(objectName, id);
|
|
210
228
|
};
|
|
211
229
|
|
|
230
|
+
kernel.count = async (objectName: string, filters?: any): Promise<number> => {
|
|
231
|
+
const driver = getDriver(objectName);
|
|
232
|
+
return await driver.count(objectName, filters || {}, {});
|
|
233
|
+
};
|
|
234
|
+
|
|
212
235
|
console.log(`[${this.name}] Repository pattern registered`);
|
|
213
236
|
}
|
|
214
237
|
|
|
@@ -216,7 +239,7 @@ export class ObjectQLPlugin implements RuntimePlugin {
|
|
|
216
239
|
* Register AI integration
|
|
217
240
|
* @private
|
|
218
241
|
*/
|
|
219
|
-
private async registerAI(kernel:
|
|
242
|
+
private async registerAI(kernel: any): Promise<void> {
|
|
220
243
|
// TODO: Implement AI registration
|
|
221
244
|
// For now, this is a placeholder to establish the structure
|
|
222
245
|
console.log(`[${this.name}] AI integration registered`);
|
|
@@ -8,141 +8,34 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Filter } from '@objectql/types';
|
|
10
10
|
import { Data } from '@objectstack/spec';
|
|
11
|
-
type
|
|
11
|
+
type FilterCondition = Data.FilterCondition;
|
|
12
12
|
import { ObjectQLError } from '@objectql/types';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Filter Translator
|
|
16
16
|
*
|
|
17
|
-
* Translates ObjectQL Filter
|
|
18
|
-
*
|
|
17
|
+
* Translates ObjectQL Filter to ObjectStack FilterCondition format.
|
|
18
|
+
* Since both now use the same format, this is mostly a pass-through.
|
|
19
19
|
*
|
|
20
20
|
* @example
|
|
21
21
|
* Input: { age: { $gte: 18 }, $or: [{ status: "active" }, { role: "admin" }] }
|
|
22
|
-
* Output:
|
|
22
|
+
* Output: { age: { $gte: 18 }, $or: [{ status: "active" }, { role: "admin" }] }
|
|
23
23
|
*/
|
|
24
24
|
export class FilterTranslator {
|
|
25
25
|
/**
|
|
26
|
-
* Translate filters from ObjectQL format to ObjectStack
|
|
26
|
+
* Translate filters from ObjectQL format to ObjectStack FilterCondition format
|
|
27
27
|
*/
|
|
28
|
-
translate(filters?: Filter):
|
|
28
|
+
translate(filters?: Filter): FilterCondition | undefined {
|
|
29
29
|
if (!filters) {
|
|
30
30
|
return undefined;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// Backward compatibility: if it's already an array (old format), pass through
|
|
34
|
-
if (Array.isArray(filters)) {
|
|
35
|
-
return filters as unknown as FilterNode;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
// If it's an empty object, return undefined
|
|
39
34
|
if (typeof filters === 'object' && Object.keys(filters).length === 0) {
|
|
40
35
|
return undefined;
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Recursively converts FilterCondition to FilterNode array format
|
|
48
|
-
*/
|
|
49
|
-
private convertToNode(filter: Filter): FilterNode {
|
|
50
|
-
const nodes: any[] = [];
|
|
51
|
-
|
|
52
|
-
// Process logical operators first
|
|
53
|
-
if (filter.$and) {
|
|
54
|
-
const andNodes = filter.$and.map((f: Filter) => this.convertToNode(f));
|
|
55
|
-
nodes.push(...this.interleaveWithOperator(andNodes, 'and'));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (filter.$or) {
|
|
59
|
-
const orNodes = filter.$or.map((f: Filter) => this.convertToNode(f));
|
|
60
|
-
if (nodes.length > 0) {
|
|
61
|
-
nodes.push('and');
|
|
62
|
-
}
|
|
63
|
-
nodes.push(...this.interleaveWithOperator(orNodes, 'or'));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Note: $not operator is not currently supported in the legacy FilterNode format
|
|
67
|
-
if (filter.$not) {
|
|
68
|
-
throw new ObjectQLError({
|
|
69
|
-
code: 'UNSUPPORTED_OPERATOR',
|
|
70
|
-
message: '$not operator is not supported. Use $ne for field negation instead.'
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Process field conditions
|
|
75
|
-
for (const [field, value] of Object.entries(filter)) {
|
|
76
|
-
if (field.startsWith('$')) {
|
|
77
|
-
continue; // Skip logical operators (already processed)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (nodes.length > 0) {
|
|
81
|
-
nodes.push('and');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Handle field value
|
|
85
|
-
if (value === null || value === undefined) {
|
|
86
|
-
nodes.push([field, '=', value]);
|
|
87
|
-
} else if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
|
|
88
|
-
// Explicit operators - multiple operators on same field are AND-ed together
|
|
89
|
-
const entries = Object.entries(value);
|
|
90
|
-
for (let i = 0; i < entries.length; i++) {
|
|
91
|
-
const [op, opValue] = entries[i];
|
|
92
|
-
|
|
93
|
-
// Add 'and' before each operator (except the very first node)
|
|
94
|
-
if (nodes.length > 0 || i > 0) {
|
|
95
|
-
nodes.push('and');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const legacyOp = this.mapOperatorToLegacy(op);
|
|
99
|
-
nodes.push([field, legacyOp, opValue]);
|
|
100
|
-
}
|
|
101
|
-
} else {
|
|
102
|
-
// Implicit equality
|
|
103
|
-
nodes.push([field, '=', value]);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Return as FilterNode (type assertion for backward compatibility)
|
|
108
|
-
return (nodes.length === 1 ? nodes[0] : nodes) as unknown as FilterNode;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Interleaves filter nodes with a logical operator
|
|
113
|
-
*/
|
|
114
|
-
private interleaveWithOperator(nodes: FilterNode[], operator: string): any[] {
|
|
115
|
-
if (nodes.length === 0) return [];
|
|
116
|
-
if (nodes.length === 1) return [nodes[0]];
|
|
117
|
-
|
|
118
|
-
const result: any[] = [nodes[0]];
|
|
119
|
-
for (let i = 1; i < nodes.length; i++) {
|
|
120
|
-
result.push(operator, nodes[i]);
|
|
121
|
-
}
|
|
122
|
-
return result;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Maps modern $-prefixed operators to legacy format
|
|
127
|
-
*/
|
|
128
|
-
private mapOperatorToLegacy(operator: string): string {
|
|
129
|
-
const mapping: Record<string, string> = {
|
|
130
|
-
'$eq': '=',
|
|
131
|
-
'$ne': '!=',
|
|
132
|
-
'$gt': '>',
|
|
133
|
-
'$gte': '>=',
|
|
134
|
-
'$lt': '<',
|
|
135
|
-
'$lte': '<=',
|
|
136
|
-
'$in': 'in',
|
|
137
|
-
'$nin': 'nin',
|
|
138
|
-
'$contains': 'contains',
|
|
139
|
-
'$startsWith': 'startswith',
|
|
140
|
-
'$endsWith': 'endswith',
|
|
141
|
-
'$null': 'is_null',
|
|
142
|
-
'$exist': 'is_not_null',
|
|
143
|
-
'$between': 'between',
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
return mapping[operator] || operator.replace('$', '');
|
|
38
|
+
// Both ObjectQL Filter and ObjectStack FilterCondition use the same format now
|
|
39
|
+
return filters as unknown as FilterCondition;
|
|
147
40
|
}
|
|
148
41
|
}
|