mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Replace eval bindings with JSON expression evaluator
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import * as Icons from '@phosphor-icons/react'
|
||||
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||
|
||||
export function resolveBinding(binding: string, data: Record<string, any>): any {
|
||||
try {
|
||||
const func = new Function(...Object.keys(data), `return ${binding}`)
|
||||
return func(...Object.values(data))
|
||||
} catch {
|
||||
return binding
|
||||
}
|
||||
return evaluateBindingExpression(binding, data, {
|
||||
fallback: binding,
|
||||
label: 'json-page-renderer binding',
|
||||
})
|
||||
}
|
||||
|
||||
export function getIcon(iconName: string, props?: any) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ComponentSchema as ComponentSchemaType } from '@/types/page-schema'
|
||||
import { getUIComponent } from '@/lib/json-ui/component-registry'
|
||||
import { evaluateConditionExpression, evaluateTransformExpression } from '@/lib/json-ui/expression-helpers'
|
||||
|
||||
interface ComponentRendererProps {
|
||||
schema: ComponentSchemaType
|
||||
@@ -17,13 +18,10 @@ export function ComponentRenderer({ schema, context, onEvent }: ComponentRendere
|
||||
}
|
||||
|
||||
if (schema.condition) {
|
||||
try {
|
||||
const conditionFn = new Function('context', `return ${schema.condition}`)
|
||||
if (!conditionFn(context)) {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Condition evaluation failed for ${schema.id}:`, error)
|
||||
const conditionMet = evaluateConditionExpression(schema.condition, context, {
|
||||
label: `component condition (${schema.id})`,
|
||||
})
|
||||
if (!conditionMet) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -34,13 +32,10 @@ export function ComponentRenderer({ schema, context, onEvent }: ComponentRendere
|
||||
schema.bindings.forEach(binding => {
|
||||
const value = getNestedValue(context, binding.source)
|
||||
if (binding.transform) {
|
||||
try {
|
||||
const transformFn = new Function('value', 'context', `return ${binding.transform}`)
|
||||
props[binding.target] = transformFn(value, context)
|
||||
} catch (error) {
|
||||
console.error(`Transform failed for ${binding.target}:`, error)
|
||||
props[binding.target] = value
|
||||
}
|
||||
props[binding.target] = evaluateTransformExpression(binding.transform, value, context, {
|
||||
fallback: value,
|
||||
label: `binding transform (${binding.target})`,
|
||||
})
|
||||
} else {
|
||||
props[binding.target] = value
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useMemo } from 'react'
|
||||
import { ComponentSchema } from '@/types/json-ui'
|
||||
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||
|
||||
export function useJSONRenderer() {
|
||||
const resolveBinding = useMemo(() => {
|
||||
return (binding: string, data: Record<string, any>): any => {
|
||||
if (!binding) return undefined
|
||||
|
||||
try {
|
||||
const func = new Function(...Object.keys(data), `return ${binding}`)
|
||||
return func(...Object.values(data))
|
||||
} catch {
|
||||
return binding
|
||||
}
|
||||
return evaluateBindingExpression(binding, data, {
|
||||
fallback: binding,
|
||||
label: 'json renderer binding',
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { PageSchema, DataSource } from '@/types/page-schema'
|
||||
import { PageSchema } from '@/types/page-schema'
|
||||
import { useFiles } from '../data/use-files'
|
||||
import { useModels } from '../data/use-models'
|
||||
import { useComponents } from '../data/use-components'
|
||||
import { useWorkflows } from '../data/use-workflows'
|
||||
import { useLambdas } from '../data/use-lambdas'
|
||||
import { useActions } from './use-actions'
|
||||
import { evaluateBindingExpression } from '@/lib/json-ui/expression-helpers'
|
||||
|
||||
export function usePage(schema: PageSchema) {
|
||||
const files = useFiles()
|
||||
@@ -46,12 +47,10 @@ export function usePage(schema: PageSchema) {
|
||||
|
||||
schema.data.forEach(source => {
|
||||
if (source.type === 'computed' && source.compute) {
|
||||
try {
|
||||
const computeFn = new Function('context', `return ${source.compute}`)
|
||||
computed[source.id] = computeFn(dataContext)
|
||||
} catch (error) {
|
||||
console.error(`Failed to compute ${source.id}:`, error)
|
||||
}
|
||||
computed[source.id] = evaluateBindingExpression(source.compute, dataContext, {
|
||||
fallback: undefined,
|
||||
label: `computed data (${source.id})`,
|
||||
})
|
||||
} else if (source.type === 'static' && source.defaultValue !== undefined) {
|
||||
computed[source.id] = source.defaultValue
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createElement, useMemo, Fragment } from 'react'
|
||||
import { UIComponent, Binding, ComponentRendererProps, EventHandler, JSONEventDefinition, JSONEventMap } from '@/types/json-ui'
|
||||
import { getUIComponent } from './component-registry'
|
||||
import { resolveDataBinding, evaluateCondition } from './utils'
|
||||
import { resolveDataBinding } from './utils'
|
||||
import { evaluateConditionExpression } from './expression-helpers'
|
||||
|
||||
function resolveBinding(
|
||||
binding: Binding,
|
||||
@@ -100,7 +101,7 @@ export function ComponentRenderer({ component, data, context = {}, state, onEven
|
||||
const conditionMet = !handler.condition
|
||||
|| (typeof handler.condition === 'function'
|
||||
? handler.condition(mergedData as Record<string, any>)
|
||||
: evaluateCondition(handler.condition, mergedData as Record<string, any>))
|
||||
: evaluateConditionExpression(handler.condition, mergedData as Record<string, any>, { label: 'event handler condition' }))
|
||||
if (conditionMet) {
|
||||
const eventPayload = typeof e === 'object' && e !== null
|
||||
? Object.assign(e as Record<string, unknown>, context)
|
||||
@@ -195,7 +196,7 @@ export function ComponentRenderer({ component, data, context = {}, state, onEven
|
||||
|
||||
const renderConditionalContent = (renderContext: Record<string, unknown>) => {
|
||||
if (!component.conditional) return undefined
|
||||
const conditionMet = evaluateCondition(component.conditional.if, { ...data, ...renderContext } as Record<string, any>)
|
||||
const conditionMet = evaluateConditionExpression(component.conditional.if, { ...data, ...renderContext } as Record<string, any>, { label: `component conditional (${component.id})` })
|
||||
if (conditionMet) {
|
||||
if (component.conditional.then !== undefined) {
|
||||
return renderBranch(component.conditional.then as UIComponent | (UIComponent | string)[] | string, renderContext)
|
||||
|
||||
122
src/lib/json-ui/expression-helpers.ts
Normal file
122
src/lib/json-ui/expression-helpers.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { evaluateCondition, evaluateExpression } from './expression-evaluator'
|
||||
|
||||
const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
||||
const NUMBER_PATTERN = /^-?\d+(?:\.\d+)?$/
|
||||
const STRING_PATTERN = /^(['"]).*\1$/
|
||||
|
||||
interface EvaluationOptions {
|
||||
fallback?: any
|
||||
label?: string
|
||||
event?: any
|
||||
}
|
||||
|
||||
const isSupportedExpression = (expression: string) => {
|
||||
if (expression === 'event' || expression === 'data') return true
|
||||
if (expression.startsWith('data.') || expression.startsWith('event.')) return true
|
||||
if (expression === 'Date.now()') return true
|
||||
if (STRING_PATTERN.test(expression)) return true
|
||||
if (NUMBER_PATTERN.test(expression)) return true
|
||||
if (['true', 'false', 'null', 'undefined'].includes(expression)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const normalizeExpression = (expression: string, coerceIdentifier = true) => {
|
||||
const trimmed = expression.trim()
|
||||
if (coerceIdentifier && IDENTIFIER_PATTERN.test(trimmed) && trimmed !== 'data' && trimmed !== 'event') {
|
||||
return `data.${trimmed}`
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const isSupportedCondition = (condition: string) => {
|
||||
return (
|
||||
/^data\.[a-zA-Z0-9_.]+\s*>\s*.+$/.test(condition)
|
||||
|| /^data\.[a-zA-Z0-9_.]+\.length\s*>\s*.+$/.test(condition)
|
||||
|| /^data\.[a-zA-Z0-9_.]+\s*===\s*['"].+['"]$/.test(condition)
|
||||
|| /^data\.[a-zA-Z0-9_.]+\s*!=\s*null$/.test(condition)
|
||||
)
|
||||
}
|
||||
|
||||
const normalizeCondition = (condition: string) => {
|
||||
const trimmed = condition.trim()
|
||||
if (trimmed.startsWith('data.') || trimmed.startsWith('event.')) {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const lengthMatch = trimmed.match(/^([a-zA-Z0-9_.]+)\.length\s*>\s*(.+)$/)
|
||||
if (lengthMatch) {
|
||||
return `data.${lengthMatch[1]}.length > ${lengthMatch[2]}`
|
||||
}
|
||||
|
||||
const gtMatch = trimmed.match(/^([a-zA-Z0-9_.]+)\s*>\s*(.+)$/)
|
||||
if (gtMatch) {
|
||||
return `data.${gtMatch[1]} > ${gtMatch[2]}`
|
||||
}
|
||||
|
||||
const eqMatch = trimmed.match(/^([a-zA-Z0-9_.]+)\s*===\s*(['"].+['"])$/)
|
||||
if (eqMatch) {
|
||||
return `data.${eqMatch[1]} === ${eqMatch[2]}`
|
||||
}
|
||||
|
||||
const nullMatch = trimmed.match(/^([a-zA-Z0-9_.]+)\s*!=\s*null$/)
|
||||
if (nullMatch) {
|
||||
return `data.${nullMatch[1]} != null`
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const warnUnsupported = (kind: string, expression: string, label?: string) => {
|
||||
const labelText = label ? ` for ${label}` : ''
|
||||
console.warn(`[json-ui] Unsupported ${kind} expression${labelText}: "${expression}"`)
|
||||
}
|
||||
|
||||
export const evaluateBindingExpression = (
|
||||
expression: string | undefined,
|
||||
data: Record<string, any>,
|
||||
options: EvaluationOptions = {}
|
||||
) => {
|
||||
if (!expression) return undefined
|
||||
const normalized = normalizeExpression(expression)
|
||||
if (!isSupportedExpression(normalized)) {
|
||||
warnUnsupported('binding', expression, options.label)
|
||||
return options.fallback ?? expression
|
||||
}
|
||||
return evaluateExpression(normalized, { data, event: options.event })
|
||||
}
|
||||
|
||||
export const evaluateTransformExpression = (
|
||||
expression: string | undefined,
|
||||
value: any,
|
||||
dataContext: Record<string, any> = {},
|
||||
options: EvaluationOptions = {}
|
||||
) => {
|
||||
if (!expression) return value
|
||||
const normalized = normalizeExpression(expression)
|
||||
if (!isSupportedExpression(normalized)) {
|
||||
warnUnsupported('transform', expression, options.label)
|
||||
return options.fallback ?? value
|
||||
}
|
||||
|
||||
const valueContext = typeof value === 'object' && value !== null ? value : {}
|
||||
const mergedData = {
|
||||
...dataContext,
|
||||
...valueContext,
|
||||
value,
|
||||
}
|
||||
|
||||
return evaluateExpression(normalized, { data: mergedData, event: options.event })
|
||||
}
|
||||
|
||||
export const evaluateConditionExpression = (
|
||||
condition: string | undefined,
|
||||
data: Record<string, any>,
|
||||
options: EvaluationOptions = {}
|
||||
) => {
|
||||
if (!condition) return true
|
||||
const normalized = normalizeCondition(condition)
|
||||
if (!isSupportedCondition(normalized)) {
|
||||
warnUnsupported('condition', condition, options.label)
|
||||
}
|
||||
return evaluateCondition(normalized, { data, event: options.event })
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import type {
|
||||
UIComponent,
|
||||
} from './types'
|
||||
import { getUIComponent } from './component-registry'
|
||||
import { resolveDataBinding, evaluateCondition } from './utils'
|
||||
import { resolveDataBinding } from './utils'
|
||||
import { evaluateConditionExpression } from './expression-helpers'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function JSONUIRenderer({
|
||||
@@ -154,7 +155,7 @@ export function JSONUIRenderer({
|
||||
if (handler.condition) {
|
||||
const conditionMet = typeof handler.condition === 'function'
|
||||
? handler.condition({ ...dataMap, ...renderContext })
|
||||
: evaluateCondition(handler.condition, { ...dataMap, ...renderContext })
|
||||
: evaluateConditionExpression(handler.condition, { ...dataMap, ...renderContext }, { label: 'event handler condition' })
|
||||
if (!conditionMet) return
|
||||
}
|
||||
const eventPayload = typeof event === 'object' && event !== null
|
||||
@@ -196,7 +197,7 @@ export function JSONUIRenderer({
|
||||
|
||||
const renderWithContext = (renderContext: Record<string, unknown>) => {
|
||||
if (component.conditional) {
|
||||
const conditionMet = evaluateCondition(component.conditional.if, { ...dataMap, ...renderContext })
|
||||
const conditionMet = evaluateConditionExpression(component.conditional.if, { ...dataMap, ...renderContext }, { label: `component conditional (${component.id})` })
|
||||
if (conditionMet) {
|
||||
if (component.conditional.then !== undefined) {
|
||||
return renderConditionalBranch(
|
||||
@@ -258,7 +259,7 @@ export function JSONUIRenderer({
|
||||
let content = renderChildren(component.children, loopContext)
|
||||
|
||||
if (component.conditional) {
|
||||
const conditionMet = evaluateCondition(component.conditional.if, { ...dataMap, ...loopContext })
|
||||
const conditionMet = evaluateConditionExpression(component.conditional.if, { ...dataMap, ...loopContext }, { label: `loop conditional (${component.id})` })
|
||||
if (conditionMet) {
|
||||
if (component.conditional.then !== undefined) {
|
||||
content = renderConditionalBranch(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { evaluateTransformExpression } from './expression-helpers'
|
||||
|
||||
type BindingTransform = string | ((data: unknown) => unknown)
|
||||
|
||||
interface BindingSourceOptions {
|
||||
@@ -52,7 +54,7 @@ function applyTransform(value: unknown, transform?: BindingTransform) {
|
||||
return transform(value)
|
||||
}
|
||||
|
||||
return transformData(value, transform)
|
||||
return evaluateTransformExpression(transform, value, {}, { label: 'data binding transform' })
|
||||
}
|
||||
|
||||
export function getNestedValue(obj: any, path: string): any {
|
||||
@@ -76,26 +78,6 @@ export function setNestedValue(obj: any, path: string, value: any): any {
|
||||
return obj
|
||||
}
|
||||
|
||||
export function evaluateCondition(condition: string, context: Record<string, any>): boolean {
|
||||
try {
|
||||
const conditionFn = new Function(...Object.keys(context), `return ${condition}`)
|
||||
return Boolean(conditionFn(...Object.values(context)))
|
||||
} catch (err) {
|
||||
console.warn('Failed to evaluate condition:', condition, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function transformData(data: any, transformFn: string): any {
|
||||
try {
|
||||
const fn = new Function('data', `return ${transformFn}`)
|
||||
return fn(data)
|
||||
} catch (err) {
|
||||
console.warn('Failed to transform data:', err)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeClassNames(...classes: (string | undefined | null | false)[]): string {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user