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