refactor: nest reaction resource names under memos

This commit is contained in:
Johnny 2025-12-30 23:29:54 +08:00
parent c2aea5a4b7
commit d7284fe867
8 changed files with 63 additions and 53 deletions

View File

@ -100,7 +100,7 @@ service MemoService {
}
// DeleteMemoReaction deletes a reaction for a memo.
rpc DeleteMemoReaction(DeleteMemoReactionRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{name=reactions/*}"};
option (google.api.http) = {delete: "/api/v1/{name=memos/*/reactions/*}"};
option (google.api.method_signature) = "name";
}
}
@ -115,14 +115,14 @@ enum Visibility {
message Reaction {
option (google.api.resource) = {
type: "memos.api.v1/Reaction"
pattern: "reactions/{reaction}"
pattern: "memos/{memo}/reactions/{reaction}"
name_field: "name"
singular: "reaction"
plural: "reactions"
};
// The resource name of the reaction.
// Format: reactions/{reaction}
// Format: memos/{memo}/reactions/{reaction}
string name = 1 [
(google.api.field_behavior) = OUTPUT_ONLY,
(google.api.field_behavior) = IDENTIFIER
@ -501,7 +501,7 @@ message UpsertMemoReactionRequest {
message DeleteMemoReactionRequest {
// Required. The resource name of the reaction to delete.
// Format: reactions/{reaction}
// Format: memos/{memo}/reactions/{reaction}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/Reaction"}

View File

@ -130,7 +130,7 @@ func (MemoRelation_Type) EnumDescriptor() ([]byte, []int) {
type Reaction struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The resource name of the reaction.
// Format: reactions/{reaction}
// Format: memos/{memo}/reactions/{reaction}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// The resource name of the creator.
// Format: users/{user}
@ -1627,7 +1627,7 @@ func (x *UpsertMemoReactionRequest) GetReaction() *Reaction {
type DeleteMemoReactionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The resource name of the reaction to delete.
// Format: reactions/{reaction}
// Format: memos/{memo}/reactions/{reaction}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@ -1799,7 +1799,7 @@ var File_api_v1_memo_service_proto protoreflect.FileDescriptor
const file_api_v1_memo_service_proto_rawDesc = "" +
"\n" +
"\x19api/v1/memo_service.proto\x12\fmemos.api.v1\x1a\x1fapi/v1/attachment_service.proto\x1a\x13api/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xce\x02\n" +
"\x19api/v1/memo_service.proto\x12\fmemos.api.v1\x1a\x1fapi/v1/attachment_service.proto\x1a\x13api/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xdb\x02\n" +
"\bReaction\x12\x1a\n" +
"\x04name\x18\x01 \x01(\tB\x06\xe0A\x03\xe0A\bR\x04name\x123\n" +
"\acreator\x18\x02 \x01(\tB\x19\xe0A\x03\xfaA\x13\n" +
@ -1809,8 +1809,8 @@ const file_api_v1_memo_service_proto_rawDesc = "" +
"\x11memos.api.v1/MemoR\tcontentId\x12(\n" +
"\rreaction_type\x18\x04 \x01(\tB\x03\xe0A\x02R\freactionType\x12@\n" +
"\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" +
"createTime:K\xeaAH\n" +
"\x15memos.api.v1/Reaction\x12\x14reactions/{reaction}\x1a\x04name*\treactions2\breaction\"\xd8\b\n" +
"createTime:X\xeaAU\n" +
"\x15memos.api.v1/Reaction\x12!memos/{memo}/reactions/{reaction}\x1a\x04name*\treactions2\breaction\"\xd8\b\n" +
"\x04Memo\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12.\n" +
"\x05state\x18\x02 \x01(\x0e2\x13.memos.api.v1.StateB\x03\xe0A\x02R\x05state\x123\n" +
@ -1953,7 +1953,7 @@ const file_api_v1_memo_service_proto_rawDesc = "" +
"\aPRIVATE\x10\x01\x12\r\n" +
"\tPROTECTED\x10\x02\x12\n" +
"\n" +
"\x06PUBLIC\x10\x032\xcb\x0e\n" +
"\x06PUBLIC\x10\x032\xd3\x0e\n" +
"\vMemoService\x12e\n" +
"\n" +
"CreateMemo\x12\x1f.memos.api.v1.CreateMemoRequest\x1a\x12.memos.api.v1.Memo\"\"\xdaA\x04memo\x82\xd3\xe4\x93\x02\x15:\x04memo\"\r/api/v1/memos\x12f\n" +
@ -1970,8 +1970,8 @@ const file_api_v1_memo_service_proto_rawDesc = "" +
"\x11CreateMemoComment\x12&.memos.api.v1.CreateMemoCommentRequest\x1a\x12.memos.api.v1.Memo\"?\xdaA\fname,comment\x82\xd3\xe4\x93\x02*:\acomment\"\x1f/api/v1/{name=memos/*}/comments\x12\x91\x01\n" +
"\x10ListMemoComments\x12%.memos.api.v1.ListMemoCommentsRequest\x1a&.memos.api.v1.ListMemoCommentsResponse\".\xdaA\x04name\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/{name=memos/*}/comments\x12\x95\x01\n" +
"\x11ListMemoReactions\x12&.memos.api.v1.ListMemoReactionsRequest\x1a'.memos.api.v1.ListMemoReactionsResponse\"/\xdaA\x04name\x82\xd3\xe4\x93\x02\"\x12 /api/v1/{name=memos/*}/reactions\x12\x89\x01\n" +
"\x12UpsertMemoReaction\x12'.memos.api.v1.UpsertMemoReactionRequest\x1a\x16.memos.api.v1.Reaction\"2\xdaA\x04name\x82\xd3\xe4\x93\x02%:\x01*\" /api/v1/{name=memos/*}/reactions\x12\x80\x01\n" +
"\x12DeleteMemoReaction\x12'.memos.api.v1.DeleteMemoReactionRequest\x1a\x16.google.protobuf.Empty\")\xdaA\x04name\x82\xd3\xe4\x93\x02\x1c*\x1a/api/v1/{name=reactions/*}B\xa8\x01\n" +
"\x12UpsertMemoReaction\x12'.memos.api.v1.UpsertMemoReactionRequest\x1a\x16.memos.api.v1.Reaction\"2\xdaA\x04name\x82\xd3\xe4\x93\x02%:\x01*\" /api/v1/{name=memos/*}/reactions\x12\x88\x01\n" +
"\x12DeleteMemoReaction\x12'.memos.api.v1.DeleteMemoReactionRequest\x1a\x16.google.protobuf.Empty\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$*\"/api/v1/{name=memos/*/reactions/*}B\xa8\x01\n" +
"\x10com.memos.api.v1B\x10MemoServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3"
var (

View File

@ -1001,7 +1001,7 @@ func RegisterMemoServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemoReaction", runtime.WithHTTPPathPattern("/api/v1/{name=reactions/*}"))
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemoReaction", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*/reactions/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@ -1280,7 +1280,7 @@ func RegisterMemoServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemoReaction", runtime.WithHTTPPathPattern("/api/v1/{name=reactions/*}"))
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemoReaction", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*/reactions/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
@ -1310,7 +1310,7 @@ var (
pattern_MemoService_ListMemoComments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "comments"}, ""))
pattern_MemoService_ListMemoReactions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "reactions"}, ""))
pattern_MemoService_UpsertMemoReaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "reactions"}, ""))
pattern_MemoService_DeleteMemoReaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "reactions", "name"}, ""))
pattern_MemoService_DeleteMemoReaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "memos", "reactions", "name"}, ""))
)
var (

View File

@ -960,6 +960,35 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/memos/{memo}/reactions/{reaction}:
delete:
tags:
- MemoService
description: DeleteMemoReaction deletes a reaction for a memo.
operationId: MemoService_DeleteMemoReaction
parameters:
- name: memo
in: path
description: The memo id.
required: true
schema:
type: string
- name: reaction
in: path
description: The reaction id.
required: true
schema:
type: string
responses:
"200":
description: OK
content: {}
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/memos/{memo}/relations:
get:
tags:
@ -1025,29 +1054,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/reactions/{reaction}:
delete:
tags:
- MemoService
description: DeleteMemoReaction deletes a reaction for a memo.
operationId: MemoService_DeleteMemoReaction
parameters:
- name: reaction
in: path
description: The reaction id.
required: true
schema:
type: string
responses:
"200":
description: OK
content: {}
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users:
get:
tags:
@ -2640,7 +2646,7 @@ components:
type: string
description: |-
The resource name of the reaction.
Format: reactions/{reaction}
Format: memos/{memo}/reactions/{reaction}
creator:
readOnly: true
type: string

View File

@ -63,7 +63,7 @@ func (s *APIV1Service) DeleteMemoReaction(ctx context.Context, request *v1pb.Del
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
reactionID, err := ExtractReactionIDFromName(request.Name)
_, reactionID, err := ExtractMemoReactionIDFromName(request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid reaction name: %v", err)
}
@ -95,8 +95,10 @@ func (s *APIV1Service) DeleteMemoReaction(ctx context.Context, request *v1pb.Del
func convertReactionFromStore(reaction *store.Reaction) *v1pb.Reaction {
reactionUID := fmt.Sprintf("%d", reaction.ID)
// Generate nested resource name: memos/{memo}/reactions/{reaction}
// reaction.ContentID already contains "memos/{memo}"
return &v1pb.Reaction{
Name: fmt.Sprintf("%s%s", ReactionNamePrefix, reactionUID),
Name: fmt.Sprintf("%s/%s%s", reaction.ContentID, ReactionNamePrefix, reactionUID),
Creator: fmt.Sprintf("%s%d", UserNamePrefix, reaction.CreatorID),
ContentId: reaction.ContentID,
ReactionType: reaction.ReactionType,

View File

@ -105,18 +105,19 @@ func ExtractAttachmentUIDFromName(name string) (string, error) {
return id, nil
}
// ExtractReactionIDFromName returns the reaction ID from a resource name.
// e.g., "reactions/123" -> 123.
func ExtractReactionIDFromName(name string) (int32, error) {
tokens, err := GetNameParentTokens(name, ReactionNamePrefix)
// ExtractMemoReactionIDFromName returns the memo UID and reaction ID from a resource name.
// e.g., "memos/abc/reactions/123" -> ("abc", 123).
func ExtractMemoReactionIDFromName(name string) (string, int32, error) {
tokens, err := GetNameParentTokens(name, MemoNamePrefix, ReactionNamePrefix)
if err != nil {
return 0, err
return "", 0, err
}
id, err := util.ConvertStringToInt32(tokens[0])
memoUID := tokens[0]
reactionID, err := util.ConvertStringToInt32(tokens[1])
if err != nil {
return 0, errors.Errorf("invalid reaction ID %q", tokens[0])
return "", 0, errors.Errorf("invalid reaction ID %q", tokens[1])
}
return id, nil
return memoUID, reactionID, nil
}
// ExtractInboxIDFromName returns the inbox ID from a resource name.

View File

@ -183,8 +183,9 @@ func TestDeleteMemoReaction(t *testing.T) {
// Try to delete non-existent reaction - should fail with permission denied
// (not "not found" to avoid information disclosure)
// Use new nested resource format: memos/{memo}/reactions/{reaction}
_, err = ts.Service.DeleteMemoReaction(userCtx, &apiv1.DeleteMemoReactionRequest{
Name: "reactions/99999",
Name: "memos/nonexistent/reactions/99999",
})
require.Error(t, err)
require.Contains(t, err.Error(), "permission denied")

File diff suppressed because one or more lines are too long