@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.
Files changed (103) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +14 -12
  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/filter-translator.d.ts +6 -18
  41. package/dist/query/filter-translator.js +6 -103
  42. package/dist/query/filter-translator.js.map +1 -1
  43. package/dist/query/query-analyzer.js +24 -25
  44. package/dist/query/query-analyzer.js.map +1 -1
  45. package/dist/query/query-builder.d.ts +9 -3
  46. package/dist/query/query-builder.js +25 -35
  47. package/dist/query/query-builder.js.map +1 -1
  48. package/dist/query/query-service.d.ts +2 -2
  49. package/dist/query/query-service.js +5 -5
  50. package/dist/query/query-service.js.map +1 -1
  51. package/dist/repository.d.ts +2 -0
  52. package/dist/repository.js +24 -17
  53. package/dist/repository.js.map +1 -1
  54. package/jest.config.js +3 -3
  55. package/package.json +8 -5
  56. package/src/app.ts +173 -47
  57. package/src/index.ts +7 -8
  58. package/src/optimizations/CompiledHookManager.ts +185 -0
  59. package/src/optimizations/DependencyGraph.ts +255 -0
  60. package/src/optimizations/GlobalConnectionPool.ts +251 -0
  61. package/src/optimizations/LazyMetadataLoader.ts +180 -0
  62. package/src/optimizations/OptimizedMetadataRegistry.ts +132 -0
  63. package/src/optimizations/OptimizedValidationEngine.ts +172 -0
  64. package/src/optimizations/QueryCompiler.ts +242 -0
  65. package/src/optimizations/SQLQueryOptimizer.ts +329 -0
  66. package/src/optimizations/index.ts +34 -0
  67. package/src/plugin.ts +51 -28
  68. package/src/query/filter-translator.ts +8 -115
  69. package/src/query/query-analyzer.ts +25 -29
  70. package/src/query/query-builder.ts +26 -43
  71. package/src/query/query-service.ts +6 -6
  72. package/src/repository.ts +35 -22
  73. package/test/__mocks__/@objectstack/runtime.ts +8 -8
  74. package/test/app.test.ts +11 -8
  75. package/test/optimizations.test.ts +440 -0
  76. package/test/plugin-integration.test.ts +30 -19
  77. package/tsconfig.json +4 -6
  78. package/tsconfig.tsbuildinfo +1 -1
  79. package/dist/ai-agent.d.ts +0 -176
  80. package/dist/ai-agent.js +0 -722
  81. package/dist/ai-agent.js.map +0 -1
  82. package/dist/formula-engine.d.ts +0 -102
  83. package/dist/formula-engine.js +0 -433
  84. package/dist/formula-engine.js.map +0 -1
  85. package/dist/formula-plugin.d.ts +0 -52
  86. package/dist/formula-plugin.js +0 -107
  87. package/dist/formula-plugin.js.map +0 -1
  88. package/dist/validator-plugin.d.ts +0 -56
  89. package/dist/validator-plugin.js +0 -106
  90. package/dist/validator-plugin.js.map +0 -1
  91. package/dist/validator.d.ts +0 -80
  92. package/dist/validator.js +0 -625
  93. package/dist/validator.js.map +0 -1
  94. package/src/ai-agent.ts +0 -868
  95. package/src/formula-engine.ts +0 -572
  96. package/src/formula-plugin.ts +0 -141
  97. package/src/validator-plugin.ts +0 -140
  98. package/src/validator.ts +0 -743
  99. package/test/formula-engine.test.ts +0 -725
  100. package/test/formula-integration.test.ts +0 -286
  101. package/test/formula-plugin.test.ts +0 -197
  102. package/test/validator-plugin.test.ts +0 -126
  103. 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/runtime';
10
- import type { ObjectStackKernel } from '@objectql/runtime';
11
- import { ValidatorPlugin, ValidatorPluginConfig } from './validator-plugin';
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 ObjectStack Kernel with ObjectQL services
17
+ * Extended kernel with ObjectQL services
19
18
  */
20
- interface ExtendedKernel extends ObjectStackKernel {
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 from @objectql/runtime
82
- * to provide ObjectQL's enhanced features (Repository, Validator, Formula, AI)
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.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 && this.config.datasources) {
136
+ if (this.config.enableQueryService !== false && datasources) {
112
137
  const queryService = new QueryService(
113
- this.config.datasources,
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(ctx.engine);
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(ctx.engine);
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: ObjectStackKernel): Promise<void> {
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<any>('object', objectName);
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: ObjectStackKernel): Promise<void> {
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 FilterNode = Data.FilterNode;
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 (FilterCondition) to ObjectStack FilterNode format.
18
- * Converts modern object-based syntax to legacy array-based syntax for backward compatibility.
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: [["age", ">=", 18], "or", [["status", "=", "active"], "or", ["role", "=", "admin"]]]
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 FilterNode format
26
+ * Translate filters from ObjectQL format to ObjectStack FilterCondition format
27
27
  */
28
- translate(filters?: Filter): FilterNode | undefined {
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
- return this.convertToNode(filters);
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
  }