@objectql/core 4.0.1 → 4.0.3
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +32 -0
- package/README.md +14 -12
- package/dist/app.d.ts +9 -6
- package/dist/app.js +151 -29
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.js +2 -5
- package/dist/index.js.map +1 -1
- package/dist/optimizations/CompiledHookManager.d.ts +55 -0
- package/dist/optimizations/CompiledHookManager.js +164 -0
- package/dist/optimizations/CompiledHookManager.js.map +1 -0
- package/dist/optimizations/DependencyGraph.d.ts +82 -0
- package/dist/optimizations/DependencyGraph.js +211 -0
- package/dist/optimizations/DependencyGraph.js.map +1 -0
- package/dist/optimizations/GlobalConnectionPool.d.ts +89 -0
- package/dist/optimizations/GlobalConnectionPool.js +193 -0
- package/dist/optimizations/GlobalConnectionPool.js.map +1 -0
- package/dist/optimizations/LazyMetadataLoader.d.ts +75 -0
- package/dist/optimizations/LazyMetadataLoader.js +149 -0
- package/dist/optimizations/LazyMetadataLoader.js.map +1 -0
- package/dist/optimizations/OptimizedMetadataRegistry.d.ts +26 -0
- package/dist/optimizations/OptimizedMetadataRegistry.js +117 -0
- package/dist/optimizations/OptimizedMetadataRegistry.js.map +1 -0
- package/dist/optimizations/OptimizedValidationEngine.d.ts +73 -0
- package/dist/optimizations/OptimizedValidationEngine.js +141 -0
- package/dist/optimizations/OptimizedValidationEngine.js.map +1 -0
- package/dist/optimizations/QueryCompiler.d.ts +51 -0
- package/dist/optimizations/QueryCompiler.js +216 -0
- package/dist/optimizations/QueryCompiler.js.map +1 -0
- package/dist/optimizations/SQLQueryOptimizer.d.ts +96 -0
- package/dist/optimizations/SQLQueryOptimizer.js +265 -0
- package/dist/optimizations/SQLQueryOptimizer.js.map +1 -0
- package/dist/optimizations/index.d.ts +32 -0
- package/dist/optimizations/index.js +44 -0
- package/dist/optimizations/index.js.map +1 -0
- package/dist/plugin.d.ts +6 -7
- package/dist/plugin.js +39 -22
- package/dist/plugin.js.map +1 -1
- package/dist/query/filter-translator.d.ts +6 -18
- package/dist/query/filter-translator.js +6 -103
- package/dist/query/filter-translator.js.map +1 -1
- package/dist/query/query-analyzer.js +24 -25
- package/dist/query/query-analyzer.js.map +1 -1
- package/dist/query/query-builder.d.ts +9 -3
- package/dist/query/query-builder.js +25 -35
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-service.d.ts +2 -2
- package/dist/query/query-service.js +5 -5
- package/dist/query/query-service.js.map +1 -1
- package/dist/repository.d.ts +2 -0
- package/dist/repository.js +24 -17
- package/dist/repository.js.map +1 -1
- package/jest.config.js +3 -3
- package/package.json +8 -5
- package/src/app.ts +173 -47
- package/src/index.ts +7 -8
- package/src/optimizations/CompiledHookManager.ts +185 -0
- package/src/optimizations/DependencyGraph.ts +255 -0
- package/src/optimizations/GlobalConnectionPool.ts +251 -0
- package/src/optimizations/LazyMetadataLoader.ts +180 -0
- package/src/optimizations/OptimizedMetadataRegistry.ts +132 -0
- package/src/optimizations/OptimizedValidationEngine.ts +172 -0
- package/src/optimizations/QueryCompiler.ts +242 -0
- package/src/optimizations/SQLQueryOptimizer.ts +329 -0
- package/src/optimizations/index.ts +34 -0
- package/src/plugin.ts +51 -28
- package/src/query/filter-translator.ts +8 -115
- package/src/query/query-analyzer.ts +25 -29
- package/src/query/query-builder.ts +26 -43
- package/src/query/query-service.ts +6 -6
- package/src/repository.ts +35 -22
- package/test/__mocks__/@objectstack/runtime.ts +8 -8
- package/test/app.test.ts +11 -8
- package/test/optimizations.test.ts +440 -0
- package/test/plugin-integration.test.ts +30 -19
- package/tsconfig.json +4 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/ai-agent.d.ts +0 -176
- package/dist/ai-agent.js +0 -722
- package/dist/ai-agent.js.map +0 -1
- package/dist/formula-engine.d.ts +0 -102
- package/dist/formula-engine.js +0 -433
- package/dist/formula-engine.js.map +0 -1
- package/dist/formula-plugin.d.ts +0 -52
- package/dist/formula-plugin.js +0 -107
- package/dist/formula-plugin.js.map +0 -1
- package/dist/validator-plugin.d.ts +0 -56
- package/dist/validator-plugin.js +0 -106
- package/dist/validator-plugin.js.map +0 -1
- package/dist/validator.d.ts +0 -80
- package/dist/validator.js +0 -625
- package/dist/validator.js.map +0 -1
- package/src/ai-agent.ts +0 -868
- package/src/formula-engine.ts +0 -572
- package/src/formula-plugin.ts +0 -141
- package/src/validator-plugin.ts +0 -140
- package/src/validator.ts +0 -743
- package/test/formula-engine.test.ts +0 -725
- package/test/formula-integration.test.ts +0 -286
- package/test/formula-plugin.test.ts +0 -197
- package/test/validator-plugin.test.ts +0 -126
- package/test/validator.test.ts +0 -440
package/src/formula-engine.ts
DELETED
|
@@ -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
|
-
}
|
package/src/formula-plugin.ts
DELETED
|
@@ -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
|
-
}
|