From 87a5a82653b6187da698852f32a7c0e18499564e Mon Sep 17 00:00:00 2001 From: zhangfisher Date: Wed, 31 Jul 2024 14:45:34 +0800 Subject: [PATCH] update --- .github/workflows/website.yaml | 72 +++++ docs/.vitepress/config.mts | 13 +- docs/guide/adapters.md | 136 +++++++- docs/guide/custom.md | 67 ++++ docs/guide/export.md | 222 +++++++++++++ docs/guide/flextree.md | 125 +++++-- docs/guide/manager.md | 2 +- docs/guide/multitree.md | 41 +++ docs/guide/verify.md | 32 ++ docs/index.md | 6 +- docs/intro/{get-starts.md => get-started.md} | 8 +- docs/intro/history.md | 1 + packages/core/src/adapter.ts | 2 +- packages/core/src/manager.ts | 6 +- packages/core/src/mixins/get.mixin.ts | 7 +- packages/core/src/mixins/verify.mixin.ts | 11 +- packages/core/src/node.ts | 36 +-- packages/core/src/tree.ts | 10 +- packages/core/src/types.ts | 3 +- packages/sqlite/src/index.ts | 4 +- readme.md | 321 +++++++++++++++++- readme_cn.md | 324 +++++++++++++++++++ 22 files changed, 1345 insertions(+), 104 deletions(-) create mode 100644 .github/workflows/website.yaml rename docs/intro/{get-starts.md => get-started.md} (97%) create mode 100644 docs/intro/history.md create mode 100644 readme_cn.md diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml new file mode 100644 index 0000000..0b4e417 --- /dev/null +++ b/.github/workflows/website.yaml @@ -0,0 +1,72 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# +name: Deploy FlexTree website to Pages + +on: + # Runs on pushes targeting the `main` branch. Change this to `master` if you're + # using the `master` branch as the default branch. + push: + branches: [master] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm + # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Install pnpm + run: + npm install -g pnpm + - name: Install dependencies + run: pnpm install # or npm ci / yarn install / bun install / + - name: Install turbo + run: pnpm add -w -D turbo + - name: Build packages + run: pnpm build:all + - name: pnpm docs:build + run: | + pnpm docs:build # or npm run docs:build / yarn docs:build / bun run docs:build + touch docs/.vitepress/dist/.nojekyll + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 \ No newline at end of file diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 36dbd23..53d8884 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -12,13 +12,12 @@ export default defineConfig({ // https://vitepress.dev/reference/default-theme-config nav: [ { text: '首页', link: '/' }, - { text: '指南', link: '/guide' }, - { text: 'API', link: '/api' }, + { text: '指南', link: '/guide' } ], sidebar: [ { text: '关于', link: '/intro/about' }, - { text: '快速入门', link: '/intro/get-starts' }, + { text: '快速入门', link: '/intro/get-started' }, { text: '工作原理', link: '/intro/principle' }, { text: '常见问题', link: '/intro/question' }, { text: '更新历史', link: '/intro/history' }, @@ -42,14 +41,14 @@ export default defineConfig({ { text: '节点关系', link: '/guide/relation' }, { text: 'FlexTree', link: '/guide/flextree' }, { text: '校验', link: '/guide/verify' }, - { text: '多树表', link: '/guide/multitree' }, { text: '导出', link: '/guide/export' }, - { text: '自定义', link: '/guide/custom' }, - { text: '数据库', link: '/guide/adapters' }, + { text: '自定义', link: '/guide/custom' }, + { text: '多树表', link: '/guide/multitree' }, + { text: '数据库适配', link: '/guide/adapters' }, ], }, { - text: '数据库适配', + text: '适配器', items: [ { text: 'Sqlite', link: '/adapters/sqlite' }, { text: 'Prisma', link: '/adapters/prisma' }, diff --git a/docs/guide/adapters.md b/docs/guide/adapters.md index f62bc70..5042b3a 100644 --- a/docs/guide/adapters.md +++ b/docs/guide/adapters.md @@ -1,26 +1,134 @@ -# 数据库适配器 +# 适配器 + `FlexTree`本质上就是将对树的查询、删除、移动、更新等操作转换为`SQL`,然后交给数据库引擎去执行。所以`FlexTree`是一个抽象的树存储库,它并不直接操作数据库,而是通过数据库适配器来操作数据库。 -## 查询方式 +## 适配器接口 -`FlexTree`提供了`2`种查询树的方式,包括: +当用户调用`FlexTree`的API后,会调用数据库适配器的方法来操作数据库。数据库适配器是一个实现了`IFlexTreeAdapter`接口的对象,它负责执行`SQL`语句并返回结果。 -### 使用FlexTreeManager查询 +`IFlexTreeAdapter`接口定义如下: + +```ts +interface IFlexTreeAdapter { + // 当数据库打开准备就绪时 + ready: boolean + // 绑定树管理器 + bind: (treeManager: manager.FlexTreeManager) => void + // 执行sql,并返回结果 + exec: (sqls: string | string[]) => Promise + // 执行查询并返回结果 + getRows: (sql: string) => Promise + // 执行查询并返回标量 + getScalar: (sql: string) => Promise + open: (config?: any) => Promise + // 返回一个数据库实例对象 + db: any +} +``` + + +### ready + +当数据库适配器准备就绪时,`ready`属性为`true`,否则为`false`。 + +### bind + +`bind`方法用于绑定树管理器,当`FlexTree`创建时,会调用`bind`方法将树管理器绑定到适配器上。 + +### exec + +执行`SQL`语句,`exec`方法接收一个`SQL`语句或`SQL`语句数组,然后执行`SQL`语句。 + +### getRows + +执行查询并返回结果集,`getRows`方法接收一个`SQL`语句,然后执行查询并返回结果集。 + +### getScalar + +执行查询并返回标量,`getScalar`方法接收一个`SQL`语句,然后执行查询并返回标量。 + + +### open + +当`FlexTree`初始化时,会调用`open`方法打开数据库连接。 + +### db + +返回一个数据库实例对象,仅仅在测试中使用。 + + +## 适配器实现示例 + +以下是`flextree-sqlite-adapter`的实现代码。 -通过核心的`FlexTreeManager`类来执行树的各种查询。 ```ts +import type { FlexTreeManager, IFlexTreeAdapter } from 'flextree' +import Database from 'better-sqlite3' + +export type SqliteDatabase = Database.Database +export default class SqliteAdapter implements IFlexTreeAdapter { + _db?: SqliteDatabase + _options: Database.Options + _ready: boolean = false + _filename?: string + _treeManager?: FlexTreeManager + constructor(filename?: string, options?: Database.Options) { + this._options = Object.assign({}, options) + this._filename = filename || ':memory:' + } + + get ready() { return this._ready } + get db() { return this._db! as SqliteDatabase } + get treeManager() { return this._treeManager! } + get tableName() { return this.treeManager.tableName } + bind(treeManager: FlexTreeManager) { + this._treeManager = treeManager + } + open(options?: Database.Options) { + return new Promise((resolve, reject) => { + try { + this._db = new Database(this._filename, Object.assign({}, this._options, options)) + this._ready = true + resolve(this._db) + } catch (e: any) { + this._ready = false + reject(e) + } + }) + } -import { FlexTreeManager } from "felxtree" -import SqliteAdapter from 'flextree-sqlite-adapter'; -const sqliteAdapter = new SqliteAdapter("org.db") + assertDbIsOpen() { + if (!this.db) { + throw new Error('Sqlite database is not opened.') + } + } -const treeManager = new FlexTreeManager({ - adapter: sqliteAdapter -}) + async getRows(sql: string): Promise { + this.assertDbIsOpen() + return await this.db.prepare(sql).all() + } + + async getScalar(sql: string): Promise { + this.assertDbIsOpen() + return await this.db.prepare(sql).pluck().get() as T + } + + async exec(sqls: string | string[]) { + this.assertDbIsOpen() + if (typeof sqls === 'string') { + sqls = [sqls] + } + const stmts = sqls.map(sql => this.db.prepare(sql)) + const trans = this.db.transaction(() => { + for (const stmt of stmts) { + stmt.run() + } + }) + trans() + } +} ``` -- 通过`FlexTreeManager`实例方法来查询树 -- 上例中使用`flextree-sqlite-adapter`来示例如,如果您使用`MySQL`、`PostgreSQL`、`MongoDB`、`Redis`等数据库,可以使用 -`prisma`等适配器,只需将`SqliteAdapter`替换为`PrismaAdapter`即可。 + diff --git a/docs/guide/custom.md b/docs/guide/custom.md index be63624..eff39d9 100644 --- a/docs/guide/custom.md +++ b/docs/guide/custom.md @@ -1,2 +1,69 @@ # 自定义 +`FlexTree`允许自定义关键字段和扩展节点字段。 + +## 关键字段 + +`FlexTree`中,默认情况下,每一个节点均具`id`、`level`、`leftValue`、`rightValue`、`name`这五个关键字段,如果要在一个表中存储多棵树,还需要加上`treeId`字段。 + + +可以通过以下方法自定义关键字段,方法如下: + +```ts + +const tree = new FlexTreeManager<{}, +// 泛型参数:节点扩展字段 + { + id:['pk',string], // id字段名称和类型 + treeId:['tree',number], + name:string + } +>('tree', { + // 自定义字段名称 + fields:{ + id:'pk', + treeId:'tree', + name:'title', + leftValue:'lft', + rightValue:"rgt", + level:'lv' + } +}) + +``` + +- 以上将`id`字段改为`pk`,`treeId`字段改为`tree`,`name`字段改为`title`,`leftValue`字段改为`lft`,`rightValue`字段改为`rgt`,`level`字段改为`lv`。 +- 通过泛型参数重新声明关键字段的名称和类型。 + + +## 扩展字段 + + +除了`id`、`level`、`leftValue`、`rightValue`、`name`这五个关键字段,还可以通过第一个泛型参数声明其他字段,比如: + +```ts + +const tree = new FlexTreeManager<{ + size:number + color:string + icon:string +}, +// 泛型参数:节点扩展字段 + { + id:['pk',string], // id字段名称和类型 + treeId:['tree',number], + name:string + } +>('tree', { + // 自定义字段名称 + fields:{ + id:'pk', + treeId:'tree', + name:'title', + leftValue:'lft', + rightValue:"rgt", + level:'lv' + } +}) + +``` \ No newline at end of file diff --git a/docs/guide/export.md b/docs/guide/export.md index 245ad5b..3fb23ad 100644 --- a/docs/guide/export.md +++ b/docs/guide/export.md @@ -1 +1,223 @@ # 导出 + +`FlexTree`支持将树导出为`Json`和`List`格式。 + + +下面以下面的树为例,说明如何导出树。 + + +Root + + A + A1 + A2 + A3 + + B + B1 + B2 + B3 + + C + C1 + C2 + C3 + + +## toJson + +`FlexTree`和`FlexTreeNode`均支持`toJson`方法,用于将树导出为`Json`格式。 + +```ts +toJson( + options?: FlexTreeExportJsonOptions +): FlexTreeExportJsonFormat + +interface FlexTreeExportJsonOptions< + Fields extends Record = object, + KeyFields extends CustomTreeKeyFields = DefaultTreeKeyFields, +> { + childrenField?: string + level?: number // 限定导出的级别 + fields?: (keyof IFlexTreeNode)[] + includeKeyFields?: boolean +} + + +``` + +- **参数** + +| 参数 | 类型 | 默认 | 描述 | +| --- | --- | --- | --- | +| `options` | `FlexTreeExportJsonOptions` | 无 | 导出选项 | +| `options.childrenField` | `string` | `'children'` | 子节点字段名 | +| `options.level` | `number` | 无 | 限定导出的级别 | +| `options.fields` | `(keyof IFlexTreeNode)[]` | 无 | 导出的字段 | +| `options.includeKeyFields` | `boolean` | `false` | 是否导出关键字段 | + + +- **返回** + +返回一个`JSON`对象,子节点字段名默认为`children`,可以通过`options.childrenField`参数自定义。 + +```ts +type FlexTreeExportJsonFormat< + Fields extends Record = object, + KeyFields extends CustomTreeKeyFields = DefaultTreeKeyFields, + TreeNode extends IFlexTreeNode = IFlexTreeNode, + NodeId = NonUndefined[1], +> = TreeNode & { + children?: FlexTreeExportJsonFormat[] +} +``` + +- **示例** + +```ts +import type { FlexTreeOptions, IFlexTreeNode } from 'flextree' +import { FlexTreeManager,FlexTree, FlexTreeVerifyError } from 'flextree' + +import SqliteAdapter from 'flextree-sqlite-adapter' +const sqliteDriver = new SqliteAdapter() +await sqliteDriver.open() + +const tree = new FlexTree('tree', { + adapter: sqliteDriver, +}) +await tree.load() + +tree.toJson() + +``` + +输出的结果如下: + +```json +{ + "id":1, + "name": "root", + "children":[ + { + "id":2, + "name": "A", + "children":[ + { "id":3, "name": "A1" }, + { "id":4, "name": "A2" }, + { "id":5, "name": "A3" } + ] + }, + { + "id":6, + "name": "B", + "children":[ + { "id":7, "name": "B1" }, + { "id":8, "name": "B2" }, + { "id":9, "name": "B3" } + ] + }, + { + "id":10, + "name": "C", + "children":[ + { "id":11, "name": "C1" }, + { "id":12, "name": "C2" }, + { "id":13, "name": "C3" } + ] + } + ] +} +``` + +- **说明** + + - `toJson`方法会将树导出为`Json`格式,可以通过`options.level`参数限定导出的级别。 + - `toJson`方法可以在`FlexTree`和`FlexTreeNode`中调用。 + - 可以通过`options.fields`参数指定导出的字段 + - 默认情况下不会导出`leftValue`和`rightValue`字段,可以通过`options.includeKeyFields`参数指定是否导出关键字段 + - 可以通过`options.childrenField`参数指定子节点字段名 + +## toList + +`FlexTree`和`FlexTreeNode`均支持`toList`方法,用于将树导出为带`pid`字段的`list`节点数组格式。 + +```ts +toList( + options?: FlexTreeExportListOptions +): FlexTreeExportListFormat +interface FlexTreeExportListOptions< + Fields extends Record = object, + KeyFields extends CustomTreeKeyFields = DefaultTreeKeyFields, +> { + pidField?: string + level?: number // 限定导出的级别 + fields?: (keyof IFlexTreeNode)[] + includeKeyFields?: boolean +} +``` +- **参数** + +| 参数 | 类型 | 默认 | 描述 | +| --- | --- | --- | --- | +| `options` | `FlexTreeExportListOptions` | 无 | 导出选项 | +| `options.pidField` | `string` | `'pid'` | 父节点字段名 | +| `options.level` | `number` | 无 | 限定导出的级别 | +| `options.fields` | `(keyof IFlexTreeNode)[]` | 无 | 导出的字段 | +| `options.includeKeyFields` | `boolean` | `false` | 是否导出关键字段 | + +- **返回** + +返回一个`list`节点数组,父节点字段名默认为`pid`,可以通过`options.pidField`参数自定义。 + +```ts +export type FlexTreeExportListFormat< + Fields extends Record = object, + KeyFields extends CustomTreeKeyFields = DefaultTreeKeyFields, + TreeNode extends IFlexTreeNode = IFlexTreeNode, + NodeId = NonUndefined[1], + OPTIONS extends FlexTreeExportListOptions = FlexTreeExportListOptions, +> = ( + (OPTIONS['fields'] extends string[] ? Extract : TreeNode) + & { [P in OPTIONS['pidField'] & string]: NodeId } + )[] +``` + +- **示例** + +```ts +import type { FlexTreeOptions, IFlexTreeNode } from 'flextree' +import { FlexTreeManager,FlexTree, FlexTreeVerifyError } from 'flextree' +import SqliteAdapter from 'flextree-sqlite-adapter' +const sqliteDriver = new SqliteAdapter() +await sqliteDriver.open() + +const tree = new FlexTree('tree', { + adapter: sqliteDriver, +}) +await tree.load() + +tree.toList() + +``` + +输出的结果如下: + +```json +[ + { "id":1, "name": "root", "pid":0 }, + { "id":2, "name": "A", "pid":1 }, + { "id":3, "name": "A1","pid":2 }, + { "id":4, "name": "A2", "pid":2 }, + { "id":5, "name": "A3", "pid":2 }, + { "id":6, "name": "B", "pid":1 }, + { "id":7, "name": "B1", "pid":6 }, + { "id":8, "name": "B2", "pid":6 }, + { "id":9, "name": "B3", "pid":6 }, + { "id":10, "name": "C", "pid":1 }, + { "id":11, "name": "C1", "pid":10 }, + { "id":12, "name": "C2", "pid":10 }, + { "id":13, "name": "C3", "pid":10 } +] +``` + + +:::warning 提示 +`toList`和`toJson`方法均支持`level`参数,用于限定导出的级别。可以在`FlexTree`和`FlexTreeNode`中调用。 +::: \ No newline at end of file diff --git a/docs/guide/flextree.md b/docs/guide/flextree.md index 166a05a..972c5e6 100644 --- a/docs/guide/flextree.md +++ b/docs/guide/flextree.md @@ -129,19 +129,19 @@ FlexTreeNode(Root) children({color:red}[]) //* [] FlexTreeNode(A) children({color:red}[]) // length=0 - {color:#ddd}FlexTreeNode({color:#ddd}A1) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}A2) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}A3) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}A1) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}A2) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}A3) // 未加载 FlexTreeNode(B) children({color:red}[]) // length=0 - {color:#ddd}FlexTreeNode({color:#ddd}B1) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}B2) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}B3) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}B1) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}B2) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}B3) // 未加载 FlexTreeNode(C) children({color:red}[]) // length=0 - {color:#ddd}FlexTreeNode({color:#ddd}C1) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}C2) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}C3) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}C1) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}C2) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}C3) // 未加载 以上`A`、`B`、`C`三个节点的状态为`not-loaded`,并且其所有子节点和后代节点均未加载。 @@ -167,9 +167,9 @@ FlexTreeNode(Root) children({color:red}[]) //* [] FlexTreeNode(A) children({color:red}[]) // length=0 - {color:#ddd}FlexTreeNode({color:#ddd}A1) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}A2) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}A3) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}A1) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}A2) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}A3) // 未加载 FlexTreeNode(B) // loaded children({color:red}[]) // length=3 FlexTreeNode(B1) @@ -177,9 +177,9 @@ FlexTreeNode(Root) FlexTreeNode(B3) FlexTreeNode(C) children({color:red}[]) // length=0 - {color:#ddd}FlexTreeNode({color:#ddd}C1) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}C2) // 未加载 - {color:#ddd}FlexTreeNode({color:#ddd}C3) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}C1) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}C2) // 未加载 + {color:#ddd;text-decoration: line-through}FlexTreeNode({color:#ddd}C3) // 未加载 :::warning 提示 @@ -188,7 +188,7 @@ FlexTreeNode(Root) ## 根据路径访问节点 -当`FlexTree`或`FlexTreeNode`加载完毕后,可以通过`getByPath`来获取指定路径的节点实例。 +当`FlexTree`或`FlexTreeNode`加载完毕后,可以通过使用`FlexTree`和`FlexTreeNode`对象实例的`getByPath`来获取指定路径的节点实例。 ```ts getByPath( @@ -236,13 +236,100 @@ b1.getByPath('B-1/B-1-1') - **说明** + - `FlexTree`和`FlexTreeNode`对象实例均有`getByPath`方法,`FlexTree.getByPath`方法用于在整个树检索,而`FlexTreeNode.getByPath`方法的路径是相对于节点的。 + - 可以使用相对路径语法,`./`表示当前节点,`../`代表父节点,`../../`代表祖先节点等。 + + +## 获取节点 + +使用`FlexTree`和`FlexTreeNode`对象实例的`get`方法来获取在所在节点及其后代节点中返回指定的实例。 + +```ts +get(nodeId: NodeId): FlexTreeNode | undefined +``` +- **参数** + +| 字段名称 | 数据类型 | 描述 | +| ---- | ---- | ---- | +| `nodeId` | `NodeId` | 节点的唯一标识 | + +- **返回值** + +返回指定`nodeId`的`FlexTreeNode`节点实例,如果节点不存在则返回`undefined`。 + +## 节点状态 + +当配置`FlexTree.options.lazt=true`启用懒加载时,`FlexTreeNode`节点实例具有状态属性,用于表示节点的加载状态。 + +```ts +type FlexTreeNodeStatus = 'not-loaded' | 'loading' | 'loaded' | 'error' +``` + +- **状态取值** + +| 状态 | 描述 | +| ---- | ---- | +| `not-loaded` | 未加载 | +| `loading` | 加载中 | +| `loaded` | 已加载 | +| `error` | 加载错误 | +## 同步数据 -:::warning 提示 -`FlexTree`和`FlexTreeNode`对象实例均有`getByPath`方法,`FlexTree.getByPath`方法用于在整个树检索,而`FlexTreeNode.getByPath`方法的路径是相对于节点的,可以使用相对路径语法等。 -::: +`FlexTree`或`FlexTreeNode`提供`sync`方法,用于重新从数据库中加载节点数据。 + +```ts +async sync(includeDescendants: boolean = false):void +``` + +## FlexTree +- **属性** +| 方法名称 | 返回类型 | 描述 | +| ---- | ---- | ---- | +| `root` | `FlexTreeNode` | 返回根节点 | +| `status` | `string` | 获取树根节点的状态 | +| `options` | `FlexTreeOptions` | 获取选项 | +| `manager` | `FlexTreeManager` | 获取管理器 | + + +- **方法** + +| 方法名称 | 返回类型 | 描述 | +| ---- | ---- | ---- | +| `load` | `Promise` | 加载树到内存中 | +| `getByPath` | `FlexTreeNode` | 根据路径获取节点 | +| `get` | `FlexTreeNode` | 获取节点 | +| `find` | `FlexTreeNode[]` | 查找节点 | +| `toJson` | `TreeNode` | 序列化树为对象 | +| `toList` | `TreeNode[]` | 序列化树为`pid`数组 | +| `on` | `void` | 监听事件 | +| `off` | `void` | 移除事件监听 | +| `emit` | `void` | 触发事件 | +| `sync` | `void` | 同步数据 | ## FlexNode + +- **属性** + +| 方法名称 | 返回类型 | 描述 | +| ---- | ---- | ---- | +| `root` | `FlexTreeNode` | 返回根节点 | +| `status` | `string` | 获取树根节点的状态 | +| `options` | `FlexTreeOptions` | 获取选项 | +| `manager` | `FlexTreeManager` | 获取管理器 | + + +- **方法** + +| 方法名称 | 返回类型 | 描述 | +| ---- | ---- | ---- | +| `load` | `Promise` | 加载树到内存中 | +| `getByPath` | `FlexTreeNode` | 根据路径获取节点 | +| `get` | `FlexTreeNode` | 获取节点 | +| `find` | `FlexTreeNode[]` | 查找节点 | +| `toJson` | `TreeNode` | 序列化树为对象 | +| `toList` | `TreeNode[]` | 序列化树为`pid`数组 | +| `sync` | `void` | 同步数据 | \ No newline at end of file diff --git a/docs/guide/manager.md b/docs/guide/manager.md index 7dbb22f..0952ec5 100644 --- a/docs/guide/manager.md +++ b/docs/guide/manager.md @@ -38,7 +38,7 @@ class FlexTreeManager{ | `tableName` | string | 无 | 必须的,数据库表名称 | | `options` | FlexTreeManagerOptions | {} | 可选的,配置选项 | | `options.treeId` | string | 无 | 可选,当多树表时指定 | -| `options.adapter` | IDatabaseDriver | 无 | 必须的,访问数据库的适配器 | +| `options.adapter` | IFlexTreeAdapter | 无 | 必须的,访问数据库的适配器 | | `options.keyFields` | KeyFields | | 可选的,自定义树节点的关键字段名称 | ## 泛型参数 diff --git a/docs/guide/multitree.md b/docs/guide/multitree.md index 02ed909..5fc8e8c 100644 --- a/docs/guide/multitree.md +++ b/docs/guide/multitree.md @@ -1,3 +1,44 @@ # 多树表 +在基于左右值的树表中,每棵树只能有一个唯一的根节点。通过`FlexTree`和`FlexTreeManager`均只能管理一棵树。 + + +如果有多棵树,即有多个根节点,则可能通过以下方式来实现: + +## 第1步: 创建多树表 + 多树表是一种特殊的树表,它可以同时展示多棵树。多树表的每一列都可以展示一棵树,这些树之间是相互独立的,互不影响。 + +多树表需要增加一个额外的字段`treeId`来区分不同的树。 + +```prisma + +model Org { + id Int @id @default(autoincrement()) + name String? + treeId Int? // [!code ++] + level Int? + leftValue Int? + rightValue Int? + // 其他字段 +} + +``` + +## 第2步: 创建多树对象 + +在创建`FlexTree`或`FlexTreeManager`时,需要指定`treeId`字段的值。 + +```ts + +const tree = new FlexTreeManager('org', { + adapter: new PrismaAdapter(prisma), + treeId:1 // [!code ++] +}) + +const tree = new FlexTree('tree', { + adapter: new PrismaAdapter(prisma), + treeId:2 // [!code ++] +}) +``` + diff --git a/docs/guide/verify.md b/docs/guide/verify.md index e3428a6..b6fd0b0 100644 --- a/docs/guide/verify.md +++ b/docs/guide/verify.md @@ -1 +1,33 @@ # 校验树 + +`FlexTree`是基于左右值算法的树结构,其**树结构的完整性严格依赖于数据库中每一个节点的`leftValue`和`rightValue`值的正确性**。 + +但是如果因为一些异常错误操作导致了树结构的`leftValue`和`rightValue`值不正确,那么将导致树结构被破坏。 + +`FlexTreeManager`提供了`verify`校验方法来检查树结构的完整性。 + +```ts +import type { FlexTreeOptions, IFlexTreeNode } from 'flextree' +import { FlexTreeManager,FlexTree, FlexTreeVerifyError } from 'flextree' + +import SqliteAdapter from 'flextree-sqlite-adapter' +const sqliteDriver = new SqliteAdapter() +await sqliteDriver.open() + +const tree = new FlexTree('tree', { + adapter: sqliteDriver, +}) +await tree.load() + // 校验树结构是否正确 +tree.verify() // true/false // [!code ++] + +``` + + +- **说明** + + - `verify`方法会检查树结构的完整性,如果树结构完整则返回`true`,否则返回`false`。 + - 如果树结构不完整,将抛出`FlexTreeVerifyError`异常,异常中包含了校验失败的节点信息。 + - `verify`方法不会对树结构进行修复,只是检查树结构的完整性。 + + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 819a457..04403f0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,12 +4,12 @@ layout: home hero: name: "FlexTree" - text: "面向数据库的树存储组件" - tagline: 基于左右值算法 + text: "树数据库存储组件" + tagline: Nodejs/TypeScript/基于左右值算法 actions: - theme: brand text: 快速开始 - link: /markdown-examples + link: /intro/get-started - theme: alt text: GitHub link: https://github.com/zhangfisher/flextree diff --git a/docs/intro/get-starts.md b/docs/intro/get-started.md similarity index 97% rename from docs/intro/get-starts.md rename to docs/intro/get-started.md index 89c0878..8f69b47 100644 --- a/docs/intro/get-starts.md +++ b/docs/intro/get-started.md @@ -34,7 +34,11 @@ 设计部 -> 以上树的显示采用开源库[LiteTree](https://zhangfisher.github.io/lite-tree/)实现。 +:::warning 提示 + +本网站采用`vitepress`构建,以上树的显示采用开源库[LiteTree](https://zhangfisher.github.io/lite-tree/)渲染。 + +::: ## 第1步:安装核心库 @@ -66,7 +70,7 @@ pnpm add flextree-sqlite-adapter `flextree-sqlite-driver`是`flextree`的`sqlite3`数据库驱动,基于`sqlite3`数据库存储。 :::warning 提示 -如果你使用的是`MySQL`、`PostgreSQL`等数据库,可以安装对应的驱动,如`flextree-prima-driver`,或者基于`flextree`提供的`IDatabaseDriver`自定义驱动。 +如果你使用的是`MySQL`、`PostgreSQL`等数据库,可以安装对应的驱动,如`flextree-prima-driver`,或者基于`flextree`提供的`IFlexTreeAdapter`自定义驱动。 ::: diff --git a/docs/intro/history.md b/docs/intro/history.md new file mode 100644 index 0000000..684cc03 --- /dev/null +++ b/docs/intro/history.md @@ -0,0 +1 @@ +# 更新历史 diff --git a/packages/core/src/adapter.ts b/packages/core/src/adapter.ts index 74b0ef0..1dfb03c 100644 --- a/packages/core/src/adapter.ts +++ b/packages/core/src/adapter.ts @@ -4,7 +4,7 @@ import type * as manager from './manager' -export interface IDatabaseAdapter { +export interface IFlexTreeAdapter { // 当数据库打开准备就绪时 ready: boolean // 绑定树管理器 diff --git a/packages/core/src/manager.ts b/packages/core/src/manager.ts index 52f648c..4dd6bd5 100644 --- a/packages/core/src/manager.ts +++ b/packages/core/src/manager.ts @@ -8,7 +8,7 @@ import type { RequiredDeep } from 'type-fest' import sqlString from 'sqlString' import { mix } from 'ts-mixer' import mitt from 'mitt' -import type { IDatabaseAdapter } from './adapter' +import type { IFlexTreeAdapter } from './adapter' import { FlexTreeDriverError, FlexTreeError, FlexTreeInvalidUpdateError } from './errors' import type { CustomTreeKeyFields, DefaultTreeKeyFields, FlexTreeEvents, IFlexTreeNode, NonUndefined } from './types' import { MoveNodeMixin } from './mixins/move.mixin' @@ -33,7 +33,7 @@ export interface FlexTreeManagerOptions { leftValue?: string rightValue?: string } - adapter: IDatabaseAdapter + adapter: IFlexTreeAdapter } export interface FlexTreeManager< @@ -94,7 +94,7 @@ export class FlexTreeManager< private _tableName: string private _treeId: any private _fields: RequiredDeep> - private _adapter: IDatabaseAdapter + private _adapter: IFlexTreeAdapter private _ready: boolean = false // 当driver准备就绪时,ready为true时,才允许执行读写操作 private _emitter = mitt() private _lastUpdateAt = 0 diff --git a/packages/core/src/mixins/get.mixin.ts b/packages/core/src/mixins/get.mixin.ts index ebe7370..4de95e2 100644 --- a/packages/core/src/mixins/get.mixin.ts +++ b/packages/core/src/mixins/get.mixin.ts @@ -47,9 +47,10 @@ export class GetNodeMixin< * @param {number} [options.level] 限定返回的层级,0表示不限制,1表示只返回根节点,2表示返回根节点和其子节点, 依次类推 * @returns TreeNode[] */ - async getNodes(this: FlexTreeManager, options?: { level?: number }): Promise { - const { level } = Object.assign({ level: 0 }, options) - const sql = this._sql(`SELECT * FROM ${this.tableName} + async getNodes(this: FlexTreeManager, options?: { level?: number,fields?:keyof TreeNode }): Promise { + const { level,fields } = Object.assign({ level: 0,fields:[] }, options) + const fieldList = fields.length>0 ? fields.map(f=>`${f}`).join(',') : '*' + const sql = this._sql(`SELECT ${fieldList} FROM ${this.tableName} WHERE {__TREE_ID__} ${this.keyFields.leftValue}>0 AND ${this.keyFields.rightValue}>0 ${level > 0 ? `AND ${this.keyFields.level}<=${level}` : ''} ORDER BY ${this.keyFields.leftValue} diff --git a/packages/core/src/mixins/verify.mixin.ts b/packages/core/src/mixins/verify.mixin.ts index be927c1..c6c0876 100644 --- a/packages/core/src/mixins/verify.mixin.ts +++ b/packages/core/src/mixins/verify.mixin.ts @@ -15,7 +15,16 @@ export class VerifyTreeMixin< * */ async verify(this: FlexTreeManager, nodes?: TreeNode[]) { - nodes = nodes || await this.getNodes() + nodes = nodes || await this.getNodes({ + // @ts-ignore + fields:[ + this.keyFields.id, + this.keyFields.name, + this.keyFields.leftValue, + this.keyFields.rightValue, + this.keyFields.level + ] + }) const pnodes: IFlexTreeNode[] = [] for (let i = 0; i < nodes.length; i++) { const node = nodes[i] as IFlexTreeNode diff --git a/packages/core/src/node.ts b/packages/core/src/node.ts index 2a47663..16b33bc 100644 --- a/packages/core/src/node.ts +++ b/packages/core/src/node.ts @@ -5,8 +5,7 @@ import type { FlexTree } from './tree' import { FlexTreeInvalidError, FlexTreeNodeNotFoundError, FlexTreeNotFoundError } from './errors' import { filterObject } from './utils/filterObject' import { getRelNodePath } from './utils/getRelNodePath' -import { isNull } from './utils/isNull' -import { createArrayProxy } from './utils/createArrayProxy' +import { isNull } from './utils/isNull' /** * 节点状态 @@ -107,22 +106,7 @@ export class FlexTreeNode< } return ancestors } - - /** - * 当所有子节点加载完成时,更新节点状态 - * - * 最后一个节点的rightValue等于其父节点的rightValue - 1 - * - * @param index - * @param node - */ - private onAddChildren(index:number,node:FlexTreeNode){ - if(this._status === 'loading'){ - if(node.rightValue + 1 === this.rightValue ){ - this._status = 'loaded' - } - } - } + /** * 从数据库中同步节点数据 * @@ -203,7 +187,7 @@ export class FlexTreeNode< /** * 获取该节点下id=nodeId的节点实例 */ - get(nodeId: NodeId, includeDescendants: boolean = false): FlexTreeNode | undefined { + get(nodeId: NodeId): FlexTreeNode | undefined { if (this.id === nodeId) { return this } @@ -212,12 +196,10 @@ export class FlexTreeNode< if (node.id === nodeId) { return node } else { - if (includeDescendants) { - const n = node.get(nodeId, includeDescendants) + const n = node.get(nodeId) if (n) { return n } - } } } } @@ -228,16 +210,14 @@ export class FlexTreeNode< * @param condition 条件函数 * @returns 返回符合条件的节点集合 */ - find(condition: (node: FlexTreeNode) => boolean, includeDescendants: boolean = true): FlexTreeNode[] { + find(condition: (node: FlexTreeNode) => boolean): FlexTreeNode[] { const nodes: FlexTreeNode[] = [] if (this._children) { for (const node of this._children) { if (condition(node)) { nodes.push(node) } - if (includeDescendants) { - nodes.push(...node.find(condition, includeDescendants)) - } + nodes.push(...node.find(condition)) } } return nodes @@ -297,7 +277,7 @@ export class FlexTreeNode< const nodeObj = new FlexTreeNode(node, preNode, this._tree) preNode.children!.push(nodeObj) preNode = nodeObj - if (nodeRightValue - nodeLeftValue > 1 && (maxLevel==0 || (maxLevel>0 && nodeLevel < maxLevel)) ) { + if (nodeRightValue - nodeLeftValue > 1 && (maxLevel===0 || (maxLevel>0 && nodeLevel < maxLevel)) ) { pnodes.push(preNode) } } else { @@ -310,7 +290,7 @@ export class FlexTreeNode< const nodeObj = new FlexTreeNode(node, parent, this._tree) parent.children!.push(nodeObj) preNode = nodeObj - if (nodeRightValue - nodeLeftValue > 1 && (maxLevel==0 || (maxLevel>0 && nodeLevel< maxLevel)) ) { + if (nodeRightValue - nodeLeftValue > 1 && (maxLevel===0 || (maxLevel>0 && nodeLevel< maxLevel)) ) { pnodes.push(preNode) } break diff --git a/packages/core/src/tree.ts b/packages/core/src/tree.ts index de03c22..48e2ca9 100644 --- a/packages/core/src/tree.ts +++ b/packages/core/src/tree.ts @@ -18,7 +18,6 @@ export class FlexTree< > { private _options: RequiredDeep> private _treeId: TreeId - private _status: FlexTreeStatus = 'not-loaded' private _manager: FlexTreeManager private _root?: FlexTreeNode @@ -79,6 +78,11 @@ export class FlexTree< } await node.update(data) } + async sync() { + if (this._root) { + await this._root.sync(true) + } + } /** * 根据节点id获取节点实例 */ @@ -86,7 +90,7 @@ export class FlexTree< if (nodeId === this._root?.id) { return this._root } else { - return this._root?.get(nodeId, true) + return this._root?.get(nodeId) } } @@ -96,7 +100,7 @@ export class FlexTree< * @returns 返回满足条件的节点列表 */ find(condition: (node: FlexTreeNode) => boolean): FlexTreeNode[] { - return this._root!.find(condition, true) + return this._root!.find(condition) } toJson(options?: FlexTreeExportJsonOptions): FlexTreeExportJsonFormat { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3fa8bea..4417217 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -110,8 +110,7 @@ export type FlexTreeExportJsonFormat< KeyFields extends CustomTreeKeyFields = DefaultTreeKeyFields, TreeNode extends IFlexTreeNode = IFlexTreeNode, NodeId = NonUndefined[1], -> -= TreeNode & +> = TreeNode & { children?: FlexTreeExportJsonFormat[] } diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts index 47ac11e..9aeccb2 100644 --- a/packages/sqlite/src/index.ts +++ b/packages/sqlite/src/index.ts @@ -1,8 +1,8 @@ -import type { FlexTreeManager, IDatabaseAdapter } from 'flextree' +import type { FlexTreeManager, IFlexTreeAdapter } from 'flextree' import Database from 'better-sqlite3' export type SqliteDatabase = Database.Database -export default class SqliteAdapter implements IDatabaseAdapter { +export default class SqliteAdapter implements IFlexTreeAdapter { _db?: SqliteDatabase _options: Database.Options _ready: boolean = false diff --git a/readme.md b/readme.md index 0ccddaa..6138195 100644 --- a/readme.md +++ b/readme.md @@ -1,25 +1,316 @@ + +[Home](https:/github.com/zhangfisher/flextree) | [简体中文](./readme_cn.md) + +`FlexTree` is a tree storage and management component based on the `left-right` algorithm, which provides efficient tree structure storage and access, and supports a variety of tree operations such as add, delete, modify, query, traversal, and movement. + +**Features** + +- based on the `Nested Set Model`, efficient tree structure storage and access. +- simple and easy-to-use `API` +- rich tree operations, such as `add`, `delete`, `modify`, `query`, `traversal`, `movement`, etc. +- developed using `TypeScript`, providing complete and friendly type definitions +- supports any database storage, such as `SQLite`, `MySQL`, `PostgreSQL`, etc. +- `95%+` test coverage to ensure code quality +- suitable for `Node.js` environment + +# Nested Set Model + +When developing `Nodejs` applications, when you need to store trees in the database, there are several common storage structures: + +- Adjacency List Structure +- Path Enumeration Structure +- Nested Tree Structure +- Closure Table Structure + +Each algorithm has its own advantages and disadvantages, and the appropriate algorithm should be chosen based on the actual application scenario. + +`Nested Set Model` (also known as `Left-Right Value` model) is a method used to store tree structure data, represented by two fields (commonly referred to as `lft` and `rgt`) indicating the position of nodes in the tree. + +In the Nested Set Model, the `lft` value of each node is less than the `lft` values of all its descendants, and the `rgt` value is greater than the `rgt` values of all its descendants. This allows us to retrieve all descendants of a node with a simple query by looking for all nodes whose `lft` and `rgt` values fall within this range. + +The distribution of left and right values in the Nested Set Model is determined by `Depth-First Search` traversal. During the traversal, a `lft` value is assigned whenever entering a node, and an `rgt` value is assigned whenever leaving a node. Thus, the `lft` and `rgt` values of each node form an interval, and all values within this interval correspond to the node's descendants. + + +![](./docs/intro/lr.png) + +eg: + +| id | leftValue | rightValue | name | +|----|-----|-----|------| +| 1 | 1 | 14 | root | +| 2 | 2 | 9 | A | +| 3 | 10 | 11 | B | +| 4 | 12 | 13 | C | +| 5 | 3 | 4 | A-1 | +| 6 | 5 | 6 | A-2 | +| 7 | 7 | 8 | A-3 | + + + +
    +
  • + root +
      +
    • A +
        +
      • A-1
      • +
      • A-2
      • +
      • A-3
      • +
      +
    • +
    • B
    • +
    • C
    • + +
    + + +# Getting Started + +## Step 1: Install the core library + + ```ts +npm install flextree +// or +yarn add flextree +// or +pnpm add flextree +``` + +## Step 2: Configure the database adapter + + +Next, depending on how your application accesses the database, you need to install the corresponding database adapter. + +In this example, we use `Sqlite`, and install the database adapter `flextree-sqlite-adapter`. + +```ts +npm install flextree-sqlite-adapter +// or +yarn add flextree-sqlite-adapter +// or +pnpm add flextree-sqlite-adapter +``` + +`flextree-sqlite-adapter` is the `sqlite3` database driver for `flextree`, based on the `sqlite3` database storage. + +If you are using a `MySQL`, `PostgreSQL`, or other database, you can install the corresponding driver, such as `flextree-prima-adapter`, or customize the driver based on the `IFlexTreeAdapter` provided by `flextree`. + +## Step 3: Create a tree table + +Next, we need to create a tree table in the database. + +If you are using the `sqlite` database, you can use the following `sql` statement to create the table: + +```ts +import SqliteAdapter from 'flextree-sqlite-adapter'; + +const sqliteAdapter = new SqliteAdapter("org.db") +await sqliteAdapter.open() +await sqliteAdapter.exec(` + CREATE TABLE IF NOT EXISTS org ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(60), + level INTEGER, + leftValue INTEGER, + rightValue INTEGER, +`) + +``` + +Above, we created an `org` table with the following fields: + +| Field | Type | Description | +| --- | --- | --- | +| `id` | INTEGER | Primary key, auto-increment | +| `name` | VARCHAR(60) | Name | +| `level` | INTEGER | Level | +| `leftValue` | INTEGER | Left value | +| `rightValue` | INTEGER | Right value | + + +In general, the above fields are required, and you can add other fields as needed. + + +## Step 4: Create a tree manager -import { FlexTree } from "flextree"; +Next, we create a tree manager `OrgManager` to manage the tree. -const tree = new FlexTree({....}) -// 更新操作 -tree.update(async ()=>{ - const node = tree.get({....}) - node.children.add({....}) - node.children.add({....}) - node.children.add({....}) - node.children.add({....}) - node.children.insert({...}) +```ts +import { FlexTreeManager } from 'flextree'; +import SqliteAdapter from 'flextree-sqlite-adapter'; + +const sqliteAdapter = new SqliteAdapter("org.db") +await sqliteAdapter.open() + +const orgManager = new FlexTreeManager("org",{ + adapter: sqliteAdapter }) +``` + + +## Step 5: Add tree nodes + +Then we can start adding nodes to the organization tree. -const node = tree.find({....}) -node.update(async (node)={ - node.children.add({....}) - node.children.add({....}) - node +然后我们就可以开始向组织架构树中添加节点了。 + +```ts +// create root node +await orgManager.createRoot({ + name: "Root" }) +// add child nodes to the root node +await orgManager.addNodes([ + { name: "A" }, + { name: "B" }, + { name: "C"} +]) + +const node = await orgManager.findNode({name:"A"}) +await orgManager.addNodes( [ + { name: "A1" }, + { name: "A2" }, + { name: "A3" }, + { name: "A4" } + ],node) + +``` + +Above, we created a complete tree structure, with the root node `Root`, and three child nodes `A`, `B`, and `C` under the root node, and four child nodes `A1`, `A2`, `A3`, and `A4` under the `A` node. + +we can use the `addNodes` method to add nodes to the tree, which supports batch addition of nodes and multiple forms of adding child nodes. + +## Step 6: Access the tree + +above, we have created a complete tree, and we can access the tree in two ways. + +- `FlexTreeManager` +- `FlexTree` + + +### 获取节点 + +```ts +// get all nodes +await orgManager.getNodes() +// get nodes at level 1-3, excluding nodes at level 4 and below +await orgManager.getNodes(3) +// get node by id +await orgManager.getNode(1) +// get root node +await orgManager.getRoot() + +// get node by name +const node = await orgManager.findNode({name:"A"}) +// get node's children +await orgManager.getChildren(node) +// get node's descendants +await orgManager.getDescendants(node) +// get node's descendants, including itself +await orgManager.getDescendants(node,{includeSelf:true}) +// get node's descendants, level=2 is equivalent to only get direct child nodes +await orgManager.getDescendants(node,{level:2}) +// get node's descendants, level=1 is equivalent to only get direct child nodes +await orgManager.getDescendants(node,{level:1}) + +// get node's ancestors +await orgManager.getAncestors(node) +// get node's parent +await orgManager.getParent(node) +// get node's siblings +await orgManager.getSiblings(node) +// get node's siblings, including itself +await orgManager.getSiblings(node,{includeSelf:true}) +// get node's next sibling +await orgManager.getNextSibling(node) +// get node's previous sibling +await orgManager.getPrevSibling(node) + +``` +### Find node + +```ts +await orgManager.findNode({name:"A"}) +await orgManager.findNodes({level:1}) +``` + +### Move node + +```ts +import { FirstChild, LastChild,PreviousSibling,NextSibling } from 'flextree' +const admin = await orgManager.findNode({name:"admin"}) +const market = await orgManager.findNode({name:"market"}) + +// move admin node to market node +await orgManager.move(admin,market) +await orgManager.move(admin,market,LastChild) +// move admin node to market node, and become the first child node +await orgManager.move(admin,market,FirstChild) +// move admin node to market node, and become the last child node +await orgManager.move(admin,market,PreviousSibling) +// move admin node to market node, and become the previous sibling node +await orgManager.move(admin,market,NextSibling) + +// move admin node up +await orgManager.moveUpNode(admin) +// move admin node down +await orgManager.moveDownNode(admin) + +``` + +### Delete node + +```ts +const admin = await orgManager.findNode({name:"admin"}) +// delete admin node +await orgManager.deleteNode(admin) +// delete all nodes +await orgManager.clear() ``` + +### Query node relation + +```ts + +const admin = await orgManager.findNode({name:"admin"}) +const market = await orgManager.findNode({name:"market"}) + +// get the relationship between admin and market nodes +const relation = await getNodeRelation(admin,market) + +export enum FlexTreeNodeRelation { + Self = 0, + Parent = 1, + Child = 2, + Siblings = 3, + Descendants = 4, + Ancestors = 5, + DiffTree = 6, + SameTree = 7, + SameLevel = 8, + Unknow = 9, +} + +``` + +# Recommendation + +- [Internationalization Solution for React/Vue/Nodejs/Solidjs - VoerkaI18n](https://zhangfisher.github.io/voerka-i18n/) +- [React Form Development Library - speedform](https://zhangfisher.github.io/speed-form/) +- [Terminal Interface Development Enhancement Library - Logsets](https://zhangfisher.github.io/logsets/) +- [Log Output Library - VoerkaLogger](https://zhangfisher.github.io/voerkalogger/) +- [Decorator Development - FlexDecorators](https://zhangfisher.github.io/flex-decorators/) +- [Finite State Machine Library - FlexState](https://zhangfisher.github.io/flexstate/) +- [Universal Function Tool Library - FlexTools](https://zhangfisher.github.io/flex-tools/) +- [CSS-IN-JS Library - Styledfc](https://zhangfisher.github.io/styledfc/) +- [VSCode Plugin for Adding Comments to JSON Files - json_comments_extension](https://github.com/zhangfisher/json_comments_extension) +- [Library for Developing Interactive Command Line Programs - mixed-cli](https://github.com/zhangfisher/mixed-cli) +- [Powerful String Interpolation Variable Processing Tool Library - flexvars](https://github.com/zhangfisher/flexvars) +- [Frontend Link Debugging Assistant Tool - yald](https://github.com/zhangfisher/yald) +- [Asynchronous Signal - asyncsignal](https://github.com/zhangfisher/asyncsignal) +- [bundle Vue styles into JavaScript - vite-plugin-vue-style-bundler ](https://github.com/zhangfisher/vite-plugin-vue-style-bundler) +- [Tree Component- LiteTree](https://github.com/zhangfisher/lite-tree) \ No newline at end of file diff --git a/readme_cn.md b/readme_cn.md new file mode 100644 index 0000000..da90657 --- /dev/null +++ b/readme_cn.md @@ -0,0 +1,324 @@ +[官网](https:/github.com/zhangfisher/flextree) | [English](./readme.md) + +`FlexTree`是`Nodejs`下一个基于左右值算法的树结构库,它提供了一种简单的方式来存储和操作树形结构数据。 +`FlexTree`提供了简单而丰富的`API`让你可以轻松的操作树,如增删改查、遍历、移动、查询等。 + +**主要特性:** + +- 基于左右值算法,高效的树结构存储和访问 +- 简单易用的`API` +- 丰富的树操作,如增删改查、遍历、移动、查询等 +- 采用`TypeScript`开发,提供完整友好的类型定义 +- 支持任意数据库存储,如`SQLite`、`MySQL`、`PostgreSQL`等 +- `95%+`的测试覆盖率,保证代码质量 +- 适用`Node.js`环境 + + +# 了解树模型 + +在开发`Nodejs`应用时,当需要在数据库中存储树时,常见的存储结构有以下几种: + +- 邻接列表结构 +- 路径枚举结构 +- 嵌套树结构 +- 闭包表结构 + +以上算法各有优缺点,应该根据实际的应用场景选择合适的算法。 + +`嵌套树模型(Nested Set Model)`也被称为`左右值`模型,它是一种用于存储树形结构数据的方法,通过两个字段(通常被称为 `lft` 和 `rgt`)来表示节点在树中的位置。 + +在嵌套树模型中,每个节点的`lft`值都小于其所有子节点的`lft`值,`rgt`值都大于其所有子节点的 `rgt` 值。这样,我们可以通过一个简单的查询来获取一个节点的所有后代,只需要查找`lft` 和 `rgt` 值在这个范围内的所有节点即可。 + +嵌套树模型的左右值分布方式是通过`深度优先遍历(Depth-First Search)`来确定的。在遍历过程中,每当进入一个节点时,就分配一个 `lft` 值,每当离开一个节点时,就分配一个 `rgt` 值。这样,每个节点的 `lft` 和 `rgt` 值就形成了一个区间,这个区间内的所有值都对应该节点的子节点。 + +![](./docs/intro/lr.png) + +例如,下面是一个嵌套树模型的例子: + +| id | leftValue | rightValue | name | +|----|-----|-----|------| +| 1 | 1 | 14 | root | +| 2 | 2 | 9 | A | +| 3 | 10 | 11 | B | +| 4 | 12 | 13 | C | +| 5 | 3 | 4 | A-1 | +| 6 | 5 | 6 | A-2 | +| 7 | 7 | 8 | A-3 | + +
      +
    • + root +
        +
      • A +
          +
        • A-1
        • +
        • A-2
        • +
        • A-3
        • +
        +
      • +
      • B
      • +
      • C
      • + +
      + +# 快速开始 + + + +## 第1步:安装核心库 + +首先安装`flextree`核心库。 + +```ts +npm install flextree +// or +yarn add flextree +// or +pnpm add flextree +``` + +## 第2步:配置数据库适配器 + +接下来,取决于您的应用是如何访问数据库,你需要安装相应的数据库适配器。 + +本例中,我们使用`Sqlite`,安装数据库安装`flextree-sqlite-adapter`。 + +```ts +npm install flextree-sqlite-adapter +// or +yarn add flextree-sqlite-adapter +// or +pnpm add flextree-sqlite-adapter +``` + +`flextree-sqlite-adapter`是`flextree`的`sqlite3`数据库驱动,基于`sqlite3`数据库存储。 + + +如果你使用的是`MySQL`、`PostgreSQL`等数据库,可以安装对应的驱动,如`flextree-prima-adapter`,或者基于`flextree`提供的`IFlexTreeAdapter`自定义驱动。 + + +## 第3步:创建树表 + +接下来,我们需要在数据库中创建**组织架构树表`org`**。 + +如果你使用的是`sqlite`数据库,可以使用以下`sql`语句创建表: + +```ts +import SqliteAdapter from 'flextree-sqlite-adapter'; + +const sqliteAdapter = new SqliteAdapter("org.db") +await sqliteAdapter.open() +await sqliteAdapter.exec(` + CREATE TABLE IF NOT EXISTS org ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(60), + level INTEGER, + leftValue INTEGER, + rightValue INTEGER, +`) + +``` + +以上,我们他创建了一个`org`表,包含以下字段: + +| 字段名 | 类型 | 说明 | +| --- | --- | --- | +| `id` | INTEGER | 主键,自增 | +| `name` | VARCHAR(60) | 名称 | +| `level` | INTEGER | 层级 | +| `leftValue` | INTEGER | 左值 | +| `rightValue` | INTEGER | 右值 | + +一般情况下,以上字段是必须的,你可以根据实际情况添加其他字段。 + + +**提示**:一般情况下,创建表是由应用程序自行完成的,`flextree`不负责创建表。本节仅演示需要创建的树表结构。 + + +## 第4步:创建树管理器 + +接下来,我们创建一个组织架构树管理器`OrgManager`,用于管理组织架构树。 + +```ts {7-9} +import { FlexTreeManager } from 'flextree'; +import SqliteAdapter from 'flextree-sqlite-adapter'; + +const sqliteAdapter = new SqliteAdapter("org.db") +await sqliteAdapter.open() + +const orgManager = new FlexTreeManager("org",{ + adapter: sqliteAdapter +}) +``` + +## 第5步:添加树节点 + +然后我们就可以开始向组织架构树中添加节点了。 + +```ts +// 创建一个根节点 +await orgManager.createRoot({ + name: "A公司" +}) +// 添加组织架构的一级部门子节点 +await orgManager.addNodes([ + { name: "行政中心" }, + { name: "市场中心" }, + { name: "研发中心"} +]) + +// 添加行政中心的部门子节点. +const node = await orgManager.findNode({name:"行政中心"}) +await orgManager.addNodes( [ + { name: "总裁办" }, + { name: "人力资源部" }, + { name: "财务部" }, + { name: "行政部" }, + { name: "法务部" }, + { name: "审计部" } + ],node) // 添加为node的子节点 + +``` + +我们可以使用`addNodes`方法向树中添加节点,`addNodes`方法支持批量添加节点,支持多种形式的添加子节点。 + +## 第6步:访问树 + +以上我们已经创建了一棵完整的树,接下来我们可以通过两种形式来访问树。 + +- 通过`FlexTreeManager`访问树 +- 通过`FlexTree`对象访问树 + +### 获取节点 + +```ts +// 获取所有节点 +await orgManager.getNodes() +// 限定层级获取节点,仅获取第1-3层节点,不包含第4层及以下节点 +await orgManager.getNodes(3) +// 根据id获取节点 +await orgManager.getNode(1) +// 获取树根节点 +await orgManager.getRoot() + +// 获取name=行政中心的节点 +const node = await orgManager.findNode({name:"行政中心"}) +// 获取节点<行政中心>的子节点集 +await orgManager.getChildren(node) +// 获取节点<行政中心>的所有后代节点集 +await orgManager.getDescendants(node) +// 获取节点<行政中心>的所有后代节点集,包括自身 +await orgManager.getDescendants(node,{includeSelf:true}) +// 获取节点<行政中心>的所有后代节点集,包括限定层级 +await orgManager.getDescendants(node,{level:2}) +// 获取节点<行政中心>的子节点集,level=1相当于只获取直接子节点 +await orgManager.getDescendants(node,{level:1}) + +// 获取节点<行政中心>的所有祖先节点集 +await orgManager.getAncestors(node) +// 获取节点<行政中心>的父节点 +await orgManager.getParent(node) +// 获取节点<行政中心>的所有兄弟节点集 +await orgManager.getSiblings(node) +// 获取节点<行政中心>的所有兄弟节点集,包括自身 +await orgManager.getSiblings(node,{includeSelf:true}) +// 获取节点<行政中心>的前一个兄弟节点 +await orgManager.getNextSibling(node) +// 获取节点<行政中心>的后一个兄弟节点 +await orgManager.getPrevSibling(node) + +``` + +### 查找节点 + +```ts +// 查找name=行政中心的节点,只返回第一个满足条件的节点 +await orgManager.findNode({name:"行政中心"}) +// 查找所有level=1的节点集 +await orgManager.findNodes({level:1}) + +``` + +:::warning 提示 +`FlexTree`只提借供简单的查询功能,如果需要更复杂的查询,可以使用数据库的查询功能。 +::: + + +### 移动节点 + +```ts +import { FirstChild, LastChild,PreviousSibling,NextSibling } from 'flextree' +const admin = await orgManager.findNode({name:"行政中心"}) +const market = await orgManager.findNode({name:"市场中心"}) + +// 将行政中心移动到市场中心下,成为其最后一个子节点 +await orgManager.move(admin,market) +await orgManager.move(admin,market,LastChild) // 与上面等价 +// 将行政中心移动到市场中心下,成为其第一个子节点 +await orgManager.move(admin,market,FirstChild) +// 将行政中心移动到市场中心前,成为其前一个兄弟节点 +await orgManager.move(admin,market,PreviousSibling) +// 将行政中心移动到市场中心后,成为其后一个兄弟节点 +await orgManager.move(admin,market,NextSibling) + +// 将行政中心上移 +await orgManager.moveUpNode(admin) +// 将行政中心下移 +await orgManager.moveDownNode(admin) + +``` + +### 删除节点 + +```ts +const admin = await orgManager.findNode({name:"行政中心"}) +// 删除行政中心节点以及其所有后代节点 +await orgManager.deleteNode(admin) +// 清空树 +await orgManager.clear() +``` + + +### 查询节点关系 + +```ts + +const admin = await orgManager.findNode({name:"行政中心"}) +const market = await orgManager.findNode({name:"市场中心"}) + +// 返回admin节点与market节点的关系 +const relation = await getNodeRelation(admin,market) + +// relation取值范围 +export enum FlexTreeNodeRelation { + Self = 0, + Parent = 1, + Child = 2, + Siblings = 3, + Descendants = 4, + Ancestors = 5, + DiffTree = 6, + SameTree = 7, + SameLevel = 8, + Unknow = 9, +} + +``` + +# 推荐 + +- [全流程一健化React/Vue/Nodejs国际化方案 - VoerkaI18n](https://zhangfisher.github.io/voerka-i18n/) +- [无以伦比的React表单开发库 - speedform](https://zhangfisher.github.io/speed-form/) +- [终端界面开发增强库 - Logsets](https://zhangfisher.github.io/logsets/) +- [简单的日志输出库 - VoerkaLogger](https://zhangfisher.github.io/voerkalogger/) +- [装饰器开发 - FlexDecorators](https://zhangfisher.github.io/flex-decorators/) +- [有限状态机库 - FlexState](https://zhangfisher.github.io/flexstate/) +- [通用函数工具库 - FlexTools](https://zhangfisher.github.io/flex-tools/) +- [小巧优雅的CSS-IN-JS库 - Styledfc](https://zhangfisher.github.io/styledfc/) +- [为JSON文件添加注释的VSCODE插件 - json_comments_extension](https://github.com/zhangfisher/json_comments_extension) +- [开发交互式命令行程序库 - mixed-cli](https://github.com/zhangfisher/mixed-cli) +- [强大的字符串插值变量处理工具库 - flexvars](https://github.com/zhangfisher/flexvars) +- [前端link调试辅助工具 - yald](https://github.com/zhangfisher/yald) +- [异步信号 - asyncsignal](https://github.com/zhangfisher/asyncsignal) +- [捆绑Vue组件CSS到JS的插件 - vite-plugin-vue-style-bundler ](https://github.com/zhangfisher/vite-plugin-vue-style-bundler) +- [轻量树组件 - LiteTree](https://github.com/zhangfisher/lite-tree)