@aiao/rxdb-model 0.0.19 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +43 -6
- package/dist/cn.d.ts +19 -0
- package/dist/cn.d.ts.map +1 -0
- package/dist/entity-capabilities.d.ts +31 -0
- package/dist/entity-capabilities.d.ts.map +1 -0
- package/dist/entity-detail/detail-tabs.d.ts +13 -2
- package/dist/entity-detail/detail-tabs.d.ts.map +1 -1
- package/dist/entity-detail/index.d.ts +5 -0
- package/dist/entity-detail/index.d.ts.map +1 -1
- package/dist/entity-detail/interfaces.d.ts +29 -1
- package/dist/entity-detail/interfaces.d.ts.map +1 -1
- package/dist/entity-field.utils.d.ts +60 -2
- package/dist/entity-field.utils.d.ts.map +1 -1
- package/dist/entity-form/form-data.d.ts +22 -1
- package/dist/entity-form/form-data.d.ts.map +1 -1
- package/dist/entity-form/form-fields.d.ts +25 -2
- package/dist/entity-form/form-fields.d.ts.map +1 -1
- package/dist/entity-form/form-validation.d.ts +20 -2
- package/dist/entity-form/form-validation.d.ts.map +1 -1
- package/dist/entity-form/index.d.ts +5 -0
- package/dist/entity-form/index.d.ts.map +1 -1
- package/dist/entity-form/interfaces.d.ts +65 -1
- package/dist/entity-form/interfaces.d.ts.map +1 -1
- package/dist/entity-list/manual-order-list.d.ts +99 -0
- package/dist/entity-list/manual-order-list.d.ts.map +1 -0
- package/dist/entity-table/columns/build-editable-columns.d.ts +26 -4
- package/dist/entity-table/columns/build-editable-columns.d.ts.map +1 -1
- package/dist/entity-table/columns/column-utils.d.ts +78 -12
- package/dist/entity-table/columns/column-utils.d.ts.map +1 -1
- package/dist/entity-table/editors/color-editor.d.ts +23 -0
- package/dist/entity-table/editors/color-editor.d.ts.map +1 -0
- package/dist/entity-table/editors/date-editor.d.ts +6 -1
- package/dist/entity-table/editors/date-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/enum-editor.d.ts +11 -1
- package/dist/entity-table/editors/enum-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/global-overlay-editor.d.ts +27 -4
- package/dist/entity-table/editors/global-overlay-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/icon-list-editor.d.ts +13 -1
- package/dist/entity-table/editors/icon-list-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/json-editor.d.ts +6 -1
- package/dist/entity-table/editors/json-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/key-value-editor.d.ts +12 -2
- package/dist/entity-table/editors/key-value-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/lucide-svg.d.ts +3 -2
- package/dist/entity-table/editors/lucide-svg.d.ts.map +1 -1
- package/dist/entity-table/editors/multiselect-editor.d.ts +24 -6
- package/dist/entity-table/editors/multiselect-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/number-editor.d.ts +33 -3
- package/dist/entity-table/editors/number-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/relation-editor.d.ts +14 -2
- package/dist/entity-table/editors/relation-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/safe-list-editor.d.ts +1 -0
- package/dist/entity-table/editors/safe-list-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/tags-editor.d.ts +12 -1
- package/dist/entity-table/editors/tags-editor.d.ts.map +1 -1
- package/dist/entity-table/editors/text-format-editor.d.ts +32 -0
- package/dist/entity-table/editors/text-format-editor.d.ts.map +1 -0
- package/dist/entity-table/editors/uuid-editor.d.ts +14 -1
- package/dist/entity-table/editors/uuid-editor.d.ts.map +1 -1
- package/dist/entity-table/index.d.ts +13 -3
- package/dist/entity-table/index.d.ts.map +1 -1
- package/dist/entity-table/interfaces.d.ts +13 -0
- package/dist/entity-table/interfaces.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-clipboard.d.ts +31 -2
- package/dist/entity-table/vtable/table-clipboard.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-factory.d.ts +25 -5
- package/dist/entity-table/vtable/table-factory.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-keyboard.d.ts +18 -1
- package/dist/entity-table/vtable/table-keyboard.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-operations.d.ts +44 -3
- package/dist/entity-table/vtable/table-operations.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-row-move.d.ts +88 -0
- package/dist/entity-table/vtable/table-row-move.d.ts.map +1 -0
- package/dist/entity-table/vtable/table-theme.d.ts +47 -2
- package/dist/entity-table/vtable/table-theme.d.ts.map +1 -1
- package/dist/entity-table/vtable/table-tooltip.d.ts +7 -1
- package/dist/entity-table/vtable/table-tooltip.d.ts.map +1 -1
- package/dist/entity-table/vtable/vtable-compat.d.ts +33 -5
- package/dist/entity-table/vtable/vtable-compat.d.ts.map +1 -1
- package/dist/entity-value.utils.d.ts +50 -2
- package/dist/entity-value.utils.d.ts.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4392 -64471
- package/dist/query-builder/models/config.interface.d.ts +2 -2
- package/dist/query-builder/models/operator.interface.d.ts +6 -2
- package/dist/query-builder/models/operator.interface.d.ts.map +1 -1
- package/dist/query-builder/models/query-builder-state.d.ts +31 -2
- package/dist/query-builder/models/query-builder-state.d.ts.map +1 -1
- package/dist/query-builder/models/validation.interface.d.ts +2 -2
- package/dist/query-builder/services/operator-registry.service.d.ts +13 -3
- package/dist/query-builder/services/operator-registry.service.d.ts.map +1 -1
- package/dist/query-builder/services/query-builder.service.d.ts +16 -6
- package/dist/query-builder/services/query-builder.service.d.ts.map +1 -1
- package/dist/query-builder/services/query-converter.service.d.ts +10 -2
- package/dist/query-builder/services/query-converter.service.d.ts.map +1 -1
- package/dist/query-builder/services/schema-parser.service.d.ts +10 -2
- package/dist/query-builder/services/schema-parser.service.d.ts.map +1 -1
- package/dist/query-builder/services/validation.service.d.ts +34 -4
- package/dist/query-builder/services/validation.service.d.ts.map +1 -1
- package/dist/query-builder/utils/date.d.ts +12 -0
- package/dist/query-builder/utils/date.d.ts.map +1 -1
- package/dist/query-builder/utils/field-tree.d.ts +7 -1
- package/dist/query-builder/utils/field-tree.d.ts.map +1 -1
- package/dist/query-builder/utils/id.d.ts +2 -0
- package/dist/query-builder/utils/id.d.ts.map +1 -1
- package/dist/query-builder/utils/metadata-field-extractor.d.ts +15 -3
- package/dist/query-builder/utils/metadata-field-extractor.d.ts.map +1 -1
- package/dist/query-builder/utils/popover-position.d.ts +11 -0
- package/dist/query-builder/utils/popover-position.d.ts.map +1 -1
- package/dist/query-builder/utils/schema-factory.d.ts +7 -3
- package/dist/query-builder/utils/structural-equal.d.ts +4 -0
- package/dist/query-builder/utils/structural-equal.d.ts.map +1 -1
- package/dist/query-builder/utils/value-input.d.ts +21 -1
- package/dist/query-builder/utils/value-input.d.ts.map +1 -1
- package/dist/structural-equal.d.ts +5 -1
- package/dist/structural-equal.d.ts.map +1 -1
- package/package.json +34 -10
- package/src/cn.ts +21 -0
- package/src/entity-capabilities.ts +37 -0
- package/src/entity-detail/detail-tabs.ts +48 -0
- package/src/entity-detail/index.ts +7 -0
- package/src/entity-detail/interfaces.ts +55 -0
- package/src/entity-field.utils.ts +205 -0
- package/src/entity-form/form-data.ts +83 -0
- package/src/entity-form/form-fields.ts +48 -0
- package/src/entity-form/form-validation.ts +34 -0
- package/src/entity-form/index.ts +9 -0
- package/src/entity-form/interfaces.ts +116 -0
- package/src/entity-list/manual-order-list.ts +159 -0
- package/src/entity-table/columns/build-editable-columns.ts +184 -0
- package/src/entity-table/columns/column-utils.ts +657 -0
- package/src/entity-table/editors/color-editor.ts +239 -0
- package/src/entity-table/editors/date-editor.ts +169 -0
- package/src/entity-table/editors/enum-editor.ts +167 -0
- package/src/entity-table/editors/global-overlay-editor.ts +277 -0
- package/src/entity-table/editors/icon-list-editor.ts +130 -0
- package/src/entity-table/editors/json-editor.ts +212 -0
- package/src/entity-table/editors/key-value-editor.ts +595 -0
- package/src/entity-table/editors/lucide-svg.ts +78 -0
- package/src/entity-table/editors/multiselect-editor.ts +171 -0
- package/src/entity-table/editors/number-editor.ts +322 -0
- package/src/entity-table/editors/relation-editor.ts +316 -0
- package/src/entity-table/editors/safe-list-editor.ts +17 -0
- package/src/entity-table/editors/tags-editor.ts +239 -0
- package/src/entity-table/editors/text-format-editor.ts +220 -0
- package/src/entity-table/editors/uuid-editor.ts +198 -0
- package/src/entity-table/index.ts +96 -0
- package/src/entity-table/interfaces.ts +72 -0
- package/src/entity-table/vtable/table-clipboard.ts +419 -0
- package/src/entity-table/vtable/table-factory.ts +117 -0
- package/src/entity-table/vtable/table-keyboard.ts +89 -0
- package/src/entity-table/vtable/table-operations.ts +123 -0
- package/src/entity-table/vtable/table-row-move.ts +122 -0
- package/src/entity-table/vtable/table-theme.ts +181 -0
- package/src/entity-table/vtable/table-tooltip.ts +70 -0
- package/src/entity-table/vtable/vtable-compat.ts +84 -0
- package/src/entity-value.utils.ts +456 -0
- package/src/index.ts +57 -0
- package/src/query-builder/models/config.interface.ts +31 -0
- package/src/query-builder/models/index.ts +8 -0
- package/src/query-builder/models/operator.interface.ts +191 -0
- package/src/query-builder/models/query-builder-state.ts +222 -0
- package/src/query-builder/models/validation.interface.ts +184 -0
- package/src/query-builder/services/index.ts +9 -0
- package/src/query-builder/services/operator-registry.service.ts +243 -0
- package/src/query-builder/services/query-builder.service.ts +568 -0
- package/src/query-builder/services/query-converter.service.ts +153 -0
- package/src/query-builder/services/schema-parser.service.ts +309 -0
- package/src/query-builder/services/validation.service.ts +390 -0
- package/src/query-builder/utils/date.ts +35 -0
- package/src/query-builder/utils/field-tree.ts +400 -0
- package/src/query-builder/utils/id.ts +15 -0
- package/src/query-builder/utils/index.ts +12 -0
- package/src/query-builder/utils/metadata-field-extractor.ts +460 -0
- package/src/query-builder/utils/popover-position.ts +59 -0
- package/src/query-builder/utils/schema-factory.ts +75 -0
- package/src/query-builder/utils/structural-equal.ts +5 -0
- package/src/query-builder/utils/value-input.ts +105 -0
- package/src/structural-equal.ts +49 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Schema 解析服务
|
|
3
|
+
* 从 EntityMetadata 解析字段元数据,用于 QueryBuilder 组件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EntityMetadata, KeyValuePropertyMetadata, PropertyType } from '@aiao/rxdb';
|
|
7
|
+
import type { FieldMetadata } from '../models/query-builder-state.js';
|
|
8
|
+
import { extractKeyValueFields } from '../utils/metadata-field-extractor.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 属性类型到 QueryBuilder 类型的映射
|
|
12
|
+
* 将 RxDB 的 PropertyType 映射到 QueryBuilder 支持的简化类型
|
|
13
|
+
*/
|
|
14
|
+
const PROPERTY_TYPE_MAP: Record<string, FieldMetadata['type']> = {
|
|
15
|
+
uuid: 'uuid',
|
|
16
|
+
string: 'string',
|
|
17
|
+
enum: 'enum',
|
|
18
|
+
number: 'number',
|
|
19
|
+
integer: 'number',
|
|
20
|
+
boolean: 'boolean',
|
|
21
|
+
date: 'date',
|
|
22
|
+
stringArray: 'array',
|
|
23
|
+
numberArray: 'array',
|
|
24
|
+
keyValue: 'keyValue',
|
|
25
|
+
json: 'object'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 解析器配置选项
|
|
30
|
+
*/
|
|
31
|
+
export interface SchemaParserOptions {
|
|
32
|
+
/**
|
|
33
|
+
* 是否包含外键字段
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
includeForeignKeys?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 是否包含计算属性
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
includeComputedProperties?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 是否包含关系字段(用于 EXISTS 操作符)
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
includeRelations?: boolean;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 实体元数据映射(用于解析关系字段的目标实体字段)
|
|
52
|
+
* 键为实体名称,值为实体元数据
|
|
53
|
+
*/
|
|
54
|
+
entityMetadataMap?: Map<string, EntityMetadata>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 自定义字段过滤器
|
|
58
|
+
*/
|
|
59
|
+
fieldFilter?: (field: FieldMetadata) => boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 自定义显示名称生成器
|
|
63
|
+
*/
|
|
64
|
+
displayNameGenerator?: (fieldName: string, metadata?: { displayName?: string }) => string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 默认显示名称生成器。
|
|
69
|
+
*
|
|
70
|
+
* 优先使用字段元数据中的 `displayName`,如果未提供,则根据字段名生成可读的标题。
|
|
71
|
+
* 生成规则是将 camelCase / PascalCase 名称转换为以空格分隔的形式,并将首字母大写。
|
|
72
|
+
*
|
|
73
|
+
* 例如:
|
|
74
|
+
* - `"userName"` -> `"User Name"`
|
|
75
|
+
* - `"createdAt"` -> `"Created At"`
|
|
76
|
+
* - `"URLValue"` -> `"URL Value"`
|
|
77
|
+
*
|
|
78
|
+
* @param fieldName 原始字段名称(通常为 camelCase 或 PascalCase)。
|
|
79
|
+
* @param metadata 字段元数据,可选。如果包含 `displayName`,则直接返回该值。
|
|
80
|
+
* @returns 适合在界面上展示的字段标题字符串。
|
|
81
|
+
*/
|
|
82
|
+
function defaultDisplayNameGenerator(fieldName: string, metadata?: { displayName?: string }): string {
|
|
83
|
+
if (metadata?.displayName) {
|
|
84
|
+
return metadata.displayName;
|
|
85
|
+
}
|
|
86
|
+
// camelCase -> Camel Case
|
|
87
|
+
return fieldName
|
|
88
|
+
.replace(/([A-Z])/g, ' $1')
|
|
89
|
+
.replace(/^./, str => str.toUpperCase())
|
|
90
|
+
.trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Schema 解析服务
|
|
95
|
+
* 负责将 RxDB EntityMetadata 转换为 QueryBuilder 可用的字段元数据
|
|
96
|
+
*/
|
|
97
|
+
export class SchemaParser {
|
|
98
|
+
private readonly options: Required<Omit<SchemaParserOptions, 'entityMetadataMap'>> & {
|
|
99
|
+
entityMetadataMap?: Map<string, EntityMetadata>;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
private readonly relationFieldsCache = new Map<string, FieldMetadata[]>();
|
|
103
|
+
|
|
104
|
+
/** @param options - 解析器配置(含关系目标字段递归解析缓存) */
|
|
105
|
+
constructor(options: SchemaParserOptions = {}) {
|
|
106
|
+
this.options = {
|
|
107
|
+
includeForeignKeys: options.includeForeignKeys ?? false,
|
|
108
|
+
includeComputedProperties: options.includeComputedProperties ?? false,
|
|
109
|
+
includeRelations: options.includeRelations ?? false,
|
|
110
|
+
entityMetadataMap: options.entityMetadataMap,
|
|
111
|
+
fieldFilter: options.fieldFilter ?? (() => true),
|
|
112
|
+
displayNameGenerator: options.displayNameGenerator ?? defaultDisplayNameGenerator
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 从 EntityMetadata 解析字段元数据
|
|
118
|
+
*
|
|
119
|
+
* @param metadata - RxDB 实体元数据
|
|
120
|
+
* @returns 字段元数据数组
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* const parser = new SchemaParser();
|
|
125
|
+
* const fields = parser.parse(userMetadata);
|
|
126
|
+
* // [{ name: 'id', displayName: 'ID', type: 'string' }, ...]
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
parse(metadata: EntityMetadata): FieldMetadata[] {
|
|
130
|
+
const fields: FieldMetadata[] = [];
|
|
131
|
+
|
|
132
|
+
// 解析普通属性
|
|
133
|
+
for (const property of metadata.properties) {
|
|
134
|
+
// 跳过外键(如果配置不包含)
|
|
135
|
+
if (!this.options.includeForeignKeys && metadata.isForeignKey(property.name)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const field = this.parseProperty(property);
|
|
140
|
+
if (field && this.options.fieldFilter(field)) {
|
|
141
|
+
// keyValue 属性:提取子字段
|
|
142
|
+
if (field.type === 'keyValue') {
|
|
143
|
+
const kvProperties = (property as unknown as { properties: KeyValuePropertyMetadata[] }).properties;
|
|
144
|
+
if (kvProperties?.length > 0) {
|
|
145
|
+
field.keyValueFields = extractKeyValueFields(kvProperties);
|
|
146
|
+
for (const kvField of field.keyValueFields) {
|
|
147
|
+
const childField: FieldMetadata = {
|
|
148
|
+
...kvField,
|
|
149
|
+
name: `${field.name}.${kvField.name}`,
|
|
150
|
+
displayName: `${field.displayName}.${kvField.displayName}`
|
|
151
|
+
};
|
|
152
|
+
if (this.options.fieldFilter(childField)) {
|
|
153
|
+
fields.push(childField);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
fields.push(field);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 解析计算属性(如果配置包含)
|
|
163
|
+
if (this.options.includeComputedProperties) {
|
|
164
|
+
for (const property of metadata.computedProperties) {
|
|
165
|
+
const field = this.parseProperty(property);
|
|
166
|
+
if (field && this.options.fieldFilter(field)) {
|
|
167
|
+
fields.push(field);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 解析关系字段(如果配置包含)
|
|
173
|
+
if (this.options.includeRelations && metadata.relations) {
|
|
174
|
+
for (const relation of metadata.relations) {
|
|
175
|
+
const field = this.parseRelation(relation);
|
|
176
|
+
if (field && this.options.fieldFilter(field)) {
|
|
177
|
+
fields.push(field);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return fields;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 从多个 EntityMetadata 批量解析
|
|
187
|
+
*
|
|
188
|
+
* @param metadataMap - 实体名称到元数据的映射
|
|
189
|
+
* @returns 实体名称到字段数组的映射
|
|
190
|
+
*/
|
|
191
|
+
parseMany(metadataMap: Map<string, EntityMetadata>): Map<string, FieldMetadata[]> {
|
|
192
|
+
const result = new Map<string, FieldMetadata[]>();
|
|
193
|
+
|
|
194
|
+
for (const [entityName, metadata] of metadataMap) {
|
|
195
|
+
result.set(entityName, this.parse(metadata));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 解析单个属性
|
|
203
|
+
*/
|
|
204
|
+
private parseProperty(property: {
|
|
205
|
+
name: string;
|
|
206
|
+
displayName?: string;
|
|
207
|
+
type: PropertyType | `${PropertyType}`;
|
|
208
|
+
description?: string;
|
|
209
|
+
nullable?: boolean;
|
|
210
|
+
enum?: readonly unknown[];
|
|
211
|
+
pattern?: string;
|
|
212
|
+
}): FieldMetadata | null {
|
|
213
|
+
const type = this.mapPropertyType(property.type);
|
|
214
|
+
if (!type) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const field: FieldMetadata = {
|
|
219
|
+
name: property.name,
|
|
220
|
+
displayName: this.options.displayNameGenerator(property.name, property),
|
|
221
|
+
type
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// ✨ T071-T073: 提取额外的字段元数据
|
|
225
|
+
|
|
226
|
+
// 从 nullable 推导 required(nullable 为 false 或 undefined 时为必填)
|
|
227
|
+
if (property.nullable !== undefined) {
|
|
228
|
+
field.required = !property.nullable;
|
|
229
|
+
field.nullable = property.nullable;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 提取 description
|
|
233
|
+
if (property.description) {
|
|
234
|
+
field.description = property.description;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 提取 enum 值
|
|
238
|
+
if (property.enum && Array.isArray(property.enum)) {
|
|
239
|
+
field.enum = property.enum;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 提取正则验证模式
|
|
243
|
+
if (property.pattern) {
|
|
244
|
+
field.pattern = property.pattern;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return field;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 将 RxDB PropertyType 映射到 QueryBuilder 类型
|
|
252
|
+
*/
|
|
253
|
+
private mapPropertyType(propertyType: PropertyType | `${PropertyType}`): FieldMetadata['type'] | null {
|
|
254
|
+
const typeString = typeof propertyType === 'string' ? propertyType : String(propertyType);
|
|
255
|
+
return PROPERTY_TYPE_MAP[typeString] ?? null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 解析关系字段
|
|
260
|
+
*/
|
|
261
|
+
private parseRelation(relation: {
|
|
262
|
+
name: string;
|
|
263
|
+
displayName?: string;
|
|
264
|
+
kind: string;
|
|
265
|
+
mappedEntity: string;
|
|
266
|
+
mappedNamespace?: string;
|
|
267
|
+
}): FieldMetadata | null {
|
|
268
|
+
const field: FieldMetadata = {
|
|
269
|
+
name: relation.name,
|
|
270
|
+
displayName: this.options.displayNameGenerator(relation.name, relation),
|
|
271
|
+
type: 'relation',
|
|
272
|
+
isRelation: true,
|
|
273
|
+
relationTarget: relation.mappedEntity,
|
|
274
|
+
relationKind: relation.kind as FieldMetadata['relationKind']
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// 如果提供了实体元数据映射,解析关系目标实体的字段
|
|
278
|
+
if (this.options.entityMetadataMap) {
|
|
279
|
+
const targetEntityName = relation.mappedEntity;
|
|
280
|
+
const targetMetadata = this.options.entityMetadataMap.get(targetEntityName);
|
|
281
|
+
if (targetMetadata) {
|
|
282
|
+
const cached = this.relationFieldsCache.get(targetEntityName);
|
|
283
|
+
if (cached) {
|
|
284
|
+
field.relationFields = cached;
|
|
285
|
+
} else {
|
|
286
|
+
const targetParser = new SchemaParser({
|
|
287
|
+
...this.options,
|
|
288
|
+
includeRelations: false
|
|
289
|
+
});
|
|
290
|
+
const parsed = targetParser.parse(targetMetadata);
|
|
291
|
+
this.relationFieldsCache.set(targetEntityName, parsed);
|
|
292
|
+
field.relationFields = parsed;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return field;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 创建 SchemaParser 实例的工厂函数
|
|
303
|
+
*
|
|
304
|
+
* @param options - 解析器配置选项
|
|
305
|
+
* @returns 新的 SchemaParser 实例
|
|
306
|
+
*/
|
|
307
|
+
export function createSchemaParser(options?: SchemaParserOptions): SchemaParser {
|
|
308
|
+
return new SchemaParser(options);
|
|
309
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 校验服务
|
|
3
|
+
* 提供规则值的即时校验功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OperatorName } from '@aiao/rxdb';
|
|
7
|
+
import type { FieldMetadata, QueryBuilderRule, QueryBuilderRuleGroup, UIRule } from '../models/query-builder-state.js';
|
|
8
|
+
import type {
|
|
9
|
+
ValidationContext,
|
|
10
|
+
ValidationError,
|
|
11
|
+
ValidationResult,
|
|
12
|
+
ValidationRule
|
|
13
|
+
} from '../models/validation.interface.js';
|
|
14
|
+
import { BUILTIN_VALIDATION_RULES } from '../models/validation.interface.js';
|
|
15
|
+
import { OperatorRegistry, getDefaultOperatorRegistry } from './operator-registry.service.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 单条校验规则输入(兼容 QueryBuilderRule 与 UIRule)
|
|
19
|
+
*/
|
|
20
|
+
type ValidationRuleInput = QueryBuilderRule<Record<string, unknown>> | UIRule;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 校验规则组输入(宽松递归结构)
|
|
24
|
+
*/
|
|
25
|
+
export type ValidationGroupInput = {
|
|
26
|
+
/**
|
|
27
|
+
* 规则组唯一标识符
|
|
28
|
+
*/
|
|
29
|
+
id: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 逻辑组合符
|
|
33
|
+
*/
|
|
34
|
+
combinator: 'and' | 'or';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 子规则列表(支持嵌套)
|
|
38
|
+
*/
|
|
39
|
+
rules: Array<ValidationRuleInput | ValidationGroupInput>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type RuleWithWhere = ValidationRuleInput & {
|
|
43
|
+
where?: ValidationGroupInput | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function hasWhereSubquery(rule: ValidationRuleInput): rule is RuleWithWhere {
|
|
47
|
+
return typeof rule === 'object' && rule !== null && 'where' in rule;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isValidationGroup(item: ValidationRuleInput | ValidationGroupInput): item is ValidationGroupInput {
|
|
51
|
+
return 'combinator' in item && 'rules' in item;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isValidationRule(item: ValidationRuleInput | ValidationGroupInput): item is ValidationRuleInput {
|
|
55
|
+
return 'field' in item && 'operator' in item;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 校验服务选项
|
|
60
|
+
*/
|
|
61
|
+
export interface ValidationServiceOptions {
|
|
62
|
+
/**
|
|
63
|
+
* 自定义校验规则
|
|
64
|
+
*/
|
|
65
|
+
customRules?: ValidationRule[];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 操作符注册表
|
|
69
|
+
*/
|
|
70
|
+
operatorRegistry?: OperatorRegistry;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 是否启用内置校验规则
|
|
74
|
+
* @default true
|
|
75
|
+
*/
|
|
76
|
+
useBuiltinRules?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 从规则中提取值(处理不同规则类型)。
|
|
81
|
+
*
|
|
82
|
+
* @template T 规则字段值的类型。
|
|
83
|
+
* @param rule 要从中提取值的规则对象。
|
|
84
|
+
* @returns 规则中的值;如果规则不包含 `value` 属性,则返回 `undefined`。
|
|
85
|
+
*/
|
|
86
|
+
function extractValueFromRule(rule: ValidationRuleInput): unknown {
|
|
87
|
+
// Rule 类型是联合类型,需要检查是否有 value 属性
|
|
88
|
+
if ('value' in rule) {
|
|
89
|
+
return rule.value;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 校验服务
|
|
96
|
+
* 负责验证 QueryBuilder 规则和值
|
|
97
|
+
*/
|
|
98
|
+
export class ValidationService {
|
|
99
|
+
private readonly rules: Map<string, ValidationRule>;
|
|
100
|
+
private readonly operatorRegistry: OperatorRegistry;
|
|
101
|
+
|
|
102
|
+
/** @param options - 校验服务配置(自定义规则等) */
|
|
103
|
+
constructor(options: ValidationServiceOptions = {}) {
|
|
104
|
+
this.rules = new Map();
|
|
105
|
+
this.operatorRegistry = options.operatorRegistry ?? getDefaultOperatorRegistry();
|
|
106
|
+
|
|
107
|
+
// 合并内置与自定义规则(后者覆盖同名规则)
|
|
108
|
+
const sources: ValidationRule[][] = [];
|
|
109
|
+
if (options.useBuiltinRules !== false) sources.push(BUILTIN_VALIDATION_RULES as ValidationRule[]);
|
|
110
|
+
if (options.customRules) sources.push(options.customRules);
|
|
111
|
+
for (const group of sources) {
|
|
112
|
+
for (const rule of group) this.rules.set(rule.name, rule);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 校验单条规则
|
|
118
|
+
*/
|
|
119
|
+
validateRule(rule: ValidationRuleInput, field: FieldMetadata): ValidationResult {
|
|
120
|
+
const errors: ValidationError[] = [];
|
|
121
|
+
const value = extractValueFromRule(rule);
|
|
122
|
+
|
|
123
|
+
// 创建校验上下文
|
|
124
|
+
const context: ValidationContext = {
|
|
125
|
+
fieldType: field.type,
|
|
126
|
+
operator: rule.operator as OperatorName,
|
|
127
|
+
value,
|
|
128
|
+
enum: field.enum,
|
|
129
|
+
pattern: field.pattern
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// 1. 检查操作符是否支持该字段
|
|
133
|
+
if (!this.operatorRegistry.isOperatorSupportedForField(rule.operator as OperatorName, field)) {
|
|
134
|
+
errors.push({
|
|
135
|
+
field: rule.field as string,
|
|
136
|
+
rule: 'operatorTypeMatch',
|
|
137
|
+
message: `操作符 "${rule.operator}" 不支持字段 "${field.name}"`
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. 检查值类型
|
|
142
|
+
const valueType = this.operatorRegistry.getValueType(rule.operator as OperatorName);
|
|
143
|
+
|
|
144
|
+
// EXISTS/NOT EXISTS 操作符的特殊处理
|
|
145
|
+
if (valueType === 'subquery') {
|
|
146
|
+
// 对于 EXISTS 操作符,value 字段可以是任何值(包括空字符串、undefined 等)
|
|
147
|
+
// 真正需要验证的是可选的 where 字段
|
|
148
|
+
if (hasWhereSubquery(rule) && rule.where !== undefined && rule.where !== null) {
|
|
149
|
+
const where = rule.where;
|
|
150
|
+
|
|
151
|
+
// 如果提供了 where,验证其有效性
|
|
152
|
+
if (typeof where === 'object' && 'combinator' in where && 'rules' in where) {
|
|
153
|
+
// 检查 where 子查询是否为空
|
|
154
|
+
if (!where.rules || where.rules.length === 0) {
|
|
155
|
+
errors.push({
|
|
156
|
+
field: rule.field as string,
|
|
157
|
+
rule: 'valueType',
|
|
158
|
+
message: 'EXISTS 子查询条件不能为空'
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
// 验证 where 子查询内部规则的基本有效性
|
|
162
|
+
const subqueryErrors = this.validateSubqueryRules(where as QueryBuilderRuleGroup<Record<string, unknown>>);
|
|
163
|
+
if (subqueryErrors.length > 0) {
|
|
164
|
+
errors.push(
|
|
165
|
+
...subqueryErrors.map(err => ({
|
|
166
|
+
...err,
|
|
167
|
+
field: `${rule.field}.${err.field}` // 添加父字段前缀
|
|
168
|
+
}))
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// where 格式无效
|
|
174
|
+
errors.push({
|
|
175
|
+
field: rule.field as string,
|
|
176
|
+
rule: 'valueType',
|
|
177
|
+
message: 'EXISTS 子查询格式无效'
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// 如果没有 where 或 where 为 undefined/null,是有效的简单 EXISTS
|
|
182
|
+
} else {
|
|
183
|
+
// 非子查询类型,正常验证 value
|
|
184
|
+
const valueError = this.validateValueType(value, valueType, rule.field as string, field.displayName);
|
|
185
|
+
if (valueError) {
|
|
186
|
+
errors.push(valueError);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 3. 运行类型相关的内置校验规则
|
|
191
|
+
for (const validationRule of this.rules.values()) {
|
|
192
|
+
// 检查规则是否适用于当前字段类型
|
|
193
|
+
if (validationRule.appliesTo && !validationRule.appliesTo.includes(field.type)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const isValid = validationRule.validate(context);
|
|
198
|
+
if (!isValid) {
|
|
199
|
+
errors.push({
|
|
200
|
+
field: rule.field as string,
|
|
201
|
+
rule: validationRule.name,
|
|
202
|
+
message: validationRule.message
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
valid: errors.length === 0,
|
|
209
|
+
errors: errors.length > 0 ? errors : undefined
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 校验整个规则组
|
|
215
|
+
*/
|
|
216
|
+
validateGroup(group: ValidationGroupInput, fields: Map<string, FieldMetadata>): ValidationResult {
|
|
217
|
+
const errors: ValidationError[] = [];
|
|
218
|
+
|
|
219
|
+
for (const item of group.rules) {
|
|
220
|
+
if (isValidationGroup(item)) {
|
|
221
|
+
const result = this.validateGroup(item, fields);
|
|
222
|
+
if (!result.valid && result.errors) {
|
|
223
|
+
errors.push(...result.errors);
|
|
224
|
+
}
|
|
225
|
+
} else if (isValidationRule(item)) {
|
|
226
|
+
const field = fields.get(item.field as string);
|
|
227
|
+
if (!field) {
|
|
228
|
+
errors.push({
|
|
229
|
+
field: item.field as string,
|
|
230
|
+
rule: 'fieldExists',
|
|
231
|
+
message: `字段 "${item.field}" 不存在`
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const result = this.validateRule(item, field);
|
|
237
|
+
if (!result.valid && result.errors) {
|
|
238
|
+
errors.push(...result.errors);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
valid: errors.length === 0,
|
|
245
|
+
errors: errors.length > 0 ? errors : undefined
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 注册自定义校验规则
|
|
251
|
+
*/
|
|
252
|
+
registerRule(rule: ValidationRule): void {
|
|
253
|
+
this.rules.set(rule.name, rule);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 移除校验规则
|
|
258
|
+
*/
|
|
259
|
+
unregisterRule(name: string): boolean {
|
|
260
|
+
return this.rules.delete(name);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 获取所有校验规则
|
|
265
|
+
*/
|
|
266
|
+
getRules(): ValidationRule[] {
|
|
267
|
+
return Array.from(this.rules.values());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 校验值类型
|
|
272
|
+
*/
|
|
273
|
+
private validateValueType(
|
|
274
|
+
value: unknown,
|
|
275
|
+
valueType: 'single' | 'array' | 'range' | 'none' | 'subquery' | undefined,
|
|
276
|
+
fieldName: string,
|
|
277
|
+
displayName?: string
|
|
278
|
+
): ValidationError | null {
|
|
279
|
+
if (valueType === 'none') {
|
|
280
|
+
// 不需要值(null/notNull)
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 注意:subquery 类型的验证在 validateRule 中处理,不应该调用此方法
|
|
285
|
+
if (valueType === 'subquery') {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (valueType === 'array') {
|
|
290
|
+
// 需要数组
|
|
291
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
292
|
+
return {
|
|
293
|
+
field: fieldName,
|
|
294
|
+
rule: 'valueType',
|
|
295
|
+
message: `${displayName || fieldName}需要提供一个或多个值`
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (valueType === 'range') {
|
|
302
|
+
// 需要两个值(between)
|
|
303
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
304
|
+
return {
|
|
305
|
+
field: fieldName,
|
|
306
|
+
rule: 'valueType',
|
|
307
|
+
message: `${displayName || fieldName}需要提供两个值(范围)`
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// single 或 undefined(默认单值)
|
|
314
|
+
if (value === undefined || value === null || value === '') {
|
|
315
|
+
return {
|
|
316
|
+
field: fieldName,
|
|
317
|
+
rule: 'valueType',
|
|
318
|
+
message: `${displayName || fieldName}需要提供一个值`
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 验证子查询规则的基本有效性(不依赖字段元数据)
|
|
327
|
+
*
|
|
328
|
+
* @description
|
|
329
|
+
* 子查询可能引用关系实体的字段,我们无法获取其 FieldMetadata。
|
|
330
|
+
* 因此只验证规则的基本结构和值类型,不验证字段是否存在或操作符是否匹配。
|
|
331
|
+
*/
|
|
332
|
+
private validateSubqueryRules(group: ValidationGroupInput): ValidationError[] {
|
|
333
|
+
const errors: ValidationError[] = [];
|
|
334
|
+
|
|
335
|
+
for (const item of group.rules) {
|
|
336
|
+
if (isValidationGroup(item)) {
|
|
337
|
+
// 递归验证嵌套组
|
|
338
|
+
const nestedErrors = this.validateSubqueryRules(item);
|
|
339
|
+
errors.push(...nestedErrors);
|
|
340
|
+
} else if (isValidationRule(item)) {
|
|
341
|
+
const value = extractValueFromRule(item);
|
|
342
|
+
const valueType = this.operatorRegistry.getValueType(item.operator as OperatorName);
|
|
343
|
+
|
|
344
|
+
// 只验证值类型,不验证字段存在性和操作符匹配
|
|
345
|
+
const valueError = this.validateValueType(value, valueType, item.field as string);
|
|
346
|
+
if (valueError) {
|
|
347
|
+
errors.push(valueError);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 如果是嵌套的 EXISTS,递归验证其 where
|
|
351
|
+
if (valueType === 'subquery') {
|
|
352
|
+
const ruleWithWhere = item as RuleWithWhere;
|
|
353
|
+
if ('where' in ruleWithWhere && ruleWithWhere.where) {
|
|
354
|
+
const where = ruleWithWhere.where;
|
|
355
|
+
if (typeof where === 'object' && 'combinator' in where && 'rules' in where) {
|
|
356
|
+
if (!where.rules || where.rules.length === 0) {
|
|
357
|
+
errors.push({
|
|
358
|
+
field: item.field as string,
|
|
359
|
+
rule: 'valueType',
|
|
360
|
+
message: 'EXISTS 子查询条件不能为空'
|
|
361
|
+
});
|
|
362
|
+
} else {
|
|
363
|
+
const subErrors = this.validateSubqueryRules(where);
|
|
364
|
+
errors.push(...subErrors);
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
errors.push({
|
|
368
|
+
field: item.field as string,
|
|
369
|
+
rule: 'valueType',
|
|
370
|
+
message: 'EXISTS 子查询格式无效'
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return errors;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 创建 ValidationService 实例的工厂函数
|
|
384
|
+
*
|
|
385
|
+
* @param options - 校验服务选项
|
|
386
|
+
* @returns 新的 ValidationService 实例
|
|
387
|
+
*/
|
|
388
|
+
export function createValidationService(options?: ValidationServiceOptions): ValidationService {
|
|
389
|
+
return new ValidationService(options);
|
|
390
|
+
}
|