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

250 lines
8.2 KiB
Go

package v1
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/usememos/memos/internal/filter"
"github.com/usememos/memos/internal/util"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
// Helper function to extract user and memo view ID from a memo view resource name.
// Format: users/{user}/views/{view}.
func (s *APIV1Service) extractUserAndMemoViewIDFromName(ctx context.Context, name string) (*store.User, string, error) {
parts := strings.Split(name, "/")
if len(parts) != 4 || parts[0] != "users" || parts[2] != "views" {
return nil, "", errors.Errorf("invalid memo view name format: %s", name)
}
user, err := ResolveUserByName(ctx, s.Store, BuildUserName(parts[1]))
if err != nil {
return nil, "", err
}
if user == nil {
return nil, "", errors.Errorf("user not found: %s", parts[1])
}
memoViewID := parts[3]
if memoViewID == "" {
return nil, "", errors.Errorf("empty memo view ID in name: %s", name)
}
return user, memoViewID, nil
}
// Helper function to construct a memo view resource name.
func constructMemoViewName(username string, memoViewID string) string {
return fmt.Sprintf("%s/views/%s", BuildUserName(username), memoViewID)
}
func convertMemoViewFromStore(username string, memoView *storepb.MemoViewsUserSetting_MemoView) *v1pb.MemoView {
return &v1pb.MemoView{
Name: constructMemoViewName(username, memoView.GetId()),
Title: memoView.GetTitle(),
Filter: memoView.GetFilter(),
}
}
// authorizeMemoViewAccess resolves the owner of a memo view collection and asserts that
// the caller is that owner.
func (s *APIV1Service) authorizeMemoViewAccess(ctx context.Context, user *store.User) error {
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser == nil {
return status.Errorf(codes.Unauthenticated, "user not authenticated")
}
if currentUser.ID != user.ID {
return status.Errorf(codes.PermissionDenied, "permission denied")
}
return nil
}
// ListMemoViews lists the saved memo views owned by a user.
func (s *APIV1Service) ListMemoViews(ctx context.Context, request *v1pb.ListMemoViewsRequest) (*v1pb.ListMemoViewsResponse, error) {
user, err := ResolveUserByName(ctx, s.Store, request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
if err := s.authorizeMemoViewAccess(ctx, user); err != nil {
return nil, err
}
storeMemoViews, err := s.Store.GetUserMemoViews(ctx, user.ID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo views: %v", err)
}
memoViews := []*v1pb.MemoView{}
for _, memoView := range storeMemoViews {
memoViews = append(memoViews, convertMemoViewFromStore(user.Username, memoView))
}
return &v1pb.ListMemoViewsResponse{
MemoViews: memoViews,
}, nil
}
// GetMemoView returns a saved memo view by resource name.
func (s *APIV1Service) GetMemoView(ctx context.Context, request *v1pb.GetMemoViewRequest) (*v1pb.MemoView, error) {
user, memoViewID, err := s.extractUserAndMemoViewIDFromName(ctx, request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo view name: %v", err)
}
if err := s.authorizeMemoViewAccess(ctx, user); err != nil {
return nil, err
}
memoViews, err := s.Store.GetUserMemoViews(ctx, user.ID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo views: %v", err)
}
for _, memoView := range memoViews {
if memoView.GetId() == memoViewID {
return convertMemoViewFromStore(user.Username, memoView), nil
}
}
return nil, status.Errorf(codes.NotFound, "memo view not found")
}
// CreateMemoView creates a saved memo view for a user.
func (s *APIV1Service) CreateMemoView(ctx context.Context, request *v1pb.CreateMemoViewRequest) (*v1pb.MemoView, error) {
user, err := ResolveUserByName(ctx, s.Store, request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
if err := s.authorizeMemoViewAccess(ctx, user); err != nil {
return nil, err
}
newMemoView := &storepb.MemoViewsUserSetting_MemoView{
Id: util.GenUUID(),
Title: request.GetMemoView().GetTitle(),
Filter: request.GetMemoView().GetFilter(),
}
if newMemoView.Title == "" {
return nil, status.Errorf(codes.InvalidArgument, "title is required")
}
if err := s.validateFilter(ctx, newMemoView.Filter); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
}
if request.ValidateOnly {
return convertMemoViewFromStore(user.Username, newMemoView), nil
}
if err := s.Store.AddUserMemoView(ctx, user.ID, newMemoView); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create memo view: %v", err)
}
return convertMemoViewFromStore(user.Username, newMemoView), nil
}
// UpdateMemoView updates the selected fields of a saved memo view.
func (s *APIV1Service) UpdateMemoView(ctx context.Context, request *v1pb.UpdateMemoViewRequest) (*v1pb.MemoView, error) {
user, memoViewID, err := s.extractUserAndMemoViewIDFromName(ctx, request.GetMemoView().GetName())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo view name: %v", err)
}
if err := s.authorizeMemoViewAccess(ctx, user); err != nil {
return nil, err
}
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
}
var title, filterValue *string
for _, field := range request.UpdateMask.Paths {
switch field {
case "title":
if request.GetMemoView().GetTitle() == "" {
return nil, status.Errorf(codes.InvalidArgument, "title is required")
}
value := request.GetMemoView().GetTitle()
title = &value
case "filter":
if err := s.validateFilter(ctx, request.GetMemoView().GetFilter()); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
}
value := request.GetMemoView().GetFilter()
filterValue = &value
default:
return nil, status.Errorf(codes.InvalidArgument, "unsupported update mask path: %s", field)
}
}
updatedMemoView, err := s.Store.UpdateUserMemoView(ctx, user.ID, memoViewID, title, filterValue)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update memo view: %v", err)
}
if updatedMemoView == nil {
return nil, status.Errorf(codes.NotFound, "memo view not found")
}
return convertMemoViewFromStore(user.Username, updatedMemoView), nil
}
// DeleteMemoView deletes a saved memo view by resource name.
func (s *APIV1Service) DeleteMemoView(ctx context.Context, request *v1pb.DeleteMemoViewRequest) (*emptypb.Empty, error) {
user, memoViewID, err := s.extractUserAndMemoViewIDFromName(ctx, request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo view name: %v", err)
}
if err := s.authorizeMemoViewAccess(ctx, user); err != nil {
return nil, err
}
found, err := s.Store.RemoveUserMemoView(ctx, user.ID, memoViewID)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete memo view: %v", err)
}
if !found {
return nil, status.Errorf(codes.NotFound, "memo view not found")
}
return &emptypb.Empty{}, nil
}
func (s *APIV1Service) validateFilter(ctx context.Context, filterStr string) error {
if filterStr == "" {
return errors.New("filter cannot be empty")
}
engine, err := filter.DefaultEngine()
if err != nil {
return err
}
if _, err := engine.CompileToStatement(ctx, filterStr, filter.RenderOptions{Dialect: s.filterDialect()}); err != nil {
return errors.Wrap(err, "failed to compile filter")
}
return nil
}
// filterDialect maps the configured database driver onto the filter engine dialect.
func (s *APIV1Service) filterDialect() filter.DialectName {
switch s.Profile.Driver {
case "mysql":
return filter.DialectMySQL
case "postgres":
return filter.DialectPostgres
default:
return filter.DialectSQLite
}
}