@objectql/core 4.0.6 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectql/core",
3
- "version": "4.0.6",
3
+ "version": "4.1.0",
4
4
  "description": "Universal runtime engine for ObjectQL - AI-native metadata-driven ORM with validation, repository pattern, and driver orchestration",
5
5
  "keywords": [
6
6
  "objectql",
@@ -23,9 +23,9 @@
23
23
  "@objectstack/runtime": "^1.0.0",
24
24
  "@objectstack/spec": "^1.0.0",
25
25
  "js-yaml": "^4.1.0",
26
- "@objectql/plugin-formula": "4.0.6",
27
- "@objectql/plugin-validator": "4.0.6",
28
- "@objectql/types": "4.0.6"
26
+ "@objectql/plugin-formula": "4.1.0",
27
+ "@objectql/plugin-validator": "4.1.0",
28
+ "@objectql/types": "4.1.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/js-yaml": "^4.0.5",
package/src/app.ts CHANGED
@@ -22,8 +22,10 @@ import {
22
22
  ActionContext,
23
23
  LoaderPlugin
24
24
  } from '@objectql/types';
25
- import { ObjectKernel, type Plugin } from '@objectstack/core';
25
+ import { ObjectKernel, type Plugin } from '@objectstack/runtime';
26
26
  import { ObjectQL as RuntimeObjectQL, SchemaRegistry } from '@objectstack/objectql';
27
+ import { ValidatorPlugin } from '@objectql/plugin-validator';
28
+ import { FormulaPlugin } from '@objectql/plugin-formula';
27
29
  import { ObjectRepository } from './repository';
28
30
  import { convertIntrospectedSchemaToObjects } from './util';
29
31
  import { CompiledHookManager } from './optimizations/CompiledHookManager';
@@ -81,10 +83,27 @@ export class ObjectQL implements IObjectQL {
81
83
  }
82
84
  }
83
85
 
86
+ // Ensure default plugins are present
87
+ if (!this.kernelPlugins.some(p => p.name === 'validator')) {
88
+ this.use(new ValidatorPlugin());
89
+ }
90
+ if (!this.kernelPlugins.some(p => p.name === 'formula')) {
91
+ this.use(new FormulaPlugin());
92
+ }
93
+
84
94
  // Create the kernel with registered plugins
85
- this.kernel = new (ObjectKernel as any)();
95
+ this.kernel = new (ObjectKernel as any)(this.kernelPlugins);
86
96
  for (const plugin of this.kernelPlugins) {
97
+ // Fallback for kernels that support .use() but maybe didn't take them in constructor or if we need to support both
98
+ // NOTE: Modern ObjectKernel takes plugins in constructor.
87
99
  if ((this.kernel as any).use) {
100
+ // Try to avoid double registration if the kernel is smart, but since we don't know the kernel logic perfectly:
101
+ // Ideally check if already added. But for now, we leave this for backward compat
102
+ // if ObjectKernel DOESN't take constructor args but HAS use().
103
+
104
+ // However, we just instantiated it.
105
+ // Let's assume constructor is the way if available.
106
+ // But we keep this check for .use() just in case the constructor signature is different (e.g. empty)
88
107
  (this.kernel as any).use(plugin);
89
108
  }
90
109
  }
