@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
package/src/constants.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tree 插件常量
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EntityMetadataOptions, OnDeleteAction, PropertyType, RelationKind } from '@aiao/rxdb';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 树查询任务类型的**单一来源**。
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* 这四个名字要被三处消费:`*Query` 接口的 `type` 字段(类型侧)、
|
|
12
|
+
* {@link TREE_QUERY_TYPES}(三个 merge 的首道过滤)、`TreeRepository._STATIC_METHODS`
|
|
13
|
+
* (挂到实体上的静态方法名)。它们此前各写一份,加第五种树查询时漏掉其中一处
|
|
14
|
+
* **不会报错**,只会让新任务静默落回核心默认 merge、或者压根不挂到实体上。
|
|
15
|
+
*
|
|
16
|
+
* 现在后两者从这里派生,接口侧用 `Extract<TreeQueryType, …>` 与它绑定,
|
|
17
|
+
* 双向一致性由 `__tests__/contracts/tree-query-type-parity.spec.ts` 在编译期钉死。
|
|
18
|
+
*/
|
|
19
|
+
export const TREE_QUERY_TYPE_LIST = ['findDescendants', 'findAncestors', 'countDescendants', 'countAncestors'] as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 树查询任务类型的联合,由 {@link TREE_QUERY_TYPE_LIST} 派生。
|
|
23
|
+
*/
|
|
24
|
+
export type TreeQueryType = (typeof TREE_QUERY_TYPE_LIST)[number];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 树查询任务类型集合
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* 三个 merge 函数用它做首道过滤:`TreeRepository` 把自己注册到 `QueryManager` 时是
|
|
31
|
+
* 按 task 类型逐个登记的,但同一个 Repository 上也跑着 `findAll` / `count` 这类通用查询,
|
|
32
|
+
* 它们必须原样落回核心的默认 merge。没有这一句,树的增量逻辑会去接管非树任务。
|
|
33
|
+
*
|
|
34
|
+
* 元素类型声明成 `string` 而不是 {@link TreeQueryType}:调用方传进来的是
|
|
35
|
+
* `QueryTask.type`(`string`),收窄成联合反而让 `has()` 在调用点编译失败。
|
|
36
|
+
*/
|
|
37
|
+
export const TREE_QUERY_TYPES: ReadonlySet<string> = new Set<string>(TREE_QUERY_TYPE_LIST);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `TreeAdjacencyListEntityBase` 的 `@Entity` 元数据。
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* 邻接表模型的那一份声明:`parentId` 外键、`children` 一对多自关联、
|
|
44
|
+
* `hasChildren` 计算属性,以及 `repository: 'TreeRepository'`。子类沿原型链继承整份,
|
|
45
|
+
* 所以 `@Entity({ name: 'Category' })` 不必重复声明任何一项就有树能力。
|
|
46
|
+
*
|
|
47
|
+
* 之所以导出而不是内联进装饰器:`@TreeEntity` 装饰器要拿它去合并用户自己的选项,
|
|
48
|
+
* 手写实体(不继承基类、自己实现 {@link ITreeEntity})也要靠它对齐字段,
|
|
49
|
+
* 两条路径必须用同一份声明,否则「继承来的树」和「手写的树」在 schema 上会分叉。
|
|
50
|
+
*
|
|
51
|
+
* 放在这个不含装饰器与 rxjs 的模块里,`/generator` 子路径才能读到它而不必导入
|
|
52
|
+
* {@link TreeAdjacencyListEntityBase} —— 生成器只在构建期跑。
|
|
53
|
+
*
|
|
54
|
+
* @see {@link TreeAdjacencyListEntityBase}
|
|
55
|
+
*/
|
|
56
|
+
export const TREE_ADJACENCY_LIST_ENTITY_BASE_OPTIONS: EntityMetadataOptions = {
|
|
57
|
+
name: 'TreeAdjacencyListEntityBase',
|
|
58
|
+
abstract: true, // 标记为抽象类,不会直接创建此类的实例
|
|
59
|
+
// 基类声明了 findDescendants / countAncestors 等静态方法,而这些只由 TreeRepository 注入。
|
|
60
|
+
// 因此这里必须显式声明 —— 子类沿原型链继承到它,才不会「类型上有、运行时没有」。
|
|
61
|
+
// 不写 repository 的子类(`@Entity({ name: 'Category' })`)正是靠这一行拿到树查询能力。
|
|
62
|
+
repository: 'TreeRepository',
|
|
63
|
+
computedProperties: [
|
|
64
|
+
{
|
|
65
|
+
name: 'hasChildren', // 是否有子节点
|
|
66
|
+
displayName: '是否有子节点',
|
|
67
|
+
type: PropertyType.boolean, // 布尔类型
|
|
68
|
+
nullable: true,
|
|
69
|
+
readonly: true
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
relations: [
|
|
73
|
+
{
|
|
74
|
+
name: 'children', // 子节点关系名称
|
|
75
|
+
displayName: '子节点',
|
|
76
|
+
kind: RelationKind.ONE_TO_MANY, // 一对多关系
|
|
77
|
+
mappedEntity: 'TreeAdjacencyListEntityBase', // 关联到同一实体类型
|
|
78
|
+
mappedProperty: 'parent' // 映射到子实体的 parent 属性
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'parent', // 父节点关系名称
|
|
82
|
+
columnName: 'parentId', // 外键列名
|
|
83
|
+
displayName: '父节点',
|
|
84
|
+
kind: RelationKind.MANY_TO_ONE, // 多对一关系
|
|
85
|
+
mappedEntity: 'TreeAdjacencyListEntityBase', // 关联到同一实体类型
|
|
86
|
+
mappedProperty: 'children', // 映射到父实体的 children 属性
|
|
87
|
+
nullable: true, // 允许根节点没有父节点
|
|
88
|
+
onDelete: OnDeleteAction.CASCADE // 删除父节点时级联删除子节点
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
features: {
|
|
92
|
+
tree: {
|
|
93
|
+
type: 'adjacency-list', // 指定为邻接表树形结构
|
|
94
|
+
hasChildren: true // 启用 hasChildren 计算属性
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} as const;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 树形实体基类定义文件
|
|
3
|
+
* 提供基于邻接表模型的树形结构实体基类
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Entity, EntityBase, RxDBEntityId, UUID } from '@aiao/rxdb';
|
|
7
|
+
import { Observable } from 'rxjs';
|
|
8
|
+
import { TREE_ADJACENCY_LIST_ENTITY_BASE_OPTIONS } from '../constants.js';
|
|
9
|
+
import type { FindTreeOptions } from '../repository/tree-repository.interface.js';
|
|
10
|
+
import { ITreeEntity } from './tree-entity.interface.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 树形实体装饰器配置
|
|
14
|
+
* 定义了树形结构所需的父子关系
|
|
15
|
+
*/
|
|
16
|
+
@Entity(TREE_ADJACENCY_LIST_ENTITY_BASE_OPTIONS)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 邻接表树形结构实体基类
|
|
20
|
+
*
|
|
21
|
+
* 基于邻接表模型实现的树形结构,每个节点通过 parent 引用其父节点,
|
|
22
|
+
* 通过 children 集合引用其所有子节点。
|
|
23
|
+
*
|
|
24
|
+
* 邻接表模型是实现树形结构的简单高效方式,适合大多数树形数据场景。
|
|
25
|
+
*
|
|
26
|
+
* 参考资料:
|
|
27
|
+
* - https://www.slideshare.net/slideshow/models-for-hierarchical-data/4179181
|
|
28
|
+
* - https://schinckel.net/2014/09/13/long-live-adjacency-lists/
|
|
29
|
+
*
|
|
30
|
+
* @typeParam Id - 主键类型,与 {@link EntityBase} 的 `Id` 同源,缺省 {@link UUID}。
|
|
31
|
+
* `parentId` 跟随收窄 —— 一个节点的 `parentId` 指向的正是同类节点的 `id`,
|
|
32
|
+
* 两者必须是同一个类型,数值主键的树才能在类型层表达。
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* @Entity({ name: 'Category' })
|
|
37
|
+
* class Category extends TreeAdjacencyListEntityBase {
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // 数值主键的树
|
|
41
|
+
* @Entity({ name: 'Menu', properties: [{ name: 'id', type: PropertyType.integer, primary: true }] })
|
|
42
|
+
* class Menu extends TreeAdjacencyListEntityBase<number> {
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export abstract class TreeAdjacencyListEntityBase<Id extends RxDBEntityId = UUID>
|
|
47
|
+
extends EntityBase<Id>
|
|
48
|
+
implements ITreeEntity
|
|
49
|
+
{
|
|
50
|
+
/**
|
|
51
|
+
* 父节点ID
|
|
52
|
+
*/
|
|
53
|
+
parentId?: Id | null;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 是否有子节点,由数据库计算属性自动填充。
|
|
57
|
+
* `declare` 因为它是 metadata 注入的计算属性,class 不需要初始化。
|
|
58
|
+
*/
|
|
59
|
+
declare hasChildren?: boolean | null;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 查询所有子孙节点
|
|
63
|
+
* @param options - 查询选项,包含实体ID和层级深度
|
|
64
|
+
* @returns Observable 包装的子孙节点数组
|
|
65
|
+
*/
|
|
66
|
+
declare static findDescendants: <T extends TreeAdjacencyListEntityBase<RxDBEntityId>>(
|
|
67
|
+
this: new () => T,
|
|
68
|
+
options: FindTreeOptions<new () => T>
|
|
69
|
+
) => Observable<T[]>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 统计子孙节点数量
|
|
73
|
+
* @param options - 查询选项,包含实体ID和层级深度
|
|
74
|
+
* @returns Observable 包装的子孙节点数量
|
|
75
|
+
*/
|
|
76
|
+
declare static countDescendants: <T extends TreeAdjacencyListEntityBase<RxDBEntityId>>(
|
|
77
|
+
this: new () => T,
|
|
78
|
+
options: FindTreeOptions<new () => T>
|
|
79
|
+
) => Observable<number>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 查询所有祖先节点
|
|
83
|
+
* @param options - 查询选项,包含实体ID和层级深度
|
|
84
|
+
* @returns Observable 包装的祖先节点数组
|
|
85
|
+
*/
|
|
86
|
+
declare static findAncestors: <T extends TreeAdjacencyListEntityBase<RxDBEntityId>>(
|
|
87
|
+
this: new () => T,
|
|
88
|
+
options: FindTreeOptions<new () => T>
|
|
89
|
+
) => Observable<T[]>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 统计祖先节点数量
|
|
93
|
+
* @param options - 查询选项,包含实体ID和层级深度
|
|
94
|
+
* @returns Observable 包装的祖先节点数量
|
|
95
|
+
*/
|
|
96
|
+
declare static countAncestors: <T extends TreeAdjacencyListEntityBase<RxDBEntityId>>(
|
|
97
|
+
this: new () => T,
|
|
98
|
+
options: FindTreeOptions<new () => T>
|
|
99
|
+
) => Observable<number>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 树形实体装饰器
|
|
3
|
+
*
|
|
4
|
+
* `@TreeEntity(...)` 是 `@Entity(...)` 在"树形结构"特化下的封装:
|
|
5
|
+
*
|
|
6
|
+
* - **补全 `features.tree.type`**:缺省时落回 `'adjacency-list'`(邻接表),
|
|
7
|
+
* 未来加入 `nested-set` / `path-enumeration` 等模型时只需扩这里。
|
|
8
|
+
* - **补全 `features.tree.hasChildren`**:默认 `false`,由 {@link TREE_ADJACENCY_LIST_ENTITY_BASE_OPTIONS}
|
|
9
|
+
* 在 `hasChildren: true` 时挂上同名计算属性(`TreeRepository` 用它做轻量 hasChildren 查询)。
|
|
10
|
+
* - **强制 `repository: 'TreeRepository'`**:子类不写 `repository` 也能拿到 `findDescendants` /
|
|
11
|
+
* `countAncestors` 这类树查询方法。`Entity` 装饰器单独使用时如果不写 `repository`,
|
|
12
|
+
* 只会得到普通的 `Repository`(没有树 API)。
|
|
13
|
+
*
|
|
14
|
+
* 也就是说,业务方"用 `@Entity` 还是 `@TreeEntity`"决定了仓库能力,不要混淆。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Entity, EntityMetadataOptions } from '@aiao/rxdb';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 树形实体装饰器
|
|
21
|
+
* 用于将类标记为树形结构实体,并处理树形特定的元数据
|
|
22
|
+
*/
|
|
23
|
+
export const TreeEntity: typeof Entity = (metadataOptions: EntityMetadataOptions) => {
|
|
24
|
+
const tree = metadataOptions.features?.tree ?? {};
|
|
25
|
+
return Entity({
|
|
26
|
+
...metadataOptions,
|
|
27
|
+
features: {
|
|
28
|
+
...metadataOptions.features,
|
|
29
|
+
tree: {
|
|
30
|
+
...tree,
|
|
31
|
+
type: tree.type ?? 'adjacency-list',
|
|
32
|
+
hasChildren: tree.hasChildren ?? false
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
repository: metadataOptions.repository ?? 'TreeRepository'
|
|
36
|
+
});
|
|
37
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* 树形实体接口定义
|
|
4
|
+
* 扩展基础实体接口,添加父子关系支持
|
|
5
|
+
*/
|
|
6
|
+
import { IEntity, IEntityStaticType, RxDBEntityId, SortOrderKey } from '@aiao/rxdb';
|
|
7
|
+
|
|
8
|
+
export interface ITreeEntity extends IEntity {
|
|
9
|
+
/**
|
|
10
|
+
* 父节点主键。与 {@link IEntity.id} 同属 {@link RxDBEntityId} 域 ——
|
|
11
|
+
* 一棵树的 `parentId` 指向的正是另一个节点的 `id`,两者必须允许同样的类型
|
|
12
|
+
* (`string | number | bigint`),否则数值主键的树在类型层根本无法表达。
|
|
13
|
+
*/
|
|
14
|
+
parentId?: RxDBEntityId | null;
|
|
15
|
+
/**
|
|
16
|
+
* 是否有子节点。由 `TreeAdjacencyListEntityBase` 的计算属性自动填充,应用层只读。
|
|
17
|
+
* 未启用 `features.tree.hasChildren` 的实体此字段为 `null`。
|
|
18
|
+
*/
|
|
19
|
+
hasChildren?: boolean | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 支持手动排序的树形实体接口。
|
|
24
|
+
*
|
|
25
|
+
* 在 {@link ITreeEntity} 基础上要求实体声明 `sortOrder` 字段(通常用 fractional indexing
|
|
26
|
+
* 如 `generateKeyBetween` 产生)。拖放、手动排序等场景的算法约束类型。
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* 键类型取自核心排序模块的 {@link SortOrderKey}(唯一来源),树节点的 `sortOrder` 仍可空可缺省;
|
|
30
|
+
* 组合的是键类型而不是非空的 `ISortableEntity`。只是类型:实现它不会让实体获得 `manualOrder` 声明。
|
|
31
|
+
*/
|
|
32
|
+
export interface ISortableTreeEntity extends ITreeEntity {
|
|
33
|
+
sortOrder?: SortOrderKey | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 树形实体类型
|
|
38
|
+
*
|
|
39
|
+
* 继承了 {@link IEntity} 的 `id` / `createdAt` 等字段,再加上
|
|
40
|
+
* `parentId` 与 `hasChildren` 这类树形独有属性。
|
|
41
|
+
* {@link TreeRepository} 据此提供 `findDescendants` / `countAncestors` 等方法。
|
|
42
|
+
*/
|
|
43
|
+
export type TreeEntityType = IEntityStaticType & (new (...args: never[]) => ITreeEntity);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 树形实体的元数据特性
|
|
3
|
+
*
|
|
4
|
+
* 经 `plugin.ts` 的 `declare module '@aiao/rxdb'` 挂到 `EntityMetadataFeatures.tree` 上。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
type TreeType = 'adjacency-list';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 实体元数据树形结构特性接口
|
|
11
|
+
*/
|
|
12
|
+
export interface EntityMetadataTreeFeatures {
|
|
13
|
+
/**
|
|
14
|
+
* 树形结构类型
|
|
15
|
+
*
|
|
16
|
+
* 目前仅支持 adjacency-list (邻接表模型),未来可能支持其他模型:
|
|
17
|
+
* - 'closure-table': 闭包表模型
|
|
18
|
+
* - 'nested-set': 嵌套集模型
|
|
19
|
+
* - 'materialized-path': 物化路径模型
|
|
20
|
+
*
|
|
21
|
+
* 参考资料:
|
|
22
|
+
* - https://www.slideshare.net/slideshow/models-for-hierarchical-data/4179181
|
|
23
|
+
* - https://schinckel.net/2014/09/13/long-live-adjacency-lists/
|
|
24
|
+
*
|
|
25
|
+
* @default 'adjacency-list'
|
|
26
|
+
*/
|
|
27
|
+
type?: TreeType;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 是否有子节点
|
|
31
|
+
* 在树结构里使用,表示当前节点是否有子节点
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
hasChildren?: boolean;
|
|
35
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 树形结构 Repository 生成器
|
|
3
|
+
* 继承基础 Repository 方法生成器,为树形结构实体生成特有的查询方法
|
|
4
|
+
*
|
|
5
|
+
* @module rxdb-plugin-tree/generator
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ENTITY_BASE_METADATA_OPTIONS, type EntityMetadata, type EntityMetadataOptions } from '@aiao/rxdb';
|
|
9
|
+
import {
|
|
10
|
+
buildRules,
|
|
11
|
+
generateEntityRules,
|
|
12
|
+
type GeneratorContext,
|
|
13
|
+
type RepositoryGeneratorSymbols,
|
|
14
|
+
RepositoryMethodsGenerator
|
|
15
|
+
} from '@aiao/rxdb-client-generator';
|
|
16
|
+
import { TREE_ADJACENCY_LIST_ENTITY_BASE_OPTIONS } from '../constants.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 树形结构 Repository 方法生成器
|
|
20
|
+
* 继承 Repository 基类方法,新增 TreeRepository 特有的查询方法
|
|
21
|
+
*/
|
|
22
|
+
export class TreeRepositoryGenerator extends RepositoryMethodsGenerator {
|
|
23
|
+
override readonly name = 'TreeRepository';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 树实体的基类与配套类型都在 `@aiao/rxdb-plugin-tree`(US-025 阶段 E 起核心不再内置树)。
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* 这个值同时决定三处产物:`.js` 里 `TreeAdjacencyListEntityBase` 的 import 来源、
|
|
30
|
+
* `.d.ts` 里 `ITreeEntity` 的 import 来源、以及基类签名带出的 `FindTreeOptions`。
|
|
31
|
+
*/
|
|
32
|
+
override readonly entityBaseModuleSpecifier = '@aiao/rxdb-plugin-tree';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `TreeAdjacencyListEntityBase` 的装饰器实参是常量标识符,CLI 静态求值取不到它的值。
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* 生成器随身带上这份元数据,CLI 才能在分析 `extends TreeAdjacencyListEntityBase` 的实体时
|
|
39
|
+
* 把它回填;否则生成器包得反向依赖本插件才拿得到(RV-015)。顺序为「自身 → 祖先」。
|
|
40
|
+
*
|
|
41
|
+
* `TreeEntityBase` 是 `@TreeEntity` 装饰器给手写实体用的别名,共用同一份声明。
|
|
42
|
+
*/
|
|
43
|
+
override readonly abstractEntityMetadata: ReadonlyMap<string, EntityMetadataOptions[]> = new Map([
|
|
44
|
+
['TreeAdjacencyListEntityBase', [TREE_ADJACENCY_LIST_ENTITY_BASE_OPTIONS, ENTITY_BASE_METADATA_OPTIONS]],
|
|
45
|
+
['TreeEntityBase', [TREE_ADJACENCY_LIST_ENTITY_BASE_OPTIONS, ENTITY_BASE_METADATA_OPTIONS]]
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @inheritDoc
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* 不合并父类结果:`generateMethods` 没有调用 `super`,基类那一份符号由
|
|
53
|
+
* `entity-definition` 单独跑的基类生成轮次自报。
|
|
54
|
+
*/
|
|
55
|
+
override declareSymbols(metadata: EntityMetadata): RepositoryGeneratorSymbols {
|
|
56
|
+
return {
|
|
57
|
+
// `ITreeEntity` 只由邻接表基类带出:`TreeEntityBase` 的实体走默认的 `IEntity`
|
|
58
|
+
entityInterfaces: metadata.extends[0]?.includes('TreeAdjacencyListEntityBase') ? ['ITreeEntity'] : [],
|
|
59
|
+
types: [{ name: `${metadata.name}TreeRule` }, { exported: true, name: `${metadata.name}TreeRuleGroup` }]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected override generateMethods(context: GeneratorContext): void {
|
|
64
|
+
// Tree 特有方法(基类方法已在 generator_entity_definition.ts 中单独生成)
|
|
65
|
+
const { metadata, rxdbNamedImports, siblingNamedImports, generator, file, staticTypesInterface } = context;
|
|
66
|
+
const { name: className } = metadata;
|
|
67
|
+
|
|
68
|
+
// TreeRuleGroup 规则。
|
|
69
|
+
const entityRules = generateEntityRules(generator, metadata).filter(
|
|
70
|
+
d => d.key.startsWith('children.') && d.key !== 'children.id'
|
|
71
|
+
);
|
|
72
|
+
if (entityRules.length === 0) {
|
|
73
|
+
throw new Error(`${className} TreeRepository requires children query rules`);
|
|
74
|
+
}
|
|
75
|
+
const rules = buildRules(entityRules, rxdbNamedImports, siblingNamedImports);
|
|
76
|
+
const names = entityRules.map(r => `'${r.key}'`).join(' | ');
|
|
77
|
+
|
|
78
|
+
file.addTypeAlias({
|
|
79
|
+
name: `${className}TreeRule`,
|
|
80
|
+
type: Array.from(new Set(rules)).join('\n| '),
|
|
81
|
+
hasDeclareKeyword: true,
|
|
82
|
+
docs: ['TreeRule']
|
|
83
|
+
});
|
|
84
|
+
file.addTypeAlias({
|
|
85
|
+
name: `${className}TreeRuleGroup`,
|
|
86
|
+
type: `RuleGroupBase<typeof ${className}, ${names}, ${className}TreeRule>`,
|
|
87
|
+
hasDeclareKeyword: true,
|
|
88
|
+
docs: ['TreeRuleGroup'],
|
|
89
|
+
isExported: true
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 添加 entity 属性到静态类型(Tree 特有)
|
|
93
|
+
staticTypesInterface.addProperty({
|
|
94
|
+
name: 'entity',
|
|
95
|
+
type: `${className}`,
|
|
96
|
+
docs: ['查询的实体']
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const treeOptions = `FindTreeOptions<typeof ${className},${className}TreeRuleGroup>`;
|
|
100
|
+
|
|
101
|
+
this.addStaticMethod(context, {
|
|
102
|
+
method: 'findDescendants',
|
|
103
|
+
options: treeOptions,
|
|
104
|
+
returnType: `${className}[]`,
|
|
105
|
+
metHodDoc: '查询子孙实体(包含自身)',
|
|
106
|
+
example: `// 查询某节点的所有后代\n${className}.findDescendants({ entityId: root.id }).subscribe(list => console.log(list));\n\n// 仅查询直接子节点(level 1)\n${className}.findDescendants({ entityId: root.id, level: 1 }).subscribe(children => console.log(children));`,
|
|
107
|
+
baseSignature: {
|
|
108
|
+
entityBase: 'TreeAdjacencyListEntityBase',
|
|
109
|
+
options: 'FindTreeOptions<new () => T>',
|
|
110
|
+
returnType: 'T[]'
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
this.addStaticMethod(context, {
|
|
115
|
+
method: 'countDescendants',
|
|
116
|
+
options: treeOptions,
|
|
117
|
+
returnType: `number`,
|
|
118
|
+
metHodDoc: '统计子孙实体数量(不包含自身)',
|
|
119
|
+
example: `// 统计某节点下的后代数量\n${className}.countDescendants({ entityId: root.id }).subscribe(count => console.log(count));`,
|
|
120
|
+
baseSignature: {
|
|
121
|
+
entityBase: 'TreeAdjacencyListEntityBase',
|
|
122
|
+
options: 'FindTreeOptions<new () => T>',
|
|
123
|
+
returnType: 'number'
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this.addStaticMethod(context, {
|
|
128
|
+
method: 'findAncestors',
|
|
129
|
+
options: treeOptions,
|
|
130
|
+
returnType: `${className}[]`,
|
|
131
|
+
metHodDoc: '查询祖先实体(包含自身)',
|
|
132
|
+
example: `// 查询某节点的所有祖先(面包屑导航)\n${className}.findAncestors({ entityId: grand.id }).subscribe(ancestors => console.log(ancestors));\n\n// 仅查询直接父节点(level 1)\n${className}.findAncestors({ entityId: grand.id, level: 1 }).subscribe(parents => console.log(parents));`,
|
|
133
|
+
baseSignature: {
|
|
134
|
+
entityBase: 'TreeAdjacencyListEntityBase',
|
|
135
|
+
options: 'FindTreeOptions<new () => T>',
|
|
136
|
+
returnType: 'T[]'
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.addStaticMethod(context, {
|
|
141
|
+
method: 'countAncestors',
|
|
142
|
+
options: treeOptions,
|
|
143
|
+
returnType: `number`,
|
|
144
|
+
metHodDoc: '统计祖先实体数量(不包含自身)',
|
|
145
|
+
example: `// 统计某节点的祖先层级深度\n${className}.countAncestors({ entityId: grand.id }).subscribe(depth => console.log(depth));`,
|
|
146
|
+
baseSignature: {
|
|
147
|
+
entityBase: 'TreeAdjacencyListEntityBase',
|
|
148
|
+
options: 'FindTreeOptions<new () => T>',
|
|
149
|
+
returnType: 'number'
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Tree 插件的构建期生成器入口
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* 供 `rxdb-client-generator` 的 CLI 通过 `repositoryGenerators` 装载:
|
|
7
|
+
*
|
|
8
|
+
* ```jsonc
|
|
9
|
+
* { "repositoryGenerators": ["@aiao/rxdb-plugin-tree/generator#TreeRepositoryGenerator"] }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* 与运行时入口分开:本入口只在构建期跑,不应把装饰器与 rxjs 拉进来。
|
|
13
|
+
*
|
|
14
|
+
* @module rxdb-plugin-tree/generator
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { TreeRepositoryGenerator } from './TreeRepositoryGenerator.js';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* RxDB Tree 插件
|
|
4
|
+
* 树形结构插件,提供邻接表模型的实体基类、装饰器、仓储与增量查询合并
|
|
5
|
+
*
|
|
6
|
+
* 主要功能:
|
|
7
|
+
* - 树形实体基类与装饰器(`TreeAdjacencyListEntityBase` / `@TreeEntity`)
|
|
8
|
+
* - 四个树查询(`findDescendants` / `countDescendants` / `findAncestors` / `countAncestors`)
|
|
9
|
+
* - 树查询的真增量 merge(不像图查询一律回 SQL 刷新)
|
|
10
|
+
*
|
|
11
|
+
* @module rxdb-plugin-tree
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export * from './constants.js';
|
|
15
|
+
export * from './entity/tree-entity-base.js';
|
|
16
|
+
export * from './entity/tree-entity.decorator.js';
|
|
17
|
+
export * from './entity/tree-entity.interface.js';
|
|
18
|
+
export * from './entity/tree-metadata.interface.js';
|
|
19
|
+
export * from './plugin.js';
|
|
20
|
+
export * from './query/tree-query.interface.js';
|
|
21
|
+
export * from './repository/tree-level.utils.js';
|
|
22
|
+
export * from './repository/tree-repository.interface.js';
|
|
23
|
+
export * from './repository/TreeRepository.js';
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview RxDB Tree 插件
|
|
3
|
+
* 树形结构插件入口,提供 TreeRepository 注册与树能力扩展
|
|
4
|
+
*
|
|
5
|
+
* @module rxdb-plugin-tree
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { IRxDBPlugin, Plugin, RxDB, RxDBPluginBase, SyncType } from '@aiao/rxdb';
|
|
9
|
+
import type { LifecycleScope } from '@aiao/utils';
|
|
10
|
+
import { EntityMetadataTreeFeatures } from './entity/tree-metadata.interface.js';
|
|
11
|
+
import { TreeRepository } from './repository/TreeRepository.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* {@link rxDBPluginTree} 的选项。
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* 目前无可配项:树的所有行为都由实体元数据(`features.tree`)和查询选项决定,
|
|
18
|
+
* 没有需要在装配期敲定的开关。保留这个类型是为了让 `Plugin<…>` 的形参具名 ——
|
|
19
|
+
* 将来加选项时是加字段,不是改签名。
|
|
20
|
+
*/
|
|
21
|
+
type RxDBPluginTreeOptions = object;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 树形结构插件。
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* 只做一件事:把 {@link TreeRepository} 登记到 `rxdb` 的仓储注册表,并声明它撑不住
|
|
28
|
+
* `SyncType.QueryCache`。登记是 **scoped** 的 —— 连接断开时随 scope 一起撤销,
|
|
29
|
+
* 不会把指向已拆纪元的仓储留在 `rxdb` 上。
|
|
30
|
+
*
|
|
31
|
+
* 四个树查询(`findDescendants` / `countDescendants` / `findAncestors` / `countAncestors`)
|
|
32
|
+
* 与它们的增量 merge 都挂在 `TreeRepository` 上,不装本插件的应用连 `QueryOptions`
|
|
33
|
+
* 里都没有这四支。
|
|
34
|
+
*
|
|
35
|
+
* 一般不直接 `new`,用工厂 {@link rxDBPluginTree} 交给 `.use()`。
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { rxDBPluginTree } from '@aiao/rxdb-plugin-tree';
|
|
40
|
+
*
|
|
41
|
+
* const rxdb = new RxDB({ entities: [Category] }).use(rxDBPluginTree);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export class RxDBPluginTree extends RxDBPluginBase implements IRxDBPlugin {
|
|
45
|
+
readonly lifecycle = 'scoped' as const;
|
|
46
|
+
name: Uncapitalize<string> = 'tree';
|
|
47
|
+
|
|
48
|
+
install(scope: LifecycleScope) {
|
|
49
|
+
// 传 scope:断开连接时这条注册跟着一起撤销,与 graph 插件同一处理。
|
|
50
|
+
this.rxdb.repository(
|
|
51
|
+
'TreeRepository',
|
|
52
|
+
{
|
|
53
|
+
class: TreeRepository,
|
|
54
|
+
// 这条限制原先是核心 `metadata-validate.ts` 里硬编码的 `unsupportedTreeQueryCache`
|
|
55
|
+
// 规则。核心不该知道 'TreeRepository' 是什么,所以改由注册方自己声明;
|
|
56
|
+
// 核心只负责把声明翻译成 `unsupportedRepositorySyncType` 违规。
|
|
57
|
+
unsupportedSyncTypes: {
|
|
58
|
+
[SyncType.QueryCache]:
|
|
59
|
+
`树查询依赖本地完整的祖先链,而缓存只覆盖查过的 where 命中的行,` +
|
|
60
|
+
`递归会在缺口处静默截断。改用 SyncType.Full / Filter,或把该实体换成普通 Repository。`
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
scope
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
declare module '@aiao/rxdb' {
|
|
69
|
+
interface RxDB {
|
|
70
|
+
tree: RxDBPluginTree;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 把 `TreeRepository` 登进门面轴注册表
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* 与上面 `install()` 里的 `rxdb.repository('TreeRepository', …)` 成对:
|
|
77
|
+
* 那一处是运行期登记,这一处是类型登记。少了它,`@Entity({ repository: 'TreeRepository' })`
|
|
78
|
+
* 只能落到 `RxDBRepositoryName` 的 `(string & {})` 那一支,补全里一个字都没有。
|
|
79
|
+
*/
|
|
80
|
+
interface RxDBRepositories {
|
|
81
|
+
TreeRepository: typeof TreeRepository;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 扩展 EntityMetadataFeatures,添加 tree 字段
|
|
85
|
+
*/
|
|
86
|
+
interface EntityMetadataFeatures {
|
|
87
|
+
/**
|
|
88
|
+
* 树形特性配置
|
|
89
|
+
* 由 rxdb-plugin-tree 插件提供
|
|
90
|
+
*/
|
|
91
|
+
tree?: EntityMetadataTreeFeatures;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* {@link RxDBPluginTree} 的工厂,交给 `RxDB.use()`。
|
|
97
|
+
*
|
|
98
|
+
* @remarks
|
|
99
|
+
* 注册树实体(继承 `TreeAdjacencyListEntityBase` 或 `@TreeEntity` 标注)的应用**必须**
|
|
100
|
+
* 装它:没装时 `repository: 'TreeRepository'` 在注册期找不到仓储,实体初始化直接抛错
|
|
101
|
+
* (契约见 `__tests__/contracts/missing-plugin-error.spec.ts`),不会静默降级成普通仓储。
|
|
102
|
+
*
|
|
103
|
+
* @param db - 宿主 `RxDB` 实例,由 `use()` 注入
|
|
104
|
+
* @returns 插件实例
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const rxdb = new RxDB({ entities: [Category] }).use(rxDBPluginTree);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export const rxDBPluginTree: Plugin<RxDBPluginTreeOptions> = (db: RxDB) => new RxDBPluginTree(db);
|