migrate from github

This commit is contained in:
2026-07-15 22:42:31 +08:00
commit 3ad559f05e
262 changed files with 21637 additions and 0 deletions
@@ -0,0 +1,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
}
+127
View File
@@ -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
}
+142
View File
@@ -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
}