@@ -406,10 +425,10 @@ export class ObjectQL implements IObjectQL {
406
425
  for (const plugin of this.kernelPlugins) {
407
426
  try {
408
427
  if (typeof (plugin as any).init === 'function') {
409
- await (plugin as any).init();
428
+ await (plugin as any).init(this.kernel);
410
429
  }
411
430
  if (typeof (plugin as any).start === 'function') {
412
- await (plugin as any).start();
431
+ await (plugin as any).start(this.kernel);
413
432
  }
414
433
  } catch (error) {
415
434
  console.error(`Failed to initialize plugin ${(plugin as any).name || 'unknown'}:`, error);
package/src/plugin.ts CHANGED
@@ -246,6 +246,16 @@ export class ObjectQLPlugin implements RuntimePlugin {
246
246
  };
247
247
 
248
248
  kernel.find = async (objectName: string, query: any): Promise<{ value: any[]; count: number }> => {
249
+ // Use QueryService if available for advanced query processing (AST, optimizations)
250
+ if ((kernel as any).queryService) {
251
+ const result = await (kernel as any).queryService.find(objectName, query);
252
+ return {
253
+ value: result.value,
254
+ count: result.count !== undefined ? result.count : result.value.length
255
+ };
256
+ }
257
+
258
+ // Fallback to direct driver call
249
259
  const driver = getDriver(objectName);
250
260
  const value = await driver.find(objectName, query);
251
261
  const count = value.length;
@@ -253,11 +263,25 @@ export class ObjectQLPlugin implements RuntimePlugin {
253
263
  };
254
264
 
255
265
  kernel.get = async (objectName: string, id: string): Promise<any> => {
266
+ // Use QueryService if available
267
+ if ((kernel as any).queryService) {
268
+ const result = await (kernel as any).queryService.findOne(objectName, id);
269
+ return result.value;
270
+ }
271
+
256
272
  const driver = getDriver(objectName);
257
273
  return await driver.findOne(objectName, id);
258
274
  };
259
275
 
260
276
  kernel.count = async (objectName: string, filters?: any): Promise<number> => {
277
+ // Use QueryService if available
278
+ if ((kernel as any).queryService) {
279
+ // QueryService.count expects a UnifiedQuery filter or just filter object?
280
+ // Looking at QueryService.count signature: count(objectName: string, where?: Filter, options?: QueryOptions)
281
+ const result = await (kernel as any).queryService.count(objectName, filters);
282
+ return result.value;
283
+ }
284
+
261
285
  const driver = getDriver(objectName);
262
286
  return await driver.count(objectName, filters || {}, {});
263
287
  };
@@ -16,6 +16,7 @@ import type {
16
16
  import { Data } from '@objectstack/spec';
17
17
  type QueryAST = Data.QueryAST;
18
18
  import { QueryBuilder } from './query-builder';
19
+ import { QueryCompiler } from '../optimizations/QueryCompiler';
19
20
 
20
21
  /**
21
22
  * Options for query execution
@@ -101,12 +102,14 @@ export interface QueryProfile {
101
102
  */
102
103
  export class QueryService {
103
104
  private queryBuilder: QueryBuilder;
105
+ private queryCompiler: QueryCompiler;
104
106
 
105
107
  constructor(
106
108
  private datasources: Record<string, Driver>,
107
109
  private metadata: MetadataRegistry
108
110
  ) {
109
111
  this.queryBuilder = new QueryBuilder();
112
+ this.queryCompiler = new QueryCompiler(1000);
110
113
  }
111
114
 
112
115
  /**
@@ -142,7 +145,9 @@ export class QueryService {
142
145
  * @private
143
146
  */
144
147
  private buildQueryAST(objectName: string, query: UnifiedQuery): QueryAST {
145
- return this.queryBuilder.build(objectName, query);
148
+ const ast = this.queryBuilder.build(objectName, query);
149
+ const compiled = this.queryCompiler.compile(objectName, ast);
150
+ return compiled.ast;
146
151
  }
147
152
 
148
153
  /**
package/src/repository.ts CHANGED
@@ -11,64 +11,16 @@ import type { ObjectKernel } from '@objectstack/runtime';
11
11
  import { Data } from '@objectstack/spec';
12
12
  type QueryAST = Data.QueryAST;
13
13
  type SortNode = Data.SortNode;
14
- import { Validator } from '@objectql/plugin-validator';
15
- import { FormulaEngine } from '@objectql/plugin-formula';
16
- import { QueryBuilder } from './query';
17
- import { QueryCompiler } from './optimizations/QueryCompiler';
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 ObjectKernel {
24
- validator?: Validator;
25
- formulaEngine?: FormulaEngine;
26
- create?: (objectName: string, data: any) => Promise<any>;
27
- update?: (objectName: string, id: string, data: any) => Promise<any>;
28
- delete?: (objectName: string, id: string) => Promise<any>;
29
- find?: (objectName: string, query: any) => Promise<any>;
30
- get?: (objectName: string, id: string) => Promise<any>;
31
- }
32
14
 
33
15
  export class ObjectRepository {
34
- private queryBuilder: QueryBuilder;
35
- // Shared query compiler for caching compiled queries
36
- private static queryCompiler = new QueryCompiler(1000);
37
16
 
38
17
  constructor(
39
18
  private objectName: string,
40
19
  private context: ObjectQLContext,
41
20
  private app: IObjectQL
42
21
  ) {
43
- this.queryBuilder = new QueryBuilder();
44
22
  }
45
23
 
46
- /**
47
- * Get validator instance from kernel (via plugin)
48
- * Falls back to creating a new instance if not available
49
- */
50
- private getValidator(): Validator {
51
- const kernel = this.getKernel() as ExtendedKernel;
52
- if (kernel.validator) {
53
- return kernel.validator;
54
- }
55
- // Fallback for backward compatibility
56
- return new Validator();
57
- }
58
-
59
- /**
60
- * Get formula engine instance from kernel (via plugin)
61
- * Falls back to creating a new instance if not available
62
- */
63
- private getFormulaEngine(): FormulaEngine {
64
- const kernel = this.getKernel() as ExtendedKernel;
65
- if (kernel.formulaEngine) {
66
- return kernel.formulaEngine;
67
- }
68
- // Fallback for backward compatibility
69
- return new FormulaEngine();
70
- }
71
-
72
24
  private getDriver(): Driver {
73
25
  const obj = this.getSchema();
74
26
  const datasourceName = obj.datasource || 'default';
@@ -86,17 +38,6 @@ export class ObjectRepository {
86
38
  };
87
39
  }
88
40
 
89
- /**
90
- * Translates ObjectQL UnifiedQuery to ObjectStack QueryAST format
91
- * Uses query compiler for caching and optimization
92
- */
93
- private buildQueryAST(query: UnifiedQuery): QueryAST {
94
- const ast = this.queryBuilder.build(this.objectName, query);
95
- // Use query compiler to cache and optimize the AST
96
- const compiled = ObjectRepository.queryCompiler.compile(this.objectName, ast);
97
- return compiled.ast;
98
- }
99
-
100
41
  getSchema(): ObjectConfig {
101
42
  const obj = this.app.getObject(this.objectName);
102
43
  if (!obj) {
@@ -129,148 +70,6 @@ export class ObjectRepository {
129
70
  };
130
71
  }
131
72
 
132
- /**
133
- * Validates a record against field-level and object-level validation rules.
134
- * For updates, only fields present in the update payload are validated at the field level,
135
- * while object-level rules use the merged record (previousRecord + updates).
136
- */
137
- private async validateRecord(
138
- operation: 'create' | 'update',
139
- record: any,
140
- previousRecord?: any
141
- ): Promise<void> {
142
- const schema = this.getSchema();
143
- const allResults: ValidationRuleResult[] = [];
144
-
145
- // 1. Validate field-level rules
146
- // For updates, only validate fields that are present in the update payload
147
- for (const [fieldName, fieldConfig] of Object.entries(schema.fields)) {
148
- // Skip field validation for updates if the field is not in the update payload
149
- if (operation === 'update' && !(fieldName in record)) {
150
- continue;
151
- }
152
-
153
- const value = record[fieldName];
154
- const fieldResults = await this.getValidator().validateField(
155
- fieldName,
156
- fieldConfig,
157
- value,
158
- {
159
- record,
160
- previousRecord,
161
- operation,
162
- user: this.getUserFromContext(),
163
- api: this.getHookAPI(),
164
- }
165
- );
166
- allResults.push(...fieldResults);
167
- }
168
-
169
- // 2. Validate object-level validation rules
170
- if (schema.validation?.rules && schema.validation.rules.length > 0) {
171
- // For updates, merge the update data with previous record to get the complete final state
172
- const mergedRecord = operation === 'update' && previousRecord
173
- ? { ...previousRecord, ...record }
174
- : record;
175
-
176
- // Track which fields changed (using shallow comparison for performance)
177
- // IMPORTANT: Shallow comparison does not detect changes in nested objects/arrays.
178
- // If your validation rules rely on detecting changes in complex nested structures,
179
- // you may need to implement custom change tracking in hooks.
180
- const changedFields = previousRecord
181
- ? Object.keys(record).filter(key => record[key] !== previousRecord[key])
182
- : undefined;
183
-
184
- const validationContext: ValidationContext = {
185
- record: mergedRecord,
186
- previousRecord,
187
- operation,
188
- user: this.getUserFromContext(),
189
- api: this.getHookAPI(),
190
- changedFields,
191
- };
192
-
193
- const result = await this.getValidator().validate(schema.validation.rules, validationContext);
194
- allResults.push(...result.results);
195
- }
196
-
197
- // 3. Collect errors and throw if any
198
- const errors = allResults.filter(r => !r.valid && r.severity === 'error');
199
- if (errors.length > 0) {
200
- const errorMessage = errors.map(e => e.message).join('; ');
201
- throw new ValidationError(errorMessage, errors);
202
- }
203
- }
204
-
205
- /**
206
- * Evaluate formula fields for a record
207
- * Adds computed formula field values to the record
208
- */
209
- private evaluateFormulas(record: any): any {
210
- const schema = this.getSchema();
211
- const now = new Date();
212
-
213
- // Build formula context
214
- const formulaContext: FormulaContext = {
215
- record,
216
- system: {
217
- today: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
218
- now: now,
219
- year: now.getFullYear(),
220
- month: now.getMonth() + 1,
221
- day: now.getDate(),
222
- hour: now.getHours(),
223
- minute: now.getMinutes(),
224
- second: now.getSeconds(),
225
- },
226
- current_user: {
227
- id: this.context.userId || '',
228
- // TODO: Retrieve actual user name from user object if available
229
- name: undefined,
230
- email: undefined,
231
- role: this.context.roles?.[0],
232
- },
233
- is_new: false,
234
- record_id: record._id || record.id,
235
- };
236
-
237
- // Evaluate each formula field
238
- for (const [fieldName, fieldConfig] of Object.entries(schema.fields)) {
239
- const formulaExpression = fieldConfig.expression;
240
-
241
- if (fieldConfig.type === 'formula' && formulaExpression) {
242
- const result = this.getFormulaEngine().evaluate(
243
- formulaExpression,
244
- formulaContext,
245
- fieldConfig.data_type || 'text',
246
- { strict: true }
247
- );
248
-
249
- if (result.success) {
250
- record[fieldName] = result.value;
251
- } else {
252
- // In case of error, set to null and log for diagnostics
253
- record[fieldName] = null;
254
- // Formula evaluation should not throw here, but we need observability
255
- // This logging is intentionally minimal and side-effect free
256
-
257
- console.error(
258
- '[ObjectQL][FormulaEngine] Formula evaluation failed',
259
- {
260
- objectName: this.objectName,
261
- fieldName,
262
- recordId: formulaContext.record_id,
263
- expression: formulaExpression,
264
- error: result.error,
265
- stack: result.stack,
266
- }
267
- );
268
- }
269
- }
270
- }
271
-
272
- return record;
273
- }
274
73
 
275
74
  async find(query: UnifiedQuery = {}): Promise<any[]> {
276
75
  const hookCtx: RetrievalHookContext = {
@@ -284,15 +83,12 @@ export class ObjectRepository {
284
83
  };
285
84
  await this.app.triggerHook('beforeFind', this.objectName, hookCtx);
286
85
 
287
- // Build QueryAST and execute via kernel
288
- const ast = this.buildQueryAST(hookCtx.query || {});
289
- const kernelResult = await (this.getKernel() as any).find(this.objectName, ast);
86
+ // Execute via kernel (delegates to QueryService)
87
+ const kernelResult = await (this.getKernel() as any).find(this.objectName, hookCtx.query || {});
290
88
  const results = kernelResult.value;
291
89
 
292
- // Evaluate formulas for each result
293
- const resultsWithFormulas = results.map((record: any) => this.evaluateFormulas(record));
294
-
295
- hookCtx.result = resultsWithFormulas;
90
+ // Formula evaluation moved to FormulaPlugin hook
91
+ hookCtx.result = results;
296
92
  await this.app.triggerHook('afterFind', this.objectName, hookCtx);
297
93
 
298
94
  return hookCtx.result as any[];
@@ -314,10 +110,8 @@ export class ObjectRepository {
314
110
  // Use kernel.get() for direct ID lookup
315
111
  const result = await (this.getKernel() as any).get(this.objectName, String(idOrQuery));
316
112
 
317
- // Evaluate formulas if result exists
318
- const resultWithFormulas = result ? this.evaluateFormulas(result) : result;
319
-
320
- hookCtx.result = resultWithFormulas;
113
+ // Formula evaluation moved to FormulaPlugin hook
114
+ hookCtx.result = result;
321
115
  await this.app.triggerHook('afterFind', this.objectName, hookCtx);
322
116
  return hookCtx.result;
323
117
  } else {
@@ -354,10 +148,8 @@ export class ObjectRepository {
354
148
  };
355
149
  await this.app.triggerHook('beforeCount', this.objectName, hookCtx);
356
150
 
357
- // Build QueryAST and execute via kernel to get count
358
- const ast = this.buildQueryAST(hookCtx.query || {});
359
- const kernelResult = await (this.getKernel() as any).find(this.objectName, ast);
360
- const result = kernelResult.count;
151
+ // Execute via kernel (delegates to QueryService)
152
+ const result = await (this.getKernel() as any).count(this.objectName, hookCtx.query || {});
361
153
 
362
154
  hookCtx.result = result;
363
155
  await this.app.triggerHook('afterCount', this.objectName, hookCtx);
@@ -380,8 +172,7 @@ export class ObjectRepository {
380
172
  if (this.context.userId) finalDoc.created_by = this.context.userId;
381
173
  if (this.context.spaceId) finalDoc.space_id = this.context.spaceId;
382
174
 
383
- // Validate the record before creating
384
- await this.validateRecord('create', finalDoc);
175
+ // Validation moved to ValidatorPlugin hook
385
176
 
386
177
  // Execute via kernel
387
178
  const result = await (this.getKernel() as any).create(this.objectName, finalDoc, this.getOptions());
@@ -407,8 +198,7 @@ export class ObjectRepository {
407
198
  };
408
199
  await this.app.triggerHook('beforeUpdate', this.objectName, hookCtx);
409
200
 
410
- // Validate the update
411
- await this.validateRecord('update', hookCtx.data, previousData);
201
+ // Validation moved to ValidatorPlugin hook
412
202
 
413
203
  // Execute via kernel
414
204
  const result = await (this.getKernel() as any).update(this.objectName, String(id), hookCtx.data, this.getOptions());
package/test/app.test.ts CHANGED
@@ -295,12 +295,12 @@ describe('ObjectQL App', () => {
295
295
 
296
296
  describe('Plugin System', () => {
297
297
  it('should initialize runtime plugins on init', async () => {
298
- const initFn = jest.fn();
299
- const startFn = jest.fn();
298
+ const installFn = jest.fn();
299
+ const onStartFn = jest.fn();
300
300
  const mockPlugin = {
301
301
  name: 'test-plugin',
302
- init: initFn,
303
- start: startFn
302
+ install: installFn,
303
+ onStart: onStartFn
304
304
  };
305
305
 
306
306
  const app = new ObjectQL({
@@ -309,8 +309,8 @@ describe('ObjectQL App', () => {
309
309
  });
310
310
 
311
311
  await app.init();
312
- expect(initFn).toHaveBeenCalled();
313
- expect(startFn).toHaveBeenCalled();
312
+ expect(installFn).toHaveBeenCalled();
313
+ expect(onStartFn).toHaveBeenCalled();
314
314
  });
315
315
 
316
316
  it('should use plugin method', () => {