feat: variable supported helpers

This commit is contained in:
sheldon66 2025-03-20 21:27:54 +08:00
parent dd2c1b64c6
commit 22a9f2bd6e
5 changed files with 272 additions and 2 deletions

View File

@ -0,0 +1,158 @@
# VariableFilterMapping 使用指南
## 概述
VariableFilterMapping 是 NocoBase 中一个用于处理变量助手条件映射的工具类。它允许开发者定义变量与助手条件之间的映射关系,使系统能够根据变量值动态生成数据查询条件。
## 主要功能
- 将变量值转换为数据查询条件
- 支持多种助手操作符
- 处理不同类型的变量(字符串、数字、日期等)
- 支持自定义变量处理器
## 基本用法
### 1. 定义助手条件映射
```typescript
import { VariableFilterMapping } from '@nocobase/database';
const helperMapping = new VariableFilterMapping({
'user.id': '$user.id', // 映射用户ID
'user.name': '$user.name', // 映射用户名
'created_at': '$dateRange', // 映射日期范围
'status': '$status' // 映射状态
});
```
### 2. 使用映射生成助手条件
```typescript
// 变量值
const variables = {
'$user.id': 1,
'$user.name': 'admin',
'$dateRange': ['2023-01-01', '2023-12-31'],
'$status': 'active'
};
// 生成助手条件
const helper = helperMapping.toHelper(variables);
```
生成的助手条件类似于:
```javascript
{
'user.id': 1,
'user.name': 'admin',
'created_at': {
$between: ['2023-01-01', '2023-12-31']
},
'status': 'active'
}
```
## 高级用法
### 1. 自定义操作符
```typescript
const helperMapping = new VariableFilterMapping({
'price': {
name: '$price',
operator: '$gt' // 使用大于操作符
}
});
const helper = helperMapping.toHelper({
'$price': 100
});
// 生成: { price: { $gt: 100 } }
```
### 2. 嵌套映射
```typescript
const helperMapping = new VariableFilterMapping({
'product': {
'price': {
name: '$product.price',
operator: '$between'
}
}
});
const helper = helperMapping.toHelper({
'$product.price': [100, 200]
});
// 生成: { product: { price: { $between: [100, 200] } } }
```
### 3. 使用处理器
```typescript
const helperMapping = new VariableFilterMapping({
'tags': {
name: '$tags',
operator: '$match',
processor: (value) => {
// 将逗号分隔的标签转换为数组
return value.split(',');
}
}
});
const helper = helperMapping.toHelper({
'$tags': 'javascript,typescript,react'
});
// 生成: { tags: { $match: ['javascript', 'typescript', 'react'] } }
```
## 实际应用场景
1. **数据表格筛选**:根据用户界面中的筛选选项动态生成查询条件
2. **权限控制**:基于当前用户信息生成数据访问限制
3. **报表生成**:使用用户选择的参数动态生成报表数据查询条件
4. **工作流触发条件**:定义基于变量的工作流触发条件
## 注意事项
- 变量名必须以 `$` 开头
- 确保变量值与期望的操作符兼容(例如,$between 操作符需要数组值)
- 注意处理变量值为 null 或 undefined 的情况
- 复杂的助手逻辑可能需要使用处理器函数进行转换
## API 参考
### VariableFilterMapping 构造函数
```typescript
constructor(options: Record<string, MappingOptions | string>)
```
### MappingOptions 接口
```typescript
interface MappingOptions {
name: string; // 变量名
operator?: string; // 操作符名称
processor?: (value) => any; // 值处理器
}
```
### toHelper 方法
```typescript
toHelper(variables: Record<string, any>): Record<string, any>
```
将变量值转换为助手条件。
## 总结
VariableFilterMapping 是 NocoBase 中强大的查询条件生成工具,它通过定义变量与助手条件的映射关系,使应用能够基于动态变量生成灵活的数据查询条件。掌握这一工具可以帮助开发者更有效地实现动态数据筛选、权限控制和自定义业务逻辑。

View File

