Files
low-code-react-app-b/src/lib/json-ui/expression-evaluator.ts
2026-01-17 22:53:54 +00:00

193 lines
5.0 KiB
TypeScript

/**
* JSON-friendly expression evaluator
* Safely evaluates simple expressions without requiring external functions
*/
interface EvaluationContext {
data: Record<string, any>
event?: any
}
/**
* Safely evaluate a JSON expression
* Supports:
* - Data access: "data.fieldName", "data.user.name"
* - Event access: "event.target.value", "event.key"
* - Literals: numbers, strings, booleans, null
* - Date operations: "Date.now()"
* - Basic operations: trim(), toLowerCase(), toUpperCase()
*/
export function evaluateExpression(
expression: string | undefined,
context: EvaluationContext
): any {
if (!expression) return undefined
const { data, event } = context
try {
// Handle direct data access: "data.fieldName"
if (expression.startsWith('data.')) {
return getNestedValue(data, expression.substring(5))
}
// Handle event access: "event.target.value"
if (expression.startsWith('event.')) {
return getNestedValue(event, expression.substring(6))
}
// Handle Date.now()
if (expression === 'Date.now()') {
return Date.now()
}
// Handle string literals
if (expression.startsWith('"') && expression.endsWith('"')) {
return expression.slice(1, -1)
}
if (expression.startsWith("'") && expression.endsWith("'")) {
return expression.slice(1, -1)
}
// Handle numbers
const num = Number(expression)
if (!isNaN(num)) {
return num
}
// Handle booleans
if (expression === 'true') return true
if (expression === 'false') return false
if (expression === 'null') return null
if (expression === 'undefined') return undefined
// If no pattern matched, return the expression as-is
console.warn(`Expression "${expression}" could not be evaluated, returning as-is`)
return expression
} catch (error) {
console.error(`Failed to evaluate expression "${expression}":`, error)
return undefined
}
}
/**
* Get nested value from object using dot notation
* Example: getNestedValue({ user: { name: 'John' } }, 'user.name') => 'John'
*/
function getNestedValue(obj: any, path: string): any {
if (!obj || !path) return undefined
const parts = path.split('.')
let current = obj
for (const part of parts) {
if (current == null) return undefined
current = current[part]
}
return current
}
/**
* Apply string operation to a value
* Supports: trim, toLowerCase, toUpperCase, length
*/
export function applyStringOperation(value: any, operation: string): any {
if (value == null) return value
const str = String(value)
switch (operation) {
case 'trim':
return str.trim()
case 'toLowerCase':
return str.toLowerCase()
case 'toUpperCase':
return str.toUpperCase()
case 'length':
return str.length
default:
console.warn(`Unknown string operation: ${operation}`)
return value
}
}
/**
* Evaluate a template object with dynamic values
* Example: { "id": "Date.now()", "text": "data.newTodo" }
*/
export function evaluateTemplate(
template: Record<string, any>,
context: EvaluationContext
): Record<string, any> {
const result: Record<string, any> = {}
for (const [key, value] of Object.entries(template)) {
if (typeof value === 'string') {
result[key] = evaluateExpression(value, context)
} else {
result[key] = value
}
}
return result
}
/**
* Evaluate a condition expression
* Supports:
* - "data.field > 0"
* - "data.field.length > 0"
* - "data.field === 'value'"
* - "data.field != null"
*/
export function evaluateCondition(
condition: string | undefined,
context: EvaluationContext
): boolean {
if (!condition) return true
const { data } = context
try {
// Simple pattern matching for common conditions
// "data.field > 0"
const gtMatch = condition.match(/^data\.([a-zA-Z0-9_.]+)\s*>\s*(.+)$/)
if (gtMatch) {
const value = getNestedValue(data, gtMatch[1])
const threshold = Number(gtMatch[2])
return (value ?? 0) > threshold
}
// "data.field.length > 0"
const lengthMatch = condition.match(/^data\.([a-zA-Z0-9_.]+)\.length\s*>\s*(.+)$/)
if (lengthMatch) {
const value = getNestedValue(data, lengthMatch[1])
const threshold = Number(lengthMatch[2])
const length = value?.length ?? 0
return length > threshold
}
// "data.field === 'value'"
const eqMatch = condition.match(/^data\.([a-zA-Z0-9_.]+)\s*===\s*['"](.+)['"]$/)
if (eqMatch) {
const value = getNestedValue(data, eqMatch[1])
return value === eqMatch[2]
}
// "data.field != null"
const nullCheck = condition.match(/^data\.([a-zA-Z0-9_.]+)\s*!=\s*null$/)
if (nullCheck) {
const value = getNestedValue(data, nullCheck[1])
return value != null
}
// If no pattern matched, log warning and return true (fail open)
console.warn(`Condition "${condition}" could not be evaluated, defaulting to true`)
return true
} catch (error) {
console.error(`Failed to evaluate condition "${condition}":`, error)
return true // Fail open
}
}