@aiao/rxdb-model 0.0.18 → 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.
Files changed (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +43 -6
  3. package/dist/cn.d.ts +19 -0
  4. package/dist/cn.d.ts.map +1 -0
  5. package/dist/entity-capabilities.d.ts +31 -0
  6. package/dist/entity-capabilities.d.ts.map +1 -0
  7. package/dist/entity-detail/detail-tabs.d.ts +13 -2
  8. package/dist/entity-detail/detail-tabs.d.ts.map +1 -1
  9. package/dist/entity-detail/index.d.ts +5 -0
  10. package/dist/entity-detail/index.d.ts.map +1 -1
  11. package/dist/entity-detail/interfaces.d.ts +29 -1
  12. package/dist/entity-detail/interfaces.d.ts.map +1 -1
  13. package/dist/entity-field.utils.d.ts +60 -2
  14. package/dist/entity-field.utils.d.ts.map +1 -1
  15. package/dist/entity-form/form-data.d.ts +22 -1
  16. package/dist/entity-form/form-data.d.ts.map +1 -1
  17. package/dist/entity-form/form-fields.d.ts +25 -2
  18. package/dist/entity-form/form-fields.d.ts.map +1 -1
  19. package/dist/entity-form/form-validation.d.ts +20 -2
  20. package/dist/entity-form/form-validation.d.ts.map +1 -1
  21. package/dist/entity-form/index.d.ts +5 -0
  22. package/dist/entity-form/index.d.ts.map +1 -1
  23. package/dist/entity-form/interfaces.d.ts +65 -1
  24. package/dist/entity-form/interfaces.d.ts.map +1 -1
  25. package/dist/entity-list/manual-order-list.d.ts +99 -0
  26. package/dist/entity-list/manual-order-list.d.ts.map +1 -0
  27. package/dist/entity-table/columns/build-editable-columns.d.ts +26 -4
  28. package/dist/entity-table/columns/build-editable-columns.d.ts.map +1 -1
  29. package/dist/entity-table/columns/column-utils.d.ts +78 -12
  30. package/dist/entity-table/columns/column-utils.d.ts.map +1 -1
  31. package/dist/entity-table/editors/color-editor.d.ts +23 -0
  32. package/dist/entity-table/editors/color-editor.d.ts.map +1 -0
  33. package/dist/entity-table/editors/date-editor.d.ts +6 -1
  34. package/dist/entity-table/editors/date-editor.d.ts.map +1 -1
  35. package/dist/entity-table/editors/enum-editor.d.ts +11 -1
  36. package/dist/entity-table/editors/enum-editor.d.ts.map +1 -1
  37. package/dist/entity-table/editors/global-overlay-editor.d.ts +27 -4
  38. package/dist/entity-table/editors/global-overlay-editor.d.ts.map +1 -1
  39. package/dist/entity-table/editors/icon-list-editor.d.ts +13 -1
  40. package/dist/entity-table/editors/icon-list-editor.d.ts.map +1 -1
  41. package/dist/entity-table/editors/json-editor.d.ts +6 -1
  42. package/dist/entity-table/editors/json-editor.d.ts.map +1 -1
  43. package/dist/entity-table/editors/key-value-editor.d.ts +12 -2
  44. package/dist/entity-table/editors/key-value-editor.d.ts.map +1 -1
  45. package/dist/entity-table/editors/lucide-svg.d.ts +3 -2
  46. package/dist/entity-table/editors/lucide-svg.d.ts.map +1 -1
  47. package/dist/entity-table/editors/multiselect-editor.d.ts +24 -6
  48. package/dist/entity-table/editors/multiselect-editor.d.ts.map +1 -1
  49. package/dist/entity-table/editors/number-editor.d.ts +33 -3
  50. package/dist/entity-table/editors/number-editor.d.ts.map +1 -1
  51. package/dist/entity-table/editors/relation-editor.d.ts +14 -2
  52. package/dist/entity-table/editors/relation-editor.d.ts.map +1 -1
  53. package/dist/entity-table/editors/safe-list-editor.d.ts +1 -0
  54. package/dist/entity-table/editors/safe-list-editor.d.ts.map +1 -1
  55. package/dist/entity-table/editors/tags-editor.d.ts +12 -1
  56. package/dist/entity-table/editors/tags-editor.d.ts.map +1 -1
  57. package/dist/entity-table/editors/text-format-editor.d.ts +32 -0
  58. package/dist/entity-table/editors/text-format-editor.d.ts.map +1 -0
  59. package/dist/entity-table/editors/uuid-editor.d.ts +14 -1
  60. package/dist/entity-table/editors/uuid-editor.d.ts.map +1 -1
  61. package/dist/entity-table/index.d.ts +13 -3
  62. package/dist/entity-table/index.d.ts.map +1 -1
  63. package/dist/entity-table/interfaces.d.ts +13 -0
  64. package/dist/entity-table/interfaces.d.ts.map +1 -1
  65. package/dist/entity-table/vtable/table-clipboard.d.ts +31 -2
  66. package/dist/entity-table/vtable/table-clipboard.d.ts.map +1 -1
  67. package/dist/entity-table/vtable/table-factory.d.ts +25 -5
  68. package/dist/entity-table/vtable/table-factory.d.ts.map +1 -1
  69. package/dist/entity-table/vtable/table-keyboard.d.ts +18 -1
  70. package/dist/entity-table/vtable/table-keyboard.d.ts.map +1 -1
  71. package/dist/entity-table/vtable/table-operations.d.ts +44 -3
  72. package/dist/entity-table/vtable/table-operations.d.ts.map +1 -1
  73. package/dist/entity-table/vtable/table-row-move.d.ts +88 -0
  74. package/dist/entity-table/vtable/table-row-move.d.ts.map +1 -0
  75. package/dist/entity-table/vtable/table-theme.d.ts +47 -2
  76. package/dist/entity-table/vtable/table-theme.d.ts.map +1 -1
  77. package/dist/entity-table/vtable/table-tooltip.d.ts +7 -1
  78. package/dist/entity-table/vtable/table-tooltip.d.ts.map +1 -1
  79. package/dist/entity-table/vtable/vtable-compat.d.ts +33 -5
  80. package/dist/entity-table/vtable/vtable-compat.d.ts.map +1 -1
  81. package/dist/entity-value.utils.d.ts +50 -2
  82. package/dist/entity-value.utils.d.ts.map +1 -1
  83. package/dist/index.d.ts +9 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +4392 -64471
  86. package/dist/query-builder/models/config.interface.d.ts +2 -2
  87. package/dist/query-builder/models/operator.interface.d.ts +6 -2
  88. package/dist/query-builder/models/operator.interface.d.ts.map +1 -1
  89. package/dist/query-builder/models/query-builder-state.d.ts +31 -2
  90. package/dist/query-builder/models/query-builder-state.d.ts.map +1 -1
  91. package/dist/query-builder/models/validation.interface.d.ts +2 -2
  92. package/dist/query-builder/services/operator-registry.service.d.ts +13 -3
  93. package/dist/query-builder/services/operator-registry.service.d.ts.map +1 -1
  94. package/dist/query-builder/services/query-builder.service.d.ts +16 -6
  95. package/dist/query-builder/services/query-builder.service.d.ts.map +1 -1
  96. package/dist/query-builder/services/query-converter.service.d.ts +10 -2
  97. package/dist/query-builder/services/query-converter.service.d.ts.map +1 -1
  98. package/dist/query-builder/services/schema-parser.service.d.ts +10 -2
  99. package/dist/query-builder/services/schema-parser.service.d.ts.map +1 -1
  100. package/dist/query-builder/services/validation.service.d.ts +34 -4
  101. package/dist/query-builder/services/validation.service.d.ts.map +1 -1
  102. package/dist/query-builder/utils/date.d.ts +12 -0
  103. package/dist/query-builder/utils/date.d.ts.map +1 -1
  104. package/dist/query-builder/utils/field-tree.d.ts +7 -1
  105. package/dist/query-builder/utils/field-tree.d.ts.map +1 -1
  106. package/dist/query-builder/utils/id.d.ts +2 -0
  107. package/dist/query-builder/utils/id.d.ts.map +1 -1
  108. package/dist/query-builder/utils/metadata-field-extractor.d.ts +15 -3
  109. package/dist/query-builder/utils/metadata-field-extractor.d.ts.map +1 -1
  110. package/dist/query-builder/utils/popover-position.d.ts +11 -0
  111. package/dist/query-builder/utils/popover-position.d.ts.map +1 -1
  112. package/dist/query-builder/utils/schema-factory.d.ts +7 -3
  113. package/dist/query-builder/utils/structural-equal.d.ts +4 -0
  114. package/dist/query-builder/utils/structural-equal.d.ts.map +1 -1
  115. package/dist/query-builder/utils/value-input.d.ts +21 -1
  116. package/dist/query-builder/utils/value-input.d.ts.map +1 -1
  117. package/dist/structural-equal.d.ts +5 -1
  118. package/dist/structural-equal.d.ts.map +1 -1
  119. package/package.json +34 -10
  120. package/src/cn.ts +21 -0
  121. package/src/entity-capabilities.ts +37 -0
  122. package/src/entity-detail/detail-tabs.ts +48 -0
  123. package/src/entity-detail/index.ts +7 -0
  124. package/src/entity-detail/interfaces.ts +55 -0
  125. package/src/entity-field.utils.ts +205 -0
  126. package/src/entity-form/form-data.ts +83 -0
  127. package/src/entity-form/form-fields.ts +48 -0
  128. package/src/entity-form/form-validation.ts +34 -0
  129. package/src/entity-form/index.ts +9 -0
  130. package/src/entity-form/interfaces.ts +116 -0
  131. package/src/entity-list/manual-order-list.ts +159 -0
  132. package/src/entity-table/columns/build-editable-columns.ts +184 -0
  133. package/src/entity-table/columns/column-utils.ts +657 -0
  134. package/src/entity-table/editors/color-editor.ts +239 -0
  135. package/src/entity-table/editors/date-editor.ts +169 -0
  136. package/src/entity-table/editors/enum-editor.ts +167 -0
  137. package/src/entity-table/editors/global-overlay-editor.ts +277 -0
  138. package/src/entity-table/editors/icon-list-editor.ts +130 -0
  139. package/src/entity-table/editors/json-editor.ts +212 -0
  140. package/src/entity-table/editors/key-value-editor.ts +595 -0
  141. package/src/entity-table/editors/lucide-svg.ts +78 -0
  142. package/src/entity-table/editors/multiselect-editor.ts +171 -0
  143. package/src/entity-table/editors/number-editor.ts +322 -0
  144. package/src/entity-table/editors/relation-editor.ts +316 -0
  145. package/src/entity-table/editors/safe-list-editor.ts +17 -0
  146. package/src/entity-table/editors/tags-editor.ts +239 -0
  147. package/src/entity-table/editors/text-format-editor.ts +220 -0
  148. package/src/entity-table/editors/uuid-editor.ts +198 -0
  149. package/src/entity-table/index.ts +96 -0
  150. package/src/entity-table/interfaces.ts +72 -0
  151. package/src/entity-table/vtable/table-clipboard.ts +419 -0
  152. package/src/entity-table/vtable/table-factory.ts +117 -0
  153. package/src/entity-table/vtable/table-keyboard.ts +89 -0
  154. package/src/entity-table/vtable/table-operations.ts +123 -0
  155. package/src/entity-table/vtable/table-row-move.ts +122 -0
  156. package/src/entity-table/vtable/table-theme.ts +181 -0
  157. package/src/entity-table/vtable/table-tooltip.ts +70 -0
  158. package/src/entity-table/vtable/vtable-compat.ts +84 -0
  159. package/src/entity-value.utils.ts +456 -0
  160. package/src/index.ts +57 -0
  161. package/src/query-builder/models/config.interface.ts +31 -0
  162. package/src/query-builder/models/index.ts +8 -0
  163. package/src/query-builder/models/operator.interface.ts +191 -0
  164. package/src/query-builder/models/query-builder-state.ts +222 -0
  165. package/src/query-builder/models/validation.interface.ts +184 -0
  166. package/src/query-builder/services/index.ts +9 -0
  167. package/src/query-builder/services/operator-registry.service.ts +243 -0
  168. package/src/query-builder/services/query-builder.service.ts +568 -0
  169. package/src/query-builder/services/query-converter.service.ts +153 -0
  170. package/src/query-builder/services/schema-parser.service.ts +309 -0
  171. package/src/query-builder/services/validation.service.ts +390 -0
  172. package/src/query-builder/utils/date.ts +35 -0
  173. package/src/query-builder/utils/field-tree.ts +400 -0
  174. package/src/query-builder/utils/id.ts +15 -0
  175. package/src/query-builder/utils/index.ts +12 -0
  176. package/src/query-builder/utils/metadata-field-extractor.ts +460 -0
  177. package/src/query-builder/utils/popover-position.ts +59 -0
  178. package/src/query-builder/utils/schema-factory.ts +75 -0
  179. package/src/query-builder/utils/structural-equal.ts +5 -0
  180. package/src/query-builder/utils/value-input.ts +105 -0
  181. package/src/structural-equal.ts +49 -0
@@ -0,0 +1,456 @@
1
+ import type { FieldFormat } from '@aiao/rxdb';
2
+ import type { EntityFieldConfig, EntityFieldType } from './entity-field.utils.js';
3
+
4
+ /**
5
+ * RxDB Model 实体值处理工具模块
6
+ * 提供实体字段值的解析、格式化、验证等功能
7
+ * @module entity-value.utils
8
+ */
9
+
10
+ /**
11
+ * UUID 字符串格式校验正则(不区分大小写)
12
+ */
13
+ export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
+
15
+ /**
16
+ * 将原始值解析为 ISO 格式的日期字符串
17
+ * @remarks 解析失败时返回 null
18
+ */
19
+ function parseDate(raw: unknown): string | null {
20
+ const d = raw instanceof Date ? raw : new Date(raw as string | number);
21
+ return isNaN(d.getTime()) ? null : d.toISOString();
22
+ }
23
+
24
+ /**
25
+ * 将原始值解析为字符串数组
26
+ * @remarks 非数组输入按逗号分隔解析,空白项会被忽略
27
+ */
28
+ function parseStringArray(raw: unknown): string[] {
29
+ if (Array.isArray(raw)) return (raw as unknown[]).map(String);
30
+ const result: string[] = [];
31
+ for (const part of String(raw).split(',')) {
32
+ const trimmed = part.trim();
33
+ if (trimmed !== '') result.push(trimmed);
34
+ }
35
+ return result;
36
+ }
37
+
38
+ /**
39
+ * 将原始值解析为数字数组
40
+ * @remarks 非数组输入按逗号分隔解析,无法转换为数字的项会被丢弃
41
+ */
42
+ function parseNumberArray(raw: unknown): number[] {
43
+ if (Array.isArray(raw)) {
44
+ const out: number[] = [];
45
+ for (const v of raw as unknown[]) {
46
+ const n = Number(v);
47
+ if (!isNaN(n)) out.push(n);
48
+ }
49
+ return out;
50
+ }
51
+ const out: number[] = [];
52
+ for (const part of String(raw).split(',')) {
53
+ const n = Number(part.trim());
54
+ if (!isNaN(n)) out.push(n);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * 将原始值解析为 JSON 类值(keyValue / json 字段共用)
61
+ * @remarks 对象原样返回,字符串按 JSON 解析,失败时输出警告并返回 null
62
+ */
63
+ function parseJsonLike(type: string, raw: unknown): unknown {
64
+ if (raw !== null && typeof raw === 'object') return raw;
65
+ try {
66
+ return JSON.parse(String(raw));
67
+ } catch {
68
+ console.warn(`[rxdb-model] Failed to parse ${type} value for field:`, raw);
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /** 有符号 64 位整数字面量(十进制) */
74
+ const BIGINT_RE = /^[+-]?\d+$/;
75
+
76
+ /** 偶数长度的十六进制字节串 */
77
+ const HEX_BYTES_RE = /^(?:[0-9a-fA-F]{2})*$/;
78
+
79
+ /**
80
+ * 将原始值解析为 bigint
81
+ * @remarks 原生 bigint 原样返回;整数数字与整数字符串转 bigint;其余返回 null
82
+ */
83
+ function parseBigint(raw: unknown): bigint | null {
84
+ if (typeof raw === 'bigint') return raw;
85
+ if (typeof raw === 'string') {
86
+ const text = raw.trim();
87
+ return BIGINT_RE.test(text) ? BigInt(text) : null;
88
+ }
89
+ if (typeof raw === 'number' && Number.isInteger(raw)) return BigInt(raw);
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * 将原始值解析为字节序列
95
+ * @remarks Uint8Array 原样返回;字符串按十六进制解码(奇数长度或非 hex 字符返回 null)
96
+ */
97
+ function parseBinary(raw: unknown): Uint8Array | null {
98
+ if (raw instanceof Uint8Array) return raw;
99
+ if (typeof raw !== 'string') return null;
100
+ const text = raw.trim();
101
+ if (text === '' || text.length % 2 !== 0 || !HEX_BYTES_RE.test(text)) return null;
102
+ const bytes = new Uint8Array(text.length / 2);
103
+ for (let i = 0; i < bytes.length; i += 1) {
104
+ bytes[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
105
+ }
106
+ return bytes;
107
+ }
108
+
109
+ /**
110
+ * 将原始值解析为指定字段类型的目标值
111
+ * @remarks 空值(null / undefined / 空字符串)统一返回 null,未识别的类型原样返回
112
+ * @param type - 字段类型
113
+ * @param raw - 原始值
114
+ * @returns 解析后的值
115
+ */
116
+ export function parseEntityFieldValue(type: EntityFieldType | string, raw: unknown): unknown {
117
+ if (raw == null || raw === '') return null;
118
+
119
+ switch (type) {
120
+ case 'uuid':
121
+ return String(raw).toLowerCase().trim();
122
+ case 'enum':
123
+ case 'oneToOne':
124
+ case 'manyToOne':
125
+ return raw === '' ? null : String(raw);
126
+ case 'number': {
127
+ const n = Number(raw);
128
+ return isNaN(n) ? null : n;
129
+ }
130
+ case 'integer': {
131
+ const n = parseInt(String(raw), 10);
132
+ return isNaN(n) ? null : n;
133
+ }
134
+ case 'boolean':
135
+ return raw === true || raw === 'true' || raw === '1';
136
+ case 'date':
137
+ return parseDate(raw);
138
+ case 'bigint':
139
+ return parseBigint(raw);
140
+ case 'binary':
141
+ return parseBinary(raw);
142
+ case 'stringArray':
143
+ return parseStringArray(raw);
144
+ case 'numberArray':
145
+ return parseNumberArray(raw);
146
+ case 'keyValue':
147
+ case 'json':
148
+ return parseJsonLike(type, raw);
149
+ case 'computed':
150
+ default:
151
+ return raw;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * 严格解析表单输入;失败时返回结构化原因,调用方不得覆盖原始输入。
157
+ */
158
+ export function parseEntityFieldValueStrict(
159
+ type: EntityFieldType | string,
160
+ raw: unknown
161
+ ): { ok: true; value: unknown } | { ok: false; message: string } {
162
+ if (raw == null || raw === '') return { ok: true, value: null };
163
+ if (type === 'integer' && typeof raw === 'string' && !/^[+-]?\d+$/.test(raw.trim())) {
164
+ return { ok: false, message: '必须是整数' };
165
+ }
166
+ if (type === 'numberArray') {
167
+ const values =
168
+ Array.isArray(raw) ? raw : (
169
+ String(raw)
170
+ .split(',')
171
+ .map(part => part.trim())
172
+ );
173
+ if (values.some(value => value === '' || !Number.isFinite(Number(value)))) {
174
+ return { ok: false, message: '必须是数字数组' };
175
+ }
176
+ }
177
+ if (type === 'json' || type === 'keyValue') {
178
+ if (typeof raw === 'string') {
179
+ try {
180
+ JSON.parse(raw);
181
+ } catch {
182
+ return { ok: false, message: 'JSON 格式不正确' };
183
+ }
184
+ }
185
+ }
186
+ if (type === 'bigint' && typeof raw === 'string' && !BIGINT_RE.test(raw.trim())) {
187
+ return { ok: false, message: '必须是大整数' };
188
+ }
189
+ if (type === 'binary' && typeof raw === 'string' && parseBinary(raw) === null) {
190
+ return { ok: false, message: '必须是十六进制字节序列' };
191
+ }
192
+ let value: unknown;
193
+ try {
194
+ value = parseEntityFieldValue(type, raw);
195
+ } catch (error) {
196
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
197
+ }
198
+ if ((type === 'number' || type === 'integer') && (value === null || !Number.isFinite(Number(value)))) {
199
+ return { ok: false, message: type === 'integer' ? '必须是整数' : '必须是数字' };
200
+ }
201
+ if (type === 'integer' && !Number.isInteger(value)) return { ok: false, message: '必须是整数' };
202
+ if (type === 'bigint' && typeof value !== 'bigint') return { ok: false, message: '必须是大整数' };
203
+ if (type === 'binary' && !(value instanceof Uint8Array)) return { ok: false, message: '必须是十六进制字节序列' };
204
+ if (type === 'date' && value === null) return { ok: false, message: '日期格式不正确' };
205
+ return { ok: true, value };
206
+ }
207
+
208
+ /** 二进制列展示时完整展开的最大字节数,超过则截断为前缀 + 字节数。 */
209
+ const BINARY_PREVIEW_BYTES = 16;
210
+
211
+ /**
212
+ * 将字节序列格式化为 hex 预览
213
+ * @remarks 16 字节以内完整展开;更长时显示前 8 字节 hex + 总字节数
214
+ */
215
+ function formatBinary(value: Uint8Array): string {
216
+ if (value.length === 0) return '';
217
+ const toHex = (bytes: Uint8Array): string => Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
218
+ if (value.length <= BINARY_PREVIEW_BYTES) return toHex(value);
219
+ return `${toHex(value.slice(0, 8))}… (${value.length} bytes)`;
220
+ }
221
+
222
+ /** 百分比展示:`0..1` 刻度的值先放大 100 倍。 */
223
+ function formatPercentage(value: number, scale: '0..1' | '0..100'): string {
224
+ return `${scale === '0..1' ? value * 100 : value}%`;
225
+ }
226
+
227
+ /**
228
+ * 将字段值格式化为展示用字符串
229
+ * @remarks 日期按本地格式展示,数组以逗号加空格连接,JSON 类值序列化为 JSON 字符串。
230
+ * `format` 只影响展示形态:货币/百分比/评分/时长/日期显示模式
231
+ * @param type - 字段类型
232
+ * @param value - 字段值
233
+ * @param format - 字段语义标注(可选)
234
+ * @returns 格式化后的字符串
235
+ */
236
+ export function formatEntityFieldValue(type: EntityFieldType | string, value: unknown, format?: FieldFormat): string {
237
+ if (value == null || value === '') return '';
238
+
239
+ switch (type) {
240
+ case 'date': {
241
+ const d = value instanceof Date ? value : new Date(value as string | number);
242
+ if (isNaN(d.getTime())) return '';
243
+ if (format?.kind === 'dateTime') {
244
+ if (format.display === 'date') return d.toLocaleDateString();
245
+ if (format.display === 'time') return d.toLocaleTimeString();
246
+ return d.toLocaleString();
247
+ }
248
+ return d.toLocaleString();
249
+ }
250
+ case 'boolean':
251
+ return value ? 'true' : 'false';
252
+ case 'bigint':
253
+ return String(value);
254
+ case 'binary':
255
+ return value instanceof Uint8Array ? formatBinary(value) : String(value);
256
+ case 'stringArray':
257
+ case 'numberArray':
258
+ return Array.isArray(value) ? (value as unknown[]).join(', ') : String(value);
259
+ case 'number':
260
+ case 'integer': {
261
+ const n = typeof value === 'number' ? value : Number(value);
262
+ if (!Number.isFinite(n)) return String(value);
263
+ switch (format?.kind) {
264
+ case 'currency':
265
+ return `${n} ${format.currency}`;
266
+ case 'percentage':
267
+ return formatPercentage(n, format.scale);
268
+ case 'rating':
269
+ return `${n} ★`;
270
+ case 'duration':
271
+ return `${n} ${format.unit}`;
272
+ default:
273
+ return String(value);
274
+ }
275
+ }
276
+ case 'keyValue':
277
+ case 'json':
278
+ if (typeof value === 'object') {
279
+ try {
280
+ return JSON.stringify(value);
281
+ } catch {
282
+ return String(value);
283
+ }
284
+ }
285
+ return String(value);
286
+ default:
287
+ return String(value);
288
+ }
289
+ }
290
+
291
+ /**
292
+ * 字段校验错误
293
+ */
294
+ export interface FieldValidationError {
295
+ /** 出错字段名 */
296
+ field: string;
297
+ /** 错误提示信息 */
298
+ message: string;
299
+ }
300
+
301
+ /** 邮箱:无空白、恰好一个 `@`、且域名部分含 `.`。 */
302
+ const isEmailText = (text: string): boolean => {
303
+ if (/\s/.test(text)) return false;
304
+ const parts = text.split('@');
305
+ return parts.length === 2 && parts[0].length > 0 && parts[1].includes('.');
306
+ };
307
+
308
+ /** 电话:可选 `+` 前缀,其余仅数字、空格、括号与连字符,长度 3~32。 */
309
+ const isPhoneText = (text: string): boolean => text.length >= 3 && text.length <= 32 && /^\+?[\d ()-]+$/.test(text);
310
+
311
+ /** 十六进制颜色(允许省略 `#`)。 */
312
+ const HEX_COLOR_RE = /^#?[0-9a-fA-F]{6}$/;
313
+
314
+ /** 步长对齐判定容差:浮点除法残差小于它即视为对齐。 */
315
+ const STEP_TOLERANCE = 1e-8;
316
+
317
+ /** 值是否落在带步长的区间刻度上(min 为刻度原点)。 */
318
+ const isOnStep = (value: number, min: number, step: number): boolean => {
319
+ const steps = (value - min) / step;
320
+ return Math.abs(steps - Math.round(steps)) < STEP_TOLERANCE;
321
+ };
322
+
323
+ /**
324
+ * 校验 format 的数字区间(min / max / step)
325
+ * @returns 违规信息;无违规返回 null
326
+ */
327
+ function validateFormatRange(
328
+ displayName: string,
329
+ value: number,
330
+ min: number | undefined,
331
+ max: number | undefined,
332
+ step: number | undefined
333
+ ): string | null {
334
+ if (min !== undefined && value < min) return `${displayName} 必须不小于 ${min}`;
335
+ if (max !== undefined && value > max) return `${displayName} 必须不大于 ${max}`;
336
+ if (step !== undefined && min !== undefined && !isOnStep(value, min, step))
337
+ return `${displayName} 必须按步长 ${step} 取值`;
338
+ return null;
339
+ }
340
+
341
+ /**
342
+ * 校验单个字段值是否满足字段配置约束
343
+ * @remarks 校验必填、UUID 格式、数字 / 整数 / bigint / binary、日期、枚举范围、JSON 格式
344
+ * 与 format 语义(url / email / phone / color / range)
345
+ * @param field - 字段配置
346
+ * @param value - 待校验的字段值
347
+ * @returns 校验错误;校验通过时返回 null
348
+ */
349
+ export function validateEntityFieldValue(field: EntityFieldConfig, value: unknown): FieldValidationError | null {
350
+ if (field.required) {
351
+ if (value == null || value === '') return { field: field.field, message: `${field.displayName} 是必填项` };
352
+ if (Array.isArray(value) && value.length === 0)
353
+ return { field: field.field, message: `${field.displayName} 是必填项` };
354
+ }
355
+
356
+ if (value == null || value === '') return null;
357
+
358
+ switch (field.type) {
359
+ case 'uuid':
360
+ if (!UUID_RE.test(String(value))) return { field: field.field, message: `${field.displayName} 格式不正确` };
361
+ break;
362
+ case 'number':
363
+ case 'integer':
364
+ if (isNaN(Number(value))) return { field: field.field, message: `${field.displayName} 必须是数字` };
365
+ if (field.type === 'integer' && !Number.isInteger(Number(value)))
366
+ return { field: field.field, message: `${field.displayName} 必须是整数` };
367
+ break;
368
+ case 'bigint':
369
+ if (parseBigint(value) === null) return { field: field.field, message: `${field.displayName} 必须是大整数` };
370
+ break;
371
+ case 'binary':
372
+ if (parseBinary(value) === null) return { field: field.field, message: `${field.displayName} 必须是字节序列` };
373
+ break;
374
+ case 'date': {
375
+ const d = value instanceof Date ? value : new Date(value as string | number);
376
+ if (isNaN(d.getTime())) return { field: field.field, message: `${field.displayName} 日期格式不正确` };
377
+ break;
378
+ }
379
+ case 'enum':
380
+ if (field.enumValues && !field.enumValues.includes(String(value))) {
381
+ return { field: field.field, message: `${field.displayName} 值不在允许范围内` };
382
+ }
383
+ break;
384
+ case 'json':
385
+ if (typeof value === 'string') {
386
+ try {
387
+ JSON.parse(value);
388
+ } catch {
389
+ return { field: field.field, message: `${field.displayName} JSON 格式不正确` };
390
+ }
391
+ }
392
+ break;
393
+ }
394
+
395
+ // ── format 语义校验(在类型校验之后) ────────────────────────────────
396
+ const format = field.format;
397
+ if (format) {
398
+ switch (format.kind) {
399
+ case 'url': {
400
+ const text = String(value);
401
+ let url: URL;
402
+ try {
403
+ url = new URL(text);
404
+ } catch {
405
+ return { field: field.field, message: `${field.displayName} URL 格式不正确` };
406
+ }
407
+ if (format.schemes && format.schemes.length > 0) {
408
+ const scheme = url.protocol.slice(0, -1).toUpperCase();
409
+ if (!format.schemes.some(item => item.toUpperCase() === scheme)) {
410
+ return { field: field.field, message: `${field.displayName} 的协议不在允许范围内` };
411
+ }
412
+ }
413
+ break;
414
+ }
415
+ case 'email':
416
+ if (!isEmailText(String(value))) return { field: field.field, message: `${field.displayName} 邮箱格式不正确` };
417
+ break;
418
+ case 'phone':
419
+ if (!isPhoneText(String(value))) return { field: field.field, message: `${field.displayName} 电话格式不正确` };
420
+ break;
421
+ case 'color':
422
+ if ((format.colorSpace === undefined || format.colorSpace === 'hex') && !HEX_COLOR_RE.test(String(value)))
423
+ return { field: field.field, message: `${field.displayName} 颜色格式不正确` };
424
+ break;
425
+ case 'rating': {
426
+ const n = Number(value);
427
+ const message = validateFormatRange(field.displayName, n, format.min, format.max, format.step);
428
+ if (message) return { field: field.field, message };
429
+ break;
430
+ }
431
+ case 'percentage': {
432
+ const n = Number(value);
433
+ const domain = format.scale === '0..1' ? ([0, 1] as const) : ([0, 100] as const);
434
+ const message = validateFormatRange(
435
+ field.displayName,
436
+ n,
437
+ format.min === undefined ? domain[0] : Math.max(format.min, domain[0]),
438
+ format.max === undefined ? domain[1] : Math.min(format.max, domain[1]),
439
+ format.step
440
+ );
441
+ if (message) return { field: field.field, message };
442
+ break;
443
+ }
444
+ case 'number':
445
+ case 'currency':
446
+ case 'duration': {
447
+ const n = Number(value);
448
+ const message = validateFormatRange(field.displayName, n, format.min, format.max, format.step);
449
+ if (message) return { field: field.field, message };
450
+ break;
451
+ }
452
+ }
453
+ }
454
+
455
+ return null;
456
+ }
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * RxDB Model - 核心实体和数据模型定义
3
+ * 提供实体字段处理、实体值处理、结构相等性判断等基础功能
4
+ * @module @aiao/rxdb-model
5
+ */
6
+ export * from './entity-field.utils.js';
7
+ export * from './entity-value.utils.js';
8
+ export * from './structural-equal.js';
9
+
10
+ // 界面能力派生(US-027)
11
+ export * from './entity-capabilities.js';
12
+
13
+ // 类名工具
14
+ export * from './cn.js';
15
+
16
+ // Entity Detail
17
+ export * from './entity-detail/detail-tabs.js';
18
+ export * from './entity-detail/interfaces.js';
19
+
20
+ // Entity List(US-028 手动排序)
21
+ export * from './entity-list/manual-order-list.js';
22
+
23
+ // Entity Form
24
+ export * from './entity-form/form-data.js';
25
+ export * from './entity-form/form-fields.js';
26
+ export * from './entity-form/form-validation.js';
27
+ export * from './entity-form/interfaces.js';
28
+
29
+ // Entity Table
30
+ export * from './entity-table/columns/build-editable-columns.js';
31
+ export * from './entity-table/columns/column-utils.js';
32
+ export * from './entity-table/editors/date-editor.js';
33
+ export * from './entity-table/editors/enum-editor.js';
34
+ export * from './entity-table/editors/global-overlay-editor.js';
35
+ export * from './entity-table/editors/icon-list-editor.js';
36
+ export * from './entity-table/editors/json-editor.js';
37
+ export * from './entity-table/editors/key-value-editor.js';
38
+ export * from './entity-table/editors/lucide-svg.js';
39
+ export * from './entity-table/editors/multiselect-editor.js';
40
+ export * from './entity-table/editors/number-editor.js';
41
+ export * from './entity-table/editors/relation-editor.js';
42
+ export * from './entity-table/editors/safe-list-editor.js';
43
+ export * from './entity-table/editors/tags-editor.js';
44
+ export * from './entity-table/interfaces.js';
45
+ export * from './entity-table/vtable/table-clipboard.js';
46
+ export * from './entity-table/vtable/table-factory.js';
47
+ export * from './entity-table/vtable/table-keyboard.js';
48
+ export * from './entity-table/vtable/table-operations.js';
49
+ export * from './entity-table/vtable/table-row-move.js';
50
+ export * from './entity-table/vtable/table-theme.js';
51
+ export * from './entity-table/vtable/table-tooltip.js';
52
+ export * from './entity-table/vtable/vtable-compat.js';
53
+
54
+ // Query Builder
55
+ export * from './query-builder/models/index.js';
56
+ export * from './query-builder/services/index.js';
57
+ export * from './query-builder/utils/index.js';
@@ -0,0 +1,31 @@
1
+ import type { OperatorDefinition } from './operator.interface.js';
2
+ import type { QueryBuilderRuleGroup, SchemaInfo } from './query-builder-state.js';
3
+ /**
4
+ * 查询构建器配置
5
+ */
6
+ export interface QueryBuilderConfig<T = Record<string, unknown>> {
7
+ /**
8
+ * Schema 信息
9
+ */
10
+ schema?: SchemaInfo;
11
+
12
+ /**
13
+ * 初始查询
14
+ */
15
+ initialQuery?: QueryBuilderRuleGroup<T>;
16
+
17
+ /**
18
+ * 最大嵌套层级(默认 5)
19
+ */
20
+ maxNestingLevel?: number;
21
+
22
+ /**
23
+ * 自定义操作符
24
+ */
25
+ customOperators?: OperatorDefinition[];
26
+
27
+ /**
28
+ * 是否只读模式
29
+ */
30
+ readonly?: boolean;
31
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Models 导出
3
+ */
4
+
5
+ export * from './config.interface.js';
6
+ export * from './operator.interface.js';
7
+ export * from './query-builder-state.js';
8
+ export * from './validation.interface.js';