@ -19,6 +19,48 @@ import { useApp } from '../../../../application';
import { SchemaComponent } from '../../../core/SchemaComponent';
import { useVariable } from '../VariableProvider';
import { helpersObs, rawHelpersObs, removeHelper } from './observables';
import { VariableHelperMapping } from '../Input';
import minimatch from 'minimatch';
/**
* Escapes special glob characters in a string
* @param str The string to escape
* @returns The escaped string
*/
function escapeGlob(str: string): string {
return str.replace(/[?*[\](){}!|+@\\]/g, '\\$&');
}
/**
* Tests if a filter is allowed for a given variable based on the variableHelperMapping configuration
* @param variableName The name of the variable to test
* @param filterName The name of the filter to test
* @param mapping The variable helper mapping configuration
* @returns boolean indicating if the filter is allowed for the variable
*/
export function isFilterAllowedForVariable(
variableName: string,
filterName: string,
mapping?: VariableHelperMapping,
): boolean {
if (!mapping?.rules) {
return true; // If no rules defined, allow all filters
}
// Check each rule
for (const rule of mapping.rules) {
// Check if variable matches the pattern
// We don't escape the pattern since it's meant to be a glob pattern
// But we escape the variable name since it's a literal value
if (minimatch(escapeGlob(variableName), rule.variables)) {
// Check if filter matches any of the allowed patterns
return rule.filters.some((pattern) => minimatch(filterName, pattern));
}
}
// If no matching rule found and strictMode is true, deny the filter
return !mapping.strictMode;
}
export const HelperConfiguator = observer(
({ index, close }: { index: number; close: () => void }) => {

View File

@ -12,7 +12,6 @@ import { css, cx } from '@emotion/css';
import { autorun } from '@formily/reactive';
import { useForm, observer } from '@formily/react';
import { error } from '@nocobase/utils/client';
import { cloneDeep } from 'lodash';
import { extractTemplateElements, composeTemplate } from '@nocobase/json-template-parser';
import {
Input as AntInput,
@ -46,6 +45,23 @@ type ParseOptions = {
stringToDate?: boolean;
};
/**
* Configuration for mapping variables to their allowed filter functions
*/
interface VariableHelperRule {
/** Pattern to match variables, supports glob patterns */
variables: string;
/** Array of allowed filter patterns, supports glob patterns */
filters: string[];
}
interface VariableHelperMapping {
/** Array of rules defining which filters are allowed for which variables */
rules: VariableHelperRule[];
/** Optional flag to determine if unlisted combinations should be allowed */
strictMode?: boolean;
}
function parseValue(value: any, options: ParseOptions = {}): string | string[] {
if (value == null || (Array.isArray(value) && value.length === 0)) {
return 'null';
@ -200,6 +216,7 @@ export type VariableInputProps = {
className?: string;
parseOptions?: ParseOptions;
hideVariableButton?: boolean;
variableHelperMapping?: VariableHelperMapping;
};
export function Input(props: VariableInputProps) {

View File

@ -14,7 +14,7 @@ interface VariableContextValue {
value: any;
}
interface eProviderProps {
interface VariableProviderProps {
variableName: string;
children: React.ReactNode;
}

View File

@ -25,6 +25,23 @@ import { useCurrentUserVariable } from './hooks/useUserVariable';
import { useVariableOptions } from './hooks/useVariableOptions';
import { Option } from './type';
/**
* Configuration for mapping variables to their allowed filter functions
*/
interface VariableFilterRule {
/** Pattern to match variables, supports glob patterns */
variables: string;
/** Array of allowed filter patterns, supports glob patterns */
filters: string[];
}
interface VariableFilterMapping {
/** Array of rules defining which filters are allowed for which variables */
rules: VariableFilterRule[];
/** Optional flag to determine if unlisted combinations should be allowed */
strictMode?: boolean;
}
interface GetShouldChangeProps {
collectionField: CollectionFieldOptions_deprecated;
variables: VariablesContextType;
@ -43,6 +60,8 @@ type Props = {
onChange: (value: any, optionPath?: any[]) => void;
renderSchemaComponent: (props: RenderSchemaComponentProps) => any;
schema?: any;
/** Configuration for mapping variables to their allowed filter functions */
variableFilterMapping?: VariableFilterMapping;
/** 消费变量值的字段 */
targetFieldSchema?: Schema;
children?: any;
@ -344,3 +363,37 @@ export function useCompatOldVariables(props: {
return { compatOldVariables };
}
/**
* Check if a variable and filter combination is allowed according to the mapping rules
*/
function isFilterAllowedForVariable(
variable: string,
filter: string,
rules: VariableFilterRule[],
strictMode = false,
): boolean {
// If no rules defined and not in strict mode, allow everything
if (!rules?.length && !strictMode) {
return true;
}
for (const rule of rules) {
const variablePattern = new RegExp('^' + rule.variables.replace(/\*/g, '.*') + '$');
if (variablePattern.test(variable)) {
// If no filters defined for this rule, allow all helpers
if (!rule.filters?.length) {
return true;
}
// Check if any of the filter patterns match the helper
return rule.filters.some((filter) => {
const filterPattern = new RegExp('^' + filter.replace(/\*/g, '.*') + '$');
return filterPattern.test(filter);
});
}
}
// If no matching rules found, return !strictMode
return !strictMode;
}