migrate from github
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/config"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/storage"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const maxAttachmentSize = 5 * 1024 * 1024
|
||||
|
||||
func (s *DocumentService) UploadAttachment(
|
||||
ctx context.Context,
|
||||
userExternalID string,
|
||||
documentExternalID string,
|
||||
file []byte,
|
||||
fileName string,
|
||||
contentType string,
|
||||
) (*dto.AttachmentResponse, error) {
|
||||
if s == nil || strings.TrimSpace(userExternalID) == "" || strings.TrimSpace(documentExternalID) == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid upload attachment request")
|
||||
}
|
||||
if len(file) == 0 || len(file) > maxAttachmentSize {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid attachment file")
|
||||
}
|
||||
if config.GlobalConfig == nil || strings.TrimSpace(config.GlobalConfig.Minio.Bucket) == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "attachment storage is not configured")
|
||||
}
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
userID, err := s.userRepo.GetIDByExternalID(userExternalID).Exec()
|
||||
if err != nil || userID <= 0 {
|
||||
logrus.Errorf("[Service layer: DocumentService] query user id failed, user_external_id=%s, err=%+v", userExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found")
|
||||
}
|
||||
|
||||
safeName := strings.TrimSpace(fileName)
|
||||
if safeName == "" {
|
||||
safeName = "attachment.bin"
|
||||
}
|
||||
contentType = strings.TrimSpace(contentType)
|
||||
if contentType == "" {
|
||||
contentType = http.DetectContentType(file)
|
||||
}
|
||||
|
||||
attachmentExternalID := utils.GenerateExternalID(utils.ExternalIDContextAttachment)
|
||||
objectName := fmt.Sprintf(
|
||||
"attachments/%s/%s-%d%s",
|
||||
documentExternalID,
|
||||
attachmentExternalID,
|
||||
time.Now().UnixNano(),
|
||||
resolveAttachmentExt(safeName, contentType),
|
||||
)
|
||||
|
||||
err = storage.PutFile(
|
||||
config.GlobalConfig.Minio.Bucket,
|
||||
objectName,
|
||||
bytes.NewReader(file),
|
||||
int64(len(file)),
|
||||
contentType,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] upload attachment to minio failed, document_external_id=%s, object_name=%s, err=%+v", documentExternalID, objectName, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "upload attachment failed")
|
||||
}
|
||||
|
||||
attachment := &model.Attachment{
|
||||
ExternalID: attachmentExternalID,
|
||||
DocumentID: documentID,
|
||||
Name: safeName,
|
||||
Path: objectName,
|
||||
Size: int64(len(file)),
|
||||
MimeType: contentType,
|
||||
UserID: userID,
|
||||
}
|
||||
if err = s.attachmentRepo.Create(attachment); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] save attachment meta failed, document_external_id=%s, attachment_external_id=%s, err=%+v", documentExternalID, attachmentExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusWriteDBError, "save attachment failed")
|
||||
}
|
||||
|
||||
return &dto.AttachmentResponse{
|
||||
ExternalID: attachment.ExternalID,
|
||||
DocumentExternalID: documentExternalID,
|
||||
Name: attachment.Name,
|
||||
Size: attachment.Size,
|
||||
MimeType: attachment.MimeType,
|
||||
Path: attachment.Path,
|
||||
URL: storage.BuildPublicObjectPath(attachment.Path),
|
||||
CreatedAt: attachment.CreatedAt,
|
||||
UpdatedAt: attachment.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) ListAttachments(ctx context.Context, documentExternalID string) ([]*dto.AttachmentResponse, error) {
|
||||
if s == nil || strings.TrimSpace(documentExternalID) == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid list attachments request")
|
||||
}
|
||||
if config.GlobalConfig == nil || strings.TrimSpace(config.GlobalConfig.Minio.Bucket) == "" {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusServiceInternalError, "attachment storage is not configured")
|
||||
}
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
attachments, err := s.attachmentRepo.ListByDocumentID(documentID)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list attachments failed, document_external_id=%s, document_id=%d, err=%+v", documentExternalID, documentID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "list attachments failed")
|
||||
}
|
||||
|
||||
resp := make([]*dto.AttachmentResponse, 0, len(attachments))
|
||||
for _, attachment := range attachments {
|
||||
resp = append(resp, &dto.AttachmentResponse{
|
||||
ExternalID: attachment.ExternalID,
|
||||
DocumentExternalID: documentExternalID,
|
||||
Name: attachment.Name,
|
||||
Size: attachment.Size,
|
||||
MimeType: attachment.MimeType,
|
||||
Path: attachment.Path,
|
||||
URL: storage.BuildPublicObjectPath(attachment.Path),
|
||||
CreatedAt: attachment.CreatedAt,
|
||||
UpdatedAt: attachment.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) DeleteAttachment(ctx context.Context, documentExternalID, attachmentExternalID string) error {
|
||||
if s == nil || strings.TrimSpace(documentExternalID) == "" || strings.TrimSpace(attachmentExternalID) == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid delete attachment request")
|
||||
}
|
||||
if config.GlobalConfig == nil || strings.TrimSpace(config.GlobalConfig.Minio.Bucket) == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "attachment storage is not configured")
|
||||
}
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
attachment, err := s.attachmentRepo.GetByExternalIDAndDocumentID(attachmentExternalID, documentID)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query attachment failed, document_external_id=%s, attachment_external_id=%s, err=%+v", documentExternalID, attachmentExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "attachment not found")
|
||||
}
|
||||
|
||||
if err = storage.DeleteFile(config.GlobalConfig.Minio.Bucket, attachment.Path); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] delete attachment object failed, attachment_external_id=%s, path=%s, err=%+v", attachmentExternalID, attachment.Path, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusServiceInternalError, "delete attachment failed")
|
||||
}
|
||||
if err = s.attachmentRepo.DeleteByID(attachment.ID); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] delete attachment meta failed, attachment_external_id=%s, err=%+v", attachmentExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "delete attachment failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveAttachmentExt(filename, contentType string) string {
|
||||
ext := strings.TrimSpace(strings.ToLower(filepath.Ext(filename)))
|
||||
if ext != "" {
|
||||
return ext
|
||||
}
|
||||
exts, err := mime.ExtensionsByType(contentType)
|
||||
if err == nil && len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
return ".bin"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/document_repo"
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) ConvertToDocumentResponse(doc *model.Document) *dto.DocumentMetaResponse {
|
||||
return dto.NewDocumentMetaResponseFromModel(doc)
|
||||
}
|
||||
|
||||
func (s *DocumentService) buildListDocumentsOperation(req *internal_dto.DocumentsListReq) (*document_repo.DocumentsListOperation, error) {
|
||||
if s == nil || s.documentRepo == nil {
|
||||
return nil, status.StatusServiceInternalError
|
||||
}
|
||||
if req == nil {
|
||||
return nil, status.StatusInternalParamsError
|
||||
}
|
||||
listOp := s.documentRepo.ListDocuments(&model.Document{})
|
||||
if req.MetaArgs != nil {
|
||||
listOp = listOp.WithUserID(req.MetaArgs.UserID).
|
||||
WithParentID(req.MetaArgs.ParentID).
|
||||
WithKnowledgeID(req.MetaArgs.KnowledgeID).
|
||||
WithListOwnDoc(req.ListOwnDoc).
|
||||
WithDirectoryOnly(req.DirectoryOnly)
|
||||
}
|
||||
if req.SearchDocIDs != nil {
|
||||
listOp = listOp.WithIDs(req.SearchDocIDs)
|
||||
}
|
||||
if req.FilterArgs != nil {
|
||||
titleKeyword := req.FilterArgs.TitleKeyword
|
||||
if req.SearchDocIDs != nil {
|
||||
titleKeyword = nil
|
||||
}
|
||||
listOp = listOp.WithTitleKeyword(titleKeyword).
|
||||
WithType(req.FilterArgs.DocType).
|
||||
WithTags(req.FilterArgs.Tags).
|
||||
WithCreateTimeRange(req.FilterArgs.CreateTimeRangeStart, req.FilterArgs.CreateTimeRangeEnd).
|
||||
WithUpdateTimeRange(req.FilterArgs.UpdateTimeRangeStart, req.FilterArgs.UpdateTimeRangeEnd)
|
||||
}
|
||||
listOp = listOp.WithSort(req.SortArgs.Field, req.SortArgs.Desc).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize)
|
||||
|
||||
return listOp, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) buildDocumentTree(rootDocs []model.Document, allDescendants []*model.Document) []*dto.DocumentMetaResponse {
|
||||
docMap := make(map[int64]*dto.DocumentMetaResponse)
|
||||
var rootResponses []*dto.DocumentMetaResponse
|
||||
|
||||
for i := range rootDocs {
|
||||
resp := s.ConvertToDocumentResponse(&rootDocs[i])
|
||||
docMap[rootDocs[i].ID] = resp
|
||||
rootResponses = append(rootResponses, resp)
|
||||
}
|
||||
|
||||
for _, doc := range allDescendants {
|
||||
childResp := s.ConvertToDocumentResponse(doc)
|
||||
docMap[doc.ID] = childResp
|
||||
|
||||
if doc.ParentID != 0 {
|
||||
parentResp, exists := docMap[doc.ParentID]
|
||||
if exists {
|
||||
parentResp.Children = append(parentResp.Children, childResp)
|
||||
parentResp.HasChildren = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootResponses
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/domain_event"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) Create(ctx context.Context, req *dto.CreateDocumentRequest) (*dto.CreateDocumentResponse, error) {
|
||||
if err := validateCreateDocumentReq(req); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] validateCreateDocumentReq failed, err=%+v", err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "invalid create document request")
|
||||
}
|
||||
|
||||
// TODO: redis support
|
||||
docExternalID := utils.GenerateExternalID(utils.ExternalIDContextDocument)
|
||||
err := utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
docModel := &model.Document{
|
||||
ExternalID: docExternalID,
|
||||
Title: req.Title,
|
||||
Type: doc_type_mapper.ToModelType(req.Type),
|
||||
ContainerType: doc_container_mapper.ToModelType(req.ContainerType),
|
||||
Summary: "",
|
||||
IsPublic: req.IsPublic,
|
||||
Content: "",
|
||||
Path: "0",
|
||||
Depth: 0,
|
||||
}
|
||||
|
||||
// query user_id
|
||||
userID, err := s.userRepo.GetIDByExternalID(*req.CreatorExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
docModel.UserID = userID
|
||||
|
||||
// query knowledge_id
|
||||
if req.ContainerType == dto.ContainerType_KnowledgeBase {
|
||||
kbID, err := s.kbRepo.GetIDByExternalID(*req.KnowledgeExternalID).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
docModel.KnowledgeID = &kbID
|
||||
}
|
||||
|
||||
// TODO: permission check for knowledgebase
|
||||
|
||||
// query parent_id
|
||||
if req.ParentExternalID != nil {
|
||||
parentDocID, err := s.documentRepo.GetIDByExternalID(*req.ParentExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
docModel.ParentID = parentDocID
|
||||
}
|
||||
|
||||
// apply template content if provided
|
||||
if req.TemplateID != nil && *req.TemplateID > 0 {
|
||||
tpl, err := s.templateRepo.GetByID(*req.TemplateID).WithTx(tx).Exec()
|
||||
if err != nil || tpl == nil {
|
||||
logrus.Errorf("template not found: err:%+v", err)
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
docModel.Content = tpl.Content
|
||||
}
|
||||
|
||||
// create document meta
|
||||
err = s.documentRepo.Create(docModel).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
status.GenErrWithCustomMsg(status.StatusWriteDBError, "create document meta failed")
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
if err := s.documentRepo.UpdatePathDepth(docModel.ID, docModel.ParentID).WithTx(tx).Exec(); err != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document path depth failed")
|
||||
}
|
||||
|
||||
// create doc version
|
||||
ver := &model.DocumentVersion{
|
||||
DocumentID: docModel.ID,
|
||||
Content: docModel.Content,
|
||||
UserID: userID,
|
||||
Title: docModel.Title,
|
||||
ChangeSummary: "Create the document",
|
||||
}
|
||||
err = s.docVersionRepo.Create(ver).WithTx(tx).Exec()
|
||||
if err != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create version failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] Create err: %+v", err)
|
||||
return nil, status.StatusWriteDBError
|
||||
}
|
||||
|
||||
if createdDoc, err := s.documentRepo.GetByExternalID(docExternalID).Exec(ctx); err == nil {
|
||||
if err := s.documentRepo.BumpSubtreeVersionsByPath(ctx, createdDoc.Path); err != nil {
|
||||
logrus.Warnf("bump subtree version failed: %v", err)
|
||||
}
|
||||
}
|
||||
if req.TemplateID != nil && req.CreatorExternalID != nil && *req.CreatorExternalID != "" {
|
||||
_ = s.CreateRecentTemplate(&dto.CreateRecentTemplateRequest{
|
||||
UserExternalID: *req.CreatorExternalID,
|
||||
TemplateID: *req.TemplateID,
|
||||
AccessTime: time.Now(),
|
||||
})
|
||||
}
|
||||
if err := domain_event.PublishDocumentUpsertEvent(ctx, docExternalID); err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] publish search sync event failed, external_id=%s, err=%+v", docExternalID, err)
|
||||
}
|
||||
|
||||
return &dto.CreateDocumentResponse{
|
||||
ExternalID: docExternalID,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) CreateRecentTemplate(req *dto.CreateRecentTemplateRequest) error {
|
||||
if req == nil || req.UserExternalID == "" || req.TemplateID <= 0 {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
err = s.templateRepo.CreateRecentTemplate(&model.RecentTemplate{
|
||||
UserID: userID,
|
||||
TemplateID: req.TemplateID,
|
||||
AccessedAt: req.AccessTime,
|
||||
}).Exec()
|
||||
if err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/template_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type templatePayload struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Content string `json:"content"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (s *DocumentService) CreateTemplate(ctx context.Context, req *dto.CreateTemplateRequest) error {
|
||||
if err := validateCreateTemplateReq(req); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] validateCreateTemplateReq failed, err=%+v", err)
|
||||
return status.GenErrWithCustomMsg(err, "invalid create template request")
|
||||
}
|
||||
_ = ctx
|
||||
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
var kbID *int64
|
||||
if template_container_mapper.RequiresKnowledgeBaseID(req.TargetContainer) {
|
||||
id, err := s.kbRepo.GetIDByExternalID(*req.KnowledgeBaseExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
kbID = &id
|
||||
}
|
||||
|
||||
name, description, content, tags := parseTemplateContent(req.TemplateContent)
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template content is empty")
|
||||
}
|
||||
|
||||
templateType := req.Type
|
||||
if !doc_type_mapper.IsSupportedDTOType(templateType) {
|
||||
templateType = doc_type_mapper.DefaultDTOType()
|
||||
}
|
||||
|
||||
scope := template_container_mapper.ToScope(req.TargetContainer)
|
||||
isPublic := template_container_mapper.ToIsPublic(req.TargetContainer)
|
||||
templateModel := &model.Template{
|
||||
Name: name,
|
||||
Description: description,
|
||||
DocumentType: doc_type_mapper.ToModelType(templateType),
|
||||
Content: content,
|
||||
UserID: userID,
|
||||
Scope: scope,
|
||||
KnowledgeBaseID: kbID,
|
||||
IsPublic: isPublic,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
if err := s.templateRepo.Create(templateModel).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTemplateContent(raw string) (name, description, content string, tags []string) {
|
||||
content = raw
|
||||
var payload templatePayload
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err == nil {
|
||||
if payload.Content != "" {
|
||||
content = payload.Content
|
||||
}
|
||||
name = payload.Name
|
||||
description = payload.Description
|
||||
tags = payload.Tags
|
||||
}
|
||||
if strings.TrimSpace(name) == "" {
|
||||
name = deriveTemplateName(content)
|
||||
}
|
||||
if len(name) > 100 {
|
||||
name = truncateRunes(name, 100)
|
||||
}
|
||||
return name, description, content, tags
|
||||
}
|
||||
|
||||
func deriveTemplateName(content string) string {
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
trimmed = strings.TrimLeft(trimmed, "#*-+ ")
|
||||
trimmed = strings.TrimSpace(trimmed)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
return "Untitled Template"
|
||||
}
|
||||
|
||||
func truncateRunes(s string, max int) string {
|
||||
if max <= 0 {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(s)
|
||||
if len(runes) <= max {
|
||||
return s
|
||||
}
|
||||
return string(runes[:max])
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/attachment_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/document_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/document_version_repo"
|
||||
doc_yjs_snapshot_repo "github.com/XingfenD/yoresee_doc/internal/repository/document_yjs_snapshot_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/knowledge_base_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/template_repo"
|
||||
"github.com/XingfenD/yoresee_doc/internal/repository/user_repo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DocumentService struct {
|
||||
db *gorm.DB
|
||||
documentRepo *document_repo.DocumentRepository
|
||||
userRepo *user_repo.UserRepository
|
||||
kbRepo *knowledge_base_repo.KnowledgeBaseRepository
|
||||
docVersionRepo *document_version_repo.DocumentVersionRepository
|
||||
snapshotRepo *doc_yjs_snapshot_repo.DocumentYjsSnapshotRepository
|
||||
templateRepo *template_repo.TemplateRepository
|
||||
attachmentRepo *attachment_repo.AttachmentRepository
|
||||
}
|
||||
|
||||
func NewDocumentService(repos *repository.Repositories) *DocumentService {
|
||||
return &DocumentService{
|
||||
db: repos.DB,
|
||||
documentRepo: repos.Document,
|
||||
userRepo: repos.User,
|
||||
kbRepo: repos.KnowledgeBase,
|
||||
docVersionRepo: repos.DocumentVersion,
|
||||
snapshotRepo: repos.DocumentYjsSnapshot,
|
||||
templateRepo: repos.Template,
|
||||
attachmentRepo: repos.Attachment,
|
||||
}
|
||||
}
|
||||
|
||||
var DocumentSvc *DocumentService
|
||||
|
||||
func (s *DocumentService) DeleteDocument(id int64) error {
|
||||
return s.documentRepo.Delete(id).Exec()
|
||||
}
|
||||
|
||||
func (s *DocumentService) DeleteDocumentByExternalID(ctx context.Context, externalID string) error {
|
||||
docID, err := s.documentRepo.GetIDByExternalID(externalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.DeleteDocument(docID)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *DocumentService) ListDocumentVersions(ctx context.Context, documentExternalID string, page, pageSize int32) ([]*dto.DocumentVersionResponse, int64, error) {
|
||||
if s == nil || strings.TrimSpace(documentExternalID) == "" {
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusParamError, "invalid list document versions request")
|
||||
}
|
||||
|
||||
p := int(page)
|
||||
if p <= 0 {
|
||||
p = 1
|
||||
}
|
||||
ps := int(pageSize)
|
||||
if ps <= 0 {
|
||||
ps = 20
|
||||
}
|
||||
if ps > 100 {
|
||||
ps = 100
|
||||
}
|
||||
offset := (p - 1) * ps
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id for list versions failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
items, total, err := s.docVersionRepo.ListByDocumentID(documentID, offset, ps)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list document versions failed, document_external_id=%s, document_id=%d, err=%+v", documentExternalID, documentID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusReadDBError, "list document versions failed")
|
||||
}
|
||||
|
||||
resp := make([]*dto.DocumentVersionResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
resp = append(resp, &dto.DocumentVersionResponse{
|
||||
Version: item.Version,
|
||||
Title: item.Title,
|
||||
ChangeSummary: item.ChangeSummary,
|
||||
CreatedAt: item.CreatedAt,
|
||||
})
|
||||
}
|
||||
return resp, total, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) GetDocumentVersionContent(ctx context.Context, documentExternalID string, version int32) (*dto.DocumentVersionResponse, error) {
|
||||
if s == nil || strings.TrimSpace(documentExternalID) == "" || version <= 0 {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "invalid get document version request")
|
||||
}
|
||||
|
||||
documentID, err := s.documentRepo.GetIDByExternalID(documentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query document id for get version failed, document_external_id=%s, err=%+v", documentExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "query document failed")
|
||||
}
|
||||
if documentID <= 0 {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
item, err := s.docVersionRepo.GetByDocumentIDAndVersion(documentID, int(version))
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] get document version failed, document_external_id=%s, document_id=%d, version=%d, err=%+v", documentExternalID, documentID, version, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document version not found")
|
||||
}
|
||||
|
||||
return &dto.DocumentVersionResponse{
|
||||
Version: item.Version,
|
||||
Title: item.Title,
|
||||
Content: item.Content,
|
||||
ChangeSummary: item.ChangeSummary,
|
||||
CreatedAt: item.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *DocumentService) GetDocumentByExternalID(ctx context.Context, externalID string) (*dto.DocumentResponse, error) {
|
||||
docModel, err := s.documentRepo.GetByExternalID(externalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] GetDocumentByExternalID failed, external_id=%s, err=%+v", externalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
return dto.NewDocumentResponseFromModel(docModel), nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *DocumentService) GetDocumentSettings(ctx context.Context, req *dto.GetDocumentSettingsRequest) (*dto.DocumentSettingsResponse, error) {
|
||||
if req == nil || req.ExternalID == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
|
||||
// Settings page expects real-time value after save/update, so read from DB directly
|
||||
// to avoid returning a stale cached model.
|
||||
docModel, err := s.documentRepo.GetByExternalID(req.ExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] GetDocumentSettings failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "document not found")
|
||||
}
|
||||
|
||||
// TODO: merge fields from document_setting table when that table is introduced.
|
||||
return &dto.DocumentSettingsResponse{
|
||||
IsPublic: docModel.IsPublic,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) GetTemplateByID(templateID int64) (*dto.TemplateResponse, error) {
|
||||
if templateID <= 0 {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
|
||||
tpl, err := s.templateRepo.GetByID(templateID).Exec()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
if tpl == nil {
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
|
||||
kbExternalID := ""
|
||||
if tpl.KnowledgeBaseID != nil {
|
||||
kbs, err := s.kbRepo.MGetKnowledgeBaseByIDs([]int64{*tpl.KnowledgeBaseID}).Exec()
|
||||
if err != nil {
|
||||
return nil, status.StatusReadDBError
|
||||
}
|
||||
if len(kbs) > 0 {
|
||||
kbExternalID = kbs[0].ExternalID
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.TemplateResponse{
|
||||
ID: tpl.ID,
|
||||
Name: tpl.Name,
|
||||
Description: tpl.Description,
|
||||
Content: tpl.Content,
|
||||
Type: doc_type_mapper.FromModelType(tpl.DocumentType),
|
||||
Scope: tpl.Scope,
|
||||
KnowledgeBaseExternalID: kbExternalID,
|
||||
Tags: tpl.Tags,
|
||||
CreatedAt: tpl.CreatedAt,
|
||||
UpdatedAt: tpl.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) GetDocumentTypeByExternalID(ctx context.Context, externalID string) (model.DocumentType, error) {
|
||||
docModel, err := s.documentRepo.GetByExternalID(externalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return "", status.StatusDocumentNotFound
|
||||
}
|
||||
return docModel.Type, nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
)
|
||||
|
||||
func (s *DocumentService) listDocuments(ctx context.Context, req *internal_dto.DocumentsListReq) ([]*dto.DocumentMetaResponse, int64, error) {
|
||||
listOp, err := s.buildListDocumentsOperation(req)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] buildListDocumentsOperation failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build document list operation failed")
|
||||
}
|
||||
models, total, err := listOp.ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list documents failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list documents failed")
|
||||
}
|
||||
|
||||
// if children is not needed or depth less than 1, return directly
|
||||
if req.Options == nil || !req.Options.IncludeChildren || (req.Options.Depth != nil && *req.Options.Depth < 1) {
|
||||
return s.buildDocumentTree(models, nil), total, nil
|
||||
}
|
||||
|
||||
parentIDs := make([]int64, len(models))
|
||||
for i := range models {
|
||||
parentIDs[i] = models[i].ID
|
||||
}
|
||||
|
||||
getOp := s.getDocumentsWithDescendants(parentIDs).
|
||||
WithDirectoryOnly(req.DirectoryOnly)
|
||||
if req.Options != nil {
|
||||
getOp = getOp.WithDepth(req.Options.Depth)
|
||||
}
|
||||
|
||||
allDescendants, err := getOp.Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] getDocumentsWithDescendants failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list document descendants failed")
|
||||
}
|
||||
|
||||
docs := s.buildDocumentTree(models, allDescendants)
|
||||
return docs, total, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) ListDocumentsByExternal(ctx context.Context, req *dto.ListDocumentsByExternalRequest) ([]*dto.DocumentMetaResponse, int64, error) {
|
||||
var userID *int64
|
||||
if req == nil {
|
||||
return nil, 0, status.StatusInternalParamsError
|
||||
}
|
||||
if req.ExternalArgs != nil && req.ExternalArgs.UserExternalID != nil && *req.ExternalArgs.UserExternalID != "" {
|
||||
id, err := s.userRepo.GetIDByExternalID(*req.ExternalArgs.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query user id by external id failed, external_id=%s, err=%+v", *req.ExternalArgs.UserExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusUserNotFound, "user not found")
|
||||
}
|
||||
userID = &id
|
||||
}
|
||||
|
||||
var parentID int64
|
||||
if req.ExternalArgs != nil && req.ExternalArgs.RootDocumentExternalID != nil && *req.ExternalArgs.RootDocumentExternalID != "" {
|
||||
doc, err := s.documentRepo.GetByExternalID(*req.ExternalArgs.RootDocumentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query root document failed, external_id=%s, err=%+v", *req.ExternalArgs.RootDocumentExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusDocumentNotFound, "root document not found")
|
||||
}
|
||||
parentID = doc.ID
|
||||
}
|
||||
|
||||
var knowledgeID *int64
|
||||
if req.ExternalArgs != nil && req.ExternalArgs.KnowledgeExternalID != nil && *req.ExternalArgs.KnowledgeExternalID != "" {
|
||||
id, err := s.kbRepo.GetIDByExternalID(*req.ExternalArgs.KnowledgeExternalID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query knowledge base id failed, external_id=%s, err=%+v", *req.ExternalArgs.KnowledgeExternalID, err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusKnowledgeBaseNotFound, "knowledge base not found")
|
||||
}
|
||||
knowledgeID = &id
|
||||
}
|
||||
|
||||
internalReq := &internal_dto.DocumentsListReq{
|
||||
MetaArgs: &internal_dto.DocumentsListMetaArgs{
|
||||
UserID: userID,
|
||||
ParentID: &parentID, // default to root
|
||||
KnowledgeID: knowledgeID,
|
||||
},
|
||||
ListDocumentsBaseArgs: dto.ListDocumentsBaseArgs{
|
||||
ListOwnDoc: req.ListOwnDoc,
|
||||
DirectoryOnly: req.DirectoryOnly,
|
||||
},
|
||||
FilterArgs: req.FilterArgs,
|
||||
SortArgs: req.SortArgs,
|
||||
Pagination: req.Pagination,
|
||||
Options: req.Options,
|
||||
}
|
||||
s.applyElasticsearchKeywordFilter(ctx, internalReq)
|
||||
|
||||
return s.listDocuments(ctx, internalReq)
|
||||
}
|
||||
|
||||
type GetDocumentsWithDescendantsOption struct {
|
||||
DirectoryOnly bool
|
||||
}
|
||||
|
||||
type GetDocumentsWithDescendantsOperation struct {
|
||||
svc *DocumentService
|
||||
parentIDs []int64
|
||||
depth *int
|
||||
|
||||
directoryOnly bool
|
||||
}
|
||||
|
||||
func (s *DocumentService) getDocumentsWithDescendants(parentIDs []int64) *GetDocumentsWithDescendantsOperation {
|
||||
return &GetDocumentsWithDescendantsOperation{
|
||||
svc: s,
|
||||
parentIDs: parentIDs,
|
||||
}
|
||||
}
|
||||
|
||||
func (op *GetDocumentsWithDescendantsOperation) WithDepth(depth *int) *GetDocumentsWithDescendantsOperation {
|
||||
op.depth = depth
|
||||
return op
|
||||
}
|
||||
|
||||
func (op *GetDocumentsWithDescendantsOperation) WithDirectoryOnly(with bool) *GetDocumentsWithDescendantsOperation {
|
||||
op.directoryOnly = with
|
||||
return op
|
||||
}
|
||||
|
||||
func (op *GetDocumentsWithDescendantsOperation) Exec(ctx context.Context) ([]*model.Document, error) {
|
||||
if len(op.parentIDs) == 0 {
|
||||
return []*model.Document{}, nil
|
||||
}
|
||||
|
||||
allDocs := make([]*model.Document, 0)
|
||||
seen := make(map[int64]bool)
|
||||
|
||||
for _, rootParentID := range op.parentIDs {
|
||||
docs, err := op.svc.documentRepo.GetSubtree(rootParentID).
|
||||
WithDepth(op.depth).
|
||||
WithDirectoryOnly(op.directoryOnly).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] get subtree failed, root_parent_id=%d, err=%+v", rootParentID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "get document subtree failed")
|
||||
}
|
||||
|
||||
for _, doc := range docs {
|
||||
if !seen[doc.ID] {
|
||||
seen[doc.ID] = true
|
||||
allDocs = append(allDocs, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allDocs, nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) ListRecentTemplates(req *dto.ListRecentTemplatesRequest) ([]*dto.TemplateResponse, int64, error) {
|
||||
if req == nil || req.UserExternalID == "" {
|
||||
return nil, 0, status.StatusInternalParamsError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusUserNotFound
|
||||
}
|
||||
|
||||
// TODO: filter the recent templates by kb
|
||||
recentRecords, total, err := s.templateRepo.ListRecentTemplates(userID).
|
||||
WithTimeRange(req.StartTime, req.EndTime).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
if len(recentRecords) == 0 {
|
||||
return []*dto.TemplateResponse{}, total, nil
|
||||
}
|
||||
|
||||
tplIDs := make([]int64, 0, len(recentRecords))
|
||||
for _, r := range recentRecords {
|
||||
tplIDs = append(tplIDs, r.TemplateID)
|
||||
}
|
||||
|
||||
tplModels, err := s.templateRepo.MGetByIDs(tplIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
tplMap := make(map[int64]*model.Template, len(tplModels))
|
||||
for _, tpl := range tplModels {
|
||||
tplMap[tpl.ID] = tpl
|
||||
}
|
||||
|
||||
// map knowledge_base external ids
|
||||
kbExternalIDMap := make(map[int64]string)
|
||||
kbIDs := make([]int64, 0)
|
||||
for _, tpl := range tplModels {
|
||||
if tpl.KnowledgeBaseID == nil || *tpl.KnowledgeBaseID == 0 {
|
||||
continue
|
||||
}
|
||||
kbIDs = append(kbIDs, *tpl.KnowledgeBaseID)
|
||||
}
|
||||
if len(kbIDs) > 0 {
|
||||
kbs, err := s.kbRepo.MGetKnowledgeBaseByIDs(kbIDs).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
for _, kb := range kbs {
|
||||
kbExternalIDMap[kb.ID] = kb.ExternalID
|
||||
}
|
||||
}
|
||||
|
||||
resp := make([]*dto.TemplateResponse, 0, len(recentRecords))
|
||||
for _, record := range recentRecords {
|
||||
tpl := tplMap[record.TemplateID]
|
||||
if tpl == nil {
|
||||
continue
|
||||
}
|
||||
kbExternalID := ""
|
||||
if tpl.KnowledgeBaseID != nil {
|
||||
kbExternalID = kbExternalIDMap[*tpl.KnowledgeBaseID]
|
||||
}
|
||||
resp = append(resp, &dto.TemplateResponse{
|
||||
ID: tpl.ID,
|
||||
Name: tpl.Name,
|
||||
Description: tpl.Description,
|
||||
Content: tpl.Content,
|
||||
Type: doc_type_mapper.FromModelType(tpl.DocumentType),
|
||||
Scope: tpl.Scope,
|
||||
KnowledgeBaseExternalID: kbExternalID,
|
||||
Tags: tpl.Tags,
|
||||
CreatedAt: tpl.CreatedAt,
|
||||
UpdatedAt: tpl.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return resp, total, nil
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/template_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/bytedance/gg/gslice"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type templateListOperation struct {
|
||||
req *internal_dto.TemplateListReq
|
||||
srvc *DocumentService
|
||||
}
|
||||
|
||||
func (s *DocumentService) listTemplates(req *internal_dto.TemplateListReq) *templateListOperation {
|
||||
return &templateListOperation{
|
||||
req: req,
|
||||
srvc: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (op *templateListOperation) ExecWithTotal() ([]*dto.TemplateResponse, int64, error) {
|
||||
listOp, err := op.srvc.buildListTemplateOperation(op.req)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] buildListTemplateOperation failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build template list operation failed")
|
||||
}
|
||||
templates, total, err := listOp.ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list templates failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list templates failed")
|
||||
}
|
||||
|
||||
kbExternalIDMap := make(map[int64]string)
|
||||
kbIDs := make([]int64, 0)
|
||||
for _, tmpl := range templates {
|
||||
if tmpl.KnowledgeBaseID == nil || *tmpl.KnowledgeBaseID == 0 {
|
||||
continue
|
||||
}
|
||||
kbIDs = append(kbIDs, *tmpl.KnowledgeBaseID)
|
||||
}
|
||||
kbIDs = gslice.Uniq(kbIDs)
|
||||
if len(kbIDs) > 0 {
|
||||
kbs, err := op.srvc.kbRepo.MGetKnowledgeBaseByIDs(kbIDs).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] list templates get knowledge bases failed: %+v", err)
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
for _, kb := range kbs {
|
||||
kbExternalIDMap[kb.ID] = kb.ExternalID
|
||||
}
|
||||
}
|
||||
|
||||
resp := make([]*dto.TemplateResponse, 0, len(templates))
|
||||
for _, tmpl := range templates {
|
||||
kbExternalID := ""
|
||||
if tmpl.KnowledgeBaseID != nil {
|
||||
kbExternalID = kbExternalIDMap[*tmpl.KnowledgeBaseID]
|
||||
}
|
||||
resp = append(resp, &dto.TemplateResponse{
|
||||
ID: tmpl.ID,
|
||||
Name: tmpl.Name,
|
||||
Description: tmpl.Description,
|
||||
Content: tmpl.Content,
|
||||
Type: doc_type_mapper.FromModelType(tmpl.DocumentType),
|
||||
Scope: tmpl.Scope,
|
||||
KnowledgeBaseExternalID: kbExternalID,
|
||||
Tags: tmpl.Tags,
|
||||
CreatedAt: tmpl.CreatedAt,
|
||||
UpdatedAt: tmpl.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return resp, total, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) ListTemplatesByExternal(req *dto.TemplateListByExternalRequest) ([]*dto.TemplateResponse, int64, error) {
|
||||
var creatorID *int64
|
||||
if req.CreatorExternalID != "" {
|
||||
id, err := s.userRepo.GetIDByExternalID(req.CreatorExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusUserNotFound
|
||||
}
|
||||
creatorID = &id
|
||||
}
|
||||
|
||||
internalReq, err := buildTemplateListReqFromExternal(req, creatorID)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] buildTemplateListReqFromExternal failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "build template list request failed")
|
||||
}
|
||||
list, total, err := s.listTemplates(internalReq).ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] ListTemplatesByExternal failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(err, "list templates failed")
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func buildTemplateListReqFromExternal(req *dto.TemplateListByExternalRequest, creatorID *int64) (*internal_dto.TemplateListReq, error) {
|
||||
filter := &dto.TemplateListFilterArgs{}
|
||||
if req.FilterArgs != nil {
|
||||
*filter = *req.FilterArgs
|
||||
}
|
||||
|
||||
return &internal_dto.TemplateListReq{
|
||||
CreatorID: creatorID,
|
||||
FilterArgs: filter,
|
||||
SortArgs: req.SortArgs,
|
||||
Pagination: req.Pagination,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) buildListTemplateOperation(req *internal_dto.TemplateListReq) (*templateListOperationBuilder, error) {
|
||||
builder := &templateListOperationBuilder{
|
||||
req: req,
|
||||
srvc: s,
|
||||
}
|
||||
return builder, nil
|
||||
}
|
||||
|
||||
type templateListOperationBuilder struct {
|
||||
req *internal_dto.TemplateListReq
|
||||
srvc *DocumentService
|
||||
}
|
||||
|
||||
func (b *templateListOperationBuilder) ExecWithTotal() ([]*model.Template, int64, error) {
|
||||
op := b.srvc.templateRepo.List(&model.Template{})
|
||||
|
||||
if b.req.CreatorID != nil {
|
||||
op = op.WithUserID(b.req.CreatorID)
|
||||
}
|
||||
|
||||
if b.req.FilterArgs != nil {
|
||||
if b.req.FilterArgs.TargetContainer != nil {
|
||||
if template_container_mapper.RequiresKnowledgeBaseID(*b.req.FilterArgs.TargetContainer) &&
|
||||
(b.req.FilterArgs.KnowledgeBaseID == nil || *b.req.FilterArgs.KnowledgeBaseID == "") {
|
||||
return nil, 0, status.StatusParamError
|
||||
}
|
||||
scope := template_container_mapper.ToScope(*b.req.FilterArgs.TargetContainer)
|
||||
op = op.WithScope(&scope)
|
||||
}
|
||||
if b.req.FilterArgs.KnowledgeBaseID != nil && *b.req.FilterArgs.KnowledgeBaseID != "" {
|
||||
kbID, err := b.srvc.kbRepo.GetIDByExternalID(*b.req.FilterArgs.KnowledgeBaseID).Exec()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] Get knowledge base id for template list failed, external_id=%s, err=%+v", *b.req.FilterArgs.KnowledgeBaseID, err)
|
||||
return nil, 0, status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
op = op.WithKnowledgeBaseID(&kbID)
|
||||
}
|
||||
if b.req.FilterArgs.NameKeyword != nil {
|
||||
op = op.WithNameKeyword(b.req.FilterArgs.NameKeyword)
|
||||
}
|
||||
if b.req.FilterArgs.Type != nil {
|
||||
modelType := model.DocumentType(doc_type_mapper.ToModelType(*b.req.FilterArgs.Type))
|
||||
op = op.WithDocumentType(&modelType)
|
||||
}
|
||||
}
|
||||
|
||||
sortField := b.req.SortArgs.Field
|
||||
sortDesc := b.req.SortArgs.Desc
|
||||
if sortField == "" {
|
||||
sortField = "created_at"
|
||||
sortDesc = true
|
||||
}
|
||||
op = op.WithSort(sortField, sortDesc)
|
||||
|
||||
if b.req.Pagination.Page > 0 && b.req.Pagination.PageSize > 0 {
|
||||
op = op.WithPagination(b.req.Pagination.Page, b.req.Pagination.PageSize)
|
||||
}
|
||||
|
||||
templates, total, err := op.ExecWithTotal()
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] template repository list failed, err=%+v", err)
|
||||
return nil, 0, status.GenErrWithCustomMsg(status.StatusReadDBError, "list templates failed")
|
||||
}
|
||||
return templates, total, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func (s *DocumentService) RecordRecentDocument(req *dto.RecordRecentDocumentRequest) error {
|
||||
if req == nil || req.UserExternalID == "" || req.DocumentExternalID == "" {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
docID, err := s.documentRepo.GetIDByExternalID(req.DocumentExternalID).Exec(context.Background())
|
||||
if err != nil || docID == 0 {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
if err := s.documentRepo.UpsertRecentDocument(&model.RecentDocument{
|
||||
UserID: userID,
|
||||
DocumentID: docID,
|
||||
AccessedAt: time.Now(),
|
||||
}).Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) ListRecentDocuments(req *dto.ListRecentDocumentsRequest) ([]*dto.DocumentResponse, int64, error) {
|
||||
if req == nil || req.UserExternalID == "" {
|
||||
return nil, 0, status.StatusInternalParamsError
|
||||
}
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusUserNotFound
|
||||
}
|
||||
|
||||
recentRecords, total, err := s.documentRepo.ListRecentDocuments(userID).
|
||||
WithTimeRange(req.StartTime, req.EndTime).
|
||||
WithPagination(req.Pagination.Page, req.Pagination.PageSize).
|
||||
Exec()
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
if len(recentRecords) == 0 {
|
||||
return []*dto.DocumentResponse{}, total, nil
|
||||
}
|
||||
|
||||
docIDs := make([]int64, 0, len(recentRecords))
|
||||
for _, record := range recentRecords {
|
||||
docIDs = append(docIDs, record.DocumentID)
|
||||
}
|
||||
|
||||
docModels, err := s.documentRepo.MGetByIDs(docIDs)
|
||||
if err != nil {
|
||||
return nil, 0, status.StatusReadDBError
|
||||
}
|
||||
|
||||
resp := make([]*dto.DocumentResponse, 0, len(docModels))
|
||||
for _, doc := range docModels {
|
||||
resp = append(resp, dto.NewDocumentResponseFromModel(doc))
|
||||
}
|
||||
|
||||
return resp, total, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/config"
|
||||
"github.com/XingfenD/yoresee_doc/internal/search"
|
||||
internal_dto "github.com/XingfenD/yoresee_doc/internal/service/dto"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/storage"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *DocumentService) applyElasticsearchKeywordFilter(ctx context.Context, req *internal_dto.DocumentsListReq) {
|
||||
if req == nil || req.FilterArgs == nil || req.FilterArgs.TitleKeyword == nil {
|
||||
return
|
||||
}
|
||||
keyword := strings.TrimSpace(*req.FilterArgs.TitleKeyword)
|
||||
if keyword == "" {
|
||||
return
|
||||
}
|
||||
if config.GlobalConfig == nil || !config.GlobalConfig.Elasticsearch.Enabled {
|
||||
logrus.Warnf("[Service layer: DocumentService] elasticsearch is disabled, degrade to db keyword search, keyword=%s", keyword)
|
||||
return
|
||||
}
|
||||
if storage.ES == nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] elasticsearch client is nil, degrade to db keyword search, keyword=%s", keyword)
|
||||
return
|
||||
}
|
||||
|
||||
searchReq := search.DocumentSearchRequest{
|
||||
Keyword: keyword,
|
||||
DocType: req.FilterArgs.DocType,
|
||||
Tags: req.FilterArgs.Tags,
|
||||
CreateTimeRangeStart: req.FilterArgs.CreateTimeRangeStart,
|
||||
CreateTimeRangeEnd: req.FilterArgs.CreateTimeRangeEnd,
|
||||
UpdateTimeRangeStart: req.FilterArgs.UpdateTimeRangeStart,
|
||||
UpdateTimeRangeEnd: req.FilterArgs.UpdateTimeRangeEnd,
|
||||
Size: 5000,
|
||||
ListOwnDoc: req.ListOwnDoc,
|
||||
}
|
||||
if req.MetaArgs != nil {
|
||||
searchReq.UserID = req.MetaArgs.UserID
|
||||
searchReq.KnowledgeID = req.MetaArgs.KnowledgeID
|
||||
}
|
||||
|
||||
ids, err := search.SearchDocumentIDs(ctx, searchReq)
|
||||
if err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] elasticsearch keyword search failed, keyword=%s, err=%+v", keyword, err)
|
||||
return
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
logrus.Warnf("[Service layer: DocumentService] elasticsearch keyword search no hit, degrade to db keyword search, keyword=%s", keyword)
|
||||
return
|
||||
}
|
||||
req.SearchDocIDs = ids
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/cache"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/key"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) SaveDocumentYjsSnapshot(ctx context.Context, docExternalID string, state []byte) error {
|
||||
if len(state) == 0 {
|
||||
return status.StatusParamError
|
||||
}
|
||||
docID, err := s.documentRepo.GetIDByExternalID(docExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
if err := s.snapshotRepo.Save(docID, state).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] SaveDocumentYjsSnapshot failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "save document snapshot failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) SaveDocumentSnapshotAndContent(ctx context.Context, docExternalID string, state []byte, content string) (bool, error) {
|
||||
if len(state) == 0 {
|
||||
return false, status.StatusParamError
|
||||
}
|
||||
|
||||
contentChanged := false
|
||||
cacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, docExternalID)
|
||||
err := cache.DoubleDelete(
|
||||
context.Background(),
|
||||
func() error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
docModel, err := s.documentRepo.GetByExternalID(docExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
if err := s.snapshotRepo.Save(docModel.ID, state).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] SaveDocumentSnapshotAndContent save snapshot failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docModel.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "save document snapshot failed")
|
||||
}
|
||||
|
||||
if docModel.Content == content {
|
||||
return nil
|
||||
}
|
||||
|
||||
versionModel := &model.DocumentVersion{
|
||||
DocumentID: docModel.ID,
|
||||
Title: docModel.Title,
|
||||
Content: docModel.Content,
|
||||
UserID: docModel.UserID,
|
||||
ChangeSummary: "Sync collab snapshot",
|
||||
}
|
||||
if err := s.docVersionRepo.Create(versionModel).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] SaveDocumentSnapshotAndContent create version failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docModel.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create document version failed")
|
||||
}
|
||||
|
||||
nextDoc := &model.Document{
|
||||
ID: docModel.ID,
|
||||
Content: content,
|
||||
}
|
||||
if err := s.documentRepo.Update(nextDoc).WithTx(tx).UpdateContent().Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] SaveDocumentSnapshotAndContent update content failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docModel.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document content failed")
|
||||
}
|
||||
contentChanged = true
|
||||
return nil
|
||||
})
|
||||
},
|
||||
cacheKey,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return contentChanged, nil
|
||||
}
|
||||
|
||||
func (s *DocumentService) GetDocumentYjsSnapshot(ctx context.Context, docExternalID string) ([]byte, error) {
|
||||
docID, err := s.documentRepo.GetIDByExternalID(docExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
snapshot, err := s.snapshotRepo.GetByDocID(docID).Exec()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
logrus.Errorf("[Service layer: DocumentService] GetDocumentYjsSnapshot failed, doc_external_id=%s, doc_id=%d, err=%+v", docExternalID, docID, err)
|
||||
return nil, status.GenErrWithCustomMsg(status.StatusReadDBError, "get document snapshot failed")
|
||||
}
|
||||
return snapshot.YjsState, nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/domain_event"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/cache"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/key"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) Update(ctx context.Context, req *dto.UpdateDocumentRequest) (bool, error) {
|
||||
if err := validateUpdateDocumentReq(req); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] validateUpdateDocumentReq failed, err=%+v", err)
|
||||
return false, status.GenErrWithCustomMsg(err, "invalid update document request")
|
||||
}
|
||||
|
||||
var (
|
||||
moved bool
|
||||
oldPath string
|
||||
newPath string
|
||||
)
|
||||
|
||||
cacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, req.ExternalID)
|
||||
err := cache.DoubleDelete(
|
||||
context.Background(),
|
||||
func() error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
oldDoc, err := s.documentRepo.GetByExternalID(req.ExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
oldPath = oldDoc.Path
|
||||
|
||||
docModel := &model.Document{
|
||||
ID: oldDoc.ID,
|
||||
}
|
||||
op := s.documentRepo.Update(docModel).WithTx(tx)
|
||||
if req.Content != nil {
|
||||
docModel.Content = *req.Content
|
||||
op.UpdateContent()
|
||||
}
|
||||
if req.Title != nil {
|
||||
docModel.Title = *req.Title
|
||||
}
|
||||
|
||||
var newParentID int64
|
||||
if req.ParentExternalID != nil {
|
||||
parentID, err := s.documentRepo.GetIDByExternalID(*req.ParentExternalID).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
docModel.ParentID = parentID
|
||||
newParentID = parentID
|
||||
moved = true
|
||||
op = op.UpdateParentID()
|
||||
}
|
||||
|
||||
// create version
|
||||
versionModel := &model.DocumentVersion{
|
||||
DocumentID: oldDoc.ID,
|
||||
Title: oldDoc.Title,
|
||||
Content: oldDoc.Content,
|
||||
UserID: oldDoc.UserID,
|
||||
ChangeSummary: "",
|
||||
}
|
||||
|
||||
if err := s.docVersionRepo.Create(versionModel).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] create document version failed, document_id=%d, err=%+v", oldDoc.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create document version failed")
|
||||
}
|
||||
|
||||
// update container relation
|
||||
if req.MoveToContainer != nil {
|
||||
if !doc_container_mapper.IsSupportedDTOType(*req.MoveToContainer) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid move_to_container")
|
||||
}
|
||||
if doc_container_mapper.RequiresKnowledgeBaseID(*req.MoveToContainer) {
|
||||
kbID, err := s.kbRepo.GetIDByExternalID(*req.KnowledgeBaseExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusKnowledgeBaseNotFound
|
||||
}
|
||||
docModel.KnowledgeID = &kbID
|
||||
} else {
|
||||
docModel.KnowledgeID = nil
|
||||
}
|
||||
docModel.ContainerType = doc_container_mapper.ToModelType(*req.MoveToContainer)
|
||||
op = op.UpdateKnowledgeID().UpdateContainerType()
|
||||
}
|
||||
|
||||
if err := op.Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] update document failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document failed")
|
||||
}
|
||||
if moved {
|
||||
if err := s.documentRepo.MoveSubtree(oldDoc.ID, newParentID).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] move subtree failed, document_id=%d, new_parent_id=%d, err=%+v", oldDoc.ID, newParentID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "move document subtree failed")
|
||||
}
|
||||
path, err := s.documentRepo.GetPathByIDWithTx(tx, oldDoc.ID)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] query new path failed, document_id=%d, err=%+v", oldDoc.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusReadDBError, "query document path failed")
|
||||
}
|
||||
newPath = path
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
cacheKey,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] Update failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return false, status.GenErrWithCustomMsg(err, "update document failed")
|
||||
}
|
||||
|
||||
if moved {
|
||||
if err := s.documentRepo.BumpSubtreeVersionsByPath(ctx, oldPath); err != nil {
|
||||
logrus.Warnf("bump subtree version failed: %v", err)
|
||||
return true, nil
|
||||
}
|
||||
if newPath != "" && newPath != oldPath {
|
||||
if err := s.documentRepo.BumpSubtreeVersionsByPath(ctx, newPath); err != nil {
|
||||
logrus.Warnf("bump subtree version failed: %v", err)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := domain_event.PublishDocumentUpsertEvent(ctx, req.ExternalID); err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] publish search sync event failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/domain_event"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/cache"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/key"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) UpdateDocumentMeta(ctx context.Context, req *dto.UpdateDocumentMetaRequest) (bool, error) {
|
||||
if err := validateUpdateDocumentMetaReq(req); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] validateUpdateDocumentMetaReq failed, err=%+v", err)
|
||||
return false, status.GenErrWithCustomMsg(err, "invalid update document meta request")
|
||||
}
|
||||
|
||||
cacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, req.ExternalID)
|
||||
err := cache.DoubleDelete(
|
||||
context.Background(),
|
||||
func() error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
oldDoc, err := s.documentRepo.GetByExternalID(req.ExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
docModel := &model.Document{ID: oldDoc.ID}
|
||||
op := s.documentRepo.Update(docModel).WithTx(tx)
|
||||
|
||||
if req.Title != nil {
|
||||
docModel.Title = *req.Title
|
||||
op.UpdateTitle()
|
||||
}
|
||||
if req.Summary != nil {
|
||||
docModel.Summary = *req.Summary
|
||||
op.UpdateSummary()
|
||||
}
|
||||
if req.Tags != nil {
|
||||
docModel.Tags = *req.Tags
|
||||
op.UpdateTags()
|
||||
}
|
||||
|
||||
if err := op.Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] update document meta failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document meta failed")
|
||||
}
|
||||
|
||||
versionModel := &model.DocumentVersion{
|
||||
DocumentID: oldDoc.ID,
|
||||
Title: oldDoc.Title,
|
||||
Content: oldDoc.Content,
|
||||
UserID: oldDoc.UserID,
|
||||
ChangeSummary: "Update document meta",
|
||||
}
|
||||
if err := s.docVersionRepo.Create(versionModel).WithTx(tx).Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] create document version failed, document_id=%d, err=%+v", oldDoc.ID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "create document version failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
cacheKey,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] UpdateDocumentMeta failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return false, status.GenErrWithCustomMsg(err, "update document meta failed")
|
||||
}
|
||||
|
||||
if err := domain_event.PublishDocumentUpsertEvent(ctx, req.ExternalID); err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] publish search sync event failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/domain_event"
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/model"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"github.com/XingfenD/yoresee_doc/internal/utils"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/cache"
|
||||
"github.com/XingfenD/yoresee_doc/pkg/key"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) UpdateDocumentSettings(ctx context.Context, req *dto.UpdateDocumentSettingsRequest) (*dto.DocumentSettingsResponse, error) {
|
||||
if req == nil || req.ExternalID == "" {
|
||||
return nil, status.StatusParamError
|
||||
}
|
||||
|
||||
cacheKey := key.KeyModelByExternalID(key.KeyObjectTypeEnum_Doc, req.ExternalID)
|
||||
err := cache.DoubleDelete(
|
||||
context.Background(),
|
||||
func() error {
|
||||
return utils.WithTransaction(s.db, func(tx *gorm.DB) error {
|
||||
oldDoc, err := s.documentRepo.GetByExternalID(req.ExternalID).WithTx(tx).Exec(ctx)
|
||||
if err != nil {
|
||||
return status.StatusDocumentNotFound
|
||||
}
|
||||
|
||||
docModel := &model.Document{
|
||||
ID: oldDoc.ID,
|
||||
IsPublic: req.IsPublic,
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Update(docModel).WithTx(tx).UpdateIsPublic().Exec(); err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] update document settings failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return status.GenErrWithCustomMsg(status.StatusWriteDBError, "update document settings failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
cacheKey,
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("[Service layer: DocumentService] UpdateDocumentSettings failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
return nil, status.GenErrWithCustomMsg(err, "update document settings failed")
|
||||
}
|
||||
|
||||
if err := domain_event.PublishDocumentUpsertEvent(ctx, req.ExternalID); err != nil {
|
||||
logrus.Warnf("[Service layer: DocumentService] publish search sync event failed, external_id=%s, err=%+v", req.ExternalID, err)
|
||||
}
|
||||
|
||||
return &dto.DocumentSettingsResponse{
|
||||
IsPublic: req.IsPublic,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (s *DocumentService) UpdateTemplateSettings(ctx context.Context, req *dto.UpdateTemplateSettingsRequest) error {
|
||||
if err := validateUpdateTemplateSettingsReq(req); err != nil {
|
||||
return status.GenErrWithCustomMsg(err, "invalid update template settings request")
|
||||
}
|
||||
_ = ctx
|
||||
|
||||
userID, err := s.userRepo.GetIDByExternalID(req.UserExternalID).Exec()
|
||||
if err != nil {
|
||||
return status.StatusUserNotFound
|
||||
}
|
||||
|
||||
tpl, err := s.templateRepo.GetByID(req.TemplateID).Exec()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
return status.StatusReadDBError
|
||||
}
|
||||
if tpl == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template not found")
|
||||
}
|
||||
if tpl.UserID != userID {
|
||||
return status.StatusPermissionDenied
|
||||
}
|
||||
|
||||
updateOp := s.templateRepo.Update(tpl)
|
||||
|
||||
if req.Name != nil {
|
||||
nextName := strings.TrimSpace(*req.Name)
|
||||
if nextName == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template name is empty")
|
||||
}
|
||||
if len([]rune(nextName)) > 100 {
|
||||
nextName = truncateRunes(nextName, 100)
|
||||
}
|
||||
tpl.Name = nextName
|
||||
updateOp.UpdateName()
|
||||
}
|
||||
|
||||
if req.Description != nil {
|
||||
tpl.Description = strings.TrimSpace(*req.Description)
|
||||
updateOp.UpdateDescription()
|
||||
}
|
||||
|
||||
if req.IsPublic != nil {
|
||||
tpl.IsPublic = *req.IsPublic
|
||||
if *req.IsPublic {
|
||||
tpl.Scope = "system"
|
||||
} else if tpl.KnowledgeBaseID != nil {
|
||||
tpl.Scope = "knowledge_base"
|
||||
} else {
|
||||
tpl.Scope = "private"
|
||||
}
|
||||
updateOp.UpdateScope().UpdateIsPublic()
|
||||
}
|
||||
|
||||
if err := updateOp.Exec(); err != nil {
|
||||
return status.StatusWriteDBError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package document_service
|
||||
|
||||
import (
|
||||
"github.com/XingfenD/yoresee_doc/internal/dto"
|
||||
doc_container_mapper "github.com/XingfenD/yoresee_doc/internal/mapper/doc_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/doc_type_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/mapper/template_container_mapper"
|
||||
"github.com/XingfenD/yoresee_doc/internal/status"
|
||||
)
|
||||
|
||||
func validateCreateDocumentReq(req *dto.CreateDocumentRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if !doc_container_mapper.IsSupportedDTOType(req.ContainerType) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid document container type")
|
||||
}
|
||||
if req.ContainerType == dto.ContainerType_Own && req.KnowledgeExternalID != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusInternalParamsError, "KnowledgeExternalID not nil when container_type is own")
|
||||
}
|
||||
if req.ContainerType == dto.ContainerType_KnowledgeBase && req.KnowledgeExternalID == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusInternalParamsError, "KnowledgeExternalID is nil when container_type is knowledge_base")
|
||||
}
|
||||
|
||||
if !doc_type_mapper.IsSupportedDTOType(req.Type) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid document type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUpdateDocumentReq(req *dto.UpdateDocumentRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if req.ExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusInternalParamsError, "external_id is zero value")
|
||||
}
|
||||
|
||||
if req.Content == nil && req.KnowledgeBaseExternalID == nil && req.ParentExternalID == nil && req.Title == nil && req.MoveToContainer == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "no update field")
|
||||
}
|
||||
|
||||
if req.MoveToContainer != nil {
|
||||
if !doc_container_mapper.IsSupportedDTOType(*req.MoveToContainer) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid move_to_container")
|
||||
}
|
||||
if *req.MoveToContainer == dto.ContainerType_Own && req.KnowledgeBaseExternalID != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "KnowledgeBaseExternalID not nil when moving to own")
|
||||
}
|
||||
if *req.MoveToContainer == dto.ContainerType_KnowledgeBase && req.KnowledgeBaseExternalID == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "KnowledgeBaseExternalID is nil when moving to knowledge_base")
|
||||
}
|
||||
}
|
||||
if req.MoveToContainer == nil && req.KnowledgeBaseExternalID != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "MoveToContainer is nil when KnowledgeBaseExternalID provided")
|
||||
}
|
||||
|
||||
if req.MoveToContainer != nil && *req.MoveToContainer == dto.ContainerType_Own && req.ParentExternalID != nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "ParentExternalID not nil when moving to own")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUpdateDocumentMetaReq(req *dto.UpdateDocumentMetaRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if req.ExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusInternalParamsError, "external_id is zero value")
|
||||
}
|
||||
if req.Title == nil && req.Summary == nil && req.Tags == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "no update field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCreateTemplateReq(req *dto.CreateTemplateRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if req.UserExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "user external id is empty")
|
||||
}
|
||||
if req.TemplateContent == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template content is empty")
|
||||
}
|
||||
if req.Type != "" && !doc_type_mapper.IsSupportedDTOType(req.Type) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid template type")
|
||||
}
|
||||
if !template_container_mapper.IsSupportedDTOType(req.TargetContainer) {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "invalid template container")
|
||||
}
|
||||
if template_container_mapper.RequiresKnowledgeBaseID(req.TargetContainer) {
|
||||
if req.KnowledgeBaseExternalID == nil || *req.KnowledgeBaseExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "knowledge_base_id is empty")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUpdateTemplateSettingsReq(req *dto.UpdateTemplateSettingsRequest) error {
|
||||
if req == nil {
|
||||
return status.StatusInternalParamsError
|
||||
}
|
||||
if req.UserExternalID == "" {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "user external id is empty")
|
||||
}
|
||||
if req.TemplateID <= 0 {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "template id is invalid")
|
||||
}
|
||||
if req.Name == nil && req.Description == nil && req.IsPublic == nil {
|
||||
return status.GenErrWithCustomMsg(status.StatusParamError, "no update field")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user