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