@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
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import type { UnifiedQuery, ObjectConfig, MetadataRegistry } from '@objectql/types';
|
|
10
10
|
import { Data } from '@objectstack/spec';
|
|
11
11
|
type QueryAST = Data.QueryAST;
|
|
12
|
-
type FilterNode = Data.FilterNode;
|
|
13
12
|
import { QueryService, QueryOptions } from './query-service';
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -167,10 +166,10 @@ export class QueryAnalyzer {
|
|
|
167
166
|
// Build the QueryAST (without executing)
|
|
168
167
|
const ast: QueryAST = {
|
|
169
168
|
object: objectName,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
where: query.where as any, // FilterCondition format
|
|
170
|
+
orderBy: query.orderBy as any, // Will be converted to SortNode[] format
|
|
171
|
+
limit: query.limit,
|
|
172
|
+
offset: query.offset,
|
|
174
173
|
fields: query.fields
|
|
175
174
|
};
|
|
176
175
|
|
|
@@ -273,7 +272,7 @@ export class QueryAnalyzer {
|
|
|
273
272
|
* @private
|
|
274
273
|
*/
|
|
275
274
|
private getSchema(objectName: string): ObjectConfig {
|
|
276
|
-
const obj = this.metadata.get
|
|
275
|
+
const obj = this.metadata.get('object', objectName);
|
|
277
276
|
if (!obj) {
|
|
278
277
|
throw new Error(`Object '${objectName}' not found`);
|
|
279
278
|
}
|
|
@@ -287,7 +286,7 @@ export class QueryAnalyzer {
|
|
|
287
286
|
private findApplicableIndexes(schema: ObjectConfig, query: UnifiedQuery): string[] {
|
|
288
287
|
const indexes: string[] = [];
|
|
289
288
|
|
|
290
|
-
if (!schema.indexes || !query.
|
|
289
|
+
if (!schema.indexes || !query.where) {
|
|
291
290
|
return indexes;
|
|
292
291
|
}
|
|
293
292
|
|
|
@@ -316,7 +315,7 @@ export class QueryAnalyzer {
|
|
|
316
315
|
}
|
|
317
316
|
};
|
|
318
317
|
|
|
319
|
-
extractFieldsFromFilter(query.
|
|
318
|
+
extractFieldsFromFilter(query.where);
|
|
320
319
|
|
|
321
320
|
// Check which indexes could be used
|
|
322
321
|
const indexesArray = Array.isArray(schema.indexes) ? schema.indexes : Object.values(schema.indexes || {});
|
|
@@ -343,7 +342,7 @@ export class QueryAnalyzer {
|
|
|
343
342
|
const warnings: string[] = [];
|
|
344
343
|
|
|
345
344
|
// Warning: No filters (full table scan)
|
|
346
|
-
if (!query.
|
|
345
|
+
if (!query.where || Object.keys(query.where).length === 0) {
|
|
347
346
|
warnings.push('No filters specified - this will scan all records');
|
|
348
347
|
}
|
|
349
348
|
|
|
@@ -361,7 +360,7 @@ export class QueryAnalyzer {
|
|
|
361
360
|
}
|
|
362
361
|
|
|
363
362
|
// Warning: Complex filters without indexes
|
|
364
|
-
if (query.
|
|
363
|
+
if (query.where && Object.keys(query.where).length > 5) {
|
|
365
364
|
const indexes = this.findApplicableIndexes(schema, query);
|
|
366
365
|
if (indexes.length === 0) {
|
|
367
366
|
warnings.push('Complex filters without matching indexes - consider adding indexes');
|
|
@@ -388,10 +387,8 @@ export class QueryAnalyzer {
|
|
|
388
387
|
}
|
|
389
388
|
|
|
390
389
|
// Suggest adding indexes
|
|
391
|
-
if (query.
|
|
392
|
-
const filterFields = query.
|
|
393
|
-
.filter((f: any) => Array.isArray(f) && f.length >= 1)
|
|
394
|
-
.map((f: any) => String(f[0]));
|
|
390
|
+
if (query.where && Object.keys(query.where).length > 0 && indexes.length === 0) {
|
|
391
|
+
const filterFields = Object.keys(query.where).filter(k => !k.startsWith('$'));
|
|
395
392
|
|
|
396
393
|
if (filterFields.length > 0) {
|
|
397
394
|
suggestions.push(`Consider adding an index on: ${filterFields.join(', ')}`);
|
|
@@ -404,10 +401,9 @@ export class QueryAnalyzer {
|
|
|
404
401
|
}
|
|
405
402
|
|
|
406
403
|
// Suggest composite index for multiple filters
|
|
407
|
-
if (query.
|
|
408
|
-
const filterFields = query.
|
|
409
|
-
.filter(
|
|
410
|
-
.map((f: any) => String(f[0]))
|
|
404
|
+
if (query.where && Object.keys(query.where).length > 1 && indexes.length < 2) {
|
|
405
|
+
const filterFields = Object.keys(query.where)
|
|
406
|
+
.filter(k => !k.startsWith('$'))
|
|
411
407
|
.slice(0, 3); // Top 3 fields
|
|
412
408
|
|
|
413
409
|
if (filterFields.length > 1) {
|
|
@@ -429,21 +425,20 @@ export class QueryAnalyzer {
|
|
|
429
425
|
complexity += 10;
|
|
430
426
|
|
|
431
427
|
// Filters add complexity
|
|
432
|
-
if (query.
|
|
433
|
-
|
|
428
|
+
if (query.where) {
|
|
429
|
+
const filterCount = Object.keys(query.where).length;
|
|
430
|
+
complexity += filterCount * 5;
|
|
434
431
|
|
|
435
|
-
// Nested filters (OR conditions) add more
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
);
|
|
439
|
-
if (hasNestedFilters) {
|
|
432
|
+
// Nested filters (OR/AND conditions) add more
|
|
433
|
+
const hasLogicalOps = query.where.$and || query.where.$or;
|
|
434
|
+
if (hasLogicalOps) {
|
|
440
435
|
complexity += 15;
|
|
441
436
|
}
|
|
442
437
|
}
|
|
443
438
|
|
|
444
439
|
// Sorting adds complexity
|
|
445
|
-
if (query.
|
|
446
|
-
complexity += query.
|
|
440
|
+
if (query.orderBy && query.orderBy.length > 0) {
|
|
441
|
+
complexity += query.orderBy.length * 3;
|
|
447
442
|
}
|
|
448
443
|
|
|
449
444
|
// Field selection reduces complexity slightly
|
|
@@ -469,13 +464,14 @@ export class QueryAnalyzer {
|
|
|
469
464
|
// from the database (row count, index selectivity, etc.)
|
|
470
465
|
|
|
471
466
|
// Default to unknown
|
|
472
|
-
if (!query.
|
|
467
|
+
if (!query.where || Object.keys(query.where).length === 0) {
|
|
473
468
|
return -1; // Unknown, full scan
|
|
474
469
|
}
|
|
475
470
|
|
|
476
471
|
// Very rough estimate based on filter count
|
|
477
472
|
const baseEstimate = 1000;
|
|
478
|
-
const
|
|
473
|
+
const filterCount = Object.keys(query.where).length;
|
|
474
|
+
const filterReduction = Math.pow(0.5, filterCount);
|
|
479
475
|
const estimated = Math.floor(baseEstimate * filterReduction);
|
|
480
476
|
|
|
481
477
|
// Apply limit if present
|
|
@@ -8,14 +8,23 @@
|
|
|
8
8
|
|
|
9
9
|
import type { UnifiedQuery } from '@objectql/types';
|
|
10
10
|
import { Data } from '@objectstack/spec';
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
// Local QueryAST type extension to include all properties we need
|
|
13
|
+
interface QueryAST extends Data.QueryAST {
|
|
14
|
+
top?: number;
|
|
15
|
+
expand?: Record<string, any>;
|
|
16
|
+
aggregations?: any[];
|
|
17
|
+
having?: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
import { FilterTranslator } from './filter-translator';
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
23
|
* Query Builder
|
|
16
24
|
*
|
|
17
25
|
* Builds ObjectStack QueryAST from ObjectQL UnifiedQuery.
|
|
18
|
-
*
|
|
26
|
+
* Since UnifiedQuery now uses the standard protocol format directly,
|
|
27
|
+
* this is now a simple pass-through with object name injection.
|
|
19
28
|
*/
|
|
20
29
|
export class QueryBuilder {
|
|
21
30
|
private filterTranslator: FilterTranslator;
|
|
@@ -28,53 +37,27 @@ export class QueryBuilder {
|
|
|
28
37
|
* Build a QueryAST from a UnifiedQuery
|
|
29
38
|
*
|
|
30
39
|
* @param objectName - Target object name
|
|
31
|
-
* @param query - ObjectQL UnifiedQuery
|
|
40
|
+
* @param query - ObjectQL UnifiedQuery (now in standard QueryAST format)
|
|
32
41
|
* @returns ObjectStack QueryAST
|
|
33
42
|
*/
|
|
34
43
|
build(objectName: string, query: UnifiedQuery): QueryAST {
|
|
44
|
+
// UnifiedQuery now uses the same format as QueryAST
|
|
45
|
+
// Just add the object name and pass through
|
|
35
46
|
const ast: QueryAST = {
|
|
36
|
-
object: objectName
|
|
47
|
+
object: objectName
|
|
37
48
|
};
|
|
38
49
|
|
|
39
|
-
// Map
|
|
40
|
-
if (query.fields)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
if (query.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (query.sort) {
|
|
51
|
-
ast.sort = query.sort.map(([field, order]) => ({
|
|
52
|
-
field,
|
|
53
|
-
order: order as 'asc' | 'desc'
|
|
54
|
-
}));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Map pagination
|
|
58
|
-
if (query.limit !== undefined) {
|
|
59
|
-
ast.top = query.limit;
|
|
60
|
-
}
|
|
61
|
-
if (query.skip !== undefined) {
|
|
62
|
-
ast.skip = query.skip;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Map groupBy
|
|
66
|
-
if (query.groupBy) {
|
|
67
|
-
ast.groupBy = query.groupBy;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Map aggregations
|
|
71
|
-
if (query.aggregate) {
|
|
72
|
-
ast.aggregations = query.aggregate.map(agg => ({
|
|
73
|
-
function: agg.func as any,
|
|
74
|
-
field: agg.field,
|
|
75
|
-
alias: agg.alias || `${agg.func}_${agg.field}`
|
|
76
|
-
}));
|
|
77
|
-
}
|
|
50
|
+
// Map UnifiedQuery properties to QueryAST
|
|
51
|
+
if (query.fields) ast.fields = query.fields;
|
|
52
|
+
if (query.where) ast.where = this.filterTranslator.translate(query.where);
|
|
53
|
+
if (query.orderBy) ast.orderBy = query.orderBy;
|
|
54
|
+
if (query.offset !== undefined) ast.offset = query.offset;
|
|
55
|
+
if (query.limit !== undefined) ast.top = query.limit; // UnifiedQuery uses 'limit', QueryAST uses 'top'
|
|
56
|
+
if (query.expand) ast.expand = query.expand;
|
|
57
|
+
if (query.groupBy) ast.groupBy = query.groupBy;
|
|
58
|
+
if (query.aggregations) ast.aggregations = query.aggregations;
|
|
59
|
+
if (query.having) ast.having = query.having;
|
|
60
|
+
if (query.distinct) ast.distinct = query.distinct;
|
|
78
61
|
|
|
79
62
|
return ast;
|
|
80
63
|
}
|
|
@@ -130,7 +130,7 @@ export class QueryService {
|
|
|
130
130
|
* @private
|
|
131
131
|
*/
|
|
132
132
|
private getSchema(objectName: string): ObjectConfig {
|
|
133
|
-
const obj = this.metadata.get
|
|
133
|
+
const obj = this.metadata.get('object', objectName);
|
|
134
134
|
if (!obj) {
|
|
135
135
|
throw new Error(`Object '${objectName}' not found in metadata`);
|
|
136
136
|
}
|
|
@@ -232,7 +232,7 @@ export class QueryService {
|
|
|
232
232
|
} else if (driver.executeQuery) {
|
|
233
233
|
// Fallback to query with ID filter
|
|
234
234
|
const query: UnifiedQuery = {
|
|
235
|
-
|
|
235
|
+
where: { _id: id }
|
|
236
236
|
};
|
|
237
237
|
const ast = this.buildQueryAST(objectName, query);
|
|
238
238
|
const queryResult = await driver.executeQuery(ast, driverOptions);
|
|
@@ -256,19 +256,19 @@ export class QueryService {
|
|
|
256
256
|
* Execute a count query
|
|
257
257
|
*
|
|
258
258
|
* @param objectName - The object to query
|
|
259
|
-
* @param
|
|
259
|
+
* @param where - Optional filter condition
|
|
260
260
|
* @param options - Query execution options
|
|
261
261
|
* @returns Count of matching records
|
|
262
262
|
*/
|
|
263
263
|
async count(
|
|
264
264
|
objectName: string,
|
|
265
|
-
|
|
265
|
+
where?: Filter,
|
|
266
266
|
options: QueryOptions = {}
|
|
267
267
|
): Promise<QueryResult<number>> {
|
|
268
268
|
const driver = this.getDriver(objectName);
|
|
269
269
|
const startTime = options.profile ? Date.now() : 0;
|
|
270
270
|
|
|
271
|
-
const query: UnifiedQuery =
|
|
271
|
+
const query: UnifiedQuery = where ? { where } : {};
|
|
272
272
|
const ast = this.buildQueryAST(objectName, query);
|
|
273
273
|
|
|
274
274
|
const driverOptions = {
|
|
@@ -280,7 +280,7 @@ export class QueryService {
|
|
|
280
280
|
|
|
281
281
|
if (driver.count) {
|
|
282
282
|
// Legacy driver interface
|
|
283
|
-
count = await driver.count(objectName,
|
|
283
|
+
count = await driver.count(objectName, where || {}, driverOptions);
|
|
284
284
|
} else if (driver.executeQuery) {
|
|
285
285
|
// Use executeQuery and count results
|
|
286
286
|
// Note: This is inefficient for large datasets
|
package/src/repository.ts
CHANGED
|
@@ -7,26 +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
|
-
type FilterNode = Data.FilterNode;
|
|
14
13
|
type SortNode = Data.SortNode;
|
|
15
|
-
import { Validator } from '
|
|
16
|
-
import { FormulaEngine } from '
|
|
14
|
+
import { Validator } from '@objectql/plugin-validator';
|
|
15
|
+
import { FormulaEngine } from '@objectql/plugin-formula';
|
|
17
16
|
import { QueryBuilder } from './query';
|
|
17
|
+
import { QueryCompiler } from './optimizations/QueryCompiler';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Extended ObjectStack Kernel with optional ObjectQL plugin capabilities.
|
|
21
21
|
* These properties are attached by ValidatorPlugin and FormulaPlugin during installation.
|
|
22
22
|
*/
|
|
23
|
-
interface ExtendedKernel extends
|
|
23
|
+
interface ExtendedKernel extends ObjectKernel {
|
|
24
24
|
validator?: Validator;
|
|
25
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>;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export class ObjectRepository {
|
|
29
34
|
private queryBuilder: QueryBuilder;
|
|
35
|
+
// Shared query compiler for caching compiled queries
|
|
36
|
+
private static queryCompiler = new QueryCompiler(1000);
|
|
30
37
|
|
|
31
38
|
constructor(
|
|
32
39
|
private objectName: string,
|
|
@@ -68,7 +75,7 @@ export class ObjectRepository {
|
|
|
68
75
|
return this.app.datasource(datasourceName);
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
private getKernel():
|
|
78
|
+
private getKernel(): ObjectKernel {
|
|
72
79
|
return this.app.getKernel();
|
|
73
80
|
}
|
|
74
81
|
|
|
@@ -81,9 +88,13 @@ export class ObjectRepository {
|
|
|
81
88
|
|
|
82
89
|
/**
|
|
83
90
|
* Translates ObjectQL UnifiedQuery to ObjectStack QueryAST format
|
|
91
|
+
* Uses query compiler for caching and optimization
|
|
84
92
|
*/
|
|
85
93
|
private buildQueryAST(query: UnifiedQuery): QueryAST {
|
|
86
|
-
|
|
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;
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
getSchema(): ObjectConfig {
|
|
@@ -225,9 +236,11 @@ export class ObjectRepository {
|
|
|
225
236
|
|
|
226
237
|
// Evaluate each formula field
|
|
227
238
|
for (const [fieldName, fieldConfig] of Object.entries(schema.fields)) {
|
|
228
|
-
|
|
239
|
+
const formulaExpression = fieldConfig.expression;
|
|
240
|
+
|
|
241
|
+
if (fieldConfig.type === 'formula' && formulaExpression) {
|
|
229
242
|
const result = this.getFormulaEngine().evaluate(
|
|
230
|
-
|
|
243
|
+
formulaExpression,
|
|
231
244
|
formulaContext,
|
|
232
245
|
fieldConfig.data_type || 'text',
|
|
233
246
|
{ strict: true }
|
|
@@ -240,14 +253,14 @@ export class ObjectRepository {
|
|
|
240
253
|
record[fieldName] = null;
|
|
241
254
|
// Formula evaluation should not throw here, but we need observability
|
|
242
255
|
// This logging is intentionally minimal and side-effect free
|
|
243
|
-
|
|
256
|
+
|
|
244
257
|
console.error(
|
|
245
258
|
'[ObjectQL][FormulaEngine] Formula evaluation failed',
|
|
246
259
|
{
|
|
247
260
|
objectName: this.objectName,
|
|
248
261
|
fieldName,
|
|
249
262
|
recordId: formulaContext.record_id,
|
|
250
|
-
|
|
263
|
+
expression: formulaExpression,
|
|
251
264
|
error: result.error,
|
|
252
265
|
stack: result.stack,
|
|
253
266
|
}
|
|
@@ -273,7 +286,7 @@ export class ObjectRepository {
|
|
|
273
286
|
|
|
274
287
|
// Build QueryAST and execute via kernel
|
|
275
288
|
const ast = this.buildQueryAST(hookCtx.query || {});
|
|
276
|
-
const kernelResult = await this.getKernel().find(this.objectName, ast);
|
|
289
|
+
const kernelResult = await (this.getKernel() as any).find(this.objectName, ast);
|
|
277
290
|
const results = kernelResult.value;
|
|
278
291
|
|
|
279
292
|
// Evaluate formulas for each result
|
|
@@ -299,7 +312,7 @@ export class ObjectRepository {
|
|
|
299
312
|
await this.app.triggerHook('beforeFind', this.objectName, hookCtx);
|
|
300
313
|
|
|
301
314
|
// Use kernel.get() for direct ID lookup
|
|
302
|
-
const result = await this.getKernel().get(this.objectName, String(idOrQuery));
|
|
315
|
+
const result = await (this.getKernel() as any).get(this.objectName, String(idOrQuery));
|
|
303
316
|
|
|
304
317
|
// Evaluate formulas if result exists
|
|
305
318
|
const resultWithFormulas = result ? this.evaluateFormulas(result) : result;
|
|
@@ -319,13 +332,13 @@ export class ObjectRepository {
|
|
|
319
332
|
// If filters is already a UnifiedQuery (has UnifiedQuery-specific properties), use it as-is
|
|
320
333
|
let query: UnifiedQuery;
|
|
321
334
|
if (Array.isArray(filters)) {
|
|
322
|
-
query = { filters };
|
|
323
|
-
} else if (filters && typeof filters === 'object' && (filters.fields || filters.
|
|
335
|
+
query = { where: filters };
|
|
336
|
+
} else if (filters && typeof filters === 'object' && (filters.fields || filters.orderBy || filters.limit !== undefined || filters.offset !== undefined)) {
|
|
324
337
|
// It's already a UnifiedQuery object
|
|
325
338
|
query = filters;
|
|
326
339
|
} else if (filters) {
|
|
327
340
|
// It's a raw filter object, wrap it
|
|
328
|
-
query = { filters };
|
|
341
|
+
query = { where: filters };
|
|
329
342
|
} else {
|
|
330
343
|
query = {};
|
|
331
344
|
}
|
|
@@ -343,7 +356,7 @@ export class ObjectRepository {
|
|
|
343
356
|
|
|
344
357
|
// Build QueryAST and execute via kernel to get count
|
|
345
358
|
const ast = this.buildQueryAST(hookCtx.query || {});
|
|
346
|
-
const kernelResult = await this.getKernel().find(this.objectName, ast);
|
|
359
|
+
const kernelResult = await (this.getKernel() as any).find(this.objectName, ast);
|
|
347
360
|
const result = kernelResult.count;
|
|
348
361
|
|
|
349
362
|
hookCtx.result = result;
|
|
@@ -371,7 +384,7 @@ export class ObjectRepository {
|
|
|
371
384
|
await this.validateRecord('create', finalDoc);
|
|
372
385
|
|
|
373
386
|
// Execute via kernel
|
|
374
|
-
const result = await this.getKernel().create(this.objectName, finalDoc);
|
|
387
|
+
const result = await (this.getKernel() as any).create(this.objectName, finalDoc, this.getOptions());
|
|
375
388
|
|
|
376
389
|
hookCtx.result = result;
|
|
377
390
|
await this.app.triggerHook('afterCreate', this.objectName, hookCtx);
|
|
@@ -398,7 +411,7 @@ export class ObjectRepository {
|
|
|
398
411
|
await this.validateRecord('update', hookCtx.data, previousData);
|
|
399
412
|
|
|
400
413
|
// Execute via kernel
|
|
401
|
-
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());
|
|
402
415
|
|
|
403
416
|
hookCtx.result = result;
|
|
404
417
|
await this.app.triggerHook('afterUpdate', this.objectName, hookCtx);
|
|
@@ -420,7 +433,7 @@ export class ObjectRepository {
|
|
|
420
433
|
await this.app.triggerHook('beforeDelete', this.objectName, hookCtx);
|
|
421
434
|
|
|
422
435
|
// Execute via kernel
|
|
423
|
-
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());
|
|
424
437
|
|
|
425
438
|
hookCtx.result = result;
|
|
426
439
|
await this.app.triggerHook('afterDelete', this.objectName, hookCtx);
|
|
@@ -463,7 +476,7 @@ export class ObjectRepository {
|
|
|
463
476
|
async updateMany(filters: any, data: any): Promise<any> {
|
|
464
477
|
// Find all matching records and update them individually
|
|
465
478
|
// to ensure validation and hooks are executed
|
|
466
|
-
const records = await this.find({ filters });
|
|
479
|
+
const records = await this.find({ where: filters });
|
|
467
480
|
let count = 0;
|
|
468
481
|
for (const record of records) {
|
|
469
482
|
if (record && record._id) {
|
|
@@ -477,7 +490,7 @@ export class ObjectRepository {
|
|
|
477
490
|
async deleteMany(filters: any): Promise<any> {
|
|
478
491
|
// Find all matching records and delete them individually
|
|
479
492
|
// to ensure hooks are executed
|
|
480
|
-
const records = await this.find({ filters });
|
|
493
|
+
const records = await this.find({ where: filters });
|
|
481
494
|
let count = 0;
|
|
482
495
|
for (const record of records) {
|
|
483
496
|
if (record && record._id) {
|
|
@@ -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
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import { ObjectQL } from '../src/app';
|
|
10
10
|
import { MockDriver } from './mock-driver';
|
|
11
11
|
import { ObjectConfig, HookContext, ActionContext, Metadata } from '@objectql/types';
|
|
12
|
-
import type {
|
|
12
|
+
import type { Kernel } from '@objectstack/spec';
|
|
13
|
+
type PluginDefinition = Kernel.PluginDefinition;
|
|
13
14
|
|
|
14
15
|
const todoObject: ObjectConfig = {
|
|
15
16
|
name: 'todo',
|
|
@@ -268,6 +269,8 @@ describe('ObjectQL App', () => {
|
|
|
268
269
|
const entry: Metadata = {
|
|
269
270
|
type: 'object',
|
|
270
271
|
id: 'todo',
|
|
272
|
+
// @ts-expect-error - SchemaRegistry typing limitation
|
|
273
|
+
name: 'todo', // Ensure SchemaRegistry keys it correctly
|
|
271
274
|
package: 'test-package',
|
|
272
275
|
content: obj
|
|
273
276
|
};
|
|
@@ -292,12 +295,12 @@ describe('ObjectQL App', () => {
|
|
|
292
295
|
|
|
293
296
|
describe('Plugin System', () => {
|
|
294
297
|
it('should initialize runtime plugins on init', async () => {
|
|
295
|
-
const
|
|
296
|
-
const
|
|
298
|
+
const initFn = jest.fn();
|
|
299
|
+
const startFn = jest.fn();
|
|
297
300
|
const mockPlugin = {
|
|
298
301
|
name: 'test-plugin',
|
|
299
|
-
|
|
300
|
-
|
|
302
|
+
init: initFn,
|
|
303
|
+
start: startFn
|
|
301
304
|
};
|
|
302
305
|
|
|
303
306
|
const app = new ObjectQL({
|
|
@@ -306,14 +309,14 @@ describe('ObjectQL App', () => {
|
|
|
306
309
|
});
|
|
307
310
|
|
|
308
311
|
await app.init();
|
|
309
|
-
expect(
|
|
310
|
-
expect(
|
|
312
|
+
expect(initFn).toHaveBeenCalled();
|
|
313
|
+
expect(startFn).toHaveBeenCalled();
|
|
311
314
|
});
|
|
312
315
|
|
|
313
316
|
it('should use plugin method', () => {
|
|
314
317
|
const mockPlugin = {
|
|
315
318
|
name: 'test-plugin',
|
|
316
|
-
|
|
319
|
+
init: jest.fn()
|
|
317
320
|
};
|
|
318
321
|
|
|
319
322
|
const app = new ObjectQL({ datasources: {} });
|