memos/server/router/api/v1/memo_create_helpers.go

97 lines
3.1 KiB
Go

package v1
import (
"context"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/server/runner/memopayload"
"github.com/usememos/memos/store"
)
func mapMemoCreateError(err error, uid, operation string) error {
errMessage := err.Error()
if strings.Contains(errMessage, "UNIQUE constraint failed") ||
strings.Contains(errMessage, "duplicate key") ||
strings.Contains(errMessage, "Duplicate entry") {
return status.Errorf(codes.AlreadyExists, "memo with ID %q already exists", uid)
}
return mapMemoWriteError(err, operation)
}
type preparedMemoCreate struct {
memo *store.Memo
attachments *preparedMemoAttachments
requiredAttachmentIDs []int32
referenceRelations []*store.MemoRelation
}
// prepareMemoCreate validates and prepares the fields shared by top-level
// memo and comment creation. It does not write database or external state.
func (s *APIV1Service) prepareMemoCreate(ctx context.Context, user *store.User, input *v1pb.Memo, uid string) (*preparedMemoCreate, error) {
if user == nil || input == nil {
return nil, status.Error(codes.InvalidArgument, "memo is required")
}
visibility, err := validateCreateMemoVisibility(input.Visibility)
if err != nil {
return nil, err
}
memo := &store.Memo{
UID: uid,
CreatorID: user.ID,
Content: input.Content,
Visibility: visibility,
}
if input.Space != nil && input.GetSpace() != "" {
space, err := s.resolveWritableSpaceByName(ctx, input.GetSpace(), user.ID)
if err != nil {
return nil, err
}
memo.SpaceID = &space.ID
}
if memo.Visibility == store.SpaceAudience && memo.SpaceID == nil {
return nil, status.Error(codes.InvalidArgument, "SPACE visibility requires a space")
}
if input.CreateTime != nil && input.CreateTime.IsValid() {
memo.CreatedTs = input.CreateTime.AsTime().Unix()
}
if input.UpdateTime != nil && input.UpdateTime.IsValid() {
memo.UpdatedTs = input.UpdateTime.AsTime().Unix()
}
contentLengthLimit, err := s.getContentLengthLimit(ctx)
if err != nil {
return nil, status.Error(codes.Internal, "failed to get content length limit")
}
if len(memo.Content) > contentLengthLimit {
return nil, status.Errorf(codes.InvalidArgument, "content too long (max %d characters)", contentLengthLimit)
}
if err := memopayload.RebuildMemoPayload(ctx, memo, s.MarkdownService); err != nil {
return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err)
}
if input.Location != nil {
memo.Payload.Location = convertLocationToStore(input.Location)
}
attachments, err := s.prepareMemoAttachments(ctx, user, memo, input.Attachments)
if err != nil {
return nil, err
}
requiredAttachmentIDs, err := s.resolveMemoAttachmentReferences(memo.Content, attachments.normalized)
if err != nil {
return nil, err
}
relations, err := s.prepareMemoRelations(ctx, memo, input.Relations)
if err != nil {
return nil, err
}
return &preparedMemoCreate{
memo: memo,
attachments: attachments,
requiredAttachmentIDs: requiredAttachmentIDs,
referenceRelations: relations,
}, nil
}