migrate from github
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user