@objectql/core 3.0.0 → 4.0.0
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 +32 -0
- package/IMPLEMENTATION_STATUS.md +364 -0
- package/README.md +31 -9
- package/RUNTIME_INTEGRATION.md +391 -0
- package/dist/ai-agent.d.ts +4 -3
- package/dist/ai-agent.js +10 -3
- package/dist/ai-agent.js.map +1 -1
- package/dist/app.d.ts +29 -6
- package/dist/app.js +117 -58
- package/dist/app.js.map +1 -1
- package/dist/formula-engine.d.ts +7 -0
- package/dist/formula-engine.js +9 -2
- package/dist/formula-engine.js.map +1 -1
- package/dist/formula-plugin.d.ts +52 -0
- package/dist/formula-plugin.js +107 -0
- package/dist/formula-plugin.js.map +1 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +89 -0
- package/dist/plugin.js +99 -0
- package/dist/plugin.js.map +1 -0
- package/dist/query/filter-translator.d.ts +37 -0
- package/dist/query/filter-translator.js +135 -0
- package/dist/query/filter-translator.js.map +1 -0
- package/dist/query/index.d.ts +22 -0
- package/dist/query/index.js +39 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/query-analyzer.d.ts +186 -0
- package/dist/query/query-analyzer.js +349 -0
- package/dist/query/query-analyzer.js.map +1 -0
- package/dist/query/query-builder.d.ts +27 -0
- package/dist/query/query-builder.js +71 -0
- package/dist/query/query-builder.js.map +1 -0
- package/dist/query/query-service.d.ts +150 -0
- package/dist/query/query-service.js +268 -0
- package/dist/query/query-service.js.map +1 -0
- package/dist/repository.d.ts +23 -2
- package/dist/repository.js +62 -13
- package/dist/repository.js.map +1 -1
- package/dist/util.d.ts +7 -0
- package/dist/util.js +18 -3
- package/dist/util.js.map +1 -1
- package/dist/validator-plugin.d.ts +56 -0
- package/dist/validator-plugin.js +106 -0
- package/dist/validator-plugin.js.map +1 -0
- package/dist/validator.d.ts +7 -0
- package/dist/validator.js +10 -8
- package/dist/validator.js.map +1 -1
- package/jest.config.js +16 -0
- package/package.json +8 -5
- package/src/ai-agent.ts +8 -0
- package/src/app.ts +136 -72
- package/src/formula-engine.ts +8 -0
- package/src/formula-plugin.ts +141 -0
- package/src/index.ts +25 -3
- package/src/plugin.ts +179 -0
- package/src/query/filter-translator.ts +147 -0
- package/src/query/index.ts +24 -0
- package/src/query/query-analyzer.ts +535 -0
- package/src/query/query-builder.ts +80 -0
- package/src/query/query-service.ts +392 -0
- package/src/repository.ts +81 -17
- package/src/util.ts +19 -3
- package/src/validator-plugin.ts +140 -0
- package/src/validator.ts +12 -5
- package/test/__mocks__/@objectstack/runtime.ts +255 -0
- package/test/app.test.ts +23 -35
- package/test/filter-syntax.test.ts +233 -0
- package/test/formula-engine.test.ts +8 -0
- package/test/formula-integration.test.ts +8 -0
- package/test/formula-plugin.test.ts +197 -0
- package/test/introspection.test.ts +8 -0
- package/test/mock-driver.ts +8 -0
- package/test/plugin-integration.test.ts +213 -0
- package/test/repository-validation.test.ts +8 -0
- package/test/repository.test.ts +8 -0
- package/test/util.test.ts +9 -1
- package/test/utils.ts +8 -0
- package/test/validator-plugin.test.ts +126 -0
- package/test/validator.test.ts +8 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/action.d.ts +0 -7
- package/dist/action.js +0 -23
- package/dist/action.js.map +0 -1
- package/dist/hook.d.ts +0 -8
- package/dist/hook.js +0 -25
- package/dist/hook.js.map +0 -1
- package/dist/object.d.ts +0 -3
- package/dist/object.js +0 -28
- package/dist/object.js.map +0 -1
- package/src/action.ts +0 -40
- package/src/hook.ts +0 -42
- package/src/object.ts +0 -26
- package/test/action.test.ts +0 -276
- package/test/hook.test.ts +0 -343
- package/test/object.test.ts +0 -183
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { UnifiedQuery } from '@objectql/types';
|
|
10
|
+
import type { QueryAST } from '@objectstack/spec';
|
|
11
|
+
import { FilterTranslator } from './filter-translator';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Query Builder
|
|
15
|
+
*
|
|
16
|
+
* Builds ObjectStack QueryAST from ObjectQL UnifiedQuery.
|
|
17
|
+
* This is the central query construction module for ObjectQL.
|
|
18
|
+
*/
|
|
19
|
+
export class QueryBuilder {
|
|
20
|
+
private filterTranslator: FilterTranslator;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
this.filterTranslator = new FilterTranslator();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a QueryAST from a UnifiedQuery
|
|
28
|
+
*
|
|
29
|
+
* @param objectName - Target object name
|
|
30
|
+
* @param query - ObjectQL UnifiedQuery
|
|
31
|
+
* @returns ObjectStack QueryAST
|
|
32
|
+
*/
|
|
33
|
+
build(objectName: string, query: UnifiedQuery): QueryAST {
|
|
34
|
+
const ast: QueryAST = {
|
|
35
|
+
object: objectName,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Map fields
|
|
39
|
+
if (query.fields) {
|
|
40
|
+
ast.fields = query.fields;
|
|
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
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return ast;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL Query Service
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
Driver,
|
|
11
|
+
ObjectConfig,
|
|
12
|
+
UnifiedQuery,
|
|
13
|
+
Filter,
|
|
14
|
+
MetadataRegistry
|
|
15
|
+
} from '@objectql/types';
|
|
16
|
+
import type { QueryAST } from '@objectstack/spec';
|
|
17
|
+
import { QueryBuilder } from './query-builder';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for query execution
|
|
21
|
+
*/
|
|
22
|
+
export interface QueryOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Transaction handle for transactional queries
|
|
25
|
+
*/
|
|
26
|
+
transaction?: any;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Skip validation (for system operations)
|
|
30
|
+
*/
|
|
31
|
+
skipValidation?: boolean;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Include profiling information
|
|
35
|
+
*/
|
|
36
|
+
profile?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Custom driver options
|
|
40
|
+
*/
|
|
41
|
+
driverOptions?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Result of a query execution with optional profiling data
|
|
46
|
+
*/
|
|
47
|
+
export interface QueryResult<T = any> {
|
|
48
|
+
/**
|
|
49
|
+
* The query results
|
|
50
|
+
*/
|
|
51
|
+
value: T;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Total count (for paginated queries)
|
|
55
|
+
*/
|
|
56
|
+
count?: number;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Profiling information (if profile option was enabled)
|
|
60
|
+
*/
|
|
61
|
+
profile?: QueryProfile;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Profiling information for a query
|
|
66
|
+
*/
|
|
67
|
+
export interface QueryProfile {
|
|
68
|
+
/**
|
|
69
|
+
* Execution time in milliseconds
|
|
70
|
+
*/
|
|
71
|
+
executionTime: number;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Number of rows scanned
|
|
75
|
+
*/
|
|
76
|
+
rowsScanned?: number;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether an index was used
|
|
80
|
+
*/
|
|
81
|
+
indexUsed?: boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The generated QueryAST
|
|
85
|
+
*/
|
|
86
|
+
ast?: QueryAST;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Query Service
|
|
91
|
+
*
|
|
92
|
+
* Handles all query execution logic, separating query concerns from
|
|
93
|
+
* the repository pattern. This service is responsible for:
|
|
94
|
+
* - Building QueryAST from UnifiedQuery
|
|
95
|
+
* - Executing queries via drivers
|
|
96
|
+
* - Optional query profiling and analysis
|
|
97
|
+
*
|
|
98
|
+
* The QueryService is registered as a service in the ObjectQLPlugin
|
|
99
|
+
* and can be used by Repository for all read operations.
|
|
100
|
+
*/
|
|
101
|
+
export class QueryService {
|
|
102
|
+
private queryBuilder: QueryBuilder;
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
private datasources: Record<string, Driver>,
|
|
106
|
+
private metadata: MetadataRegistry
|
|
107
|
+
) {
|
|
108
|
+
this.queryBuilder = new QueryBuilder();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the driver for a specific object
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
115
|
+
private getDriver(objectName: string): Driver {
|
|
116
|
+
const obj = this.getSchema(objectName);
|
|
117
|
+
const datasourceName = obj.datasource || 'default';
|
|
118
|
+
const driver = this.datasources[datasourceName];
|
|
119
|
+
|
|
120
|
+
if (!driver) {
|
|
121
|
+
throw new Error(`Datasource '${datasourceName}' not found for object '${objectName}'`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return driver;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get the schema for an object
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
private getSchema(objectName: string): ObjectConfig {
|
|
132
|
+
const obj = this.metadata.get<ObjectConfig>('object', objectName);
|
|
133
|
+
if (!obj) {
|
|
134
|
+
throw new Error(`Object '${objectName}' not found in metadata`);
|
|
135
|
+
}
|
|
136
|
+
return obj;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build QueryAST from UnifiedQuery
|
|
141
|
+
* @private
|
|
142
|
+
*/
|
|
143
|
+
private buildQueryAST(objectName: string, query: UnifiedQuery): QueryAST {
|
|
144
|
+
return this.queryBuilder.build(objectName, query);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Execute a find query
|
|
149
|
+
*
|
|
150
|
+
* @param objectName - The object to query
|
|
151
|
+
* @param query - The unified query
|
|
152
|
+
* @param options - Query execution options
|
|
153
|
+
* @returns Array of matching records
|
|
154
|
+
*/
|
|
155
|
+
async find(
|
|
156
|
+
objectName: string,
|
|
157
|
+
query: UnifiedQuery = {},
|
|
158
|
+
options: QueryOptions = {}
|
|
159
|
+
): Promise<QueryResult<any[]>> {
|
|
160
|
+
const driver = this.getDriver(objectName);
|
|
161
|
+
const startTime = options.profile ? Date.now() : 0;
|
|
162
|
+
|
|
163
|
+
// Build QueryAST
|
|
164
|
+
const ast = this.buildQueryAST(objectName, query);
|
|
165
|
+
|
|
166
|
+
// Execute query via driver
|
|
167
|
+
const driverOptions = {
|
|
168
|
+
transaction: options.transaction,
|
|
169
|
+
...options.driverOptions
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
let results: any[];
|
|
173
|
+
let count: number | undefined;
|
|
174
|
+
|
|
175
|
+
if (driver.find) {
|
|
176
|
+
// Legacy driver interface
|
|
177
|
+
const result: any = await driver.find(objectName, query, driverOptions);
|
|
178
|
+
results = Array.isArray(result) ? result : (result?.value || []);
|
|
179
|
+
count = (typeof result === 'object' && !Array.isArray(result) && result?.count !== undefined) ? result.count : undefined;
|
|
180
|
+
} else if (driver.executeQuery) {
|
|
181
|
+
// New DriverInterface
|
|
182
|
+
const result = await driver.executeQuery(ast, driverOptions);
|
|
183
|
+
results = result.value || [];
|
|
184
|
+
count = result.count;
|
|
185
|
+
} else {
|
|
186
|
+
throw new Error(`Driver does not support query execution`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const executionTime = options.profile ? Date.now() - startTime : 0;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
value: results,
|
|
193
|
+
count,
|
|
194
|
+
profile: options.profile ? {
|
|
195
|
+
executionTime,
|
|
196
|
+
ast,
|
|
197
|
+
rowsScanned: results.length,
|
|
198
|
+
} : undefined
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Execute a findOne query by ID
|
|
204
|
+
*
|
|
205
|
+
* @param objectName - The object to query
|
|
206
|
+
* @param id - The record ID
|
|
207
|
+
* @param options - Query execution options
|
|
208
|
+
* @returns The matching record or undefined
|
|
209
|
+
*/
|
|
210
|
+
async findOne(
|
|
211
|
+
objectName: string,
|
|
212
|
+
id: string | number,
|
|
213
|
+
options: QueryOptions = {}
|
|
214
|
+
): Promise<QueryResult<any>> {
|
|
215
|
+
const driver = this.getDriver(objectName);
|
|
216
|
+
const startTime = options.profile ? Date.now() : 0;
|
|
217
|
+
|
|
218
|
+
const driverOptions = {
|
|
219
|
+
transaction: options.transaction,
|
|
220
|
+
...options.driverOptions
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
let result: any;
|
|
224
|
+
|
|
225
|
+
if (driver.findOne) {
|
|
226
|
+
// Legacy driver interface
|
|
227
|
+
result = await driver.findOne(objectName, id, driverOptions);
|
|
228
|
+
} else if (driver.get) {
|
|
229
|
+
// Alternative method name
|
|
230
|
+
result = await driver.get(objectName, String(id), driverOptions);
|
|
231
|
+
} else if (driver.executeQuery) {
|
|
232
|
+
// Fallback to query with ID filter
|
|
233
|
+
const query: UnifiedQuery = {
|
|
234
|
+
filters: [['_id', '=', id]]
|
|
235
|
+
};
|
|
236
|
+
const ast = this.buildQueryAST(objectName, query);
|
|
237
|
+
const queryResult = await driver.executeQuery(ast, driverOptions);
|
|
238
|
+
result = queryResult.value?.[0];
|
|
239
|
+
} else {
|
|
240
|
+
throw new Error(`Driver does not support findOne operation`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const executionTime = options.profile ? Date.now() - startTime : 0;
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
value: result,
|
|
247
|
+
profile: options.profile ? {
|
|
248
|
+
executionTime,
|
|
249
|
+
rowsScanned: result ? 1 : 0,
|
|
250
|
+
} : undefined
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Execute a count query
|
|
256
|
+
*
|
|
257
|
+
* @param objectName - The object to query
|
|
258
|
+
* @param filters - Optional filters
|
|
259
|
+
* @param options - Query execution options
|
|
260
|
+
* @returns Count of matching records
|
|
261
|
+
*/
|
|
262
|
+
async count(
|
|
263
|
+
objectName: string,
|
|
264
|
+
filters?: Filter[],
|
|
265
|
+
options: QueryOptions = {}
|
|
266
|
+
): Promise<QueryResult<number>> {
|
|
267
|
+
const driver = this.getDriver(objectName);
|
|
268
|
+
const startTime = options.profile ? Date.now() : 0;
|
|
269
|
+
|
|
270
|
+
const query: UnifiedQuery = filters ? { filters } : {};
|
|
271
|
+
const ast = this.buildQueryAST(objectName, query);
|
|
272
|
+
|
|
273
|
+
const driverOptions = {
|
|
274
|
+
transaction: options.transaction,
|
|
275
|
+
...options.driverOptions
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
let count: number;
|
|
279
|
+
|
|
280
|
+
if (driver.count) {
|
|
281
|
+
// Legacy driver interface
|
|
282
|
+
count = await driver.count(objectName, filters || [], driverOptions);
|
|
283
|
+
} else if (driver.executeQuery) {
|
|
284
|
+
// Use executeQuery and count results
|
|
285
|
+
// Note: This is inefficient for large datasets
|
|
286
|
+
// Ideally, driver should support count-specific optimization
|
|
287
|
+
const result = await driver.executeQuery(ast, driverOptions);
|
|
288
|
+
count = result.count ?? result.value?.length ?? 0;
|
|
289
|
+
} else {
|
|
290
|
+
throw new Error(`Driver does not support count operation`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const executionTime = options.profile ? Date.now() - startTime : 0;
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
value: count,
|
|
297
|
+
profile: options.profile ? {
|
|
298
|
+
executionTime,
|
|
299
|
+
ast,
|
|
300
|
+
} : undefined
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Execute an aggregate query
|
|
306
|
+
*
|
|
307
|
+
* @param objectName - The object to query
|
|
308
|
+
* @param query - The aggregation query using UnifiedQuery format
|
|
309
|
+
* @param options - Query execution options
|
|
310
|
+
* @returns Aggregation results
|
|
311
|
+
*/
|
|
312
|
+
async aggregate(
|
|
313
|
+
objectName: string,
|
|
314
|
+
query: UnifiedQuery,
|
|
315
|
+
options: QueryOptions = {}
|
|
316
|
+
): Promise<QueryResult<any[]>> {
|
|
317
|
+
const driver = this.getDriver(objectName);
|
|
318
|
+
const startTime = options.profile ? Date.now() : 0;
|
|
319
|
+
|
|
320
|
+
const driverOptions = {
|
|
321
|
+
transaction: options.transaction,
|
|
322
|
+
...options.driverOptions
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
let results: any[];
|
|
326
|
+
|
|
327
|
+
if (driver.aggregate) {
|
|
328
|
+
// Driver supports aggregation
|
|
329
|
+
results = await driver.aggregate(objectName, query, driverOptions);
|
|
330
|
+
} else {
|
|
331
|
+
// Driver doesn't support aggregation
|
|
332
|
+
throw new Error(`Driver does not support aggregate operations. Consider using a driver that supports aggregation.`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const executionTime = options.profile ? Date.now() - startTime : 0;
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
value: results,
|
|
339
|
+
profile: options.profile ? {
|
|
340
|
+
executionTime,
|
|
341
|
+
rowsScanned: results.length,
|
|
342
|
+
} : undefined
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Execute a direct SQL/query passthrough
|
|
348
|
+
*
|
|
349
|
+
* This bypasses ObjectQL's query builder and executes raw queries.
|
|
350
|
+
* Use with caution as it bypasses security and validation.
|
|
351
|
+
*
|
|
352
|
+
* @param objectName - The object (determines which datasource to use)
|
|
353
|
+
* @param queryString - Raw query string (SQL, MongoDB query, etc.)
|
|
354
|
+
* @param params - Query parameters (for parameterized queries)
|
|
355
|
+
* @param options - Query execution options
|
|
356
|
+
* @returns Query results
|
|
357
|
+
*/
|
|
358
|
+
async directQuery(
|
|
359
|
+
objectName: string,
|
|
360
|
+
queryString: string,
|
|
361
|
+
params?: any[],
|
|
362
|
+
options: QueryOptions = {}
|
|
363
|
+
): Promise<QueryResult<any>> {
|
|
364
|
+
const driver = this.getDriver(objectName);
|
|
365
|
+
const startTime = options.profile ? Date.now() : 0;
|
|
366
|
+
|
|
367
|
+
const driverOptions = {
|
|
368
|
+
transaction: options.transaction,
|
|
369
|
+
...options.driverOptions
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
let results: any;
|
|
373
|
+
|
|
374
|
+
if (driver.directQuery) {
|
|
375
|
+
results = await driver.directQuery(queryString, params);
|
|
376
|
+
} else if (driver.query) {
|
|
377
|
+
// Alternative method name
|
|
378
|
+
results = await driver.query(queryString, params);
|
|
379
|
+
} else {
|
|
380
|
+
throw new Error(`Driver does not support direct query execution`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const executionTime = options.profile ? Date.now() - startTime : 0;
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
value: results,
|
|
387
|
+
profile: options.profile ? {
|
|
388
|
+
executionTime,
|
|
389
|
+
} : undefined
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
package/src/repository.ts
CHANGED
|
@@ -1,18 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ObjectQLContext, IObjectQL, ObjectConfig, Driver, UnifiedQuery, ActionContext, HookAPI, RetrievalHookContext, MutationHookContext, UpdateHookContext, ValidationContext, ValidationError, ValidationRuleResult, FormulaContext, Filter } from '@objectql/types';
|
|
10
|
+
import type { ObjectStackKernel } from '@objectstack/runtime';
|
|
11
|
+
import type { QueryAST, FilterNode, SortNode } from '@objectstack/spec';
|
|
2
12
|
import { Validator } from './validator';
|
|
3
13
|
import { FormulaEngine } from './formula-engine';
|
|
14
|
+
import { QueryBuilder } from './query';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extended ObjectStack Kernel with optional ObjectQL plugin capabilities.
|
|
18
|
+
* These properties are attached by ValidatorPlugin and FormulaPlugin during installation.
|
|
19
|
+
*/
|
|
20
|
+
interface ExtendedKernel extends ObjectStackKernel {
|
|
21
|
+
validator?: Validator;
|
|
22
|
+
formulaEngine?: FormulaEngine;
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
export class ObjectRepository {
|
|
6
|
-
private
|
|
7
|
-
private formulaEngine: FormulaEngine;
|
|
26
|
+
private queryBuilder: QueryBuilder;
|
|
8
27
|
|
|
9
28
|
constructor(
|
|
10
29
|
private objectName: string,
|
|
11
30
|
private context: ObjectQLContext,
|
|
12
31
|
private app: IObjectQL
|
|
13
32
|
) {
|
|
14
|
-
this.
|
|
15
|
-
|
|
33
|
+
this.queryBuilder = new QueryBuilder();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get validator instance from kernel (via plugin)
|
|
38
|
+
* Falls back to creating a new instance if not available
|
|
39
|
+
*/
|
|
40
|
+
private getValidator(): Validator {
|
|
41
|
+
const kernel = this.getKernel() as ExtendedKernel;
|
|
42
|
+
if (kernel.validator) {
|
|
43
|
+
return kernel.validator;
|
|
44
|
+
}
|
|
45
|
+
// Fallback for backward compatibility
|
|
46
|
+
return new Validator();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get formula engine instance from kernel (via plugin)
|
|
51
|
+
* Falls back to creating a new instance if not available
|
|
52
|
+
*/
|
|
53
|
+
private getFormulaEngine(): FormulaEngine {
|
|
54
|
+
const kernel = this.getKernel() as ExtendedKernel;
|
|
55
|
+
if (kernel.formulaEngine) {
|
|
56
|
+
return kernel.formulaEngine;
|
|
57
|
+
}
|
|
58
|
+
// Fallback for backward compatibility
|
|
59
|
+
return new FormulaEngine();
|
|
16
60
|
}
|
|
17
61
|
|
|
18
62
|
private getDriver(): Driver {
|
|
@@ -21,13 +65,24 @@ export class ObjectRepository {
|
|
|
21
65
|
return this.app.datasource(datasourceName);
|
|
22
66
|
}
|
|
23
67
|
|
|
24
|
-
private
|
|
68
|
+
private getKernel(): ObjectStackKernel {
|
|
69
|
+
return this.app.getKernel();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private getOptions(extra: Record<string, unknown> = {}) {
|
|
25
73
|
return {
|
|
26
74
|
transaction: this.context.transactionHandle,
|
|
27
75
|
...extra
|
|
28
76
|
};
|
|
29
77
|
}
|
|
30
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Translates ObjectQL UnifiedQuery to ObjectStack QueryAST format
|
|
81
|
+
*/
|
|
82
|
+
private buildQueryAST(query: UnifiedQuery): QueryAST {
|
|
83
|
+
return this.queryBuilder.build(this.objectName, query);
|
|
84
|
+
}
|
|
85
|
+
|
|
31
86
|
getSchema(): ObjectConfig {
|
|
32
87
|
const obj = this.app.getObject(this.objectName);
|
|
33
88
|
if (!obj) {
|
|
@@ -82,7 +137,7 @@ export class ObjectRepository {
|
|
|
82
137
|
}
|
|
83
138
|
|
|
84
139
|
const value = record[fieldName];
|
|
85
|
-
const fieldResults = await this.
|
|
140
|
+
const fieldResults = await this.getValidator().validateField(
|
|
86
141
|
fieldName,
|
|
87
142
|
fieldConfig,
|
|
88
143
|
value,
|
|
@@ -121,7 +176,7 @@ export class ObjectRepository {
|
|
|
121
176
|
changedFields,
|
|
122
177
|
};
|
|
123
178
|
|
|
124
|
-
const result = await this.
|
|
179
|
+
const result = await this.getValidator().validate(schema.validation.rules, validationContext);
|
|
125
180
|
allResults.push(...result.results);
|
|
126
181
|
}
|
|
127
182
|
|
|
@@ -168,7 +223,7 @@ export class ObjectRepository {
|
|
|
168
223
|
// Evaluate each formula field
|
|
169
224
|
for (const [fieldName, fieldConfig] of Object.entries(schema.fields)) {
|
|
170
225
|
if (fieldConfig.type === 'formula' && fieldConfig.formula) {
|
|
171
|
-
const result = this.
|
|
226
|
+
const result = this.getFormulaEngine().evaluate(
|
|
172
227
|
fieldConfig.formula,
|
|
173
228
|
formulaContext,
|
|
174
229
|
fieldConfig.data_type || 'text',
|
|
@@ -213,11 +268,13 @@ export class ObjectRepository {
|
|
|
213
268
|
};
|
|
214
269
|
await this.app.triggerHook('beforeFind', this.objectName, hookCtx);
|
|
215
270
|
|
|
216
|
-
//
|
|
217
|
-
const
|
|
271
|
+
// Build QueryAST and execute via kernel
|
|
272
|
+
const ast = this.buildQueryAST(hookCtx.query || {});
|
|
273
|
+
const kernelResult = await this.getKernel().find(this.objectName, ast);
|
|
274
|
+
const results = kernelResult.value;
|
|
218
275
|
|
|
219
276
|
// Evaluate formulas for each result
|
|
220
|
-
const resultsWithFormulas = results.map(record => this.evaluateFormulas(record));
|
|
277
|
+
const resultsWithFormulas = results.map((record: any) => this.evaluateFormulas(record));
|
|
221
278
|
|
|
222
279
|
hookCtx.result = resultsWithFormulas;
|
|
223
280
|
await this.app.triggerHook('afterFind', this.objectName, hookCtx);
|
|
@@ -238,7 +295,8 @@ export class ObjectRepository {
|
|
|
238
295
|
};
|
|
239
296
|
await this.app.triggerHook('beforeFind', this.objectName, hookCtx);
|
|
240
297
|
|
|
241
|
-
|
|
298
|
+
// Use kernel.get() for direct ID lookup
|
|
299
|
+
const result = await this.getKernel().get(this.objectName, String(idOrQuery));
|
|
242
300
|
|
|
243
301
|
// Evaluate formulas if result exists
|
|
244
302
|
const resultWithFormulas = result ? this.evaluateFormulas(result) : result;
|
|
@@ -264,7 +322,10 @@ export class ObjectRepository {
|
|
|
264
322
|
};
|
|
265
323
|
await this.app.triggerHook('beforeCount', this.objectName, hookCtx);
|
|
266
324
|
|
|
267
|
-
|
|
325
|
+
// Build QueryAST and execute via kernel to get count
|
|
326
|
+
const ast = this.buildQueryAST(hookCtx.query || {});
|
|
327
|
+
const kernelResult = await this.getKernel().find(this.objectName, ast);
|
|
328
|
+
const result = kernelResult.count;
|
|
268
329
|
|
|
269
330
|
hookCtx.result = result;
|
|
270
331
|
await this.app.triggerHook('afterCount', this.objectName, hookCtx);
|
|
@@ -290,7 +351,8 @@ export class ObjectRepository {
|
|
|
290
351
|
// Validate the record before creating
|
|
291
352
|
await this.validateRecord('create', finalDoc);
|
|
292
353
|
|
|
293
|
-
|
|
354
|
+
// Execute via kernel
|
|
355
|
+
const result = await this.getKernel().create(this.objectName, finalDoc);
|
|
294
356
|
|
|
295
357
|
hookCtx.result = result;
|
|
296
358
|
await this.app.triggerHook('afterCreate', this.objectName, hookCtx);
|
|
@@ -316,7 +378,8 @@ export class ObjectRepository {
|
|
|
316
378
|
// Validate the update
|
|
317
379
|
await this.validateRecord('update', hookCtx.data, previousData);
|
|
318
380
|
|
|
319
|
-
|
|
381
|
+
// Execute via kernel
|
|
382
|
+
const result = await this.getKernel().update(this.objectName, String(id), hookCtx.data);
|
|
320
383
|
|
|
321
384
|
hookCtx.result = result;
|
|
322
385
|
await this.app.triggerHook('afterUpdate', this.objectName, hookCtx);
|
|
@@ -337,7 +400,8 @@ export class ObjectRepository {
|
|
|
337
400
|
};
|
|
338
401
|
await this.app.triggerHook('beforeDelete', this.objectName, hookCtx);
|
|
339
402
|
|
|
340
|
-
|
|
403
|
+
// Execute via kernel
|
|
404
|
+
const result = await this.getKernel().delete(this.objectName, String(id));
|
|
341
405
|
|
|
342
406
|
hookCtx.result = result;
|
|
343
407
|
await this.app.triggerHook('afterDelete', this.objectName, hookCtx);
|