diff --git a/proto/gen/store/README.md b/proto/gen/store/README.md index 5b7f7db8..363536c4 100644 --- a/proto/gen/store/README.md +++ b/proto/gen/store/README.md @@ -16,6 +16,11 @@ - [InboxMessage.Type](#memos-store-InboxMessage-Type) +- [store/reaction.proto](#store_reaction-proto) + - [Reaction](#memos-store-Reaction) + + - [Reaction.Type](#memos-store-Reaction-Type) + - [store/user_setting.proto](#store_user_setting-proto) - [AccessTokensUserSetting](#memos-store-AccessTokensUserSetting) - [AccessTokensUserSetting.AccessToken](#memos-store-AccessTokensUserSetting-AccessToken) @@ -172,6 +177,57 @@ + +
+ +## store/reaction.proto + + + + + +### Reaction + + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| id | [int32](#int32) | | | +| created_ts | [int64](#int64) | | | +| creator_id | [int32](#int32) | | | +| content_id | [string](#string) | | content_id is the id of the content that the reaction is for. This can be a memo. e.g. memos/101 | +| reaction_type | [Reaction.Type](#memos-store-Reaction-Type) | | | + + + + + + + + + + +### Reaction.Type + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| TYPE_UNSPECIFIED | 0 | | +| HEART | 1 | | +| THUMBS_UP | 2 | | +| THUMBS_DOWN | 3 | | +| LAUGH | 4 | | +| ROCKET | 5 | | + + + + + + + + + + diff --git a/proto/gen/store/reaction.pb.go b/proto/gen/store/reaction.pb.go new file mode 100644 index 00000000..8701e9ac --- /dev/null +++ b/proto/gen/store/reaction.pb.go @@ -0,0 +1,263 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: store/reaction.proto + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +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 Reaction_Type int32 + +const ( + Reaction_TYPE_UNSPECIFIED Reaction_Type = 0 + Reaction_HEART Reaction_Type = 1 + Reaction_THUMBS_UP Reaction_Type = 2 + Reaction_THUMBS_DOWN Reaction_Type = 3 + Reaction_LAUGH Reaction_Type = 4 + Reaction_ROCKET Reaction_Type = 5 +) + +// Enum value maps for Reaction_Type. +var ( + Reaction_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "HEART", + 2: "THUMBS_UP", + 3: "THUMBS_DOWN", + 4: "LAUGH", + 5: "ROCKET", + } + Reaction_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "HEART": 1, + "THUMBS_UP": 2, + "THUMBS_DOWN": 3, + "LAUGH": 4, + "ROCKET": 5, + } +) + +func (x Reaction_Type) Enum() *Reaction_Type { + p := new(Reaction_Type) + *p = x + return p +} + +func (x Reaction_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Reaction_Type) Descriptor() protoreflect.EnumDescriptor { + return file_store_reaction_proto_enumTypes[0].Descriptor() +} + +func (Reaction_Type) Type() protoreflect.EnumType { + return &file_store_reaction_proto_enumTypes[0] +} + +func (x Reaction_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Reaction_Type.Descriptor instead. +func (Reaction_Type) EnumDescriptor() ([]byte, []int) { + return file_store_reaction_proto_rawDescGZIP(), []int{0, 0} +} + +type Reaction struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + CreatedTs int64 `protobuf:"varint,2,opt,name=created_ts,json=createdTs,proto3" json:"created_ts,omitempty"` + CreatorId int32 `protobuf:"varint,3,opt,name=creator_id,json=creatorId,proto3" json:"creator_id,omitempty"` + // content_id is the id of the content that the reaction is for. + // This can be a memo. e.g. memos/101 + ContentId string `protobuf:"bytes,4,opt,name=content_id,json=contentId,proto3" json:"content_id,omitempty"` + ReactionType Reaction_Type `protobuf:"varint,5,opt,name=reaction_type,json=reactionType,proto3,enum=memos.store.Reaction_Type" json:"reaction_type,omitempty"` +} + +func (x *Reaction) Reset() { + *x = Reaction{} + if protoimpl.UnsafeEnabled { + mi := &file_store_reaction_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Reaction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Reaction) ProtoMessage() {} + +func (x *Reaction) ProtoReflect() protoreflect.Message { + mi := &file_store_reaction_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Reaction.ProtoReflect.Descriptor instead. +func (*Reaction) Descriptor() ([]byte, []int) { + return file_store_reaction_proto_rawDescGZIP(), []int{0} +} + +func (x *Reaction) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Reaction) GetCreatedTs() int64 { + if x != nil { + return x.CreatedTs + } + return 0 +} + +func (x *Reaction) GetCreatorId() int32 { + if x != nil { + return x.CreatorId + } + return 0 +} + +func (x *Reaction) GetContentId() string { + if x != nil { + return x.ContentId + } + return "" +} + +func (x *Reaction) GetReactionType() Reaction_Type { + if x != nil { + return x.ReactionType + } + return Reaction_TYPE_UNSPECIFIED +} + +var File_store_reaction_proto protoreflect.FileDescriptor + +var file_store_reaction_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x22, 0x98, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x54, 0x73, 0x12, + 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x1d, + 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x3f, 0x0a, + 0x0d, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, + 0x52, 0x0c, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x22, 0x5e, + 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, + 0x48, 0x45, 0x41, 0x52, 0x54, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x48, 0x55, 0x4d, 0x42, + 0x53, 0x5f, 0x55, 0x50, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x54, 0x48, 0x55, 0x4d, 0x42, 0x53, + 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x41, 0x55, 0x47, 0x48, + 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x4f, 0x43, 0x4b, 0x45, 0x54, 0x10, 0x05, 0x42, 0x98, + 0x01, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x42, 0x0d, 0x52, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x50, 0x01, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0xa2, 0x02, + 0x03, 0x4d, 0x53, 0x58, 0xaa, 0x02, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x53, 0x74, 0x6f, + 0x72, 0x65, 0xca, 0x02, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x53, 0x74, 0x6f, 0x72, 0x65, + 0xe2, 0x02, 0x17, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x5c, 0x47, + 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0c, 0x4d, 0x65, 0x6d, + 0x6f, 0x73, 0x3a, 0x3a, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_store_reaction_proto_rawDescOnce sync.Once + file_store_reaction_proto_rawDescData = file_store_reaction_proto_rawDesc +) + +func file_store_reaction_proto_rawDescGZIP() []byte { + file_store_reaction_proto_rawDescOnce.Do(func() { + file_store_reaction_proto_rawDescData = protoimpl.X.CompressGZIP(file_store_reaction_proto_rawDescData) + }) + return file_store_reaction_proto_rawDescData +} + +var file_store_reaction_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_store_reaction_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_store_reaction_proto_goTypes = []interface{}{ + (Reaction_Type)(0), // 0: memos.store.Reaction.Type + (*Reaction)(nil), // 1: memos.store.Reaction +} +var file_store_reaction_proto_depIdxs = []int32{ + 0, // 0: memos.store.Reaction.reaction_type:type_name -> memos.store.Reaction.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 +} + +func init() { file_store_reaction_proto_init() } +func file_store_reaction_proto_init() { + if File_store_reaction_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_store_reaction_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Reaction); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_store_reaction_proto_rawDesc, + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_store_reaction_proto_goTypes, + DependencyIndexes: file_store_reaction_proto_depIdxs, + EnumInfos: file_store_reaction_proto_enumTypes, + MessageInfos: file_store_reaction_proto_msgTypes, + }.Build() + File_store_reaction_proto = out.File + file_store_reaction_proto_rawDesc = nil + file_store_reaction_proto_goTypes = nil + file_store_reaction_proto_depIdxs = nil +} diff --git a/proto/store/reaction.proto b/proto/store/reaction.proto new file mode 100644 index 00000000..47c93410 --- /dev/null +++ b/proto/store/reaction.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package memos.store; + +option go_package = "gen/store"; + +message Reaction { + int32 id = 1; + + int64 created_ts = 2; + + int32 creator_id = 3; + + // content_id is the id of the content that the reaction is for. + // This can be a memo. e.g. memos/101 + string content_id = 4; + + enum Type { + TYPE_UNSPECIFIED = 0; + HEART = 1; + THUMBS_UP = 2; + THUMBS_DOWN = 3; + LAUGH = 4; + ROCKET = 5; + } + Type reaction_type = 5; +} diff --git a/store/db/mysql/reaction.go b/store/db/mysql/reaction.go new file mode 100644 index 00000000..22642824 --- /dev/null +++ b/store/db/mysql/reaction.go @@ -0,0 +1,83 @@ +package mysql + +import ( + "context" + "strings" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateReaction(ctx context.Context, create *storepb.Reaction) (*storepb.Reaction, error) { + fields := []string{"`creator_id`", "`content_id`", "`reaction_type`"} + placeholder := []string{"?", "?", "?"} + args := []interface{}{create.CreatorId, create.ContentId, create.ReactionType.String()} + stmt := "INSERT INTO `reaction` (" + 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 + } + + reaction := create + return reaction, nil +} + +func (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*storepb.Reaction, error) { + where, args := []string{"1 = 1"}, []interface{}{} + if find.ID != nil { + where, args = append(where, "`id` = ?"), append(args, *find.ID) + } + if find.CreatorID != nil { + where, args = append(where, "`creator_id` = ?"), append(args, *find.CreatorID) + } + if find.ContentID != nil { + where, args = append(where, "`content_id` = ?"), append(args, *find.ContentID) + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + id, + created_ts, + creator_id, + content_id, + reaction_type + FROM reaction + WHERE `+strings.Join(where, " AND ")+` + ORDER BY id DESC`, + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*storepb.Reaction{} + for rows.Next() { + reaction := &storepb.Reaction{} + var reactionType string + if err := rows.Scan( + &reaction.Id, + &reaction.CreatedTs, + &reaction.CreatorId, + &reaction.ContentId, + &reactionType, + ); err != nil { + return nil, err + } + reaction.ReactionType = storepb.Reaction_Type(storepb.Reaction_Type_value[reactionType]) + list = append(list, reaction) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error { + _, err := d.db.ExecContext(ctx, "DELETE FROM `reaction` WHERE `id` = ?", delete.ID) + return err +} diff --git a/store/db/postgres/reaction.go b/store/db/postgres/reaction.go new file mode 100644 index 00000000..e1ffefb3 --- /dev/null +++ b/store/db/postgres/reaction.go @@ -0,0 +1,82 @@ +package postgres + +import ( + "context" + "strings" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateReaction(ctx context.Context, create *storepb.Reaction) (*storepb.Reaction, error) { + fields := []string{"creator_id", "content_id", "reaction_type"} + args := []interface{}{create.CreatorId, create.ContentId, create.ReactionType.String()} + stmt := "INSERT INTO reaction (" + 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 + } + + reaction := create + return reaction, nil +} + +func (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*storepb.Reaction, error) { + where, args := []string{"1 = 1"}, []interface{}{} + if find.ID != nil { + where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID) + } + if find.CreatorID != nil { + where, args = append(where, "creator_id = "+placeholder(len(args)+1)), append(args, *find.CreatorID) + } + if find.ContentID != nil { + where, args = append(where, "content_id = "+placeholder(len(args)+1)), append(args, *find.ContentID) + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + id, + created_ts, + creator_id, + content_id, + reaction_type + FROM reaction + WHERE `+strings.Join(where, " AND ")+` + ORDER BY id DESC`, + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*storepb.Reaction{} + for rows.Next() { + reaction := &storepb.Reaction{} + var reactionType string + if err := rows.Scan( + &reaction.Id, + &reaction.CreatedTs, + &reaction.CreatorId, + &reaction.ContentId, + &reactionType, + ); err != nil { + return nil, err + } + reaction.ReactionType = storepb.Reaction_Type(storepb.Reaction_Type_value[reactionType]) + list = append(list, reaction) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error { + _, err := d.db.ExecContext(ctx, "DELETE FROM reaction WHERE id = $1", delete.ID) + return err +} diff --git a/store/db/sqlite/migration/dev/LATEST__SCHEMA.sql b/store/db/sqlite/migration/dev/LATEST__SCHEMA.sql index 5ddce546..33338db6 100644 --- a/store/db/sqlite/migration/dev/LATEST__SCHEMA.sql +++ b/store/db/sqlite/migration/dev/LATEST__SCHEMA.sql @@ -144,3 +144,11 @@ CREATE TABLE webhook ( ); CREATE INDEX idx_webhook_creator_id ON webhook (creator_id); + +CREATE TABLE reaction ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + creator_id INTEGER NOT NULL, + content_id TEXT NOT NULL, + reaction_type TEXT NOT NULL +); diff --git a/store/db/sqlite/reaction.go b/store/db/sqlite/reaction.go new file mode 100644 index 00000000..3d1bd8b6 --- /dev/null +++ b/store/db/sqlite/reaction.go @@ -0,0 +1,83 @@ +package sqlite + +import ( + "context" + "strings" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateReaction(ctx context.Context, create *storepb.Reaction) (*storepb.Reaction, error) { + fields := []string{"`creator_id`", "`content_id`", "`reaction_type`"} + placeholder := []string{"?", "?", "?"} + args := []interface{}{create.CreatorId, create.ContentId, create.ReactionType.String()} + stmt := "INSERT INTO `reaction` (" + 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 + } + + reaction := create + return reaction, nil +} + +func (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*storepb.Reaction, error) { + where, args := []string{"1 = 1"}, []interface{}{} + if find.ID != nil { + where, args = append(where, "id = ?"), append(args, *find.ID) + } + if find.CreatorID != nil { + where, args = append(where, "creator_id = ?"), append(args, *find.CreatorID) + } + if find.ContentID != nil { + where, args = append(where, "content_id = ?"), append(args, *find.ContentID) + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + id, + created_ts, + creator_id, + content_id, + reaction_type + FROM reaction + WHERE `+strings.Join(where, " AND ")+` + ORDER BY id DESC`, + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*storepb.Reaction{} + for rows.Next() { + reaction := &storepb.Reaction{} + var reactionType string + if err := rows.Scan( + &reaction.Id, + &reaction.CreatedTs, + &reaction.CreatorId, + &reaction.ContentId, + &reactionType, + ); err != nil { + return nil, err + } + reaction.ReactionType = storepb.Reaction_Type(storepb.Reaction_Type_value[reactionType]) + list = append(list, reaction) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error { + _, err := d.db.ExecContext(ctx, "DELETE FROM `reaction` WHERE `id` = ?", delete.ID) + return err +} diff --git a/store/db/sqlite/webhook.go b/store/db/sqlite/webhook.go index 3047d94b..21df940d 100644 --- a/store/db/sqlite/webhook.go +++ b/store/db/sqlite/webhook.go @@ -31,10 +31,10 @@ func (d *DB) CreateWebhook(ctx context.Context, create *storepb.Webhook) (*store func (d *DB) ListWebhooks(ctx context.Context, find *store.FindWebhook) ([]*storepb.Webhook, error) { where, args := []string{"1 = 1"}, []any{} if find.ID != nil { - where, args = append(where, "id = ?"), append(args, *find.ID) + where, args = append(where, "`id` = ?"), append(args, *find.ID) } if find.CreatorID != nil { - where, args = append(where, "creator_id = ?"), append(args, *find.CreatorID) + where, args = append(where, "`creator_id` = ?"), append(args, *find.CreatorID) } rows, err := d.db.QueryContext(ctx, ` diff --git a/store/driver.go b/store/driver.go index ad86a66b..f06c02b4 100644 --- a/store/driver.go +++ b/store/driver.go @@ -91,4 +91,9 @@ type Driver interface { ListWebhooks(ctx context.Context, find *FindWebhook) ([]*storepb.Webhook, error) UpdateWebhook(ctx context.Context, update *UpdateWebhook) (*storepb.Webhook, error) DeleteWebhook(ctx context.Context, delete *DeleteWebhook) error + + // Reaction model related methods. + CreateReaction(ctx context.Context, create *storepb.Reaction) (*storepb.Reaction, error) + ListReactions(ctx context.Context, find *FindReaction) ([]*storepb.Reaction, error) + DeleteReaction(ctx context.Context, delete *DeleteReaction) error } diff --git a/store/reaction.go b/store/reaction.go new file mode 100644 index 00000000..95e0846b --- /dev/null +++ b/store/reaction.go @@ -0,0 +1,29 @@ +package store + +import ( + "context" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +type FindReaction struct { + ID *int32 + CreatorID *int32 + ContentID *string +} + +type DeleteReaction struct { + ID int32 +} + +func (s *Store) CreateReaction(ctx context.Context, create *storepb.Reaction) (*storepb.Reaction, error) { + return s.driver.CreateReaction(ctx, create) +} + +func (s *Store) ListReactions(ctx context.Context, find *FindReaction) ([]*storepb.Reaction, error) { + return s.driver.ListReactions(ctx, find) +} + +func (s *Store) DeleteReaction(ctx context.Context, delete *DeleteReaction) error { + return s.driver.DeleteReaction(ctx, delete) +} diff --git a/test/store/reaction_test.go b/test/store/reaction_test.go new file mode 100644 index 00000000..ceac828d --- /dev/null +++ b/test/store/reaction_test.go @@ -0,0 +1,49 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func TestReactionStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + + contentID := "test_content_id" + reaction, err := ts.CreateReaction(ctx, &storepb.Reaction{ + CreatorId: user.ID, + ContentId: contentID, + ReactionType: storepb.Reaction_HEART, + }) + require.NoError(t, err) + require.NotNil(t, reaction) + require.NotEmpty(t, reaction.Id) + + reactions, err := ts.ListReactions(ctx, &store.FindReaction{ + ContentID: &contentID, + }) + require.NoError(t, err) + require.Len(t, reactions, 1) + require.Equal(t, reaction, reactions[0]) + + err = ts.DeleteReaction(ctx, &store.DeleteReaction{ + ID: reaction.Id, + }) + require.NoError(t, err) + + reactions, err = ts.ListReactions(ctx, &store.FindReaction{ + ContentID: &contentID, + }) + require.NoError(t, err) + require.Len(t, reactions, 0) + + ts.Close() +}