diff --git a/proto/gen/store/activity.pb.go b/proto/gen/store/activity.pb.go deleted file mode 100644 index f0e03cbf2..000000000 --- a/proto/gen/store/activity.pb.go +++ /dev/null @@ -1,180 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc (unknown) -// source: store/activity.proto - -package store - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type ActivityMemoCommentPayload struct { - state protoimpl.MessageState `protogen:"open.v1"` - MemoId int32 `protobuf:"varint,1,opt,name=memo_id,json=memoId,proto3" json:"memo_id,omitempty"` - RelatedMemoId int32 `protobuf:"varint,2,opt,name=related_memo_id,json=relatedMemoId,proto3" json:"related_memo_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ActivityMemoCommentPayload) Reset() { - *x = ActivityMemoCommentPayload{} - mi := &file_store_activity_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ActivityMemoCommentPayload) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ActivityMemoCommentPayload) ProtoMessage() {} - -func (x *ActivityMemoCommentPayload) ProtoReflect() protoreflect.Message { - mi := &file_store_activity_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ActivityMemoCommentPayload.ProtoReflect.Descriptor instead. -func (*ActivityMemoCommentPayload) Descriptor() ([]byte, []int) { - return file_store_activity_proto_rawDescGZIP(), []int{0} -} - -func (x *ActivityMemoCommentPayload) GetMemoId() int32 { - if x != nil { - return x.MemoId - } - return 0 -} - -func (x *ActivityMemoCommentPayload) GetRelatedMemoId() int32 { - if x != nil { - return x.RelatedMemoId - } - return 0 -} - -type ActivityPayload struct { - state protoimpl.MessageState `protogen:"open.v1"` - MemoComment *ActivityMemoCommentPayload `protobuf:"bytes,1,opt,name=memo_comment,json=memoComment,proto3" json:"memo_comment,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ActivityPayload) Reset() { - *x = ActivityPayload{} - mi := &file_store_activity_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ActivityPayload) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ActivityPayload) ProtoMessage() {} - -func (x *ActivityPayload) ProtoReflect() protoreflect.Message { - mi := &file_store_activity_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ActivityPayload.ProtoReflect.Descriptor instead. -func (*ActivityPayload) Descriptor() ([]byte, []int) { - return file_store_activity_proto_rawDescGZIP(), []int{1} -} - -func (x *ActivityPayload) GetMemoComment() *ActivityMemoCommentPayload { - if x != nil { - return x.MemoComment - } - return nil -} - -var File_store_activity_proto protoreflect.FileDescriptor - -const file_store_activity_proto_rawDesc = "" + - "\n" + - "\x14store/activity.proto\x12\vmemos.store\"]\n" + - "\x1aActivityMemoCommentPayload\x12\x17\n" + - "\amemo_id\x18\x01 \x01(\x05R\x06memoId\x12&\n" + - "\x0frelated_memo_id\x18\x02 \x01(\x05R\rrelatedMemoId\"]\n" + - "\x0fActivityPayload\x12J\n" + - "\fmemo_comment\x18\x01 \x01(\v2'.memos.store.ActivityMemoCommentPayloadR\vmemoCommentB\x98\x01\n" + - "\x0fcom.memos.storeB\rActivityProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" - -var ( - file_store_activity_proto_rawDescOnce sync.Once - file_store_activity_proto_rawDescData []byte -) - -func file_store_activity_proto_rawDescGZIP() []byte { - file_store_activity_proto_rawDescOnce.Do(func() { - file_store_activity_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_activity_proto_rawDesc), len(file_store_activity_proto_rawDesc))) - }) - return file_store_activity_proto_rawDescData -} - -var file_store_activity_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_store_activity_proto_goTypes = []any{ - (*ActivityMemoCommentPayload)(nil), // 0: memos.store.ActivityMemoCommentPayload - (*ActivityPayload)(nil), // 1: memos.store.ActivityPayload -} -var file_store_activity_proto_depIdxs = []int32{ - 0, // 0: memos.store.ActivityPayload.memo_comment:type_name -> memos.store.ActivityMemoCommentPayload - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name -} - -func init() { file_store_activity_proto_init() } -func file_store_activity_proto_init() { - if File_store_activity_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_activity_proto_rawDesc), len(file_store_activity_proto_rawDesc)), - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_store_activity_proto_goTypes, - DependencyIndexes: file_store_activity_proto_depIdxs, - MessageInfos: file_store_activity_proto_msgTypes, - }.Build() - File_store_activity_proto = out.File - file_store_activity_proto_goTypes = nil - file_store_activity_proto_depIdxs = nil -} diff --git a/proto/gen/store/inbox.pb.go b/proto/gen/store/inbox.pb.go index 7bcc06d60..1469aefa4 100644 --- a/proto/gen/store/inbox.pb.go +++ b/proto/gen/store/inbox.pb.go @@ -72,8 +72,10 @@ type InboxMessage struct { state protoimpl.MessageState `protogen:"open.v1"` // The type of the inbox message. Type InboxMessage_Type `protobuf:"varint,1,opt,name=type,proto3,enum=memos.store.InboxMessage_Type" json:"type,omitempty"` - // The system-generated unique ID of related activity. - ActivityId *int32 `protobuf:"varint,2,opt,name=activity_id,json=activityId,proto3,oneof" json:"activity_id,omitempty"` + // Types that are valid to be assigned to Payload: + // + // *InboxMessage_MemoComment + Payload isInboxMessage_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -115,9 +117,80 @@ func (x *InboxMessage) GetType() InboxMessage_Type { return InboxMessage_TYPE_UNSPECIFIED } -func (x *InboxMessage) GetActivityId() int32 { - if x != nil && x.ActivityId != nil { - return *x.ActivityId +func (x *InboxMessage) GetPayload() isInboxMessage_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *InboxMessage) GetMemoComment() *InboxMessage_MemoCommentPayload { + if x != nil { + if x, ok := x.Payload.(*InboxMessage_MemoComment); ok { + return x.MemoComment + } + } + return nil +} + +type isInboxMessage_Payload interface { + isInboxMessage_Payload() +} + +type InboxMessage_MemoComment struct { + MemoComment *InboxMessage_MemoCommentPayload `protobuf:"bytes,2,opt,name=memo_comment,json=memoComment,proto3,oneof"` +} + +func (*InboxMessage_MemoComment) isInboxMessage_Payload() {} + +type InboxMessage_MemoCommentPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + MemoId int32 `protobuf:"varint,1,opt,name=memo_id,json=memoId,proto3" json:"memo_id,omitempty"` + RelatedMemoId int32 `protobuf:"varint,2,opt,name=related_memo_id,json=relatedMemoId,proto3" json:"related_memo_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InboxMessage_MemoCommentPayload) Reset() { + *x = InboxMessage_MemoCommentPayload{} + mi := &file_store_inbox_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InboxMessage_MemoCommentPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InboxMessage_MemoCommentPayload) ProtoMessage() {} + +func (x *InboxMessage_MemoCommentPayload) ProtoReflect() protoreflect.Message { + mi := &file_store_inbox_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InboxMessage_MemoCommentPayload.ProtoReflect.Descriptor instead. +func (*InboxMessage_MemoCommentPayload) Descriptor() ([]byte, []int) { + return file_store_inbox_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *InboxMessage_MemoCommentPayload) GetMemoId() int32 { + if x != nil { + return x.MemoId + } + return 0 +} + +func (x *InboxMessage_MemoCommentPayload) GetRelatedMemoId() int32 { + if x != nil { + return x.RelatedMemoId } return 0 } @@ -126,15 +199,17 @@ var File_store_inbox_proto protoreflect.FileDescriptor const file_store_inbox_proto_rawDesc = "" + "\n" + - "\x11store/inbox.proto\x12\vmemos.store\"\xa8\x01\n" + + "\x11store/inbox.proto\x12\vmemos.store\"\xa7\x02\n" + "\fInboxMessage\x122\n" + - "\x04type\x18\x01 \x01(\x0e2\x1e.memos.store.InboxMessage.TypeR\x04type\x12$\n" + - "\vactivity_id\x18\x02 \x01(\x05H\x00R\n" + - "activityId\x88\x01\x01\".\n" + + "\x04type\x18\x01 \x01(\x0e2\x1e.memos.store.InboxMessage.TypeR\x04type\x12Q\n" + + "\fmemo_comment\x18\x02 \x01(\v2,.memos.store.InboxMessage.MemoCommentPayloadH\x00R\vmemoComment\x1aU\n" + + "\x12MemoCommentPayload\x12\x17\n" + + "\amemo_id\x18\x01 \x01(\x05R\x06memoId\x12&\n" + + "\x0frelated_memo_id\x18\x02 \x01(\x05R\rrelatedMemoId\".\n" + "\x04Type\x12\x14\n" + "\x10TYPE_UNSPECIFIED\x10\x00\x12\x10\n" + - "\fMEMO_COMMENT\x10\x01B\x0e\n" + - "\f_activity_idB\x95\x01\n" + + "\fMEMO_COMMENT\x10\x01B\t\n" + + "\apayloadB\x95\x01\n" + "\x0fcom.memos.storeB\n" + "InboxProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" @@ -151,18 +226,20 @@ func file_store_inbox_proto_rawDescGZIP() []byte { } var file_store_inbox_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_store_inbox_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_store_inbox_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_store_inbox_proto_goTypes = []any{ - (InboxMessage_Type)(0), // 0: memos.store.InboxMessage.Type - (*InboxMessage)(nil), // 1: memos.store.InboxMessage + (InboxMessage_Type)(0), // 0: memos.store.InboxMessage.Type + (*InboxMessage)(nil), // 1: memos.store.InboxMessage + (*InboxMessage_MemoCommentPayload)(nil), // 2: memos.store.InboxMessage.MemoCommentPayload } var file_store_inbox_proto_depIdxs = []int32{ 0, // 0: memos.store.InboxMessage.type:type_name -> memos.store.InboxMessage.Type - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 2, // 1: memos.store.InboxMessage.memo_comment:type_name -> memos.store.InboxMessage.MemoCommentPayload + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_store_inbox_proto_init() } @@ -170,14 +247,16 @@ func file_store_inbox_proto_init() { if File_store_inbox_proto != nil { return } - file_store_inbox_proto_msgTypes[0].OneofWrappers = []any{} + file_store_inbox_proto_msgTypes[0].OneofWrappers = []any{ + (*InboxMessage_MemoComment)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_inbox_proto_rawDesc), len(file_store_inbox_proto_rawDesc)), NumEnums: 1, - NumMessages: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/store/activity.proto b/proto/store/activity.proto deleted file mode 100644 index d6613bd9e..000000000 --- a/proto/store/activity.proto +++ /dev/null @@ -1,14 +0,0 @@ -syntax = "proto3"; - -package memos.store; - -option go_package = "gen/store"; - -message ActivityMemoCommentPayload { - int32 memo_id = 1; - int32 related_memo_id = 2; -} - -message ActivityPayload { - ActivityMemoCommentPayload memo_comment = 1; -} diff --git a/proto/store/inbox.proto b/proto/store/inbox.proto index 002c6697d..545935c21 100644 --- a/proto/store/inbox.proto +++ b/proto/store/inbox.proto @@ -5,10 +5,16 @@ package memos.store; option go_package = "gen/store"; message InboxMessage { + message MemoCommentPayload { + int32 memo_id = 1; + int32 related_memo_id = 2; + } + // The type of the inbox message. Type type = 1; - // The system-generated unique ID of related activity. - optional int32 activity_id = 2; + oneof payload { + MemoCommentPayload memo_comment = 2; + } enum Type { TYPE_UNSPECIFIED = 0; diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index dcc01668a..b45a40792 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -648,27 +648,18 @@ func (s *APIV1Service) CreateMemoComment(ctx context.Context, request *v1pb.Crea return nil, status.Errorf(codes.InvalidArgument, "invalid memo creator") } if memoComment.Visibility != v1pb.Visibility_PRIVATE && creatorID != relatedMemo.CreatorID { - activity, err := s.Store.CreateActivity(ctx, &store.Activity{ - CreatorID: creatorID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{ - MemoComment: &storepb.ActivityMemoCommentPayload{ - MemoId: memo.ID, - RelatedMemoId: relatedMemo.ID, - }, - }, - }) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to create activity") - } if _, err := s.Store.CreateInbox(ctx, &store.Inbox{ SenderID: creatorID, ReceiverID: relatedMemo.CreatorID, Status: store.UNREAD, Message: &storepb.InboxMessage{ - Type: storepb.InboxMessage_MEMO_COMMENT, - ActivityId: &activity.ID, + Type: storepb.InboxMessage_MEMO_COMMENT, + Payload: &storepb.InboxMessage_MemoComment{ + MemoComment: &storepb.InboxMessage_MemoCommentPayload{ + MemoId: memo.ID, + RelatedMemoId: relatedMemo.ID, + }, + }, }, }); err != nil { return nil, status.Errorf(codes.Internal, "failed to create inbox") diff --git a/server/router/api/v1/test/user_notification_test.go b/server/router/api/v1/test/user_notification_test.go index f1792c5d8..5adeed5d4 100644 --- a/server/router/api/v1/test/user_notification_test.go +++ b/server/router/api/v1/test/user_notification_test.go @@ -55,7 +55,7 @@ func TestListUserNotificationsIncludesMemoCommentPayload(t *testing.T) { require.Equal(t, memo.Name, notification.GetMemoComment().RelatedMemo) } -func TestListUserNotificationsOmitsPayloadWhenActivityMissing(t *testing.T) { +func TestListUserNotificationsStoresMemoCommentPayloadInInbox(t *testing.T) { ctx := context.Background() ts := NewTestService(t) defer ts.Cleanup() @@ -93,17 +93,9 @@ func TestListUserNotificationsOmitsPayloadWhenActivityMissing(t *testing.T) { require.NoError(t, err) require.Len(t, inboxes, 1) require.NotNil(t, inboxes[0].Message) - require.NotNil(t, inboxes[0].Message.ActivityId) - - _, err = ts.Store.GetDriver().GetDB().ExecContext(ctx, "DELETE FROM activity WHERE id = ?", *inboxes[0].Message.ActivityId) - require.NoError(t, err) - - resp, err := ts.Service.ListUserNotifications(ownerCtx, &apiv1.ListUserNotificationsRequest{ - Parent: fmt.Sprintf("users/%d", owner.ID), - }) - require.NoError(t, err) - require.Len(t, resp.Notifications, 1) - require.Nil(t, resp.Notifications[0].GetMemoComment()) + require.NotNil(t, inboxes[0].Message.GetMemoComment()) + require.NotZero(t, inboxes[0].Message.GetMemoComment().MemoId) + require.NotZero(t, inboxes[0].Message.GetMemoComment().RelatedMemoId) } func TestListUserNotificationsOmitsPayloadWhenMemosDeleted(t *testing.T) { diff --git a/server/router/api/v1/user_service.go b/server/router/api/v1/user_service.go index 98b746a6b..072d3efb3 100644 --- a/server/router/api/v1/user_service.go +++ b/server/router/api/v1/user_service.go @@ -1414,7 +1414,7 @@ func (s *APIV1Service) convertInboxToUserNotification(ctx context.Context, inbox notification.Status = v1pb.UserNotification_STATUS_UNSPECIFIED } - // Extract notification type and activity ID from inbox message + // Extract notification type and payload from the inbox message. if inbox.Message != nil { switch inbox.Message.Type { case storepb.InboxMessage_MEMO_COMMENT: @@ -1438,22 +1438,13 @@ func (s *APIV1Service) convertInboxToUserNotification(ctx context.Context, inbox } func (s *APIV1Service) convertUserNotificationPayload(ctx context.Context, message *storepb.InboxMessage) (*v1pb.UserNotification_MemoCommentPayload, error) { - if message == nil || message.Type != storepb.InboxMessage_MEMO_COMMENT || message.ActivityId == nil { - return nil, nil - } - - activity, err := s.Store.GetActivity(ctx, &store.FindActivity{ - ID: message.ActivityId, - }) - if err != nil { - return nil, errors.Wrap(err, "failed to get activity") - } - if activity == nil || activity.Payload == nil || activity.Payload.MemoComment == nil { + memoComment := message.GetMemoComment() + if message == nil || message.Type != storepb.InboxMessage_MEMO_COMMENT || memoComment == nil { return nil, nil } commentMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ - ID: &activity.Payload.MemoComment.MemoId, + ID: &memoComment.MemoId, ExcludeContent: true, }) if err != nil { @@ -1464,7 +1455,7 @@ func (s *APIV1Service) convertUserNotificationPayload(ctx context.Context, messa } relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ - ID: &activity.Payload.MemoComment.RelatedMemoId, + ID: &memoComment.RelatedMemoId, ExcludeContent: true, }) if err != nil { diff --git a/store/activity.go b/store/activity.go deleted file mode 100644 index aa4533ea4..000000000 --- a/store/activity.go +++ /dev/null @@ -1,68 +0,0 @@ -package store - -import ( - "context" - - storepb "github.com/usememos/memos/proto/gen/store" -) - -type ActivityType string - -const ( - ActivityTypeMemoComment ActivityType = "MEMO_COMMENT" -) - -func (t ActivityType) String() string { - return string(t) -} - -type ActivityLevel string - -const ( - ActivityLevelInfo ActivityLevel = "INFO" -) - -func (l ActivityLevel) String() string { - return string(l) -} - -type Activity struct { - ID int32 - - // Standard fields - CreatorID int32 - CreatedTs int64 - - // Domain specific fields - Type ActivityType - Level ActivityLevel - Payload *storepb.ActivityPayload -} - -type FindActivity struct { - ID *int32 - Type *ActivityType - - // Pagination - Limit *int - Offset *int -} - -func (s *Store) CreateActivity(ctx context.Context, create *Activity) (*Activity, error) { - return s.driver.CreateActivity(ctx, create) -} - -func (s *Store) ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error) { - return s.driver.ListActivities(ctx, find) -} - -func (s *Store) GetActivity(ctx context.Context, find *FindActivity) (*Activity, error) { - list, err := s.ListActivities(ctx, find) - if err != nil { - return nil, err - } - if len(list) == 0 { - return nil, nil - } - return list[0], nil -} diff --git a/store/db/mysql/activity.go b/store/db/mysql/activity.go deleted file mode 100644 index e304a9487..000000000 --- a/store/db/mysql/activity.go +++ /dev/null @@ -1,101 +0,0 @@ -package mysql - -import ( - "context" - "fmt" - "strings" - - "github.com/pkg/errors" - "google.golang.org/protobuf/encoding/protojson" - - storepb "github.com/usememos/memos/proto/gen/store" - "github.com/usememos/memos/store" -) - -func (d *DB) CreateActivity(ctx context.Context, create *store.Activity) (*store.Activity, error) { - payloadString := "{}" - if create.Payload != nil { - bytes, err := protojson.Marshal(create.Payload) - if err != nil { - return nil, errors.Wrap(err, "failed to marshal activity payload") - } - payloadString = string(bytes) - } - fields := []string{"`creator_id`", "`type`", "`level`", "`payload`"} - placeholder := []string{"?", "?", "?", "?"} - args := []any{create.CreatorID, create.Type.String(), create.Level.String(), payloadString} - - stmt := "INSERT INTO `activity` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")" - result, err := d.db.ExecContext(ctx, stmt, args...) - if err != nil { - return nil, errors.Wrap(err, "failed to execute statement") - } - - id, err := result.LastInsertId() - if err != nil { - return nil, errors.Wrap(err, "failed to get last insert id") - } - - id32 := int32(id) - - list, err := d.ListActivities(ctx, &store.FindActivity{ID: &id32}) - if err != nil || len(list) == 0 { - return nil, errors.Wrap(err, "failed to find activity") - } - - return list[0], nil -} - -func (d *DB) ListActivities(ctx context.Context, find *store.FindActivity) ([]*store.Activity, error) { - where, args := []string{"1 = 1"}, []any{} - - if find.ID != nil { - where, args = append(where, "`id` = ?"), append(args, *find.ID) - } - if find.Type != nil { - where, args = append(where, "`type` = ?"), append(args, find.Type.String()) - } - - query := "SELECT `id`, `creator_id`, `type`, `level`, `payload`, UNIX_TIMESTAMP(`created_ts`) FROM `activity` WHERE " + strings.Join(where, " AND ") + " ORDER BY `created_ts` DESC" - if find.Limit != nil { - query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) - if find.Offset != nil { - query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) - } - } - - rows, err := d.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - list := []*store.Activity{} - for rows.Next() { - activity := &store.Activity{} - var payloadBytes []byte - if err := rows.Scan( - &activity.ID, - &activity.CreatorID, - &activity.Type, - &activity.Level, - &payloadBytes, - &activity.CreatedTs, - ); err != nil { - return nil, err - } - - payload := &storepb.ActivityPayload{} - if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { - return nil, err - } - activity.Payload = payload - list = append(list, activity) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return list, nil -} diff --git a/store/db/postgres/activity.go b/store/db/postgres/activity.go deleted file mode 100644 index 96140e87e..000000000 --- a/store/db/postgres/activity.go +++ /dev/null @@ -1,89 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "strings" - - "github.com/pkg/errors" - "google.golang.org/protobuf/encoding/protojson" - - storepb "github.com/usememos/memos/proto/gen/store" - "github.com/usememos/memos/store" -) - -func (d *DB) CreateActivity(ctx context.Context, create *store.Activity) (*store.Activity, error) { - payloadString := "{}" - if create.Payload != nil { - bytes, err := protojson.Marshal(create.Payload) - if err != nil { - return nil, errors.Wrap(err, "failed to marshal activity payload") - } - payloadString = string(bytes) - } - - fields := []string{"creator_id", "type", "level", "payload"} - args := []any{create.CreatorID, create.Type.String(), create.Level.String(), payloadString} - stmt := "INSERT INTO activity (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts" - if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( - &create.ID, - &create.CreatedTs, - ); err != nil { - return nil, err - } - - return create, nil -} - -func (d *DB) ListActivities(ctx context.Context, find *store.FindActivity) ([]*store.Activity, error) { - where, args := []string{"1 = 1"}, []any{} - if find.ID != nil { - where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID) - } - if find.Type != nil { - where, args = append(where, "type = "+placeholder(len(args)+1)), append(args, find.Type.String()) - } - - query := "SELECT id, creator_id, type, level, payload, created_ts FROM activity WHERE " + strings.Join(where, " AND ") + " ORDER BY created_ts DESC" - if find.Limit != nil { - query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) - if find.Offset != nil { - query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) - } - } - - rows, err := d.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - list := []*store.Activity{} - for rows.Next() { - activity := &store.Activity{} - var payloadBytes []byte - if err := rows.Scan( - &activity.ID, - &activity.CreatorID, - &activity.Type, - &activity.Level, - &payloadBytes, - &activity.CreatedTs, - ); err != nil { - return nil, err - } - - payload := &storepb.ActivityPayload{} - if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { - return nil, err - } - activity.Payload = payload - list = append(list, activity) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return list, nil -} diff --git a/store/db/sqlite/activity.go b/store/db/sqlite/activity.go deleted file mode 100644 index eaa2fcb03..000000000 --- a/store/db/sqlite/activity.go +++ /dev/null @@ -1,91 +0,0 @@ -package sqlite - -import ( - "context" - "fmt" - "strings" - - "github.com/pkg/errors" - "google.golang.org/protobuf/encoding/protojson" - - storepb "github.com/usememos/memos/proto/gen/store" - "github.com/usememos/memos/store" -) - -func (d *DB) CreateActivity(ctx context.Context, create *store.Activity) (*store.Activity, error) { - payloadString := "{}" - if create.Payload != nil { - bytes, err := protojson.Marshal(create.Payload) - if err != nil { - return nil, errors.Wrap(err, "failed to marshal activity payload") - } - payloadString = string(bytes) - } - - fields := []string{"`creator_id`", "`type`", "`level`", "`payload`"} - placeholder := []string{"?", "?", "?", "?"} - args := []any{create.CreatorID, create.Type.String(), create.Level.String(), payloadString} - - stmt := "INSERT INTO activity (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`" - if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( - &create.ID, - &create.CreatedTs, - ); err != nil { - return nil, err - } - - return create, nil -} - -func (d *DB) ListActivities(ctx context.Context, find *store.FindActivity) ([]*store.Activity, error) { - where, args := []string{"1 = 1"}, []any{} - if find.ID != nil { - where, args = append(where, "`id` = ?"), append(args, *find.ID) - } - if find.Type != nil { - where, args = append(where, "`type` = ?"), append(args, find.Type.String()) - } - - query := "SELECT `id`, `creator_id`, `type`, `level`, `payload`, `created_ts` FROM `activity` WHERE " + strings.Join(where, " AND ") + " ORDER BY `created_ts` DESC" - if find.Limit != nil { - query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) - if find.Offset != nil { - query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) - } - } - - rows, err := d.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - list := []*store.Activity{} - for rows.Next() { - activity := &store.Activity{} - var payloadBytes []byte - if err := rows.Scan( - &activity.ID, - &activity.CreatorID, - &activity.Type, - &activity.Level, - &payloadBytes, - &activity.CreatedTs, - ); err != nil { - return nil, err - } - - payload := &storepb.ActivityPayload{} - if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { - return nil, err - } - activity.Payload = payload - list = append(list, activity) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return list, nil -} diff --git a/store/driver.go b/store/driver.go index ec3ba9082..ee686c0e0 100644 --- a/store/driver.go +++ b/store/driver.go @@ -13,10 +13,6 @@ type Driver interface { IsInitialized(ctx context.Context) (bool, error) - // Activity model related methods. - CreateActivity(ctx context.Context, create *Activity) (*Activity, error) - ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error) - // Attachment model related methods. CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error) ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error) diff --git a/store/inbox.go b/store/inbox.go index 417ea9f86..bb93ff42c 100644 --- a/store/inbox.go +++ b/store/inbox.go @@ -21,7 +21,6 @@ func (s InboxStatus) String() string { } // Inbox represents a notification in a user's inbox. -// It connects activities to users who should be notified. type Inbox struct { ID int32 CreatedTs int64 diff --git a/store/migration/mysql/0.27/02__migrate_inbox_message_payload.sql b/store/migration/mysql/0.27/02__migrate_inbox_message_payload.sql new file mode 100644 index 000000000..8800176b6 --- /dev/null +++ b/store/migration/mysql/0.27/02__migrate_inbox_message_payload.sql @@ -0,0 +1,14 @@ +UPDATE `inbox` AS i +JOIN `activity` AS a + ON a.`id` = CAST(JSON_UNQUOTE(JSON_EXTRACT(i.`message`, '$.activityId')) AS UNSIGNED) +SET i.`message` = JSON_SET( + JSON_REMOVE(i.`message`, '$.activityId'), + '$.memoComment', + JSON_OBJECT( + 'memoId', + JSON_EXTRACT(a.`payload`, '$.memoComment.memoId'), + 'relatedMemoId', + JSON_EXTRACT(a.`payload`, '$.memoComment.relatedMemoId') + ) +) +WHERE JSON_EXTRACT(i.`message`, '$.activityId') IS NOT NULL; diff --git a/store/migration/mysql/0.27/03__drop_activity.sql b/store/migration/mysql/0.27/03__drop_activity.sql new file mode 100644 index 000000000..bfda5a812 --- /dev/null +++ b/store/migration/mysql/0.27/03__drop_activity.sql @@ -0,0 +1 @@ +DROP TABLE `activity`; diff --git a/store/migration/mysql/LATEST.sql b/store/migration/mysql/LATEST.sql index 017854c27..58b9b91dc 100644 --- a/store/migration/mysql/LATEST.sql +++ b/store/migration/mysql/LATEST.sql @@ -67,16 +67,6 @@ CREATE TABLE `attachment` ( `payload` TEXT NOT NULL ); --- activity -CREATE TABLE `activity` ( - `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - `creator_id` INT NOT NULL, - `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - `type` VARCHAR(256) NOT NULL DEFAULT '', - `level` VARCHAR(256) NOT NULL DEFAULT 'INFO', - `payload` TEXT NOT NULL -); - -- idp CREATE TABLE `idp` ( `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, diff --git a/store/migration/postgres/0.27/02__migrate_inbox_message_payload.sql b/store/migration/postgres/0.27/02__migrate_inbox_message_payload.sql new file mode 100644 index 000000000..413915e76 --- /dev/null +++ b/store/migration/postgres/0.27/02__migrate_inbox_message_payload.sql @@ -0,0 +1,13 @@ +UPDATE inbox AS i +SET message = jsonb_set( + i.message::jsonb - 'activityId', + '{memoComment}', + jsonb_build_object( + 'memoId', + a.payload->'memoComment'->'memoId', + 'relatedMemoId', + a.payload->'memoComment'->'relatedMemoId' + ) +)::text +FROM activity AS a +WHERE (i.message::jsonb->>'activityId')::integer = a.id; diff --git a/store/migration/postgres/0.27/03__drop_activity.sql b/store/migration/postgres/0.27/03__drop_activity.sql new file mode 100644 index 000000000..6a508d33f --- /dev/null +++ b/store/migration/postgres/0.27/03__drop_activity.sql @@ -0,0 +1 @@ +DROP TABLE activity; diff --git a/store/migration/postgres/LATEST.sql b/store/migration/postgres/LATEST.sql index b5faf9f38..1affc26bf 100644 --- a/store/migration/postgres/LATEST.sql +++ b/store/migration/postgres/LATEST.sql @@ -67,16 +67,6 @@ CREATE TABLE attachment ( payload TEXT NOT NULL DEFAULT '{}' ); --- activity -CREATE TABLE activity ( - id SERIAL PRIMARY KEY, - creator_id INTEGER NOT NULL, - created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), - type TEXT NOT NULL DEFAULT '', - level TEXT NOT NULL DEFAULT 'INFO', - payload JSONB NOT NULL DEFAULT '{}' -); - -- idp CREATE TABLE idp ( id SERIAL PRIMARY KEY, diff --git a/store/migration/sqlite/0.27/02__migrate_inbox_message_payload.sql b/store/migration/sqlite/0.27/02__migrate_inbox_message_payload.sql new file mode 100644 index 000000000..0a6714f8d --- /dev/null +++ b/store/migration/sqlite/0.27/02__migrate_inbox_message_payload.sql @@ -0,0 +1,25 @@ +UPDATE inbox +SET message = json_set( + json_remove(message, '$.activityId'), + '$.memoComment', + json_object( + 'memoId', + ( + SELECT json_extract(activity.payload, '$.memoComment.memoId') + FROM activity + WHERE activity.id = json_extract(inbox.message, '$.activityId') + ), + 'relatedMemoId', + ( + SELECT json_extract(activity.payload, '$.memoComment.relatedMemoId') + FROM activity + WHERE activity.id = json_extract(inbox.message, '$.activityId') + ) + ) +) +WHERE json_extract(message, '$.activityId') IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM activity + WHERE activity.id = json_extract(inbox.message, '$.activityId') + ); diff --git a/store/migration/sqlite/0.27/03__drop_activity.sql b/store/migration/sqlite/0.27/03__drop_activity.sql new file mode 100644 index 000000000..6a508d33f --- /dev/null +++ b/store/migration/sqlite/0.27/03__drop_activity.sql @@ -0,0 +1 @@ +DROP TABLE activity; diff --git a/store/migration/sqlite/LATEST.sql b/store/migration/sqlite/LATEST.sql index 8daa49a11..bdb04054f 100644 --- a/store/migration/sqlite/LATEST.sql +++ b/store/migration/sqlite/LATEST.sql @@ -68,16 +68,6 @@ CREATE TABLE attachment ( payload TEXT NOT NULL DEFAULT '{}' ); --- activity -CREATE TABLE activity ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - creator_id INTEGER NOT NULL, - created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), - type TEXT NOT NULL DEFAULT '', - level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO', - payload TEXT NOT NULL DEFAULT '{}' -); - -- idp CREATE TABLE idp ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/store/test/activity_test.go b/store/test/activity_test.go deleted file mode 100644 index 20eecca52..000000000 --- a/store/test/activity_test.go +++ /dev/null @@ -1,380 +0,0 @@ -package test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - storepb "github.com/usememos/memos/proto/gen/store" - "github.com/usememos/memos/store" -) - -func TestActivityStore(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - create := &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - } - activity, err := ts.CreateActivity(ctx, create) - require.NoError(t, err) - require.NotNil(t, activity) - activities, err := ts.ListActivities(ctx, &store.FindActivity{ - ID: &activity.ID, - }) - require.NoError(t, err) - require.Equal(t, 1, len(activities)) - require.Equal(t, activity, activities[0]) - ts.Close() -} - -func TestActivityGetByID(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - - activity, err := ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - - // Get activity by ID - found, err := ts.GetActivity(ctx, &store.FindActivity{ID: &activity.ID}) - require.NoError(t, err) - require.NotNil(t, found) - require.Equal(t, activity.ID, found.ID) - - // Get non-existent activity - nonExistentID := int32(99999) - notFound, err := ts.GetActivity(ctx, &store.FindActivity{ID: &nonExistentID}) - require.NoError(t, err) - require.Nil(t, notFound) - - ts.Close() -} - -func TestActivityListMultiple(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - - // Create multiple activities - _, err = ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - - _, err = ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - - // List all activities - allActivities, err := ts.ListActivities(ctx, &store.FindActivity{}) - require.NoError(t, err) - require.Equal(t, 2, len(allActivities)) - - // List by type - commentType := store.ActivityTypeMemoComment - commentActivities, err := ts.ListActivities(ctx, &store.FindActivity{Type: &commentType}) - require.NoError(t, err) - require.Equal(t, 2, len(commentActivities)) - require.Equal(t, store.ActivityTypeMemoComment, commentActivities[0].Type) - - ts.Close() -} - -func TestActivityListByType(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - - // Create activities with MEMO_COMMENT type - _, err = ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - - _, err = ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - - // List by type - activityType := store.ActivityTypeMemoComment - activities, err := ts.ListActivities(ctx, &store.FindActivity{Type: &activityType}) - require.NoError(t, err) - require.Len(t, activities, 2) - for _, activity := range activities { - require.Equal(t, store.ActivityTypeMemoComment, activity.Type) - } - - ts.Close() -} - -func TestActivityPayloadMemoComment(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - - // Create activity with MemoComment payload - memoID := int32(123) - relatedMemoID := int32(456) - activity, err := ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{ - MemoComment: &storepb.ActivityMemoCommentPayload{ - MemoId: memoID, - RelatedMemoId: relatedMemoID, - }, - }, - }) - require.NoError(t, err) - require.NotNil(t, activity.Payload) - require.NotNil(t, activity.Payload.MemoComment) - require.Equal(t, memoID, activity.Payload.MemoComment.MemoId) - require.Equal(t, relatedMemoID, activity.Payload.MemoComment.RelatedMemoId) - - // Verify payload is preserved when listing - found, err := ts.GetActivity(ctx, &store.FindActivity{ID: &activity.ID}) - require.NoError(t, err) - require.NotNil(t, found.Payload.MemoComment) - require.Equal(t, memoID, found.Payload.MemoComment.MemoId) - require.Equal(t, relatedMemoID, found.Payload.MemoComment.RelatedMemoId) - - ts.Close() -} - -func TestActivityEmptyPayload(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - - // Create activity with empty payload - activity, err := ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - require.NotNil(t, activity.Payload) - - // Verify empty payload is handled correctly - found, err := ts.GetActivity(ctx, &store.FindActivity{ID: &activity.ID}) - require.NoError(t, err) - require.NotNil(t, found.Payload) - require.Nil(t, found.Payload.MemoComment) - - ts.Close() -} - -func TestActivityLevel(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - - // Create activity with INFO level - activity, err := ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - require.Equal(t, store.ActivityLevelInfo, activity.Level) - - // Verify level is preserved when listing - found, err := ts.GetActivity(ctx, &store.FindActivity{ID: &activity.ID}) - require.NoError(t, err) - require.Equal(t, store.ActivityLevelInfo, found.Level) - - ts.Close() -} - -func TestActivityCreatorID(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user1, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - user2, err := createTestingUserWithRole(ctx, ts, "user2", store.RoleUser) - require.NoError(t, err) - - // Create activity for user1 - activity1, err := ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user1.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - require.Equal(t, user1.ID, activity1.CreatorID) - - // Create activity for user2 - activity2, err := ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user2.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - require.Equal(t, user2.ID, activity2.CreatorID) - - // List all and verify creator IDs - activities, err := ts.ListActivities(ctx, &store.FindActivity{}) - require.NoError(t, err) - require.Len(t, activities, 2) - - ts.Close() -} - -func TestActivityCreatedTs(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - - activity, err := ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - require.NotZero(t, activity.CreatedTs) - - // Verify timestamp is preserved when listing - found, err := ts.GetActivity(ctx, &store.FindActivity{ID: &activity.ID}) - require.NoError(t, err) - require.Equal(t, activity.CreatedTs, found.CreatedTs) - - ts.Close() -} - -func TestActivityListEmpty(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - - // List activities when none exist - activities, err := ts.ListActivities(ctx, &store.FindActivity{}) - require.NoError(t, err) - require.Len(t, activities, 0) - - ts.Close() -} - -func TestActivityListWithIDAndType(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - - activity, err := ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{}, - }) - require.NoError(t, err) - - // List with both ID and Type filters - activityType := store.ActivityTypeMemoComment - activities, err := ts.ListActivities(ctx, &store.FindActivity{ - ID: &activity.ID, - Type: &activityType, - }) - require.NoError(t, err) - require.Len(t, activities, 1) - require.Equal(t, activity.ID, activities[0].ID) - - ts.Close() -} - -func TestActivityPayloadComplexMemoComment(t *testing.T) { - t.Parallel() - ctx := context.Background() - ts := NewTestingStore(ctx, t) - user, err := createTestingHostUser(ctx, ts) - require.NoError(t, err) - - // Create a memo first to use its ID - memo, err := ts.CreateMemo(ctx, &store.Memo{ - UID: "test-memo-for-activity", - CreatorID: user.ID, - Content: "Test memo content", - Visibility: store.Public, - }) - require.NoError(t, err) - - // Create comment memo - commentMemo, err := ts.CreateMemo(ctx, &store.Memo{ - UID: "comment-memo", - CreatorID: user.ID, - Content: "This is a comment", - Visibility: store.Public, - }) - require.NoError(t, err) - - // Create activity with real memo IDs - activity, err := ts.CreateActivity(ctx, &store.Activity{ - CreatorID: user.ID, - Type: store.ActivityTypeMemoComment, - Level: store.ActivityLevelInfo, - Payload: &storepb.ActivityPayload{ - MemoComment: &storepb.ActivityMemoCommentPayload{ - MemoId: memo.ID, - RelatedMemoId: commentMemo.ID, - }, - }, - }) - require.NoError(t, err) - require.Equal(t, memo.ID, activity.Payload.MemoComment.MemoId) - require.Equal(t, commentMemo.ID, activity.Payload.MemoComment.RelatedMemoId) - - // Verify payload is preserved - found, err := ts.GetActivity(ctx, &store.FindActivity{ID: &activity.ID}) - require.NoError(t, err) - require.Equal(t, memo.ID, found.Payload.MemoComment.MemoId) - require.Equal(t, commentMemo.ID, found.Payload.MemoComment.RelatedMemoId) - - ts.Close() -} diff --git a/store/test/inbox_test.go b/store/test/inbox_test.go index 8af2c7082..68e7ab2ef 100644 --- a/store/test/inbox_test.go +++ b/store/test/inbox_test.go @@ -329,27 +329,37 @@ func TestInboxMessagePayload(t *testing.T) { user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) - // Create inbox with message payload containing activity ID - activityID := int32(123) + // Create inbox with message payload containing memo references. + memoID := int32(123) + relatedMemoID := int32(456) inbox, err := ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{ - Type: storepb.InboxMessage_MEMO_COMMENT, - ActivityId: &activityID, + Type: storepb.InboxMessage_MEMO_COMMENT, + Payload: &storepb.InboxMessage_MemoComment{ + MemoComment: &storepb.InboxMessage_MemoCommentPayload{ + MemoId: memoID, + RelatedMemoId: relatedMemoID, + }, + }, }, }) require.NoError(t, err) require.NotNil(t, inbox.Message) require.Equal(t, storepb.InboxMessage_MEMO_COMMENT, inbox.Message.Type) - require.Equal(t, activityID, *inbox.Message.ActivityId) + require.NotNil(t, inbox.Message.GetMemoComment()) + require.Equal(t, memoID, inbox.Message.GetMemoComment().MemoId) + require.Equal(t, relatedMemoID, inbox.Message.GetMemoComment().RelatedMemoId) // List and verify payload is preserved inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ReceiverID: &user.ID}) require.NoError(t, err) require.Len(t, inboxes, 1) - require.Equal(t, activityID, *inboxes[0].Message.ActivityId) + require.NotNil(t, inboxes[0].Message.GetMemoComment()) + require.Equal(t, memoID, inboxes[0].Message.GetMemoComment().MemoId) + require.Equal(t, relatedMemoID, inboxes[0].Message.GetMemoComment().RelatedMemoId) ts.Close() } @@ -452,33 +462,43 @@ func TestInboxMessageTypeFilterWithPayload(t *testing.T) { user, err := createTestingHostUser(ctx, ts) require.NoError(t, err) - // Create inbox with full payload - activityID := int32(456) + // Create inbox with full payload. + memoID := int32(456) _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{ - Type: storepb.InboxMessage_MEMO_COMMENT, - ActivityId: &activityID, + Type: storepb.InboxMessage_MEMO_COMMENT, + Payload: &storepb.InboxMessage_MemoComment{ + MemoComment: &storepb.InboxMessage_MemoCommentPayload{ + MemoId: memoID, + RelatedMemoId: 654, + }, + }, }, }) require.NoError(t, err) - // Create inbox with different type but also has payload - otherActivityID := int32(789) + // Create inbox with different type but also has payload. + otherMemoID := int32(789) _, err = ts.CreateInbox(ctx, &store.Inbox{ SenderID: 0, ReceiverID: user.ID, Status: store.UNREAD, Message: &storepb.InboxMessage{ - Type: storepb.InboxMessage_TYPE_UNSPECIFIED, - ActivityId: &otherActivityID, + Type: storepb.InboxMessage_TYPE_UNSPECIFIED, + Payload: &storepb.InboxMessage_MemoComment{ + MemoComment: &storepb.InboxMessage_MemoCommentPayload{ + MemoId: otherMemoID, + RelatedMemoId: 987, + }, + }, }, }) require.NoError(t, err) - // Filter by type should work correctly even with complex JSON payload + // Filter by type should work correctly even with complex JSON payload. memoCommentType := storepb.InboxMessage_MEMO_COMMENT inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ ReceiverID: &user.ID, @@ -486,7 +506,8 @@ func TestInboxMessageTypeFilterWithPayload(t *testing.T) { }) require.NoError(t, err) require.Len(t, inboxes, 1) - require.Equal(t, activityID, *inboxes[0].Message.ActivityId) + require.NotNil(t, inboxes[0].Message.GetMemoComment()) + require.Equal(t, memoID, inboxes[0].Message.GetMemoComment().MemoId) ts.Close() } diff --git a/store/test/migrator_test.go b/store/test/migrator_test.go index 3eb541381..369438e0b 100644 --- a/store/test/migrator_test.go +++ b/store/test/migrator_test.go @@ -7,8 +7,10 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/stretchr/testify/require" + storepb "github.com/usememos/memos/proto/gen/store" "github.com/usememos/memos/store" ) @@ -87,6 +89,58 @@ func TestMigrationWithData(t *testing.T) { require.Equal(t, "Data before migration re-run", memo.Content, "memo content should be preserved") } +func TestMigrationBackfillsInboxMemoCommentPayload(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ts := NewTestingStore(ctx, t) + + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + commentMemo, err := ts.CreateMemo(ctx, &store.Memo{ + UID: "migration-comment-memo", + CreatorID: user.ID, + Content: "Comment memo", + Visibility: store.Public, + }) + require.NoError(t, err) + relatedMemo, err := ts.CreateMemo(ctx, &store.Memo{ + UID: "migration-related-memo", + CreatorID: user.ID, + Content: "Related memo", + Visibility: store.Public, + }) + require.NoError(t, err) + + driver := getDriverFromEnv() + require.NoError(t, createLegacyActivityTable(ctx, ts, driver)) + require.NoError(t, insertLegacyInboxActivity(ctx, ts, driver, user.ID, commentMemo.ID, relatedMemo.ID)) + _, err = ts.UpsertInstanceSetting(ctx, &storepb.InstanceSetting{ + Key: storepb.InstanceSettingKey_BASIC, + Value: &storepb.InstanceSetting_BasicSetting{ + BasicSetting: &storepb.InstanceBasicSetting{ + SchemaVersion: "0.27.2", + }, + }, + }) + require.NoError(t, err) + + err = ts.Migrate(ctx) + require.NoError(t, err) + + messageType := storepb.InboxMessage_MEMO_COMMENT + inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ + ReceiverID: &user.ID, + MessageType: &messageType, + }) + require.NoError(t, err) + require.Len(t, inboxes, 1) + require.NotNil(t, inboxes[0].Message) + require.NotNil(t, inboxes[0].Message.GetMemoComment()) + require.Equal(t, commentMemo.ID, inboxes[0].Message.GetMemoComment().MemoId) + require.Equal(t, relatedMemo.ID, inboxes[0].Message.GetMemoComment().RelatedMemoId) +} + // TestMigrationMultipleReRuns verifies that migration is idempotent // even when run multiple times in succession. func TestMigrationMultipleReRuns(t *testing.T) { @@ -111,6 +165,69 @@ func TestMigrationMultipleReRuns(t *testing.T) { require.Equal(t, initialVersion, finalVersion, "version should remain unchanged after multiple re-runs") } +func createLegacyActivityTable(ctx context.Context, ts *store.Store, driver string) error { + var stmt string + switch driver { + case "sqlite": + stmt = `CREATE TABLE activity ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + type TEXT NOT NULL DEFAULT '', + level TEXT NOT NULL DEFAULT 'INFO', + payload TEXT NOT NULL DEFAULT '{}' + );` + case "mysql": + stmt = `CREATE TABLE activity ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + creator_id INT NOT NULL, + created_ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + type VARCHAR(256) NOT NULL DEFAULT '', + level VARCHAR(256) NOT NULL DEFAULT 'INFO', + payload TEXT NOT NULL + );` + case "postgres": + stmt = `CREATE TABLE activity ( + id SERIAL PRIMARY KEY, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + type TEXT NOT NULL DEFAULT '', + level TEXT NOT NULL DEFAULT 'INFO', + payload JSONB NOT NULL DEFAULT '{}' + );` + default: + return errors.Errorf("unsupported driver: %s", driver) + } + _, err := ts.GetDriver().GetDB().ExecContext(ctx, stmt) + return err +} + +func insertLegacyInboxActivity(ctx context.Context, ts *store.Store, driver string, receiverID, memoID, relatedMemoID int32) error { + var insertActivityStmt string + var insertInboxStmt string + payload := fmt.Sprintf(`{"memoComment":{"memoId":%d,"relatedMemoId":%d}}`, memoID, relatedMemoID) + message := `{"type":"MEMO_COMMENT","activityId":1}` + + switch driver { + case "sqlite", "mysql": + insertActivityStmt = `INSERT INTO activity (id, creator_id, type, level, payload) VALUES (?, ?, ?, ?, ?)` + insertInboxStmt = `INSERT INTO inbox (sender_id, receiver_id, status, message) VALUES (?, ?, ?, ?)` + case "postgres": + insertActivityStmt = `INSERT INTO activity (id, creator_id, type, level, payload) VALUES ($1, $2, $3, $4, $5::jsonb)` + insertInboxStmt = `INSERT INTO inbox (sender_id, receiver_id, status, message) VALUES ($1, $2, $3, $4)` + default: + return errors.Errorf("unsupported driver: %s", driver) + } + + if _, err := ts.GetDriver().GetDB().ExecContext(ctx, insertActivityStmt, 1, receiverID, "MEMO_COMMENT", "INFO", payload); err != nil { + return err + } + if _, err := ts.GetDriver().GetDB().ExecContext(ctx, insertInboxStmt, receiverID, receiverID, store.UNREAD, message); err != nil { + return err + } + return nil +} + // TestMigrationFromStableVersion verifies that upgrading from a stable Memos version // to the current version works correctly. This is the critical upgrade path test. //