Commit d0655ece authored by Steven's avatar Steven

refactor: update memo tags

parent 2c270438
This diff is collapsed.
......@@ -61,3 +61,13 @@ func RandomString(n int) (string, error) {
}
return sb.String(), nil
}
// ReplaceString replaces all occurrences of old in slice with new.
func ReplaceString(slice []string, old, new string) []string {
for i, s := range slice {
if s == old {
slice[i] = new
}
}
return slice
}
......@@ -57,6 +57,21 @@ service MemoService {
body: "*"
};
}
// ListMemoTags lists tags for a memo.
rpc ListMemoTags(ListMemoTagsRequest) returns (ListMemoTagsResponse) {
option (google.api.http) = {get: "/api/v1/{parent=memos/*}/tags"};
}
// RenameMemoTag renames a tag for a memo.
rpc RenameMemoTag(RenameMemoTagRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
patch: "/api/v1/{parent=memos/*}/tags:rename"
body: "*"
};
}
// DeleteMemoTag deletes a tag for a memo.
rpc DeleteMemoTag(DeleteMemoTagRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{parent=memos/*}/tags/{tag}"};
}
// SetMemoResources sets resources for a memo.
rpc SetMemoResources(SetMemoResourcesRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
......@@ -155,15 +170,17 @@ message Memo {
Visibility visibility = 10;
bool pinned = 11;
repeated string tags = 11;
optional int32 parent_id = 12 [(google.api.field_behavior) = OUTPUT_ONLY];
bool pinned = 12;
repeated Resource resources = 13 [(google.api.field_behavior) = OUTPUT_ONLY];
optional int32 parent_id = 13 [(google.api.field_behavior) = OUTPUT_ONLY];
repeated MemoRelation relations = 14 [(google.api.field_behavior) = OUTPUT_ONLY];
repeated Resource resources = 14 [(google.api.field_behavior) = OUTPUT_ONLY];
repeated Reaction reactions = 15 [(google.api.field_behavior) = OUTPUT_ONLY];
repeated MemoRelation relations = 15 [(google.api.field_behavior) = OUTPUT_ONLY];
repeated Reaction reactions = 16 [(google.api.field_behavior) = OUTPUT_ONLY];
}
message CreateMemoRequest {
......@@ -230,6 +247,38 @@ message ExportMemosResponse {
bytes content = 1;
}
message ListMemoTagsRequest {
// The parent, who owns the tags.
// Format: memos/{id}. Use "memos/-" to list all tags.
string parent = 1;
// Rebuild the tags.
bool rebuild = 2;
}
message ListMemoTagsResponse {
// tag_amounts is the amount of tags.
// key is the tag name. e.g. "tag1".
// value is the amount of the tag.
map<string, int32> tag_amounts = 1;
}
message RenameMemoTagRequest {
// The parent, who owns the tags.
// Format: memos/{id}. Use "memos/-" to rename all tags.
string parent = 1;
string old_tag = 2;
string new_tag = 3;
}
message DeleteMemoTagRequest {
// The parent, who owns the tags.
// Format: memos/{id}. Use "memos/-" to delete all tags.
string parent = 1;
string tag = 2;
bool delete_related_memos = 3;
}
message SetMemoResourcesRequest {
// The name of the memo.
// Format: memos/{id}
......
syntax = "proto3";
package memos.api.v1;
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
option go_package = "gen/api/v1";
service TagService {
// UpsertTag upserts a tag.
rpc UpsertTag(UpsertTagRequest) returns (Tag) {
option (google.api.http) = {
post: "/api/v1/tags",
body: "*"
};
}
// BatchUpsertTag upserts multiple tags.
rpc BatchUpsertTag(BatchUpsertTagRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/api/v1/tags:batchUpsert",
body: "*"
};
}
// ListTags lists tags.
rpc ListTags(ListTagsRequest) returns (ListTagsResponse) {
option (google.api.http) = {get: "/api/v1/tags"};
}
// RenameTag renames a tag.
// All related memos will be updated.
rpc RenameTag(RenameTagRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
patch: "/api/v1/tags:rename",
body: "*"
};
}
// DeleteTag deletes a tag.
rpc DeleteTag(DeleteTagRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/tags"};
}
// GetTagSuggestions gets tag suggestions from the user's memos.
rpc GetTagSuggestions(GetTagSuggestionsRequest) returns (GetTagSuggestionsResponse) {
option (google.api.http) = {get: "/api/v1/tags/suggestion"};
}
}
message Tag {
string name = 1;
// The creator of tags.
// Format: users/{id}
string creator = 2;
}
message UpsertTagRequest {
string name = 1;
}
message BatchUpsertTagRequest {
repeated UpsertTagRequest requests = 1;
}
message ListTagsRequest {}
message ListTagsResponse {
repeated Tag tags = 1;
}
message RenameTagRequest {
// The creator of tags.
// Format: users/{id}
string user = 1;
string old_name = 2;
string new_name = 3;
}
message DeleteTagRequest {
Tag tag = 1;
}
message GetTagSuggestionsRequest {
// The creator of tags.
// Format: users/{id}
string user = 1;
}
message GetTagSuggestionsResponse {
repeated string tags = 1;
}
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/activity_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/auth_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/common.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/idp_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/inbox_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/markdown_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/memo_relation_service.proto
......
This diff is collapsed.
This diff is collapsed.
......@@ -27,6 +27,9 @@ const (
MemoService_UpdateMemo_FullMethodName = "/memos.api.v1.MemoService/UpdateMemo"
MemoService_DeleteMemo_FullMethodName = "/memos.api.v1.MemoService/DeleteMemo"
MemoService_ExportMemos_FullMethodName = "/memos.api.v1.MemoService/ExportMemos"
MemoService_ListMemoTags_FullMethodName = "/memos.api.v1.MemoService/ListMemoTags"
MemoService_RenameMemoTag_FullMethodName = "/memos.api.v1.MemoService/RenameMemoTag"
MemoService_DeleteMemoTag_FullMethodName = "/memos.api.v1.MemoService/DeleteMemoTag"
MemoService_SetMemoResources_FullMethodName = "/memos.api.v1.MemoService/SetMemoResources"
MemoService_ListMemoResources_FullMethodName = "/memos.api.v1.MemoService/ListMemoResources"
MemoService_SetMemoRelations_FullMethodName = "/memos.api.v1.MemoService/SetMemoRelations"
......@@ -57,6 +60,12 @@ type MemoServiceClient interface {
DeleteMemo(ctx context.Context, in *DeleteMemoRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ExportMemos exports memos.
ExportMemos(ctx context.Context, in *ExportMemosRequest, opts ...grpc.CallOption) (*ExportMemosResponse, error)
// ListMemoTags lists tags for a memo.
ListMemoTags(ctx context.Context, in *ListMemoTagsRequest, opts ...grpc.CallOption) (*ListMemoTagsResponse, error)
// RenameMemoTag renames a tag for a memo.
RenameMemoTag(ctx context.Context, in *RenameMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// DeleteMemoTag deletes a tag for a memo.
DeleteMemoTag(ctx context.Context, in *DeleteMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// SetMemoResources sets resources for a memo.
SetMemoResources(ctx context.Context, in *SetMemoResourcesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListMemoResources lists resources for a memo.
......@@ -150,6 +159,33 @@ func (c *memoServiceClient) ExportMemos(ctx context.Context, in *ExportMemosRequ
return out, nil
}
func (c *memoServiceClient) ListMemoTags(ctx context.Context, in *ListMemoTagsRequest, opts ...grpc.CallOption) (*ListMemoTagsResponse, error) {
out := new(ListMemoTagsResponse)
err := c.cc.Invoke(ctx, MemoService_ListMemoTags_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *memoServiceClient) RenameMemoTag(ctx context.Context, in *RenameMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, MemoService_RenameMemoTag_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *memoServiceClient) DeleteMemoTag(ctx context.Context, in *DeleteMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, MemoService_DeleteMemoTag_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *memoServiceClient) SetMemoResources(ctx context.Context, in *SetMemoResourcesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, MemoService_SetMemoResources_FullMethodName, in, out, opts...)
......@@ -258,6 +294,12 @@ type MemoServiceServer interface {
DeleteMemo(context.Context, *DeleteMemoRequest) (*emptypb.Empty, error)
// ExportMemos exports memos.
ExportMemos(context.Context, *ExportMemosRequest) (*ExportMemosResponse, error)
// ListMemoTags lists tags for a memo.
ListMemoTags(context.Context, *ListMemoTagsRequest) (*ListMemoTagsResponse, error)
// RenameMemoTag renames a tag for a memo.
RenameMemoTag(context.Context, *RenameMemoTagRequest) (*emptypb.Empty, error)
// DeleteMemoTag deletes a tag for a memo.
DeleteMemoTag(context.Context, *DeleteMemoTagRequest) (*emptypb.Empty, error)
// SetMemoResources sets resources for a memo.
SetMemoResources(context.Context, *SetMemoResourcesRequest) (*emptypb.Empty, error)
// ListMemoResources lists resources for a memo.
......@@ -306,6 +348,15 @@ func (UnimplementedMemoServiceServer) DeleteMemo(context.Context, *DeleteMemoReq
func (UnimplementedMemoServiceServer) ExportMemos(context.Context, *ExportMemosRequest) (*ExportMemosResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExportMemos not implemented")
}
func (UnimplementedMemoServiceServer) ListMemoTags(context.Context, *ListMemoTagsRequest) (*ListMemoTagsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMemoTags not implemented")
}
func (UnimplementedMemoServiceServer) RenameMemoTag(context.Context, *RenameMemoTagRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RenameMemoTag not implemented")
}
func (UnimplementedMemoServiceServer) DeleteMemoTag(context.Context, *DeleteMemoTagRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteMemoTag not implemented")
}
func (UnimplementedMemoServiceServer) SetMemoResources(context.Context, *SetMemoResourcesRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetMemoResources not implemented")
}
......@@ -475,6 +526,60 @@ func _MemoService_ExportMemos_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _MemoService_ListMemoTags_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListMemoTagsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MemoServiceServer).ListMemoTags(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MemoService_ListMemoTags_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MemoServiceServer).ListMemoTags(ctx, req.(*ListMemoTagsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MemoService_RenameMemoTag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RenameMemoTagRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MemoServiceServer).RenameMemoTag(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MemoService_RenameMemoTag_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MemoServiceServer).RenameMemoTag(ctx, req.(*RenameMemoTagRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MemoService_DeleteMemoTag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteMemoTagRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MemoServiceServer).DeleteMemoTag(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MemoService_DeleteMemoTag_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MemoServiceServer).DeleteMemoTag(ctx, req.(*DeleteMemoTagRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MemoService_SetMemoResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetMemoResourcesRequest)
if err := dec(in); err != nil {
......@@ -690,6 +795,18 @@ var MemoService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ExportMemos",
Handler: _MemoService_ExportMemos_Handler,
},
{
MethodName: "ListMemoTags",
Handler: _MemoService_ListMemoTags_Handler,
},
{
MethodName: "RenameMemoTag",
Handler: _MemoService_RenameMemoTag_Handler,
},
{
MethodName: "DeleteMemoTag",
Handler: _MemoService_DeleteMemoTag_Handler,
},
{
MethodName: "SetMemoResources",
Handler: _MemoService_SetMemoResources_Handler,
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/reaction_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/resource_service.proto
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/user_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/webhook_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/workspace_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: api/v1/workspace_setting_service.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: store/activity.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: store/idp.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: store/inbox.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: store/reaction.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: store/resource.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: store/user_setting.proto
......
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.0
// protoc-gen-go v1.34.1
// protoc (unknown)
// source: store/workspace_setting.proto
......
This diff is collapsed.
package v1
import (
"context"
"fmt"
"slices"
"sort"
"github.com/pkg/errors"
"github.com/yourselfhosted/gomark/ast"
"github.com/yourselfhosted/gomark/parser"
"github.com/yourselfhosted/gomark/parser/tokenizer"
"github.com/yourselfhosted/gomark/restore"
"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) UpsertTag(ctx context.Context, request *v1pb.UpsertTagRequest) (*v1pb.Tag, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
tag, err := s.Store.UpsertTag(ctx, &store.Tag{
Name: request.Name,
CreatorID: user.ID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err)
}
tagMessage, err := s.convertTagFromStore(ctx, tag)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err)
}
return tagMessage, nil
}
func (s *APIV1Service) BatchUpsertTag(ctx context.Context, request *v1pb.BatchUpsertTagRequest) (*emptypb.Empty, error) {
for _, r := range request.Requests {
if _, err := s.UpsertTag(ctx, r); err != nil {
return nil, status.Errorf(codes.Internal, "failed to batch upsert tags: %v", err)
}
}
return &emptypb.Empty{}, nil
}
func (s *APIV1Service) ListTags(ctx context.Context, _ *v1pb.ListTagsRequest) (*v1pb.ListTagsResponse, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
tagFind := &store.FindTag{
CreatorID: user.ID,
}
tags, err := s.Store.ListTags(ctx, tagFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
}
response := &v1pb.ListTagsResponse{}
for _, tag := range tags {
t, err := s.convertTagFromStore(ctx, tag)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert tag: %v", err)
}
response.Tags = append(response.Tags, t)
}
return response, nil
}
func (s *APIV1Service) RenameTag(ctx context.Context, request *v1pb.RenameTagRequest) (*emptypb.Empty, error) {
userID, err := ExtractUserIDFromName(request.User)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
// Find all related memos.
memos, err := s.Store.ListMemos(ctx, &store.FindMemo{
CreatorID: &user.ID,
ContentSearch: []string{fmt.Sprintf("#%s", request.OldName)},
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
// Replace tag name in memo content.
for _, memo := range memos {
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to parse memo: %v", err)
}
TraverseASTNodes(nodes, func(node ast.Node) {
if tag, ok := node.(*ast.Tag); ok && tag.Content == request.OldName {
tag.Content = request.NewName
}
})
content := restore.Restore(nodes)
if err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{
ID: memo.ID,
Content: &content,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update memo: %v", err)
}
}
// Delete old tag and create new tag.
if err := s.Store.DeleteTag(ctx, &store.DeleteTag{
CreatorID: user.ID,
Name: request.OldName,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err)
}
if _, err := s.Store.UpsertTag(ctx, &store.Tag{
CreatorID: user.ID,
Name: request.NewName,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to upsert tag: %v", err)
}
return &emptypb.Empty{}, nil
}
func (s *APIV1Service) DeleteTag(ctx context.Context, request *v1pb.DeleteTagRequest) (*emptypb.Empty, error) {
user, err := getCurrentUser(ctx, s.Store)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
if err := s.Store.DeleteTag(ctx, &store.DeleteTag{
Name: request.Tag.Name,
CreatorID: user.ID,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete tag: %v", err)
}
return &emptypb.Empty{}, nil
}
func (s *APIV1Service) GetTagSuggestions(ctx context.Context, request *v1pb.GetTagSuggestionsRequest) (*v1pb.GetTagSuggestionsResponse, error) {
userID, err := ExtractUserIDFromName(request.User)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
CreatorID: &user.ID,
ContentSearch: []string{"#"},
RowStatus: &normalRowStatus,
}
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
tagList, err := s.Store.ListTags(ctx, &store.FindTag{
CreatorID: user.ID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err)
}
tagNameList := []string{}
for _, tag := range tagList {
tagNameList = append(tagNameList, tag.Name)
}
tagMapSet := make(map[string]bool)
for _, memo := range memos {
nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content))
if err != nil {
return nil, errors.Wrap(err, "failed to parse memo content")
}
// Dynamically upsert tags from memo content.
TraverseASTNodes(nodes, func(node ast.Node) {
if tagNode, ok := node.(*ast.Tag); ok {
tag := tagNode.Content
if !slices.Contains(tagNameList, tag) {
tagMapSet[tag] = true
}
}
})
}
suggestions := []string{}
for tag := range tagMapSet {
suggestions = append(suggestions, tag)
}
sort.Strings(suggestions)
return &v1pb.GetTagSuggestionsResponse{
Tags: suggestions,
}, nil
}
func (s *APIV1Service) convertTagFromStore(ctx context.Context, tag *store.Tag) (*v1pb.Tag, error) {
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &tag.CreatorID,
})
if err != nil {
return nil, errors.Wrap(err, "failed to get user")
}
return &v1pb.Tag{
Name: tag.Name,
Creator: fmt.Sprintf("%s%d", UserNamePrefix, user.ID),
}, nil
}
func TraverseASTNodes(nodes []ast.Node, fn func(ast.Node)) {
for _, node := range nodes {
fn(node)
switch n := node.(type) {
case *ast.Paragraph:
TraverseASTNodes(n.Children, fn)
case *ast.Heading:
TraverseASTNodes(n.Children, fn)
case *ast.Blockquote:
TraverseASTNodes(n.Children, fn)
case *ast.OrderedList:
TraverseASTNodes(n.Children, fn)
case *ast.UnorderedList:
TraverseASTNodes(n.Children, fn)
case *ast.TaskList:
TraverseASTNodes(n.Children, fn)
case *ast.Bold:
TraverseASTNodes(n.Children, fn)
}
}
}
......@@ -23,7 +23,6 @@ type APIV1Service struct {
v1pb.UnimplementedUserServiceServer
v1pb.UnimplementedMemoServiceServer
v1pb.UnimplementedResourceServiceServer
v1pb.UnimplementedTagServiceServer
v1pb.UnimplementedInboxServiceServer
v1pb.UnimplementedActivityServiceServer
v1pb.UnimplementedWebhookServiceServer
......@@ -50,7 +49,6 @@ func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store
v1pb.RegisterAuthServiceServer(grpcServer, apiv1Service)
v1pb.RegisterUserServiceServer(grpcServer, apiv1Service)
v1pb.RegisterMemoServiceServer(grpcServer, apiv1Service)
v1pb.RegisterTagServiceServer(grpcServer, apiv1Service)
v1pb.RegisterResourceServiceServer(grpcServer, apiv1Service)
v1pb.RegisterInboxServiceServer(grpcServer, apiv1Service)
v1pb.RegisterActivityServiceServer(grpcServer, apiv1Service)
......@@ -102,9 +100,6 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech
if err := v1pb.RegisterMemoServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := v1pb.RegisterTagServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
if err := v1pb.RegisterResourceServiceHandler(context.Background(), gwMux, conn); err != nil {
return err
}
......
......@@ -2,6 +2,7 @@ package mysql
import (
"context"
"encoding/json"
"fmt"
"strings"
......@@ -11,9 +12,17 @@ import (
)
func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) {
fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`"}
placeholder := []string{"?", "?", "?", "?"}
args := []any{create.UID, create.CreatorID, create.Content, create.Visibility}
fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`tags`"}
placeholder := []string{"?", "?", "?", "?", "?"}
tags := "[]"
if len(create.Tags) != 0 {
tagsBytes, err := json.Marshal(create.Tags)
if err != nil {
return nil, err
}
tags = string(tagsBytes)
}
args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, tags}
stmt := "INSERT INTO `memo` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")"
result, err := d.db.ExecContext(ctx, stmt, args...)
......@@ -76,6 +85,9 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
}
where = append(where, fmt.Sprintf("`memo`.`visibility` in (%s)", strings.Join(placeholder, ",")))
}
if v := find.Tag; v != nil {
where, args = append(where, "JSON_CONTAINS(`memo`.`tags`, ?, '$')"), append(args, *v)
}
if find.ExcludeComments {
having = append(having, "`parent_id` IS NULL")
}
......@@ -102,6 +114,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
"UNIX_TIMESTAMP(`memo`.`updated_ts`) AS `updated_ts`",
"`memo`.`row_status` AS `row_status`",
"`memo`.`visibility` AS `visibility`",
"`memo`.`tags` AS `tags`",
"IFNULL(`memo_organizer`.`pinned`, 0) AS `pinned`",
"`memo_relation`.`related_memo_id` AS `parent_id`",
}
......@@ -126,6 +139,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
list := make([]*store.Memo, 0)
for rows.Next() {
var memo store.Memo
var tagsBytes []byte
dests := []any{
&memo.ID,
&memo.UID,
......@@ -134,6 +148,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
&memo.UpdatedTs,
&memo.RowStatus,
&memo.Visibility,
&tagsBytes,
&memo.Pinned,
&memo.ParentID,
}
......@@ -143,6 +158,9 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
if err := rows.Scan(dests...); err != nil {
return nil, err
}
if err := json.Unmarshal(tagsBytes, &memo.Tags); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal tags")
}
list = append(list, &memo)
}
......@@ -186,6 +204,13 @@ func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error {
if v := update.Visibility; v != nil {
set, args = append(set, "`visibility` = ?"), append(args, *v)
}
if v := update.Tags; v != nil {
tagsBytes, err := json.Marshal(v)
if err != nil {
return err
}
set, args = append(set, "`tags` = ?"), append(args, string(tagsBytes))
}
args = append(args, update.ID)
stmt := "UPDATE `memo` SET " + strings.Join(set, ", ") + " WHERE `id` = ?"
......
......@@ -43,7 +43,8 @@ CREATE TABLE `memo` (
`updated_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`row_status` VARCHAR(256) NOT NULL DEFAULT 'NORMAL',
`content` TEXT NOT NULL,
`visibility` VARCHAR(256) NOT NULL DEFAULT 'PRIVATE'
`visibility` VARCHAR(256) NOT NULL DEFAULT 'PRIVATE',
`tags` JSON NOT NULL
);
-- memo_organizer
......
ALTER TABLE `memo` ADD COLUMN `tags_temp` JSON;
UPDATE `memo` SET `tags_temp` = '[]';
ALTER TABLE `memo` DROP COLUMN `tags`;
ALTER TABLE `memo` CHANGE COLUMN `tags_temp` `tags` JSON NOT NULL;
This diff is collapsed.
......@@ -43,7 +43,8 @@ CREATE TABLE memo (
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
row_status TEXT NOT NULL DEFAULT 'NORMAL',
content TEXT NOT NULL,
visibility TEXT NOT NULL DEFAULT 'PRIVATE'
visibility TEXT NOT NULL DEFAULT 'PRIVATE',
tags JSONB NOT NULL DEFAULT '[]'
);
-- memo_organizer
......
ALTER TABLE memo ADD COLUMN tags JSONB NOT NULL DEFAULT '[]';
This diff is collapsed.
......@@ -46,12 +46,14 @@ CREATE TABLE memo (
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
tags TEXT NOT NULL DEFAULT '[]'
);
CREATE INDEX idx_memo_creator_id ON memo (creator_id);
CREATE INDEX idx_memo_content ON memo (content);
CREATE INDEX idx_memo_visibility ON memo (visibility);
CREATE INDEX idx_memo_tags ON memo (tags);
-- memo_organizer
CREATE TABLE memo_organizer (
......
ALTER TABLE memo ADD COLUMN tags TEXT NOT NULL DEFAULT '[]';
CREATE INDEX idx_memo_tags ON memo (tags);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment