migrate from github

This commit is contained in:
2026-07-15 22:42:31 +08:00
commit 3ad559f05e
262 changed files with 21637 additions and 0 deletions
@@ -0,0 +1,32 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentCreateOperation struct {
repo *DocumentRepository
doc *model.Document
tx *gorm.DB
}
func (r *DocumentRepository) Create(doc *model.Document) *DocumentCreateOperation {
return &DocumentCreateOperation{
repo: r,
doc: doc,
}
}
func (op *DocumentCreateOperation) WithTx(tx *gorm.DB) *DocumentCreateOperation {
op.tx = tx
return op
}
func (op *DocumentCreateOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
return op.tx.Create(op.doc).Error
}
@@ -0,0 +1,36 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type UpsertRecentDocumentOperation struct {
repo *DocumentRepository
m *model.RecentDocument
tx *gorm.DB
}
func (r *DocumentRepository) UpsertRecentDocument(m *model.RecentDocument) *UpsertRecentDocumentOperation {
return &UpsertRecentDocumentOperation{
repo: r,
m: m,
}
}
func (op *UpsertRecentDocumentOperation) WithTx(tx *gorm.DB) *UpsertRecentDocumentOperation {
op.tx = tx
return op
}
func (op *UpsertRecentDocumentOperation) Exec() error {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
return db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "document_id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"accessed_at": op.m.AccessedAt}),
}).Create(op.m).Error
}
@@ -0,0 +1,92 @@
package document_repo
import (
"context"
cache_loader "github.com/XingfenD/yoresee_doc/internal/cache"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type DocumentRepository struct {
db *gorm.DB
redis *redis.Client
Loader cache_loader.Loader
}
func NewDocumentRepository(db *gorm.DB, redis *redis.Client) *DocumentRepository {
return &DocumentRepository{
db: db,
redis: redis,
Loader: *cache_loader.NewLoader(redis),
}
}
type DocumentDeleteOperation struct {
repo *DocumentRepository
id int64
tx *gorm.DB
ctx context.Context
}
func (r *DocumentRepository) Delete(id int64) *DocumentDeleteOperation {
return &DocumentDeleteOperation{
repo: r,
id: id,
ctx: context.Background(),
}
}
func (op *DocumentDeleteOperation) WithTx(tx *gorm.DB) *DocumentDeleteOperation {
op.tx = tx
return op
}
func (op *DocumentDeleteOperation) WithContext(ctx context.Context) *DocumentDeleteOperation {
op.ctx = ctx
return op
}
func (op *DocumentDeleteOperation) Exec() error {
var doc model.Document
var db *gorm.DB
var err error
if op.tx != nil {
db = op.tx
} else {
db = op.repo.db
}
if err := db.First(&doc, op.id).Error; err != nil {
return err
}
if op.tx == nil {
db = db.Begin()
defer func() {
if err != nil {
db.Rollback()
} else {
db.Commit()
}
}()
}
if err := db.Delete(&model.Document{}, op.id).Error; err != nil {
return err
}
if op.tx == nil {
docCacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, doc.ExternalID)
if err := op.repo.redis.Del(op.ctx, docCacheKey).Err(); err != nil {
}
if err := op.repo.BumpSubtreeVersionsByPath(op.ctx, doc.Path); err != nil {
}
}
return nil
}
@@ -0,0 +1,60 @@
package document_repo
import (
"context"
cache_loader "github.com/XingfenD/yoresee_doc/internal/cache"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type DocumentGetByExternalIDOperation struct {
repo *DocumentRepository
externalID string
tx *gorm.DB
}
func (r *DocumentRepository) GetByExternalID(externalID string) *DocumentGetByExternalIDOperation {
return &DocumentGetByExternalIDOperation{
repo: r,
externalID: externalID,
}
}
func (op *DocumentGetByExternalIDOperation) WithTx(tx *gorm.DB) *DocumentGetByExternalIDOperation {
op.tx = tx
return op
}
func (op *DocumentGetByExternalIDOperation) query(db *gorm.DB) (*model.Document, error) {
var document model.Document
var err error
err = db.First(&document, "external_id = ?", op.externalID).Error
return &document, err
}
func (op *DocumentGetByExternalIDOperation) Exec(ctx context.Context) (*model.Document, error) {
var err error
if op.tx != nil {
return op.query(op.tx)
}
documentCacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, op.externalID)
document, err := cache_loader.NewCacheLoadOperation[model.Document](&op.repo.Loader).
WithDBLoader(func() (*model.Document, error) {
return op.query(op.repo.db)
}).WithDefaultKeyAndParser(documentCacheKey, nil).
Exec(ctx)
if err != nil {
logrus.Errorf("load data failed for DocumentGetByExternalIDOperation: %+v", err)
return nil, err
}
return document, nil
}
@@ -0,0 +1,41 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentGetContentOperation struct {
repo *DocumentRepository
documentID int64
tx *gorm.DB
}
func (r *DocumentRepository) GetContent(documentID int64) *DocumentGetContentOperation {
return &DocumentGetContentOperation{
repo: r,
documentID: documentID,
}
}
func (op *DocumentGetContentOperation) WithTx(tx *gorm.DB) *DocumentGetContentOperation {
op.tx = tx
return op
}
func (op *DocumentGetContentOperation) Exec() (string, error) {
var docMeta model.Document
var err error
db := op.repo.db
if op.tx != nil {
db = op.tx
}
err = db.Where("id = ?", op.documentID).First(&docMeta).Error
if err != nil {
return "", err
}
return docMeta.Content, nil
}
@@ -0,0 +1,66 @@
package document_repo
import (
"context"
cache_loader "github.com/XingfenD/yoresee_doc/internal/cache"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/key"
"gorm.io/gorm"
)
type DocumentGetIDByExternalIDOperation struct {
repo *DocumentRepository
externalID string
tx *gorm.DB
}
func (r *DocumentRepository) GetIDByExternalID(externalID string) *DocumentGetIDByExternalIDOperation {
return &DocumentGetIDByExternalIDOperation{
repo: r,
externalID: externalID,
}
}
func (op *DocumentGetIDByExternalIDOperation) WithTx(tx *gorm.DB) *DocumentGetIDByExternalIDOperation {
op.tx = tx
return op
}
func (op DocumentGetIDByExternalIDOperation) query(tx *gorm.DB) (int64, error) {
var id int64
err := tx.Model(&model.Document{}).Where("external_id = ?", op.externalID).Pluck("id", &id).Error
if err != nil {
return 0, err
}
return id, nil
}
func (op *DocumentGetIDByExternalIDOperation) Exec(ctx context.Context) (int64, error) {
if op.tx != nil {
return op.query(op.tx)
}
extidKey := key.KeyIDByExternalID(key.KeyObjectTypeEnum_Doc, op.externalID)
modelKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, op.externalID)
id, err := cache_loader.NewCacheLoadOperation[int64](&op.repo.Loader).
WithDefaultKeyAndParser(extidKey, cache_loader.ParseInt64).
WithKeyAndParser(modelKey, cache_loader.ParseIDFromDocument).
WithDBLoader(func() (*int64, error) {
id, err := op.query(op.repo.db)
if err != nil {
return nil, err
}
return &id, nil
}).Exec(ctx)
if err != nil {
return 0, err
}
if id == nil {
return 0, nil
}
return *id, nil
}
@@ -0,0 +1,149 @@
package document_repo
import (
"context"
"errors"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/key"
"gorm.io/gorm"
)
type DocumentGetSubtreeOperation struct {
repo *DocumentRepository
rootParentID int64
knowledgeID *int64
depth *int
directoryOnly bool
tx *gorm.DB
}
func (r *DocumentRepository) GetSubtree(rootParentID int64) *DocumentGetSubtreeOperation {
return &DocumentGetSubtreeOperation{
repo: r,
rootParentID: rootParentID,
}
}
func (op *DocumentGetSubtreeOperation) WithTx(tx *gorm.DB) *DocumentGetSubtreeOperation {
op.tx = tx
return op
}
func (op *DocumentGetSubtreeOperation) WithKnowledgeID(knowledgeID *int64) *DocumentGetSubtreeOperation {
op.knowledgeID = knowledgeID
return op
}
func (op *DocumentGetSubtreeOperation) WithDepth(depth *int) *DocumentGetSubtreeOperation {
op.depth = depth
return op
}
func (op *DocumentGetSubtreeOperation) WithDirectoryOnly(with bool) *DocumentGetSubtreeOperation {
op.directoryOnly = with
return op
}
func (op *DocumentGetSubtreeOperation) Exec(ctx context.Context) ([]*model.Document, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
var documents []*model.Document
if op.depth != nil && *op.depth == 0 {
return documents, nil
}
type pathDepth struct {
Path string
Depth int
}
var root pathDepth
err := db.Model(&model.Document{}).
Select("path, depth").
Where("id = ?", op.rootParentID).
Take(&root).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return documents, nil
}
return nil, err
}
// in-transaction query should bypass cache to avoid stale reads
if op.tx != nil || op.repo.redis == nil {
return op.queryWithRoot(db, root.Path, root.Depth)
}
version, err := op.repo.getSubtreeVersion(ctx, root.Path)
if err == nil {
cacheKey := key.KeyDocSubtree(root.Path, version, op.depth)
if cachedIDs, ok, err := getCachedSubtreeIDs(ctx, cacheKey); err == nil && ok {
return op.repo.fetchDocumentsByIDs(cachedIDs)
}
val, err, _ := subtreeCacheSF.Do(cacheKey, func() (interface{}, error) {
dbDocs, err := op.queryWithRoot(db, root.Path, root.Depth)
if err != nil {
return nil, err
}
ids := make([]int64, 0, len(dbDocs))
for _, doc := range dbDocs {
ids = append(ids, doc.ID)
}
setCachedSubtreeIDs(ctx, cacheKey, ids)
return dbDocs, nil
})
if err != nil {
return nil, err
}
if typed, ok := val.([]*model.Document); ok {
return typed, nil
}
}
return op.queryWithRoot(db, root.Path, root.Depth)
}
func (op *DocumentGetSubtreeOperation) queryWithRoot(db *gorm.DB, rootPath string, rootDepth int) ([]*model.Document, error) {
var documents []*model.Document
query := `
SELECT *
FROM document_metas
WHERE deleted_at IS NULL
AND id <> ?
AND path <@ ?
`
if op.directoryOnly {
query = `
SELECT id, external_id, title, parent_id, type
FROM document_metas
WHERE deleted_at IS NULL
AND id <> ?
AND path <@ ?
`
}
args := []interface{}{op.rootParentID, rootPath}
if op.knowledgeID != nil {
query += " AND knowledge_id = ?"
args = append(args, *op.knowledgeID)
}
if op.depth != nil {
maxDepth := rootDepth + *op.depth
query += " AND depth <= ?"
args = append(args, maxDepth)
}
query += " ORDER BY depth, created_at"
if err := db.Raw(query, args...).Find(&documents).Error; err != nil {
return nil, err
}
return documents, nil
}
@@ -0,0 +1,64 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentGetSubtreeByKnowledgeIDOperation struct {
repo *DocumentRepository
knowledgeID int64
depth *int
tx *gorm.DB
}
func (r *DocumentRepository) GetSubtreeByKnowledgeID(knowledgeID int64) *DocumentGetSubtreeByKnowledgeIDOperation {
return &DocumentGetSubtreeByKnowledgeIDOperation{
repo: r,
knowledgeID: knowledgeID,
}
}
func (op *DocumentGetSubtreeByKnowledgeIDOperation) WithTx(tx *gorm.DB) *DocumentGetSubtreeByKnowledgeIDOperation {
op.tx = tx
return op
}
func (op *DocumentGetSubtreeByKnowledgeIDOperation) WithDepth(depth int) *DocumentGetSubtreeByKnowledgeIDOperation {
op.depth = &depth
return op
}
func (op *DocumentGetSubtreeByKnowledgeIDOperation) Exec() ([]model.Document, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
var documents []model.Document
if op.depth != nil && *op.depth == 0 {
return documents, nil
}
query := `
SELECT *
FROM document_metas
WHERE deleted_at IS NULL
AND knowledge_id = ?
`
args := []interface{}{op.knowledgeID}
if op.depth != nil {
query += " AND depth <= ?"
args = append(args, *op.depth)
}
query += " ORDER BY depth, created_at"
err := db.Raw(query, args...).Find(&documents).Error
if err != nil {
return nil, err
}
return documents, nil
}
+246
View File
@@ -0,0 +1,246 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentsListOperation struct {
repo *DocumentRepository
model *model.Document
userID *int64
parentID *int64
knowledgeID *int64
listOwnDoc bool
ids []int64
titleKeyword *string
docType *string
tags []string
createTimeRangeStart *string
createTimeRangeEnd *string
updateTimeRangeStart *string
updateTimeRangeEnd *string
sortField string
sortDesc bool
page int
pageSize int
directoryOnly bool
tx *gorm.DB
}
func (r *DocumentRepository) ListDocuments(documentModel *model.Document) *DocumentsListOperation {
return &DocumentsListOperation{
repo: r,
model: documentModel,
}
}
func (op *DocumentsListOperation) WithUserID(userID *int64) *DocumentsListOperation {
op.userID = userID
return op
}
func (op *DocumentsListOperation) WithParentID(parentID *int64) *DocumentsListOperation {
op.parentID = parentID
return op
}
func (op *DocumentsListOperation) WithKnowledgeID(knowledgeID *int64) *DocumentsListOperation {
op.knowledgeID = knowledgeID
return op
}
func (op *DocumentsListOperation) WithListOwnDoc(listOwnDoc bool) *DocumentsListOperation {
op.listOwnDoc = listOwnDoc
return op
}
func (op *DocumentsListOperation) WithIDs(ids []int64) *DocumentsListOperation {
op.ids = ids
return op
}
func (op *DocumentsListOperation) WithTitleKeyword(titleKeyword *string) *DocumentsListOperation {
op.titleKeyword = titleKeyword
return op
}
func (op *DocumentsListOperation) WithType(docType *string) *DocumentsListOperation {
op.docType = docType
return op
}
func (op *DocumentsListOperation) WithTags(tags []string) *DocumentsListOperation {
op.tags = tags
return op
}
func (op *DocumentsListOperation) WithCreateTimeRange(start, end *string) *DocumentsListOperation {
op.createTimeRangeStart = start
op.createTimeRangeEnd = end
return op
}
func (op *DocumentsListOperation) WithUpdateTimeRange(start, end *string) *DocumentsListOperation {
op.updateTimeRangeStart = start
op.updateTimeRangeEnd = end
return op
}
func (op *DocumentsListOperation) WithPagination(page, pageSize int) *DocumentsListOperation {
op.page = page
op.pageSize = pageSize
return op
}
func (op *DocumentsListOperation) WithSort(field string, desc bool) *DocumentsListOperation {
op.sortField = field
op.sortDesc = desc
return op
}
func (op *DocumentsListOperation) WithDirectoryOnly(with bool) *DocumentsListOperation {
op.directoryOnly = with
return op
}
func (op *DocumentsListOperation) WithTx(tx *gorm.DB) *DocumentsListOperation {
op.tx = tx
return op
}
func (op *DocumentsListOperation) buildBaseQuery() *gorm.DB {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
dbQuery := db.Model(&model.Document{})
if op.directoryOnly {
dbQuery = dbQuery.Select("id, external_id, title, parent_id, type")
}
if op.model != nil {
dbQuery = dbQuery.Where(op.model)
}
if op.userID != nil {
dbQuery = dbQuery.Where("user_id = ?", *op.userID)
}
if op.parentID != nil {
dbQuery = dbQuery.Where("parent_id = ?", *op.parentID)
}
if op.listOwnDoc {
dbQuery = dbQuery.Where("knowledge_id IS NULL")
}
if op.ids != nil {
if len(op.ids) == 0 {
dbQuery = dbQuery.Where("1 = 0")
} else {
dbQuery = dbQuery.Where("id IN ?", op.ids)
}
}
if op.knowledgeID != nil {
dbQuery = dbQuery.Where("knowledge_id = ?", *op.knowledgeID)
}
if op.titleKeyword != nil && *op.titleKeyword != "" {
like := "%" + *op.titleKeyword + "%"
dbQuery = dbQuery.Where("(title LIKE ? OR content LIKE ?)", like, like)
}
if op.docType != nil && *op.docType != "" {
dbQuery = dbQuery.Where("type = ?", *op.docType)
}
if len(op.tags) > 0 {
for _, tag := range op.tags {
dbQuery = dbQuery.Where("JSON_CONTAINS(tags, ?)", "\""+tag+"\"")
}
}
if op.createTimeRangeStart != nil && *op.createTimeRangeStart != "" {
dbQuery = dbQuery.Where("created_at >= ?", *op.createTimeRangeStart)
}
if op.createTimeRangeEnd != nil && *op.createTimeRangeEnd != "" {
dbQuery = dbQuery.Where("created_at <= ?", *op.createTimeRangeEnd)
}
if op.updateTimeRangeStart != nil && *op.updateTimeRangeStart != "" {
dbQuery = dbQuery.Where("updated_at >= ?", *op.updateTimeRangeStart)
}
if op.updateTimeRangeEnd != nil && *op.updateTimeRangeEnd != "" {
dbQuery = dbQuery.Where("updated_at <= ?", *op.updateTimeRangeEnd)
}
return dbQuery
}
func (op *DocumentsListOperation) appendOtherArgs(db *gorm.DB) *gorm.DB {
dbQuery := db
sortField := op.sortField
if sortField == "" {
sortField = "created_at"
}
orderDirection := "ASC"
if op.sortDesc {
orderDirection = "DESC"
}
dbQuery = dbQuery.Order(sortField + " " + orderDirection)
page := op.page
pageSize := op.pageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
offset := (page - 1) * pageSize
dbQuery = dbQuery.Offset(offset).Limit(pageSize)
return dbQuery
}
func (op *DocumentsListOperation) Exec() ([]model.Document, error) {
dbQuery := op.buildBaseQuery()
dbQuery = op.appendOtherArgs(dbQuery)
var documents []model.Document
err := dbQuery.Find(&documents).Error
if err != nil {
return nil, err
}
return documents, nil
}
func (op *DocumentsListOperation) ExecWithTotal() ([]model.Document, int64, error) {
dbQuery := op.buildBaseQuery()
var total int64
err := dbQuery.Count(&total).Error
if err != nil {
return nil, 0, err
}
dbQuery = op.appendOtherArgs(dbQuery)
var documents []model.Document
err = dbQuery.Find(&documents).Error
if err != nil {
return nil, 0, err
}
return documents, total, nil
}
@@ -0,0 +1,82 @@
package document_repo
import (
"time"
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type ListRecentDocumentsOperation struct {
repo *DocumentRepository
userID int64
startTime *time.Time
endTime *time.Time
page int
pageSize int
tx *gorm.DB
}
func (r *DocumentRepository) ListRecentDocuments(userID int64) *ListRecentDocumentsOperation {
return &ListRecentDocumentsOperation{
repo: r,
userID: userID,
}
}
func (op *ListRecentDocumentsOperation) WithTimeRange(start, end *time.Time) *ListRecentDocumentsOperation {
op.startTime = start
op.endTime = end
return op
}
func (op *ListRecentDocumentsOperation) WithPagination(page, pageSize int) *ListRecentDocumentsOperation {
op.page = page
op.pageSize = pageSize
return op
}
func (op *ListRecentDocumentsOperation) WithTx(tx *gorm.DB) *ListRecentDocumentsOperation {
op.tx = tx
return op
}
func (op *ListRecentDocumentsOperation) Exec() ([]*model.RecentDocument, int64, error) {
db := op.repo.db
if op.tx != nil {
db = op.tx
}
query := db.Model(&model.RecentDocument{}).
Where("user_id = ?", op.userID)
if op.startTime != nil {
query = query.Where("accessed_at >= ?", *op.startTime)
}
if op.endTime != nil {
query = query.Where("accessed_at <= ?", *op.endTime)
}
page := op.page
pageSize := op.pageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
offset := (page - 1) * pageSize
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var records []*model.RecentDocument
if err := query.Order("accessed_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
return nil, 0, err
}
return records, total, nil
}
@@ -0,0 +1,29 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
)
func (r *DocumentRepository) MGetByIDs(ids []int64) ([]*model.Document, error) {
if len(ids) == 0 {
return []*model.Document{}, nil
}
var docs []*model.Document
if err := r.db.Model(&model.Document{}).Where("id IN ?", ids).Find(&docs).Error; err != nil {
return nil, err
}
docMap := make(map[int64]*model.Document, len(docs))
for _, doc := range docs {
docMap[doc.ID] = doc
}
ordered := make([]*model.Document, 0, len(ids))
for _, id := range ids {
if doc, ok := docMap[id]; ok {
ordered = append(ordered, doc)
}
}
return ordered, nil
}
@@ -0,0 +1,78 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentMoveSubtreeOperation struct {
repo *DocumentRepository
docID int64
newParentID int64
tx *gorm.DB
}
func (r *DocumentRepository) MoveSubtree(docID, newParentID int64) *DocumentMoveSubtreeOperation {
return &DocumentMoveSubtreeOperation{
repo: r,
docID: docID,
newParentID: newParentID,
}
}
func (op *DocumentMoveSubtreeOperation) WithTx(tx *gorm.DB) *DocumentMoveSubtreeOperation {
op.tx = tx
return op
}
func (op *DocumentMoveSubtreeOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
type pathDepth struct {
Path string
Depth int
}
var old pathDepth
if err := op.tx.Model(&model.Document{}).
Select("path, depth").
Where("id = ? AND deleted_at IS NULL", op.docID).
Take(&old).Error; err != nil {
return err
}
var newPath string
var newDepth int
if op.newParentID == 0 {
newDepth = 0
} else {
var parent pathDepth
if err := op.tx.Model(&model.Document{}).
Select("path, depth").
Where("id = ? AND deleted_at IS NULL", op.newParentID).
Take(&parent).Error; err != nil {
return err
}
newPath = parent.Path
newDepth = parent.Depth + 1
}
depthDelta := newDepth - old.Depth
if op.newParentID == 0 {
return op.tx.Exec(`
UPDATE document_metas
SET path = (?::text)::ltree || COALESCE(subpath(path, nlevel(?::ltree)), ''::ltree),
depth = depth + ?
WHERE path <@ ?::ltree AND deleted_at IS NULL
`, op.docID, old.Path, depthDelta, old.Path).Error
}
return op.tx.Exec(`
UPDATE document_metas
SET path = (?::ltree) || (?::text)::ltree || COALESCE(subpath(path, nlevel(?::ltree)), ''::ltree),
depth = depth + ?
WHERE path <@ ?::ltree AND deleted_at IS NULL
`, newPath, op.docID, old.Path, depthDelta, old.Path).Error
}
@@ -0,0 +1,137 @@
package document_repo
import (
"context"
"strconv"
"strings"
"time"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/cache"
"github.com/XingfenD/yoresee_doc/pkg/key"
"github.com/redis/go-redis/v9"
"github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"
"gorm.io/gorm"
)
const (
subtreeCacheTTL = 5 * time.Minute
subtreeEmptyTTL = 1 * time.Minute
subtreeCacheMaxDepth = 3
)
var subtreeCacheSF singleflight.Group
func splitPathPrefixes(path string) []string {
parts := strings.Split(path, ".")
out := make([]string, 0, len(parts))
for i := range parts {
out = append(out, strings.Join(parts[:i+1], "."))
}
return out
}
func getDocPathByID(tx *gorm.DB, docID int64) (string, error) {
type docPath struct {
Path string
}
var result docPath
err := tx.Model(&model.Document{}).
Select("path").
Where("id = ?", docID).
Take(&result).Error
if err != nil {
return "", err
}
return result.Path, nil
}
func (r *DocumentRepository) GetPathByID(id int64) (string, error) {
return getDocPathByID(r.db, id)
}
func (r *DocumentRepository) GetPathByIDWithTx(tx *gorm.DB, id int64) (string, error) {
return getDocPathByID(tx, id)
}
func (r *DocumentRepository) getSubtreeVersion(ctx context.Context, path string) (int64, error) {
if r.redis == nil {
return 0, nil
}
val, err := r.redis.Get(ctx, key.KeyDocSubtreeVersion(path)).Result()
if err == redis.Nil {
return 0, nil
}
if err != nil {
return 0, err
}
version, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return 0, nil
}
return version, nil
}
func (r *DocumentRepository) BumpSubtreeVersionsByPath(ctx context.Context, path string) error {
if r.redis == nil {
return nil
}
prefixes := splitPathPrefixes(path)
pipe := r.redis.Pipeline()
for _, prefix := range prefixes {
pipe.Incr(ctx, key.KeyDocSubtreeVersion(prefix))
}
_, err := pipe.Exec(ctx)
if err == redis.Nil {
return nil
}
return err
}
func getCachedSubtreeIDs(ctx context.Context, key string) ([]int64, bool, error) {
var ids []int64
ok, err := cache.GetJSON(ctx, key, &ids)
if err != nil {
return nil, false, err
}
if !ok {
return nil, false, nil
}
return ids, true, nil
}
func setCachedSubtreeIDs(ctx context.Context, key string, ids []int64) {
ttl := subtreeCacheTTL
if len(ids) == 0 {
ttl = subtreeEmptyTTL
}
if err := cache.SetJSON(ctx, key, ids, ttl); err != nil {
logrus.Warnf("set subtree cache failed, key=%s, err=%v", key, err)
}
}
func (r *DocumentRepository) fetchDocumentsByIDs(ids []int64) ([]*model.Document, error) {
if len(ids) == 0 {
return []*model.Document{}, nil
}
var docs []*model.Document
if err := r.db.Model(&model.Document{}).Where("id IN ?", ids).Find(&docs).Error; err != nil {
return nil, err
}
docMap := make(map[int64]*model.Document, len(docs))
for _, doc := range docs {
docMap[doc.ID] = doc
}
ordered := make([]*model.Document, 0, len(ids))
for _, id := range ids {
if doc, ok := docMap[id]; ok {
ordered = append(ordered, doc)
}
}
return ordered, nil
}
@@ -0,0 +1,84 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentUpdateOperation struct {
repo *DocumentRepository
doc *model.Document
updateFields map[string]bool
tx *gorm.DB
}
func (r *DocumentRepository) Update(doc *model.Document) *DocumentUpdateOperation {
return &DocumentUpdateOperation{
repo: r,
doc: doc,
updateFields: make(map[string]bool),
}
}
func (op *DocumentUpdateOperation) UpdateTitle() *DocumentUpdateOperation {
op.updateFields["title"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateSummary() *DocumentUpdateOperation {
op.updateFields["summary"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateContent() *DocumentUpdateOperation {
op.updateFields["content"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateParentID() *DocumentUpdateOperation {
op.updateFields["parent_id"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateKnowledgeID() *DocumentUpdateOperation {
op.updateFields["knowledge_id"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateContainerType() *DocumentUpdateOperation {
op.updateFields["container_type"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateIsPublic() *DocumentUpdateOperation {
op.updateFields["is_public"] = true
return op
}
func (op *DocumentUpdateOperation) UpdateTags() *DocumentUpdateOperation {
op.updateFields["tags"] = true
return op
}
func (op *DocumentUpdateOperation) WithTx(tx *gorm.DB) *DocumentUpdateOperation {
op.tx = tx
return op
}
func (op *DocumentUpdateOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
query := op.tx.Model(op.doc)
if len(op.updateFields) > 0 {
fields := make([]string, 0, len(op.updateFields))
for field := range op.updateFields {
fields = append(fields, field)
}
query = query.Select(fields)
}
return query.Updates(*op.doc).Error
}
@@ -0,0 +1,48 @@
package document_repo
import (
"context"
"github.com/XingfenD/yoresee_doc/internal/model"
"github.com/XingfenD/yoresee_doc/pkg/cache"
"github.com/XingfenD/yoresee_doc/pkg/key"
"gorm.io/gorm"
)
type DocumentUpdateContentByExternalIDOperation struct {
repo *DocumentRepository
externalID string
content string
tx *gorm.DB
}
func (r *DocumentRepository) UpdateContentByExternalID(externalID, content string) *DocumentUpdateContentByExternalIDOperation {
return &DocumentUpdateContentByExternalIDOperation{
repo: r,
externalID: externalID,
content: content,
}
}
func (op *DocumentUpdateContentByExternalIDOperation) WithTx(tx *gorm.DB) *DocumentUpdateContentByExternalIDOperation {
op.tx = tx
return op
}
func (op *DocumentUpdateContentByExternalIDOperation) Exec(ctx context.Context) error {
if op.tx == nil {
op.tx = op.repo.db
}
docModelCacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, op.externalID)
return cache.DoubleDelete(
context.Background(),
func() error {
return op.tx.WithContext(ctx).Model(&model.Document{}).
Where("external_id = ?", op.externalID).
Select("content").
Updates(map[string]interface{}{"content": op.content}).Error
},
docModelCacheKey,
)
}
@@ -0,0 +1,58 @@
package document_repo
import (
"github.com/XingfenD/yoresee_doc/internal/model"
"gorm.io/gorm"
)
type DocumentUpdatePathDepthOperation struct {
repo *DocumentRepository
docID int64
parentID int64
tx *gorm.DB
}
func (r *DocumentRepository) UpdatePathDepth(docID, parentID int64) *DocumentUpdatePathDepthOperation {
return &DocumentUpdatePathDepthOperation{
repo: r,
docID: docID,
parentID: parentID,
}
}
func (op *DocumentUpdatePathDepthOperation) WithTx(tx *gorm.DB) *DocumentUpdatePathDepthOperation {
op.tx = tx
return op
}
func (op *DocumentUpdatePathDepthOperation) Exec() error {
if op.tx == nil {
op.tx = op.repo.db
}
if op.parentID == 0 {
return op.tx.Exec(`
UPDATE document_metas
SET path = (id::text)::ltree, depth = 0
WHERE id = ? AND deleted_at IS NULL
`, op.docID).Error
}
type pathDepth struct {
Path string
Depth int
}
var parent pathDepth
if err := op.tx.Model(&model.Document{}).
Select("path, depth").
Where("id = ? AND deleted_at IS NULL", op.parentID).
Take(&parent).Error; err != nil {
return err
}
return op.tx.Exec(`
UPDATE document_metas
SET path = (?::ltree) || (id::text)::ltree, depth = ?
WHERE id = ? AND deleted_at IS NULL
`, parent.Path, parent.Depth+1, op.docID).Error
}