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

172 lines
5.7 KiB
Go

package v1
import (
"context"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
"github.com/usememos/memos/store"
)
func (s *APIV1Service) SetMemoRelations(ctx context.Context, request *v1pb.SetMemoRelationsRequest) (*emptypb.Empty, error) {
user, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
memoUID, err := ExtractMemoUIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo")
}
if memo == nil {
return nil, status.Errorf(codes.NotFound, "memo not found")
}
if memo.CreatorID != user.ID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
relations, err := s.prepareMemoRelations(ctx, memo, request.Relations)
if err != nil {
return nil, err
}
updatedTsSec := time.Now().Unix()
if err := s.applyMemoMutation(ctx, memo, nil, &store.UpdateMemo{ID: memo.ID, UpdatedTs: &updatedTsSec}, nil, &relations); err != nil {
return nil, err
}
_, _, memoMessage, err := s.buildUpdatedMemoState(ctx, memo.ID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to build updated memo state")
}
s.dispatchMemoUpdatedSideEffects(ctx, memoMessage)
return &emptypb.Empty{}, nil
}
func (s *APIV1Service) prepareMemoRelations(ctx context.Context, memo *store.Memo, relations []*v1pb.MemoRelation) ([]*store.MemoRelation, error) {
prepared := make([]*store.MemoRelation, 0, len(relations))
seenRelatedMemoIDs := make(map[int32]struct{}, len(relations))
for _, relation := range relations {
if relation == nil || relation.RelatedMemo == nil {
return nil, status.Errorf(codes.InvalidArgument, "related memo is required")
}
switch relation.Type {
case v1pb.MemoRelation_REFERENCE:
// REFERENCE is the only client-mutable relation type.
case v1pb.MemoRelation_COMMENT:
// COMMENT context is immutable and may only be created atomically with a
// new memo through CreateMemoComment.
return nil, status.Errorf(codes.InvalidArgument, "COMMENT relations are output only")
default:
return nil, status.Errorf(codes.InvalidArgument, "relation type must be REFERENCE")
}
// Ignore reflexive relations.
if buildMemoName(memo.UID) == relation.RelatedMemo.Name {
continue
}
relatedMemoUID, err := ExtractMemoUIDFromName(relation.RelatedMemo.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid related memo name: %v", err)
}
relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &relatedMemoUID})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get related memo")
}
if relatedMemo == nil {
return nil, status.Errorf(codes.NotFound, "related memo not found")
}
if err := s.checkMemoReadAccess(ctx, relatedMemo); err != nil {
return nil, err
}
if _, ok := seenRelatedMemoIDs[relatedMemo.ID]; ok {
continue
}
seenRelatedMemoIDs[relatedMemo.ID] = struct{}{}
prepared = append(prepared, &store.MemoRelation{
MemoID: memo.ID,
RelatedMemoID: relatedMemo.ID,
Type: convertMemoRelationTypeToStore(relation.Type),
})
}
return prepared, nil
}
func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.ListMemoRelationsRequest) (*v1pb.ListMemoRelationsResponse, error) {
memoUID, err := ExtractMemoUIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
}
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo")
}
if memo == nil {
return nil, status.Errorf(codes.NotFound, "memo not found")
}
if err := s.checkMemoReadAccess(ctx, memo); err != nil {
return nil, err
}
relationMap, err := s.batchConvertMemoRelations(ctx, []*store.Memo{memo}, true)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memo relations: %v", err)
}
relationList := relationMap[memo.ID]
if relationList == nil {
relationList = []*v1pb.MemoRelation{}
}
limit := normalizePageSize(request.PageSize)
offset := 0
if request.PageToken != "" {
var token v1pb.PageToken
if err := unmarshalPageToken(request.PageToken, &token); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err)
}
limit = normalizePageSize(token.Limit)
offset = max(int(token.Offset), 0)
}
end := min(offset+limit, len(relationList))
if offset > len(relationList) {
offset = len(relationList)
}
nextPageToken := ""
if end < len(relationList) {
nextPageToken, err = getPageToken(limit, end)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create page token")
}
}
response := &v1pb.ListMemoRelationsResponse{
Relations: relationList[offset:end],
NextPageToken: nextPageToken,
}
return response, nil
}
func convertMemoRelationTypeFromStore(relationType store.MemoRelationType) v1pb.MemoRelation_Type {
switch relationType {
case store.MemoRelationReference:
return v1pb.MemoRelation_REFERENCE
case store.MemoRelationComment:
return v1pb.MemoRelation_COMMENT
default:
return v1pb.MemoRelation_TYPE_UNSPECIFIED
}
}
func convertMemoRelationTypeToStore(relationType v1pb.MemoRelation_Type) store.MemoRelationType {
switch relationType {
case v1pb.MemoRelation_COMMENT:
return store.MemoRelationComment
default:
return store.MemoRelationReference
}
}