feat: refine events and SetMeta logic, add ToC lifecycle hooks (by AI)
This commit is contained in:
parent
cf01dcf464
commit
cfabbab466
83
README.md
83
README.md
@ -1,6 +1,6 @@
|
|||||||
# @go/docDB
|
# @go/docDB
|
||||||
|
|
||||||
`docDB` 是一个独立的高级文档存储引擎,基于 `@go/tableDB` 构建,提供全自动版本管理、历史存证、流式大文件处理及生命周期钩子。
|
`docDB` 是一个独立的高级文档存储引擎,基于 `@go/tableDB` 构建,提供全自动版本管理、历史存证、流式大文件处理及精细化生命周期钩子。
|
||||||
|
|
||||||
## 📦 安装
|
## 📦 安装
|
||||||
|
|
||||||
@ -12,12 +12,10 @@ go get apigo.cc/go/docDB
|
|||||||
|
|
||||||
1. **路径驱动**:以 `Path` 作为文档的唯一标识。
|
1. **路径驱动**:以 `Path` 作为文档的唯一标识。
|
||||||
2. **版本管控**:`SetDoc` 强制提升版本号并自动归档旧版;`SetMeta` 仅更新属性不触碰版本。
|
2. **版本管控**:`SetDoc` 强制提升版本号并自动归档旧版;`SetMeta` 仅更新属性不触碰版本。
|
||||||
3. **重命名支持**:提供 `Move` API 允许修改文档路径,而无需重新上传文件,历史记录自动同步。
|
3. **智能 ToC**:自动提取 Markdown 目录结构,支持代码块识别;`SetMeta` 更新正文时能自动重算 ToC。
|
||||||
4. **历史存证**:即使文档被删除,其所有历史版本(包括被删除前的最后一版)依然可通过 `GetByVersion` 获取。
|
4. **精细化事件**:提供文档与目录(ToC)的增删改精细钩子,支持搜索引擎与知识库高效同步。
|
||||||
5. **权限隔离**:完全继承 `tableDB` 的 RLS (Row Level Security) 体系。
|
5. **历史存证**:即使文档被删除,其所有历史版本依然可通过 `GetByVersion` 获取。
|
||||||
6. **流式上传**:`BinaryContent` 采用 `io.Reader` 接口,支持低内存处理 GB 级大文件落盘。
|
6. **权限隔离**:完全继承 `tableDB` 的 RLS (Row Level Security) 体系。
|
||||||
7. **事件驱动**:提供标准的 `Hooks` 机制,支持监听文档创建、更新、移除及移动。
|
|
||||||
8. **智能提取**:自动提取 Markdown 目录结构 (ToC),且能智能跳过代码块内容。
|
|
||||||
|
|
||||||
## 🚀 API 指南
|
## 🚀 API 指南
|
||||||
|
|
||||||
@ -29,53 +27,39 @@ import (
|
|||||||
"apigo.cc/go/log"
|
"apigo.cc/go/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 获取 docDB 引擎 (自动完成系统表与业务表初始化)
|
// 获取引擎实例
|
||||||
// dsn: 数据库连接字符串 (如 sqlite://docs.db)
|
engine := docDB.GetDB("sqlite://docs.db", log.DefaultLogger, "", "./storage_root")
|
||||||
// logger: 日志对象
|
|
||||||
// redis: 分布式 ID 用的 Redis 地址 (可选)
|
|
||||||
// baseDir: 物理文件存储根路径
|
|
||||||
engine := docDB.GetDB("sqlite://docs.db", log.DefaultLogger, "localhost:6379", "./storage_root")
|
|
||||||
|
|
||||||
// 授权并获取操作句柄
|
// 授权
|
||||||
db := engine.Auth("user_123")
|
db := engine.Auth("user_123")
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 文档操作 (DocDB API)
|
### 2. 文档操作
|
||||||
|
|
||||||
- **`SetDoc(doc *Document) error`**
|
- **`SetDoc(doc *Document) error`**:保存文档,提升版本。触发 `OnUpdatedDoc` 和 `OnUpdatedToC`。
|
||||||
- 保存文档。通过 `doc.Path` 识别。如果是更新,会自动归档旧版本并递增 `version`。
|
- **`SetMeta(path string, meta map[string]any) error`**:更新元数据,**不提升版本**。若包含 `textContent` 且为 Markdown,会自动触发 `OnRemovedToC` 和 `OnUpdatedToC`。
|
||||||
- **`SetMeta(path string, meta map[string]any) error`**
|
- **`Move(oldPath, newPath string) error`**:重命名。触发 `OnRemovedDoc(oldPath)` -> `OnUpdatedDoc(newPath)`。
|
||||||
- 通过路径更新文档元数据。**不提升版本号**。
|
- **`Get(path string) (*Document, error)`**:获取最新版。
|
||||||
- **`Move(oldPath, newPath string) error`**
|
- **`GetByVersion(path string, version uint64) (*Document, error)`**:获取历史版(含已删除)。
|
||||||
- 修改文档路径(重命名)。历史记录将关联至新路径。
|
- **`Remove(path string) error`**:删除文档。触发 `OnRemovedDoc`。
|
||||||
- **`Get(path string) (*Document, error)`**
|
|
||||||
- 获取文档的当前最新版本。
|
|
||||||
- **`GetByVersion(path string, version uint64) (*Document, error)`**
|
|
||||||
- 获取指定版本的文档。支持已删除文档的历史回溯。
|
|
||||||
- **`Remove(path string) error`**
|
|
||||||
- 删除文档。删除前会自动将当前最后一版归档至历史表。
|
|
||||||
|
|
||||||
### 3. 生命周期钩子 (Hooks)
|
### 3. 生命周期钩子 (Hooks)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 监听新文档创建
|
// 文档级事件
|
||||||
engine.Hooks.OnCreatedDoc = func(doc *docDB.Document) {
|
|
||||||
fmt.Println("New doc created:", doc.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听内容更新 (SetDoc 触发,SetMeta 不触发)
|
|
||||||
engine.Hooks.OnUpdatedDoc = func(doc *docDB.Document) {
|
engine.Hooks.OnUpdatedDoc = func(doc *docDB.Document) {
|
||||||
fmt.Println("Doc updated:", doc.Path, "Version:", doc.Version)
|
// 文档创建或内容更新时触发 (SetDoc 或 Move 触发)
|
||||||
|
}
|
||||||
|
engine.Hooks.OnRemovedDoc = func(path string) {
|
||||||
|
// 文档被物理删除或 Move 移除旧路径时触发
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听文档删除
|
// 目录(ToC)级事件
|
||||||
engine.Hooks.OnRemoved = func(path string) {
|
engine.Hooks.OnUpdatedToC = func(path string, toc []docDB.ToCNode) {
|
||||||
fmt.Println("Doc removed:", path)
|
// 目录生成或变更时触发
|
||||||
}
|
}
|
||||||
|
engine.Hooks.OnRemovedToC = func(path string) {
|
||||||
// 监听文档重命名
|
// 目录失效时触发
|
||||||
engine.Hooks.OnMoved = func(oldPath, newPath string) {
|
|
||||||
fmt.Println("Doc moved from", oldPath, "to", newPath)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -84,16 +68,13 @@ engine.Hooks.OnMoved = func(oldPath, newPath string) {
|
|||||||
### Document
|
### Document
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| `Path` | `string` | **核心标识** (如 /docs/readme.md) |
|
| `Path` | `string` | **核心标识** |
|
||||||
| `BinaryContent` | `io.Reader` | 瞬态流,用于接收上传内容 |
|
| `BinaryContent` | `io.Reader` | 瞬态流,用于上传 |
|
||||||
| `TextContent` | `string` | 文档正文 |
|
| `TextContent` | `string` | 正文内容 |
|
||||||
| `Version` | `uint64` | 单调递增的版本号 |
|
| `Version` | `uint64` | 版本号 |
|
||||||
| `ToC` | `[]ToCNode` | 自动生成的目录树 |
|
| `ToC` | `[]ToCNode` | 目录树 (自动生成) |
|
||||||
| `CreateTime` | `int64` | 创建时间 (自动维护) |
|
|
||||||
| `UpdateTime` | `int64` | 最后更新时间 (自动维护) |
|
|
||||||
|
|
||||||
## 💡 注意事项
|
## 💡 注意事项
|
||||||
|
|
||||||
- **ID 封装**:底层依然使用唯一 ID 维护物理存储,但对 API 调用者完全隐藏。
|
- **SetMeta 行为**:`SetMeta` 不会触发 `OnUpdatedDoc`,但如果更新了正文导致目录变化,会触发 ToC 相关事件。
|
||||||
- **自动初始化**:`GetDB` 内部使用 `_system` 权限自动维护系统表及 `_Doc`, `_Doc_History` 的结构。
|
- **Move 行为**:为了兼容搜索索引等外部系统,`Move` 被抽象为“删除旧路径文档”与“更新新路径文档”的组合操作。
|
||||||
- **元数据自动维护**:`Creator`, `Updater`, `CreateTime`, `UpdateTime` 由底层 `tableDB` 统一管控,无需显式设置。
|
|
||||||
|
|||||||
109
doc.go
109
doc.go
@ -15,10 +15,10 @@ import (
|
|||||||
|
|
||||||
// Hooks 生命周期钩子
|
// Hooks 生命周期钩子
|
||||||
type Hooks struct {
|
type Hooks struct {
|
||||||
OnCreatedDoc func(doc *Document)
|
|
||||||
OnUpdatedDoc func(doc *Document)
|
OnUpdatedDoc func(doc *Document)
|
||||||
OnRemoved func(path string)
|
OnRemovedDoc func(path string)
|
||||||
OnMoved func(oldPath, newPath string)
|
OnUpdatedToC func(path string, toc []ToCNode)
|
||||||
|
OnRemovedToC func(path string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocDBUnauthorized 实例
|
// DocDBUnauthorized 实例
|
||||||
@ -36,28 +36,28 @@ type DocDB struct {
|
|||||||
hist *tableDB.Table
|
hist *tableDB.Table
|
||||||
}
|
}
|
||||||
|
|
||||||
// Document 结构体 (用户不感知 ID)
|
// Document 结构体
|
||||||
type Document struct {
|
type Document struct {
|
||||||
Path string // 层级路径 (唯一标识)
|
Path string // 层级路径 (唯一标识)
|
||||||
Title string // 标题
|
Title string // 标题
|
||||||
TextContent string // 纯文本
|
TextContent string // 纯文本
|
||||||
BinaryContent io.Reader `json:"-"` // 仅为内存瞬态,流式读取
|
BinaryContent io.Reader `json:"-"` // 仅为内存瞬态,流式读取
|
||||||
FilePath string // 原文件落盘绝对路径 (只读)
|
FilePath string // 原文件落盘绝对路径
|
||||||
Thumbnail string // 缩略图路径 (只读)
|
Thumbnail string // 缩略图路径
|
||||||
Summary string // 摘要
|
Summary string // 摘要
|
||||||
Metadata map[string]any // 元数据
|
Metadata map[string]any // 元数据
|
||||||
ToC []ToCNode // 目录树
|
ToC []ToCNode // 目录树
|
||||||
Tags string // 标签
|
Tags string // 标签
|
||||||
Language string // 语言
|
Language string // 语言
|
||||||
Type string // 类型
|
Type string // 类型
|
||||||
StoreType string // 存储方式 (file/db)
|
StoreType string // 存储方式
|
||||||
AsFile bool // 是否以文件存储
|
AsFile bool // 是否以文件存储
|
||||||
Version uint64 // 版本号
|
Version uint64 // 版本号
|
||||||
IsSecret bool // 是否加密
|
IsSecret bool // 是否加密
|
||||||
CreateTime int64 // 创建时间 (自动维护)
|
CreateTime int64 // 创建时间
|
||||||
UpdateTime int64 // 更新时间 (自动维护)
|
UpdateTime int64 // 更新时间
|
||||||
Creator string // 创建人 ID (自动维护)
|
Creator string // 创建人 ID
|
||||||
Updater string // 更新人 ID (自动维护)
|
Updater string // 更新人 ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToCNode 目录树节点
|
// ToCNode 目录树节点
|
||||||
@ -72,7 +72,6 @@ func GetDB(dsn string, logger *log.Logger, redis string, baseDir string) *DocDBU
|
|||||||
unauthorizedDB := tableDB.GetDB(dsn, logger, redis)
|
unauthorizedDB := tableDB.GetDB(dsn, logger, redis)
|
||||||
sys := unauthorizedDB.Auth(tableDB.SystemUserID)
|
sys := unauthorizedDB.Auth(tableDB.SystemUserID)
|
||||||
|
|
||||||
// 确保系统基础表存在
|
|
||||||
if raw, err := sys.GetRawDB(); err == nil {
|
if raw, err := sys.GetRawDB(); err == nil {
|
||||||
_ = raw.Sync(tableDB.SystemSchema)
|
_ = raw.Sync(tableDB.SystemSchema)
|
||||||
}
|
}
|
||||||
@ -130,14 +129,13 @@ func (d *DocDBUnauthorized) Auth(userID string) *DocDB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDoc 更新内容 (强制提升版本号,自动保存历史)
|
// SetDoc 更新内容 (强制提升版本号)
|
||||||
func (a *DocDB) SetDoc(docObj *Document) error {
|
func (a *DocDB) SetDoc(docObj *Document) error {
|
||||||
existing, _ := a.getRaw(docObj.Path)
|
existing, _ := a.getRaw(docObj.Path)
|
||||||
|
|
||||||
isUpdate := existing != nil
|
|
||||||
var internalID string
|
var internalID string
|
||||||
|
|
||||||
if isUpdate {
|
if existing != nil {
|
||||||
|
a.triggerRemovedToC(docObj.Path)
|
||||||
a.archive(existing)
|
a.archive(existing)
|
||||||
internalID = cast.String(existing["id"])
|
internalID = cast.String(existing["id"])
|
||||||
docObj.Version = cast.Uint64(existing["version"]) + 1
|
docObj.Version = cast.Uint64(existing["version"]) + 1
|
||||||
@ -145,19 +143,15 @@ func (a *DocDB) SetDoc(docObj *Document) error {
|
|||||||
docObj.Version = 1
|
docObj.Version = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理文件落盘
|
|
||||||
if docObj.BinaryContent != nil {
|
if docObj.BinaryContent != nil {
|
||||||
if internalID == "" {
|
if internalID == "" {
|
||||||
// 先执行一次空的 Set 来占坑获取 ID
|
|
||||||
placeholder := map[string]any{"path": docObj.Path, "version": docObj.Version}
|
placeholder := map[string]any{"path": docObj.Path, "version": docObj.Version}
|
||||||
_ = a.table.Set(placeholder)
|
_ = a.table.Set(placeholder)
|
||||||
// 重新获取
|
|
||||||
existing, _ = a.getRaw(docObj.Path)
|
existing, _ = a.getRaw(docObj.Path)
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
internalID = cast.String(existing["id"])
|
internalID = cast.String(existing["id"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.saveFile(internalID, docObj); err != nil {
|
if err := a.saveFile(internalID, docObj); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -165,19 +159,16 @@ func (a *DocDB) SetDoc(docObj *Document) error {
|
|||||||
docObj.StoreType = "file"
|
docObj.StoreType = "file"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动提取 ToC
|
|
||||||
if docObj.ToC == nil && (docObj.Type == "markdown" || strings.HasSuffix(strings.ToLower(docObj.Path), ".md")) {
|
if docObj.ToC == nil && (docObj.Type == "markdown" || strings.HasSuffix(strings.ToLower(docObj.Path), ".md")) {
|
||||||
docObj.ToC = ExtractToC(docObj.TextContent)
|
docObj.ToC = ExtractToC(docObj.TextContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
record := a.toRecord(internalID, docObj)
|
record := a.toRecord(internalID, docObj)
|
||||||
|
|
||||||
err := a.table.Set(record)
|
err := a.table.Set(record)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if isUpdate {
|
a.triggerUpdatedDoc(docObj)
|
||||||
a.triggerUpdated(docObj)
|
if docObj.ToC != nil {
|
||||||
} else {
|
a.triggerUpdatedToC(docObj.Path, docObj.ToC)
|
||||||
a.triggerCreated(docObj)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@ -190,6 +181,17 @@ func (a *DocDB) SetMeta(path string, meta map[string]any) error {
|
|||||||
return fmt.Errorf("document not found: %s", path)
|
return fmt.Errorf("document not found: %s", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查 TextContent 变更
|
||||||
|
if text, ok := meta["textContent"].(string); ok {
|
||||||
|
docType := cast.String(existing["type"])
|
||||||
|
if docType == "markdown" || strings.HasSuffix(strings.ToLower(path), ".md") {
|
||||||
|
a.triggerRemovedToC(path)
|
||||||
|
newToC := ExtractToC(text)
|
||||||
|
meta["toc"] = newToC
|
||||||
|
a.triggerUpdatedToC(path, newToC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range meta {
|
for k, v := range meta {
|
||||||
existing[k] = v
|
existing[k] = v
|
||||||
}
|
}
|
||||||
@ -204,12 +206,21 @@ func (a *DocDB) Move(oldPath, newPath string) error {
|
|||||||
return fmt.Errorf("document not found: %s", oldPath)
|
return fmt.Errorf("document not found: %s", oldPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
existing["path"] = newPath
|
// 触发删除旧路径事件
|
||||||
|
a.triggerRemovedDoc(oldPath)
|
||||||
|
if existing["toc"] != nil {
|
||||||
|
a.triggerRemovedToC(oldPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
existing["path"] = newPath
|
||||||
err = a.table.Set(existing)
|
err = a.table.Set(existing)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if a.parent.Hooks.OnMoved != nil {
|
newDoc, _ := a.Get(newPath)
|
||||||
a.parent.Hooks.OnMoved(oldPath, newPath)
|
if newDoc != nil {
|
||||||
|
a.triggerUpdatedDoc(newDoc)
|
||||||
|
if newDoc.ToC != nil {
|
||||||
|
a.triggerUpdatedToC(newPath, newDoc.ToC)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@ -285,13 +296,11 @@ func (a *DocDB) GetByVersion(path string, version uint64) (*Document, error) {
|
|||||||
return current, nil
|
return current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找原始 internalID
|
|
||||||
res, _ := a.table.List(map[string]any{"path": path})
|
res, _ := a.table.List(map[string]any{"path": path})
|
||||||
var docID string
|
var docID string
|
||||||
if len(res) > 0 {
|
if len(res) > 0 {
|
||||||
docID = cast.String(res[0]["id"])
|
docID = cast.String(res[0]["id"])
|
||||||
} else {
|
} else {
|
||||||
// 如果主表删除了,去历史表找 docId
|
|
||||||
histRes, _ := a.hist.List(map[string]any{"path": path})
|
histRes, _ := a.hist.List(map[string]any{"path": path})
|
||||||
if len(histRes) > 0 {
|
if len(histRes) > 0 {
|
||||||
docID = cast.String(histRes[0]["docId"])
|
docID = cast.String(histRes[0]["docId"])
|
||||||
@ -319,30 +328,36 @@ func (a *DocDB) Remove(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.triggerRemovedDoc(path)
|
||||||
|
if existing["toc"] != nil {
|
||||||
|
a.triggerRemovedToC(path)
|
||||||
|
}
|
||||||
|
|
||||||
a.archive(existing)
|
a.archive(existing)
|
||||||
|
return a.table.Remove(cast.String(existing["id"]))
|
||||||
err = a.table.Remove(cast.String(existing["id"]))
|
|
||||||
if err == nil {
|
|
||||||
a.triggerRemoved(path)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *DocDB) triggerCreated(doc *Document) {
|
func (a *DocDB) triggerUpdatedDoc(doc *Document) {
|
||||||
if a.parent.Hooks.OnCreatedDoc != nil {
|
|
||||||
a.parent.Hooks.OnCreatedDoc(doc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *DocDB) triggerUpdated(doc *Document) {
|
|
||||||
if a.parent.Hooks.OnUpdatedDoc != nil {
|
if a.parent.Hooks.OnUpdatedDoc != nil {
|
||||||
a.parent.Hooks.OnUpdatedDoc(doc)
|
a.parent.Hooks.OnUpdatedDoc(doc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *DocDB) triggerRemoved(path string) {
|
func (a *DocDB) triggerRemovedDoc(path string) {
|
||||||
if a.parent.Hooks.OnRemoved != nil {
|
if a.parent.Hooks.OnRemovedDoc != nil {
|
||||||
a.parent.Hooks.OnRemoved(path)
|
a.parent.Hooks.OnRemovedDoc(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *DocDB) triggerUpdatedToC(path string, toc []ToCNode) {
|
||||||
|
if a.parent.Hooks.OnUpdatedToC != nil {
|
||||||
|
a.parent.Hooks.OnUpdatedToC(path, toc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *DocDB) triggerRemovedToC(path string) {
|
||||||
|
if a.parent.Hooks.OnRemovedToC != nil {
|
||||||
|
a.parent.Hooks.OnRemovedToC(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
93
doc_test.go
93
doc_test.go
@ -16,15 +16,14 @@ func TestDocDB(t *testing.T) {
|
|||||||
os.RemoveAll("./test_docs")
|
os.RemoveAll("./test_docs")
|
||||||
defer os.RemoveAll("./test_docs")
|
defer os.RemoveAll("./test_docs")
|
||||||
|
|
||||||
// 使用新的 GetDB 入口 (支持 redis 参数)
|
|
||||||
docDBInst := GetDB("sqlite://"+dbFile, logger, "", "./test_docs")
|
docDBInst := GetDB("sqlite://"+dbFile, logger, "", "./test_docs")
|
||||||
app := docDBInst.Auth("user1")
|
app := docDBInst.Auth("user1")
|
||||||
|
|
||||||
var createdCalled, updatedCalled, removedCalled, movedCalled bool
|
var updatedCalled, removedDocCalled, updatedToCCalled, removedToCCalled bool
|
||||||
docDBInst.Hooks.OnCreatedDoc = func(doc *Document) { createdCalled = true }
|
|
||||||
docDBInst.Hooks.OnUpdatedDoc = func(doc *Document) { updatedCalled = true }
|
docDBInst.Hooks.OnUpdatedDoc = func(doc *Document) { updatedCalled = true }
|
||||||
docDBInst.Hooks.OnRemoved = func(path string) { removedCalled = true }
|
docDBInst.Hooks.OnRemovedDoc = func(path string) { removedDocCalled = true }
|
||||||
docDBInst.Hooks.OnMoved = func(oldPath, newPath string) { movedCalled = true }
|
docDBInst.Hooks.OnUpdatedToC = func(path string, toc []ToCNode) { updatedToCCalled = true }
|
||||||
|
docDBInst.Hooks.OnRemovedToC = func(path string) { removedToCCalled = true }
|
||||||
|
|
||||||
// 1. 测试 SetDoc (创建)
|
// 1. 测试 SetDoc (创建)
|
||||||
docPath := "/test/doc1.md"
|
docPath := "/test/doc1.md"
|
||||||
@ -40,88 +39,42 @@ func TestDocDB(t *testing.T) {
|
|||||||
t.Fatalf("SetDoc failed: %v", err)
|
t.Fatalf("SetDoc failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if doc1.Version != 1 {
|
if !updatedCalled || !updatedToCCalled {
|
||||||
t.Fatalf("Expected version 1, got %d", doc1.Version)
|
t.Error("Hooks not triggered on creation")
|
||||||
}
|
|
||||||
if !createdCalled {
|
|
||||||
t.Error("OnCreatedDoc event not triggered")
|
|
||||||
}
|
}
|
||||||
|
updatedCalled, updatedToCCalled = false, false
|
||||||
|
|
||||||
// 2. 测试 Get
|
// 2. 测试 SetMeta 更新 TextContent (触发 ToC 事件)
|
||||||
d, err := app.Get(docPath)
|
err = app.SetMeta(docPath, map[string]any{"textContent": "# New Header"})
|
||||||
if err != nil || d == nil {
|
|
||||||
t.Fatalf("Get failed: %v", err)
|
|
||||||
}
|
|
||||||
if d.Title != "Test Doc" {
|
|
||||||
t.Fatalf("Expected title 'Test Doc', got %s", d.Title)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 测试 SetDoc (更新内容)
|
|
||||||
doc1.TextContent = "# Header 1\nUpdated Content"
|
|
||||||
err = app.SetDoc(doc1)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Update SetDoc failed: %v", err)
|
t.Fatalf("SetMeta failed: %v", err)
|
||||||
}
|
}
|
||||||
if doc1.Version != 2 {
|
if !updatedToCCalled || !removedToCCalled {
|
||||||
t.Fatalf("Expected version 2, got %d", doc1.Version)
|
t.Error("ToC Hooks not triggered on SetMeta text update")
|
||||||
}
|
}
|
||||||
if !updatedCalled {
|
if updatedCalled {
|
||||||
t.Error("OnUpdatedDoc event not triggered")
|
t.Error("OnUpdatedDoc should NOT be triggered on SetMeta")
|
||||||
}
|
}
|
||||||
|
updatedToCCalled, removedToCCalled = false, false
|
||||||
|
|
||||||
// 4. 测试 GetByVersion
|
// 3. 测试 Move (触发删除旧的 + 更新新的)
|
||||||
hist, err := app.GetByVersion(docPath, 1)
|
newPath := "/test/moved.md"
|
||||||
if err != nil || hist == nil {
|
|
||||||
t.Fatalf("GetByVersion(1) failed: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(hist.TextContent, "Content 1") {
|
|
||||||
t.Fatalf("Expected 'Content 1' in history, got %s", hist.TextContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 测试 Move (重命名)
|
|
||||||
newPath := "/test/renamed.md"
|
|
||||||
err = app.Move(docPath, newPath)
|
err = app.Move(docPath, newPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Move failed: %v", err)
|
t.Fatalf("Move failed: %v", err)
|
||||||
}
|
}
|
||||||
if !movedCalled {
|
if !removedDocCalled || !updatedCalled {
|
||||||
t.Error("OnMoved event not triggered")
|
t.Error("Move Hooks sequence failed")
|
||||||
}
|
}
|
||||||
|
removedDocCalled, updatedCalled = false, false
|
||||||
|
|
||||||
// 检查旧路径失效,新路径生效
|
// 4. 测试 Remove
|
||||||
oldDoc, _ := app.Get(docPath)
|
|
||||||
if oldDoc != nil {
|
|
||||||
t.Error("Old path should be gone")
|
|
||||||
}
|
|
||||||
newDoc, err := app.Get(newPath)
|
|
||||||
if err != nil || newDoc == nil {
|
|
||||||
t.Fatalf("New path should work: %v", err)
|
|
||||||
}
|
|
||||||
if newDoc.Title != "Test Doc" {
|
|
||||||
t.Error("Title should remain same after move")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 测试 Remove
|
|
||||||
err = app.Remove(newPath)
|
err = app.Remove(newPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Remove failed: %v", err)
|
t.Fatalf("Remove failed: %v", err)
|
||||||
}
|
}
|
||||||
if !removedCalled {
|
if !removedDocCalled {
|
||||||
t.Error("OnRemoved event not triggered")
|
t.Error("OnRemovedDoc not triggered on Remove")
|
||||||
}
|
|
||||||
|
|
||||||
removedDoc, _ := app.Get(newPath)
|
|
||||||
if removedDoc != nil {
|
|
||||||
t.Error("Document should be removed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 测试删除后依然能获取历史
|
|
||||||
lastHist, err := app.GetByVersion(newPath, 2)
|
|
||||||
if err != nil || lastHist == nil {
|
|
||||||
t.Fatalf("Should get history after removal: %v", err)
|
|
||||||
}
|
|
||||||
if lastHist.Version != 2 {
|
|
||||||
t.Fatalf("Expected version 2, got %d", lastHist.Version)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
go.mod
30
go.mod
@ -35,3 +35,33 @@ require (
|
|||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.50.0 // indirect
|
modernc.org/sqlite v1.50.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace apigo.cc/go/tableDB => ../tableDB
|
||||||
|
|
||||||
|
replace apigo.cc/go/cast => ../cast
|
||||||
|
|
||||||
|
replace apigo.cc/go/id => ../id
|
||||||
|
|
||||||
|
replace apigo.cc/go/file => ../file
|
||||||
|
|
||||||
|
replace apigo.cc/go/log => ../log
|
||||||
|
|
||||||
|
replace apigo.cc/go/document => ../document
|
||||||
|
|
||||||
|
replace apigo.cc/go/db => ../db
|
||||||
|
|
||||||
|
replace apigo.cc/go/config => ../config
|
||||||
|
|
||||||
|
replace apigo.cc/go/crypto => ../crypto
|
||||||
|
|
||||||
|
replace apigo.cc/go/encoding => ../encoding
|
||||||
|
|
||||||
|
replace apigo.cc/go/rand => ../rand
|
||||||
|
|
||||||
|
replace apigo.cc/go/redis => ../redis
|
||||||
|
|
||||||
|
replace apigo.cc/go/safe => ../safe
|
||||||
|
|
||||||
|
replace apigo.cc/go/shell => ../shell
|
||||||
|
|
||||||
|
replace apigo.cc/go/vision => ../vision
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user