@objectql/core 4.0.2 → 4.0.4

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.
Files changed (98) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +4 -4
  4. package/dist/app.d.ts +9 -6
  5. package/dist/app.js +151 -29
  6. package/dist/app.js.map +1 -1
  7. package/dist/index.d.ts +6 -9
  8. package/dist/index.js +2 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/optimizations/CompiledHookManager.d.ts +55 -0
  11. package/dist/optimizations/CompiledHookManager.js +164 -0
  12. package/dist/optimizations/CompiledHookManager.js.map +1 -0
  13. package/dist/optimizations/DependencyGraph.d.ts +82 -0
  14. package/dist/optimizations/DependencyGraph.js +211 -0
  15. package/dist/optimizations/DependencyGraph.js.map +1 -0
  16. package/dist/optimizations/GlobalConnectionPool.d.ts +89 -0
  17. package/dist/optimizations/GlobalConnectionPool.js +193 -0
  18. package/dist/optimizations/GlobalConnectionPool.js.map +1 -0
  19. package/dist/optimizations/LazyMetadataLoader.d.ts +75 -0
  20. package/dist/optimizations/LazyMetadataLoader.js +149 -0
  21. package/dist/optimizations/LazyMetadataLoader.js.map +1 -0
  22. package/dist/optimizations/OptimizedMetadataRegistry.d.ts +26 -0
  23. package/dist/optimizations/OptimizedMetadataRegistry.js +117 -0
  24. package/dist/optimizations/OptimizedMetadataRegistry.js.map +1 -0
  25. package/dist/optimizations/OptimizedValidationEngine.d.ts +73 -0
  26. package/dist/optimizations/OptimizedValidationEngine.js +141 -0
  27. package/dist/optimizations/OptimizedValidationEngine.js.map +1 -0
  28. package/dist/optimizations/QueryCompiler.d.ts +51 -0
  29. package/dist/optimizations/QueryCompiler.js +216 -0
  30. package/dist/optimizations/QueryCompiler.js.map +1 -0
  31. package/dist/optimizations/SQLQueryOptimizer.d.ts +96 -0
  32. package/dist/optimizations/SQLQueryOptimizer.js +265 -0
  33. package/dist/optimizations/SQLQueryOptimizer.js.map +1 -0
  34. package/dist/optimizations/index.d.ts +32 -0
  35. package/dist/optimizations/index.js +44 -0
  36. package/dist/optimizations/index.js.map +1 -0
  37. package/dist/plugin.d.ts +8 -7
  38. package/dist/plugin.js +57 -22
  39. package/dist/plugin.js.map +1 -1
  40. package/dist/query/query-analyzer.js.map +1 -1
  41. package/dist/query/query-builder.d.ts +6 -1
  42. package/dist/query/query-builder.js +21 -5
  43. package/dist/query/query-builder.js.map +1 -1
  44. package/dist/query/query-service.js.map +1 -1
  45. package/dist/repository.d.ts +2 -0
  46. package/dist/repository.js +15 -9
  47. package/dist/repository.js.map +1 -1
  48. package/jest.config.js +3 -3
  49. package/package.json +8 -5
  50. package/src/app.ts +173 -47
  51. package/src/index.ts +8 -9
  52. package/src/optimizations/CompiledHookManager.ts +185 -0
  53. package/src/optimizations/DependencyGraph.ts +255 -0
  54. package/src/optimizations/GlobalConnectionPool.ts +251 -0
  55. package/src/optimizations/LazyMetadataLoader.ts +180 -0
  56. package/src/optimizations/OptimizedMetadataRegistry.ts +132 -0
  57. package/src/optimizations/OptimizedValidationEngine.ts +172 -0
  58. package/src/optimizations/QueryCompiler.ts +242 -0
  59. package/src/optimizations/SQLQueryOptimizer.ts +329 -0
  60. package/src/optimizations/index.ts +34 -0
  61. package/src/plugin.ts +71 -28
  62. package/src/query/query-analyzer.ts +1 -1
  63. package/src/query/query-builder.ts +21 -7
  64. package/src/query/query-service.ts +1 -1
  65. package/src/repository.ts +25 -13
  66. package/test/__mocks__/@objectstack/runtime.ts +8 -8
  67. package/test/app.test.ts +9 -7
  68. package/test/optimizations.test.ts +440 -0
  69. package/test/plugin-integration.test.ts +30 -19
  70. package/tsconfig.json +4 -6
  71. package/tsconfig.tsbuildinfo +1 -1
  72. package/dist/ai-agent.d.ts +0 -176
  73. package/dist/ai-agent.js +0 -722
  74. package/dist/ai-agent.js.map +0 -1
  75. package/dist/formula-engine.d.ts +0 -102
  76. package/dist/formula-engine.js +0 -433
  77. package/dist/formula-engine.js.map +0 -1
  78. package/dist/formula-plugin.d.ts +0 -52
  79. package/dist/formula-plugin.js +0 -107
  80. package/dist/formula-plugin.js.map +0 -1
  81. package/dist/validator-plugin.d.ts +0 -56
  82. package/dist/validator-plugin.js +0 -106
  83. package/dist/validator-plugin.js.map +0 -1
  84. package/dist/validator.d.ts +0 -80
  85. package/dist/validator.js +0 -625
  86. package/dist/validator.js.map +0 -1
  87. package/src/ai-agent.ts +0 -868
  88. package/src/formula-engine.ts +0 -572
  89. package/src/formula-plugin.ts +0 -141
  90. package/src/validator-plugin.ts +0 -140
  91. package/src/validator.ts +0 -743
  92. package/test/formula-engine.test.ts +0 -725
  93. package/test/formula-integration.test.ts +0 -286
  94. package/test/formula-plugin.test.ts +0 -197
  95. package/test/formula-spec-compliance.test.ts +0 -258
  96. package/test/validation-spec-compliance.test.ts +0 -440
  97. package/test/validator-plugin.test.ts +0 -126
  98. package/test/validator.test.ts +0 -440
