@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/src/engine.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import { QueryAST, HookContext } from '@objectstack/spec/data';
2
- import { ObjectStackManifest } from '@objectstack/spec/system';
3
- import { DriverOptions } from '@objectstack/spec/system';
4
- import { DriverInterface, IDataEngine, DataEngineQueryOptions } from '@objectstack/core';
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: Console;
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
- console.log(`[ObjectQL] Engine Instance Created`);
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: console,
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
- console.log(`[ObjectQL] Registered hook for ${event}`);
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
- registerApp(manifestPart: any) {
94
- // 1. Handle Module Imports (commonjs/esm interop)
95
- // If the passed object is a module namespace with a default export, use that.
96
- const raw = manifestPart.default || manifestPart;
97
-
98
- // Support nested manifest property (Stack Definition)
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 Objects from App/Plugin
124
+ // Register objects
115
125
  if (manifest.objects) {
116
- for (const obj of manifest.objects) {
117
- // Ensure object name is registered globally
118
- SchemaRegistry.registerObject(obj);
119
- console.log(`[ObjectQL] Registered Object: ${obj.name}`);
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
- console.warn(`[ObjectQL] Driver ${driver.name} is already registered. Skipping.`);
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
- console.log(`[ObjectQL] Registered driver: ${driver.name} v${driver.version}`);
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
- console.log('[ObjectQL] Initializing drivers...');
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
- console.error(`[ObjectQL] Failed to connect driver ${name}`, e);
227
+ this.logger.error('Failed to connect driver', e as Error, { driverName: name });
205
228
  }
206
229
  }
207
- // In a real app, we would sync schemas here
230
+
231
+ this.logger.info('ObjectQL engine initialization complete');
208
232
  }
209
233
 
210
234
  async destroy() {
211
- for (const driver of this.drivers.values()) {
212
- await driver.disconnect();
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
- // Data Access Methods (IDataEngine Interface)
249
+ // Helper: Query Conversion
218
250
  // ============================================
219
251
 
220
- /**
221
- * Find records matching a query (IDataEngine interface)
222
- *
223
- * @param object - Object name
224
- * @param query - Query options (IDataEngine format)
225
- * @returns Promise resolving to array of records
226
- */
227
- async find(object: string, query?: DataEngineQueryOptions): Promise<any[]> {
228
- const driver = this.getDriver(object);
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
- // Convert DataEngineQueryOptions to QueryAST
231
- let ast: QueryAST = { object };
276
+ if (options.top !== undefined) ast.limit = options.top;
277
+ else if (options.limit !== undefined) ast.limit = options.limit;
232
278
 
233
- if (query) {
234
- // Map DataEngineQueryOptions to QueryAST
235
- if (query.filter) {
236
- ast.where = query.filter;
237
- }
238
- if (query.select) {
239
- ast.fields = query.select;
240
- }
241
- if (query.sort) {
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
- // Set default limit if not specified
261
- if (ast.limit === undefined) ast.limit = 100;
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
- // hookContext.error = e;
311
+ this.logger.error('Find operation failed', e as Error, { object });
283
312
  throw e;
284
313
  }
285
314
  }
286
315
 
287
- async findOne(object: string, idOrQuery: string | any, options?: DriverOptions) {
288
- const driver = this.getDriver(object);
289
-
290
- let ast: QueryAST;
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
- return driver.findOne(object, ast, options);
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
- * Insert a new record (IDataEngine interface)
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: undefined },
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
- return hookContext.result;
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
- * Update a record by ID (IDataEngine interface)
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
- const hookContext: HookContext = {
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: undefined },
383
+ input: { id, data, options },
364
384
  ql: this
365
385
  };
366
386
  await this.triggerHooks('beforeUpdate', hookContext);
367
387
 
368
- const result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
369
-
370
- hookContext.event = 'afterUpdate';
371
- hookContext.result = result;
372
- await this.triggerHooks('afterUpdate', hookContext);
373
-
374
- return hookContext.result;
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
- * Delete a record by ID (IDataEngine interface)
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: undefined },
426
+ input: { id, options },
391
427
  ql: this
392
428
  };
393
429
  await this.triggerHooks('beforeDelete', hookContext);
394
430
 
395
- const result = await driver.delete(object, hookContext.input.id, hookContext.input.options);
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
- hookContext.event = 'afterDelete';
398
- hookContext.result = result;
399
- await this.triggerHooks('afterDelete', hookContext);
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
- // Driver.delete() already returns boolean per DriverInterface spec
402
- return hookContext.result;
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
- if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] ObjectQL engine registered as service`);
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
- if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] Protocol service registered`);
39
+ ctx.logger.info('Protocol service registered');
40
40
  }
41
41
 
42
42
  async start(ctx: PluginContext) {
43
- if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] ObjectQL engine initialized`);
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
- if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] Discovered and registered driver service: ${name}`);
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
- if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] Discovered and registered app service: ${name}`);
57
+ ctx.logger.debug('Discovered and registered app service', { serviceName: name });
58
58
  }
59
59
  }
60
60
  }