From 388a59d06d4dd6aa3dd46dc8ae3466fe96b1ad84 Mon Sep 17 00:00:00 2001 From: hsm-lv <80095014+hsm-lv@users.noreply.github.com> Date: Tue, 21 Mar 2023 19:35:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=94=AF=E6=8C=81condition-builder?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E8=BF=90=E7=AE=97=20(#6430)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat:支持condition-builder条件运算 * feat:支持condition-builder条件运算 * feat:支持condition-builder条件运算 * feat:支持condition-builder条件运算 * feat:支持condition-builder条件运算 * feat:支持condition-builder条件运算 * feat:支持condition-builder条件运算 * feat:支持condition-builder条件运算 --- docs/zh-CN/concepts/event-action.md | 61 +- .../amis-core/__tests__/condition.test.ts | 506 ++++++++++++++ packages/amis-core/src/actions/Action.ts | 39 +- .../amis-core/src/actions/SwitchAction.ts | 11 +- packages/amis-core/src/index.tsx | 4 + packages/amis-core/src/types.ts | 67 ++ packages/amis-core/src/utils/index.ts | 2 + .../amis-core/src/utils/resolveCondition.ts | 341 +++++++++ .../utils/resolveVariableAndFilterForAsync.ts | 30 + packages/amis-core/src/utils/tpl.ts | 21 +- .../__tests__/async-evalute.test.ts | 589 ++++++++++++++++ .../__tests__/async-fomula.test.ts | 390 +++++++++++ packages/amis-formula/src/evalutor.ts | 8 +- packages/amis-formula/src/evalutorForAsync.ts | 659 ++++++++++++++++++ packages/amis-formula/src/index.ts | 16 + .../condition-builder/Expression.tsx | 8 +- .../src/components/condition-builder/Func.tsx | 2 +- .../components/condition-builder/Group.tsx | 9 +- .../condition-builder/GroupOrItem.tsx | 8 +- .../src/components/condition-builder/Item.tsx | 16 +- .../components/condition-builder/Value.tsx | 3 +- .../components/condition-builder/config.ts | 2 +- .../components/condition-builder/index.tsx | 7 +- .../src/components/condition-builder/types.ts | 69 +- 24 files changed, 2735 insertions(+), 133 deletions(-) create mode 100644 packages/amis-core/__tests__/condition.test.ts create mode 100644 packages/amis-core/src/utils/resolveCondition.ts create mode 100644 packages/amis-core/src/utils/resolveVariableAndFilterForAsync.ts create mode 100644 packages/amis-formula/__tests__/async-evalute.test.ts create mode 100644 packages/amis-formula/__tests__/async-fomula.test.ts create mode 100644 packages/amis-formula/src/evalutorForAsync.ts diff --git a/docs/zh-CN/concepts/event-action.md b/docs/zh-CN/concepts/event-action.md index c2e96590e..76d7fe531 100644 --- a/docs/zh-CN/concepts/event-action.md +++ b/docs/zh-CN/concepts/event-action.md @@ -2062,7 +2062,7 @@ registerAction('my-action', new MyAction()); ## 条件 -通过配置`expression: 表达式`来实现条件逻辑。 +通过配置`expression: 表达式或ConditionBuilder组合条件`来实现条件逻辑。 ```schema { @@ -2071,7 +2071,15 @@ registerAction('my-action', new MyAction()); type: 'form', wrapWithPanel: false, data: { - expression: 'okk' + expression: 'okk', + name: 'amis', + features: ['flexible', 'powerful'], + tool: 'amis-editor', + platform: 'aisuda', + detail: { + version: '2.8.0', + github: 'https://github.com/baidu/amis' + } }, body: [ { @@ -2085,7 +2093,7 @@ registerAction('my-action', new MyAction()); actionType: 'toast', args: { msgType: 'success', - msg: '我okk~' + msg: 'expression表达式 ok~' }, expression: 'expression === "okk"' }, @@ -2100,9 +2108,32 @@ registerAction('my-action', new MyAction()); actionType: 'toast', args: { msgType: 'success', - msg: '我也okk~' + msg: 'conditin-builder条件组合 也ok~' }, - expression: 'expression === "okk"' + expression: { + id: 'b6434ead40cc', + conjunction: 'and', + children: [ + { + id: 'e92b93840f37', + left: { + type: 'field', + field: 'name' + }, + op: 'equal', + right: 'amis' + }, + { + id: '3779845521db', + left: { + type: 'field', + field: 'features' + }, + op: 'select_any_in', + right: '${[LAST(features)]}' + } + ] + } } ] } @@ -2825,13 +2856,13 @@ http 请求动作执行结束后,后面的动作可以通过 `${responseResult # 属性表 -| 属性名 | 类型 | 默认值 | 说明 | -| --------------- | ------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- | -| actionType | `string` | - | 动作名称 | -| args | `object` | - | 动作属性`{key:value}`,支持数据映射 | -| data | `object` | - | 追加数据`{key:value}`,支持数据映射,如果是触发其他组件的动作,则该数据会传递给目标组件,`> 2.3.2 及以上版本` | -| dataMergeMode | `string` | 'merge' | 当配置了 data 的时候,可以控制数据追加方式,支持合并(`merge`)和覆盖(`override`)两种模式,`> 2.3.2 及以上版本` | -| preventDefault | `boolean`\|[表达式](../concepts/expression) | false | 阻止事件默认行为,`> 1.10.0 及以上版本支持表达式` | -| stopPropagation | `boolean`\|[表达式](../concepts/expression) | false | 停止后续动作执行,`> 1.10.0 及以上版本支持表达式` | -| expression | `boolean`\|[表达式](../concepts/expression) | - | 执行条件,不设置表示默认执行 | -| outputVar | `string` | - | 输出数据变量名 | +| 属性名 | 类型 | 默认值 | 说明 | +| --------------- | -------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- | +| actionType | `string` | - | 动作名称 | +| args | `object` | - | 动作属性`{key:value}`,支持数据映射 | +| data | `object` | - | 追加数据`{key:value}`,支持数据映射,如果是触发其他组件的动作,则该数据会传递给目标组件,`> 2.3.2 及以上版本` | +| dataMergeMode | `string` | 'merge' | 当配置了 data 的时候,可以控制数据追加方式,支持合并(`merge`)和覆盖(`override`)两种模式,`> 2.3.2 及以上版本` | +| preventDefault | `boolean`\|[表达式](../concepts/expression)\|[ConditionBuilder](../../components/form/condition-builder) | false | 阻止事件默认行为,`> 1.10.0 及以上版本支持表达式,> 2.9.0 及以上版本支持ConditionBuilder` | +| stopPropagation | `boolean`\|[表达式](../concepts/expression)\|[ConditionBuilder](../../components/form/condition-builder) | false | 停止后续动作执行,`> 1.10.0 及以上版本支持表达式,> 2.9.0 及以上版本支持ConditionBuilder` | +| expression | `boolean`\|[表达式](../concepts/expression)\|[ConditionBuilder](../../components/form/condition-builder) | - | 执行条件,不设置表示默认执行,`> 1.10.0 及以上版本支持表达式,> 2.9.0 及以上版本支持ConditionBuilder` | +| outputVar | `string` | - | 输出数据变量名 | diff --git a/packages/amis-core/__tests__/condition.test.ts b/packages/amis-core/__tests__/condition.test.ts new file mode 100644 index 000000000..6c4991d93 --- /dev/null +++ b/packages/amis-core/__tests__/condition.test.ts @@ -0,0 +1,506 @@ +import moment from 'moment'; +import {resolveCondition, guid, registerConditionComputer} from '../src/utils/'; + +const data = { + name: 'amis', + feature: 'flexible', + features: ['flexible', 'powerful'], + tool: 'amis-editor', + platform: 'aisuda', + version: '2.9.0', + num: 1, + num2: 0, + num3: undefined, + str: '', + obj: {}, + arr: [], + bool: true, + detail: { + version: '2.8.0', + github: 'https://github.com/baidu/amis' + }, + range: 1678723200, + date: '2023-03-19', + datetime: '2023-03-20T04:55:00+08:00', + time: '00:05' +}; + +const equal1 = { + id: guid(), + left: { + type: 'field', + field: 'name' + }, + op: 'equal', + right: '${name}' +}; +const equal2 = { + id: guid(), + left: { + type: 'field', + field: 'detail' + }, + op: 'equal', + right: '${detail}' +}; +const equal3 = { + id: guid(), + left: { + type: 'field', + field: 'bool' + }, + op: 'equal', + right: '${!num2}' +}; +const not_equal = { + id: guid(), + left: { + type: 'field', + field: 'version' + }, + op: 'not_equal', + right: '${version}' +}; +const greater = { + id: guid(), + left: { + type: 'field', + field: 'num' + }, + op: 'greater', + right: '${num + 0.5}' +}; +const less = { + id: guid(), + left: { + type: 'field', + field: 'num' + }, + op: 'less', + right: '${num - 0.5}' +}; +const greater_or_equal = { + id: guid(), + left: { + type: 'field', + field: 'num' + }, + op: 'greater_or_equal', + right: '${num}' +}; +const less_or_equal = { + id: guid(), + left: { + type: 'field', + field: 'num' + }, + op: 'less_or_equal', + right: '${num}' +}; +const starts_with = { + id: guid(), + left: { + type: 'field', + field: 'tool' + }, + op: 'starts_with', + right: 'amis-' +}; +const ends_with = { + id: guid(), + left: { + type: 'field', + field: 'tool' + }, + op: 'ends_with', + right: '-editor' +}; +const like = { + id: guid(), + left: { + type: 'field', + field: 'tool' + }, + op: 'like', + right: '${tool}-core' +}; +const not_like = { + id: guid(), + left: { + type: 'field', + field: 'tool' + }, + op: 'not_like', + right: '${tool}r' +}; +const is_empty1 = { + id: guid(), + left: { + type: 'field', + field: 'num2' + }, + op: 'is_empty' +}; +const is_empty2 = { + id: guid(), + left: { + type: 'field', + field: 'str' + }, + op: 'is_empty' +}; +const is_empty3 = { + id: guid(), + left: { + type: 'field', + field: 'obj' + }, + op: 'is_empty' +}; +const is_not_empty1 = { + id: guid(), + left: { + type: 'field', + field: 'num3' + }, + op: 'is_not_empty' +}; +const is_not_empty2 = { + id: guid(), + left: { + type: 'field', + field: 'name' + }, + op: 'is_not_empty' +}; +const is_not_empty3 = { + id: guid(), + left: { + type: 'field', + field: 'detail' + }, + op: 'is_not_empty' +}; +const is_not_empty4 = { + id: guid(), + left: { + type: 'field', + field: 'arr' + }, + op: 'is_not_empty' +}; +const between = { + id: guid(), + left: { + type: 'field', + field: 'num' + }, + op: 'between', + right: '${[0.5,1]}' +}; +const not_between = { + id: guid(), + left: { + type: 'field', + field: 'range' + }, + op: 'not_between', + right: '${[1678636800,1678895999]}' +}; +const select_any_in1 = { + id: guid(), + left: { + type: 'field', + field: 'features' + }, + op: 'select_any_in', + right: '${[LAST(features)]}' +}; +const select_any_in2 = { + id: guid(), + left: { + type: 'field', + field: 'feature' + }, + op: 'select_any_in', + right: '${features}' +}; +const select_not_any_in = { + id: guid(), + left: { + type: 'field', + field: 'features' + }, + op: 'select_not_any_in', + right: "${['powerful']}" +}; + +const conditions1 = { + id: guid(), + conjunction: 'and', + children: [equal1, equal2, equal3, not_equal] +}; + +const conditions2 = { + id: guid(), + conjunction: 'or', + children: [greater, less, greater_or_equal, less_or_equal] +}; + +const conditions3 = { + id: guid(), + conjunction: 'and', + children: [starts_with, ends_with, like, not_like] +}; + +const conditions4 = { + id: guid(), + conjunction: 'and', + children: [is_empty1, is_empty2, is_empty3] +}; + +const conditions5 = { + id: guid(), + conjunction: 'and', + children: [is_not_empty1, is_not_empty2, is_not_empty3, is_not_empty4] +}; + +const conditions6 = { + id: guid(), + conjunction: 'and', + children: [between, not_between] +}; + +const conditions7 = { + id: guid(), + conjunction: 'and', + children: [select_any_in1, select_any_in2, select_not_any_in] +}; + +test(`condition`, async () => { + expect(await resolveCondition(conditions1, data)).toBe(false); + expect(await resolveCondition(conditions2, data)).toBe(true); + expect(await resolveCondition(conditions3, data)).toBe(false); + expect(await resolveCondition(conditions4, data)).toBe(false); + expect(await resolveCondition(conditions5, data)).toBe(false); + expect(await resolveCondition(conditions6, data)).toBe(false); + expect(await resolveCondition(conditions7, data)).toBe(false); +}); + +test(`condition date`, async () => { + const conditions = { + id: guid(), + conjunction: 'and', + children: [ + { + id: guid(), + left: { + type: 'date', + field: 'date' + }, + op: 'less', + right: '2023-03-20' + }, + { + id: guid(), + left: { + type: 'date', + field: 'datetime' + }, + op: 'less', + right: '2023-03-21T04:55:00+08:00' + }, + { + id: guid(), + left: { + type: 'date', + field: 'time' + }, + op: 'less', + right: '00:08' + }, + { + id: guid(), + left: { + type: 'date', + field: 'date' + }, + op: 'less_or_equal', + right: '2023-03-19' + }, + { + id: guid(), + left: { + type: 'date', + field: 'datetime' + }, + op: 'less_or_equal', + right: '2023-03-20T04:55:00+08:00' + }, + { + id: guid(), + left: { + type: 'date', + field: 'time' + }, + op: 'less_or_equal', + right: '00:05' + }, + { + id: guid(), + left: { + type: 'date', + field: 'date' + }, + op: 'between', + right: '${["2023-03-19","2023-03-20"]}' + }, + { + id: guid(), + left: { + type: 'date', + field: 'datetime' + }, + op: 'between', + right: '${["2023-03-20T04:55:00+08:00","2023-03-21T04:55:00+08:00"]}' + }, + { + id: guid(), + left: { + type: 'date', + field: 'time' + }, + op: 'between', + right: '${["00:05","00:06"]}' + } + ] + }; + + expect(await resolveCondition(conditions, data)).toBe(true); +}); + +test(`condition tree`, async () => { + const conditions8 = { + id: guid(), + conjunction: 'and', + children: [ + select_any_in1, + { + id: guid(), + conjunction: 'and', + children: [ + equal1, + { + id: guid(), + conjunction: 'or', + children: [between, not_between] + }, + between, + { + id: guid(), + conjunction: 'and', + children: [between, select_any_in2, conditions5] + } + ] + } + ] + }; + + const conditions9 = { + id: guid(), + conjunction: 'or', + children: [ + is_empty1, + { + id: guid(), + conjunction: 'or', + children: [ + is_empty1, + conditions6, + between, + { + id: guid(), + conjunction: 'or', + children: [is_empty1, select_any_in2, conditions5] + } + ] + } + ] + }; + + const conditions10 = { + id: guid(), + conjunction: 'or', + children: [ + { + id: guid(), + conjunction: 'or', + children: [ + conditions6, + is_empty1, + { + id: guid(), + conjunction: 'or', + children: [ + is_empty1, + { + id: guid(), + conjunction: 'or', + children: [ + is_not_empty1, + is_not_empty2, + is_not_empty3, + is_not_empty4 + ] + } + ] + } + ] + }, + is_empty1 + ] + }; + + expect(await resolveCondition(conditions8, data)).toBe(false); + expect(await resolveCondition(conditions9, data)).toBe(true); + expect(await resolveCondition(conditions10, data)).toBe(true); +}); + +test(`condition register`, async () => { + registerConditionComputer( + 'customless', + (left: any, right?: any, fieldType?: string) => { + if (fieldType === 'date') { + return moment(left).isBefore(moment(right), 'day'); + } + return left < right; + } + ); + + const conditions = { + id: guid(), + conjunction: 'and', + children: [ + { + id: guid(), + left: { + type: 'date', + field: 'date' + }, + op: 'customless', + right: '2023-03-20' + }, + { + id: guid(), + left: { + type: 'field', + field: 'num' + }, + op: 'customless', + right: '5' + } + ] + }; + + expect(await resolveCondition(conditions, data)).toBe(true); +}); diff --git a/packages/amis-core/src/actions/Action.ts b/packages/amis-core/src/actions/Action.ts index 0524c3d12..7b86ee83d 100644 --- a/packages/amis-core/src/actions/Action.ts +++ b/packages/amis-core/src/actions/Action.ts @@ -1,8 +1,9 @@ import omit from 'lodash/omit'; import {RendererProps} from '../factory'; +import {ConditionGroupValue} from '../types'; import {createObject} from '../utils/helper'; import {RendererEvent} from '../utils/renderer-event'; -import {evalExpression} from '../utils/tpl'; +import {evalExpressionWithConditionBuilder} from '../utils/tpl'; import {dataMapping} from '../utils/tpl-builtin'; import {IBreakAction} from './BreakAction'; import {IContinueAction} from './ContinueAction'; @@ -28,7 +29,7 @@ export interface ListenerAction { outputVar?: string; // 输出数据变量名 preventDefault?: boolean; // 阻止原有组件的动作行为 stopPropagation?: boolean; // 阻止后续的事件处理器执行 - expression?: string; // 执行条件 + expression?: string | ConditionGroupValue; // 执行条件 execOn?: string; // 执行条件,1.9.0废弃 } @@ -207,18 +208,38 @@ export const runAction = async ( ); // 兼容一下1.9.0之前的版本 const expression = actionConfig.expression ?? actionConfig.execOn; + // 执行条件 + let isStop = false; - if (expression && !evalExpression(expression, mergeData)) { + if (expression) { + isStop = !(await evalExpressionWithConditionBuilder( + expression, + mergeData, + true + )); + } + + if (isStop) { return; } // 支持表达式 >=1.10.0 - const preventDefault = - actionConfig.preventDefault && - evalExpression(String(actionConfig.preventDefault), mergeData); - const stopPropagation = - actionConfig.stopPropagation && - evalExpression(String(actionConfig.stopPropagation), mergeData); + let preventDefault = false; + if (actionConfig.preventDefault) { + preventDefault = await evalExpressionWithConditionBuilder( + String(actionConfig.preventDefault), + mergeData, + false + ); + } + let stopPropagation = false; + if (actionConfig.stopPropagation) { + stopPropagation = await evalExpressionWithConditionBuilder( + String(actionConfig.stopPropagation), + mergeData, + false + ); + } // 动作配置 const args = dataMapping(actionConfig.args, mergeData, key => diff --git a/packages/amis-core/src/actions/SwitchAction.ts b/packages/amis-core/src/actions/SwitchAction.ts index bf0227f3a..fe48a40ce 100644 --- a/packages/amis-core/src/actions/SwitchAction.ts +++ b/packages/amis-core/src/actions/SwitchAction.ts @@ -1,5 +1,5 @@ -import { RendererEvent } from '../utils/renderer-event'; -import { evalExpression } from '../utils/tpl'; +import {RendererEvent} from '../utils/renderer-event'; +import {evalExpressionWithConditionBuilder} from '../utils/tpl'; import { RendererAction, ListenerContext, @@ -27,7 +27,12 @@ export class SwitchAction implements RendererAction { continue; } - if (evalExpression(branch.expression, mergeData)) { + const isPass = await evalExpressionWithConditionBuilder( + branch.expression, + mergeData + ); + + if (isPass) { await runActions(branch, renderer, event); // 去掉runAllMatch,这里只做排他,多个可以直接通过expression break; diff --git a/packages/amis-core/src/index.tsx b/packages/amis-core/src/index.tsx index b4a848887..49ba29cbb 100644 --- a/packages/amis-core/src/index.tsx +++ b/packages/amis-core/src/index.tsx @@ -79,7 +79,9 @@ import type {RendererEnv} from './env'; import React from 'react'; import { evaluate, + evaluateForAsync, Evaluator, + AsyncEvaluator, extendsFilters, filters, getFilters, @@ -152,6 +154,7 @@ export { parse, lexer, Evaluator, + AsyncEvaluator, FilterContext, filters, getFilters, @@ -159,6 +162,7 @@ export { extendsFilters, registerFunction, evaluate, + evaluateForAsync, // 其他 LazyComponent, Overlay, diff --git a/packages/amis-core/src/types.ts b/packages/amis-core/src/types.ts index a88031f8e..8eb6ed1f4 100644 --- a/packages/amis-core/src/types.ts +++ b/packages/amis-core/src/types.ts @@ -626,3 +626,70 @@ export interface BaseSchemaWithoutType { staticInputClassName?: SchemaClassName; staticSchema?: any; } + +export type OperatorType = + | 'equal' + | 'not_equal' + | 'is_empty' + | 'is_not_empty' + | 'like' + | 'not_like' + | 'starts_with' + | 'ends_with' + | 'less' + | 'less_or_equal' + | 'greater' + | 'greater_or_equal' + | 'between' + | 'not_between' + | 'select_equals' + | 'select_not_equals' + | 'select_any_in' + | 'select_not_any_in' + | { + label: string; + value: string; + }; + +export type ExpressionSimple = string | number | object | undefined; +export type ExpressionValue = + | ExpressionSimple + | { + type: 'value'; + value: ExpressionSimple; + }; +export type ExpressionFunc = { + type: 'func'; + func: string; + args: Array; +}; +export type ExpressionField = { + type: 'field'; + field: string; +}; +export type ExpressionFormula = { + type: 'formula'; + value: string; +}; + +export type ExpressionComplex = + | ExpressionValue + | ExpressionFunc + | ExpressionField + | ExpressionFormula; + +export interface ConditionRule { + id: any; + left?: ExpressionComplex; + op?: OperatorType; + right?: ExpressionComplex | Array; +} + +export interface ConditionGroupValue { + id: string; + conjunction: 'and' | 'or'; + not?: boolean; + children?: Array; +} + +export interface ConditionValue extends ConditionGroupValue {} diff --git a/packages/amis-core/src/utils/index.ts b/packages/amis-core/src/utils/index.ts index cc86c0d17..063deee2c 100644 --- a/packages/amis-core/src/utils/index.ts +++ b/packages/amis-core/src/utils/index.ts @@ -39,6 +39,7 @@ export * from './replaceText'; export * from './resize-sensor'; export * from './resolveVariable'; export * from './resolveVariableAndFilter'; +export * from './resolveVariableAndFilterForAsync'; export * from './RootClose'; export * from './scrollPosition'; export * from './SimpleMap'; @@ -52,6 +53,7 @@ export * from './validations'; export * from './toNumber'; export * from './decodeEntity'; export * from './style-helper'; +export * from './resolveCondition'; import animation from './Animation'; diff --git a/packages/amis-core/src/utils/resolveCondition.ts b/packages/amis-core/src/utils/resolveCondition.ts new file mode 100644 index 000000000..46921c48e --- /dev/null +++ b/packages/amis-core/src/utils/resolveCondition.ts @@ -0,0 +1,341 @@ +import get from 'lodash/get'; +import endsWith from 'lodash/endsWith'; +import isEmpty from 'lodash/isEmpty'; +import isEqual from 'lodash/isEqual'; +import startsWith from 'lodash/startsWith'; +import {resolveVariableAndFilterForAsync} from './resolveVariableAndFilterForAsync'; +import moment from 'moment'; +import capitalize from 'lodash/capitalize'; + +const conditionResolverMap: { + [op: string]: (left: any, right: any, fieldType?: string) => boolean; +} = {}; +const DEFAULT_RESULT = true; + +export async function resolveCondition( + conditions: any, + data: any, + defaultResult: boolean = true +) { + if ( + !conditions || + !conditions.conjunction || + !Array.isArray(conditions.children) || + !conditions.children.length + ) { + return defaultResult; + } + + return await computeConditions( + conditions.children, + conditions.conjunction, + data + ); +} + +async function computeConditions( + conditions: any[], + conjunction: 'or' | 'and' = 'and', + data: any +): Promise { + let computeResult = true; + for (let index = 0, len = conditions.length; index < len; index++) { + const item = conditions[index]; + const result = + item.conjunction && Array.isArray(item.children) && item.children.length + ? await computeConditions(item.children, item.conjunction, data) + : await computeCondition(item, index, data); + + computeResult = !!result; + + if ( + (result && conjunction === 'or') || + (!result && conjunction === 'and') + ) { + break; + } + } + return computeResult; +} + +async function computeCondition( + rule: { + op: string; + left: { + type: string; + field: string; + }; + right: any; + }, + index: number, + data: any +) { + const leftValue = get(data, rule.left.field); + const rightValue: any = await resolveVariableAndFilterForAsync( + rule.right, + data + ); + + const func = + conditionResolverMap[`${rule.op}For${capitalize(rule.left.type)}`] ?? + conditionResolverMap[rule.op]; + + return func ? func(leftValue, rightValue, rule.left.type) : DEFAULT_RESULT; +} + +function startsWithFunc(left: any, right: any) { + if (left === undefined || right === undefined) { + return DEFAULT_RESULT; + } + return startsWith(left, right); +} + +function endsWithFunc(left: any, right: any) { + if (left === undefined || right === undefined) { + return DEFAULT_RESULT; + } + return endsWith(left, right); +} + +function equalFunc(left: any, right: any) { + return isEqual(left, right); +} + +function notEqualFunc(left: any, right: any) { + return !isEqual(left, right); +} + +function isEmptyFunc(left: any) { + if (typeof left === 'string') { + return !left; + } else if (typeof left === 'number') { + return left === undefined; + } else if (Array.isArray(left)) { + return !left.length; + } else if (typeof left === 'object') { + return isEmpty(left); + } + return DEFAULT_RESULT; +} + +function isNotEmptyFunc(left: any) { + if (typeof left === 'string') { + return !left; + } else if (typeof left === 'number') { + return left !== undefined; + } else if (Array.isArray(left)) { + return !!left.length; + } else if (typeof left === 'object') { + return !isEmpty(left); + } + return DEFAULT_RESULT; +} + +function greaterFunc(left: any, right: any) { + if (left === undefined || right === undefined) { + return DEFAULT_RESULT; + } + return parseFloat(left as any) > parseFloat(right as any); +} + +function normalizeDate(raw: any): Date { + if (typeof raw === 'string' || typeof raw === 'number') { + let formats = ['', 'YYYY-MM-DD HH:mm:ss', 'X']; + + if (/^\d{10}((\.\d+)*)$/.test(raw.toString())) { + formats = ['X', 'x', 'YYYY-MM-DD HH:mm:ss', '']; + } else if (/^\d{13}((\.\d+)*)$/.test(raw.toString())) { + formats = ['x', 'X', 'YYYY-MM-DD HH:mm:ss', '']; + } + while (formats.length) { + const format = formats.shift()!; + const date = moment(raw, format); + + if (date.isValid()) { + return date.toDate(); + } + } + } + + return raw; +} + +function normalizeDateRange(raw: string | Date[]): Date[] { + return (Array.isArray(raw) ? raw : raw.split(',')).map((item: any) => + normalizeDate(String(item).trim()) + ); +} + +function greaterForDateFunc(left: any, right: any) { + left = normalizeDate(left); + right = normalizeDate(right); + return moment(left).isAfter(moment(right), 's'); +} + +function greaterOrEqualForDateFunc(left: any, right: any) { + left = normalizeDate(left); + right = normalizeDate(right); + return moment(left).isSameOrAfter(moment(right), 's'); +} + +function greaterOrEqualFunc(left: any, right: any) { + if (left === undefined || right === undefined) { + return DEFAULT_RESULT; + } + return parseFloat(left as any) >= parseFloat(right as any); +} + +function lessFunc(left: any, right: any) { + if (left === undefined || right === undefined) { + return DEFAULT_RESULT; + } + return parseFloat(left as any) < parseFloat(right as any); +} + +function lessForDateFunc(left: any, right: any) { + left = normalizeDate(left); + right = normalizeDate(right); + return moment(left).isBefore(moment(right), 's'); +} + +function lessOrEqualForDateFunc(left: any, right: any) { + left = normalizeDate(left); + right = normalizeDate(right); + return moment(left).isSameOrBefore(moment(right), 's'); +} + +function lessOrEqualFunc(left: any, right: any) { + if (left === undefined || right === undefined) { + return DEFAULT_RESULT; + } + return parseFloat(left as any) <= parseFloat(right as any); +} + +function likeFunc(left: any, right: any) { + if (left === undefined || right === undefined) { + return DEFAULT_RESULT; + } + return !!~left.indexOf(right); +} + +function notLikeFunc(left: any, right: any) { + if (left === undefined || right === undefined) { + return DEFAULT_RESULT; + } + return !~left.indexOf(right); +} + +function betweenFunc(left: any, right: any) { + if (typeof left === 'number' && right !== undefined) { + const [min, max] = right.sort(); + return left >= parseFloat(min) && left <= parseFloat(max); + } + return DEFAULT_RESULT; +} + +function betweenForDateFunc(left: any, right: any) { + if (right !== undefined) { + const [min, max] = normalizeDateRange(right); + return moment(normalizeDate(left)).isBetween(min, max, 's', '[]'); + } + return DEFAULT_RESULT; +} + +function notBetweenFunc(left: any, right: any) { + if (typeof left === 'number' && right !== undefined) { + const [min, max] = right.sort(); + return left < parseFloat(min) && left > parseFloat(max); + } + return DEFAULT_RESULT; +} + +function notBetweenForDateFunc(left: any, right: any) { + if (right !== undefined) { + const [min, max] = normalizeDateRange(right); + return !moment(normalizeDate(left)).isBetween(min, max, 's', '[]'); + } + return DEFAULT_RESULT; +} + +function selectAnyInFunc(left: any, right: any) { + if (!Array.isArray(right)) { + return DEFAULT_RESULT; + } + + if (Array.isArray(left)) { + return right.every((item: any) => left.includes(item)); + } + return right.includes(left); +} + +function selectNotAnyInFunc(left: any, right: any) { + if (!Array.isArray(right)) { + return DEFAULT_RESULT; + } + + if (Array.isArray(left)) { + return !right.every((item: any) => left.includes(item)); + } + return !right.includes(left); +} + +export function registerConditionComputer( + op: string, + func: (left: any, right: any, fieldType?: string) => boolean, + fieldType?: string +) { + conditionResolverMap[ + `${op}${fieldType ? 'For' + capitalize(fieldType) : ''}` + ] = func; +} + +export function getConditionComputers() { + return conditionResolverMap; +} + +registerConditionComputer('greater', greaterFunc); +registerConditionComputer('greater', greaterForDateFunc, 'date'); +registerConditionComputer('greater', greaterForDateFunc, 'time'); +registerConditionComputer('greater', greaterForDateFunc, 'datetime'); +registerConditionComputer('greater_or_equal', greaterOrEqualFunc); +registerConditionComputer( + 'greater_or_equal', + greaterOrEqualForDateFunc, + 'date' +); +registerConditionComputer( + 'greater_or_equal', + greaterOrEqualForDateFunc, + 'time' +); +registerConditionComputer( + 'greater_or_equal', + greaterOrEqualForDateFunc, + 'datetime' +); +registerConditionComputer('less', lessFunc); +registerConditionComputer('less', lessForDateFunc, 'date'); +registerConditionComputer('less', lessForDateFunc, 'time'); +registerConditionComputer('less', lessForDateFunc, 'datetime'); +registerConditionComputer('less_or_equal', lessOrEqualFunc); +registerConditionComputer('less_or_equal', lessOrEqualForDateFunc, 'date'); +registerConditionComputer('less_or_equal', lessOrEqualForDateFunc, 'time'); +registerConditionComputer('less_or_equal', lessOrEqualForDateFunc, 'datetime'); +registerConditionComputer('is_empty', isEmptyFunc); +registerConditionComputer('is_not_empty', isNotEmptyFunc); +registerConditionComputer('between', betweenFunc); +registerConditionComputer('between', betweenForDateFunc, 'date'); +registerConditionComputer('between', betweenForDateFunc, 'time'); +registerConditionComputer('between', betweenForDateFunc, 'datetime'); +registerConditionComputer('not_between', notBetweenFunc); +registerConditionComputer('not_between', notBetweenForDateFunc, 'date'); +registerConditionComputer('not_between', notBetweenForDateFunc, 'time'); +registerConditionComputer('not_between', notBetweenForDateFunc, 'datetime'); +registerConditionComputer('equal', equalFunc); +registerConditionComputer('not_equal', notEqualFunc); +registerConditionComputer('like', likeFunc); +registerConditionComputer('not_like', notLikeFunc); +registerConditionComputer('select_any_in', selectAnyInFunc); +registerConditionComputer('select_not_any_in', selectNotAnyInFunc); +registerConditionComputer('starts_with', startsWithFunc); +registerConditionComputer('ends_with', endsWithFunc); diff --git a/packages/amis-core/src/utils/resolveVariableAndFilterForAsync.ts b/packages/amis-core/src/utils/resolveVariableAndFilterForAsync.ts new file mode 100644 index 000000000..93a890167 --- /dev/null +++ b/packages/amis-core/src/utils/resolveVariableAndFilterForAsync.ts @@ -0,0 +1,30 @@ +import {AsyncEvaluator, parse} from 'amis-formula'; + +export const resolveVariableAndFilterForAsync = async ( + path?: string, + data: object = {}, + defaultFilter: string = '| html', + fallbackValue = (value: any) => value +) => { + if (!path || typeof path !== 'string') { + return undefined; + } + + try { + const ast = parse(path, { + evalMode: false, + allowFilter: true + }); + + const ret = await new AsyncEvaluator(data, { + defaultFilter + }).evalute(ast); + + return ret == null && !~path.indexOf('default') && !~path.indexOf('now') + ? fallbackValue(ret) + : ret; + } catch (e) { + console.warn(e); + return undefined; + } +}; diff --git a/packages/amis-core/src/utils/tpl.ts b/packages/amis-core/src/utils/tpl.ts index 6e9e3c2fc..ee67d9cf0 100644 --- a/packages/amis-core/src/utils/tpl.ts +++ b/packages/amis-core/src/utils/tpl.ts @@ -1,7 +1,7 @@ -import {createObject} from './helper'; import {register as registerBulitin, getFilters} from './tpl-builtin'; import {register as registerLodash} from './tpl-lodash'; import {parse, evaluate} from 'amis-formula'; +import {resolveCondition} from './resolveCondition'; export interface Enginer { test: (tpl: string) => boolean; @@ -99,6 +99,25 @@ export function evalExpression(expression: string, data?: object): boolean { } } +/** + * 解析表达式(支持condition-builder) + * @param expression 表达式 or condition-builder对象 + * @param data 上下文 + * @returns + */ +export async function evalExpressionWithConditionBuilder( + expression: any, + data?: object, + defaultResult?: boolean +): Promise { + // 支持ConditionBuilder + if (Object.prototype.toString.call(expression) === '[object Object]') { + return await resolveCondition(expression, data, defaultResult); + } + + return evalExpression(expression, data); +} + const AST_CACHE: {[key: string]: any} = {}; function evalFormula(expression: string, data: any) { const ast = diff --git a/packages/amis-formula/__tests__/async-evalute.test.ts b/packages/amis-formula/__tests__/async-evalute.test.ts new file mode 100644 index 000000000..c75b21ee3 --- /dev/null +++ b/packages/amis-formula/__tests__/async-evalute.test.ts @@ -0,0 +1,589 @@ +import {evaluateForAsync, parse} from '../src'; + +test('evalute:simple', async () => { + expect( + await evaluateForAsync('a is ${a}', { + a: 123 + }) + ).toBe('a is 123'); +}); + +test('evalute:filter', async () => { + expect( + await evaluateForAsync( + 'a is ${a | abc}', + { + a: 123 + }, + { + filters: { + abc(input: any) { + return `${input}456`; + } + } + } + ) + ).toBe('a is 123456'); + + expect( + await evaluateForAsync( + 'a is ${a | concat:233}', + { + a: 123 + }, + { + filters: { + concat(input: any, arg: string) { + return `${input}${arg}`; + } + } + } + ) + ).toBe('a is 123233'); + + expect( + await evaluateForAsync( + 'a is ${concat(a, a)}', + { + a: 123 + }, + { + filters: { + concat(input: any, arg: string) { + return `${input}${arg}`; + } + } + } + ) + ).toBe('a is 123123'); +}); + +test('evalute:filter2', async () => { + expect( + await evaluateForAsync( + 'a is ${[1, 2, 3] | concat:4 | join}', + {}, + { + filters: { + concat(input: any, ...args: Array) { + return input.concat.apply(input, args); + }, + join(input: any) { + return input.join(','); + } + } + } + ) + ).toBe('a is 1,2,3,4'); +}); + +test('evalute:filter3', async () => { + expect( + await evaluateForAsync( + 'a is ${[1, 2, 3] | concat:"4" | join}', + {}, + { + filters: { + concat(input: any, ...args: Array) { + return input.concat.apply(input, args); + }, + join(input: any) { + return input.join(','); + } + } + } + ) + ).toBe('a is 1,2,3,4'); +}); + +test('evalute:filter4', async () => { + expect( + await evaluateForAsync( + 'a is ${[1, 2, 3] | concat:${a + 3} | join}', + { + a: 4 + }, + { + filters: { + concat(input: any, ...args: Array) { + return input.concat.apply(input, args); + }, + join(input: any) { + return input.join(','); + } + } + } + ) + ).toBe('a is 1,2,3,7'); +}); + +test('evalute:oldVariable', async () => { + expect( + await evaluateForAsync('a is $a', { + a: 4 + }) + ).toBe('a is 4'); + + expect( + await evaluateForAsync('b is $b', { + a: 4 + }) + ).toBe('b is '); + + expect( + await evaluateForAsync('a.b is $a.b', { + a: { + b: 233 + } + }) + ).toBe('a.b is 233'); +}); + +test('evalute:ariable2', async () => { + expect( + await evaluateForAsync('a is $$', { + a: 4 + }) + ).toBe('a is [object Object]'); +}); + +test('evalute:ariable3', async () => { + expect( + await evaluateForAsync( + '$$', + { + a: 4 + }, + { + defaultFilter: 'raw' + } + ) + ).toMatchObject({ + a: 4 + }); +}); + +test('evalute:object-variable', async () => { + const data = { + key: 'x', + obj: { + x: 1, + y: 2 + } + }; + + expect(await evaluateForAsync('a is ${obj.x}', data)).toBe('a is 1'); + expect(await evaluateForAsync('a is ${obj[x]}', data)).toBe('a is 1'); + expect(await evaluateForAsync('a is ${obj[`x`]}', data)).toBe('a is 1'); + expect(await evaluateForAsync('a is ${obj["x"]}', data)).toBe('a is 1'); + expect(await evaluateForAsync('a is ${obj[key]}', data)).toBe('a is 1'); + expect(await evaluateForAsync('a is ${obj[`${key}`]}', data)).toBe('a is 1'); + expect(await evaluateForAsync('a is ${obj[${key}]}', data)).toBe('a is 1'); +}); + +test('evalute:literal-variable', async () => { + const data = { + key: 'x', + index: 0, + obj: { + x: 1, + y: 2 + } + }; + + expect(await evaluateForAsync('a is ${({x: 1})["x"]}', data)).toBe('a is 1'); + expect(await evaluateForAsync('a is ${({x: 1}).x}', data)).toBe('a is 1'); + expect(await evaluateForAsync('a is ${(["a", "b"])[index]}', data)).toBe( + 'a is a' + ); + expect(await evaluateForAsync('a is ${(["a", "b"])[1]}', data)).toBe( + 'a is b' + ); + expect(await evaluateForAsync('a is ${(["a", "b"]).0}', data)).toBe('a is a'); +}); + +test('evalute:tempalte', async () => { + const data = { + key: 'x' + }; + + expect(await evaluateForAsync('abc${`11${3}22`}xyz', data)).toBe( + 'abc11322xyz' + ); + expect(await evaluateForAsync('abc${`${3}22`}xyz', data)).toBe('abc322xyz'); + expect(await evaluateForAsync('abc${`11${3}`}xyz', data)).toBe('abc113xyz'); + expect(await evaluateForAsync('abc${`${3}`}xyz', data)).toBe('abc3xyz'); + expect(await evaluateForAsync('abc${`${key}`}xyz', data)).toBe('abcxxyz'); +}); + +test('evalute:literal', async () => { + const data = { + dynamicKey: 'alpha' + }; + + expect( + await evaluateForAsync('${{a: 1, 0: 2, "3": 3}}', data, { + defaultFilter: 'raw' + }) + ).toMatchObject({ + a: 1, + 0: 2, + 3: 3 + }); + + expect( + await evaluateForAsync('${{a: 1, 0: 2, "3": 3, [`4`]: 4}}', data, { + defaultFilter: 'raw' + }) + ).toMatchObject({ + a: 1, + 0: 2, + 3: 3, + 4: 4 + }); + + expect( + await evaluateForAsync( + '${{a: 1, 0: 2, "3": 3, [`${dynamicKey}233`]: 4}}', + data, + { + defaultFilter: 'raw' + } + ) + ).toMatchObject({ + a: 1, + 0: 2, + 3: 3, + alpha233: 4 + }); + + expect( + await evaluateForAsync( + '${[1, 2, `2${dynamicKey}2`, {a: 1, 0: 2, [`2`]: "3"}]}', + data, + { + defaultFilter: 'raw' + } + ) + ).toMatchObject([1, 2, `2alpha2`, {a: 1, 0: 2, [`2`]: '3'}]); +}); + +test('evalute:variableName', async () => { + const data = { + 'a-b': 'c', + '222': 10222, + '222_221': 233, + '222_abcde': 'abcde', + '222-221': 333 + }; + + expect(await evaluateForAsync('${a-b}', data)).toBe('c'); + expect(await evaluateForAsync('${222}', data)).toBe(222); + expect(await evaluateForAsync('${222_221}', data)).toBe('233'); + expect(await evaluateForAsync('${222-221}', data)).toBe(1); + expect(await evaluateForAsync('${222_abcde}', data)).toBe('abcde'); + expect( + await evaluateForAsync('${&["222-221"]}', data, { + defaultFilter: 'raw' + }) + ).toBe(333); + expect( + await evaluateForAsync('222', data, { + variableMode: true + }) + ).toBe(10222); +}); + +test('evalute:3-1', async () => { + const data = {}; + + expect(await evaluateForAsync('${3-1}', data)).toBe(2); + expect(await evaluateForAsync('${-1 + 2.5 + 3}', data)).toBe(4.5); + expect(await evaluateForAsync('${-1 + -1}', data)).toBe(-2); + expect(await evaluateForAsync('${3 * -1}', data)).toBe(-3); + + expect(await evaluateForAsync('${3 + +1}', data)).toBe(4); +}); + +test('evalate:0.1+0.2', async () => { + expect(await evaluateForAsync('${0.1 + 0.2}', {})).toBe(0.3); +}); + +test('evalute:variable:com.xxx.xx', async () => { + const data = { + 'com.xxx.xx': 'abc', + 'com xxx%xx': 'cde', + 'com[xxx]': 'eee' + }; + + expect(await evaluateForAsync('${com\\.xxx\\.xx}', data)).toBe('abc'); + expect(await evaluateForAsync('${com\\ xxx\\%xx}', data)).toBe('cde'); + expect(await evaluateForAsync('${com\\[xxx\\]}', data)).toBe('eee'); +}); + +test('evalute:anonymous:function', async () => { + const data = { + arr: [1, 2, 3], + arr2: [ + { + a: 1 + }, + { + a: 2 + }, + { + a: 3 + } + ], + outter: 4 + }; + + expect(await evaluateForAsync('${() => 233}', data)).toMatchObject({ + args: [], + return: {type: 'literal', value: 233}, + type: 'anonymous_function' + }); + + expect( + await evaluateForAsync('${ARRAYMAP(arr, () => 1)}', data) + ).toMatchObject([1, 1, 1]); + expect( + await evaluateForAsync('${ARRAYMAP(arr, item => item)}', data) + ).toMatchObject([1, 2, 3]); + expect( + await evaluateForAsync('${ARRAYMAP(arr, item => item * 2)}', data) + ).toMatchObject([2, 4, 6]); + expect( + await evaluateForAsync( + '${ARRAYMAP(arr2, (item, index) => `a${item.a}${index}`)}', + data + ) + ).toMatchObject(['a10', 'a21', 'a32']); + expect( + await evaluateForAsync( + '${ARRAYMAP(arr2, (item, index) => `a${item.a}${index}${outter}`)}', + data + ) + ).toMatchObject(['a104', 'a214', 'a324']); + expect( + await evaluateForAsync( + '${ARRAYMAP(arr2, (item, index) => {x: item.a, index: index})}', + data + ) + ).toMatchObject([ + { + x: 1, + index: 0 + }, + { + x: 2, + index: 1 + }, + { + x: 3, + index: 2 + } + ]); +}); + +test('evalute:anonymous:function2', async () => { + const data = { + arr: [1, 2, 3], + arr2: [ + { + x: 1, + y: [ + { + z: 1 + }, + { + z: 1 + } + ] + }, + { + x: 2, + y: [ + { + z: 2 + }, + { + z: 2 + } + ] + } + ] + }; + + expect( + await evaluateForAsync( + '${ARRAYMAP(ARRAYMAP(arr, item => item * 2), item => item + 2)}', + data + ) + ).toMatchObject([4, 6, 8]); + + expect( + await evaluateForAsync( + '${ARRAYMAP(arr2, item => ARRAYMAP(item.y, i => i.z))}', + data + ) + ).toMatchObject([ + [1, 1], + [2, 2] + ]); +}); + +test('evalute:array:func', async () => { + const data = { + arr1: [0, 1, false, 2, '', 3], + arr2: ['a', 'b', 'c'], + arr3: [1, 2, 3], + arr4: [2, 4, 6], + arr5: [ + { + id: 1.1 + }, + { + id: 2.2 + }, + { + id: 1.1, + name: 1.3 + } + ], + obj1: { + p1: 'name', + p2: 'age', + p3: 'obj', + p4: [ + { + p41: 'Tom', + p42: 'Jerry' + }, + { + p41: 'baidu', + p42: 'amis' + } + ] + } + }; + + expect(await evaluateForAsync('${COMPACT(arr1)}', data)).toMatchObject([ + 1, 2, 3 + ]); + + expect( + await evaluateForAsync("${COMPACT([0, 1, false, 2, '', 3])}", data) + ).toMatchObject([1, 2, 3]); + + expect(await evaluateForAsync('${JOIN(arr2, "~")}', data)).toMatch('a~b~c'); + + expect(await evaluateForAsync('${SUM(arr3)}', data)).toBe(6); + + expect(await evaluateForAsync('${AVG(arr4)}', data)).toBe(4); + + expect(await evaluateForAsync('${MIN(arr4)}', data)).toBe(2); + + expect(await evaluateForAsync('${MAX(arr4)}', data)).toBe(6); + + expect(await evaluateForAsync('${CONCAT(arr3, arr4)}', data)).toMatchObject([ + 1, 2, 3, 2, 4, 6 + ]); + + expect(await evaluateForAsync('${CONCAT(arr, arr4)}', data)).toMatchObject([ + 2, 4, 6 + ]); + + expect(await evaluateForAsync('${UNIQ(arr5)}', data)).toMatchObject( + data.arr5 + ); + + expect(await evaluateForAsync('${UNIQ(arr5, "id")}', data)).toMatchObject([ + {id: 1.1}, + {id: 2.2} + ]); + + expect( + await evaluateForAsync('${ARRAYFILTER(arr1, item => item)}', data) + ).toMatchObject([1, 2, 3]); + expect( + await evaluateForAsync( + '${ARRAYFILTER(arr1, item => item && item >=2)}', + data + ) + ).toMatchObject([2, 3]); + + expect( + await evaluateForAsync('${ARRAYFINDINDEX(arr3, item => item === 2)}', data) + ).toBe(1); + + expect( + await evaluateForAsync( + '${ARRAYFIND(arr5, item => item.name === 1.3)}', + data + ) + ).toMatchObject({ + id: 1.1, + name: 1.3 + }); + + expect( + await evaluateForAsync( + '${ARRAYSOME(arr5, item => item.name === 1.3)}', + data + ) + ).toBe(true); + + expect( + await evaluateForAsync( + '${ARRAYEVERY(arr5, item => item.name === 1.3)}', + data + ) + ).toBe(false); + + expect(await evaluateForAsync('${ARRAYINCLUDES(arr1, false)}', data)).toBe( + true + ); + + expect(await evaluateForAsync('${GET(arr1, 2)}', data)).toBe(false); + expect(await evaluateForAsync('${GET(arr1, 6, "not-found")}', data)).toBe( + 'not-found' + ); + expect(await evaluateForAsync('${GET(arr5, "[2].name")}', data)).toBe(1.3); + expect(await evaluateForAsync('${GET(arr5, "2.name")}', data)).toBe(1.3); + expect(await evaluateForAsync('${GET(obj1, "p2")}', data)).toBe('age'); + expect(await evaluateForAsync('${GET(obj1, "p4.1.p42")}', data)).toBe('amis'); + expect(await evaluateForAsync('${GET(obj1, "p4[1].p42")}', data)).toBe( + 'amis' + ); + + expect(await evaluateForAsync('${ENCODEJSON(obj1)}', data)).toBe( + JSON.stringify(data.obj1) + ); + expect( + await evaluateForAsync('${DECODEJSON("{\\"name\\":\\"amis\\"}")}', data) + ).toMatchObject(JSON.parse('{"name":"amis"}')); +}); + +test('evalute:ISTYPE', async () => { + const data = { + a: 1, + b: 'string', + c: null, + d: undefined, + e: [1, 2], + f: {a: 1, b: 2}, + g: new Date() + }; + expect(await evaluateForAsync('${ISTYPE(a, "number")}', data)).toBe(true); + expect(await evaluateForAsync('${ISTYPE(b, "number")}', data)).toBe(false); + expect(await evaluateForAsync('${ISTYPE(b, "string")}', data)).toBe(true); + expect(await evaluateForAsync('${ISTYPE(c, "nil")}', data)).toBe(true); + expect(await evaluateForAsync('${ISTYPE(d, "nil")}', data)).toBe(true); + expect(await evaluateForAsync('${ISTYPE(e, "array")}', data)).toBe(true); + expect(await evaluateForAsync('${ISTYPE(f, "array")}', data)).toBe(false); + expect(await evaluateForAsync('${ISTYPE(f, "plain-object")}', data)).toBe( + true + ); + expect(await evaluateForAsync('${ISTYPE(g, "date")}', data)).toBe(true); +}); diff --git a/packages/amis-formula/__tests__/async-fomula.test.ts b/packages/amis-formula/__tests__/async-fomula.test.ts new file mode 100644 index 000000000..3aa3223af --- /dev/null +++ b/packages/amis-formula/__tests__/async-fomula.test.ts @@ -0,0 +1,390 @@ +import moment from 'moment'; +import {evaluateForAsync, registerFunction} from '../src'; + +const defaultContext = { + a: 1, + b: 2, + c: 3, + d: 4, + e: 5 +}; + +async function evalFormual(expression: string, data: any = defaultContext) { + return evaluateForAsync(expression, data, { + evalMode: true + }); +} + +test('formula:expression', async () => { + expect(await evalFormual('a + 3')).toBe(4); + expect(await evalFormual('b * 3')).toBe(6); + expect(await evalFormual('b * 3 + 4')).toBe(10); + expect(await evalFormual('c * (3 + 4)')).toBe(21); + expect(await evalFormual('d / (a + 1)')).toBe(2); + expect(await evalFormual('5 % 3')).toBe(2); + expect(await evalFormual('3 | 4')).toBe(7); + expect(await evalFormual('4 ^ 4')).toBe(0); + expect(await evalFormual('4 ^ 4')).toBe(0); + expect(await evalFormual('4 & 4')).toBe(4); + expect(await evalFormual('4 & 3')).toBe(0); + expect(await evalFormual('~-1')).toBe(0); + expect(await evalFormual('!!1')).toBe(true); + expect(await evalFormual('!!""')).toBe(false); + expect(await evalFormual('1 || 2')).toBe(1); + expect(await evalFormual('1 && 2')).toBe(2); + expect(await evalFormual('1 && 2 || 3')).toBe(2); + expect(await evalFormual('1 || 2 || 3')).toBe(1); + expect(await evalFormual('1 || 2 && 3')).toBe(1); + expect(await evalFormual('(1 || 2) && 3')).toBe(3); + expect(await evalFormual('1 == "1"')).toBe(true); + expect(await evalFormual('1 === "1"')).toBe(false); + expect(await evalFormual('1 < 1')).toBe(false); + expect(await evalFormual('1 <= 1')).toBe(true); + expect(await evalFormual('1 > 1')).toBe(false); + expect(await evalFormual('1 >= 1')).toBe(true); + expect(await evalFormual('3 >> 1')).toBe(1); + expect(await evalFormual('3 << 1')).toBe(6); + expect(await evalFormual('10 ** 3')).toBe(1000); + + expect(await evalFormual('10 ? 3 : 2')).toBe(3); + expect(await evalFormual('0 ? 3 : 2')).toBe(2); +}); + +test('formula:expression2', async () => { + expect(await evalFormual('a[0]', {a: [1, 2, 3]})).toBe(1); + expect(await evalFormual('a[b]', {a: [1, 2, 3], b: 1})).toBe(2); + expect(await evalFormual('a[b - 1]', {a: [1, 2, 3], b: 1})).toBe(1); + expect(await evalFormual('a[b ? 1 : 2]', {a: [1, 2, 3], b: 1})).toBe(2); + expect(await evalFormual('a[c ? 1 : 2]', {a: [1, 2, 3], b: 1})).toBe(3); +}); + +test('formula:expression3', async () => { + // expect(await evalFormual('${a} === "b"', {a: 'b'})).toBe(true); + expect(await evalFormual('b === "b"')).toBe(false); + // expect(await evalFormual('${a}', {a: 'b'})).toBe('b'); + + expect(await evalFormual('obj.x.a', {obj: {x: {a: 1}}})).toBe(1); + expect(await evalFormual('obj.y.a', {obj: {x: {a: 1}}})).toBe(undefined); +}); + +test('formula:if', async () => { + expect(await evalFormual('IF(true, 2, 3)')).toBe(2); + expect(await evalFormual('IF(false, 2, 3)')).toBe(3); + expect(await evalFormual('IF(false, 2, IF(true, 3, 4))')).toBe(3); +}); + +test('formula:and', async () => { + expect(!!(await evalFormual('AND(0, 1)'))).toBe(false); + expect(!!(await evalFormual('AND(1, 1)'))).toBe(true); + expect(!!(await evalFormual('AND(1, 1, 1, 0)'))).toBe(false); +}); + +test('formula:or', async () => { + expect(!!(await evalFormual('OR(0, 1)'))).toBe(true); + expect(!!(await evalFormual('OR(1, 1)'))).toBe(true); + expect(!!(await evalFormual('OR(1, 1, 1, 0)'))).toBe(true); + expect(!!(await evalFormual('OR(0, 0, 0, 0)'))).toBe(false); +}); + +test('formula:xor', async () => { + expect(await evalFormual('XOR(0, 1)')).toBe(true); + expect(await evalFormual('XOR(1, 0)')).toBe(true); + expect(await evalFormual('XOR(1, 1)')).toBe(false); + expect(await evalFormual('XOR(0, 0)')).toBe(false); + + expect(await evalFormual('XOR(0, 0, 1)')).toBe(true); + expect(await evalFormual('XOR(0, 1, 1)')).toBe(false); +}); + +test('formula:ifs', async () => { + expect(!!(await evalFormual('IFS(0, 1, 2)'))).toBe(true); + expect(!!(await evalFormual('IFS(0, 1, 2, 2, 3)'))).toBe(true); + expect(!!(await evalFormual('IFS(0, 1, 0, 2, 0)'))).toBe(false); + expect(await evalFormual('IFS(0, 1, 2, 2)')).toBe(2); + expect(await evalFormual('IFS(0, 1, 0, 2)')).toBe(undefined); +}); + +test('formula:math', async () => { + expect(await evalFormual('ABS(1)')).toBe(1); + expect(await evalFormual('ABS(-1)')).toBe(1); + expect(await evalFormual('ABS(0)')).toBe(0); + + expect(await evalFormual('MAX(1, -1, 2, 3, 5, -9)')).toBe(5); + expect(await evalFormual('MIN(1, -1, 2, 3, 5, -9)')).toBe(-9); + + expect(await evalFormual('MOD(3, 2)')).toBe(1); + + expect(await evalFormual('PI()')).toBe(Math.PI); + + expect(await evalFormual('ROUND(3.55)')).toBe(3.55); + expect(await evalFormual('ROUND(3.45)')).toBe(3.45); + + expect(await evalFormual('ROUND(3.456789, 2)')).toBe(3.46); + expect(await evalFormual('CEIL(3.456789)')).toBe(3.46); + expect(await evalFormual('FLOOR(3.456789)')).toBe(3.45); + + expect(await evalFormual('SQRT(4)')).toBe(2); + expect(await evalFormual('AVG(4, 6, 10, 10, 10)')).toBe(8); + + // 示例来自 https://support.microsoft.com/zh-cn/office/devsq-%E5%87%BD%E6%95%B0-8b739616-8376-4df5-8bd0-cfe0a6caf444 + expect(await evalFormual('DEVSQ(4,5,8,7,11,4,3)')).toBe(48); + // 示例来自 https://support.microsoft.com/zh-cn/office/avedev-%E5%87%BD%E6%95%B0-58fe8d65-2a84-4dc7-8052-f3f87b5c6639 + expect(await evalFormual('ROUND(AVEDEV(4,5,6,7,5,4,3), 2)')).toBe(1.02); + // 示例来自 https://support.microsoft.com/zh-cn/office/harmean-%E5%87%BD%E6%95%B0-5efd9184-fab5-42f9-b1d3-57883a1d3bc6 + expect(await evalFormual('ROUND(HARMEAN(4,5,8,7,11,4,3), 3)')).toBe(5.028); + + expect(await evalFormual('LARGE([1,3,5,4,7,6], 3)')).toBe(5); + expect(await evalFormual('LARGE([1,3,5,4,7,6], 1)')).toBe(7); + + expect(await evalFormual('UPPERMONEY(7682.01)')).toBe('柒仟陆佰捌拾贰元壹分'); + expect(await evalFormual('UPPERMONEY(7682)')).toBe('柒仟陆佰捌拾贰元整'); + + // 非数字类型转换是否正常? + expect(await evalFormual('"3" + "3"')).toBe(6); + expect(await evalFormual('"3" - "3"')).toBe(0); + expect(await evalFormual('AVG(4, "6", "10", 10, 10)')).toBe(8); + expect(await evalFormual('MAX(4, "6", "10", 2, 3)')).toBe(10); + + expect(await evalFormual('"a" + "b"')).toBe('ab'); +}); + +test('formula:text', async () => { + expect(await evalFormual('LEFT("abcdefg", 2)')).toBe('ab'); + expect(await evalFormual('RIGHT("abcdefg", 2)')).toBe('fg'); + expect(await evalFormual('LENGTH("abcdefg")')).toBe(7); + expect(await evalFormual('LEN("abcdefg")')).toBe(7); + expect(await evalFormual('ISEMPTY("abcdefg")')).toBe(false); + expect(await evalFormual('ISEMPTY("")')).toBe(true); + expect(await evalFormual('CONCATENATE("a", "b", "c", "d")')).toBe('abcd'); + expect(await evalFormual('CHAR(97)')).toBe('a'); + expect(await evalFormual('LOWER("AB")')).toBe('ab'); + expect(await evalFormual('UPPER("ab")')).toBe('AB'); + expect(await evalFormual('SPLIT("a,b,c")')).toMatchObject(['a', 'b', 'c']); + expect(await evalFormual('TRIM(" ab ")')).toBe('ab'); + expect(await evalFormual('STARTSWITH("xab", "ab")')).toBe(false); + expect(await evalFormual('STARTSWITH("xab", "x")')).toBe(true); + expect(await evalFormual('ENDSWITH("xab", "x")')).toBe(false); + expect(await evalFormual('ENDSWITH("xab", "b")')).toBe(true); + expect(await evalFormual('UPPERFIRST("xab")')).toBe('Xab'); + expect(await evalFormual('PADSTART("5", 3, "0")')).toBe('005'); + expect(await evalFormual('PADSTART(5, 3, 0)')).toBe('005'); + expect(await evalFormual('CAPITALIZE("star")')).toBe('Star'); + expect(await evalFormual('ESCAPE("&")')).toBe('&'); + expect(await evalFormual('TRUNCATE("amis.baidu.com", 7)')).toBe('amis...'); + expect(await evalFormual('BEFORELAST("amis.baidu.com", ".")')).toBe( + 'amis.baidu' + ); + expect(await evalFormual('BEFORELAST("amis", ".")')).toBe('amis'); + expect(await evalFormual('STRIPTAG("amis")')).toBe('amis'); + expect(await evalFormual('LINEBREAK("am\nis")')).toBe('am
is'); + expect(await evalFormual('CONTAINS("xab", "x")')).toBe(true); + expect(await evalFormual('CONTAINS("xab", "b")')).toBe(true); + expect(await evalFormual('REPLACE("xabab", "ab", "cd")')).toBe('xcdcd'); + expect(await evalFormual('SEARCH("xabab", "ab")')).toBe(1); + expect(await evalFormual('SEARCH("xabab", "cd")')).toBe(-1); + expect(await evalFormual('SEARCH("xabab", "ab", 2)')).toBe(3); + expect(await evalFormual('MID("xabab", 2, 2)')).toBe('ba'); +}); + +test('formula:date', async () => { + expect(await evalFormual('TIMESTAMP(DATE(2021, 11, 21, 0, 0, 0), "x")')).toBe( + new Date(2021, 11, 21, 0, 0, 0).getTime() + ); + expect( + await evalFormual('DATETOSTR(DATE(2021, 11, 21, 0, 0, 0), "YYYY-MM-DD")') + ).toBe('2021-12-21'); + expect(await evalFormual('DATETOSTR(DATE("2021-12-21"), "YYYY-MM-DD")')).toBe( + '2021-12-21' + ); + expect(await evalFormual('DATETOSTR(TODAY(), "YYYY-MM-DD")')).toBe( + moment().format('YYYY-MM-DD') + ); + expect(await evalFormual('DATETOSTR(NOW(), "YYYY-MM-DD")')).toBe( + moment().format('YYYY-MM-DD') + ); + expect(await evalFormual('DATETOSTR(1676563200, "YYYY-MM-DD")')).toBe( + moment(1676563200, 'X').format('YYYY-MM-DD') + ); + expect(await evalFormual('DATETOSTR(1676563200000, "YYYY-MM-DD")')).toBe( + moment(1676563200000, 'x').format('YYYY-MM-DD') + ); + expect(await evalFormual('DATETOSTR("12/25/2022", "YYYY-MM-DD")')).toBe( + moment('12/25/2022').format('YYYY-MM-DD') + ); + expect(await evalFormual('DATETOSTR("12-25-2022", "YYYY/MM/DD")')).toBe( + moment('12-25-2022').format('YYYY/MM/DD') + ); + expect(await evalFormual('DATETOSTR("2022年12月25日", "YYYY/MM/DD")')).toBe( + moment('2022年12月25日', 'YYYY-MM-DD').format('YYYY/MM/DD') + ); + expect( + await evalFormual( + 'DATETOSTR("2022年12月25日 14时23分56秒", "YYYY/MM/DD HH:mm:ss")' + ) + ).toBe( + moment('2022年12月25日 14时23分56秒', 'YYYY-MM-DD HH:mm:ss').format( + 'YYYY/MM/DD HH:mm:ss' + ) + ); + expect(await evalFormual('DATETOSTR("20230105", "YYYY/MM/DD")')).toBe( + moment('20230105', 'YYYY-MM-DD').format('YYYY/MM/DD') + ); + expect(await evalFormual('DATETOSTR("2023.01.05", "YYYY/MM/DD")')).toBe( + moment('2023.01.05', 'YYYY-MM-DD').format('YYYY/MM/DD') + ); + expect( + await evalFormual( + 'DATETOSTR("2010-10-20 4:30 +0000", "YYYY-MM-DD HH:mm Z")' + ) + ).toBe(moment('2010-10-20 4:30 +0000').format('YYYY-MM-DD HH:mm Z')); + expect( + await evalFormual( + 'DATETOSTR("2013-02-04T10:35:24-08:00", "YYYY-MM-DD HH:mm:ss")' + ) + ).toBe(moment('2013-02-04T10:35:24-08:00').format('YYYY-MM-DD HH:mm:ss')); + expect(await evalFormual('YEAR(STRTODATE("2021-10-24 10:10:10"))')).toBe( + 2021 + ); + expect( + await evalFormual( + 'DATERANGESPLIT("1676563200,1676735999", undefined, "YYYY.MM.DD hh:mm:ss")' + ) + ).toEqual(['2023.02.17 12:00:00', '2023.02.18 11:59:59']); + expect(await evalFormual('DATERANGESPLIT("1676563200,1676735999", 0)')).toBe( + '1676563200' + ); + expect( + await evalFormual( + 'DATERANGESPLIT("1676563200,1676735999", 0 , "YYYY.MM.DD hh:mm:ss")' + ) + ).toBe('2023.02.17 12:00:00'); + expect( + await evalFormual( + 'DATERANGESPLIT("1676563200,1676735999", "start" , "YYYY.MM.DD hh:mm:ss")' + ) + ).toBe('2023.02.17 12:00:00'); + expect( + await evalFormual( + 'DATERANGESPLIT("1676563200,1676735999", 1 , "YYYY.MM.DD hh:mm:ss")' + ) + ).toBe('2023.02.18 11:59:59'); + expect( + await evalFormual( + 'DATERANGESPLIT("1676563200,1676735999", "end" , "YYYY.MM.DD hh:mm:ss")' + ) + ).toBe('2023.02.18 11:59:59'); + expect(await evalFormual('WEEKDAY("2023-02-27")')).toBe( + moment('2023-02-27').weekday() + ); + expect(await evalFormual('WEEKDAY("2023-02-27", 2)')).toBe( + moment('2023-02-27').isoWeekday() + ); + expect(await evalFormual('WEEK("2023-03-05")')).toBe( + moment('2023-03-05').week() + ); + expect( + await evalFormual( + 'BETWEENRANGE("2023-03-08", ["2023-03-01", "2024-04-07"], "year")' + ) + ).toBe( + moment('2023-03-08').isBetween('2023-03-01', '2024-04-07', 'year', '[]') + ); + expect( + await evalFormual( + 'BETWEENRANGE("2022-03-08", ["2023-03-01", "2024-04-07"], "year")' + ) + ).toBe( + moment('2022-03-08').isBetween('2023-03-01', '2024-04-07', 'year', '[]') + ); + expect( + await evalFormual( + 'BETWEENRANGE("2023-03-08", ["2023-03-01", "2023-04-07"], "month")' + ) + ).toBe( + moment('2023-03-08').isBetween('2023-03-01', '2023-04-07', 'month', '[]') + ); + expect( + await evalFormual( + 'BETWEENRANGE("2023-05-08", ["2023-03-01", "2023-04-07", "month"])' + ) + ).toBe( + moment('2023-05-08').isBetween('2023-03-01', '2023-04-07', 'month', '[]') + ); + expect( + await evalFormual( + 'BETWEENRANGE("2023-03-06", ["2023-03-01", "2023-05-07"])' + ) + ).toBe( + moment('2023-03-06').isBetween('2023-03-01', '2023-05-07', 'day', '[]') + ); + expect( + await evalFormual( + 'BETWEENRANGE("2023-05-08", ["2023-03-01", "2023-05-07"])' + ) + ).toBe( + moment('2023-05-08').isBetween('2023-03-01', '2023-05-07', 'day', '[]') + ); + expect( + await evalFormual( + 'BETWEENRANGE("2023-05-07", ["2023-03-01", "2023-05-07"], "day", "()")' + ) + ).toBe( + moment('2023-05-07').isBetween('2023-03-01', '2023-05-07', 'day', '()') + ); + expect( + await evalFormual( + 'CONCATENATE(STARTOF("2023-02-28", "day"), "," ,ENDOF("2023-02-28", "day"))' + ) + ).toBe( + `${moment('2023-02-28').startOf('day').toDate()},${moment('2023-02-28') + .endOf('day') + .toDate()}` + ); + expect( + await evalFormual( + 'CONCATENATE(STARTOF("2023-02-28", "day", "YYYY-MM-DD HH:mm:ss"), ",", ENDOF("2023-02-28", "day", "YYYY-MM-DD HH:mm:ss"))' + ) + ).toBe( + `${moment('2023-02-28') + .startOf('day') + .format('YYYY-MM-DD HH:mm:ss')},${moment('2023-02-28') + .endOf('day') + .format('YYYY-MM-DD HH:mm:ss')}` + ); + expect( + await evalFormual( + 'CONCATENATE(STARTOF("2023-02-28", "day", "X"), "," ,ENDOF("2023-02-28", "day", "X"))' + ) + ).toBe( + `${moment('2023-02-28').startOf('day').format('X')},${moment('2023-02-28') + .endOf('day') + .format('X')}` + ); +}); + +test('formula:last', async () => { + expect(await evalFormual('LAST([1, 2, 3])')).toBe(3); +}); + +test('formula:basename', async () => { + expect(await evalFormual('BASENAME("/home/amis/a.json")')).toBe('a.json'); +}); + +test('formula:customFunction', async () => { + registerFunction('ASYNCFUNCTION', async input => { + const response = await Promise.resolve({ + status: 0, + data: { + name: 'amis' + } + }); + return response; + }); + + expect(await evalFormual('ASYNCFUNCTION()')).toMatchObject({ + status: 0, + data: { + name: 'amis' + } + }); +}); diff --git a/packages/amis-formula/src/evalutor.ts b/packages/amis-formula/src/evalutor.ts index 6ff031ee4..82aa9d9f3 100644 --- a/packages/amis-formula/src/evalutor.ts +++ b/packages/amis-formula/src/evalutor.ts @@ -2265,7 +2265,7 @@ export class Evaluator { } } -function getCookie(name: string) { +export function getCookie(name: string) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) { @@ -2274,7 +2274,7 @@ function getCookie(name: string) { return undefined; } -function parseJson(str: string, defaultValue?: any) { +export function parseJson(str: string, defaultValue?: any) { try { return JSON.parse(str); } catch (e) { @@ -2282,7 +2282,7 @@ function parseJson(str: string, defaultValue?: any) { } } -function stripNumber(number: number) { +export function stripNumber(number: number) { if (typeof number === 'number' && !Number.isInteger(number)) { return parseFloat(number.toPrecision(16)); } else { @@ -2292,7 +2292,7 @@ function stripNumber(number: number) { // 如果只有一个成员,同时第一个成员为 args // 则把它展开,当成是多个参数,毕竟公式里面还不支持 ...args 语法, -function normalizeArgs(args: Array) { +export function normalizeArgs(args: Array) { if (args.length === 1 && Array.isArray(args[0])) { args = args[0]; } diff --git a/packages/amis-formula/src/evalutorForAsync.ts b/packages/amis-formula/src/evalutorForAsync.ts new file mode 100644 index 000000000..d95516327 --- /dev/null +++ b/packages/amis-formula/src/evalutorForAsync.ts @@ -0,0 +1,659 @@ +/** + * @file 公式内置函数 + */ +import {FilterContext} from './types'; +import {createObject, Evaluator, stripNumber} from './evalutor'; + +export async function runSequence( + arr: Array, + fn: (item: T, index: number) => Promise +) { + const result: Array = []; + + await arr.reduce(async (promise, item: T, index: number) => { + await promise; + result.push(await fn(item, index)); + }, Promise.resolve()); + + return result; +} + +export class AsyncEvaluator extends (Evaluator as any) { + constructor(data: any, options?: any) { + super(data, options); + } + + async document(ast: {type: 'document'; body: Array}) { + if (!ast.body.length) { + return undefined; + } + const isString = ast.body.length > 1; + const content = await runSequence(ast.body, async item => { + let result = this.evalute(item); + + if (isString && result == null) { + // 不要出现 undefined, null 之类的文案 + return ''; + } + + return result; + }); + + return content.length === 1 ? content[0] : content.join(''); + } + + async filter(ast: { + type: 'filter'; + input: any; + filters: Array<{name: string; args: Array}>; + }) { + let input = await this.evalute(ast.input); + const filters = ast.filters.concat(); + const context: FilterContext = { + filter: undefined, + data: this.context, + restFilters: filters + }; + + const result = await filters.reduce(async (ps, filter, index) => { + const input = await ps; + const fn = this.filters[filter.name]; + + if (!fn) { + throw new Error(`filter \`${filter.name}\` not exists.`); + } + context.filter = filter; + + const argsRes = await filter.args.reduce(async (promise, item) => { + await promise; + if (item?.type === 'mixed') { + return runSequence(item.body, item => + typeof item === 'string' ? item : this.evalute(item) + ); + } else if (item.type) { + return this.evalute(item); + } + return item; + }, Promise.resolve([])); + + return fn.apply(context, [input].concat(argsRes)); + }, Promise.resolve(input)); + + return result; + } + + async template(ast: {type: 'template'; body: Array}) { + const args = await runSequence(ast.body, arg => this.evalute(arg)); + return args.join(''); + } + + // 下标获取 + async getter(ast: {host: any; key: any}) { + const host = await this.evalute(ast.host); + let key = await this.evalute(ast.key); + if (typeof key === 'undefined' && ast.key?.type === 'variable') { + key = ast.key.name; + } + return host?.[key]; + } + + // 位操作如 +2 ~3 ! + async unary(ast: {op: '+' | '-' | '~' | '!'; value: any}) { + let value = await this.evalute(ast.value); + + switch (ast.op) { + case '+': + return +value; + case '-': + return -value; + case '~': + return ~value; + case '!': + return !value; + } + } + + async power(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + return Math.pow(this.formatNumber(left), this.formatNumber(right)); + } + + async multiply(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + return stripNumber(this.formatNumber(left) * this.formatNumber(right)); + } + + async divide(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + return stripNumber(this.formatNumber(left) / this.formatNumber(right)); + } + + async remainder(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + return this.formatNumber(left) % this.formatNumber(right); + } + + async add(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + // 如果有一个不是数字就变成字符串拼接 + if (isNaN(left) || isNaN(right)) { + return left + right; + } + return stripNumber(this.formatNumber(left) + this.formatNumber(right)); + } + + async minus(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + return stripNumber(this.formatNumber(left) - this.formatNumber(right)); + } + + async shift(ast: {op: '<<' | '>>' | '>>>'; left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.formatNumber(this.evalute(ast.right), true); + + if (ast.op === '<<') { + return left << right; + } else if (ast.op == '>>') { + return left >> right; + } else { + return left >>> right; + } + } + + async lt(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + + // todo 如果是日期的对比,这个地方可以优化一下。 + + return left < right; + } + + async gt(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + + // todo 如果是日期的对比,这个地方可以优化一下。 + return left > right; + } + + async le(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + + // todo 如果是日期的对比,这个地方可以优化一下。 + + return left <= right; + } + + async ge(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + + // todo 如果是日期的对比,这个地方可以优化一下。 + + return left >= right; + } + + async eq(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + + // todo 如果是日期的对比,这个地方可以优化一下。 + + return left == right; + } + + async ne(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + + // todo 如果是日期的对比,这个地方可以优化一下。 + + return left != right; + } + + async streq(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + + // todo 如果是日期的对比,这个地方可以优化一下。 + + return left === right; + } + + async strneq(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + + // todo 如果是日期的对比,这个地方可以优化一下。 + + return left !== right; + } + + async binary(ast: {op: '&' | '^' | '|'; left: any; right: any}) { + const left = await this.evalute(ast.left); + const right = await this.evalute(ast.right); + + if (ast.op === '&') { + return left & right; + } else if (ast.op === '^') { + return left ^ right; + } else { + return left | right; + } + } + + async and(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + return left && this.evalute(ast.right); + } + + async or(ast: {left: any; right: any}) { + const left = await this.evalute(ast.left); + return left || this.evalute(ast.right); + } + + array(ast: {type: 'array'; members: Array}) { + return runSequence(ast.members, member => this.evalute(member)); + } + + async object(ast: {members: Array<{key: string; value: any}>}) { + let object: any = {}; + await ast.members.reduce( + async (promise: any, {key, value}: any, index: number) => { + await promise; + const objKey = await this.evalute(key); + const objVal = await this.evalute(value); + object[objKey] = objVal; + }, + Promise.resolve() + ); + + return object; + } + + async conditional(ast: { + type: 'conditional'; + test: any; + consequent: any; + alternate: any; + }) { + return (await this.evalute(ast.test)) + ? await this.evalute(ast.consequent) + : await this.evalute(ast.alternate); + } + + async funcCall(this: any, ast: {identifier: string; args: Array}) { + const fnName = `fn${ast.identifier}`; + const fn = + this.functions[fnName] || + this[fnName] || + (this.filters.hasOwnProperty(ast.identifier) && + this.filters[ast.identifier]); + + if (!fn) { + throw new Error(`${ast.identifier}函数没有定义`); + } + + let args: Array = ast.args; + + // 逻辑函数特殊处理,因为有时候有些运算是可以跳过的。 + if (~['IF', 'AND', 'OR', 'XOR', 'IFS'].indexOf(ast.identifier)) { + args = args.map(a => () => this.evalute(a)); + } else { + args = await runSequence(args, a => this.evalute(a)); + } + + return fn.apply(this, args); + } + + async callAnonymousFunction( + ast: { + args: any[]; + return: any; + }, + args: Array + ) { + const ctx: any = createObject( + this.contextStack[this.contextStack.length - 1]('&') || {}, + {} + ); + ast.args.forEach((arg: any) => { + if (arg.type !== 'variable') { + throw new Error('expected a variable as argument'); + } + ctx[arg.name] = args.shift(); + }); + this.contextStack.push((varName: string) => + varName === '&' ? ctx : ctx[varName] + ); + const result = await this.evalute(ast.return); + this.contextStack.pop(); + return result; + } + + /** + * 示例:IF(A, B, C) + * + * 如果满足条件A,则返回B,否则返回C,支持多层嵌套IF函数。 + * + * 也可以用表达式如:A ? B : C + * + * @example IF(condition, consequent, alternate) + * @param {expression} condition - 条件表达式. + * @param {any} consequent 条件判断通过的返回结果 + * @param {any} alternate 条件判断不通过的返回结果 + * @namespace 逻辑函数 + * + * @returns {any} 根据条件返回不同的结果 + */ + async fnIF( + condition: () => any, + trueValue: () => any, + falseValue: () => any + ) { + return (await condition()) ? await trueValue() : await falseValue(); + } + + /** + * 条件全部符合,返回 true,否则返回 false + * + * 示例:AND(语文成绩>80, 数学成绩>80) + * + * 语文成绩和数学成绩都大于 80,则返回 true,否则返回 false + * + * 也可以直接用表达式如:语文成绩>80 && 数学成绩>80 + * + * @example AND(expression1, expression2, ...expressionN) + * @param {...expression} conditions - 条件表达式. + * @namespace 逻辑函数 + * + * @returns {boolean} + */ + async fnAND(...condtions: Array<() => any>) { + if (!condtions.length) { + return false; + } + + return condtions.reduce(async (promise, c) => { + const result = await promise; + if (result) { + return c(); + } + return result; + }, Promise.resolve(true)); + } + + /** + * 条件任意一个满足条件,返回 true,否则返回 false + * + * 示例:OR(语文成绩>80, 数学成绩>80) + * + * 语文成绩和数学成绩任意一个大于 80,则返回 true,否则返回 false + * + * 也可以直接用表达式如:语文成绩>80 || 数学成绩>80 + * + * @example OR(expression1, expression2, ...expressionN) + * @param {...expression} conditions - 条件表达式. + * @namespace 逻辑函数 + * + * @returns {boolean} + */ + async fnOR(...condtions: Array<() => any>) { + if (!condtions.length) { + return false; + } + + return condtions.reduce(async (promise, c) => { + const result = await promise; + if (result) { + return true; + } + return c(); + }, Promise.resolve(false)); + } + + /** + * 异或处理,多个表达式组中存在奇数个真时认为真。 + * + * @example XOR(condition1, condition2) + * @param {expression} condition1 - 条件表达式1 + * @param {expression} condition2 - 条件表达式2 + * @namespace 逻辑函数 + * + * @returns {boolean} + */ + async fnXOR(...condtions: Array<() => any>) { + if (!condtions.length) { + return false; + } + + return !!( + (await runSequence(condtions, c => c())).filter(item => item).length % 2 + ); + } + + /** + * 判断函数集合,相当于多个 else if 合并成一个。 + * + * 示例:IFS(语文成绩 > 80, "优秀", 语文成绩 > 60, "良", "继续努力") + * + * 如果语文成绩大于 80,则返回优秀,否则判断大于 60 分,则返回良,否则返回继续努力。 + * + * @example IFS(condition1, result1, condition2, result2,...conditionN, resultN) + * @param {...any} args - 条件,返回值集合 + * @namespace 逻辑函数 + * @returns {any} 第一个满足条件的结果,没有命中的返回 false。 + */ + async fnIFS(...args: Array<() => any>) { + if (args.length % 2) { + args.splice(args.length - 1, 0, () => true); + } + + while (args.length) { + const c = args.shift()!; + const v = args.shift()!; + + if (await c()) { + return await v(); + } + } + return; + } + + /** + * 数组做数据转换,需要搭配箭头函数一起使用,注意箭头函数只支持单表达式用法。 + * + * @param {Array} arr 数组 + * @param {Function} iterator 箭头函数 + * @namespace 数组 + * @example ARRAYMAP(arr, item => item) + * @returns {boolean} 结果 + */ + fnARRAYMAP(value: any, iterator: any) { + if (!iterator || iterator.type !== 'anonymous_function') { + throw new Error('expected an anonymous function get ' + iterator); + } + + return (Array.isArray(value) ? value : []).reduce( + async (promise, item, index) => { + const arr = await promise; + arr.push(await this.callAnonymousFunction(iterator, [item, index])); + return arr; + }, + Promise.resolve([]) + ); + } + + /** + * 数据做数据过滤,需要搭配箭头函数一起使用,注意箭头函数只支持单表达式用法。 + * 将第二个箭头函数返回为 false 的成员过滤掉。 + * + * @param {Array} arr 数组 + * @param {Function} iterator 箭头函数 + * @namespace 数组 + * @example ARRAYFILTER(arr, item => item) + * @returns {boolean} 结果 + */ + async fnARRAYFILTER(value: any, iterator: any) { + if (!iterator || iterator.type !== 'anonymous_function') { + throw new Error('expected an anonymous function get ' + iterator); + } + + return await (Array.isArray(value) ? value : []).reduce( + async (promise, item, index) => { + let arr = await promise; + const hit = await this.callAnonymousFunction(iterator, [item, index]); + + if (hit) { + arr.push(item); + } + + return arr; + }, + Promise.resolve([]) + ); + } + + /** + * 数据做数据查找,需要搭配箭头函数一起使用,注意箭头函数只支持单表达式用法。 + * 找出第二个箭头函数返回为 true 的成员的索引。 + * + * 示例: + * + * ARRAYFINDINDEX([0, 2, false], item => item === 2) 得到 1 + * + * @param {Array} arr 数组 + * @param {Function} iterator 箭头函数 + * @namespace 数组 + * @example ARRAYFINDINDEX(arr, item => item === 2) + * @returns {number} 结果 + */ + async fnARRAYFINDINDEX(arr: any[], iterator: any) { + if (!iterator || iterator.type !== 'anonymous_function') { + throw new Error('expected an anonymous function get ' + iterator); + } + + let hitIndex = -1; + + await (Array.isArray(arr) ? arr : []).reduce( + async (promise: any, item: any, index: number) => { + await promise; + const hit = await this.callAnonymousFunction(iterator, [item, index]); + + if (hit) { + hitIndex = index; + } + }, + Promise.resolve() + ); + + return hitIndex; + } + + /** + * 数据做数据查找,需要搭配箭头函数一起使用,注意箭头函数只支持单表达式用法。 + * 找出第二个箭头函数返回为 true 的成员。 + * + * 示例: + * + * ARRAYFIND([0, 2, false], item => item === 2) 得到 2 + * + * @param {Array} arr 数组 + * @param {Function} iterator 箭头函数 + * @namespace 数组 + * @example ARRAYFIND(arr, item => item === 2) + * @returns {any} 结果 + */ + async fnARRAYFIND(arr: any[], iterator: any) { + if (!iterator || iterator.type !== 'anonymous_function') { + throw new Error('expected an anonymous function get ' + iterator); + } + + let hitItem = undefined; + + await (Array.isArray(arr) ? arr : []).reduce( + async (promise: any, item: any, index: number) => { + await promise; + const hit = await this.callAnonymousFunction(iterator, [item, index]); + + if (hit) { + hitItem = item; + } + }, + Promise.resolve() + ); + + return hitItem; + } + + /** + * 数据做数据遍历判断,需要搭配箭头函数一起使用,注意箭头函数只支持单表达式用法。 + * 判断第二个箭头函数是否存在返回为 true 的成员。 + * + * 示例: + * + * ARRAYSOME([0, 2, false], item => item === 2) 得到 true + * + * @param {Array} arr 数组 + * @param {Function} iterator 箭头函数 + * @namespace 数组 + * @example ARRAYSOME(arr, item => item === 2) + * @returns {boolean} 结果 + */ + async fnARRAYSOME(arr: any[], iterator: any) { + if (!iterator || iterator.type !== 'anonymous_function') { + throw new Error('expected an anonymous function get ' + iterator); + } + + let result = await (Array.isArray(arr) ? arr : []).reduce( + async (promise: any, item: any, index: number) => { + const prev = await promise; + const hit = await this.callAnonymousFunction(iterator, [item, index]); + return prev || hit; + }, + Promise.resolve(false) + ); + + return result; + } + + /** + * 数据做数据遍历判断,需要搭配箭头函数一起使用,注意箭头函数只支持单表达式用法。 + * 判断第二个箭头函数返回是否都为 true。 + * + * 示例: + * + * ARRAYEVERY([0, 2, false], item => item === 2) 得到 false + * + * @param {Array} arr 数组 + * @param {Function} iterator 箭头函数 + * @namespace 数组 + * @example ARRAYEVERY(arr, item => item === 2) + * @returns {boolean} 结果 + */ + async fnARRAYEVERY(arr: any[], iterator: any) { + if (!iterator || iterator.type !== 'anonymous_function') { + throw new Error('expected an anonymous function get ' + iterator); + } + + let result = await (Array.isArray(arr) ? arr : []).reduce( + async (promise: any, item: any, index: number) => { + const prev = await promise; + const hit = await this.callAnonymousFunction(iterator, [item, index]); + + return prev && hit; + }, + Promise.resolve(true) + ); + + return result; + } +} diff --git a/packages/amis-formula/src/index.ts b/packages/amis-formula/src/index.ts index 23dfcd66f..61c06fac7 100644 --- a/packages/amis-formula/src/index.ts +++ b/packages/amis-formula/src/index.ts @@ -1,4 +1,5 @@ import {Evaluator} from './evalutor'; +import {AsyncEvaluator} from './evalutorForAsync'; import {parse} from './parser'; import {lexer} from './lexer'; import {registerFilter, filters, getFilters, extendsFilters} from './filter'; @@ -13,6 +14,7 @@ export { parse, lexer, Evaluator, + AsyncEvaluator, FilterContext, filters, getFilters, @@ -34,4 +36,18 @@ export function evaluate( return new Evaluator(data, options).evalute(ast); } +export async function evaluateForAsync( + astOrString: string | ASTNode, + data: any, + options?: ParserOptions & EvaluatorOptions +) { + let ast: ASTNode = astOrString as ASTNode; + if (typeof astOrString === 'string') { + ast = parse(astOrString, options); + } + + return new AsyncEvaluator(data, options).evalute(ast); +} + Evaluator.setDefaultFilters(getFilters()); +AsyncEvaluator.setDefaultFilters(getFilters()); diff --git a/packages/amis-ui/src/components/condition-builder/Expression.tsx b/packages/amis-ui/src/components/condition-builder/Expression.tsx index fb78ad820..627eab8a9 100644 --- a/packages/amis-ui/src/components/condition-builder/Expression.tsx +++ b/packages/amis-ui/src/components/condition-builder/Expression.tsx @@ -1,13 +1,8 @@ import { - ExpressionComplex, ConditionBuilderField, ConditionBuilderFuncs, ConditionFieldFunc, - ExpressionFunc, - ConditionBuilderType, - FieldSimple, - FieldGroup, - OperatorType + FieldSimple } from './types'; import React from 'react'; import ConditionField from './Field'; @@ -27,6 +22,7 @@ import ConditionFunc from './Func'; import {ConditionBuilderConfig} from './config'; import Formula from './Formula'; import {FormulaPickerProps} from '../formula/Picker'; +import type {ExpressionComplex, OperatorType, ExpressionFunc} from 'amis-core'; /** * 支持4中表达式设置方式 diff --git a/packages/amis-ui/src/components/condition-builder/Func.tsx b/packages/amis-ui/src/components/condition-builder/Func.tsx index 6391cb59b..8f8c1fd6e 100644 --- a/packages/amis-ui/src/components/condition-builder/Func.tsx +++ b/packages/amis-ui/src/components/condition-builder/Func.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { ConditionFieldFunc, - ExpressionFunc, ConditionBuilderField, ConditionBuilderFuncs } from './types'; @@ -21,6 +20,7 @@ import ResultBox from '../ResultBox'; import {Icon} from '../icons'; import Expression from './Expression'; import {ConditionBuilderConfig} from './config'; +import type {ExpressionFunc} from 'amis-core'; export interface ConditionFuncProps extends ThemeProps, LocaleProps { value: ExpressionFunc; diff --git a/packages/amis-ui/src/components/condition-builder/Group.tsx b/packages/amis-ui/src/components/condition-builder/Group.tsx index 527a226de..848aa65b0 100644 --- a/packages/amis-ui/src/components/condition-builder/Group.tsx +++ b/packages/amis-ui/src/components/condition-builder/Group.tsx @@ -1,9 +1,5 @@ import React from 'react'; -import { - ConditionBuilderFields, - ConditionGroupValue, - ConditionBuilderFuncs -} from './types'; +import {ConditionBuilderFields, ConditionBuilderFuncs} from './types'; import { ThemeProps, themeable, @@ -11,7 +7,8 @@ import { utils, localeable, LocaleProps, - guid + guid, + ConditionGroupValue } from 'amis-core'; import Button from '../Button'; import GroupOrItem from './GroupOrItem'; diff --git a/packages/amis-ui/src/components/condition-builder/GroupOrItem.tsx b/packages/amis-ui/src/components/condition-builder/GroupOrItem.tsx index 7ee901014..8803e2c65 100644 --- a/packages/amis-ui/src/components/condition-builder/GroupOrItem.tsx +++ b/packages/amis-ui/src/components/condition-builder/GroupOrItem.tsx @@ -1,10 +1,5 @@ import {ConditionBuilderConfig} from './config'; -import { - ConditionBuilderFields, - ConditionGroupValue, - ConditionBuilderFuncs, - ConditionValue -} from './types'; +import {ConditionBuilderFields, ConditionBuilderFuncs} from './types'; import {ThemeProps, themeable, autobind} from 'amis-core'; import React from 'react'; import {Icon} from '../icons'; @@ -12,6 +7,7 @@ import ConditionGroup from './Group'; import ConditionItem from './Item'; import {FormulaPickerProps} from '../formula/Picker'; import Button from '../Button'; +import type {ConditionGroupValue, ConditionValue} from 'amis-core'; export interface CBGroupOrItemProps extends ThemeProps { builderMode?: 'simple' | 'full'; diff --git a/packages/amis-ui/src/components/condition-builder/Item.tsx b/packages/amis-ui/src/components/condition-builder/Item.tsx index 545093012..56d97ebfc 100644 --- a/packages/amis-ui/src/components/condition-builder/Item.tsx +++ b/packages/amis-ui/src/components/condition-builder/Item.tsx @@ -2,15 +2,10 @@ import React from 'react'; import {findDOMNode} from 'react-dom'; import { ConditionBuilderFields, - ConditionRule, ConditionBuilderFuncs, - ExpressionFunc, ConditionFieldFunc, ConditionBuilderField, - FieldSimple, - ExpressionField, - OperatorType, - ExpressionComplex + FieldSimple } from './types'; import { ThemeProps, @@ -30,7 +25,14 @@ import GroupedSelection from '../GroupedSelection'; import ResultBox from '../ResultBox'; import {FormulaPickerProps} from '../formula/Picker'; -import type {PlainObject} from 'amis-core'; +import type { + PlainObject, + ConditionRule, + OperatorType, + ExpressionFunc, + ExpressionField, + ExpressionComplex +} from 'amis-core'; const option2value = (item: any) => item.value; diff --git a/packages/amis-ui/src/components/condition-builder/Value.tsx b/packages/amis-ui/src/components/condition-builder/Value.tsx index af99f877f..36feff45a 100644 --- a/packages/amis-ui/src/components/condition-builder/Value.tsx +++ b/packages/amis-ui/src/components/condition-builder/Value.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {FieldSimple, OperatorType} from './types'; +import {FieldSimple} from './types'; import {ThemeProps, themeable, localeable, LocaleProps} from 'amis-core'; import InputBox from '../InputBox'; import NumberInput from '../NumberInput'; @@ -7,6 +7,7 @@ import DatePicker from '../DatePicker'; import {SelectWithRemoteOptions as Select} from '../Select'; import Switch from '../Switch'; import {FormulaPicker, FormulaPickerProps} from '../formula/Picker'; +import type {OperatorType} from 'amis-core'; export interface ValueProps extends ThemeProps, LocaleProps { value: any; diff --git a/packages/amis-ui/src/components/condition-builder/config.ts b/packages/amis-ui/src/components/condition-builder/config.ts index c81dac234..155de7284 100644 --- a/packages/amis-ui/src/components/condition-builder/config.ts +++ b/packages/amis-ui/src/components/condition-builder/config.ts @@ -1,10 +1,10 @@ import { FieldTypes, - OperatorType, ConditionBuilderFuncs, ConditionBuilderFields, ConditionBuilderType } from './types'; +import type {OperatorType} from 'amis-core'; export interface BaseFieldConfig { operations: Array; diff --git a/packages/amis-ui/src/components/condition-builder/index.tsx b/packages/amis-ui/src/components/condition-builder/index.tsx index 797e950f7..68c434334 100644 --- a/packages/amis-ui/src/components/condition-builder/index.tsx +++ b/packages/amis-ui/src/components/condition-builder/index.tsx @@ -14,16 +14,13 @@ import { noop } from 'amis-core'; import {uncontrollable} from 'amis-core'; -import { - ConditionBuilderFields, - ConditionGroupValue, - ConditionBuilderFuncs -} from './types'; +import {ConditionBuilderFields, ConditionBuilderFuncs} from './types'; import ConditionGroup from './Group'; import defaultConfig, {ConditionBuilderConfig} from './config'; import {FormulaPickerProps} from '../formula/Picker'; import PickerContainer from '../PickerContainer'; import ResultBox from '../ResultBox'; +import type {ConditionGroupValue} from 'amis-core'; export interface ConditionBuilderProps extends ThemeProps, LocaleProps { builderMode?: 'simple' | 'full'; // 简单模式|完整模式 diff --git a/packages/amis-ui/src/components/condition-builder/types.ts b/packages/amis-ui/src/components/condition-builder/types.ts index b09ad29b7..b6f50311e 100644 --- a/packages/amis-ui/src/components/condition-builder/types.ts +++ b/packages/amis-ui/src/components/condition-builder/types.ts @@ -1,4 +1,4 @@ -import {Api, BaseApiObject} from 'amis-core'; +import type {BaseApiObject, OperatorType} from 'amis-core'; export type FieldTypes = | 'text' @@ -10,78 +10,11 @@ export type FieldTypes = | 'select' | 'custom'; -export type OperatorType = - | 'equal' - | 'not_equal' - | 'is_empty' - | 'is_not_empty' - | 'like' - | 'not_like' - | 'starts_with' - | 'ends_with' - | 'less' - | 'less_or_equal' - | 'greater' - | 'greater_or_equal' - | 'between' - | 'not_between' - | 'select_equals' - | 'select_not_equals' - | 'select_any_in' - | 'select_not_any_in' - | { - label: string; - value: string; - }; - export type FieldItem = { type: 'text'; operators: Array; }; -export type ExpressionSimple = string | number | object | undefined; -export type ExpressionValue = - | ExpressionSimple - | { - type: 'value'; - value: ExpressionSimple; - }; -export type ExpressionFunc = { - type: 'func'; - func: string; - args: Array; -}; -export type ExpressionField = { - type: 'field'; - field: string; -}; -export type ExpressionFormula = { - type: 'formula'; - value: string; -}; - -export type ExpressionComplex = - | ExpressionValue - | ExpressionFunc - | ExpressionField - | ExpressionFormula; - -export interface ConditionRule { - id: any; - left?: ExpressionComplex; - op?: OperatorType; - right?: ExpressionComplex | Array; -} - -export interface ConditionGroupValue { - id: string; - conjunction: 'and' | 'or'; - not?: boolean; - children?: Array; -} - -export interface ConditionValue extends ConditionGroupValue {} - interface customOperator { lable: string; value: string;