@objectstack/objectql 0.6.1 → 0.7.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 +20 -0
- package/dist/engine.d.ts +137 -47
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +238 -155
- package/dist/plugin.js +5 -10
- package/dist/protocol.d.ts +103 -33
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +224 -46
- package/package.json +5 -5
- package/src/engine.ts +256 -176
- package/src/plugin.ts +5 -5
- package/src/protocol.ts +255 -49
package/src/engine.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { QueryAST, HookContext } from '@objectstack/spec/data';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
DataEngineQueryOptions,
|
|
4
|
+
DataEngineInsertOptions,
|
|
5
|
+
DataEngineUpdateOptions,
|
|
6
|
+
DataEngineDeleteOptions,
|
|
7
|
+
DataEngineAggregateOptions,
|
|
8
|
+
DataEngineCountOptions
|
|
9
|
+
} from '@objectstack/spec/data';
|
|
10
|
+
import { DriverInterface, IDataEngine, Logger, createLogger } from '@objectstack/core';
|
|
5
11
|
import { SchemaRegistry } from './registry';
|
|
6
12
|
|
|
7
13
|
export type HookHandler = (context: HookContext) => Promise<void> | void;
|
|
@@ -11,7 +17,7 @@ export type HookHandler = (context: HookContext) => Promise<void> | void;
|
|
|
11
17
|
*/
|
|
12
18
|
export interface ObjectQLHostContext {
|
|
13
19
|
ql: ObjectQL;
|
|
14
|
-
logger:
|
|
20
|
+
logger: Logger;
|
|
15
21
|
// Extensible map for host-specific globals (like HTTP Router, etc.)
|
|
16
22
|
[key: string]: any;
|
|
17
23
|
}
|
|
@@ -24,6 +30,7 @@ export interface ObjectQLHostContext {
|
|
|
24
30
|
export class ObjectQL implements IDataEngine {
|
|
25
31
|
private drivers = new Map<string, DriverInterface>();
|
|
26
32
|
private defaultDriver: string | null = null;
|
|
33
|
+
private logger: Logger;
|
|
27
34
|
|
|
28
35
|
// Hooks Registry
|
|
29
36
|
private hooks: Record<string, HookHandler[]> = {
|
|
@@ -38,13 +45,20 @@ export class ObjectQL implements IDataEngine {
|
|
|
38
45
|
|
|
39
46
|
constructor(hostContext: Record<string, any> = {}) {
|
|
40
47
|
this.hostContext = hostContext;
|
|
41
|
-
|
|
48
|
+
// Use provided logger or create a new one
|
|
49
|
+
this.logger = hostContext.logger || createLogger({ level: 'info', format: 'pretty' });
|
|
50
|
+
this.logger.info('ObjectQL Engine Instance Created');
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
/**
|
|
45
54
|
* Load and Register a Plugin
|
|
46
55
|
*/
|
|
47
56
|
async use(manifestPart: any, runtimePart?: any) {
|
|
57
|
+
this.logger.debug('Loading plugin', {
|
|
58
|
+
hasManifest: !!manifestPart,
|
|
59
|
+
hasRuntime: !!runtimePart
|
|
60
|
+
});
|
|
61
|
+
|
|
48
62
|
// 1. Validate / Register Manifest
|
|
49
63
|
if (manifestPart) {
|
|
50
64
|
this.registerApp(manifestPart);
|
|
@@ -54,9 +68,11 @@ export class ObjectQL implements IDataEngine {
|
|
|
54
68
|
if (runtimePart) {
|
|
55
69
|
const pluginDef = (runtimePart as any).default || runtimePart;
|
|
56
70
|
if (pluginDef.onEnable) {
|
|
71
|
+
this.logger.debug('Executing plugin runtime onEnable');
|
|
72
|
+
|
|
57
73
|
const context: ObjectQLHostContext = {
|
|
58
74
|
ql: this,
|
|
59
|
-
logger:
|
|
75
|
+
logger: this.logger,
|
|
60
76
|
// Expose the driver registry helper explicitly if needed
|
|
61
77
|
drivers: {
|
|
62
78
|
register: (driver: DriverInterface) => this.registerDriver(driver)
|
|
@@ -65,6 +81,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
65
81
|
};
|
|
66
82
|
|
|
67
83
|
await pluginDef.onEnable(context);
|
|
84
|
+
this.logger.debug('Plugin runtime onEnable completed');
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
87
|
}
|
|
@@ -79,51 +96,56 @@ export class ObjectQL implements IDataEngine {
|
|
|
79
96
|
this.hooks[event] = [];
|
|
80
97
|
}
|
|
81
98
|
this.hooks[event].push(handler);
|
|
82
|
-
|
|
99
|
+
this.logger.debug('Registered hook', { event, totalHandlers: this.hooks[event].length });
|
|
83
100
|
}
|
|
84
101
|
|
|
85
102
|
public async triggerHooks(event: string, context: HookContext) {
|
|
86
103
|
const handlers = this.hooks[event] || [];
|
|
104
|
+
|
|
105
|
+
if (handlers.length === 0) {
|
|
106
|
+
this.logger.debug('No hooks registered for event', { event });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.logger.debug('Triggering hooks', { event, count: handlers.length });
|
|
111
|
+
|
|
87
112
|
for (const handler of handlers) {
|
|
88
|
-
// In a real system, we might want to catch errors here or allow them to bubble up
|
|
89
113
|
await handler(context);
|
|
90
114
|
}
|
|
91
115
|
}
|
|
92
116
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// We merge the inner manifest metadata (id, version, etc) with the outer container (objects, apps)
|
|
100
|
-
const manifest = raw.manifest ? { ...raw, ...raw.manifest } : raw;
|
|
101
|
-
|
|
102
|
-
// In a real scenario, we might strictly parse this using Zod
|
|
103
|
-
// For now, simple ID check
|
|
104
|
-
const id = manifest.id || manifest.name;
|
|
105
|
-
if (!id) {
|
|
106
|
-
console.warn(`[ObjectQL] Plugin manifest missing ID (keys: ${Object.keys(manifest)})`, manifest);
|
|
107
|
-
// Don't return, try to proceed if it looks like an App (Apps might use 'name' instead of 'id')
|
|
108
|
-
// return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
console.log(`[ObjectQL] Loading App: ${id}`);
|
|
112
|
-
SchemaRegistry.registerPlugin(manifest as ObjectStackManifest);
|
|
117
|
+
/**
|
|
118
|
+
* Register contribution (Manifest)
|
|
119
|
+
*/
|
|
120
|
+
registerApp(manifest: any) {
|
|
121
|
+
const id = manifest.id;
|
|
122
|
+
this.logger.debug('Registering app manifest', { id });
|
|
113
123
|
|
|
114
|
-
// Register
|
|
124
|
+
// Register objects
|
|
115
125
|
if (manifest.objects) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
if (Array.isArray(manifest.objects)) {
|
|
127
|
+
this.logger.debug('Registering objects from manifest (Array)', { id, objectCount: manifest.objects.length });
|
|
128
|
+
for (const objDef of manifest.objects) {
|
|
129
|
+
SchemaRegistry.registerObject(objDef);
|
|
130
|
+
this.logger.debug('Registered Object', { object: objDef.name, from: id });
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
this.logger.debug('Registering objects from manifest (Map)', { id, objectCount: Object.keys(manifest.objects).length });
|
|
134
|
+
for (const [name, objDef] of Object.entries(manifest.objects)) {
|
|
135
|
+
// Ensure name in definition matches key
|
|
136
|
+
(objDef as any).name = name;
|
|
137
|
+
SchemaRegistry.registerObject(objDef as any);
|
|
138
|
+
this.logger.debug('Registered Object', { object: name, from: id });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
// Register contributions
|
|
124
144
|
if (manifest.contributes?.kinds) {
|
|
145
|
+
this.logger.debug('Registering kinds from manifest', { id, kindCount: manifest.contributes.kinds.length });
|
|
125
146
|
for (const kind of manifest.contributes.kinds) {
|
|
126
147
|
SchemaRegistry.registerKind(kind);
|
|
148
|
+
this.logger.debug('Registered Kind', { kind: kind.name || kind.type, from: id });
|
|
127
149
|
}
|
|
128
150
|
}
|
|
129
151
|
}
|
|
@@ -133,15 +155,19 @@ export class ObjectQL implements IDataEngine {
|
|
|
133
155
|
*/
|
|
134
156
|
registerDriver(driver: DriverInterface, isDefault: boolean = false) {
|
|
135
157
|
if (this.drivers.has(driver.name)) {
|
|
136
|
-
|
|
158
|
+
this.logger.warn('Driver already registered, skipping', { driverName: driver.name });
|
|
137
159
|
return;
|
|
138
160
|
}
|
|
139
161
|
|
|
140
162
|
this.drivers.set(driver.name, driver);
|
|
141
|
-
|
|
163
|
+
this.logger.info('Registered driver', {
|
|
164
|
+
driverName: driver.name,
|
|
165
|
+
version: driver.version
|
|
166
|
+
});
|
|
142
167
|
|
|
143
168
|
if (isDefault || this.drivers.size === 1) {
|
|
144
169
|
this.defaultDriver = driver.name;
|
|
170
|
+
this.logger.info('Set default driver', { driverName: driver.name });
|
|
145
171
|
}
|
|
146
172
|
}
|
|
147
173
|
|
|
@@ -167,25 +193,17 @@ export class ObjectQL implements IDataEngine {
|
|
|
167
193
|
if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
|
|
168
194
|
return this.drivers.get(this.defaultDriver)!;
|
|
169
195
|
}
|
|
170
|
-
// Fallback: If 'default' not explicitly set, use the first available driver?
|
|
171
|
-
// Better to be strict.
|
|
172
196
|
} else {
|
|
173
197
|
// Specific datasource requested
|
|
174
198
|
if (this.drivers.has(datasourceName)) {
|
|
175
199
|
return this.drivers.get(datasourceName)!;
|
|
176
200
|
}
|
|
177
|
-
// If not found, fall back to default? Or error?
|
|
178
|
-
// Standard behavior: Error if specific datasource is missing.
|
|
179
201
|
throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
|
|
180
202
|
}
|
|
181
203
|
}
|
|
182
204
|
|
|
183
205
|
// 2. Fallback for ad-hoc objects or missing definitions
|
|
184
|
-
// If we have a default driver, use it.
|
|
185
206
|
if (this.defaultDriver) {
|
|
186
|
-
if (!object) {
|
|
187
|
-
console.warn(`[ObjectQL] Object '${objectName}' not found in registry. Using default driver.`);
|
|
188
|
-
}
|
|
189
207
|
return this.drivers.get(this.defaultDriver)!;
|
|
190
208
|
}
|
|
191
209
|
|
|
@@ -196,75 +214,87 @@ export class ObjectQL implements IDataEngine {
|
|
|
196
214
|
* Initialize the engine and all registered drivers
|
|
197
215
|
*/
|
|
198
216
|
async init() {
|
|
199
|
-
|
|
217
|
+
this.logger.info('Initializing ObjectQL engine', {
|
|
218
|
+
driverCount: this.drivers.size,
|
|
219
|
+
drivers: Array.from(this.drivers.keys())
|
|
220
|
+
});
|
|
221
|
+
|
|
200
222
|
for (const [name, driver] of this.drivers) {
|
|
201
223
|
try {
|
|
202
224
|
await driver.connect();
|
|
225
|
+
this.logger.info('Driver connected successfully', { driverName: name });
|
|
203
226
|
} catch (e) {
|
|
204
|
-
|
|
227
|
+
this.logger.error('Failed to connect driver', e as Error, { driverName: name });
|
|
205
228
|
}
|
|
206
229
|
}
|
|
207
|
-
|
|
230
|
+
|
|
231
|
+
this.logger.info('ObjectQL engine initialization complete');
|
|
208
232
|
}
|
|
209
233
|
|
|
210
234
|
async destroy() {
|
|
211
|
-
|
|
212
|
-
|
|
235
|
+
this.logger.info('Destroying ObjectQL engine', { driverCount: this.drivers.size });
|
|
236
|
+
|
|
237
|
+
for (const [name, driver] of this.drivers.entries()) {
|
|
238
|
+
try {
|
|
239
|
+
await driver.disconnect();
|
|
240
|
+
} catch (e) {
|
|
241
|
+
this.logger.error('Error disconnecting driver', e as Error, { driverName: name });
|
|
242
|
+
}
|
|
213
243
|
}
|
|
244
|
+
|
|
245
|
+
this.logger.info('ObjectQL engine destroyed');
|
|
214
246
|
}
|
|
215
247
|
|
|
216
248
|
// ============================================
|
|
217
|
-
//
|
|
249
|
+
// Helper: Query Conversion
|
|
218
250
|
// ============================================
|
|
219
251
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
252
|
+
private toQueryAST(object: string, options?: DataEngineQueryOptions): QueryAST {
|
|
253
|
+
const ast: QueryAST = { object };
|
|
254
|
+
if (!options) return ast;
|
|
255
|
+
|
|
256
|
+
if (options.filter) {
|
|
257
|
+
ast.where = options.filter;
|
|
258
|
+
}
|
|
259
|
+
if (options.select) {
|
|
260
|
+
ast.fields = options.select;
|
|
261
|
+
}
|
|
262
|
+
if (options.sort) {
|
|
263
|
+
// Support DataEngineSortSchema variant
|
|
264
|
+
if (Array.isArray(options.sort)) {
|
|
265
|
+
// [{ field: 'a', order: 'asc' }]
|
|
266
|
+
ast.orderBy = options.sort;
|
|
267
|
+
} else {
|
|
268
|
+
// Record<string, 'asc' | 'desc' | 1 | -1>
|
|
269
|
+
ast.orderBy = Object.entries(options.sort).map(([field, order]) => ({
|
|
270
|
+
field,
|
|
271
|
+
order: (order === -1 || order === 'desc') ? 'desc' : 'asc'
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
229
275
|
|
|
230
|
-
|
|
231
|
-
|
|
276
|
+
if (options.top !== undefined) ast.limit = options.top;
|
|
277
|
+
else if (options.limit !== undefined) ast.limit = options.limit;
|
|
232
278
|
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Convert sort Record to orderBy array
|
|
243
|
-
// sort: { createdAt: -1, name: 'asc' } => orderBy: [{ field: 'createdAt', order: 'desc' }, { field: 'name', order: 'asc' }]
|
|
244
|
-
ast.orderBy = Object.entries(query.sort).map(([field, order]) => ({
|
|
245
|
-
field,
|
|
246
|
-
order: (order === -1 || order === 'desc') ? 'desc' : 'asc'
|
|
247
|
-
}));
|
|
248
|
-
}
|
|
249
|
-
// Handle both limit and top (top takes precedence)
|
|
250
|
-
if (query.top !== undefined) {
|
|
251
|
-
ast.limit = query.top;
|
|
252
|
-
} else if (query.limit !== undefined) {
|
|
253
|
-
ast.limit = query.limit;
|
|
254
|
-
}
|
|
255
|
-
if (query.skip !== undefined) {
|
|
256
|
-
ast.offset = query.skip;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
279
|
+
if (options.skip !== undefined) ast.offset = options.skip;
|
|
280
|
+
|
|
281
|
+
// TODO: Handle populate/joins mapping if Driver supports it in QueryAST
|
|
282
|
+
return ast;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ============================================
|
|
286
|
+
// Data Access Methods (IDataEngine Interface)
|
|
287
|
+
// ============================================
|
|
259
288
|
|
|
260
|
-
|
|
261
|
-
|
|
289
|
+
async find(object: string, query?: DataEngineQueryOptions): Promise<any[]> {
|
|
290
|
+
this.logger.debug('Find operation starting', { object, query });
|
|
291
|
+
const driver = this.getDriver(object);
|
|
292
|
+
const ast = this.toQueryAST(object, query);
|
|
262
293
|
|
|
263
|
-
// Trigger Before Hook
|
|
264
294
|
const hookContext: HookContext = {
|
|
265
295
|
object,
|
|
266
296
|
event: 'beforeFind',
|
|
267
|
-
input: { ast, options: undefined },
|
|
297
|
+
input: { ast, options: undefined }, // Should map options?
|
|
268
298
|
ql: this
|
|
269
299
|
};
|
|
270
300
|
await this.triggerHooks('beforeFind', hookContext);
|
|
@@ -272,133 +302,183 @@ export class ObjectQL implements IDataEngine {
|
|
|
272
302
|
try {
|
|
273
303
|
const result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
|
|
274
304
|
|
|
275
|
-
// Trigger After Hook
|
|
276
305
|
hookContext.event = 'afterFind';
|
|
277
306
|
hookContext.result = result;
|
|
278
307
|
await this.triggerHooks('afterFind', hookContext);
|
|
279
308
|
|
|
280
309
|
return hookContext.result;
|
|
281
310
|
} catch (e) {
|
|
282
|
-
|
|
311
|
+
this.logger.error('Find operation failed', e as Error, { object });
|
|
283
312
|
throw e;
|
|
284
313
|
}
|
|
285
314
|
}
|
|
286
315
|
|
|
287
|
-
async findOne(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (typeof idOrQuery === 'string') {
|
|
292
|
-
ast = {
|
|
293
|
-
object,
|
|
294
|
-
where: { _id: idOrQuery }
|
|
295
|
-
};
|
|
296
|
-
} else {
|
|
297
|
-
// Assume query object
|
|
298
|
-
// reuse logic from find() or just wrap it
|
|
299
|
-
if (idOrQuery.where || idOrQuery.fields) {
|
|
300
|
-
ast = { object, ...idOrQuery };
|
|
301
|
-
} else {
|
|
302
|
-
ast = { object, where: idOrQuery };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
// Limit 1 for findOne
|
|
316
|
+
async findOne(objectName: string, query?: DataEngineQueryOptions): Promise<any> {
|
|
317
|
+
this.logger.debug('FindOne operation', { objectName });
|
|
318
|
+
const driver = this.getDriver(objectName);
|
|
319
|
+
const ast = this.toQueryAST(objectName, query);
|
|
306
320
|
ast.limit = 1;
|
|
307
321
|
|
|
308
|
-
|
|
322
|
+
// Reuse find logic or call generic driver.findOne if available
|
|
323
|
+
// Assuming driver has findOne
|
|
324
|
+
return driver.findOne(objectName, ast);
|
|
309
325
|
}
|
|
310
326
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
*
|
|
314
|
-
* @param object - Object name
|
|
315
|
-
* @param data - Data to insert
|
|
316
|
-
* @returns Promise resolving to the created record
|
|
317
|
-
*/
|
|
318
|
-
async insert(object: string, data: any): Promise<any> {
|
|
327
|
+
async insert(object: string, data: any | any[], options?: DataEngineInsertOptions): Promise<any> {
|
|
328
|
+
this.logger.debug('Insert operation starting', { object, isBatch: Array.isArray(data) });
|
|
319
329
|
const driver = this.getDriver(object);
|
|
320
|
-
|
|
321
|
-
// 1. Get Schema
|
|
322
|
-
const schema = SchemaRegistry.getObject(object);
|
|
323
|
-
|
|
324
|
-
if (schema) {
|
|
325
|
-
// TODO: Validation Logic
|
|
326
|
-
// validate(schema, data);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 2. Trigger Before Hook
|
|
330
|
+
|
|
330
331
|
const hookContext: HookContext = {
|
|
331
332
|
object,
|
|
332
333
|
event: 'beforeInsert',
|
|
333
|
-
input: { data, options
|
|
334
|
+
input: { data, options },
|
|
334
335
|
ql: this
|
|
335
336
|
};
|
|
336
337
|
await this.triggerHooks('beforeInsert', hookContext);
|
|
337
|
-
|
|
338
|
-
// 3. Execute Driver
|
|
339
|
-
const result = await driver.create(object, hookContext.input.data, hookContext.input.options);
|
|
340
|
-
|
|
341
|
-
// 4. Trigger After Hook
|
|
342
|
-
hookContext.event = 'afterInsert';
|
|
343
|
-
hookContext.result = result;
|
|
344
|
-
await this.triggerHooks('afterInsert', hookContext);
|
|
345
338
|
|
|
346
|
-
|
|
347
|
-
|
|
339
|
+
try {
|
|
340
|
+
let result;
|
|
341
|
+
if (Array.isArray(hookContext.input.data)) {
|
|
342
|
+
// Bulk Create
|
|
343
|
+
if (driver.bulkCreate) {
|
|
344
|
+
result = await driver.bulkCreate(object, hookContext.input.data, hookContext.input.options);
|
|
345
|
+
} else {
|
|
346
|
+
// Fallback loop
|
|
347
|
+
result = await Promise.all(hookContext.input.data.map((item: any) => driver.create(object, item, hookContext.input.options)));
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
result = await driver.create(object, hookContext.input.data, hookContext.input.options);
|
|
351
|
+
}
|
|
348
352
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
* @param object - Object name
|
|
353
|
-
* @param id - Record ID
|
|
354
|
-
* @param data - Updated data
|
|
355
|
-
* @returns Promise resolving to the updated record
|
|
356
|
-
*/
|
|
357
|
-
async update(object: string, id: any, data: any): Promise<any> {
|
|
358
|
-
const driver = this.getDriver(object);
|
|
353
|
+
hookContext.event = 'afterInsert';
|
|
354
|
+
hookContext.result = result;
|
|
355
|
+
await this.triggerHooks('afterInsert', hookContext);
|
|
359
356
|
|
|
360
|
-
|
|
357
|
+
return hookContext.result;
|
|
358
|
+
} catch (e) {
|
|
359
|
+
this.logger.error('Insert operation failed', e as Error, { object });
|
|
360
|
+
throw e;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async update(object: string, data: any, options?: DataEngineUpdateOptions): Promise<any> {
|
|
365
|
+
// NOTE: This signature is tricky because Driver expects (obj, id, data) usually.
|
|
366
|
+
// DataEngine protocol puts filter in options.
|
|
367
|
+
this.logger.debug('Update operation starting', { object });
|
|
368
|
+
const driver = this.getDriver(object);
|
|
369
|
+
|
|
370
|
+
// 1. Extract ID from data or filter if it's a single update by ID
|
|
371
|
+
// This is a simplification. Real implementation needs robust filter handling.
|
|
372
|
+
let id = data.id || data._id;
|
|
373
|
+
if (!id && options?.filter) {
|
|
374
|
+
// Optimization: If filter is simple ID check, extract it
|
|
375
|
+
if (typeof options.filter === 'string') id = options.filter;
|
|
376
|
+
else if (options.filter._id) id = options.filter._id;
|
|
377
|
+
else if (options.filter.id) id = options.filter.id;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const hookContext: HookContext = {
|
|
361
381
|
object,
|
|
362
382
|
event: 'beforeUpdate',
|
|
363
|
-
input: { id, data, options
|
|
383
|
+
input: { id, data, options },
|
|
364
384
|
ql: this
|
|
365
385
|
};
|
|
366
386
|
await this.triggerHooks('beforeUpdate', hookContext);
|
|
367
387
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
388
|
+
try {
|
|
389
|
+
let result;
|
|
390
|
+
if (hookContext.input.id) {
|
|
391
|
+
// Single update by ID
|
|
392
|
+
result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
|
|
393
|
+
} else if (options?.multi && driver.updateMany) {
|
|
394
|
+
// Bulk update by Query
|
|
395
|
+
const ast = this.toQueryAST(object, { filter: options.filter });
|
|
396
|
+
result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options);
|
|
397
|
+
} else {
|
|
398
|
+
throw new Error('Update requires an ID or options.multi=true');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
hookContext.event = 'afterUpdate';
|
|
402
|
+
hookContext.result = result;
|
|
403
|
+
await this.triggerHooks('afterUpdate', hookContext);
|
|
404
|
+
return hookContext.result;
|
|
405
|
+
} catch (e) {
|
|
406
|
+
this.logger.error('Update operation failed', e as Error, { object });
|
|
407
|
+
throw e;
|
|
408
|
+
}
|
|
375
409
|
}
|
|
376
410
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
*
|
|
380
|
-
* @param object - Object name
|
|
381
|
-
* @param id - Record ID
|
|
382
|
-
* @returns Promise resolving to true if deleted, false otherwise
|
|
383
|
-
*/
|
|
384
|
-
async delete(object: string, id: any): Promise<boolean> {
|
|
411
|
+
async delete(object: string, options?: DataEngineDeleteOptions): Promise<any> {
|
|
412
|
+
this.logger.debug('Delete operation starting', { object });
|
|
385
413
|
const driver = this.getDriver(object);
|
|
386
414
|
|
|
415
|
+
// Extract ID logic similar to update
|
|
416
|
+
let id: any = undefined;
|
|
417
|
+
if (options?.filter) {
|
|
418
|
+
if (typeof options.filter === 'string') id = options.filter;
|
|
419
|
+
else if (options.filter._id) id = options.filter._id;
|
|
420
|
+
else if (options.filter.id) id = options.filter.id;
|
|
421
|
+
}
|
|
422
|
+
|
|
387
423
|
const hookContext: HookContext = {
|
|
388
424
|
object,
|
|
389
425
|
event: 'beforeDelete',
|
|
390
|
-
input: { id, options
|
|
426
|
+
input: { id, options },
|
|
391
427
|
ql: this
|
|
392
428
|
};
|
|
393
429
|
await this.triggerHooks('beforeDelete', hookContext);
|
|
394
430
|
|
|
395
|
-
|
|
431
|
+
try {
|
|
432
|
+
let result;
|
|
433
|
+
if (hookContext.input.id) {
|
|
434
|
+
result = await driver.delete(object, hookContext.input.id, hookContext.input.options);
|
|
435
|
+
} else if (options?.multi && driver.deleteMany) {
|
|
436
|
+
const ast = this.toQueryAST(object, { filter: options.filter });
|
|
437
|
+
result = await driver.deleteMany(object, ast, hookContext.input.options);
|
|
438
|
+
} else {
|
|
439
|
+
throw new Error('Delete requires an ID or options.multi=true');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
hookContext.event = 'afterDelete';
|
|
443
|
+
hookContext.result = result;
|
|
444
|
+
await this.triggerHooks('afterDelete', hookContext);
|
|
445
|
+
return hookContext.result;
|
|
446
|
+
} catch (e) {
|
|
447
|
+
this.logger.error('Delete operation failed', e as Error, { object });
|
|
448
|
+
throw e;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
396
451
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
452
|
+
async count(object: string, query?: DataEngineCountOptions): Promise<number> {
|
|
453
|
+
const driver = this.getDriver(object);
|
|
454
|
+
if (driver.count) {
|
|
455
|
+
const ast = this.toQueryAST(object, { filter: query?.filter });
|
|
456
|
+
return driver.count(object, ast);
|
|
457
|
+
}
|
|
458
|
+
// Fallback to find().length
|
|
459
|
+
const res = await this.find(object, { filter: query?.filter, select: ['_id'] });
|
|
460
|
+
return res.length;
|
|
461
|
+
}
|
|
400
462
|
|
|
401
|
-
|
|
402
|
-
|
|
463
|
+
async aggregate(object: string, query: DataEngineAggregateOptions): Promise<any[]> {
|
|
464
|
+
const driver = this.getDriver(object);
|
|
465
|
+
this.logger.debug(`Aggregate on ${object} using ${driver.name}`, query);
|
|
466
|
+
// Driver needs support for raw aggregation or mapped aggregation
|
|
467
|
+
// For now, if driver supports 'execute', we might pass it down, or we need to add 'aggregate' to DriverInterface
|
|
468
|
+
// In this version, we'll assume driver might handle it via special 'find' or throw not implemented
|
|
469
|
+
throw new Error('Aggregate not yet fully implemented in ObjectQL->Driver mapping');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async execute(command: any, options?: Record<string, any>): Promise<any> {
|
|
473
|
+
// Direct pass-through implies we know which driver to use?
|
|
474
|
+
// Usually execute is tied to a specific object context OR we need a way to select driver.
|
|
475
|
+
// If command has 'object', we use that.
|
|
476
|
+
if (options?.object) {
|
|
477
|
+
const driver = this.getDriver(options.object);
|
|
478
|
+
if (driver.execute) {
|
|
479
|
+
return driver.execute(command, undefined, options);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
throw new Error('Execute requires options.object to select driver');
|
|
403
483
|
}
|
|
404
484
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -27,7 +27,7 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
ctx.registerService('objectql', this.ql);
|
|
30
|
-
|
|
30
|
+
ctx.logger.info('ObjectQL engine registered as service');
|
|
31
31
|
|
|
32
32
|
// Register Protocol Implementation
|
|
33
33
|
if (!this.ql) {
|
|
@@ -36,11 +36,11 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
36
36
|
const protocolShim = new ObjectStackProtocolImplementation(this.ql);
|
|
37
37
|
|
|
38
38
|
ctx.registerService('protocol', protocolShim);
|
|
39
|
-
|
|
39
|
+
ctx.logger.info('Protocol service registered');
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
async start(ctx: PluginContext) {
|
|
43
|
-
|
|
43
|
+
ctx.logger.info('ObjectQL engine initialized');
|
|
44
44
|
|
|
45
45
|
// Discover features from Kernel Services
|
|
46
46
|
if (ctx.getServices && this.ql) {
|
|
@@ -49,12 +49,12 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
49
49
|
if (name.startsWith('driver.')) {
|
|
50
50
|
// Register Driver
|
|
51
51
|
this.ql.registerDriver(service);
|
|
52
|
-
|
|
52
|
+
ctx.logger.debug('Discovered and registered driver service', { serviceName: name });
|
|
53
53
|
}
|
|
54
54
|
if (name.startsWith('app.')) {
|
|
55
55
|
// Register App
|
|
56
56
|
this.ql.registerApp(service); // service is Manifest
|
|
57
|
-
|
|
57
|
+
ctx.logger.debug('Discovered and registered app service', { serviceName: name });
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
}
|