@@ -1,572 +0,0 @@
1
- /**
2
- * ObjectQL
3
- * Copyright (c) 2026-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * Formula Engine Implementation
11
- *
12
- * Evaluates formula expressions defined in object metadata.
13
- * Formulas are read-only calculated fields computed at query time.
14
- *
15
- * @see @objectql/types/formula for type definitions
16
- * @see docs/spec/formula.md for complete specification
17
- */
18
-
19
- import {
20
- FormulaContext,
21
- FormulaEvaluationResult,
22
- FormulaEvaluationOptions,
23
- FormulaError,
24
- FormulaErrorType,
25
- FormulaValue,
26
- FormulaDataType,
27
- FormulaMetadata,
28
- FormulaEngineConfig,
29
- FormulaCustomFunction,
30
- } from '@objectql/types';
31
-
32
- /**
33
- * Formula Engine for evaluating JavaScript-style expressions
34
- *
35
- * Features:
36
- * - Field references (e.g., `quantity * unit_price`)
37
- * - System variables (e.g., `$today`, `$current_user.id`)
38
- * - Lookup chains (e.g., `account.owner.name`)
39
- * - Built-in functions (Math, String, Date methods)
40
- * - Conditional logic (ternary, if/else)
41
- * - Safe sandbox execution
42
- */
43
- export class FormulaEngine {
44
- private config: FormulaEngineConfig;
45
- private customFunctions: Record<string, FormulaCustomFunction>;
46
-
47
- constructor(config: FormulaEngineConfig = {}) {
48
- this.config = {
49
- enable_cache: config.enable_cache ?? false,
50
- cache_ttl: config.cache_ttl ?? 300,
51
- max_execution_time: config.max_execution_time ?? 0, // 0 means no timeout enforcement
52
- enable_monitoring: config.enable_monitoring ?? false,
53
- custom_functions: config.custom_functions ?? {},
54
- sandbox: {
55
- enabled: config.sandbox?.enabled ?? true,
56
- allowed_globals: config.sandbox?.allowed_globals ?? ['Math', 'String', 'Number', 'Boolean', 'Date', 'Array', 'Object'],
57
- blocked_operations: config.sandbox?.blocked_operations ?? ['eval', 'Function', 'require', 'import'],
58
- },
59
- };
60
- this.customFunctions = this.config.custom_functions || {};
61
- }
62
-
63
- /**
64
- * Evaluate a formula expression
65
- *
66
- * @param expression - The JavaScript expression to evaluate
67
- * @param context - Runtime context with record data, system variables, user context
68
- * @param dataType - Expected return data type
69
- * @param options - Evaluation options
70
- * @returns Evaluation result with value, type, success flag, and optional error
71
- */
72
- evaluate(
73
- expression: string,
74
- context: FormulaContext,
75
- dataType: FormulaDataType,
76
- options: FormulaEvaluationOptions = {}
77
- ): FormulaEvaluationResult {
78
- const startTime = Date.now();
79
- const timeout = options.timeout ?? this.config.max_execution_time ?? 0;
80
-
81
- try {
82
- // Validate expression
83
- if (!expression || expression.trim() === '') {
84
- throw new FormulaError(
85
- FormulaErrorType.SYNTAX_ERROR,
86
- 'Formula expression cannot be empty',
87
- expression
88
- );
89
- }
90
-
91
- // Prepare evaluation context
92
- const evalContext = this.buildEvaluationContext(context);
93
-
94
- // Execute expression with timeout
95
- const value = this.executeExpression(expression, evalContext, timeout, options);
96
-
97
- // Validate and coerce result type
98
- const coercedValue = this.coerceValue(value, dataType, expression);
99
-
100
- const executionTime = Date.now() - startTime;
101
-
102
- return {
103
- value: coercedValue,
104
- type: dataType,
105
- success: true,
106
- execution_time: executionTime,
107
- };
108
- } catch (error) {
109
- const executionTime = Date.now() - startTime;
110
-
111
- if (error instanceof FormulaError) {
112
- return {
113
- value: null,
114
- type: dataType,
115
- success: false,
116
- error: error.message,
117
- stack: error.stack,
118
- execution_time: executionTime,
119
- };
120
- }
121
-
122
- // Wrap unknown errors
123
- return {
124
- value: null,
125
- type: dataType,
126
- success: false,
127
- error: error instanceof Error ? error.message : String(error),
128
- stack: error instanceof Error ? error.stack : undefined,
129
- execution_time: executionTime,
130
- };
131
- }
132
- }
133
-
134
- /**
135
- * Build the evaluation context from FormulaContext
136
- * Creates a safe object with field values, system variables, and user context
137
- */
138
- private buildEvaluationContext(context: FormulaContext): Record<string, any> {
139
- const evalContext: Record<string, any> = {};
140
-
141
- // Add all record fields to context
142
- for (const [key, value] of Object.entries(context.record)) {
143
- evalContext[key] = value;
144
- }
145
-
146
- // Add system variables with $ prefix
147
- evalContext.$today = context.system.today;
148
- evalContext.$now = context.system.now;
149
- evalContext.$year = context.system.year;
150
- evalContext.$month = context.system.month;
151
- evalContext.$day = context.system.day;
152
- evalContext.$hour = context.system.hour;
153
- evalContext.$minute = context.system.minute;
154
- evalContext.$second = context.system.second;
155
-
156
- // Add current user context
157
- evalContext.$current_user = context.current_user;
158
-
159
- // Add record context flags
160
- evalContext.$is_new = context.is_new;
161
- evalContext.$record_id = context.record_id;
162
-
163
- // Add custom functions
164
- for (const [name, func] of Object.entries(this.customFunctions)) {
165
- evalContext[name] = func;
166
- }
167
-
168
- return evalContext;
169
- }
170
-
171
- /**
172
- * Execute the expression in a sandboxed environment
173
- *
174
- * SECURITY NOTE: Uses Function constructor for dynamic evaluation.
175
- * While we check for blocked operations, this is not a complete security sandbox.
176
- * For production use with untrusted formulas, consider using a proper sandboxing library
177
- * like vm2 or implementing an AST-based evaluator.
178
- */
179
- private executeExpression(
180
- expression: string,
181
- context: Record<string, any>,
182
- timeout: number,
183
- options: FormulaEvaluationOptions
184
- ): FormulaValue {
185
- // Check for blocked operations
186
- // NOTE: This is a basic check using string matching. It will have false positives
187
- // (e.g., a field named 'evaluation' contains 'eval') and can be bypassed
188
- // (e.g., using this['eval'] or globalThis['eval']).
189
- // For production security with untrusted formulas, use AST parsing or vm2.
190
- if (this.config.sandbox?.enabled) {
191
- const blockedOps = this.config.sandbox.blocked_operations || [];
192
- for (const op of blockedOps) {
193
- if (expression.includes(op)) {
194
- throw new FormulaError(
195
- FormulaErrorType.SECURITY_VIOLATION,
196
- `Blocked operation detected: ${op}`,
197
- expression
198
- );
199
- }
200
- }
201
- }
202
-
203
- try {
204
- // Create function parameters from context keys
205
- const paramNames = Object.keys(context);
206
- const paramValues = Object.values(context);
207
-
208
- // Wrap expression to handle both expression-style and statement-style formulas
209
- const wrappedExpression = this.wrapExpression(expression);
210
-
211
- // Create and execute function
212
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
213
- const func = new Function(...paramNames, wrappedExpression);
214
-
215
- // Execute with timeout protection
216
- const result = this.executeWithTimeout(func, paramValues, timeout);
217
-
218
- return result as FormulaValue;
219
- } catch (error) {
220
- if (error instanceof FormulaError) {
221
- throw error;
222
- }
223
-
224
- // Parse JavaScript errors
225
- const err = error as Error;
226
-
227
- if (error instanceof ReferenceError) {
228
- throw new FormulaError(
229
- FormulaErrorType.FIELD_NOT_FOUND,
230
- `Referenced field not found: ${err.message}`,
231
- expression,
232
- { original_error: err.message }
233
- );
234
- }
235
-
236
- if (error instanceof TypeError) {
237
- throw new FormulaError(
238
- FormulaErrorType.TYPE_ERROR,
239
- `Type error in formula: ${err.message}`,
240
- expression,
241
- { original_error: err.message }
242
- );
243
- }
244
-
245
- if (error instanceof SyntaxError) {
246
- throw new FormulaError(
247
- FormulaErrorType.SYNTAX_ERROR,
248
- `Syntax error in formula: ${err.message}`,
249
- expression,
250
- { original_error: err.message }
251
- );
252
- }
253
-
254
- throw new FormulaError(
255
- FormulaErrorType.RUNTIME_ERROR,
256
- `Runtime error: ${error instanceof Error ? err.message : String(error)}`,
257
- expression,
258
- { original_error: error instanceof Error ? err.message : String(error) }
259
- );
260
- }
261
- }
262
-
263
- /**
264
- * Wrap expression to handle both expression and statement styles
265
- */
266
- private wrapExpression(expression: string): string {
267
- const trimmed = expression.trim();
268
-
269
- // If it contains a return statement or is a block, use as-is
270
- if (trimmed.includes('return ') || (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
271
- return trimmed;
272
- }
273
-
274
- // If it's multi-line with if/else, wrap in a function body
275
- if (trimmed.includes('\n') || trimmed.match(/if\s*\(/)) {
276
- return trimmed;
277
- }
278
-
279
- // Otherwise, treat as expression and return it
280
- return `return (${trimmed});`;
281
- }
282
-
283
- /**
284
- * Execute function with timeout protection
285
- *
286
- * NOTE: This synchronous implementation **cannot** pre-emptively interrupt execution.
287
- * To avoid giving a false sense of safety, any positive finite timeout configuration
288
- * is rejected up-front. Callers must not rely on timeout-based protection in this
289
- * runtime; instead, formulas must be written to be fast and side-effect free.
290
- */
291
- private executeWithTimeout(
292
- func: Function,
293
- args: any[],
294
- timeout: number
295
- ): unknown {
296
- // Reject any positive finite timeout to avoid misleading "protection" semantics.
297
- if (Number.isFinite(timeout) && timeout > 0) {
298
- throw new FormulaError(
299
- FormulaErrorType.TIMEOUT,
300
- 'Formula timeout enforcement is not supported for synchronous execution. ' +
301
- 'Remove the timeout configuration or migrate to an async/isolated runtime ' +
302
- 'that can safely interrupt long-running formulas.',
303
- '',
304
- { requestedTimeoutMs: timeout }
305
- );
306
- }
307
-
308
- // No timeout configured (or non-positive/invalid value): execute directly.
309
- return func(...args);
310
- }
311
-
312
- /**
313
- * Coerce the result value to the expected data type
314
- */
315
- private coerceValue(value: unknown, dataType: FormulaDataType, expression: string): FormulaValue {
316
- // Handle null/undefined
317
- if (value === null || value === undefined) {
318
- return null;
319
- }
320
-
321
- try {
322
- switch (dataType) {
323
- case 'number':
324
- case 'currency':
325
- case 'percent':
326
- if (typeof value === 'number') {
327
- // Check for division by zero result
328
- if (!isFinite(value)) {
329
- throw new FormulaError(
330
- FormulaErrorType.DIVISION_BY_ZERO,
331
- 'Formula resulted in Infinity or NaN (possible division by zero)',
332
- expression
333
- );
334
- }
335
- return value;
336
- }
337
- if (typeof value === 'string') {
338
- const num = Number(value);
339
- if (isNaN(num)) {
340
- throw new FormulaError(
341
- FormulaErrorType.TYPE_ERROR,
342
- `Cannot convert "${value}" to number`,
343
- expression
344
- );
345
- }
346
- return num;
347
- }
348
- if (typeof value === 'boolean') {
349
- return value ? 1 : 0;
350
- }
351
- throw new FormulaError(
352
- FormulaErrorType.TYPE_ERROR,
353
- `Expected number, got ${typeof value}`,
354
- expression
355
- );
356
-
357
- case 'text':
358
- return String(value);
359
-
360
- case 'boolean':
361
- return Boolean(value);
362
-
363
- case 'date':
364
- case 'datetime':
365
- if (value instanceof Date) {
366
- return value;
367
- }
368
- if (typeof value === 'string') {
369
- const date = new Date(value);
370
- if (isNaN(date.getTime())) {
371
- throw new FormulaError(
372
- FormulaErrorType.TYPE_ERROR,
373
- `Cannot convert "${value}" to date`,
374
- expression
375
- );
376
- }
377
- return date;
378
- }
379
- if (typeof value === 'number') {
380
- return new Date(value);
381
- }
382
- throw new FormulaError(
383
- FormulaErrorType.TYPE_ERROR,
384
- `Expected date, got ${typeof value}`,
385
- expression
386
- );
387
-
388
- default:
389
- return value as FormulaValue;
390
- }
391
- } catch (error) {
392
- if (error instanceof FormulaError) {
393
- throw error;
394
- }
395
- throw new FormulaError(
396
- FormulaErrorType.TYPE_ERROR,
397
- `Type coercion failed: ${error instanceof Error ? error.message : String(error)}`,
398
- expression
399
- );
400
- }
401
- }
402
-
403
- /**
404
- * Extract metadata from a formula expression
405
- * Analyzes the expression to determine dependencies and complexity
406
- */
407
- extractMetadata(
408
- fieldName: string,
409
- expression: string,
410
- dataType: FormulaDataType
411
- ): FormulaMetadata {
412
- const dependencies: string[] = [];
413
- const lookupChains: string[] = [];
414
- const systemVariables: string[] = [];
415
- const validationErrors: string[] = [];
416
-
417
- try {
418
- // Extract system variables (start with $)
419
- const systemVarPattern = /\$([a-z_][a-z0-9_]*)/gi;
420
- const systemMatches = Array.from(expression.matchAll(systemVarPattern));
421
- for (const match of systemMatches) {
422
- const sysVar = '$' + match[1];
423
- if (!systemVariables.includes(sysVar)) {
424
- systemVariables.push(sysVar);
425
- }
426
- }
427
-
428
- // Extract field references (but not system variables or keywords)
429
- const fieldPattern = /\b([a-z_][a-z0-9_]*)\b/gi;
430
- const matches = Array.from(expression.matchAll(fieldPattern));
431
-
432
- for (const match of matches) {
433
- const identifier = match[1];
434
-
435
- // Skip JavaScript keywords and built-ins
436
- if (this.isJavaScriptKeyword(identifier)) {
437
- continue;
438
- }
439
-
440
- // Field references
441
- if (!dependencies.includes(identifier)) {
442
- dependencies.push(identifier);
443
- }
444
- }
445
-
446
- // Extract lookup chains (e.g., account.owner.name)
447
- const lookupPattern = /\b([a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)+)\b/gi;
448
- const lookupMatches = Array.from(expression.matchAll(lookupPattern));
449
-
450
- for (const match of lookupMatches) {
451
- const chain = match[1];
452
- if (!lookupChains.includes(chain)) {
453
- lookupChains.push(chain);
454
- }
455
- }
456
-
457
- // Basic validation
458
- if (!expression.trim()) {
459
- validationErrors.push('Expression cannot be empty');
460
- }
461
-
462
- // Estimate complexity
463
- const complexity = this.estimateComplexity(expression);
464
-
465
- return {
466
- field_name: fieldName,
467
- expression,
468
- data_type: dataType,
469
- dependencies,
470
- lookup_chains: lookupChains,
471
- system_variables: systemVariables,
472
- is_valid: validationErrors.length === 0,
473
- validation_errors: validationErrors.length > 0 ? validationErrors : undefined,
474
- complexity,
475
- };
476
- } catch (error) {
477
- return {
478
- field_name: fieldName,
479
- expression,
480
- data_type: dataType,
481
- dependencies: [],
482
- lookup_chains: [],
483
- system_variables: [],
484
- is_valid: false,
485
- validation_errors: [
486
- error instanceof Error ? error.message : String(error),
487
- ],
488
- complexity: 'simple',
489
- };
490
- }
491
- }
492
-
493
- /**
494
- * Check if identifier is a JavaScript keyword or built-in
495
- */
496
- private isJavaScriptKeyword(identifier: string): boolean {
497
- const keywords = new Set([
498
- 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue',
499
- 'return', 'function', 'var', 'let', 'const', 'true', 'false', 'null',
500
- 'undefined', 'this', 'new', 'typeof', 'instanceof', 'in', 'of',
501
- 'Math', 'String', 'Number', 'Boolean', 'Date', 'Array', 'Object',
502
- 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
503
- ]);
504
- return keywords.has(identifier);
505
- }
506
-
507
- /**
508
- * Estimate formula complexity based on heuristics
509
- */
510
- private estimateComplexity(expression: string): 'simple' | 'medium' | 'complex' {
511
- const lines = expression.split('\n').length;
512
- const hasConditionals = /if\s*\(|switch\s*\(|\?/.test(expression);
513
- const hasLoops = /for\s*\(|while\s*\(/.test(expression);
514
- const hasLookups = /\.[a-z_][a-z0-9_]*/.test(expression);
515
-
516
- if (lines > 20 || hasLoops) {
517
- return 'complex';
518
- }
519
-
520
- if (lines > 5 || hasConditionals || hasLookups) {
521
- return 'medium';
522
- }
523
-
524
- return 'simple';
525
- }
526
-
527
- /**
528
- * Register a custom function for use in formulas
529
- */
530
- registerFunction(name: string, func: FormulaCustomFunction): void {
531
- this.customFunctions[name] = func;
532
- }
533
-
534
- /**
535
- * Validate a formula expression without executing it
536
- *
537
- * SECURITY NOTE: Uses Function constructor for syntax validation.
538
- * This doesn't execute the code but creates a function object.
539
- * For stricter validation, consider using a parser library like @babel/parser.
540
- */
541
- validate(expression: string): { valid: boolean; errors: string[] } {
542
- const errors: string[] = [];
543
-
544
- if (!expression || expression.trim() === '') {
545
- errors.push('Expression cannot be empty');
546
- return { valid: false, errors };
547
- }
548
-
549
- // Check for blocked operations
550
- if (this.config.sandbox?.enabled) {
551
- const blockedOps = this.config.sandbox.blocked_operations || [];
552
- for (const op of blockedOps) {
553
- if (expression.includes(op)) {
554
- errors.push(`Blocked operation detected: ${op}`);
555
- }
556
- }
557
- }
558
-
559
- // Try to parse as JavaScript (basic syntax check)
560
- try {
561
- const wrappedExpression = this.wrapExpression(expression);
562
- new Function('', wrappedExpression);
563
- } catch (error) {
564
- errors.push(`Syntax error: ${error instanceof Error ? error.message : String(error)}`);
565
- }
566
-
567
- return {
568
- valid: errors.length === 0,
569
- errors,
570
- };
571
- }
572
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * ObjectQL Formula Plugin
3
- * Copyright (c) 2026-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import type { RuntimePlugin, RuntimeContext, ObjectStackKernel } from '@objectql/runtime';
10
- import { FormulaEngine } from './formula-engine';
11
- import type { FormulaEngineConfig } from '@objectql/types';
12
-
13
- /**
14
- * Extended ObjectStack Kernel with formula engine capability
15
- */
16
- interface KernelWithFormulas extends ObjectStackKernel {
17
- formulaEngine?: FormulaEngine;
18
- }
19
-
20
- /**
21
- * Configuration for the Formula Plugin
22
- */
23
- export interface FormulaPluginConfig extends FormulaEngineConfig {
24
- /**
25
- * Enable automatic formula evaluation on queries
26
- * @default true
27
- */
28
- autoEvaluateOnQuery?: boolean;
29
- }
30
-
31
- /**
32
- * Formula Plugin
33
- *
34
- * Wraps the ObjectQL Formula Engine as an ObjectStack plugin.
35
- * Registers formula evaluation capabilities into the kernel.
36
- */
37
- export class FormulaPlugin implements RuntimePlugin {
38
- name = '@objectql/formulas';
39
- version = '4.0.0';
40
-
41
- private engine: FormulaEngine;
42
- private config: FormulaPluginConfig;
43
-
44
- constructor(config: FormulaPluginConfig = {}) {
45
- this.config = {
46
- autoEvaluateOnQuery: true,
47
- ...config
48
- };
49
-
50
- // Initialize the formula engine with configuration
51
- this.engine = new FormulaEngine(config);
52
- }
53
-
54
- /**
55
- * Install the plugin into the kernel
56
- * Registers formula evaluation capabilities
57
- */
58
- async install(ctx: RuntimeContext): Promise<void> {
59
- const kernel = ctx.engine as KernelWithFormulas;
60
-
61
- console.log(`[${this.name}] Installing formula plugin...`);
62
-
63
- // Make formula engine accessible from the kernel for direct usage
64
- kernel.formulaEngine = this.engine;
65
-
66
- // Register formula provider if the kernel supports it
67
- this.registerFormulaProvider(kernel);
68
-
69
- // Register formula evaluation middleware if auto-evaluation is enabled
70
- if (this.config.autoEvaluateOnQuery !== false) {
71
- this.registerFormulaMiddleware(kernel);
72
- }
73
-
74
- console.log(`[${this.name}] Formula plugin installed`);
75
- }
76
-
77
- /**
78
- * Register the formula provider with the kernel
79
- * @private
80
- */
81
- private registerFormulaProvider(kernel: KernelWithFormulas): void {
82
- // Check if kernel supports formula provider registration
83
- if (typeof (kernel as any).registerFormulaProvider === 'function') {
84
- (kernel as any).registerFormulaProvider({
85
- evaluate: (formula: string, context: any) => {
86
- // Delegate to the formula engine
87
- // Note: In a real implementation, we would need to properly construct
88
- // the FormulaContext from the provided context
89
- return this.engine.evaluate(
90
- formula,
91
- context,
92
- 'text', // default data type
93
- {}
94
- );
95
- },
96
- validate: (expression: string) => {
97
- return this.engine.validate(expression);
98
- },
99
- extractMetadata: (fieldName: string, expression: string, dataType: any) => {
100
- return this.engine.extractMetadata(fieldName, expression, dataType);
101
- }
102
- });
103
- }
104
- // Note: formulaEngine is already registered in install() method above
105
- }
106
-
107
- /**
108
- * Register formula evaluation middleware
109
- * @private
110
- */
111
- private registerFormulaMiddleware(kernel: KernelWithFormulas): void {
112
- // Check if kernel supports middleware hooks
113
- if (typeof (kernel as any).use === 'function') {
114
- // Register middleware to evaluate formulas after queries
115
- (kernel as any).use('afterQuery', async (context: any) => {
116
- // Formula evaluation logic would go here
117
- // This would automatically compute formula fields after data is retrieved
118
- if (context.results && context.metadata?.fields) {
119
- // Iterate through fields and evaluate formulas
120
- // const formulaFields = Object.entries(context.metadata.fields)
121
- // .filter(([_, fieldConfig]) => (fieldConfig as any).formula);
122
- //
123
- // for (const record of context.results) {
124
- // for (const [fieldName, fieldConfig] of formulaFields) {
125
- // const formula = (fieldConfig as any).formula;
126
- // const result = this.engine.evaluate(formula, /* context */, /* dataType */);
127
- // record[fieldName] = result.value;
128
- // }
129
- // }
130
- }
131
- });
132
- }
133
- }
134
-
135
- /**
136
- * Get the formula engine instance for direct access
137
- */
138
- getEngine(): FormulaEngine {
139
- return this.engine;
140
- }
141
- }