feat: refactor to path-based API, add Move support, and encapsulate internal ID (by AI)
This commit is contained in:
parent
8499f40e48
commit
081a362b97
54
README.md
54
README.md
@ -10,12 +10,14 @@ go get apigo.cc/go/docDB
|
|||||||
|
|
||||||
## 🛠 核心功能
|
## 🛠 核心功能
|
||||||
|
|
||||||
1. **版本管控**:`SetDoc` 强制提升版本号并自动归档旧版;`SetMeta` 仅更新属性不触碰版本。
|
1. **路径驱动**:以 `Path` 作为文档的唯一标识。
|
||||||
2. **历史存证**:即使文档被删除,其所有历史版本(包括被删除前的最后一版)依然可通过 `GetByVersion` 获取。
|
2. **版本管控**:`SetDoc` 强制提升版本号并自动归档旧版;`SetMeta` 仅更新属性不触碰版本。
|
||||||
3. **权限隔离**:完全继承 `tableDB` 的 RLS (Row Level Security) 体系。
|
3. **重命名支持**:提供 `Move` API 允许修改文档路径,而无需重新上传文件,历史记录自动同步。
|
||||||
4. **流式上传**:`BinaryContent` 采用 `io.Reader` 接口,支持低内存处理 GB 级大文件落盘。
|
4. **历史存证**:即使文档被删除,其所有历史版本(包括被删除前的最后一版)依然可通过 `GetByVersion` 获取。
|
||||||
5. **事件驱动**:提供标准的 `Hooks` 机制,支持监听文档创建、更新与移除。
|
5. **权限隔离**:完全继承 `tableDB` 的 RLS (Row Level Security) 体系。
|
||||||
6. **智能提取**:自动提取 Markdown 目录结构 (ToC),且能智能跳过代码块内容。
|
6. **流式上传**:`BinaryContent` 采用 `io.Reader` 接口,支持低内存处理 GB 级大文件落盘。
|
||||||
|
7. **事件驱动**:提供标准的 `Hooks` 机制,支持监听文档创建、更新、移除及移动。
|
||||||
|
8. **智能提取**:自动提取 Markdown 目录结构 (ToC),且能智能跳过代码块内容。
|
||||||
|
|
||||||
## 🚀 API 指南
|
## 🚀 API 指南
|
||||||
|
|
||||||
@ -40,30 +42,25 @@ db := engine.Auth("user_123")
|
|||||||
### 2. 文档操作 (DocDB API)
|
### 2. 文档操作 (DocDB API)
|
||||||
|
|
||||||
- **`SetDoc(doc *Document) error`**
|
- **`SetDoc(doc *Document) error`**
|
||||||
- 保存文档。如果是更新,会自动将旧版本移入 `_Doc_History`,并递增 `version`。
|
- 保存文档。通过 `doc.Path` 识别。如果是更新,会自动归档旧版本并递增 `version`。
|
||||||
- **`SetMeta(id string, meta map[string]any) error`**
|
- **`SetMeta(path string, meta map[string]any) error`**
|
||||||
- 更新文档元数据(如 Title, Tags)。**不提升版本号**。
|
- 通过路径更新文档元数据。**不提升版本号**。
|
||||||
- **`Get(id string) (*Document, error)`**
|
- **`Move(oldPath, newPath string) error`**
|
||||||
|
- 修改文档路径(重命名)。历史记录将关联至新路径。
|
||||||
|
- **`Get(path string) (*Document, error)`**
|
||||||
- 获取文档的当前最新版本。
|
- 获取文档的当前最新版本。
|
||||||
- **`GetByVersion(id string, version uint64) (*Document, error)`**
|
- **`GetByVersion(path string, version uint64) (*Document, error)`**
|
||||||
- 获取指定版本的文档。即使当前文档已被 `Remove`,历史版本依然可查。
|
- 获取指定版本的文档。支持已删除文档的历史回溯。
|
||||||
- **`Remove(id string) error`**
|
- **`Remove(path string) error`**
|
||||||
- 删除文档。删除前会自动将当前最后一版归档至历史表。
|
- 删除文档。删除前会自动将当前最后一版归档至历史表。
|
||||||
|
|
||||||
### 3. 生命周期钩子 (Hooks)
|
### 3. 生命周期钩子 (Hooks)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
engine.Hooks.OnCreatedDoc = func(doc *docDB.Document) {
|
engine.Hooks.OnCreatedDoc = func(doc *docDB.Document) { /* ... */ }
|
||||||
// 处理新文档创建,如提交搜索索引
|
engine.Hooks.OnUpdatedDoc = func(doc *docDB.Document) { /* ... */ }
|
||||||
}
|
engine.Hooks.OnRemoved = func(path string) { /* ... */ }
|
||||||
|
engine.Hooks.OnMoved = func(oldPath, newPath string) { /* ... */ }
|
||||||
engine.Hooks.OnUpdatedDoc = func(doc *docDB.Document) {
|
|
||||||
// 处理内容更新
|
|
||||||
}
|
|
||||||
|
|
||||||
engine.Hooks.OnRemoved = func(id string) {
|
|
||||||
// 处理物理删除后的清理工作
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📝 数据结构
|
## 📝 数据结构
|
||||||
@ -71,14 +68,13 @@ engine.Hooks.OnRemoved = func(id string) {
|
|||||||
### Document
|
### Document
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| `ID` | `string` | 唯一标识 (10位随机码) |
|
| `Path` | `string` | **核心标识** (如 /docs/readme.md) |
|
||||||
| `Path` | `string` | 逻辑路径 (如 /docs/readme.md) |
|
|
||||||
| `BinaryContent` | `io.Reader` | 瞬态流,用于接收上传内容 |
|
| `BinaryContent` | `io.Reader` | 瞬态流,用于接收上传内容 |
|
||||||
| `TextContent` | `string` | 文档正文 (Markdown/Plain) |
|
| `TextContent` | `string` | 文档正文 |
|
||||||
| `Version` | `uint64` | 单调递增的版本号 |
|
| `Version` | `uint64` | 单调递增的版本号 |
|
||||||
| `ToC` | `[]ToCNode` | 自动生成的目录树 |
|
| `ToC` | `[]ToCNode` | 自动生成的目录树 |
|
||||||
|
|
||||||
## 💡 注意事项
|
## 💡 注意事项
|
||||||
|
|
||||||
- **路径对齐**:所有物理文件存储在 `baseDir/doc/` 下,按 ID 前缀进行三级目录隔离。
|
- **ID 封装**:底层依然使用唯一 ID 维护物理存储,但对 API 调用者完全隐藏。
|
||||||
- **自动初始化**:`GetDB` 内部使用 `_system` 权限自动维护 `_Doc` 和 `_Doc_History` 的表结构。
|
- **自动初始化**:`GetDB` 会自动维护 `_Doc` 和 `_Doc_History` 的表结构。
|
||||||
|
|||||||
137
doc.go
137
doc.go
@ -18,7 +18,8 @@ import (
|
|||||||
type Hooks struct {
|
type Hooks struct {
|
||||||
OnCreatedDoc func(doc *Document)
|
OnCreatedDoc func(doc *Document)
|
||||||
OnUpdatedDoc func(doc *Document)
|
OnUpdatedDoc func(doc *Document)
|
||||||
OnRemoved func(id string)
|
OnRemoved func(path string)
|
||||||
|
OnMoved func(oldPath, newPath string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocDBUnauthorized 实例
|
// DocDBUnauthorized 实例
|
||||||
@ -36,15 +37,14 @@ type DocDB struct {
|
|||||||
hist *tableDB.Table
|
hist *tableDB.Table
|
||||||
}
|
}
|
||||||
|
|
||||||
// Document 结构体
|
// Document 结构体 (用户不感知 ID)
|
||||||
type Document struct {
|
type Document struct {
|
||||||
ID string // 唯一标识符
|
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 // 目录树
|
||||||
@ -70,10 +70,8 @@ type ToCNode struct {
|
|||||||
|
|
||||||
// GetDB 获取 DocDBUnauthorized 实例并初始化表结构
|
// GetDB 获取 DocDBUnauthorized 实例并初始化表结构
|
||||||
func GetDB(db *tableDB.TableDBUnauthorized, baseDir string) *DocDBUnauthorized {
|
func GetDB(db *tableDB.TableDBUnauthorized, baseDir string) *DocDBUnauthorized {
|
||||||
// 使用系统管理员身份初始化
|
|
||||||
sys := db.Auth(tableDB.SystemUserID)
|
sys := db.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)
|
||||||
}
|
}
|
||||||
@ -133,23 +131,20 @@ func (d *DocDBUnauthorized) Auth(userID string) *DocDB {
|
|||||||
|
|
||||||
// SetDoc 更新内容 (强制提升版本号,自动保存历史)
|
// SetDoc 更新内容 (强制提升版本号,自动保存历史)
|
||||||
func (a *DocDB) SetDoc(docObj *Document) error {
|
func (a *DocDB) SetDoc(docObj *Document) error {
|
||||||
if docObj.ID == "" {
|
|
||||||
docObj.ID = id.MakeID(10)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
existing, err := a.table.Get(docObj.ID)
|
existing, _ := a.getRaw(docObj.Path)
|
||||||
|
|
||||||
isUpdate := err == nil && existing != nil
|
isUpdate := existing != nil
|
||||||
|
var internalID string
|
||||||
|
|
||||||
if isUpdate {
|
if isUpdate {
|
||||||
// 保存历史版本
|
|
||||||
a.archive(existing)
|
a.archive(existing)
|
||||||
|
internalID = cast.String(existing["id"])
|
||||||
docObj.Version = cast.Uint64(existing["version"]) + 1
|
docObj.Version = cast.Uint64(existing["version"]) + 1
|
||||||
docObj.CreateTime = cast.Int64(existing["createTime"])
|
docObj.CreateTime = cast.Int64(existing["createTime"])
|
||||||
docObj.Creator = cast.String(existing["creator"])
|
docObj.Creator = cast.String(existing["creator"])
|
||||||
} else {
|
} else {
|
||||||
|
internalID = id.MakeID(10)
|
||||||
docObj.Version = 1
|
docObj.Version = 1
|
||||||
docObj.CreateTime = now
|
docObj.CreateTime = now
|
||||||
docObj.Creator = a.userID
|
docObj.Creator = a.userID
|
||||||
@ -160,7 +155,7 @@ func (a *DocDB) SetDoc(docObj *Document) error {
|
|||||||
|
|
||||||
// 处理文件落盘
|
// 处理文件落盘
|
||||||
if docObj.BinaryContent != nil {
|
if docObj.BinaryContent != nil {
|
||||||
if err := a.saveFile(docObj); err != nil {
|
if err := a.saveFile(internalID, docObj); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
docObj.AsFile = true
|
docObj.AsFile = true
|
||||||
@ -172,10 +167,9 @@ func (a *DocDB) SetDoc(docObj *Document) error {
|
|||||||
docObj.ToC = ExtractToC(docObj.TextContent)
|
docObj.ToC = ExtractToC(docObj.TextContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为 map 并清理瞬态/冲突字段
|
record := a.toRecord(internalID, docObj)
|
||||||
record := a.toRecord(docObj)
|
|
||||||
|
|
||||||
err = a.table.Set(record)
|
err := a.table.Set(record)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if isUpdate {
|
if isUpdate {
|
||||||
a.triggerUpdated(docObj)
|
a.triggerUpdated(docObj)
|
||||||
@ -187,10 +181,10 @@ func (a *DocDB) SetDoc(docObj *Document) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetMeta 更新元数据 (不提升版本号)
|
// SetMeta 更新元数据 (不提升版本号)
|
||||||
func (a *DocDB) SetMeta(id string, meta map[string]any) error {
|
func (a *DocDB) SetMeta(path string, meta map[string]any) error {
|
||||||
existing, err := a.table.Get(id)
|
existing, err := a.getRaw(path)
|
||||||
if err != nil || existing == nil {
|
if err != nil || existing == nil {
|
||||||
return fmt.Errorf("document not found: %s", id)
|
return fmt.Errorf("document not found: %s", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
@ -203,34 +197,56 @@ func (a *DocDB) SetMeta(id string, meta map[string]any) error {
|
|||||||
return a.table.Set(existing)
|
return a.table.Set(existing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move 修改路径 (重命名)
|
||||||
|
func (a *DocDB) Move(oldPath, newPath string) error {
|
||||||
|
existing, err := a.getRaw(oldPath)
|
||||||
|
if err != nil || existing == nil {
|
||||||
|
return fmt.Errorf("document not found: %s", oldPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
existing["path"] = newPath
|
||||||
|
existing["updateTime"] = time.Now().UnixMilli()
|
||||||
|
existing["updater"] = a.userID
|
||||||
|
|
||||||
|
err = a.table.Set(existing)
|
||||||
|
if err == nil {
|
||||||
|
if a.parent.Hooks.OnMoved != nil {
|
||||||
|
a.parent.Hooks.OnMoved(oldPath, newPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *DocDB) getRaw(path string) (map[string]any, error) {
|
||||||
|
res, err := a.table.List(map[string]any{"path": path})
|
||||||
|
if err != nil || len(res) == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *DocDB) archive(existing map[string]any) {
|
func (a *DocDB) archive(existing map[string]any) {
|
||||||
histRecord := make(map[string]any)
|
histRecord := make(map[string]any)
|
||||||
for k, v := range existing {
|
for k, v := range existing {
|
||||||
histRecord[k] = v
|
histRecord[k] = v
|
||||||
}
|
}
|
||||||
histRecord["docId"] = cast.String(existing["id"])
|
histRecord["docId"] = cast.String(existing["id"])
|
||||||
delete(histRecord, "id") // 让 tableDB 自动生成历史记录 ID
|
delete(histRecord, "id")
|
||||||
_ = a.hist.Set(histRecord)
|
_ = a.hist.Set(histRecord)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *DocDB) toRecord(docObj *Document) map[string]any {
|
func (a *DocDB) toRecord(internalID string, docObj *Document) map[string]any {
|
||||||
record := make(map[string]any)
|
record := make(map[string]any)
|
||||||
cast.Convert(&record, docObj)
|
cast.Convert(&record, docObj)
|
||||||
|
record["id"] = internalID
|
||||||
if v, ok := record["ID"]; ok {
|
|
||||||
record["id"] = v
|
|
||||||
delete(record, "ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(record, "binaryContent")
|
delete(record, "binaryContent")
|
||||||
|
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *DocDB) saveFile(docObj *Document) error {
|
func (a *DocDB) saveFile(internalID string, docObj *Document) error {
|
||||||
subDir1 := docObj.ID[0:2]
|
subDir1 := internalID[0:2]
|
||||||
subDir2 := docObj.ID[2:6]
|
subDir2 := internalID[2:6]
|
||||||
subDir3 := docObj.ID[6:10]
|
subDir3 := internalID[6:10]
|
||||||
|
|
||||||
ext := filepath.Ext(docObj.Path)
|
ext := filepath.Ext(docObj.Path)
|
||||||
fileName := fmt.Sprintf("v%d%s", docObj.Version, ext)
|
fileName := fmt.Sprintf("v%d%s", docObj.Version, ext)
|
||||||
@ -252,8 +268,8 @@ func (a *DocDB) saveFile(docObj *Document) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get 获取文档 (当前版本)
|
// Get 获取文档 (当前版本)
|
||||||
func (a *DocDB) Get(id string) (*Document, error) {
|
func (a *DocDB) Get(path string) (*Document, error) {
|
||||||
res, err := a.table.Get(id)
|
res, err := a.getRaw(path)
|
||||||
if err != nil || res == nil {
|
if err != nil || res == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -263,36 +279,51 @@ func (a *DocDB) Get(id string) (*Document, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetByVersion 按版本获取文档
|
// GetByVersion 按版本获取文档
|
||||||
func (a *DocDB) GetByVersion(id string, version uint64) (*Document, error) {
|
func (a *DocDB) GetByVersion(path string, version uint64) (*Document, error) {
|
||||||
current, _ := a.Get(id)
|
current, _ := a.Get(path)
|
||||||
if current != nil && current.Version == version {
|
if current != nil && current.Version == version {
|
||||||
return current, nil
|
return current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := a.hist.List(map[string]any{"docId": id, "version": version})
|
// 查找原始 internalID
|
||||||
if err != nil || len(res) == 0 {
|
res, _ := a.table.List(map[string]any{"path": path})
|
||||||
|
var docID string
|
||||||
|
if len(res) > 0 {
|
||||||
|
docID = cast.String(res[0]["id"])
|
||||||
|
} else {
|
||||||
|
// 如果主表删除了,去历史表找 docId
|
||||||
|
histRes, _ := a.hist.List(map[string]any{"path": path})
|
||||||
|
if len(histRes) > 0 {
|
||||||
|
docID = cast.String(histRes[0]["docId"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if docID == "" {
|
||||||
|
return nil, fmt.Errorf("document not found: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
histRes, err := a.hist.List(map[string]any{"docId": docID, "version": version})
|
||||||
|
if err != nil || len(histRes) == 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var doc Document
|
var doc Document
|
||||||
cast.Convert(&doc, res[0])
|
cast.Convert(&doc, histRes[0])
|
||||||
doc.ID = id // 确保 ID 是原始文档 ID 而非历史记录 ID
|
|
||||||
return &doc, nil
|
return &doc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove 删除文档 (自动保存最后一版到历史)
|
// Remove 删除文档 (自动保存最后一版到历史)
|
||||||
func (a *DocDB) Remove(id string) error {
|
func (a *DocDB) Remove(path string) error {
|
||||||
existing, err := a.table.Get(id)
|
existing, err := a.getRaw(path)
|
||||||
if err != nil || existing == nil {
|
if err != nil || existing == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存最后一版到历史
|
|
||||||
a.archive(existing)
|
a.archive(existing)
|
||||||
|
|
||||||
err = a.table.Remove(id)
|
err = a.table.Remove(cast.String(existing["id"]))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
a.triggerRemoved(id)
|
a.triggerRemoved(path)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -309,9 +340,9 @@ func (a *DocDB) triggerUpdated(doc *Document) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *DocDB) triggerRemoved(id string) {
|
func (a *DocDB) triggerRemoved(path string) {
|
||||||
if a.parent.Hooks.OnRemoved != nil {
|
if a.parent.Hooks.OnRemoved != nil {
|
||||||
a.parent.Hooks.OnRemoved(id)
|
a.parent.Hooks.OnRemoved(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,17 +352,15 @@ func ExtractToC(content string) []ToCNode {
|
|||||||
var root []ToCNode
|
var root []ToCNode
|
||||||
stack := []*[]ToCNode{&root}
|
stack := []*[]ToCNode{&root}
|
||||||
|
|
||||||
inCodeBlock := false // 状态机:是否在代码块内
|
inCodeBlock := false
|
||||||
|
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
// 切换代码块状态
|
|
||||||
if strings.HasPrefix(trimmed, "```") {
|
if strings.HasPrefix(trimmed, "```") {
|
||||||
inCodeBlock = !inCodeBlock
|
inCodeBlock = !inCodeBlock
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 如果在代码块内部,直接跳过解析
|
|
||||||
if inCodeBlock {
|
if inCodeBlock {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
65
doc_test.go
65
doc_test.go
@ -19,18 +19,19 @@ func TestDocDB(t *testing.T) {
|
|||||||
|
|
||||||
unauthorizedDB := tableDB.GetDB("sqlite://"+dbFile, logger)
|
unauthorizedDB := tableDB.GetDB("sqlite://"+dbFile, logger)
|
||||||
|
|
||||||
// 使用 GetDB 入口
|
|
||||||
docDBInst := GetDB(unauthorizedDB, "./test_docs")
|
docDBInst := GetDB(unauthorizedDB, "./test_docs")
|
||||||
app := docDBInst.Auth("user1")
|
app := docDBInst.Auth("user1")
|
||||||
|
|
||||||
var createdCalled, updatedCalled, removedCalled bool
|
var createdCalled, updatedCalled, removedCalled, movedCalled bool
|
||||||
docDBInst.Hooks.OnCreatedDoc = func(doc *Document) { createdCalled = true }
|
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(id string) { removedCalled = true }
|
docDBInst.Hooks.OnRemoved = func(path string) { removedCalled = true }
|
||||||
|
docDBInst.Hooks.OnMoved = func(oldPath, newPath string) { movedCalled = true }
|
||||||
|
|
||||||
// 1. 测试 SetDoc (创建)
|
// 1. 测试 SetDoc (创建)
|
||||||
|
docPath := "/test/doc1.md"
|
||||||
doc1 := &Document{
|
doc1 := &Document{
|
||||||
Path: "/test/doc1.md",
|
Path: docPath,
|
||||||
Title: "Test Doc",
|
Title: "Test Doc",
|
||||||
TextContent: "# Header 1\nContent 1",
|
TextContent: "# Header 1\nContent 1",
|
||||||
Type: "markdown",
|
Type: "markdown",
|
||||||
@ -49,7 +50,7 @@ func TestDocDB(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 测试 Get
|
// 2. 测试 Get
|
||||||
d, err := app.Get(doc1.ID)
|
d, err := app.Get(docPath)
|
||||||
if err != nil || d == nil {
|
if err != nil || d == nil {
|
||||||
t.Fatalf("Get failed: %v", err)
|
t.Fatalf("Get failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -57,36 +58,53 @@ func TestDocDB(t *testing.T) {
|
|||||||
t.Fatalf("Expected title 'Test Doc', got %s", d.Title)
|
t.Fatalf("Expected title 'Test Doc', got %s", d.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 测试 SetDoc (更新内容, 提升版本并存入历史)
|
// 3. 测试 SetDoc (更新内容)
|
||||||
doc1.TextContent = "# Header 1\nUpdated Content"
|
doc1.TextContent = "# Header 1\nUpdated Content"
|
||||||
err = app.SetDoc(doc1)
|
err = app.SetDoc(doc1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Update SetDoc failed: %v", err)
|
t.Fatalf("Update SetDoc failed: %v", err)
|
||||||
}
|
}
|
||||||
if doc1.Version != 2 {
|
if doc1.Version != 2 {
|
||||||
t.Fatalf("Expected version 2 after content update, got %d", doc1.Version)
|
t.Fatalf("Expected version 2, got %d", doc1.Version)
|
||||||
}
|
}
|
||||||
if !updatedCalled {
|
if !updatedCalled {
|
||||||
t.Error("OnUpdatedDoc event not triggered")
|
t.Error("OnUpdatedDoc event not triggered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 测试 GetByVersion
|
// 4. 测试 GetByVersion
|
||||||
hist, err := app.GetByVersion(doc1.ID, 1)
|
hist, err := app.GetByVersion(docPath, 1)
|
||||||
if err != nil || hist == nil {
|
if err != nil || hist == nil {
|
||||||
t.Fatalf("GetByVersion(1) failed: %v", err)
|
t.Fatalf("GetByVersion(1) failed: %v", err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(hist.TextContent, "Content 1") {
|
if !strings.Contains(hist.TextContent, "Content 1") {
|
||||||
t.Fatalf("Expected 'Content 1' in history version 1, got %s", hist.TextContent)
|
t.Fatalf("Expected 'Content 1' in history, got %s", hist.TextContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
curr, _ := app.GetByVersion(doc1.ID, 2)
|
// 5. 测试 Move (重命名)
|
||||||
if curr.Version != 2 || !strings.Contains(curr.TextContent, "Updated Content") {
|
newPath := "/test/renamed.md"
|
||||||
t.Fatalf("Expected version 2 content, got version %d", curr.Version)
|
err = app.Move(docPath, newPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Move failed: %v", err)
|
||||||
|
}
|
||||||
|
if !movedCalled {
|
||||||
|
t.Error("OnMoved event not triggered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 测试 Remove 后依然能通过 GetByVersion 获取
|
// 检查旧路径失效,新路径生效
|
||||||
docID := doc1.ID
|
oldDoc, _ := app.Get(docPath)
|
||||||
err = app.Remove(docID)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Remove failed: %v", err)
|
t.Fatalf("Remove failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -94,18 +112,17 @@ func TestDocDB(t *testing.T) {
|
|||||||
t.Error("OnRemoved event not triggered")
|
t.Error("OnRemoved event not triggered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查当前版本已删除
|
removedDoc, _ := app.Get(newPath)
|
||||||
removedDoc, _ := app.Get(docID)
|
|
||||||
if removedDoc != nil {
|
if removedDoc != nil {
|
||||||
t.Error("Document should be removed from _Doc")
|
t.Error("Document should be removed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查依然能获取历史版本 (包括刚删除的最后一版 v2)
|
// 7. 测试删除后依然能获取历史
|
||||||
histV2, err := app.GetByVersion(docID, 2)
|
lastHist, err := app.GetByVersion(newPath, 2)
|
||||||
if err != nil || histV2 == nil {
|
if err != nil || lastHist == nil {
|
||||||
t.Fatalf("GetByVersion(2) should work after removal: %v", err)
|
t.Fatalf("Should get history after removal: %v", err)
|
||||||
}
|
}
|
||||||
if histV2.Version != 2 {
|
if lastHist.Version != 2 {
|
||||||
t.Fatalf("Expected version 2 from history after removal, got %d", histV2.Version)
|
t.Fatalf("Expected version 2, got %d", lastHist.Version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user