@objectql/core 4.0.0 → 4.0.2
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/CHANGELOG.md +31 -3
- package/README.md +13 -11
- package/dist/app.d.ts +1 -1
- package/dist/app.js +1 -1
- package/dist/app.js.map +1 -1
- package/dist/formula-plugin.d.ts +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.js +40 -3
- package/dist/plugin.js.map +1 -1
- package/dist/query/filter-translator.d.ts +8 -18
- package/dist/query/filter-translator.js +6 -103
- package/dist/query/filter-translator.js.map +1 -1
- package/dist/query/query-analyzer.d.ts +3 -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 +6 -3
- package/dist/query/query-builder.js +9 -35
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-service.d.ts +5 -3
- package/dist/query/query-service.js +5 -5
- package/dist/query/query-service.js.map +1 -1
- package/dist/repository.js +25 -6
- package/dist/repository.js.map +1 -1
- package/dist/validator-plugin.d.ts +1 -1
- package/jest.config.js +1 -1
- package/package.json +4 -5
- package/src/app.ts +3 -3
- package/src/formula-plugin.ts +1 -1
- package/src/index.ts +5 -2
- package/src/plugin.ts +50 -5
- package/src/query/filter-translator.ts +9 -115
- package/src/query/query-analyzer.ts +26 -28
- package/src/query/query-builder.ts +11 -41
- package/src/query/query-service.ts +7 -6
- package/src/repository.ts +28 -8
- package/src/validator-plugin.ts +1 -1
- package/test/__mocks__/@objectstack/runtime.ts +1 -1
- package/test/app.test.ts +2 -1
- package/test/formula-integration.test.ts +6 -6
- package/test/formula-plugin.test.ts +1 -1
- package/test/formula-spec-compliance.test.ts +258 -0
- package/test/validation-spec-compliance.test.ts +440 -0
- package/test/validator-plugin.test.ts +1 -1
- package/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/IMPLEMENTATION_STATUS.md +0 -364
- package/RUNTIME_INTEGRATION.md +0 -391
|
@@ -7,141 +7,35 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Filter } from '@objectql/types';
|
|
10
|
-
import
|
|
10
|
+
import { Data } from '@objectstack/spec';
|
|
11
|
+
type FilterCondition = Data.FilterCondition;
|
|
11
12
|
import { ObjectQLError } from '@objectql/types';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Filter Translator
|
|
15
16
|
*
|
|
16
|
-
* Translates ObjectQL Filter
|
|
17
|
-
*
|
|
17
|
+
* Translates ObjectQL Filter to ObjectStack FilterCondition format.
|
|
18
|
+
* Since both now use the same format, this is mostly a pass-through.
|
|
18
19
|
*
|
|
19
20
|
* @example
|
|
20
21
|
* Input: { age: { $gte: 18 }, $or: [{ status: "active" }, { role: "admin" }] }
|
|
21
|
-
* Output:
|
|
22
|
+
* Output: { age: { $gte: 18 }, $or: [{ status: "active" }, { role: "admin" }] }
|
|
22
23
|
*/
|
|
23
24
|
export class FilterTranslator {
|
|
24
25
|
/**
|
|
25
|
-
* Translate filters from ObjectQL format to ObjectStack
|
|
26
|
+
* Translate filters from ObjectQL format to ObjectStack FilterCondition format
|
|
26
27
|
*/
|
|
27
|
-
translate(filters?: Filter):
|
|
28
|
+
translate(filters?: Filter): FilterCondition | undefined {
|
|
28
29
|
if (!filters) {
|
|
29
30
|
return undefined;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// Backward compatibility: if it's already an array (old format), pass through
|
|
33
|
-
if (Array.isArray(filters)) {
|
|
34
|
-
return filters as unknown as FilterNode;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
33
|
// If it's an empty object, return undefined
|
|
38
34
|
if (typeof filters === 'object' && Object.keys(filters).length === 0) {
|
|
39
35
|
return undefined;
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Recursively converts FilterCondition to FilterNode array format
|
|
47
|
-
*/
|
|
48
|
-
private convertToNode(filter: Filter): FilterNode {
|
|
49
|
-
const nodes: any[] = [];
|
|
50
|
-
|
|
51
|
-
// Process logical operators first
|
|
52
|
-
if (filter.$and) {
|
|
53
|
-
const andNodes = filter.$and.map((f: Filter) => this.convertToNode(f));
|
|
54
|
-
nodes.push(...this.interleaveWithOperator(andNodes, 'and'));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (filter.$or) {
|
|
58
|
-
const orNodes = filter.$or.map((f: Filter) => this.convertToNode(f));
|
|
59
|
-
if (nodes.length > 0) {
|
|
60
|
-
nodes.push('and');
|
|
61
|
-
}
|
|
62
|
-
nodes.push(...this.interleaveWithOperator(orNodes, 'or'));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Note: $not operator is not currently supported in the legacy FilterNode format
|
|
66
|
-
if (filter.$not) {
|
|
67
|
-
throw new ObjectQLError({
|
|
68
|
-
code: 'UNSUPPORTED_OPERATOR',
|
|
69
|
-
message: '$not operator is not supported. Use $ne for field negation instead.'
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Process field conditions
|
|
74
|
-
for (const [field, value] of Object.entries(filter)) {
|
|
75
|
-
if (field.startsWith('$')) {
|
|
76
|
-
continue; // Skip logical operators (already processed)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (nodes.length > 0) {
|
|
80
|
-
nodes.push('and');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Handle field value
|
|
84
|
-
if (value === null || value === undefined) {
|
|
85
|
-
nodes.push([field, '=', value]);
|
|
86
|
-
} else if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
|
|
87
|
-
// Explicit operators - multiple operators on same field are AND-ed together
|
|
88
|
-
const entries = Object.entries(value);
|
|
89
|
-
for (let i = 0; i < entries.length; i++) {
|
|
90
|
-
const [op, opValue] = entries[i];
|
|
91
|
-
|
|
92
|
-
// Add 'and' before each operator (except the very first node)
|
|
93
|
-
if (nodes.length > 0 || i > 0) {
|
|
94
|
-
nodes.push('and');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const legacyOp = this.mapOperatorToLegacy(op);
|
|
98
|
-
nodes.push([field, legacyOp, opValue]);
|
|
99
|
-
}
|
|
100
|
-
} else {
|
|
101
|
-
// Implicit equality
|
|
102
|
-
nodes.push([field, '=', value]);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Return as FilterNode (type assertion for backward compatibility)
|
|
107
|
-
return (nodes.length === 1 ? nodes[0] : nodes) as unknown as FilterNode;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Interleaves filter nodes with a logical operator
|
|
112
|
-
*/
|
|
113
|
-
private interleaveWithOperator(nodes: FilterNode[], operator: string): any[] {
|
|
114
|
-
if (nodes.length === 0) return [];
|
|
115
|
-
if (nodes.length === 1) return [nodes[0]];
|
|
116
|
-
|
|
117
|
-
const result: any[] = [nodes[0]];
|
|
118
|
-
for (let i = 1; i < nodes.length; i++) {
|
|
119
|
-
result.push(operator, nodes[i]);
|
|
120
|
-
}
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Maps modern $-prefixed operators to legacy format
|
|
126
|
-
*/
|
|
127
|
-
private mapOperatorToLegacy(operator: string): string {
|
|
128
|
-
const mapping: Record<string, string> = {
|
|
129
|
-
'$eq': '=',
|
|
130
|
-
'$ne': '!=',
|
|
131
|
-
'$gt': '>',
|
|
132
|
-
'$gte': '>=',
|
|
133
|
-
'$lt': '<',
|
|
134
|
-
'$lte': '<=',
|
|
135
|
-
'$in': 'in',
|
|
136
|
-
'$nin': 'nin',
|
|
137
|
-
'$contains': 'contains',
|
|
138
|
-
'$startsWith': 'startswith',
|
|
139
|
-
'$endsWith': 'endswith',
|
|
140
|
-
'$null': 'is_null',
|
|
141
|
-
'$exist': 'is_not_null',
|
|
142
|
-
'$between': 'between',
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
return mapping[operator] || operator.replace('$', '');
|
|
38
|
+
// Both ObjectQL Filter and ObjectStack FilterCondition use the same format now
|
|
39
|
+
return filters as unknown as FilterCondition;
|
|
146
40
|
}
|
|
147
41
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { UnifiedQuery, ObjectConfig, MetadataRegistry } from '@objectql/types';
|
|
10
|
-
import
|
|
10
|
+
import { Data } from '@objectstack/spec';
|
|
11
|
+
type QueryAST = Data.QueryAST;
|
|
11
12
|
import { QueryService, QueryOptions } from './query-service';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -165,10 +166,10 @@ export class QueryAnalyzer {
|
|
|
165
166
|
// Build the QueryAST (without executing)
|
|
166
167
|
const ast: QueryAST = {
|
|
167
168
|
object: objectName,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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,
|
|
172
173
|
fields: query.fields
|
|
173
174
|
};
|
|
174
175
|
|
|
@@ -285,7 +286,7 @@ export class QueryAnalyzer {
|
|
|
285
286
|
private findApplicableIndexes(schema: ObjectConfig, query: UnifiedQuery): string[] {
|
|
286
287
|
const indexes: string[] = [];
|
|
287
288
|
|
|
288
|
-
if (!schema.indexes || !query.
|
|
289
|
+
if (!schema.indexes || !query.where) {
|
|
289
290
|
return indexes;
|
|
290
291
|
}
|
|
291
292
|
|
|
@@ -314,7 +315,7 @@ export class QueryAnalyzer {
|
|
|
314
315
|
}
|
|
315
316
|
};
|
|
316
317
|
|
|
317
|
-
extractFieldsFromFilter(query.
|
|
318
|
+
extractFieldsFromFilter(query.where);
|
|
318
319
|
|
|
319
320
|
// Check which indexes could be used
|
|
320
321
|
const indexesArray = Array.isArray(schema.indexes) ? schema.indexes : Object.values(schema.indexes || {});
|
|
@@ -341,7 +342,7 @@ export class QueryAnalyzer {
|
|
|
341
342
|
const warnings: string[] = [];
|
|
342
343
|
|
|
343
344
|
// Warning: No filters (full table scan)
|
|
344
|
-
if (!query.
|
|
345
|
+
if (!query.where || Object.keys(query.where).length === 0) {
|
|
345
346
|
warnings.push('No filters specified - this will scan all records');
|
|
346
347
|
}
|
|
347
348
|
|
|
@@ -359,7 +360,7 @@ export class QueryAnalyzer {
|
|
|
359
360
|
}
|
|
360
361
|
|
|
361
362
|
// Warning: Complex filters without indexes
|
|
362
|
-
if (query.
|
|
363
|
+
if (query.where && Object.keys(query.where).length > 5) {
|
|
363
364
|
const indexes = this.findApplicableIndexes(schema, query);
|
|
364
365
|
if (indexes.length === 0) {
|
|
365
366
|
warnings.push('Complex filters without matching indexes - consider adding indexes');
|
|
@@ -386,10 +387,8 @@ export class QueryAnalyzer {
|
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
// Suggest adding indexes
|
|
389
|
-
if (query.
|
|
390
|
-
const filterFields = query.
|
|
391
|
-
.filter((f: any) => Array.isArray(f) && f.length >= 1)
|
|
392
|
-
.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('$'));
|
|
393
392
|
|
|
394
393
|
if (filterFields.length > 0) {
|
|
395
394
|
suggestions.push(`Consider adding an index on: ${filterFields.join(', ')}`);
|
|
@@ -402,10 +401,9 @@ export class QueryAnalyzer {
|
|
|
402
401
|
}
|
|
403
402
|
|
|
404
403
|
// Suggest composite index for multiple filters
|
|
405
|
-
if (query.
|
|
406
|
-
const filterFields = query.
|
|
407
|
-
.filter(
|
|
408
|
-
.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('$'))
|
|
409
407
|
.slice(0, 3); // Top 3 fields
|
|
410
408
|
|
|
411
409
|
if (filterFields.length > 1) {
|
|
@@ -427,21 +425,20 @@ export class QueryAnalyzer {
|
|
|
427
425
|
complexity += 10;
|
|
428
426
|
|
|
429
427
|
// Filters add complexity
|
|
430
|
-
if (query.
|
|
431
|
-
|
|
428
|
+
if (query.where) {
|
|
429
|
+
const filterCount = Object.keys(query.where).length;
|
|
430
|
+
complexity += filterCount * 5;
|
|
432
431
|
|
|
433
|
-
// Nested filters (OR conditions) add more
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
);
|
|
437
|
-
if (hasNestedFilters) {
|
|
432
|
+
// Nested filters (OR/AND conditions) add more
|
|
433
|
+
const hasLogicalOps = query.where.$and || query.where.$or;
|
|
434
|
+
if (hasLogicalOps) {
|
|
438
435
|
complexity += 15;
|
|
439
436
|
}
|
|
440
437
|
}
|
|
441
438
|
|
|
442
439
|
// Sorting adds complexity
|
|
443
|
-
if (query.
|
|
444
|
-
complexity += query.
|
|
440
|
+
if (query.orderBy && query.orderBy.length > 0) {
|
|
441
|
+
complexity += query.orderBy.length * 3;
|
|
445
442
|
}
|
|
446
443
|
|
|
447
444
|
// Field selection reduces complexity slightly
|
|
@@ -467,13 +464,14 @@ export class QueryAnalyzer {
|
|
|
467
464
|
// from the database (row count, index selectivity, etc.)
|
|
468
465
|
|
|
469
466
|
// Default to unknown
|
|
470
|
-
if (!query.
|
|
467
|
+
if (!query.where || Object.keys(query.where).length === 0) {
|
|
471
468
|
return -1; // Unknown, full scan
|
|
472
469
|
}
|
|
473
470
|
|
|
474
471
|
// Very rough estimate based on filter count
|
|
475
472
|
const baseEstimate = 1000;
|
|
476
|
-
const
|
|
473
|
+
const filterCount = Object.keys(query.where).length;
|
|
474
|
+
const filterReduction = Math.pow(0.5, filterCount);
|
|
477
475
|
const estimated = Math.floor(baseEstimate * filterReduction);
|
|
478
476
|
|
|
479
477
|
// Apply limit if present
|
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { UnifiedQuery } from '@objectql/types';
|
|
10
|
-
import
|
|
10
|
+
import { Data } from '@objectstack/spec';
|
|
11
|
+
type QueryAST = Data.QueryAST;
|
|
11
12
|
import { FilterTranslator } from './filter-translator';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Query Builder
|
|
15
16
|
*
|
|
16
17
|
* Builds ObjectStack QueryAST from ObjectQL UnifiedQuery.
|
|
17
|
-
*
|
|
18
|
+
* Since UnifiedQuery now uses the standard protocol format directly,
|
|
19
|
+
* this is now a simple pass-through with object name injection.
|
|
18
20
|
*/
|
|
19
21
|
export class QueryBuilder {
|
|
20
22
|
private filterTranslator: FilterTranslator;
|
|
@@ -27,52 +29,20 @@ export class QueryBuilder {
|
|
|
27
29
|
* Build a QueryAST from a UnifiedQuery
|
|
28
30
|
*
|
|
29
31
|
* @param objectName - Target object name
|
|
30
|
-
* @param query - ObjectQL UnifiedQuery
|
|
32
|
+
* @param query - ObjectQL UnifiedQuery (now in standard QueryAST format)
|
|
31
33
|
* @returns ObjectStack QueryAST
|
|
32
34
|
*/
|
|
33
35
|
build(objectName: string, query: UnifiedQuery): QueryAST {
|
|
36
|
+
// UnifiedQuery now uses the same format as QueryAST
|
|
37
|
+
// Just add the object name and pass through
|
|
34
38
|
const ast: QueryAST = {
|
|
35
39
|
object: objectName,
|
|
40
|
+
...query
|
|
36
41
|
};
|
|
37
42
|
|
|
38
|
-
//
|
|
39
|
-
if (query.
|
|
40
|
-
ast.
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Map filters using FilterTranslator
|
|
44
|
-
if (query.filters) {
|
|
45
|
-
ast.filters = this.filterTranslator.translate(query.filters);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Map sort
|
|
49
|
-
if (query.sort) {
|
|
50
|
-
ast.sort = query.sort.map(([field, order]) => ({
|
|
51
|
-
field,
|
|
52
|
-
order: order as 'asc' | 'desc'
|
|
53
|
-
}));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Map pagination
|
|
57
|
-
if (query.limit !== undefined) {
|
|
58
|
-
ast.top = query.limit;
|
|
59
|
-
}
|
|
60
|
-
if (query.skip !== undefined) {
|
|
61
|
-
ast.skip = query.skip;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Map groupBy
|
|
65
|
-
if (query.groupBy) {
|
|
66
|
-
ast.groupBy = query.groupBy;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Map aggregations
|
|
70
|
-
if (query.aggregate) {
|
|
71
|
-
ast.aggregations = query.aggregate.map(agg => ({
|
|
72
|
-
function: agg.func as any,
|
|
73
|
-
field: agg.field,
|
|
74
|
-
alias: agg.alias || `${agg.func}_${agg.field}`
|
|
75
|
-
}));
|
|
43
|
+
// Ensure where is properly formatted
|
|
44
|
+
if (query.where) {
|
|
45
|
+
ast.where = this.filterTranslator.translate(query.where);
|
|
76
46
|
}
|
|
77
47
|
|
|
78
48
|
return ast;
|
|
@@ -13,7 +13,8 @@ import type {
|
|
|
13
13
|
Filter,
|
|
14
14
|
MetadataRegistry
|
|
15
15
|
} from '@objectql/types';
|
|
16
|
-
import
|
|
16
|
+
import { Data } from '@objectstack/spec';
|
|
17
|
+
type QueryAST = Data.QueryAST;
|
|
17
18
|
import { QueryBuilder } from './query-builder';
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -231,7 +232,7 @@ export class QueryService {
|
|
|
231
232
|
} else if (driver.executeQuery) {
|
|
232
233
|
// Fallback to query with ID filter
|
|
233
234
|
const query: UnifiedQuery = {
|
|
234
|
-
|
|
235
|
+
where: { _id: id }
|
|
235
236
|
};
|
|
236
237
|
const ast = this.buildQueryAST(objectName, query);
|
|
237
238
|
const queryResult = await driver.executeQuery(ast, driverOptions);
|
|
@@ -255,19 +256,19 @@ export class QueryService {
|
|
|
255
256
|
* Execute a count query
|
|
256
257
|
*
|
|
257
258
|
* @param objectName - The object to query
|
|
258
|
-
* @param
|
|
259
|
+
* @param where - Optional filter condition
|
|
259
260
|
* @param options - Query execution options
|
|
260
261
|
* @returns Count of matching records
|
|
261
262
|
*/
|
|
262
263
|
async count(
|
|
263
264
|
objectName: string,
|
|
264
|
-
|
|
265
|
+
where?: Filter,
|
|
265
266
|
options: QueryOptions = {}
|
|
266
267
|
): Promise<QueryResult<number>> {
|
|
267
268
|
const driver = this.getDriver(objectName);
|
|
268
269
|
const startTime = options.profile ? Date.now() : 0;
|
|
269
270
|
|
|
270
|
-
const query: UnifiedQuery =
|
|
271
|
+
const query: UnifiedQuery = where ? { where } : {};
|
|
271
272
|
const ast = this.buildQueryAST(objectName, query);
|
|
272
273
|
|
|
273
274
|
const driverOptions = {
|
|
@@ -279,7 +280,7 @@ export class QueryService {
|
|
|
279
280
|
|
|
280
281
|
if (driver.count) {
|
|
281
282
|
// Legacy driver interface
|
|
282
|
-
count = await driver.count(objectName,
|
|
283
|
+
count = await driver.count(objectName, where || {}, driverOptions);
|
|
283
284
|
} else if (driver.executeQuery) {
|
|
284
285
|
// Use executeQuery and count results
|
|
285
286
|
// Note: This is inefficient for large datasets
|
package/src/repository.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
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 { ObjectStackKernel } from '@
|
|
11
|
-
import
|
|
10
|
+
import type { ObjectStackKernel } from '@objectql/runtime';
|
|
11
|
+
import { Data } from '@objectstack/spec';
|
|
12
|
+
type QueryAST = Data.QueryAST;
|
|
13
|
+
type SortNode = Data.SortNode;
|
|
12
14
|
import { Validator } from './validator';
|
|
13
15
|
import { FormulaEngine } from './formula-engine';
|
|
14
16
|
import { QueryBuilder } from './query';
|
|
@@ -222,9 +224,11 @@ export class ObjectRepository {
|
|
|
222
224
|
|
|
223
225
|
// Evaluate each formula field
|
|
224
226
|
for (const [fieldName, fieldConfig] of Object.entries(schema.fields)) {
|
|
225
|
-
|
|
227
|
+
const formulaExpression = fieldConfig.expression;
|
|
228
|
+
|
|
229
|
+
if (fieldConfig.type === 'formula' && formulaExpression) {
|
|
226
230
|
const result = this.getFormulaEngine().evaluate(
|
|
227
|
-
|
|
231
|
+
formulaExpression,
|
|
228
232
|
formulaContext,
|
|
229
233
|
fieldConfig.data_type || 'text',
|
|
230
234
|
{ strict: true }
|
|
@@ -244,7 +248,7 @@ export class ObjectRepository {
|
|
|
244
248
|
objectName: this.objectName,
|
|
245
249
|
fieldName,
|
|
246
250
|
recordId: formulaContext.record_id,
|
|
247
|
-
|
|
251
|
+
expression: formulaExpression,
|
|
248
252
|
error: result.error,
|
|
249
253
|
stack: result.stack,
|
|
250
254
|
}
|
|
@@ -311,6 +315,22 @@ export class ObjectRepository {
|
|
|
311
315
|
}
|
|
312
316
|
|
|
313
317
|
async count(filters: any): Promise<number> {
|
|
318
|
+
// Normalize filters to UnifiedQuery format
|
|
319
|
+
// If filters is an array, wrap it in a query object
|
|
320
|
+
// If filters is already a UnifiedQuery (has UnifiedQuery-specific properties), use it as-is
|
|
321
|
+
let query: UnifiedQuery;
|
|
322
|
+
if (Array.isArray(filters)) {
|
|
323
|
+
query = { where: filters };
|
|
324
|
+
} else if (filters && typeof filters === 'object' && (filters.fields || filters.orderBy || filters.limit !== undefined || filters.offset !== undefined)) {
|
|
325
|
+
// It's already a UnifiedQuery object
|
|
326
|
+
query = filters;
|
|
327
|
+
} else if (filters) {
|
|
328
|
+
// It's a raw filter object, wrap it
|
|
329
|
+
query = { where: filters };
|
|
330
|
+
} else {
|
|
331
|
+
query = {};
|
|
332
|
+
}
|
|
333
|
+
|
|
314
334
|
const hookCtx: RetrievalHookContext = {
|
|
315
335
|
...this.context,
|
|
316
336
|
objectName: this.objectName,
|
|
@@ -318,7 +338,7 @@ export class ObjectRepository {
|
|
|
318
338
|
api: this.getHookAPI(),
|
|
319
339
|
user: this.getUserFromContext(),
|
|
320
340
|
state: {},
|
|
321
|
-
query
|
|
341
|
+
query
|
|
322
342
|
};
|
|
323
343
|
await this.app.triggerHook('beforeCount', this.objectName, hookCtx);
|
|
324
344
|
|
|
@@ -444,7 +464,7 @@ export class ObjectRepository {
|
|
|
444
464
|
async updateMany(filters: any, data: any): Promise<any> {
|
|
445
465
|
// Find all matching records and update them individually
|
|
446
466
|
// to ensure validation and hooks are executed
|
|
447
|
-
const records = await this.find({ filters });
|
|
467
|
+
const records = await this.find({ where: filters });
|
|
448
468
|
let count = 0;
|
|
449
469
|
for (const record of records) {
|
|
450
470
|
if (record && record._id) {
|
|
@@ -458,7 +478,7 @@ export class ObjectRepository {
|
|
|
458
478
|
async deleteMany(filters: any): Promise<any> {
|
|
459
479
|
// Find all matching records and delete them individually
|
|
460
480
|
// to ensure hooks are executed
|
|
461
|
-
const records = await this.find({ filters });
|
|
481
|
+
const records = await this.find({ where: filters });
|
|
462
482
|
let count = 0;
|
|
463
483
|
for (const record of records) {
|
|
464
484
|
if (record && record._id) {
|
package/src/validator-plugin.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { RuntimePlugin, RuntimeContext, ObjectStackKernel } from '@
|
|
9
|
+
import type { RuntimePlugin, RuntimeContext, ObjectStackKernel } from '@objectql/runtime';
|
|
10
10
|
import { Validator, ValidatorOptions } from './validator';
|
|
11
11
|
|
|
12
12
|
/**
|
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',
|
|
@@ -42,7 +42,7 @@ describe('Formula Integration', () => {
|
|
|
42
42
|
},
|
|
43
43
|
full_name: {
|
|
44
44
|
type: 'formula',
|
|
45
|
-
|
|
45
|
+
expression: 'first_name + " " + last_name',
|
|
46
46
|
data_type: 'text',
|
|
47
47
|
label: 'Full Name',
|
|
48
48
|
},
|
|
@@ -54,7 +54,7 @@ describe('Formula Integration', () => {
|
|
|
54
54
|
},
|
|
55
55
|
total: {
|
|
56
56
|
type: 'formula',
|
|
57
|
-
|
|
57
|
+
expression: 'quantity * unit_price',
|
|
58
58
|
data_type: 'currency',
|
|
59
59
|
label: 'Total',
|
|
60
60
|
},
|
|
@@ -63,7 +63,7 @@ describe('Formula Integration', () => {
|
|
|
63
63
|
},
|
|
64
64
|
status_label: {
|
|
65
65
|
type: 'formula',
|
|
66
|
-
|
|
66
|
+
expression: 'is_active ? "Active" : "Inactive"',
|
|
67
67
|
data_type: 'text',
|
|
68
68
|
label: 'Status',
|
|
69
69
|
},
|
|
@@ -165,7 +165,7 @@ describe('Formula Integration', () => {
|
|
|
165
165
|
tax_rate: { type: 'percent' },
|
|
166
166
|
final_price: {
|
|
167
167
|
type: 'formula',
|
|
168
|
-
|
|
168
|
+
expression: 'subtotal * (1 - discount_rate / 100) * (1 + tax_rate / 100)',
|
|
169
169
|
data_type: 'currency',
|
|
170
170
|
label: 'Final Price',
|
|
171
171
|
},
|
|
@@ -173,7 +173,7 @@ describe('Formula Integration', () => {
|
|
|
173
173
|
status: { type: 'select', options: ['draft', 'confirmed', 'shipped'] },
|
|
174
174
|
risk_level: {
|
|
175
175
|
type: 'formula',
|
|
176
|
-
|
|
176
|
+
expression: `
|
|
177
177
|
if (subtotal > 10000) {
|
|
178
178
|
return 'High';
|
|
179
179
|
} else if (subtotal > 1000) {
|
|
@@ -256,7 +256,7 @@ describe('Formula Integration', () => {
|
|
|
256
256
|
price: { type: 'currency' },
|
|
257
257
|
invalid_formula: {
|
|
258
258
|
type: 'formula',
|
|
259
|
-
|
|
259
|
+
expression: 'nonexistent_field * 2',
|
|
260
260
|
data_type: 'number',
|
|
261
261
|
},
|
|
262
262
|
},
|