@aiao/rxdb-plugin-tree 0.0.0-stage → 0.0.28
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 +21 -0
- package/README.md +111 -2
- package/dist/constants-DRludxr-.js +41 -0
- package/dist/constants.d.ts +49 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/entity/tree-entity-base.d.ts +44 -0
- package/dist/entity/tree-entity-base.d.ts.map +1 -0
- package/dist/entity/tree-entity.decorator.d.ts +7 -0
- package/dist/entity/tree-entity.decorator.d.ts.map +1 -0
- package/dist/entity/tree-entity.interface.d.ts +36 -0
- package/dist/entity/tree-entity.interface.d.ts.map +1 -0
- package/dist/entity/tree-metadata.interface.d.ts +34 -0
- package/dist/entity/tree-metadata.interface.d.ts.map +1 -0
- package/dist/generator/TreeRepositoryGenerator.d.ts +37 -0
- package/dist/generator/TreeRepositoryGenerator.d.ts.map +1 -0
- package/dist/generator/index.d.ts +17 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator.js +87 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +564 -0
- package/dist/plugin.d.ts +84 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/query/merge-update-tree.d.ts +80 -0
- package/dist/query/merge-update-tree.d.ts.map +1 -0
- package/dist/query/merge_create.d.ts +9 -0
- package/dist/query/merge_create.d.ts.map +1 -0
- package/dist/query/merge_remove.d.ts +9 -0
- package/dist/query/merge_remove.d.ts.map +1 -0
- package/dist/query/merge_update.d.ts +9 -0
- package/dist/query/merge_update.d.ts.map +1 -0
- package/dist/query/query-tree.utils.d.ts +87 -0
- package/dist/query/query-tree.utils.d.ts.map +1 -0
- package/dist/query/tree-helper.d.ts +143 -0
- package/dist/query/tree-helper.d.ts.map +1 -0
- package/dist/query/tree-query.interface.d.ts +53 -0
- package/dist/query/tree-query.interface.d.ts.map +1 -0
- package/dist/repository/TreeRepository.d.ts +56 -0
- package/dist/repository/TreeRepository.d.ts.map +1 -0
- package/dist/repository/tree-level.utils.d.ts +9 -0
- package/dist/repository/tree-level.utils.d.ts.map +1 -0
- package/dist/repository/tree-repository.interface.d.ts +70 -0
- package/dist/repository/tree-repository.interface.d.ts.map +1 -0
- package/package.json +63 -3
- package/src/constants.ts +97 -0
- package/src/entity/tree-entity-base.ts +100 -0
- package/src/entity/tree-entity.decorator.ts +37 -0
- package/src/entity/tree-entity.interface.ts +43 -0
- package/src/entity/tree-metadata.interface.ts +35 -0
- package/src/generator/TreeRepositoryGenerator.ts +153 -0
- package/src/generator/index.ts +17 -0
- package/src/index.ts +23 -0
- package/src/plugin.ts +111 -0
- package/src/query/merge-update-tree.ts +558 -0
- package/src/query/merge_create.ts +125 -0
- package/src/query/merge_remove.ts +164 -0
- package/src/query/merge_update.ts +132 -0
- package/src/query/query-tree.utils.ts +204 -0
- package/src/query/tree-helper.ts +342 -0
- package/src/query/tree-query.interface.ts +66 -0
- package/src/repository/TreeRepository.ts +133 -0
- package/src/repository/tree-level.utils.ts +14 -0
- package/src/repository/tree-repository.interface.ts +76 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { EntityType, getEntityId, RxDBEntityId, UpdateDataCache } from '@aiao/rxdb';
|
|
2
|
+
import { get_tree_parent_id } from './query-tree.utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 目标实体的祖先 id 集合,{@link TreeHelper.collectAncestorIdsForCount} 的产物
|
|
6
|
+
*/
|
|
7
|
+
export interface TreeAncestorIdSet {
|
|
8
|
+
/** 自目标向上、`maxLevel` 以内解析到的全部祖先 id */
|
|
9
|
+
ids: ReadonlySet<RxDBEntityId>;
|
|
10
|
+
/**
|
|
11
|
+
* 父链是否走到了尽头。
|
|
12
|
+
*
|
|
13
|
+
* `false` 表示中途有一环取不到实体 —— 集合**之外**的候选无从判定,只能回 SQL;
|
|
14
|
+
* 集合**之内**的候选仍是确定的 `true`(走链在断点之前就已经经过它)。
|
|
15
|
+
*/
|
|
16
|
+
complete: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 用预先收集的祖先集合判定单个候选实体(`count` 口径)
|
|
21
|
+
*
|
|
22
|
+
* @param ancestors {@link TreeHelper.collectAncestorIdsForCount} 的结果
|
|
23
|
+
* @param candidateEntity 候选祖先实体
|
|
24
|
+
* @returns true=是祖先, false=不是祖先, undefined=无法确定(调用方回 SQL 重算)
|
|
25
|
+
*/
|
|
26
|
+
export const resolveAncestorForCount = <T extends EntityType>(
|
|
27
|
+
ancestors: TreeAncestorIdSet,
|
|
28
|
+
candidateEntity: InstanceType<T> | null | undefined
|
|
29
|
+
): boolean | undefined => {
|
|
30
|
+
const candidateId = getEntityId(candidateEntity);
|
|
31
|
+
if (candidateId === undefined) {
|
|
32
|
+
return undefined; // 候选实体ID不存在
|
|
33
|
+
}
|
|
34
|
+
if (ancestors.ids.has(candidateId)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return ancestors.complete ? false : undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 树形结构辅助函数管理器
|
|
42
|
+
*
|
|
43
|
+
* 提供树形查询所需的核心功能:
|
|
44
|
+
* - 解析父实体引用
|
|
45
|
+
* - 判断后代/祖先关系(普通 + count 专用变体)
|
|
46
|
+
* - 支持层级限制和循环检测
|
|
47
|
+
*
|
|
48
|
+
* @template T 实体类型
|
|
49
|
+
*
|
|
50
|
+
* @internal 由 merge-update-tree.ts 的 handle*Update 函数使用
|
|
51
|
+
*/
|
|
52
|
+
export class TreeHelper<T extends EntityType> {
|
|
53
|
+
constructor(
|
|
54
|
+
private cache: UpdateDataCache<T>,
|
|
55
|
+
private oldResultMap: Map<RxDBEntityId, InstanceType<T>>
|
|
56
|
+
) {}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 解析父实体
|
|
60
|
+
*
|
|
61
|
+
* 从更新缓存或旧结果集中获取父实体,避免重复查询数据库
|
|
62
|
+
*
|
|
63
|
+
* @param id 父实体ID
|
|
64
|
+
* @returns 父实体实例,如果不存在则返回 undefined
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* **更新缓存优先,旧结果集兜后**。`oldResultMap` 是本批事件**之前**的快照;
|
|
68
|
+
* 同一批里父节点自己也被移动时(`Y.parentId A→null` 与 `M.parentId Z→Y` 同批到达),
|
|
69
|
+
* 先读旧结果集会顺着过期的 `parentId` 往上走,把已经移出子树的节点判成仍在子树内。
|
|
70
|
+
* `cache.getSerializedUpdate` 给的是本批应用后的状态,与 `merge-update-tree.ts`
|
|
71
|
+
* 孤儿复查里的 `resolveParent` 同口径。
|
|
72
|
+
*/
|
|
73
|
+
resolveParentEntity(id: RxDBEntityId): InstanceType<T> | undefined {
|
|
74
|
+
// 本批更新后的状态优先
|
|
75
|
+
const serialized = this.cache.getSerializedUpdate(id);
|
|
76
|
+
if (serialized) {
|
|
77
|
+
this.oldResultMap.set(id, serialized);
|
|
78
|
+
return serialized;
|
|
79
|
+
}
|
|
80
|
+
// 未被本批更新:回旧结果集取已序列化的实体
|
|
81
|
+
return this.oldResultMap.get(id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 检查实体是否是目标实体的后代
|
|
86
|
+
*
|
|
87
|
+
* 从实体向上遍历父级链,检查是否能到达目标实体
|
|
88
|
+
* 包含循环检测,防止无限递归
|
|
89
|
+
*
|
|
90
|
+
* @param entity 要检查的实体
|
|
91
|
+
* @param targetEntityId 目标实体ID (潜在的祖先),为 null 或 undefined 时表示查找所有树(所有节点都有效)
|
|
92
|
+
* @returns 包含是否为后代和层级深度的对象
|
|
93
|
+
*/
|
|
94
|
+
isEntityDescendant(
|
|
95
|
+
entity: InstanceType<T>,
|
|
96
|
+
targetEntityId: RxDBEntityId | null | undefined
|
|
97
|
+
): { isDescendant: boolean; level: number } {
|
|
98
|
+
// 特殊情况:当 targetEntityId 为 null 或 undefined 时,表示查询所有树
|
|
99
|
+
// 此时所有节点都是有效的(包括根节点和子节点)
|
|
100
|
+
if (targetEntityId === null || targetEntityId === undefined) {
|
|
101
|
+
// 计算层级:根节点层级为 0,子节点层级根据到根节点的距离计算
|
|
102
|
+
let currentParentId = get_tree_parent_id<RxDBEntityId>(entity);
|
|
103
|
+
let level = 0;
|
|
104
|
+
const visited = new Set<RxDBEntityId>();
|
|
105
|
+
|
|
106
|
+
while (currentParentId !== null && !visited.has(currentParentId)) {
|
|
107
|
+
level++;
|
|
108
|
+
visited.add(currentParentId);
|
|
109
|
+
const parent = this.resolveParentEntity(currentParentId);
|
|
110
|
+
if (parent) {
|
|
111
|
+
currentParentId = get_tree_parent_id<RxDBEntityId>(parent);
|
|
112
|
+
} else {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { isDescendant: true, level };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let currentParentId = get_tree_parent_id<RxDBEntityId>(entity);
|
|
121
|
+
const visited = new Set<RxDBEntityId>(); // 防止循环引用
|
|
122
|
+
let currentLevel = 0;
|
|
123
|
+
|
|
124
|
+
// 向上遍历父级链
|
|
125
|
+
while (currentParentId !== null && !visited.has(currentParentId)) {
|
|
126
|
+
currentLevel++;
|
|
127
|
+
// 找到目标实体,确认是后代
|
|
128
|
+
if (currentParentId === targetEntityId) {
|
|
129
|
+
return { isDescendant: true, level: currentLevel };
|
|
130
|
+
}
|
|
131
|
+
visited.add(currentParentId);
|
|
132
|
+
|
|
133
|
+
// 解析并继续向上查找
|
|
134
|
+
const parent = this.resolveParentEntity(currentParentId);
|
|
135
|
+
if (parent) {
|
|
136
|
+
currentParentId = get_tree_parent_id<RxDBEntityId>(parent);
|
|
137
|
+
} else {
|
|
138
|
+
break; // 父实体不存在,停止遍历
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { isDescendant: false, level: 0 };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 检查实体是否是目标的祖先
|
|
146
|
+
*
|
|
147
|
+
* 从目标实体向上遍历父级链,检查是否能到达候选实体
|
|
148
|
+
*
|
|
149
|
+
* @param targetEntity 目标实体 (要查找其祖先)
|
|
150
|
+
* @param candidateEntity 候选祖先实体
|
|
151
|
+
* @param maxLevel 层级上限,口径同 `FindTreeOptions.level`:目标自身为 0、父节点为 1。
|
|
152
|
+
* `undefined` 表示不限层级。
|
|
153
|
+
* @returns true 表示候选实体是目标的祖先
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* `maxLevel` 不是可选的性能优化,而是正确性要求:调用方给了 `level` 时,
|
|
157
|
+
* 适配器的递归成员带 `c.__level < level`,`__level` 超出上限的祖先 SQL 根本不会返回;
|
|
158
|
+
* 这里不跟着截断就会把深处的祖先并进增量结果,与全量刷新分叉。
|
|
159
|
+
* 调用方没给 `level`(不限深度)时传 `undefined`,两边都走到根。
|
|
160
|
+
*/
|
|
161
|
+
isEntityAncestor(targetEntity: InstanceType<T>, candidateEntity: InstanceType<T>, maxLevel?: number): boolean {
|
|
162
|
+
const candidateId = getEntityId(candidateEntity);
|
|
163
|
+
if (candidateId === undefined) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 实体不是自己的祖先
|
|
168
|
+
if (candidateId === getEntityId(targetEntity)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let currentParentId = get_tree_parent_id<RxDBEntityId>(targetEntity);
|
|
173
|
+
const visited = new Set<RxDBEntityId>(); // 防止循环引用
|
|
174
|
+
let level = 0; // 距目标实体的跳数:父节点为 1
|
|
175
|
+
|
|
176
|
+
// 从目标实体向上遍历
|
|
177
|
+
while (currentParentId !== null && !visited.has(currentParentId)) {
|
|
178
|
+
level++;
|
|
179
|
+
// 超出层级上限:再往上的节点 SQL 都不会返回,无需继续遍历
|
|
180
|
+
if (maxLevel !== undefined && level > maxLevel) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
// 找到候选实体,确认是祖先
|
|
184
|
+
if (currentParentId === candidateId) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
visited.add(currentParentId);
|
|
188
|
+
|
|
189
|
+
// 解析并继续向上查找
|
|
190
|
+
const parent = this.resolveParentEntity(currentParentId);
|
|
191
|
+
if (parent) {
|
|
192
|
+
currentParentId = get_tree_parent_id<RxDBEntityId>(parent);
|
|
193
|
+
} else {
|
|
194
|
+
break; // 父实体不存在,停止遍历
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return false; // 未找到候选实体
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 检查实体是否是目标的后代 (用于 count 查询)
|
|
203
|
+
*
|
|
204
|
+
* 与 isEntityDescendant 类似,但返回 undefined 表示无法确定
|
|
205
|
+
* 当父实体链中有实体不在更新数据中时,无法继续追踪,返回 undefined
|
|
206
|
+
* 此时需要触发 SQL 刷新以获取准确结果
|
|
207
|
+
*
|
|
208
|
+
* @param entity 要检查的实体
|
|
209
|
+
* @param targetEntityId 目标实体ID,为 null 或 undefined 时表示查找所有树(所有节点都有效)
|
|
210
|
+
* @returns true=是后代, false=不是后代, undefined=无法确定
|
|
211
|
+
*/
|
|
212
|
+
isEntityDescendantForCount(
|
|
213
|
+
entity: InstanceType<T> | null | undefined,
|
|
214
|
+
targetEntityId: RxDBEntityId | null | undefined
|
|
215
|
+
): boolean | undefined {
|
|
216
|
+
if (!entity) {
|
|
217
|
+
return undefined; // 实体不存在,无法判断
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 特殊情况:当 targetEntityId 为 null 或 undefined 时,表示查询所有树
|
|
221
|
+
// 此时所有节点都是有效的(包括根节点和子节点)
|
|
222
|
+
if (targetEntityId === null || targetEntityId === undefined) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let currentParentId = get_tree_parent_id<RxDBEntityId>(entity);
|
|
227
|
+
const visited = new Set<RxDBEntityId>(); // 防止循环引用
|
|
228
|
+
|
|
229
|
+
while (currentParentId !== null && !visited.has(currentParentId)) {
|
|
230
|
+
// 找到目标实体,确认是后代
|
|
231
|
+
if (currentParentId === targetEntityId) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
visited.add(currentParentId);
|
|
235
|
+
|
|
236
|
+
// 尝试从更新数据中获取父实体
|
|
237
|
+
const parentEntity = this.cache.getSerializedUpdate(currentParentId);
|
|
238
|
+
if (parentEntity) {
|
|
239
|
+
currentParentId = get_tree_parent_id<RxDBEntityId>(parentEntity);
|
|
240
|
+
} else {
|
|
241
|
+
// 父实体不在更新数据中,无法继续追踪,返回 undefined
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return false; // 到达根节点仍未找到目标实体
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 计算实体相对目标实体的后代深度 (用于 count 查询)
|
|
251
|
+
*
|
|
252
|
+
* @param entity 要检查的实体
|
|
253
|
+
* @param targetEntityId 目标实体ID;为 null 或 undefined 时表示查询所有树,深度从根节点算起
|
|
254
|
+
* @returns `{ isDescendant, depth }`;无法确定时返回 `undefined`
|
|
255
|
+
*
|
|
256
|
+
* @remarks
|
|
257
|
+
* {@link isEntityDescendantForCount} 只回答「是不是后代」,答不出「隔了几层」,
|
|
258
|
+
* 于是 `countDescendants` 的 `level` 限制在增量合并里整个缺席:`{ entityId, level: 0 }`
|
|
259
|
+
* (只数目标节点自己,后代一个都不算)会被一个直接子节点的 where 翻转加成 1。
|
|
260
|
+
*
|
|
261
|
+
* 深度口径与 `FindTreeOptions.level` 一致:给了 `entityId` 时目标节点自身深度为 0、
|
|
262
|
+
* 直接子节点为 1;没给 `entityId` 时根节点深度为 0。调用方用 `depth <= level` 过滤。
|
|
263
|
+
*
|
|
264
|
+
* 与 {@link isEntityDescendantForCount} 一样,父链上任何一环取不到就返回 `undefined`
|
|
265
|
+
* 让调用方回 SQL 重算 —— 深度算不准时不能猜。
|
|
266
|
+
*/
|
|
267
|
+
descendantDepthForCount(
|
|
268
|
+
entity: InstanceType<T> | null | undefined,
|
|
269
|
+
targetEntityId: RxDBEntityId | null | undefined
|
|
270
|
+
): { isDescendant: boolean; depth: number } | undefined {
|
|
271
|
+
if (!entity) {
|
|
272
|
+
return undefined; // 实体不存在,无法判断
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const findsWholeTree = targetEntityId === null || targetEntityId === undefined;
|
|
276
|
+
let currentParentId = get_tree_parent_id<RxDBEntityId>(entity);
|
|
277
|
+
const visited = new Set<RxDBEntityId>(); // 防止循环引用
|
|
278
|
+
let depth = 0;
|
|
279
|
+
|
|
280
|
+
while (currentParentId !== null && !visited.has(currentParentId)) {
|
|
281
|
+
depth++;
|
|
282
|
+
if (!findsWholeTree && currentParentId === targetEntityId) {
|
|
283
|
+
return { isDescendant: true, depth };
|
|
284
|
+
}
|
|
285
|
+
visited.add(currentParentId);
|
|
286
|
+
|
|
287
|
+
// 尝试从更新数据中获取父实体
|
|
288
|
+
const parentEntity = this.cache.getSerializedUpdate(currentParentId);
|
|
289
|
+
if (!parentEntity) {
|
|
290
|
+
return undefined; // 父实体不在更新数据中,深度无从算起
|
|
291
|
+
}
|
|
292
|
+
currentParentId = get_tree_parent_id<RxDBEntityId>(parentEntity);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 走到根节点:查全树时任何节点都算命中,深度即到根的距离;限定了目标则说明不是它的后代
|
|
296
|
+
return findsWholeTree ? { isDescendant: true, depth } : { isDescendant: false, depth };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 收集目标实体的祖先 id 集合 (用于 count 查询)
|
|
301
|
+
*
|
|
302
|
+
* @param targetEntity 目标实体(要查找其祖先)
|
|
303
|
+
* @param maxLevel 层级上限,口径同 {@link isEntityAncestor};`undefined` 表示不限层级。
|
|
304
|
+
* @returns 祖先 id 集合 + 「父链是否完整」标记,交给 {@link resolveAncestorForCount} 判定候选
|
|
305
|
+
*
|
|
306
|
+
* @remarks
|
|
307
|
+
* `countAncestors` 的目标祖先链在一批事件内是**不变**的(目标自己改父会被调用方
|
|
308
|
+
* 提前 `refresh()` 拦掉),而这条链只取决于起点的 `parentId` 加逐跳的
|
|
309
|
+
* `cache.getSerializedUpdate` —— 对批内每个候选各走一遍是 O(N×depth) 的逐字节重复。
|
|
310
|
+
* 走一次建 Set,候选判定降到 O(1)。
|
|
311
|
+
*
|
|
312
|
+
* 与 {@link isEntityAncestor} 同因,`maxLevel` 不是性能优化而是正确性要求:
|
|
313
|
+
* `countAncestors` 数的是 `__level <= level` 的祖先,不带上限会把超深祖先也计进去。
|
|
314
|
+
* 超出 `maxLevel` 时停止收集并标记 `complete: true` —— 更上方的祖先 SQL 不会返回,
|
|
315
|
+
* 「不是祖先」是能确定的答案,不必为此回一次 SQL。只有父链中途取不到实体
|
|
316
|
+
* 才标记 `complete: false`。环由 `ids` 自身兜住,无需额外的跳数上限。
|
|
317
|
+
*/
|
|
318
|
+
collectAncestorIdsForCount(targetEntity: InstanceType<T>, maxLevel?: number): TreeAncestorIdSet {
|
|
319
|
+
// `ids` 同时充当 visited:祖先链上的节点两种身份完全重合,环用它一并兜住
|
|
320
|
+
const ids = new Set<RxDBEntityId>();
|
|
321
|
+
let currentParentId = get_tree_parent_id<RxDBEntityId>(targetEntity);
|
|
322
|
+
let level = 0; // 距目标实体的跳数:父节点为 1
|
|
323
|
+
|
|
324
|
+
while (currentParentId !== null && !ids.has(currentParentId)) {
|
|
325
|
+
level++;
|
|
326
|
+
// 超出层级上限:SQL 不会返回更上方的祖先,链到此为止即可
|
|
327
|
+
if (maxLevel !== undefined && level > maxLevel) {
|
|
328
|
+
return { ids, complete: true };
|
|
329
|
+
}
|
|
330
|
+
ids.add(currentParentId);
|
|
331
|
+
|
|
332
|
+
// 尝试从更新数据中获取父实体
|
|
333
|
+
const parentEntity = this.cache.getSerializedUpdate(currentParentId);
|
|
334
|
+
if (!parentEntity) {
|
|
335
|
+
return { ids, complete: false }; // 父实体不在更新数据中,无法继续追踪
|
|
336
|
+
}
|
|
337
|
+
currentParentId = get_tree_parent_id<RxDBEntityId>(parentEntity);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { ids, complete: true }; // 走到根节点,或被环截断
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 树查询任务类型
|
|
3
|
+
*
|
|
4
|
+
* 四个查询任务经 `RepositoryQueryExtensions` 的模块扩展并进核心的 `QueryOptions` 联合:
|
|
5
|
+
* 那是插件往闭合联合里加支的唯一入口。不装本插件的应用,`QueryOptions` 上没有这四支,
|
|
6
|
+
* `QueryTask.type` 也不会出现树类型 —— 这正是 US-025 阶段 E 要的「核心不内置树」。
|
|
7
|
+
*/
|
|
8
|
+
import { EntityType } from '@aiao/rxdb';
|
|
9
|
+
import type { TreeQueryType } from '../constants.js';
|
|
10
|
+
import { FindTreeOptions } from '../repository/tree-repository.interface.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* FindDescendants 查询任务
|
|
14
|
+
* 查找指定实体的所有后代节点
|
|
15
|
+
*/
|
|
16
|
+
export interface FindDescendantsQuery<T extends EntityType> {
|
|
17
|
+
type: Extract<TreeQueryType, 'findDescendants'>;
|
|
18
|
+
options: FindTreeOptions<T>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* FindAncestors 查询任务
|
|
23
|
+
* 查找指定实体的所有祖先节点
|
|
24
|
+
*/
|
|
25
|
+
export interface FindAncestorsQuery<T extends EntityType> {
|
|
26
|
+
type: Extract<TreeQueryType, 'findAncestors'>;
|
|
27
|
+
options: FindTreeOptions<T>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* CountDescendants 查询任务
|
|
32
|
+
* 统计指定实体的后代节点数量
|
|
33
|
+
*/
|
|
34
|
+
export interface CountDescendantsQuery<T extends EntityType> {
|
|
35
|
+
type: Extract<TreeQueryType, 'countDescendants'>;
|
|
36
|
+
options: FindTreeOptions<T>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* CountAncestors 查询任务
|
|
41
|
+
* 统计指定实体的祖先节点数量
|
|
42
|
+
*/
|
|
43
|
+
export interface CountAncestorsQuery<T extends EntityType> {
|
|
44
|
+
type: Extract<TreeQueryType, 'countAncestors'>;
|
|
45
|
+
options: FindTreeOptions<T>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 四支树查询任务的联合。
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* 与 {@link TreeQueryType} 的双向一致性由
|
|
53
|
+
* `__tests__/contracts/tree-query-type-parity.spec.ts` 钉死:这里加一支而
|
|
54
|
+
* {@link TREE_QUERY_TYPE_LIST} 没加(或反过来),那条断言编译失败。
|
|
55
|
+
*/
|
|
56
|
+
export type TreeQuery<T extends EntityType> =
|
|
57
|
+
FindDescendantsQuery<T> | FindAncestorsQuery<T> | CountDescendantsQuery<T> | CountAncestorsQuery<T>;
|
|
58
|
+
|
|
59
|
+
declare module '@aiao/rxdb' {
|
|
60
|
+
interface RepositoryQueryExtensions<T extends EntityType> {
|
|
61
|
+
treeFindDescendants: FindDescendantsQuery<T>;
|
|
62
|
+
treeFindAncestors: FindAncestorsQuery<T>;
|
|
63
|
+
treeCountDescendants: CountDescendantsQuery<T>;
|
|
64
|
+
treeCountAncestors: CountAncestorsQuery<T>;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { EntityType, getFingerprintByEntities, getFingerprintPrimitive, Repository, RxDB } from '@aiao/rxdb';
|
|
2
|
+
import { Observable, switchMap, tap } from 'rxjs';
|
|
3
|
+
import { TREE_QUERY_TYPE_LIST, TREE_QUERY_TYPES } from '../constants.js';
|
|
4
|
+
import { ITreeEntity } from '../entity/tree-entity.interface.js';
|
|
5
|
+
import { merge_create } from '../query/merge_create.js';
|
|
6
|
+
import { merge_remove } from '../query/merge_remove.js';
|
|
7
|
+
import { merge_update } from '../query/merge_update.js';
|
|
8
|
+
import { assertTreeLevel } from './tree-level.utils.js';
|
|
9
|
+
import { FindTreeOptions, ITreeRepository } from './tree-repository.interface.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 树结构实体仓库
|
|
13
|
+
* 根据配置决策实体的具体操作
|
|
14
|
+
*
|
|
15
|
+
* @template T - 必须是实现 ITreeEntity 接口的实体类型(通常继承 TreeAdjacencyListEntityBase)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // ✅ 正确:继承 TreeAdjacencyListEntityBase
|
|
20
|
+
* // 不必写 repository:`repository: 'TreeRepository'` 与 `features.tree` 都由基类声明,
|
|
21
|
+
* // 沿原型链继承下来(要覆盖就在自己的 @Entity 里显式声明)
|
|
22
|
+
* @Entity({ name: 'Category' })
|
|
23
|
+
* class Category extends TreeAdjacencyListEntityBase {
|
|
24
|
+
* name!: string;
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // 可以使用树查询方法
|
|
28
|
+
* Category.findDescendants({ entityId: 'root-id' });
|
|
29
|
+
* Category.findAncestors({ entityId: 'leaf-id' });
|
|
30
|
+
*
|
|
31
|
+
* // ❌ 错误:只继承 EntityBase,没有实现 ITreeEntity
|
|
32
|
+
* @Entity({ name: 'User' })
|
|
33
|
+
* class User extends EntityBase {
|
|
34
|
+
* name!: string;
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* // 编译错误:User 不满足 TreeRepository 的类型约束
|
|
38
|
+
* // User.findDescendants({ entityId: 'id' }); // TypeScript 错误
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export class TreeRepository<
|
|
43
|
+
T extends EntityType & (new (...args: never[]) => ITreeEntity),
|
|
44
|
+
RepositoryType extends ITreeRepository<T> = ITreeRepository<T>
|
|
45
|
+
> extends Repository<T, RepositoryType> {
|
|
46
|
+
// 直接摊开单一来源:挂到实体上的静态方法名与 merge 登记的 task 类型本就是同一批,
|
|
47
|
+
// 各写一份时漏掉这里 = 实体上根本没有这个查询方法。
|
|
48
|
+
protected static override _STATIC_METHODS = [...super._STATIC_METHODS, ...TREE_QUERY_TYPE_LIST];
|
|
49
|
+
|
|
50
|
+
constructor(rxdb: RxDB, EntityType: T) {
|
|
51
|
+
super(rxdb, EntityType);
|
|
52
|
+
// 四个树 task 类型各自登记增量 merge。核心的默认 merge 已不认识树类型,
|
|
53
|
+
// 这三行是树查询拿到增量行为的唯一入口;未登记的类型(findAll / count 等)
|
|
54
|
+
// 仍走核心默认实现。
|
|
55
|
+
TREE_QUERY_TYPES.forEach(type => {
|
|
56
|
+
this.queryManager.registerMergeCreateFn(type, merge_create);
|
|
57
|
+
this.queryManager.registerMergeUpdateFn(type, merge_update);
|
|
58
|
+
this.queryManager.registerMergeRemoveFn(type, merge_remove);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 查询子孙
|
|
64
|
+
*/
|
|
65
|
+
findDescendants(options: FindTreeOptions<T>): Observable<InstanceType<T>[]> {
|
|
66
|
+
options = this.#normalizeOptions(options);
|
|
67
|
+
const runner = () =>
|
|
68
|
+
this.primary$.pipe(
|
|
69
|
+
switchMap(local => local.findDescendants(options)),
|
|
70
|
+
tap(this._setLocals)
|
|
71
|
+
);
|
|
72
|
+
return this.queryManager.createTask({
|
|
73
|
+
options: { type: 'findDescendants', options },
|
|
74
|
+
runner,
|
|
75
|
+
getFingerprint: getFingerprintByEntities
|
|
76
|
+
}).result$;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 查询子孙数量
|
|
81
|
+
*/
|
|
82
|
+
countDescendants(options: FindTreeOptions<T>): Observable<number> {
|
|
83
|
+
options = this.#normalizeOptions(options);
|
|
84
|
+
const runner = () => this.primary$.pipe(switchMap(local => local.countDescendants(options)));
|
|
85
|
+
return this.queryManager.createTask({
|
|
86
|
+
options: { type: 'countDescendants', options },
|
|
87
|
+
runner,
|
|
88
|
+
getFingerprint: getFingerprintPrimitive
|
|
89
|
+
}).result$;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 查询祖先
|
|
94
|
+
*/
|
|
95
|
+
findAncestors(options: FindTreeOptions<T>): Observable<InstanceType<T>[]> {
|
|
96
|
+
options = this.#normalizeOptions(options);
|
|
97
|
+
const runner = () =>
|
|
98
|
+
this.primary$.pipe(
|
|
99
|
+
switchMap(local => local.findAncestors(options)),
|
|
100
|
+
tap(this._setLocals)
|
|
101
|
+
);
|
|
102
|
+
return this.queryManager.createTask({
|
|
103
|
+
options: { type: 'findAncestors', options },
|
|
104
|
+
runner,
|
|
105
|
+
getFingerprint: getFingerprintByEntities
|
|
106
|
+
}).result$;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 查询祖先数量
|
|
111
|
+
*/
|
|
112
|
+
countAncestors(options: FindTreeOptions<T>): Observable<number> {
|
|
113
|
+
options = this.#normalizeOptions(options);
|
|
114
|
+
const runner = () => this.primary$.pipe(switchMap(local => local.countAncestors(options)));
|
|
115
|
+
return this.queryManager.createTask({
|
|
116
|
+
options: { type: 'countAncestors', options },
|
|
117
|
+
runner,
|
|
118
|
+
getFingerprint: getFingerprintPrimitive
|
|
119
|
+
}).result$;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 归一查询选项。
|
|
124
|
+
*
|
|
125
|
+
* 两件事:`entityId` 的 `undefined` 与 `null` 在树语义里都表示「查全树」,统一成 `null`;
|
|
126
|
+
* `level` 交给 {@link assertTreeLevel} 校验后原样透传 —— 不传就是 `undefined`(不限深度),
|
|
127
|
+
* 非法值直接抛错。这里**不做裁剪**:把越界值悄悄改成边界值,只会让调用方拿到一份
|
|
128
|
+
* 自己没要过的结果,且掩盖掉注入探针。
|
|
129
|
+
*/
|
|
130
|
+
#normalizeOptions(options: FindTreeOptions<T>): FindTreeOptions<T> {
|
|
131
|
+
return { ...options, level: assertTreeLevel(options.level), entityId: options.entityId ?? null };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { assertOptionalNonNegativeSafeInteger } from '@aiao/rxdb';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 校验树查询深度,未提供时保留不限深度的语义。
|
|
5
|
+
*
|
|
6
|
+
* @param level - 查询深度
|
|
7
|
+
* @returns 非负安全整数;未提供时返回 `undefined`
|
|
8
|
+
* @throws 当深度不是非负安全整数时抛出 `RxDBError`
|
|
9
|
+
*/
|
|
10
|
+
export const assertTreeLevel = (level: number | undefined): number | undefined =>
|
|
11
|
+
assertOptionalNonNegativeSafeInteger(
|
|
12
|
+
level,
|
|
13
|
+
`tree query 'level' must be a non-negative integer, received: ${String(level)}`
|
|
14
|
+
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { EntityStaticType, EntityType, IRepository, RuleGroup } from '@aiao/rxdb';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 树结构查询配置
|
|
5
|
+
*/
|
|
6
|
+
export interface FindTreeOptions<T extends EntityType = EntityType, WhereType = RuleGroup<InstanceType<T>>> {
|
|
7
|
+
/**
|
|
8
|
+
* 查询的根实体 ID
|
|
9
|
+
*/
|
|
10
|
+
entityId?: EntityStaticType<T, 'idType'> | null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 查询条件
|
|
14
|
+
*/
|
|
15
|
+
where?: WhereType;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 查询深度(层级数包含当前节点本身)
|
|
19
|
+
*
|
|
20
|
+
* - 不传:**不限深度** —— `findDescendants` 返回整棵子树、`findAncestors` 返回整条祖先链
|
|
21
|
+
* - `0`:仅当前节点(`entityId` 为空时即所有根节点)
|
|
22
|
+
* - `1`:当前节点 + 直接子节点
|
|
23
|
+
* - `n`:查询到第 n 层深度
|
|
24
|
+
*
|
|
25
|
+
* @defaultValue `undefined`(不限深度)
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* 不裁剪、不兜底:非负整数以外的值(负数、小数、`NaN`、字符串)一律抛 `RxDBError`。
|
|
29
|
+
* `level` 是树查询里唯一被直接插值进递归 CTE 比较式的选项,悄悄改写它等于把注入
|
|
30
|
+
* 变成一次静默的错误查询。插件的 `assertTreeLevel` 与各适配器复用核心的通用数值校验,
|
|
31
|
+
* 绕过 Repository 直接调适配器时同样抛错。
|
|
32
|
+
*
|
|
33
|
+
* 不传时递归深度由各 SQL 适配器内部的失控保护常量兜底(防脏数据把 `parentId` 连成环),
|
|
34
|
+
* 那是实现细节而非查询语义:正常树不会触到。
|
|
35
|
+
*/
|
|
36
|
+
level?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 树结构仓库接口
|
|
41
|
+
*/
|
|
42
|
+
export interface ITreeRepository<T extends EntityType> extends IRepository<T> {
|
|
43
|
+
/**
|
|
44
|
+
* 查询子孙实体
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* - 指定 entityId 时:包含当前节点 + 子孙节点
|
|
48
|
+
* - 不指定 entityId 时:返回所有根节点及其子孙
|
|
49
|
+
*/
|
|
50
|
+
findDescendants(options: FindTreeOptions<T>): Promise<InstanceType<T>[]>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 查询子孙实体数量
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* - 指定 entityId 时:**不包含当前节点**,只统计后代数量
|
|
57
|
+
* - 不指定 entityId 时:统计所有根节点及其后代的总数
|
|
58
|
+
*/
|
|
59
|
+
countDescendants(options: FindTreeOptions<T>): Promise<number>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 查询祖先节点
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* 指定 entityId 时:包含当前节点 + 祖先节点
|
|
66
|
+
*/
|
|
67
|
+
findAncestors(options: FindTreeOptions<T>): Promise<InstanceType<T>[]>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 查询祖先节点数量
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* 指定 entityId 时:**不包含当前节点**,只统计祖先数量
|
|
74
|
+
*/
|
|
75
|
+
countAncestors(options: FindTreeOptions<T>): Promise<number>;
|
|
76
|
+
}
|