From 3220adbff95c352ec797b98b070b2c8cd013b282 Mon Sep 17 00:00:00 2001 From: johnnyjoy Date: Thu, 26 Dec 2024 20:13:05 +0800 Subject: [PATCH] refactor: split memo service --- server/router/api/v1/common.go | 9 + server/router/api/v1/memo_service.go | 393 ------------------ .../router/api/v1/memo_service_converter.go | 145 +++++++ server/router/api/v1/memo_service_filter.go | 266 ++++++++++++ 4 files changed, 420 insertions(+), 393 deletions(-) create mode 100644 server/router/api/v1/memo_service_converter.go create mode 100644 server/router/api/v1/memo_service_filter.go diff --git a/server/router/api/v1/common.go b/server/router/api/v1/common.go index 9294cd09..8702e629 100644 --- a/server/router/api/v1/common.go +++ b/server/router/api/v1/common.go @@ -10,6 +10,11 @@ import ( "github.com/usememos/memos/store" ) +const ( + // DefaultPageSize is the default page size for requests. + DefaultPageSize = 10 +) + func convertRowStatusFromStore(rowStatus store.RowStatus) v1pb.RowStatus { switch rowStatus { case store.Normal: @@ -57,3 +62,7 @@ func unmarshalPageToken(s string, pageToken *v1pb.PageToken) error { } return nil } + +func isSuperUser(user *store.User) bool { + return user.Role == store.RoleAdmin || user.Role == store.RoleHost +} diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index 17526c01..0a4f7531 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -7,7 +7,6 @@ import ( "time" "unicode/utf8" - "github.com/google/cel-go/cel" "github.com/lithammer/shortuuid/v4" "github.com/pkg/errors" "github.com/usememos/gomark/ast" @@ -15,7 +14,6 @@ import ( "github.com/usememos/gomark/parser/tokenizer" "github.com/usememos/gomark/renderer" "github.com/usememos/gomark/restore" - expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" @@ -28,11 +26,6 @@ import ( "github.com/usememos/memos/store" ) -const ( - // DefaultPageSize is the default page size for listing memos. - DefaultPageSize = 10 -) - func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoRequest) (*v1pb.Memo, error) { user, err := s.GetCurrentUser(ctx) if err != nil { @@ -619,253 +612,6 @@ func (s *APIV1Service) DeleteMemoTag(ctx context.Context, request *v1pb.DeleteMe return &emptypb.Empty{}, nil } -func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, view v1pb.MemoView) (*v1pb.Memo, error) { - displayTs := memo.CreatedTs - workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to get workspace memo related setting") - } - if workspaceMemoRelatedSetting.DisplayWithUpdateTime { - displayTs = memo.UpdatedTs - } - - name := fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID) - memoMessage := &v1pb.Memo{ - Name: name, - Uid: memo.UID, - RowStatus: convertRowStatusFromStore(memo.RowStatus), - Creator: fmt.Sprintf("%s%d", UserNamePrefix, memo.CreatorID), - CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)), - UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)), - DisplayTime: timestamppb.New(time.Unix(displayTs, 0)), - Content: memo.Content, - Visibility: convertVisibilityFromStore(memo.Visibility), - Pinned: memo.Pinned, - Tags: memo.Payload.Tags, - } - if memo.Payload != nil { - memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property) - memoMessage.Location = convertLocationFromStore(memo.Payload.Location) - } - if memo.ParentID != nil { - parent := fmt.Sprintf("%s%d", MemoNamePrefix, *memo.ParentID) - memoMessage.Parent = &parent - } - - // Fill content when view is MEMO_VIEW_FULL. - if view == v1pb.MemoView_MEMO_VIEW_FULL { - listMemoRelationsResponse, err := s.ListMemoRelations(ctx, &v1pb.ListMemoRelationsRequest{Name: name}) - if err != nil { - return nil, errors.Wrap(err, "failed to list memo relations") - } - memoMessage.Relations = listMemoRelationsResponse.Relations - - listMemoResourcesResponse, err := s.ListMemoResources(ctx, &v1pb.ListMemoResourcesRequest{Name: name}) - if err != nil { - return nil, errors.Wrap(err, "failed to list memo resources") - } - memoMessage.Resources = listMemoResourcesResponse.Resources - - listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &v1pb.ListMemoReactionsRequest{Name: name}) - if err != nil { - return nil, errors.Wrap(err, "failed to list memo reactions") - } - memoMessage.Reactions = listMemoReactionsResponse.Reactions - - nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content)) - if err != nil { - return nil, errors.Wrap(err, "failed to parse content") - } - memoMessage.Nodes = convertFromASTNodes(nodes) - - snippet, err := getMemoContentSnippet(memo.Content) - if err != nil { - return nil, errors.Wrap(err, "failed to get memo content snippet") - } - memoMessage.Snippet = snippet - } - - return memoMessage, nil -} - -func convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.MemoProperty { - if property == nil { - return nil - } - return &v1pb.MemoProperty{ - HasLink: property.HasLink, - HasTaskList: property.HasTaskList, - HasCode: property.HasCode, - HasIncompleteTasks: property.HasIncompleteTasks, - } -} - -func convertLocationFromStore(location *storepb.MemoPayload_Location) *v1pb.Location { - if location == nil { - return nil - } - return &v1pb.Location{ - Placeholder: location.Placeholder, - Latitude: location.Latitude, - Longitude: location.Longitude, - } -} - -func convertLocationToStore(location *v1pb.Location) *storepb.MemoPayload_Location { - if location == nil { - return nil - } - return &storepb.MemoPayload_Location{ - Placeholder: location.Placeholder, - Latitude: location.Latitude, - Longitude: location.Longitude, - } -} - -func convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility { - switch visibility { - case store.Private: - return v1pb.Visibility_PRIVATE - case store.Protected: - return v1pb.Visibility_PROTECTED - case store.Public: - return v1pb.Visibility_PUBLIC - default: - return v1pb.Visibility_VISIBILITY_UNSPECIFIED - } -} - -func convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility { - switch visibility { - case v1pb.Visibility_PRIVATE: - return store.Private - case v1pb.Visibility_PROTECTED: - return store.Protected - case v1pb.Visibility_PUBLIC: - return store.Public - default: - return store.Private - } -} - -func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store.FindMemo, filter string) error { - if find.PayloadFind == nil { - find.PayloadFind = &store.FindMemoPayload{} - } - if filter != "" { - filter, err := parseMemoFilter(filter) - if err != nil { - return status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) - } - if len(filter.ContentSearch) > 0 { - find.ContentSearch = filter.ContentSearch - } - if len(filter.Visibilities) > 0 { - find.VisibilityList = filter.Visibilities - } - if filter.TagSearch != nil { - if find.PayloadFind == nil { - find.PayloadFind = &store.FindMemoPayload{} - } - find.PayloadFind.TagSearch = filter.TagSearch - } - if filter.OrderByPinned { - find.OrderByPinned = filter.OrderByPinned - } - if filter.OrderByTimeAsc { - find.OrderByTimeAsc = filter.OrderByTimeAsc - } - if filter.DisplayTimeAfter != nil { - workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) - if err != nil { - return status.Errorf(codes.Internal, "failed to get workspace memo related setting") - } - if workspaceMemoRelatedSetting.DisplayWithUpdateTime { - find.UpdatedTsAfter = filter.DisplayTimeAfter - } else { - find.CreatedTsAfter = filter.DisplayTimeAfter - } - } - if filter.DisplayTimeBefore != nil { - workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) - if err != nil { - return status.Errorf(codes.Internal, "failed to get workspace memo related setting") - } - if workspaceMemoRelatedSetting.DisplayWithUpdateTime { - find.UpdatedTsBefore = filter.DisplayTimeBefore - } else { - find.CreatedTsBefore = filter.DisplayTimeBefore - } - } - if filter.Creator != nil { - userID, err := ExtractUserIDFromName(*filter.Creator) - if err != nil { - return errors.Wrap(err, "invalid user name") - } - user, err := s.Store.GetUser(ctx, &store.FindUser{ - ID: &userID, - }) - if err != nil { - return status.Errorf(codes.Internal, "failed to get user") - } - if user == nil { - return status.Errorf(codes.NotFound, "user not found") - } - find.CreatorID = &user.ID - } - if filter.RowStatus != nil { - find.RowStatus = filter.RowStatus - } - if filter.Random { - find.Random = filter.Random - } - if filter.Limit != nil { - find.Limit = filter.Limit - } - if filter.IncludeComments { - find.ExcludeComments = false - } - if filter.HasLink { - find.PayloadFind.HasLink = true - } - if filter.HasTaskList { - find.PayloadFind.HasTaskList = true - } - if filter.HasCode { - find.PayloadFind.HasCode = true - } - if filter.HasIncompleteTasks { - find.PayloadFind.HasIncompleteTasks = true - } - } - - user, err := s.GetCurrentUser(ctx) - if err != nil { - return status.Errorf(codes.Internal, "failed to get current user") - } - // If the user is not authenticated, only public memos are visible. - if user == nil { - if filter == "" { - // If no filter is provided, return an error. - return status.Errorf(codes.InvalidArgument, "filter is required for unauthenticated user") - } - - find.VisibilityList = []store.Visibility{store.Public} - } else if find.CreatorID == nil || *find.CreatorID != user.ID { - // If creator is not specified or the creator is not the current user, only public and protected memos are visible. - find.VisibilityList = []store.Visibility{store.Public, store.Protected} - } - - workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) - if err != nil { - return status.Errorf(codes.Internal, "failed to get workspace memo related setting") - } - if workspaceMemoRelatedSetting.DisplayWithUpdateTime { - find.OrderByUpdatedTs = true - } - return nil -} - func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) { workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) if err != nil { @@ -874,141 +620,6 @@ func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) { return int(workspaceMemoRelatedSetting.ContentLengthLimit), nil } -// MemoFilterCELAttributes are the CEL attributes. -var MemoFilterCELAttributes = []cel.EnvOption{ - cel.Variable("content_search", cel.ListType(cel.StringType)), - cel.Variable("visibilities", cel.ListType(cel.StringType)), - cel.Variable("tag_search", cel.ListType(cel.StringType)), - cel.Variable("order_by_pinned", cel.BoolType), - cel.Variable("order_by_time_asc", cel.BoolType), - cel.Variable("display_time_before", cel.IntType), - cel.Variable("display_time_after", cel.IntType), - cel.Variable("creator", cel.StringType), - cel.Variable("uid", cel.StringType), - cel.Variable("row_status", cel.StringType), - cel.Variable("random", cel.BoolType), - cel.Variable("limit", cel.IntType), - cel.Variable("include_comments", cel.BoolType), - cel.Variable("has_link", cel.BoolType), - cel.Variable("has_task_list", cel.BoolType), - cel.Variable("has_code", cel.BoolType), - cel.Variable("has_incomplete_tasks", cel.BoolType), -} - -type MemoFilter struct { - ContentSearch []string - Visibilities []store.Visibility - TagSearch []string - OrderByPinned bool - OrderByTimeAsc bool - DisplayTimeBefore *int64 - DisplayTimeAfter *int64 - Creator *string - RowStatus *store.RowStatus - Random bool - Limit *int - IncludeComments bool - HasLink bool - HasTaskList bool - HasCode bool - HasIncompleteTasks bool -} - -func parseMemoFilter(expression string) (*MemoFilter, error) { - e, err := cel.NewEnv(MemoFilterCELAttributes...) - if err != nil { - return nil, err - } - ast, issues := e.Compile(expression) - if issues != nil { - return nil, errors.Errorf("found issue %v", issues) - } - filter := &MemoFilter{} - expr, err := cel.AstToParsedExpr(ast) - if err != nil { - return nil, err - } - callExpr := expr.GetExpr().GetCallExpr() - findMemoField(callExpr, filter) - return filter, nil -} - -func findMemoField(callExpr *expr.Expr_Call, filter *MemoFilter) { - if len(callExpr.Args) == 2 { - idExpr := callExpr.Args[0].GetIdentExpr() - if idExpr != nil { - if idExpr.Name == "content_search" { - contentSearch := []string{} - for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { - value := expr.GetConstExpr().GetStringValue() - contentSearch = append(contentSearch, value) - } - filter.ContentSearch = contentSearch - } else if idExpr.Name == "visibilities" { - visibilities := []store.Visibility{} - for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { - value := expr.GetConstExpr().GetStringValue() - visibilities = append(visibilities, store.Visibility(value)) - } - filter.Visibilities = visibilities - } else if idExpr.Name == "tag_search" { - tagSearch := []string{} - for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { - value := expr.GetConstExpr().GetStringValue() - tagSearch = append(tagSearch, value) - } - filter.TagSearch = tagSearch - } else if idExpr.Name == "order_by_pinned" { - value := callExpr.Args[1].GetConstExpr().GetBoolValue() - filter.OrderByPinned = value - } else if idExpr.Name == "order_by_time_asc" { - value := callExpr.Args[1].GetConstExpr().GetBoolValue() - filter.OrderByTimeAsc = value - } else if idExpr.Name == "display_time_before" { - displayTimeBefore := callExpr.Args[1].GetConstExpr().GetInt64Value() - filter.DisplayTimeBefore = &displayTimeBefore - } else if idExpr.Name == "display_time_after" { - displayTimeAfter := callExpr.Args[1].GetConstExpr().GetInt64Value() - filter.DisplayTimeAfter = &displayTimeAfter - } else if idExpr.Name == "creator" { - creator := callExpr.Args[1].GetConstExpr().GetStringValue() - filter.Creator = &creator - } else if idExpr.Name == "row_status" { - rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue()) - filter.RowStatus = &rowStatus - } else if idExpr.Name == "random" { - value := callExpr.Args[1].GetConstExpr().GetBoolValue() - filter.Random = value - } else if idExpr.Name == "limit" { - limit := int(callExpr.Args[1].GetConstExpr().GetInt64Value()) - filter.Limit = &limit - } else if idExpr.Name == "include_comments" { - value := callExpr.Args[1].GetConstExpr().GetBoolValue() - filter.IncludeComments = value - } else if idExpr.Name == "has_link" { - value := callExpr.Args[1].GetConstExpr().GetBoolValue() - filter.HasLink = value - } else if idExpr.Name == "has_task_list" { - value := callExpr.Args[1].GetConstExpr().GetBoolValue() - filter.HasTaskList = value - } else if idExpr.Name == "has_code" { - value := callExpr.Args[1].GetConstExpr().GetBoolValue() - filter.HasCode = value - } else if idExpr.Name == "has_incomplete_tasks" { - value := callExpr.Args[1].GetConstExpr().GetBoolValue() - filter.HasIncompleteTasks = value - } - return - } - } - for _, arg := range callExpr.Args { - callExpr := arg.GetCallExpr() - if callExpr != nil { - findMemoField(callExpr, filter) - } - } -} - // DispatchMemoCreatedWebhook dispatches webhook when memo is created. func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *v1pb.Memo) error { return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created") @@ -1092,7 +703,3 @@ func substring(s string, length int) string { return s[:byteIndex] } - -func isSuperUser(user *store.User) bool { - return user.Role == store.RoleAdmin || user.Role == store.RoleHost -} diff --git a/server/router/api/v1/memo_service_converter.go b/server/router/api/v1/memo_service_converter.go new file mode 100644 index 00000000..8253e14b --- /dev/null +++ b/server/router/api/v1/memo_service_converter.go @@ -0,0 +1,145 @@ +package v1 + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/usememos/gomark/parser" + "github.com/usememos/gomark/parser/tokenizer" + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo, view v1pb.MemoView) (*v1pb.Memo, error) { + displayTs := memo.CreatedTs + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace memo related setting") + } + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + displayTs = memo.UpdatedTs + } + + name := fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID) + memoMessage := &v1pb.Memo{ + Name: name, + Uid: memo.UID, + RowStatus: convertRowStatusFromStore(memo.RowStatus), + Creator: fmt.Sprintf("%s%d", UserNamePrefix, memo.CreatorID), + CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)), + UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)), + DisplayTime: timestamppb.New(time.Unix(displayTs, 0)), + Content: memo.Content, + Visibility: convertVisibilityFromStore(memo.Visibility), + Pinned: memo.Pinned, + Tags: memo.Payload.Tags, + } + if memo.Payload != nil { + memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property) + memoMessage.Location = convertLocationFromStore(memo.Payload.Location) + } + if memo.ParentID != nil { + parent := fmt.Sprintf("%s%d", MemoNamePrefix, *memo.ParentID) + memoMessage.Parent = &parent + } + + // Fill content when view is MEMO_VIEW_FULL. + if view == v1pb.MemoView_MEMO_VIEW_FULL { + listMemoRelationsResponse, err := s.ListMemoRelations(ctx, &v1pb.ListMemoRelationsRequest{Name: name}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo relations") + } + memoMessage.Relations = listMemoRelationsResponse.Relations + + listMemoResourcesResponse, err := s.ListMemoResources(ctx, &v1pb.ListMemoResourcesRequest{Name: name}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo resources") + } + memoMessage.Resources = listMemoResourcesResponse.Resources + + listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &v1pb.ListMemoReactionsRequest{Name: name}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo reactions") + } + memoMessage.Reactions = listMemoReactionsResponse.Reactions + + nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse content") + } + memoMessage.Nodes = convertFromASTNodes(nodes) + + snippet, err := getMemoContentSnippet(memo.Content) + if err != nil { + return nil, errors.Wrap(err, "failed to get memo content snippet") + } + memoMessage.Snippet = snippet + } + + return memoMessage, nil +} + +func convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.MemoProperty { + if property == nil { + return nil + } + return &v1pb.MemoProperty{ + HasLink: property.HasLink, + HasTaskList: property.HasTaskList, + HasCode: property.HasCode, + HasIncompleteTasks: property.HasIncompleteTasks, + } +} + +func convertLocationFromStore(location *storepb.MemoPayload_Location) *v1pb.Location { + if location == nil { + return nil + } + return &v1pb.Location{ + Placeholder: location.Placeholder, + Latitude: location.Latitude, + Longitude: location.Longitude, + } +} + +func convertLocationToStore(location *v1pb.Location) *storepb.MemoPayload_Location { + if location == nil { + return nil + } + return &storepb.MemoPayload_Location{ + Placeholder: location.Placeholder, + Latitude: location.Latitude, + Longitude: location.Longitude, + } +} + +func convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility { + switch visibility { + case store.Private: + return v1pb.Visibility_PRIVATE + case store.Protected: + return v1pb.Visibility_PROTECTED + case store.Public: + return v1pb.Visibility_PUBLIC + default: + return v1pb.Visibility_VISIBILITY_UNSPECIFIED + } +} + +func convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility { + switch visibility { + case v1pb.Visibility_PRIVATE: + return store.Private + case v1pb.Visibility_PROTECTED: + return store.Protected + case v1pb.Visibility_PUBLIC: + return store.Public + default: + return store.Private + } +} diff --git a/server/router/api/v1/memo_service_filter.go b/server/router/api/v1/memo_service_filter.go new file mode 100644 index 00000000..7f6b6aa8 --- /dev/null +++ b/server/router/api/v1/memo_service_filter.go @@ -0,0 +1,266 @@ +package v1 + +import ( + "context" + + "github.com/google/cel-go/cel" + "github.com/pkg/errors" + expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store.FindMemo, filter string) error { + if find.PayloadFind == nil { + find.PayloadFind = &store.FindMemoPayload{} + } + if filter != "" { + filter, err := parseMemoFilter(filter) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) + } + if len(filter.ContentSearch) > 0 { + find.ContentSearch = filter.ContentSearch + } + if len(filter.Visibilities) > 0 { + find.VisibilityList = filter.Visibilities + } + if filter.TagSearch != nil { + if find.PayloadFind == nil { + find.PayloadFind = &store.FindMemoPayload{} + } + find.PayloadFind.TagSearch = filter.TagSearch + } + if filter.OrderByPinned { + find.OrderByPinned = filter.OrderByPinned + } + if filter.OrderByTimeAsc { + find.OrderByTimeAsc = filter.OrderByTimeAsc + } + if filter.DisplayTimeAfter != nil { + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + find.UpdatedTsAfter = filter.DisplayTimeAfter + } else { + find.CreatedTsAfter = filter.DisplayTimeAfter + } + } + if filter.DisplayTimeBefore != nil { + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + find.UpdatedTsBefore = filter.DisplayTimeBefore + } else { + find.CreatedTsBefore = filter.DisplayTimeBefore + } + } + if filter.Creator != nil { + userID, err := ExtractUserIDFromName(*filter.Creator) + if err != nil { + return errors.Wrap(err, "invalid user name") + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &userID, + }) + if err != nil { + return status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return status.Errorf(codes.NotFound, "user not found") + } + find.CreatorID = &user.ID + } + if filter.RowStatus != nil { + find.RowStatus = filter.RowStatus + } + if filter.Random { + find.Random = filter.Random + } + if filter.Limit != nil { + find.Limit = filter.Limit + } + if filter.IncludeComments { + find.ExcludeComments = false + } + if filter.HasLink { + find.PayloadFind.HasLink = true + } + if filter.HasTaskList { + find.PayloadFind.HasTaskList = true + } + if filter.HasCode { + find.PayloadFind.HasCode = true + } + if filter.HasIncompleteTasks { + find.PayloadFind.HasIncompleteTasks = true + } + } + + user, err := s.GetCurrentUser(ctx) + if err != nil { + return status.Errorf(codes.Internal, "failed to get current user") + } + // If the user is not authenticated, only public memos are visible. + if user == nil { + if filter == "" { + // If no filter is provided, return an error. + return status.Errorf(codes.InvalidArgument, "filter is required for unauthenticated user") + } + + find.VisibilityList = []store.Visibility{store.Public} + } else if find.CreatorID == nil || *find.CreatorID != user.ID { + // If creator is not specified or the creator is not the current user, only public and protected memos are visible. + find.VisibilityList = []store.Visibility{store.Public, store.Protected} + } + + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + find.OrderByUpdatedTs = true + } + return nil +} + +// MemoFilterCELAttributes are the CEL attributes. +var MemoFilterCELAttributes = []cel.EnvOption{ + cel.Variable("content_search", cel.ListType(cel.StringType)), + cel.Variable("visibilities", cel.ListType(cel.StringType)), + cel.Variable("tag_search", cel.ListType(cel.StringType)), + cel.Variable("order_by_pinned", cel.BoolType), + cel.Variable("order_by_time_asc", cel.BoolType), + cel.Variable("display_time_before", cel.IntType), + cel.Variable("display_time_after", cel.IntType), + cel.Variable("creator", cel.StringType), + cel.Variable("uid", cel.StringType), + cel.Variable("row_status", cel.StringType), + cel.Variable("random", cel.BoolType), + cel.Variable("limit", cel.IntType), + cel.Variable("include_comments", cel.BoolType), + cel.Variable("has_link", cel.BoolType), + cel.Variable("has_task_list", cel.BoolType), + cel.Variable("has_code", cel.BoolType), + cel.Variable("has_incomplete_tasks", cel.BoolType), +} + +type MemoFilter struct { + ContentSearch []string + Visibilities []store.Visibility + TagSearch []string + OrderByPinned bool + OrderByTimeAsc bool + DisplayTimeBefore *int64 + DisplayTimeAfter *int64 + Creator *string + RowStatus *store.RowStatus + Random bool + Limit *int + IncludeComments bool + HasLink bool + HasTaskList bool + HasCode bool + HasIncompleteTasks bool +} + +func parseMemoFilter(expression string) (*MemoFilter, error) { + e, err := cel.NewEnv(MemoFilterCELAttributes...) + if err != nil { + return nil, err + } + ast, issues := e.Compile(expression) + if issues != nil { + return nil, errors.Errorf("found issue %v", issues) + } + filter := &MemoFilter{} + expr, err := cel.AstToParsedExpr(ast) + if err != nil { + return nil, err + } + callExpr := expr.GetExpr().GetCallExpr() + findMemoField(callExpr, filter) + return filter, nil +} + +func findMemoField(callExpr *expr.Expr_Call, filter *MemoFilter) { + if len(callExpr.Args) == 2 { + idExpr := callExpr.Args[0].GetIdentExpr() + if idExpr != nil { + if idExpr.Name == "content_search" { + contentSearch := []string{} + for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { + value := expr.GetConstExpr().GetStringValue() + contentSearch = append(contentSearch, value) + } + filter.ContentSearch = contentSearch + } else if idExpr.Name == "visibilities" { + visibilities := []store.Visibility{} + for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { + value := expr.GetConstExpr().GetStringValue() + visibilities = append(visibilities, store.Visibility(value)) + } + filter.Visibilities = visibilities + } else if idExpr.Name == "tag_search" { + tagSearch := []string{} + for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { + value := expr.GetConstExpr().GetStringValue() + tagSearch = append(tagSearch, value) + } + filter.TagSearch = tagSearch + } else if idExpr.Name == "order_by_pinned" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.OrderByPinned = value + } else if idExpr.Name == "order_by_time_asc" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.OrderByTimeAsc = value + } else if idExpr.Name == "display_time_before" { + displayTimeBefore := callExpr.Args[1].GetConstExpr().GetInt64Value() + filter.DisplayTimeBefore = &displayTimeBefore + } else if idExpr.Name == "display_time_after" { + displayTimeAfter := callExpr.Args[1].GetConstExpr().GetInt64Value() + filter.DisplayTimeAfter = &displayTimeAfter + } else if idExpr.Name == "creator" { + creator := callExpr.Args[1].GetConstExpr().GetStringValue() + filter.Creator = &creator + } else if idExpr.Name == "row_status" { + rowStatus := store.RowStatus(callExpr.Args[1].GetConstExpr().GetStringValue()) + filter.RowStatus = &rowStatus + } else if idExpr.Name == "random" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.Random = value + } else if idExpr.Name == "limit" { + limit := int(callExpr.Args[1].GetConstExpr().GetInt64Value()) + filter.Limit = &limit + } else if idExpr.Name == "include_comments" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.IncludeComments = value + } else if idExpr.Name == "has_link" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasLink = value + } else if idExpr.Name == "has_task_list" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasTaskList = value + } else if idExpr.Name == "has_code" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasCode = value + } else if idExpr.Name == "has_incomplete_tasks" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasIncompleteTasks = value + } + return + } + } + for _, arg := range callExpr.Args { + callExpr := arg.GetCallExpr() + if callExpr != nil { + findMemoField(callExpr, filter) + } + } +}