diff --git a/internal/filter/schema.go b/internal/filter/schema.go index d9fdd9c3a534b..4e88a3e1c7a76 100644 --- a/internal/filter/schema.go +++ b/internal/filter/schema.go @@ -122,6 +122,17 @@ func NewSchema() Schema { CompareNeq: true, }, }, + "group_id": { + Name: "group_id", + Kind: FieldKindScalar, + Type: FieldTypeInt, + Column: Column{Table: "memo", Name: "group_id"}, + Expressions: map[DialectName]string{}, + AllowedComparisonOps: map[ComparisonOperator]bool{ + CompareEq: true, + CompareNeq: true, + }, + }, "created_ts": { Name: "created_ts", Kind: FieldKindScalar, @@ -233,6 +244,7 @@ func NewSchema() Schema { cel.Variable("content", cel.StringType), cel.Variable("creator", cel.StringType), cel.Variable("creator_id", cel.IntType), + cel.Variable("group_id", cel.IntType), cel.Variable("created_ts", cel.TimestampType), cel.Variable("updated_ts", cel.TimestampType), cel.Variable("pinned", cel.BoolType), diff --git a/proto/api/v1/group_service.proto b/proto/api/v1/group_service.proto new file mode 100644 index 0000000000000..a92a39b89724f --- /dev/null +++ b/proto/api/v1/group_service.proto @@ -0,0 +1,186 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "gen/api/v1"; + +service GroupService { + // CreateGroup creates a new group. + rpc CreateGroup(CreateGroupRequest) returns (Group) { + option (google.api.http) = { + post: "/api/v1/groups" + body: "group" + }; + } + + // ListGroups lists all groups. + rpc ListGroups(ListGroupsRequest) returns (ListGroupsResponse) { + option (google.api.http) = {get: "/api/v1/groups"}; + } + + // GetGroup gets a group by name. + rpc GetGroup(GetGroupRequest) returns (Group) { + option (google.api.http) = {get: "/api/v1/{name=groups/*}"}; + } + + // UpdateGroup updates a group. + rpc UpdateGroup(UpdateGroupRequest) returns (Group) { + option (google.api.http) = { + patch: "/api/v1/{group.name=groups/*}" + body: "group" + }; + } + + // DeleteGroup deletes a group. + rpc DeleteGroup(DeleteGroupRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=groups/*}"}; + } + + // AddGroupMember adds a member to a group. + rpc AddGroupMember(AddGroupMemberRequest) returns (GroupMember) { + option (google.api.http) = { + post: "/api/v1/{parent=groups/*}/members" + body: "member" + }; + } + + // ListGroupMembers lists all members of a group. + rpc ListGroupMembers(ListGroupMembersRequest) returns (ListGroupMembersResponse) { + option (google.api.http) = {get: "/api/v1/{parent=groups/*}/members"}; + } + + // UpdateGroupMember updates a member of a group. + rpc UpdateGroupMember(UpdateGroupMemberRequest) returns (GroupMember) { + option (google.api.http) = { + patch: "/api/v1/{member.name=groups/*/members/*}" + body: "member" + }; + } + + // RemoveGroupMember removes a member from a group. + rpc RemoveGroupMember(RemoveGroupMemberRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=groups/*/members/*}"}; + } +} + +message Group { + option (google.api.resource) = { + type: "memos.api.v1/Group" + pattern: "groups/{group}" + singular: "group" + plural: "groups" + }; + + // The resource name of the group. + // Format: groups/{group} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The display name of the group. + string display_name = 2 [(google.api.field_behavior) = REQUIRED]; + + // The description of the group. + string description = 3 [(google.api.field_behavior) = OPTIONAL]; + + // The name of the creator. + // Format: users/{user} + string creator = 4 [ + (google.api.field_behavior) = OUTPUT_ONLY, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; + + // The creation timestamp. + google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; +} + +message GroupMember { + option (google.api.resource) = { + type: "memos.api.v1/GroupMember" + pattern: "groups/{group}/members/{member}" + singular: "member" + plural: "members" + }; + + // The resource name of the group member. + // Format: groups/{group}/members/{member} + // The {member} segment is the user ID. + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The role of the member in the group. + enum Role { + ROLE_UNSPECIFIED = 0; + MEMBER = 1; + ADMIN = 2; + OWNER = 3; + } + Role role = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message CreateGroupRequest { + Group group = 1 [(google.api.field_behavior) = REQUIRED]; +} + +message ListGroupsRequest { + // Optional pagination, etc. can be added later. +} + +message ListGroupsResponse { + repeated Group groups = 1; +} + +message GetGroupRequest { + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Group"} + ]; +} + +message UpdateGroupRequest { + Group group = 1 [(google.api.field_behavior) = REQUIRED]; + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message DeleteGroupRequest { + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Group"} + ]; +} + +message AddGroupMemberRequest { + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Group"} + ]; + GroupMember member = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message ListGroupMembersRequest { + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Group"} + ]; +} + +message ListGroupMembersResponse { + repeated GroupMember members = 1; +} + +message UpdateGroupMemberRequest { + GroupMember member = 1 [(google.api.field_behavior) = REQUIRED]; + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message RemoveGroupMemberRequest { + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/GroupMember"} + ]; +} diff --git a/proto/api/v1/memo_service.proto b/proto/api/v1/memo_service.proto index ce56832716e85..c81e7ea910977 100644 --- a/proto/api/v1/memo_service.proto +++ b/proto/api/v1/memo_service.proto @@ -155,6 +155,7 @@ enum Visibility { PROTECTED = 2; // PUBLIC: anyone, including anonymous visitors, can read the memo. PUBLIC = 3; + GROUP = 4; } message Reaction { @@ -268,6 +269,13 @@ message Memo { // Optional. The location of the memo. optional Location location = 18 [(google.api.field_behavior) = OPTIONAL]; + // Optional. The resource name of the group. + // Format: groups/{group} + optional string group = 19 [ + (google.api.field_behavior) = OPTIONAL, + (google.api.resource_reference) = {type: "memos.api.v1/Group"} + ]; + // Computed properties of a memo. message Property { bool has_link = 1; diff --git a/proto/gen/api/v1/apiv1connect/group_service.connect.go b/proto/gen/api/v1/apiv1connect/group_service.connect.go new file mode 100644 index 0000000000000..8862f8a63cbc8 --- /dev/null +++ b/proto/gen/api/v1/apiv1connect/group_service.connect.go @@ -0,0 +1,358 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: api/v1/group_service.proto + +package apiv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/usememos/memos/proto/gen/api/v1" + emptypb "google.golang.org/protobuf/types/known/emptypb" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // GroupServiceName is the fully-qualified name of the GroupService service. + GroupServiceName = "memos.api.v1.GroupService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // GroupServiceCreateGroupProcedure is the fully-qualified name of the GroupService's CreateGroup + // RPC. + GroupServiceCreateGroupProcedure = "/memos.api.v1.GroupService/CreateGroup" + // GroupServiceListGroupsProcedure is the fully-qualified name of the GroupService's ListGroups RPC. + GroupServiceListGroupsProcedure = "/memos.api.v1.GroupService/ListGroups" + // GroupServiceGetGroupProcedure is the fully-qualified name of the GroupService's GetGroup RPC. + GroupServiceGetGroupProcedure = "/memos.api.v1.GroupService/GetGroup" + // GroupServiceUpdateGroupProcedure is the fully-qualified name of the GroupService's UpdateGroup + // RPC. + GroupServiceUpdateGroupProcedure = "/memos.api.v1.GroupService/UpdateGroup" + // GroupServiceDeleteGroupProcedure is the fully-qualified name of the GroupService's DeleteGroup + // RPC. + GroupServiceDeleteGroupProcedure = "/memos.api.v1.GroupService/DeleteGroup" + // GroupServiceAddGroupMemberProcedure is the fully-qualified name of the GroupService's + // AddGroupMember RPC. + GroupServiceAddGroupMemberProcedure = "/memos.api.v1.GroupService/AddGroupMember" + // GroupServiceListGroupMembersProcedure is the fully-qualified name of the GroupService's + // ListGroupMembers RPC. + GroupServiceListGroupMembersProcedure = "/memos.api.v1.GroupService/ListGroupMembers" + // GroupServiceUpdateGroupMemberProcedure is the fully-qualified name of the GroupService's + // UpdateGroupMember RPC. + GroupServiceUpdateGroupMemberProcedure = "/memos.api.v1.GroupService/UpdateGroupMember" + // GroupServiceRemoveGroupMemberProcedure is the fully-qualified name of the GroupService's + // RemoveGroupMember RPC. + GroupServiceRemoveGroupMemberProcedure = "/memos.api.v1.GroupService/RemoveGroupMember" +) + +// GroupServiceClient is a client for the memos.api.v1.GroupService service. +type GroupServiceClient interface { + // CreateGroup creates a new group. + CreateGroup(context.Context, *connect.Request[v1.CreateGroupRequest]) (*connect.Response[v1.Group], error) + // ListGroups lists all groups. + ListGroups(context.Context, *connect.Request[v1.ListGroupsRequest]) (*connect.Response[v1.ListGroupsResponse], error) + // GetGroup gets a group by name. + GetGroup(context.Context, *connect.Request[v1.GetGroupRequest]) (*connect.Response[v1.Group], error) + // UpdateGroup updates a group. + UpdateGroup(context.Context, *connect.Request[v1.UpdateGroupRequest]) (*connect.Response[v1.Group], error) + // DeleteGroup deletes a group. + DeleteGroup(context.Context, *connect.Request[v1.DeleteGroupRequest]) (*connect.Response[emptypb.Empty], error) + // AddGroupMember adds a member to a group. + AddGroupMember(context.Context, *connect.Request[v1.AddGroupMemberRequest]) (*connect.Response[v1.GroupMember], error) + // ListGroupMembers lists all members of a group. + ListGroupMembers(context.Context, *connect.Request[v1.ListGroupMembersRequest]) (*connect.Response[v1.ListGroupMembersResponse], error) + // UpdateGroupMember updates a member of a group. + UpdateGroupMember(context.Context, *connect.Request[v1.UpdateGroupMemberRequest]) (*connect.Response[v1.GroupMember], error) + // RemoveGroupMember removes a member from a group. + RemoveGroupMember(context.Context, *connect.Request[v1.RemoveGroupMemberRequest]) (*connect.Response[emptypb.Empty], error) +} + +// NewGroupServiceClient constructs a client for the memos.api.v1.GroupService service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewGroupServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) GroupServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + groupServiceMethods := v1.File_api_v1_group_service_proto.Services().ByName("GroupService").Methods() + return &groupServiceClient{ + createGroup: connect.NewClient[v1.CreateGroupRequest, v1.Group]( + httpClient, + baseURL+GroupServiceCreateGroupProcedure, + connect.WithSchema(groupServiceMethods.ByName("CreateGroup")), + connect.WithClientOptions(opts...), + ), + listGroups: connect.NewClient[v1.ListGroupsRequest, v1.ListGroupsResponse]( + httpClient, + baseURL+GroupServiceListGroupsProcedure, + connect.WithSchema(groupServiceMethods.ByName("ListGroups")), + connect.WithClientOptions(opts...), + ), + getGroup: connect.NewClient[v1.GetGroupRequest, v1.Group]( + httpClient, + baseURL+GroupServiceGetGroupProcedure, + connect.WithSchema(groupServiceMethods.ByName("GetGroup")), + connect.WithClientOptions(opts...), + ), + updateGroup: connect.NewClient[v1.UpdateGroupRequest, v1.Group]( + httpClient, + baseURL+GroupServiceUpdateGroupProcedure, + connect.WithSchema(groupServiceMethods.ByName("UpdateGroup")), + connect.WithClientOptions(opts...), + ), + deleteGroup: connect.NewClient[v1.DeleteGroupRequest, emptypb.Empty]( + httpClient, + baseURL+GroupServiceDeleteGroupProcedure, + connect.WithSchema(groupServiceMethods.ByName("DeleteGroup")), + connect.WithClientOptions(opts...), + ), + addGroupMember: connect.NewClient[v1.AddGroupMemberRequest, v1.GroupMember]( + httpClient, + baseURL+GroupServiceAddGroupMemberProcedure, + connect.WithSchema(groupServiceMethods.ByName("AddGroupMember")), + connect.WithClientOptions(opts...), + ), + listGroupMembers: connect.NewClient[v1.ListGroupMembersRequest, v1.ListGroupMembersResponse]( + httpClient, + baseURL+GroupServiceListGroupMembersProcedure, + connect.WithSchema(groupServiceMethods.ByName("ListGroupMembers")), + connect.WithClientOptions(opts...), + ), + updateGroupMember: connect.NewClient[v1.UpdateGroupMemberRequest, v1.GroupMember]( + httpClient, + baseURL+GroupServiceUpdateGroupMemberProcedure, + connect.WithSchema(groupServiceMethods.ByName("UpdateGroupMember")), + connect.WithClientOptions(opts...), + ), + removeGroupMember: connect.NewClient[v1.RemoveGroupMemberRequest, emptypb.Empty]( + httpClient, + baseURL+GroupServiceRemoveGroupMemberProcedure, + connect.WithSchema(groupServiceMethods.ByName("RemoveGroupMember")), + connect.WithClientOptions(opts...), + ), + } +} + +// groupServiceClient implements GroupServiceClient. +type groupServiceClient struct { + createGroup *connect.Client[v1.CreateGroupRequest, v1.Group] + listGroups *connect.Client[v1.ListGroupsRequest, v1.ListGroupsResponse] + getGroup *connect.Client[v1.GetGroupRequest, v1.Group] + updateGroup *connect.Client[v1.UpdateGroupRequest, v1.Group] + deleteGroup *connect.Client[v1.DeleteGroupRequest, emptypb.Empty] + addGroupMember *connect.Client[v1.AddGroupMemberRequest, v1.GroupMember] + listGroupMembers *connect.Client[v1.ListGroupMembersRequest, v1.ListGroupMembersResponse] + updateGroupMember *connect.Client[v1.UpdateGroupMemberRequest, v1.GroupMember] + removeGroupMember *connect.Client[v1.RemoveGroupMemberRequest, emptypb.Empty] +} + +// CreateGroup calls memos.api.v1.GroupService.CreateGroup. +func (c *groupServiceClient) CreateGroup(ctx context.Context, req *connect.Request[v1.CreateGroupRequest]) (*connect.Response[v1.Group], error) { + return c.createGroup.CallUnary(ctx, req) +} + +// ListGroups calls memos.api.v1.GroupService.ListGroups. +func (c *groupServiceClient) ListGroups(ctx context.Context, req *connect.Request[v1.ListGroupsRequest]) (*connect.Response[v1.ListGroupsResponse], error) { + return c.listGroups.CallUnary(ctx, req) +} + +// GetGroup calls memos.api.v1.GroupService.GetGroup. +func (c *groupServiceClient) GetGroup(ctx context.Context, req *connect.Request[v1.GetGroupRequest]) (*connect.Response[v1.Group], error) { + return c.getGroup.CallUnary(ctx, req) +} + +// UpdateGroup calls memos.api.v1.GroupService.UpdateGroup. +func (c *groupServiceClient) UpdateGroup(ctx context.Context, req *connect.Request[v1.UpdateGroupRequest]) (*connect.Response[v1.Group], error) { + return c.updateGroup.CallUnary(ctx, req) +} + +// DeleteGroup calls memos.api.v1.GroupService.DeleteGroup. +func (c *groupServiceClient) DeleteGroup(ctx context.Context, req *connect.Request[v1.DeleteGroupRequest]) (*connect.Response[emptypb.Empty], error) { + return c.deleteGroup.CallUnary(ctx, req) +} + +// AddGroupMember calls memos.api.v1.GroupService.AddGroupMember. +func (c *groupServiceClient) AddGroupMember(ctx context.Context, req *connect.Request[v1.AddGroupMemberRequest]) (*connect.Response[v1.GroupMember], error) { + return c.addGroupMember.CallUnary(ctx, req) +} + +// ListGroupMembers calls memos.api.v1.GroupService.ListGroupMembers. +func (c *groupServiceClient) ListGroupMembers(ctx context.Context, req *connect.Request[v1.ListGroupMembersRequest]) (*connect.Response[v1.ListGroupMembersResponse], error) { + return c.listGroupMembers.CallUnary(ctx, req) +} + +// UpdateGroupMember calls memos.api.v1.GroupService.UpdateGroupMember. +func (c *groupServiceClient) UpdateGroupMember(ctx context.Context, req *connect.Request[v1.UpdateGroupMemberRequest]) (*connect.Response[v1.GroupMember], error) { + return c.updateGroupMember.CallUnary(ctx, req) +} + +// RemoveGroupMember calls memos.api.v1.GroupService.RemoveGroupMember. +func (c *groupServiceClient) RemoveGroupMember(ctx context.Context, req *connect.Request[v1.RemoveGroupMemberRequest]) (*connect.Response[emptypb.Empty], error) { + return c.removeGroupMember.CallUnary(ctx, req) +} + +// GroupServiceHandler is an implementation of the memos.api.v1.GroupService service. +type GroupServiceHandler interface { + // CreateGroup creates a new group. + CreateGroup(context.Context, *connect.Request[v1.CreateGroupRequest]) (*connect.Response[v1.Group], error) + // ListGroups lists all groups. + ListGroups(context.Context, *connect.Request[v1.ListGroupsRequest]) (*connect.Response[v1.ListGroupsResponse], error) + // GetGroup gets a group by name. + GetGroup(context.Context, *connect.Request[v1.GetGroupRequest]) (*connect.Response[v1.Group], error) + // UpdateGroup updates a group. + UpdateGroup(context.Context, *connect.Request[v1.UpdateGroupRequest]) (*connect.Response[v1.Group], error) + // DeleteGroup deletes a group. + DeleteGroup(context.Context, *connect.Request[v1.DeleteGroupRequest]) (*connect.Response[emptypb.Empty], error) + // AddGroupMember adds a member to a group. + AddGroupMember(context.Context, *connect.Request[v1.AddGroupMemberRequest]) (*connect.Response[v1.GroupMember], error) + // ListGroupMembers lists all members of a group. + ListGroupMembers(context.Context, *connect.Request[v1.ListGroupMembersRequest]) (*connect.Response[v1.ListGroupMembersResponse], error) + // UpdateGroupMember updates a member of a group. + UpdateGroupMember(context.Context, *connect.Request[v1.UpdateGroupMemberRequest]) (*connect.Response[v1.GroupMember], error) + // RemoveGroupMember removes a member from a group. + RemoveGroupMember(context.Context, *connect.Request[v1.RemoveGroupMemberRequest]) (*connect.Response[emptypb.Empty], error) +} + +// NewGroupServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewGroupServiceHandler(svc GroupServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + groupServiceMethods := v1.File_api_v1_group_service_proto.Services().ByName("GroupService").Methods() + groupServiceCreateGroupHandler := connect.NewUnaryHandler( + GroupServiceCreateGroupProcedure, + svc.CreateGroup, + connect.WithSchema(groupServiceMethods.ByName("CreateGroup")), + connect.WithHandlerOptions(opts...), + ) + groupServiceListGroupsHandler := connect.NewUnaryHandler( + GroupServiceListGroupsProcedure, + svc.ListGroups, + connect.WithSchema(groupServiceMethods.ByName("ListGroups")), + connect.WithHandlerOptions(opts...), + ) + groupServiceGetGroupHandler := connect.NewUnaryHandler( + GroupServiceGetGroupProcedure, + svc.GetGroup, + connect.WithSchema(groupServiceMethods.ByName("GetGroup")), + connect.WithHandlerOptions(opts...), + ) + groupServiceUpdateGroupHandler := connect.NewUnaryHandler( + GroupServiceUpdateGroupProcedure, + svc.UpdateGroup, + connect.WithSchema(groupServiceMethods.ByName("UpdateGroup")), + connect.WithHandlerOptions(opts...), + ) + groupServiceDeleteGroupHandler := connect.NewUnaryHandler( + GroupServiceDeleteGroupProcedure, + svc.DeleteGroup, + connect.WithSchema(groupServiceMethods.ByName("DeleteGroup")), + connect.WithHandlerOptions(opts...), + ) + groupServiceAddGroupMemberHandler := connect.NewUnaryHandler( + GroupServiceAddGroupMemberProcedure, + svc.AddGroupMember, + connect.WithSchema(groupServiceMethods.ByName("AddGroupMember")), + connect.WithHandlerOptions(opts...), + ) + groupServiceListGroupMembersHandler := connect.NewUnaryHandler( + GroupServiceListGroupMembersProcedure, + svc.ListGroupMembers, + connect.WithSchema(groupServiceMethods.ByName("ListGroupMembers")), + connect.WithHandlerOptions(opts...), + ) + groupServiceUpdateGroupMemberHandler := connect.NewUnaryHandler( + GroupServiceUpdateGroupMemberProcedure, + svc.UpdateGroupMember, + connect.WithSchema(groupServiceMethods.ByName("UpdateGroupMember")), + connect.WithHandlerOptions(opts...), + ) + groupServiceRemoveGroupMemberHandler := connect.NewUnaryHandler( + GroupServiceRemoveGroupMemberProcedure, + svc.RemoveGroupMember, + connect.WithSchema(groupServiceMethods.ByName("RemoveGroupMember")), + connect.WithHandlerOptions(opts...), + ) + return "/memos.api.v1.GroupService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case GroupServiceCreateGroupProcedure: + groupServiceCreateGroupHandler.ServeHTTP(w, r) + case GroupServiceListGroupsProcedure: + groupServiceListGroupsHandler.ServeHTTP(w, r) + case GroupServiceGetGroupProcedure: + groupServiceGetGroupHandler.ServeHTTP(w, r) + case GroupServiceUpdateGroupProcedure: + groupServiceUpdateGroupHandler.ServeHTTP(w, r) + case GroupServiceDeleteGroupProcedure: + groupServiceDeleteGroupHandler.ServeHTTP(w, r) + case GroupServiceAddGroupMemberProcedure: + groupServiceAddGroupMemberHandler.ServeHTTP(w, r) + case GroupServiceListGroupMembersProcedure: + groupServiceListGroupMembersHandler.ServeHTTP(w, r) + case GroupServiceUpdateGroupMemberProcedure: + groupServiceUpdateGroupMemberHandler.ServeHTTP(w, r) + case GroupServiceRemoveGroupMemberProcedure: + groupServiceRemoveGroupMemberHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedGroupServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedGroupServiceHandler struct{} + +func (UnimplementedGroupServiceHandler) CreateGroup(context.Context, *connect.Request[v1.CreateGroupRequest]) (*connect.Response[v1.Group], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.GroupService.CreateGroup is not implemented")) +} + +func (UnimplementedGroupServiceHandler) ListGroups(context.Context, *connect.Request[v1.ListGroupsRequest]) (*connect.Response[v1.ListGroupsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.GroupService.ListGroups is not implemented")) +} + +func (UnimplementedGroupServiceHandler) GetGroup(context.Context, *connect.Request[v1.GetGroupRequest]) (*connect.Response[v1.Group], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.GroupService.GetGroup is not implemented")) +} + +func (UnimplementedGroupServiceHandler) UpdateGroup(context.Context, *connect.Request[v1.UpdateGroupRequest]) (*connect.Response[v1.Group], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.GroupService.UpdateGroup is not implemented")) +} + +func (UnimplementedGroupServiceHandler) DeleteGroup(context.Context, *connect.Request[v1.DeleteGroupRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.GroupService.DeleteGroup is not implemented")) +} + +func (UnimplementedGroupServiceHandler) AddGroupMember(context.Context, *connect.Request[v1.AddGroupMemberRequest]) (*connect.Response[v1.GroupMember], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.GroupService.AddGroupMember is not implemented")) +} + +func (UnimplementedGroupServiceHandler) ListGroupMembers(context.Context, *connect.Request[v1.ListGroupMembersRequest]) (*connect.Response[v1.ListGroupMembersResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.GroupService.ListGroupMembers is not implemented")) +} + +func (UnimplementedGroupServiceHandler) UpdateGroupMember(context.Context, *connect.Request[v1.UpdateGroupMemberRequest]) (*connect.Response[v1.GroupMember], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.GroupService.UpdateGroupMember is not implemented")) +} + +func (UnimplementedGroupServiceHandler) RemoveGroupMember(context.Context, *connect.Request[v1.RemoveGroupMemberRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.GroupService.RemoveGroupMember is not implemented")) +} diff --git a/proto/gen/api/v1/group_service.pb.go b/proto/gen/api/v1/group_service.pb.go new file mode 100644 index 0000000000000..8e42a2dd0d4c2 --- /dev/null +++ b/proto/gen/api/v1/group_service.pb.go @@ -0,0 +1,879 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: api/v1/group_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + 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) +) + +// The role of the member in the group. +type GroupMember_Role int32 + +const ( + GroupMember_ROLE_UNSPECIFIED GroupMember_Role = 0 + GroupMember_MEMBER GroupMember_Role = 1 + GroupMember_ADMIN GroupMember_Role = 2 + GroupMember_OWNER GroupMember_Role = 3 +) + +// Enum value maps for GroupMember_Role. +var ( + GroupMember_Role_name = map[int32]string{ + 0: "ROLE_UNSPECIFIED", + 1: "MEMBER", + 2: "ADMIN", + 3: "OWNER", + } + GroupMember_Role_value = map[string]int32{ + "ROLE_UNSPECIFIED": 0, + "MEMBER": 1, + "ADMIN": 2, + "OWNER": 3, + } +) + +func (x GroupMember_Role) Enum() *GroupMember_Role { + p := new(GroupMember_Role) + *p = x + return p +} + +func (x GroupMember_Role) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (GroupMember_Role) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_group_service_proto_enumTypes[0].Descriptor() +} + +func (GroupMember_Role) Type() protoreflect.EnumType { + return &file_api_v1_group_service_proto_enumTypes[0] +} + +func (x GroupMember_Role) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use GroupMember_Role.Descriptor instead. +func (GroupMember_Role) EnumDescriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{1, 0} +} + +type Group struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the group. + // Format: groups/{group} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The display name of the group. + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // The description of the group. + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // The name of the creator. + // Format: users/{user} + Creator string `protobuf:"bytes,4,opt,name=creator,proto3" json:"creator,omitempty"` + // The creation timestamp. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Group) Reset() { + *x = Group{} + mi := &file_api_v1_group_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Group) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Group) ProtoMessage() {} + +func (x *Group) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_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 Group.ProtoReflect.Descriptor instead. +func (*Group) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{0} +} + +func (x *Group) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Group) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *Group) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Group) GetCreator() string { + if x != nil { + return x.Creator + } + return "" +} + +func (x *Group) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +type GroupMember struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the group member. + // Format: groups/{group}/members/{member} + // The {member} segment is the user ID. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Role GroupMember_Role `protobuf:"varint,2,opt,name=role,proto3,enum=memos.api.v1.GroupMember_Role" json:"role,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GroupMember) Reset() { + *x = GroupMember{} + mi := &file_api_v1_group_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GroupMember) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GroupMember) ProtoMessage() {} + +func (x *GroupMember) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_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 GroupMember.ProtoReflect.Descriptor instead. +func (*GroupMember) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{1} +} + +func (x *GroupMember) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GroupMember) GetRole() GroupMember_Role { + if x != nil { + return x.Role + } + return GroupMember_ROLE_UNSPECIFIED +} + +type CreateGroupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Group *Group `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateGroupRequest) Reset() { + *x = CreateGroupRequest{} + mi := &file_api_v1_group_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateGroupRequest) ProtoMessage() {} + +func (x *CreateGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[2] + 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 CreateGroupRequest.ProtoReflect.Descriptor instead. +func (*CreateGroupRequest) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{2} +} + +func (x *CreateGroupRequest) GetGroup() *Group { + if x != nil { + return x.Group + } + return nil +} + +type ListGroupsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGroupsRequest) Reset() { + *x = ListGroupsRequest{} + mi := &file_api_v1_group_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGroupsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGroupsRequest) ProtoMessage() {} + +func (x *ListGroupsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[3] + 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 ListGroupsRequest.ProtoReflect.Descriptor instead. +func (*ListGroupsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{3} +} + +type ListGroupsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Groups []*Group `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGroupsResponse) Reset() { + *x = ListGroupsResponse{} + mi := &file_api_v1_group_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGroupsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGroupsResponse) ProtoMessage() {} + +func (x *ListGroupsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[4] + 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 ListGroupsResponse.ProtoReflect.Descriptor instead. +func (*ListGroupsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{4} +} + +func (x *ListGroupsResponse) GetGroups() []*Group { + if x != nil { + return x.Groups + } + return nil +} + +type GetGroupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGroupRequest) Reset() { + *x = GetGroupRequest{} + mi := &file_api_v1_group_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGroupRequest) ProtoMessage() {} + +func (x *GetGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[5] + 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 GetGroupRequest.ProtoReflect.Descriptor instead. +func (*GetGroupRequest) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{5} +} + +func (x *GetGroupRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type UpdateGroupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Group *Group `protobuf:"bytes,1,opt,name=group,proto3" json:"group,omitempty"` + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateGroupRequest) Reset() { + *x = UpdateGroupRequest{} + mi := &file_api_v1_group_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateGroupRequest) ProtoMessage() {} + +func (x *UpdateGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[6] + 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 UpdateGroupRequest.ProtoReflect.Descriptor instead. +func (*UpdateGroupRequest) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateGroupRequest) GetGroup() *Group { + if x != nil { + return x.Group + } + return nil +} + +func (x *UpdateGroupRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +type DeleteGroupRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteGroupRequest) Reset() { + *x = DeleteGroupRequest{} + mi := &file_api_v1_group_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteGroupRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteGroupRequest) ProtoMessage() {} + +func (x *DeleteGroupRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[7] + 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 DeleteGroupRequest.ProtoReflect.Descriptor instead. +func (*DeleteGroupRequest) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{7} +} + +func (x *DeleteGroupRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type AddGroupMemberRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + Member *GroupMember `protobuf:"bytes,2,opt,name=member,proto3" json:"member,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddGroupMemberRequest) Reset() { + *x = AddGroupMemberRequest{} + mi := &file_api_v1_group_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddGroupMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddGroupMemberRequest) ProtoMessage() {} + +func (x *AddGroupMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[8] + 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 AddGroupMemberRequest.ProtoReflect.Descriptor instead. +func (*AddGroupMemberRequest) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{8} +} + +func (x *AddGroupMemberRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *AddGroupMemberRequest) GetMember() *GroupMember { + if x != nil { + return x.Member + } + return nil +} + +type ListGroupMembersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGroupMembersRequest) Reset() { + *x = ListGroupMembersRequest{} + mi := &file_api_v1_group_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGroupMembersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGroupMembersRequest) ProtoMessage() {} + +func (x *ListGroupMembersRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[9] + 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 ListGroupMembersRequest.ProtoReflect.Descriptor instead. +func (*ListGroupMembersRequest) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{9} +} + +func (x *ListGroupMembersRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +type ListGroupMembersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Members []*GroupMember `protobuf:"bytes,1,rep,name=members,proto3" json:"members,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGroupMembersResponse) Reset() { + *x = ListGroupMembersResponse{} + mi := &file_api_v1_group_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGroupMembersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGroupMembersResponse) ProtoMessage() {} + +func (x *ListGroupMembersResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[10] + 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 ListGroupMembersResponse.ProtoReflect.Descriptor instead. +func (*ListGroupMembersResponse) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{10} +} + +func (x *ListGroupMembersResponse) GetMembers() []*GroupMember { + if x != nil { + return x.Members + } + return nil +} + +type UpdateGroupMemberRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Member *GroupMember `protobuf:"bytes,1,opt,name=member,proto3" json:"member,omitempty"` + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateGroupMemberRequest) Reset() { + *x = UpdateGroupMemberRequest{} + mi := &file_api_v1_group_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateGroupMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateGroupMemberRequest) ProtoMessage() {} + +func (x *UpdateGroupMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[11] + 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 UpdateGroupMemberRequest.ProtoReflect.Descriptor instead. +func (*UpdateGroupMemberRequest) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{11} +} + +func (x *UpdateGroupMemberRequest) GetMember() *GroupMember { + if x != nil { + return x.Member + } + return nil +} + +func (x *UpdateGroupMemberRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +type RemoveGroupMemberRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveGroupMemberRequest) Reset() { + *x = RemoveGroupMemberRequest{} + mi := &file_api_v1_group_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveGroupMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveGroupMemberRequest) ProtoMessage() {} + +func (x *RemoveGroupMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_group_service_proto_msgTypes[12] + 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 RemoveGroupMemberRequest.ProtoReflect.Descriptor instead. +func (*RemoveGroupMemberRequest) Descriptor() ([]byte, []int) { + return file_api_v1_group_service_proto_rawDescGZIP(), []int{12} +} + +func (x *RemoveGroupMemberRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_api_v1_group_service_proto protoreflect.FileDescriptor + +const file_api_v1_group_service_proto_rawDesc = "" + + "\n" + + "\x1aapi/v1/group_service.proto\x12\fmemos.api.v1\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\"\x9e\x02\n" + + "\x05Group\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12&\n" + + "\fdisplay_name\x18\x02 \x01(\tB\x03\xe0A\x02R\vdisplayName\x12%\n" + + "\vdescription\x18\x03 \x01(\tB\x03\xe0A\x01R\vdescription\x123\n" + + "\acreator\x18\x04 \x01(\tB\x19\xe0A\x03\xfaA\x13\n" + + "\x11memos.api.v1/UserR\acreator\x12@\n" + + "\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "createTime:6\xeaA3\n" + + "\x12memos.api.v1/Group\x12\x0egroups/{group}*\x06groups2\x05group\"\xf0\x01\n" + + "\vGroupMember\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x127\n" + + "\x04role\x18\x02 \x01(\x0e2\x1e.memos.api.v1.GroupMember.RoleB\x03\xe0A\x02R\x04role\">\n" + + "\x04Role\x12\x14\n" + + "\x10ROLE_UNSPECIFIED\x10\x00\x12\n" + + "\n" + + "\x06MEMBER\x10\x01\x12\t\n" + + "\x05ADMIN\x10\x02\x12\t\n" + + "\x05OWNER\x10\x03:O\xeaAL\n" + + "\x18memos.api.v1/GroupMember\x12\x1fgroups/{group}/members/{member}*\amembers2\x06member\"D\n" + + "\x12CreateGroupRequest\x12.\n" + + "\x05group\x18\x01 \x01(\v2\x13.memos.api.v1.GroupB\x03\xe0A\x02R\x05group\"\x13\n" + + "\x11ListGroupsRequest\"A\n" + + "\x12ListGroupsResponse\x12+\n" + + "\x06groups\x18\x01 \x03(\v2\x13.memos.api.v1.GroupR\x06groups\"A\n" + + "\x0fGetGroupRequest\x12.\n" + + "\x04name\x18\x01 \x01(\tB\x1a\xe0A\x02\xfaA\x14\n" + + "\x12memos.api.v1/GroupR\x04name\"\x86\x01\n" + + "\x12UpdateGroupRequest\x12.\n" + + "\x05group\x18\x01 \x01(\v2\x13.memos.api.v1.GroupB\x03\xe0A\x02R\x05group\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + + "updateMask\"D\n" + + "\x12DeleteGroupRequest\x12.\n" + + "\x04name\x18\x01 \x01(\tB\x1a\xe0A\x02\xfaA\x14\n" + + "\x12memos.api.v1/GroupR\x04name\"\x83\x01\n" + + "\x15AddGroupMemberRequest\x122\n" + + "\x06parent\x18\x01 \x01(\tB\x1a\xe0A\x02\xfaA\x14\n" + + "\x12memos.api.v1/GroupR\x06parent\x126\n" + + "\x06member\x18\x02 \x01(\v2\x19.memos.api.v1.GroupMemberB\x03\xe0A\x02R\x06member\"M\n" + + "\x17ListGroupMembersRequest\x122\n" + + "\x06parent\x18\x01 \x01(\tB\x1a\xe0A\x02\xfaA\x14\n" + + "\x12memos.api.v1/GroupR\x06parent\"O\n" + + "\x18ListGroupMembersResponse\x123\n" + + "\amembers\x18\x01 \x03(\v2\x19.memos.api.v1.GroupMemberR\amembers\"\x94\x01\n" + + "\x18UpdateGroupMemberRequest\x126\n" + + "\x06member\x18\x01 \x01(\v2\x19.memos.api.v1.GroupMemberB\x03\xe0A\x02R\x06member\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + + "updateMask\"P\n" + + "\x18RemoveGroupMemberRequest\x124\n" + + "\x04name\x18\x01 \x01(\tB \xe0A\x02\xfaA\x1a\n" + + "\x18memos.api.v1/GroupMemberR\x04name2\xc3\b\n" + + "\fGroupService\x12c\n" + + "\vCreateGroup\x12 .memos.api.v1.CreateGroupRequest\x1a\x13.memos.api.v1.Group\"\x1d\x82\xd3\xe4\x93\x02\x17:\x05group\"\x0e/api/v1/groups\x12g\n" + + "\n" + + "ListGroups\x12\x1f.memos.api.v1.ListGroupsRequest\x1a .memos.api.v1.ListGroupsResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/api/v1/groups\x12_\n" + + "\bGetGroup\x12\x1d.memos.api.v1.GetGroupRequest\x1a\x13.memos.api.v1.Group\"\x1f\x82\xd3\xe4\x93\x02\x19\x12\x17/api/v1/{name=groups/*}\x12r\n" + + "\vUpdateGroup\x12 .memos.api.v1.UpdateGroupRequest\x1a\x13.memos.api.v1.Group\",\x82\xd3\xe4\x93\x02&:\x05group2\x1d/api/v1/{group.name=groups/*}\x12h\n" + + "\vDeleteGroup\x12 .memos.api.v1.DeleteGroupRequest\x1a\x16.google.protobuf.Empty\"\x1f\x82\xd3\xe4\x93\x02\x19*\x17/api/v1/{name=groups/*}\x12\x83\x01\n" + + "\x0eAddGroupMember\x12#.memos.api.v1.AddGroupMemberRequest\x1a\x19.memos.api.v1.GroupMember\"1\x82\xd3\xe4\x93\x02+:\x06member\"!/api/v1/{parent=groups/*}/members\x12\x8c\x01\n" + + "\x10ListGroupMembers\x12%.memos.api.v1.ListGroupMembersRequest\x1a&.memos.api.v1.ListGroupMembersResponse\")\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=groups/*}/members\x12\x90\x01\n" + + "\x11UpdateGroupMember\x12&.memos.api.v1.UpdateGroupMemberRequest\x1a\x19.memos.api.v1.GroupMember\"8\x82\xd3\xe4\x93\x022:\x06member2(/api/v1/{member.name=groups/*/members/*}\x12~\n" + + "\x11RemoveGroupMember\x12&.memos.api.v1.RemoveGroupMemberRequest\x1a\x16.google.protobuf.Empty\")\x82\xd3\xe4\x93\x02#*!/api/v1/{name=groups/*/members/*}B\xa9\x01\n" + + "\x10com.memos.api.v1B\x11GroupServiceProtoP\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 ( + file_api_v1_group_service_proto_rawDescOnce sync.Once + file_api_v1_group_service_proto_rawDescData []byte +) + +func file_api_v1_group_service_proto_rawDescGZIP() []byte { + file_api_v1_group_service_proto_rawDescOnce.Do(func() { + file_api_v1_group_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_group_service_proto_rawDesc), len(file_api_v1_group_service_proto_rawDesc))) + }) + return file_api_v1_group_service_proto_rawDescData +} + +var file_api_v1_group_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_api_v1_group_service_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_api_v1_group_service_proto_goTypes = []any{ + (GroupMember_Role)(0), // 0: memos.api.v1.GroupMember.Role + (*Group)(nil), // 1: memos.api.v1.Group + (*GroupMember)(nil), // 2: memos.api.v1.GroupMember + (*CreateGroupRequest)(nil), // 3: memos.api.v1.CreateGroupRequest + (*ListGroupsRequest)(nil), // 4: memos.api.v1.ListGroupsRequest + (*ListGroupsResponse)(nil), // 5: memos.api.v1.ListGroupsResponse + (*GetGroupRequest)(nil), // 6: memos.api.v1.GetGroupRequest + (*UpdateGroupRequest)(nil), // 7: memos.api.v1.UpdateGroupRequest + (*DeleteGroupRequest)(nil), // 8: memos.api.v1.DeleteGroupRequest + (*AddGroupMemberRequest)(nil), // 9: memos.api.v1.AddGroupMemberRequest + (*ListGroupMembersRequest)(nil), // 10: memos.api.v1.ListGroupMembersRequest + (*ListGroupMembersResponse)(nil), // 11: memos.api.v1.ListGroupMembersResponse + (*UpdateGroupMemberRequest)(nil), // 12: memos.api.v1.UpdateGroupMemberRequest + (*RemoveGroupMemberRequest)(nil), // 13: memos.api.v1.RemoveGroupMemberRequest + (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp + (*fieldmaskpb.FieldMask)(nil), // 15: google.protobuf.FieldMask + (*emptypb.Empty)(nil), // 16: google.protobuf.Empty +} +var file_api_v1_group_service_proto_depIdxs = []int32{ + 14, // 0: memos.api.v1.Group.create_time:type_name -> google.protobuf.Timestamp + 0, // 1: memos.api.v1.GroupMember.role:type_name -> memos.api.v1.GroupMember.Role + 1, // 2: memos.api.v1.CreateGroupRequest.group:type_name -> memos.api.v1.Group + 1, // 3: memos.api.v1.ListGroupsResponse.groups:type_name -> memos.api.v1.Group + 1, // 4: memos.api.v1.UpdateGroupRequest.group:type_name -> memos.api.v1.Group + 15, // 5: memos.api.v1.UpdateGroupRequest.update_mask:type_name -> google.protobuf.FieldMask + 2, // 6: memos.api.v1.AddGroupMemberRequest.member:type_name -> memos.api.v1.GroupMember + 2, // 7: memos.api.v1.ListGroupMembersResponse.members:type_name -> memos.api.v1.GroupMember + 2, // 8: memos.api.v1.UpdateGroupMemberRequest.member:type_name -> memos.api.v1.GroupMember + 15, // 9: memos.api.v1.UpdateGroupMemberRequest.update_mask:type_name -> google.protobuf.FieldMask + 3, // 10: memos.api.v1.GroupService.CreateGroup:input_type -> memos.api.v1.CreateGroupRequest + 4, // 11: memos.api.v1.GroupService.ListGroups:input_type -> memos.api.v1.ListGroupsRequest + 6, // 12: memos.api.v1.GroupService.GetGroup:input_type -> memos.api.v1.GetGroupRequest + 7, // 13: memos.api.v1.GroupService.UpdateGroup:input_type -> memos.api.v1.UpdateGroupRequest + 8, // 14: memos.api.v1.GroupService.DeleteGroup:input_type -> memos.api.v1.DeleteGroupRequest + 9, // 15: memos.api.v1.GroupService.AddGroupMember:input_type -> memos.api.v1.AddGroupMemberRequest + 10, // 16: memos.api.v1.GroupService.ListGroupMembers:input_type -> memos.api.v1.ListGroupMembersRequest + 12, // 17: memos.api.v1.GroupService.UpdateGroupMember:input_type -> memos.api.v1.UpdateGroupMemberRequest + 13, // 18: memos.api.v1.GroupService.RemoveGroupMember:input_type -> memos.api.v1.RemoveGroupMemberRequest + 1, // 19: memos.api.v1.GroupService.CreateGroup:output_type -> memos.api.v1.Group + 5, // 20: memos.api.v1.GroupService.ListGroups:output_type -> memos.api.v1.ListGroupsResponse + 1, // 21: memos.api.v1.GroupService.GetGroup:output_type -> memos.api.v1.Group + 1, // 22: memos.api.v1.GroupService.UpdateGroup:output_type -> memos.api.v1.Group + 16, // 23: memos.api.v1.GroupService.DeleteGroup:output_type -> google.protobuf.Empty + 2, // 24: memos.api.v1.GroupService.AddGroupMember:output_type -> memos.api.v1.GroupMember + 11, // 25: memos.api.v1.GroupService.ListGroupMembers:output_type -> memos.api.v1.ListGroupMembersResponse + 2, // 26: memos.api.v1.GroupService.UpdateGroupMember:output_type -> memos.api.v1.GroupMember + 16, // 27: memos.api.v1.GroupService.RemoveGroupMember:output_type -> google.protobuf.Empty + 19, // [19:28] is the sub-list for method output_type + 10, // [10:19] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name +} + +func init() { file_api_v1_group_service_proto_init() } +func file_api_v1_group_service_proto_init() { + if File_api_v1_group_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_group_service_proto_rawDesc), len(file_api_v1_group_service_proto_rawDesc)), + NumEnums: 1, + NumMessages: 13, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_group_service_proto_goTypes, + DependencyIndexes: file_api_v1_group_service_proto_depIdxs, + EnumInfos: file_api_v1_group_service_proto_enumTypes, + MessageInfos: file_api_v1_group_service_proto_msgTypes, + }.Build() + File_api_v1_group_service_proto = out.File + file_api_v1_group_service_proto_goTypes = nil + file_api_v1_group_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/group_service.pb.gw.go b/proto/gen/api/v1/group_service.pb.gw.go new file mode 100644 index 0000000000000..c594506b6da1a --- /dev/null +++ b/proto/gen/api/v1/group_service.pb.gw.go @@ -0,0 +1,853 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/group_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_GroupService_CreateGroup_0(ctx context.Context, marshaler runtime.Marshaler, client GroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateGroupRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Group); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.CreateGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_GroupService_CreateGroup_0(ctx context.Context, marshaler runtime.Marshaler, server GroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateGroupRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Group); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateGroup(ctx, &protoReq) + return msg, metadata, err +} + +func request_GroupService_ListGroups_0(ctx context.Context, marshaler runtime.Marshaler, client GroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListGroupsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ListGroups(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_GroupService_ListGroups_0(ctx context.Context, marshaler runtime.Marshaler, server GroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListGroupsRequest + metadata runtime.ServerMetadata + ) + msg, err := server.ListGroups(ctx, &protoReq) + return msg, metadata, err +} + +func request_GroupService_GetGroup_0(ctx context.Context, marshaler runtime.Marshaler, client GroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetGroupRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_GroupService_GetGroup_0(ctx context.Context, marshaler runtime.Marshaler, server GroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetGroupRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetGroup(ctx, &protoReq) + return msg, metadata, err +} + +var filter_GroupService_UpdateGroup_0 = &utilities.DoubleArray{Encoding: map[string]int{"group": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_GroupService_UpdateGroup_0(ctx context.Context, marshaler runtime.Marshaler, client GroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateGroupRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Group); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Group); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["group.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "group.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GroupService_UpdateGroup_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_GroupService_UpdateGroup_0(ctx context.Context, marshaler runtime.Marshaler, server GroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateGroupRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Group); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Group); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["group.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "group.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "group.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "group.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GroupService_UpdateGroup_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateGroup(ctx, &protoReq) + return msg, metadata, err +} + +func request_GroupService_DeleteGroup_0(ctx context.Context, marshaler runtime.Marshaler, client GroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteGroupRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.DeleteGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_GroupService_DeleteGroup_0(ctx context.Context, marshaler runtime.Marshaler, server GroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteGroupRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.DeleteGroup(ctx, &protoReq) + return msg, metadata, err +} + +func request_GroupService_AddGroupMember_0(ctx context.Context, marshaler runtime.Marshaler, client GroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq AddGroupMemberRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Member); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := client.AddGroupMember(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_GroupService_AddGroupMember_0(ctx context.Context, marshaler runtime.Marshaler, server GroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq AddGroupMemberRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Member); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := server.AddGroupMember(ctx, &protoReq) + return msg, metadata, err +} + +func request_GroupService_ListGroupMembers_0(ctx context.Context, marshaler runtime.Marshaler, client GroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListGroupMembersRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := client.ListGroupMembers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_GroupService_ListGroupMembers_0(ctx context.Context, marshaler runtime.Marshaler, server GroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListGroupMembersRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := server.ListGroupMembers(ctx, &protoReq) + return msg, metadata, err +} + +var filter_GroupService_UpdateGroupMember_0 = &utilities.DoubleArray{Encoding: map[string]int{"member": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_GroupService_UpdateGroupMember_0(ctx context.Context, marshaler runtime.Marshaler, client GroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateGroupMemberRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Member); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Member); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["member.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "member.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "member.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "member.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GroupService_UpdateGroupMember_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateGroupMember(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_GroupService_UpdateGroupMember_0(ctx context.Context, marshaler runtime.Marshaler, server GroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateGroupMemberRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Member); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Member); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["member.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "member.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "member.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "member.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GroupService_UpdateGroupMember_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateGroupMember(ctx, &protoReq) + return msg, metadata, err +} + +func request_GroupService_RemoveGroupMember_0(ctx context.Context, marshaler runtime.Marshaler, client GroupServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RemoveGroupMemberRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.RemoveGroupMember(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_GroupService_RemoveGroupMember_0(ctx context.Context, marshaler runtime.Marshaler, server GroupServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RemoveGroupMemberRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.RemoveGroupMember(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterGroupServiceHandlerServer registers the http handlers for service GroupService to "mux". +// UnaryRPC :call GroupServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterGroupServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterGroupServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server GroupServiceServer) error { + mux.Handle(http.MethodPost, pattern_GroupService_CreateGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + 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.GroupService/CreateGroup", runtime.WithHTTPPathPattern("/api/v1/groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GroupService_CreateGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_CreateGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_GroupService_ListGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + 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.GroupService/ListGroups", runtime.WithHTTPPathPattern("/api/v1/groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GroupService_ListGroups_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_ListGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_GroupService_GetGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + 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.GroupService/GetGroup", runtime.WithHTTPPathPattern("/api/v1/{name=groups/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GroupService_GetGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_GetGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_GroupService_UpdateGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + 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.GroupService/UpdateGroup", runtime.WithHTTPPathPattern("/api/v1/{group.name=groups/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GroupService_UpdateGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_UpdateGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_GroupService_DeleteGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + 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.GroupService/DeleteGroup", runtime.WithHTTPPathPattern("/api/v1/{name=groups/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GroupService_DeleteGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_DeleteGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_GroupService_AddGroupMember_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + 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.GroupService/AddGroupMember", runtime.WithHTTPPathPattern("/api/v1/{parent=groups/*}/members")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GroupService_AddGroupMember_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_AddGroupMember_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_GroupService_ListGroupMembers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + 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.GroupService/ListGroupMembers", runtime.WithHTTPPathPattern("/api/v1/{parent=groups/*}/members")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GroupService_ListGroupMembers_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_ListGroupMembers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_GroupService_UpdateGroupMember_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + 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.GroupService/UpdateGroupMember", runtime.WithHTTPPathPattern("/api/v1/{member.name=groups/*/members/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GroupService_UpdateGroupMember_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_UpdateGroupMember_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_GroupService_RemoveGroupMember_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + 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.GroupService/RemoveGroupMember", runtime.WithHTTPPathPattern("/api/v1/{name=groups/*/members/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GroupService_RemoveGroupMember_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_RemoveGroupMember_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterGroupServiceHandlerFromEndpoint is same as RegisterGroupServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterGroupServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterGroupServiceHandler(ctx, mux, conn) +} + +// RegisterGroupServiceHandler registers the http handlers for service GroupService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterGroupServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterGroupServiceHandlerClient(ctx, mux, NewGroupServiceClient(conn)) +} + +// RegisterGroupServiceHandlerClient registers the http handlers for service GroupService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "GroupServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "GroupServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "GroupServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterGroupServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client GroupServiceClient) error { + mux.Handle(http.MethodPost, pattern_GroupService_CreateGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.GroupService/CreateGroup", runtime.WithHTTPPathPattern("/api/v1/groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GroupService_CreateGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_CreateGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_GroupService_ListGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.GroupService/ListGroups", runtime.WithHTTPPathPattern("/api/v1/groups")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GroupService_ListGroups_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_ListGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_GroupService_GetGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.GroupService/GetGroup", runtime.WithHTTPPathPattern("/api/v1/{name=groups/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GroupService_GetGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_GetGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_GroupService_UpdateGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.GroupService/UpdateGroup", runtime.WithHTTPPathPattern("/api/v1/{group.name=groups/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GroupService_UpdateGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_UpdateGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_GroupService_DeleteGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.GroupService/DeleteGroup", runtime.WithHTTPPathPattern("/api/v1/{name=groups/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GroupService_DeleteGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_DeleteGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_GroupService_AddGroupMember_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.GroupService/AddGroupMember", runtime.WithHTTPPathPattern("/api/v1/{parent=groups/*}/members")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GroupService_AddGroupMember_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_AddGroupMember_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_GroupService_ListGroupMembers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.GroupService/ListGroupMembers", runtime.WithHTTPPathPattern("/api/v1/{parent=groups/*}/members")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GroupService_ListGroupMembers_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_ListGroupMembers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_GroupService_UpdateGroupMember_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.GroupService/UpdateGroupMember", runtime.WithHTTPPathPattern("/api/v1/{member.name=groups/*/members/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GroupService_UpdateGroupMember_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_UpdateGroupMember_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_GroupService_RemoveGroupMember_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.GroupService/RemoveGroupMember", runtime.WithHTTPPathPattern("/api/v1/{name=groups/*/members/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GroupService_RemoveGroupMember_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_GroupService_RemoveGroupMember_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_GroupService_CreateGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "groups"}, "")) + pattern_GroupService_ListGroups_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "groups"}, "")) + pattern_GroupService_GetGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "groups", "name"}, "")) + pattern_GroupService_UpdateGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "groups", "group.name"}, "")) + pattern_GroupService_DeleteGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "groups", "name"}, "")) + pattern_GroupService_AddGroupMember_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "groups", "parent", "members"}, "")) + pattern_GroupService_ListGroupMembers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "groups", "parent", "members"}, "")) + pattern_GroupService_UpdateGroupMember_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", "groups", "members", "member.name"}, "")) + pattern_GroupService_RemoveGroupMember_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", "groups", "members", "name"}, "")) +) + +var ( + forward_GroupService_CreateGroup_0 = runtime.ForwardResponseMessage + forward_GroupService_ListGroups_0 = runtime.ForwardResponseMessage + forward_GroupService_GetGroup_0 = runtime.ForwardResponseMessage + forward_GroupService_UpdateGroup_0 = runtime.ForwardResponseMessage + forward_GroupService_DeleteGroup_0 = runtime.ForwardResponseMessage + forward_GroupService_AddGroupMember_0 = runtime.ForwardResponseMessage + forward_GroupService_ListGroupMembers_0 = runtime.ForwardResponseMessage + forward_GroupService_UpdateGroupMember_0 = runtime.ForwardResponseMessage + forward_GroupService_RemoveGroupMember_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/group_service_grpc.pb.go b/proto/gen/api/v1/group_service_grpc.pb.go new file mode 100644 index 0000000000000..8b1de3841b198 --- /dev/null +++ b/proto/gen/api/v1/group_service_grpc.pb.go @@ -0,0 +1,444 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: api/v1/group_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + GroupService_CreateGroup_FullMethodName = "/memos.api.v1.GroupService/CreateGroup" + GroupService_ListGroups_FullMethodName = "/memos.api.v1.GroupService/ListGroups" + GroupService_GetGroup_FullMethodName = "/memos.api.v1.GroupService/GetGroup" + GroupService_UpdateGroup_FullMethodName = "/memos.api.v1.GroupService/UpdateGroup" + GroupService_DeleteGroup_FullMethodName = "/memos.api.v1.GroupService/DeleteGroup" + GroupService_AddGroupMember_FullMethodName = "/memos.api.v1.GroupService/AddGroupMember" + GroupService_ListGroupMembers_FullMethodName = "/memos.api.v1.GroupService/ListGroupMembers" + GroupService_UpdateGroupMember_FullMethodName = "/memos.api.v1.GroupService/UpdateGroupMember" + GroupService_RemoveGroupMember_FullMethodName = "/memos.api.v1.GroupService/RemoveGroupMember" +) + +// GroupServiceClient is the client API for GroupService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GroupServiceClient interface { + // CreateGroup creates a new group. + CreateGroup(ctx context.Context, in *CreateGroupRequest, opts ...grpc.CallOption) (*Group, error) + // ListGroups lists all groups. + ListGroups(ctx context.Context, in *ListGroupsRequest, opts ...grpc.CallOption) (*ListGroupsResponse, error) + // GetGroup gets a group by name. + GetGroup(ctx context.Context, in *GetGroupRequest, opts ...grpc.CallOption) (*Group, error) + // UpdateGroup updates a group. + UpdateGroup(ctx context.Context, in *UpdateGroupRequest, opts ...grpc.CallOption) (*Group, error) + // DeleteGroup deletes a group. + DeleteGroup(ctx context.Context, in *DeleteGroupRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // AddGroupMember adds a member to a group. + AddGroupMember(ctx context.Context, in *AddGroupMemberRequest, opts ...grpc.CallOption) (*GroupMember, error) + // ListGroupMembers lists all members of a group. + ListGroupMembers(ctx context.Context, in *ListGroupMembersRequest, opts ...grpc.CallOption) (*ListGroupMembersResponse, error) + // UpdateGroupMember updates a member of a group. + UpdateGroupMember(ctx context.Context, in *UpdateGroupMemberRequest, opts ...grpc.CallOption) (*GroupMember, error) + // RemoveGroupMember removes a member from a group. + RemoveGroupMember(ctx context.Context, in *RemoveGroupMemberRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type groupServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGroupServiceClient(cc grpc.ClientConnInterface) GroupServiceClient { + return &groupServiceClient{cc} +} + +func (c *groupServiceClient) CreateGroup(ctx context.Context, in *CreateGroupRequest, opts ...grpc.CallOption) (*Group, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Group) + err := c.cc.Invoke(ctx, GroupService_CreateGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groupServiceClient) ListGroups(ctx context.Context, in *ListGroupsRequest, opts ...grpc.CallOption) (*ListGroupsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListGroupsResponse) + err := c.cc.Invoke(ctx, GroupService_ListGroups_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groupServiceClient) GetGroup(ctx context.Context, in *GetGroupRequest, opts ...grpc.CallOption) (*Group, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Group) + err := c.cc.Invoke(ctx, GroupService_GetGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groupServiceClient) UpdateGroup(ctx context.Context, in *UpdateGroupRequest, opts ...grpc.CallOption) (*Group, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Group) + err := c.cc.Invoke(ctx, GroupService_UpdateGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groupServiceClient) DeleteGroup(ctx context.Context, in *DeleteGroupRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, GroupService_DeleteGroup_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groupServiceClient) AddGroupMember(ctx context.Context, in *AddGroupMemberRequest, opts ...grpc.CallOption) (*GroupMember, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GroupMember) + err := c.cc.Invoke(ctx, GroupService_AddGroupMember_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groupServiceClient) ListGroupMembers(ctx context.Context, in *ListGroupMembersRequest, opts ...grpc.CallOption) (*ListGroupMembersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListGroupMembersResponse) + err := c.cc.Invoke(ctx, GroupService_ListGroupMembers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groupServiceClient) UpdateGroupMember(ctx context.Context, in *UpdateGroupMemberRequest, opts ...grpc.CallOption) (*GroupMember, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GroupMember) + err := c.cc.Invoke(ctx, GroupService_UpdateGroupMember_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *groupServiceClient) RemoveGroupMember(ctx context.Context, in *RemoveGroupMemberRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, GroupService_RemoveGroupMember_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GroupServiceServer is the server API for GroupService service. +// All implementations must embed UnimplementedGroupServiceServer +// for forward compatibility. +type GroupServiceServer interface { + // CreateGroup creates a new group. + CreateGroup(context.Context, *CreateGroupRequest) (*Group, error) + // ListGroups lists all groups. + ListGroups(context.Context, *ListGroupsRequest) (*ListGroupsResponse, error) + // GetGroup gets a group by name. + GetGroup(context.Context, *GetGroupRequest) (*Group, error) + // UpdateGroup updates a group. + UpdateGroup(context.Context, *UpdateGroupRequest) (*Group, error) + // DeleteGroup deletes a group. + DeleteGroup(context.Context, *DeleteGroupRequest) (*emptypb.Empty, error) + // AddGroupMember adds a member to a group. + AddGroupMember(context.Context, *AddGroupMemberRequest) (*GroupMember, error) + // ListGroupMembers lists all members of a group. + ListGroupMembers(context.Context, *ListGroupMembersRequest) (*ListGroupMembersResponse, error) + // UpdateGroupMember updates a member of a group. + UpdateGroupMember(context.Context, *UpdateGroupMemberRequest) (*GroupMember, error) + // RemoveGroupMember removes a member from a group. + RemoveGroupMember(context.Context, *RemoveGroupMemberRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedGroupServiceServer() +} + +// UnimplementedGroupServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGroupServiceServer struct{} + +func (UnimplementedGroupServiceServer) CreateGroup(context.Context, *CreateGroupRequest) (*Group, error) { + return nil, status.Error(codes.Unimplemented, "method CreateGroup not implemented") +} +func (UnimplementedGroupServiceServer) ListGroups(context.Context, *ListGroupsRequest) (*ListGroupsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListGroups not implemented") +} +func (UnimplementedGroupServiceServer) GetGroup(context.Context, *GetGroupRequest) (*Group, error) { + return nil, status.Error(codes.Unimplemented, "method GetGroup not implemented") +} +func (UnimplementedGroupServiceServer) UpdateGroup(context.Context, *UpdateGroupRequest) (*Group, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateGroup not implemented") +} +func (UnimplementedGroupServiceServer) DeleteGroup(context.Context, *DeleteGroupRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteGroup not implemented") +} +func (UnimplementedGroupServiceServer) AddGroupMember(context.Context, *AddGroupMemberRequest) (*GroupMember, error) { + return nil, status.Error(codes.Unimplemented, "method AddGroupMember not implemented") +} +func (UnimplementedGroupServiceServer) ListGroupMembers(context.Context, *ListGroupMembersRequest) (*ListGroupMembersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListGroupMembers not implemented") +} +func (UnimplementedGroupServiceServer) UpdateGroupMember(context.Context, *UpdateGroupMemberRequest) (*GroupMember, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateGroupMember not implemented") +} +func (UnimplementedGroupServiceServer) RemoveGroupMember(context.Context, *RemoveGroupMemberRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveGroupMember not implemented") +} +func (UnimplementedGroupServiceServer) mustEmbedUnimplementedGroupServiceServer() {} +func (UnimplementedGroupServiceServer) testEmbeddedByValue() {} + +// UnsafeGroupServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GroupServiceServer will +// result in compilation errors. +type UnsafeGroupServiceServer interface { + mustEmbedUnimplementedGroupServiceServer() +} + +func RegisterGroupServiceServer(s grpc.ServiceRegistrar, srv GroupServiceServer) { + // If the following call panics, it indicates UnimplementedGroupServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&GroupService_ServiceDesc, srv) +} + +func _GroupService_CreateGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).CreateGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_CreateGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).CreateGroup(ctx, req.(*CreateGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GroupService_ListGroups_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListGroupsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).ListGroups(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_ListGroups_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).ListGroups(ctx, req.(*ListGroupsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GroupService_GetGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).GetGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_GetGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).GetGroup(ctx, req.(*GetGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GroupService_UpdateGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).UpdateGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_UpdateGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).UpdateGroup(ctx, req.(*UpdateGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GroupService_DeleteGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteGroupRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).DeleteGroup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_DeleteGroup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).DeleteGroup(ctx, req.(*DeleteGroupRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GroupService_AddGroupMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddGroupMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).AddGroupMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_AddGroupMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).AddGroupMember(ctx, req.(*AddGroupMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GroupService_ListGroupMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListGroupMembersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).ListGroupMembers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_ListGroupMembers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).ListGroupMembers(ctx, req.(*ListGroupMembersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GroupService_UpdateGroupMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateGroupMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).UpdateGroupMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_UpdateGroupMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).UpdateGroupMember(ctx, req.(*UpdateGroupMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GroupService_RemoveGroupMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveGroupMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GroupServiceServer).RemoveGroupMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GroupService_RemoveGroupMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GroupServiceServer).RemoveGroupMember(ctx, req.(*RemoveGroupMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GroupService_ServiceDesc is the grpc.ServiceDesc for GroupService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GroupService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.GroupService", + HandlerType: (*GroupServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateGroup", + Handler: _GroupService_CreateGroup_Handler, + }, + { + MethodName: "ListGroups", + Handler: _GroupService_ListGroups_Handler, + }, + { + MethodName: "GetGroup", + Handler: _GroupService_GetGroup_Handler, + }, + { + MethodName: "UpdateGroup", + Handler: _GroupService_UpdateGroup_Handler, + }, + { + MethodName: "DeleteGroup", + Handler: _GroupService_DeleteGroup_Handler, + }, + { + MethodName: "AddGroupMember", + Handler: _GroupService_AddGroupMember_Handler, + }, + { + MethodName: "ListGroupMembers", + Handler: _GroupService_ListGroupMembers_Handler, + }, + { + MethodName: "UpdateGroupMember", + Handler: _GroupService_UpdateGroupMember_Handler, + }, + { + MethodName: "RemoveGroupMember", + Handler: _GroupService_RemoveGroupMember_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/group_service.proto", +} diff --git a/proto/gen/api/v1/memo_service.pb.go b/proto/gen/api/v1/memo_service.pb.go index 7224f429d368a..b05a766b6c250 100644 --- a/proto/gen/api/v1/memo_service.pb.go +++ b/proto/gen/api/v1/memo_service.pb.go @@ -25,17 +25,14 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// Visibility controls who can read a memo. type Visibility int32 const ( Visibility_VISIBILITY_UNSPECIFIED Visibility = 0 - // PRIVATE: only the creator can read the memo. - Visibility_PRIVATE Visibility = 1 - // PROTECTED: signed-in users of the instance can read the memo. - Visibility_PROTECTED Visibility = 2 - // PUBLIC: anyone, including anonymous visitors, can read the memo. - Visibility_PUBLIC Visibility = 3 + Visibility_PRIVATE Visibility = 1 + Visibility_PROTECTED Visibility = 2 + Visibility_PUBLIC Visibility = 3 + Visibility_GROUP Visibility = 4 ) // Enum value maps for Visibility. @@ -45,12 +42,14 @@ var ( 1: "PRIVATE", 2: "PROTECTED", 3: "PUBLIC", + 4: "GROUP", } Visibility_value = map[string]int32{ "VISIBILITY_UNSPECIFIED": 0, "PRIVATE": 1, "PROTECTED": 2, "PUBLIC": 3, + "GROUP": 4, } ) @@ -235,8 +234,6 @@ type Memo struct { // Required. The content of the memo in Markdown format. Content string `protobuf:"bytes,7,opt,name=content,proto3" json:"content,omitempty"` // The visibility of the memo. - // One of PRIVATE (creator only), PROTECTED (signed-in users), or - // PUBLIC (anyone). Defaults to PRIVATE on creation when unspecified. Visibility Visibility `protobuf:"varint,9,opt,name=visibility,proto3,enum=memos.api.v1.Visibility" json:"visibility,omitempty"` // Output only. The tags extracted from the content. Tags []string `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"` @@ -256,7 +253,10 @@ type Memo struct { // Output only. The snippet of the memo content. Plain text only. Snippet string `protobuf:"bytes,17,opt,name=snippet,proto3" json:"snippet,omitempty"` // Optional. The location of the memo. - Location *Location `protobuf:"bytes,18,opt,name=location,proto3,oneof" json:"location,omitempty"` + Location *Location `protobuf:"bytes,18,opt,name=location,proto3,oneof" json:"location,omitempty"` + // Optional. The resource name of the group. + // Format: groups/{group} + Group *string `protobuf:"bytes,19,opt,name=group,proto3,oneof" json:"group,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -403,6 +403,13 @@ func (x *Memo) GetLocation() *Location { return nil } +func (x *Memo) GetGroup() string { + if x != nil && x.Group != nil { + return *x.Group + } + return "" +} + type Location struct { state protoimpl.MessageState `protogen:"open.v1"` // A placeholder text for the location. @@ -538,26 +545,11 @@ type ListMemosRequest struct { // Default to "create_time desc". // Supports comma-separated list of fields following AIP-132. // Example: "pinned desc, create_time desc" or "update_time asc" - // Supported fields: pinned, create_time, update_time, name. - // Note: order_by uses create_time / update_time, while the filter - // expression uses created_ts / updated_ts for the same timestamps. + // Supported fields: pinned, create_time, update_time, name OrderBy string `protobuf:"bytes,4,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` - // Optional. A CEL expression to filter memos. Combine terms with && and ||. - // Available fields: - // - // content (string), creator (string, e.g. "users/1"), - // created_ts / updated_ts (timestamp), pinned (bool), - // visibility (string: PRIVATE | PROTECTED | PUBLIC), - // tags (list; match with `"work" in tags`, not `tag == "work"`), - // has_task_list / has_link / has_code / has_incomplete_tasks (bool). - // - // Note: the time fields here are created_ts / updated_ts, which differ from - // the create_time / update_time names used by order_by. - // Examples: - // - // pinned == true && visibility == "PUBLIC" - // tags.exists(t, t == "urgent") - // content.contains("roadmap") && created_ts > now - duration("168h") + // Optional. Filter to apply to the list results. + // Filter is a CEL expression to filter memos. + // Refer to `Shortcut.filter`. Filter string `protobuf:"bytes,5,opt,name=filter,proto3" json:"filter,omitempty"` // Optional. If true, show deleted memos in the response. ShowDeleted bool `protobuf:"varint,6,opt,name=show_deleted,json=showDeleted,proto3" json:"show_deleted,omitempty"` @@ -2343,7 +2335,7 @@ const file_api_v1_memo_service_proto_rawDesc = "" + "\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:X\xeaAU\n" + - "\x15memos.api.v1/Reaction\x12!memos/{memo}/reactions/{reaction}\x1a\x04name*\treactions2\breaction\"\xbe\b\n" + + "\x15memos.api.v1/Reaction\x12!memos/{memo}/reactions/{reaction}\x1a\x04name*\treactions2\breaction\"\xff\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" + @@ -2367,7 +2359,9 @@ const file_api_v1_memo_service_proto_rawDesc = "" + "\x06parent\x18\x10 \x01(\tB\x19\xe0A\x03\xfaA\x13\n" + "\x11memos.api.v1/MemoH\x00R\x06parent\x88\x01\x01\x12\x1d\n" + "\asnippet\x18\x11 \x01(\tB\x03\xe0A\x03R\asnippet\x12<\n" + - "\blocation\x18\x12 \x01(\v2\x16.memos.api.v1.LocationB\x03\xe0A\x01H\x01R\blocation\x88\x01\x01\x1a\xac\x01\n" + + "\blocation\x18\x12 \x01(\v2\x16.memos.api.v1.LocationB\x03\xe0A\x01H\x01R\blocation\x88\x01\x01\x125\n" + + "\x05group\x18\x13 \x01(\tB\x1a\xe0A\x01\xfaA\x14\n" + + "\x12memos.api.v1/GroupH\x02R\x05group\x88\x01\x01\x1a\xac\x01\n" + "\bProperty\x12\x19\n" + "\bhas_link\x18\x01 \x01(\bR\ahasLink\x12\"\n" + "\rhas_task_list\x18\x02 \x01(\bR\vhasTaskList\x12\x19\n" + @@ -2376,7 +2370,8 @@ const file_api_v1_memo_service_proto_rawDesc = "" + "\x05title\x18\x05 \x01(\tR\x05title:7\xeaA4\n" + "\x11memos.api.v1/Memo\x12\fmemos/{memo}\x1a\x04name*\x05memos2\x04memoB\t\n" + "\a_parentB\v\n" + - "\t_locationJ\x04\b\x06\x10\aR\fdisplay_time\"u\n" + + "\t_locationB\b\n" + + "\x06_groupJ\x04\b\x06\x10\aR\fdisplay_time\"u\n" + "\bLocation\x12%\n" + "\vplaceholder\x18\x01 \x01(\tB\x03\xe0A\x01R\vplaceholder\x12\x1f\n" + "\blatitude\x18\x02 \x01(\x01B\x03\xe0A\x01R\blatitude\x12!\n" + @@ -2514,14 +2509,15 @@ const file_api_v1_memo_service_proto_rawDesc = "" + "\x03url\x18\x01 \x01(\tR\x03url\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + "\vdescription\x18\x03 \x01(\tR\vdescription\x12\x14\n" + - "\x05image\x18\x04 \x01(\tR\x05image*P\n" + + "\x05image\x18\x04 \x01(\tR\x05image*[\n" + "\n" + "Visibility\x12\x1a\n" + "\x16VISIBILITY_UNSPECIFIED\x10\x00\x12\v\n" + "\aPRIVATE\x10\x01\x12\r\n" + "\tPROTECTED\x10\x02\x12\n" + "\n" + - "\x06PUBLIC\x10\x032\x8b\x15\n" + + "\x06PUBLIC\x10\x03\x12\t\n" + + "\x05GROUP\x10\x042\x8b\x15\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" + diff --git a/proto/gen/openapi.yaml b/proto/gen/openapi.yaml index 7e9f90b81654d..48cdb1d6a36d2 100644 --- a/proto/gen/openapi.yaml +++ b/proto/gen/openapi.yaml @@ -40,35 +40,23 @@ paths: parameters: - name: pageSize in: query - description: |- - Optional. The maximum number of attachments to return. - The service may return fewer than this value. - If unspecified, at most 50 attachments will be returned. - The maximum value is 1000; values above 1000 will be coerced to 1000. + description: "Optional. The maximum number of attachments to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 attachments will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000." schema: type: integer format: int32 - name: pageToken in: query - description: |- - Optional. A page token, received from a previous `ListAttachments` call. - Provide this to retrieve the subsequent page. + description: "Optional. A page token, received from a previous `ListAttachments` call.\r\n Provide this to retrieve the subsequent page." schema: type: string - name: filter in: query - description: |- - Optional. Filter to apply to the list results. - Example: "mime_type==\"image/png\"" or "filename.contains(\"test\")" - Supported operators: =, !=, <, <=, >, >=, : (contains), in - Supported fields: filename, mime_type, create_time, memo + description: "Optional. Filter to apply to the list results.\r\n Example: \"mime_type==\\\"image/png\\\"\" or \"filename.contains(\\\"test\\\")\"\r\n Supported operators: =, !=, <, <=, >, >=, : (contains), in\r\n Supported fields: filename, mime_type, create_time, memo" schema: type: string - name: orderBy in: query - description: |- - Optional. The order to sort results by. - Example: "create_time desc" or "filename asc" + description: "Optional. The order to sort results by.\r\n Example: \"create_time desc\" or \"filename asc\"" schema: type: string responses: @@ -92,9 +80,7 @@ paths: parameters: - name: attachmentId in: query - description: |- - Optional. The attachment ID to use for this attachment. - If empty, a unique ID will be generated. + description: "Optional. The attachment ID to use for this attachment.\r\n If empty, a unique ID will be generated." schema: type: string requestBody: @@ -227,10 +213,7 @@ paths: get: tags: - AuthService - description: |- - GetCurrentUser returns the authenticated user's information. - Validates the access token and returns user details. - Similar to OIDC's /userinfo endpoint. + description: "GetCurrentUser returns the authenticated user's information.\r\n Validates the access token and returns user details.\r\n Similar to OIDC's /userinfo endpoint." operationId: AuthService_GetCurrentUser responses: "200": @@ -249,10 +232,7 @@ paths: post: tags: - AuthService - description: |- - RefreshToken exchanges a valid refresh token for a new access token. - The refresh token is read from the HttpOnly cookie. - Returns a new short-lived access token. + description: "RefreshToken exchanges a valid refresh token for a new access token.\r\n The refresh token is read from the HttpOnly cookie.\r\n Returns a new short-lived access token." operationId: AuthService_RefreshToken requestBody: content: @@ -277,10 +257,7 @@ paths: post: tags: - AuthService - description: |- - SignIn authenticates a user with credentials and returns tokens. - On success, returns an access token and sets a refresh token cookie. - Supports password-based and SSO authentication methods. + description: "SignIn authenticates a user with credentials and returns tokens.\r\n On success, returns an access token and sets a refresh token cookie.\r\n Supports password-based and SSO authentication methods." operationId: AuthService_SignIn requestBody: content: @@ -305,9 +282,7 @@ paths: post: tags: - AuthService - description: |- - SignOut terminates the user's authentication. - Revokes the refresh token and clears the authentication cookie. + description: "SignOut terminates the user's authentication.\r\n Revokes the refresh token and clears the authentication cookie." operationId: AuthService_SignOut responses: "200": @@ -319,6 +294,261 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' + /api/v1/groups: + get: + tags: + - GroupService + description: ListGroups lists all groups. + operationId: GroupService_ListGroups + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListGroupsResponse' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + post: + tags: + - GroupService + description: CreateGroup creates a new group. + operationId: GroupService_CreateGroup + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /api/v1/groups/{group}: + get: + tags: + - GroupService + description: GetGroup gets a group by name. + operationId: GroupService_GetGroup + parameters: + - name: group + in: path + description: The group id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + delete: + tags: + - GroupService + description: DeleteGroup deletes a group. + operationId: GroupService_DeleteGroup + parameters: + - name: group + in: path + description: The group id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + patch: + tags: + - GroupService + description: UpdateGroup updates a group. + operationId: GroupService_UpdateGroup + parameters: + - name: group + in: path + description: The group id. + required: true + schema: + type: string + - name: updateMask + in: query + schema: + type: string + format: field-mask + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /api/v1/groups/{group}/members: + get: + tags: + - GroupService + description: ListGroupMembers lists all members of a group. + operationId: GroupService_ListGroupMembers + parameters: + - name: group + in: path + description: The group id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListGroupMembersResponse' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + post: + tags: + - GroupService + description: AddGroupMember adds a member to a group. + operationId: GroupService_AddGroupMember + parameters: + - name: group + in: path + description: The group id. + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GroupMember' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GroupMember' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + /api/v1/groups/{group}/members/{member}: + delete: + tags: + - GroupService + description: RemoveGroupMember removes a member from a group. + operationId: GroupService_RemoveGroupMember + parameters: + - name: group + in: path + description: The group id. + required: true + schema: + type: string + - name: member + in: path + description: The member id. + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + patch: + tags: + - GroupService + description: UpdateGroupMember updates a member of a group. + operationId: GroupService_UpdateGroupMember + parameters: + - name: group + in: path + description: The group id. + required: true + schema: + type: string + - name: member + in: path + description: The member id. + required: true + schema: + type: string + - name: updateMask + in: query + schema: + type: string + format: field-mask + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GroupMember' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GroupMember' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/Status' /api/v1/identity-providers: get: tags: @@ -346,9 +576,7 @@ paths: parameters: - name: identityProviderId in: query - description: |- - Optional. The ID to use for the identity provider, which will become the final component of the resource name. - If not provided, the system will generate one. + description: "Optional. The ID to use for the identity provider, which will become the final component of the resource name.\r\n If not provided, the system will generate one." schema: type: string requestBody: @@ -432,9 +660,7 @@ paths: type: string - name: updateMask in: query - description: |- - Required. The update mask applies to the resource. Only the top level fields of - IdentityProvider are supported. + description: "Required. The update mask applies to the resource. Only the top level fields of\r\n IdentityProvider are supported." schema: type: string format: field-mask @@ -614,26 +840,18 @@ paths: parameters: - name: pageSize in: query - description: |- - Optional. The maximum number of memos to return. - The service may return fewer than this value. - If unspecified, at most 50 memos will be returned. - The maximum value is 1000; values above 1000 will be coerced to 1000. + description: "Optional. The maximum number of memos to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 memos will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000." schema: type: integer format: int32 - name: pageToken in: query - description: |- - Optional. A page token, received from a previous `ListMemos` call. - Provide this to retrieve the subsequent page. + description: "Optional. A page token, received from a previous `ListMemos` call.\r\n Provide this to retrieve the subsequent page." schema: type: string - name: state in: query - description: |- - Optional. The state of the memos to list. - Default to `NORMAL`. Set to `ARCHIVED` to list archived memos. + description: "Optional. The state of the memos to list.\r\n Default to `NORMAL`. Set to `ARCHIVED` to list archived memos." schema: enum: - STATE_UNSPECIFIED @@ -643,32 +861,12 @@ paths: format: enum - name: orderBy in: query - description: |- - Optional. The order to sort results by. - Default to "create_time desc". - Supports comma-separated list of fields following AIP-132. - Example: "pinned desc, create_time desc" or "update_time asc" - Supported fields: pinned, create_time, update_time, name. - Note: order_by uses create_time / update_time, while the filter - expression uses created_ts / updated_ts for the same timestamps. + description: "Optional. The order to sort results by.\r\n Default to \"create_time desc\".\r\n Supports comma-separated list of fields following AIP-132.\r\n Example: \"pinned desc, create_time desc\" or \"update_time asc\"\r\n Supported fields: pinned, create_time, update_time, name" schema: type: string - name: filter in: query - description: |- - Optional. A CEL expression to filter memos. Combine terms with && and ||. - Available fields: - content (string), creator (string, e.g. "users/1"), - created_ts / updated_ts (timestamp), pinned (bool), - visibility (string: PRIVATE | PROTECTED | PUBLIC), - tags (list; match with `"work" in tags`, not `tag == "work"`), - has_task_list / has_link / has_code / has_incomplete_tasks (bool). - Note: the time fields here are created_ts / updated_ts, which differ from - the create_time / update_time names used by order_by. - Examples: - pinned == true && visibility == "PUBLIC" - tags.exists(t, t == "urgent") - content.contains("roadmap") && created_ts > now - duration("168h") + description: "Optional. Filter to apply to the list results.\r\n Filter is a CEL expression to filter memos.\r\n Refer to `Shortcut.filter`." schema: type: string - name: showDeleted @@ -692,17 +890,12 @@ paths: post: tags: - MemoService - description: |- - CreateMemo creates a memo. The request body is a Memo; set its content - (Markdown) and visibility (PRIVATE | PROTECTED | PUBLIC, default PRIVATE). - The memo is owned by the authenticated user; requires authentication. + description: CreateMemo creates a memo. operationId: MemoService_CreateMemo parameters: - name: memoId in: query - description: |- - Optional. The memo ID to use for this memo. - If empty, a unique ID will be generated. + description: "Optional. The memo ID to use for this memo.\r\n If empty, a unique ID will be generated." schema: type: string requestBody: @@ -904,10 +1097,7 @@ paths: patch: tags: - MemoService - description: |- - SetMemoAttachments replaces the full set of attachments on a memo with the - provided list (not an append). Pass the complete desired set; an empty list - clears all attachments. Idempotent. + description: SetMemoAttachments sets attachments for a memo. operationId: MemoService_SetMemoAttachments parameters: - name: memo @@ -1050,9 +1240,7 @@ paths: post: tags: - MemoService - description: |- - UpsertMemoReaction adds or updates the authenticated user's reaction on a - memo. The reaction's content_id is the memo's resource name (memos/{memo}). + description: UpsertMemoReaction upserts a reaction for a memo. operationId: MemoService_UpsertMemoReaction parameters: - name: memo @@ -1149,10 +1337,7 @@ paths: patch: tags: - MemoService - description: |- - SetMemoRelations replaces the full set of relations on a memo with the - provided list (not an append). Pass the complete desired set; an empty list - clears all relations. Idempotent. + description: SetMemoRelations sets relations for a memo. operationId: MemoService_SetMemoRelations parameters: - name: memo @@ -1267,9 +1452,7 @@ paths: get: tags: - MemoService - description: |- - GetMemoByShare resolves a share token to its memo. No authentication required. - Returns NOT_FOUND if the token is invalid or expired. + description: "GetMemoByShare resolves a share token to its memo. No authentication required.\r\n Returns NOT_FOUND if the token is invalid or expired." operationId: MemoService_GetMemoByShare parameters: - name: shareId @@ -1300,28 +1483,18 @@ paths: parameters: - name: pageSize in: query - description: |- - Optional. The maximum number of users to return. - The service may return fewer than this value. - If unspecified, at most 50 users will be returned. - The maximum value is 1000; values above 1000 will be coerced to 1000. + description: "Optional. The maximum number of users to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 users will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000." schema: type: integer format: int32 - name: pageToken in: query - description: |- - Optional. A page token, received from a previous `ListUsers` call. - Provide this to retrieve the subsequent page. + description: "Optional. A page token, received from a previous `ListUsers` call.\r\n Provide this to retrieve the subsequent page." schema: type: string - name: filter in: query - description: |- - Optional. Filter to apply to the list results. - Example: "username == 'steven'" - Supported operators: == - Supported fields: username + description: "Optional. Filter to apply to the list results.\r\n Example: \"username == 'steven'\"\r\n Supported operators: ==\r\n Supported fields: username" schema: type: string - name: showDeleted @@ -1350,10 +1523,7 @@ paths: parameters: - name: userId in: query - description: |- - Optional. The user ID to use for this user. - If empty, a unique ID will be generated. - Must match the pattern [a-z0-9-]+ + description: "Optional. The user ID to use for this user.\r\n If empty, a unique ID will be generated.\r\n Must match the pattern [a-z0-9-]+" schema: type: string - name: validateOnly @@ -1363,9 +1533,7 @@ paths: type: boolean - name: requestId in: query - description: |- - Optional. An idempotency token that can be used to ensure that multiple - requests to create a user have the same result. + description: "Optional. An idempotency token that can be used to ensure that multiple\r\n requests to create a user have the same result." schema: type: string requestBody: @@ -1391,9 +1559,7 @@ paths: get: tags: - UserService - description: |- - GetUser gets a user by username. - Format: users/{username} (e.g., users/steven) + description: "GetUser gets a user by username.\r\n Format: users/{username} (e.g., users/steven)" operationId: UserService_GetUser parameters: - name: user @@ -1404,9 +1570,7 @@ paths: type: string - name: readMask in: query - description: |- - Optional. The fields to return in the response. - If not specified, all fields are returned. + description: "Optional. The fields to return in the response.\r\n If not specified, all fields are returned." schema: type: string format: field-mask @@ -1723,9 +1887,7 @@ paths: get: tags: - UserService - description: |- - ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. - PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. + description: "ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.\r\n PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens." operationId: UserService_ListPersonalAccessTokens parameters: - name: user @@ -1761,9 +1923,7 @@ paths: post: tags: - UserService - description: |- - CreatePersonalAccessToken creates a new Personal Access Token for a user. - The token value is only returned once upon creation. + description: "CreatePersonalAccessToken creates a new Personal Access Token for a user.\r\n The token value is only returned once upon creation." operationId: UserService_CreatePersonalAccessToken parameters: - name: user @@ -1835,19 +1995,13 @@ paths: type: string - name: pageSize in: query - description: |- - Optional. The maximum number of settings to return. - The service may return fewer than this value. - If unspecified, at most 50 settings will be returned. - The maximum value is 1000; values above 1000 will be coerced to 1000. + description: "Optional. The maximum number of settings to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 settings will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000." schema: type: integer format: int32 - name: pageToken in: query - description: |- - Optional. A page token, received from a previous `ListUserSettings` call. - Provide this to retrieve the subsequent page. + description: "Optional. A page token, received from a previous `ListUserSettings` call.\r\n Provide this to retrieve the subsequent page." schema: type: string responses: @@ -1942,10 +2096,7 @@ paths: get: tags: - ShortcutService - description: |- - ListShortcuts returns a user's saved shortcuts. Each shortcut is a named, - reusable CEL filter (see Shortcut.filter); pass its filter string directly - to the ListMemos `filter` argument to reuse a saved view. + description: ListShortcuts returns a list of shortcuts for a user. operationId: ShortcutService_ListShortcuts parameters: - name: user @@ -2235,41 +2386,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Status' - /api/v1/users/{user}/webhooks/{webhook}:getSigningSecret: - get: - tags: - - UserService - description: |- - GetUserWebhookSigningSecret returns the signing secret for a webhook. - The secret is returned only through this explicit, owner-gated call; it is - never included in List/Create/Update responses. - operationId: UserService_GetUserWebhookSigningSecret - parameters: - - name: user - in: path - description: The user id. - required: true - schema: - type: string - - name: webhook - in: path - description: The webhook id. - required: true - schema: - type: string - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/GetUserWebhookSigningSecretResponse' - default: - description: Default error response - content: - application/json: - schema: - $ref: '#/components/schemas/Status' /api/v1/users/{user}:getStats: get: tags: @@ -2340,9 +2456,7 @@ paths: format: enum - name: filter in: query - description: |- - Optional. Filter to apply to memo stats. - Uses the same filter syntax as ListMemos. + description: "Optional. Filter to apply to memo stats.\r\n Uses the same filter syntax as ListMemos." schema: type: string responses: @@ -2368,9 +2482,7 @@ components: properties: name: type: string - description: |- - The name of the attachment. - Format: attachments/{attachment} + description: "The name of the attachment.\r\n Format: attachments/{attachment}" createTime: readOnly: true type: string @@ -2396,9 +2508,7 @@ components: description: Output only. The size of the attachment in bytes. memo: type: string - description: |- - Optional. The related memo. Refer to `Memo.name`. - Format: memos/{memo} + description: "Optional. The related memo. Refer to `Memo.name`.\r\n Format: memos/{memo}" motionMedia: allOf: - $ref: '#/components/schemas/MotionMedia' @@ -2421,9 +2531,7 @@ components: type: array items: type: string - description: |- - The resource names of the instance settings. - Format: instance/settings/{setting} + description: "The resource names of the instance settings.\r\n Format: instance/settings/{setting}" description: Request message for BatchGetInstanceSettings method. BatchGetInstanceSettingsResponse: type: object @@ -2631,14 +2739,10 @@ components: properties: parent: type: string - description: |- - Required. The parent user who owns the linked identity. - Format: users/{user} + description: "Required. The parent user who owns the linked identity.\r\n Format: users/{user}" idpName: type: string - description: |- - Required. The identity provider to link. - Format: identity-providers/{uid} + description: "Required. The identity provider to link.\r\n Format: identity-providers/{uid}" code: type: string description: Required. The authorization code from the identity provider. @@ -2655,9 +2759,7 @@ components: properties: parent: type: string - description: |- - Required. The parent resource where this token will be created. - Format: users/{user} + description: "Required. The parent resource where this token will be created.\r\n Format: users/{user}" description: type: string description: Optional. Description of the personal access token. @@ -2674,9 +2776,7 @@ components: description: The personal access token metadata. token: type: string - description: |- - The actual token value - only returned on creation. - This is the only time the token value will be visible. + description: "The actual token value - only returned on creation.\r\n This is the only time the token value will be visible." FieldMapping: type: object properties: @@ -2705,12 +2805,6 @@ components: allOf: - $ref: '#/components/schemas/User' description: The authenticated user's information. - GetUserWebhookSigningSecretResponse: - type: object - properties: - signingSecret: - type: string - description: The signing secret, in the Standard Webhooks "whsec_" form. GoogleProtobufAny: type: object properties: @@ -2719,6 +2813,52 @@ components: description: The type of the serialized message. additionalProperties: true description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. + Group: + required: + - displayName + type: object + properties: + name: + type: string + description: |- + The resource name of the group. + Format: groups/{group} + displayName: + type: string + description: The display name of the group. + description: + type: string + description: The description of the group. + creator: + readOnly: true + type: string + description: |- + The name of the creator. + Format: users/{user} + createTime: + readOnly: true + type: string + description: The creation timestamp. + format: date-time + GroupMember: + required: + - role + type: object + properties: + name: + type: string + description: |- + The resource name of the group member. + Format: groups/{group}/members/{member} + The {member} segment is the user ID. + role: + enum: + - ROLE_UNSPECIFIED + - MEMBER + - ADMIN + - OWNER + type: string + format: enum IdentityProvider: required: - type @@ -2728,9 +2868,7 @@ components: properties: name: type: string - description: |- - The resource name of the identity provider. - Format: identity-providers/{idp} + description: "The resource name of the identity provider.\r\n Format: identity-providers/{idp}" type: enum: - TYPE_UNSPECIFIED @@ -2768,29 +2906,17 @@ components: admin: allOf: - $ref: '#/components/schemas/User' - description: |- - The first administrator who set up this instance, for display purposes. - May be null on an instance that has lost all admins; use needs_setup to - determine whether initial setup is actually required. + description: "The first administrator who set up this instance.\r\n When null, instance requires initial setup (creating the first admin account)." commit: type: string description: Commit is the current build commit of instance. - needsSetup: - type: boolean - description: |- - NeedsSetup is true when the instance has no users yet and requires initial - setup (creating the first admin account). Unlike a null admin, this stays - false once any user exists, so an instance that has lost its admins is not - mistaken for a fresh install. description: Instance profile message containing basic instance information. InstanceSetting: type: object properties: name: type: string - description: |- - The name of the instance setting. - Format: instance/settings/{setting} + description: "The name of the instance setting.\r\n Format: instance/settings/{setting}" generalSetting: $ref: '#/components/schemas/InstanceSetting_GeneralSetting' storageSetting: @@ -2844,9 +2970,7 @@ components: transcription: allOf: - $ref: '#/components/schemas/InstanceSetting_TranscriptionConfig' - description: |- - transcription is the speech-to-text feature configuration. - When unset or transcription.provider_id is empty, transcription is disabled. + description: "transcription is the speech-to-text feature configuration.\r\n When unset or transcription.provider_id is empty, transcription is disabled." description: AI provider configuration settings. InstanceSetting_GeneralSetting: type: object @@ -2869,10 +2993,7 @@ components: description: custom_profile is the custom profile. weekStartDayOffset: type: integer - description: |- - week_start_day_offset is the week start day offset from Sunday. - 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday - Default is Sunday. + description: "week_start_day_offset is the week start day offset from Sunday.\r\n 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\r\n Default is Sunday." format: int32 disallowChangeUsername: type: boolean @@ -2917,9 +3038,7 @@ components: format: enum filepathTemplate: type: string - description: |- - The template of file path. - e.g. assets/{timestamp}_{filename} + description: "The template of file path.\r\n e.g. assets/{timestamp}_{filename}" uploadSizeLimitMb: type: string description: The max upload size in megabytes. @@ -2934,9 +3053,7 @@ components: backgroundColor: allOf: - $ref: '#/components/schemas/Color' - description: |- - Optional background color for the tag label. - When unset, the default tag color is used. + description: "Optional background color for the tag label.\r\n When unset, the default tag color is used." blurContent: type: boolean description: Whether memos with this tag should have their content blurred. @@ -2948,34 +3065,20 @@ components: type: object additionalProperties: $ref: '#/components/schemas/InstanceSetting_TagMetadata' - description: |- - Map of tag name pattern to tag metadata. - Each key is treated as an anchored regular expression (^pattern$), - so a single entry like "project/.*" matches all tags under that prefix. - Exact tag names are also valid (they are trivially valid regex patterns). - description: |- - Tag metadata configuration. - Active tag metadata is stored in per-user tag settings. - This message remains for backward compatibility with existing clients and migrations. + description: "Map of tag name pattern to tag metadata.\r\n Each key is treated as an anchored regular expression (^pattern$),\r\n so a single entry like \"project/.*\" matches all tags under that prefix.\r\n Exact tag names are also valid (they are trivially valid regex patterns)." + description: Tag metadata configuration. InstanceSetting_TranscriptionConfig: type: object properties: providerId: type: string - description: |- - provider_id references an entry in AISetting.providers[].id. - Empty string means transcription is disabled. + description: "provider_id references an entry in AISetting.providers[].id.\r\n Empty string means transcription is disabled." model: type: string - description: |- - model is the provider-specific model identifier. - Empty string falls back to the engine default - (whisper-1 for OPENAI providers, gemini-2.5-flash for GEMINI providers). + description: "model is the provider-specific model identifier.\r\n Empty string falls back to the engine default\r\n (whisper-1 for OPENAI providers, gemini-2.5-flash for GEMINI providers)." language: type: string - description: |- - language is the default ISO 639-1 language hint sent to the provider. - Empty string lets the provider auto-detect. + description: "language is the default ISO 639-1 language hint sent to the provider.\r\n Empty string lets the provider auto-detect." prompt: type: string description: prompt is a default spelling/vocabulary hint passed to the provider. @@ -3023,15 +3126,11 @@ components: properties: name: type: string - description: |- - The resource name of the linked identity. - Format: users/{user}/linkedIdentities/{linked_identity} + description: "The resource name of the linked identity.\r\n Format: users/{user}/linkedIdentities/{linked_identity}" idpName: readOnly: true type: string - description: |- - The resource name of the identity provider. - Format: identity-providers/{uid} + description: "The resource name of the identity provider.\r\n Format: identity-providers/{uid}" externUid: readOnly: true type: string @@ -3055,13 +3154,25 @@ components: description: The list of attachments. nextPageToken: type: string - description: |- - A token that can be sent as `page_token` to retrieve the next page. - If this field is omitted, there are no subsequent pages. + description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages." totalSize: type: integer description: The total count of attachments (may be approximate). format: int32 + ListGroupMembersResponse: + type: object + properties: + members: + type: array + items: + $ref: '#/components/schemas/GroupMember' + ListGroupsResponse: + type: object + properties: + groups: + type: array + items: + $ref: '#/components/schemas/Group' ListIdentityProvidersResponse: type: object properties: @@ -3148,9 +3259,7 @@ components: description: The list of memos. nextPageToken: type: string - description: |- - A token that can be sent as `page_token` to retrieve the next page. - If this field is omitted, there are no subsequent pages. + description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages." ListPersonalAccessTokensResponse: type: object properties: @@ -3193,9 +3302,7 @@ components: description: The list of user settings. nextPageToken: type: string - description: |- - A token that can be sent as `page_token` to retrieve the next page. - If this field is omitted, there are no subsequent pages. + description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages." totalSize: type: integer description: The total count of settings (may be approximate). @@ -3219,9 +3326,7 @@ components: description: The list of users. nextPageToken: type: string - description: |- - A token that can be sent as `page_token` to retrieve the next page. - If this field is omitted, there are no subsequent pages. + description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages." totalSize: type: integer description: The total count of users (may be approximate). @@ -3249,9 +3354,7 @@ components: properties: name: type: string - description: |- - The resource name of the memo. - Format: memos/{memo}, memo is the user defined id or uuid. + description: "The resource name of the memo.\r\n Format: memos/{memo}, memo is the user defined id or uuid." state: enum: - STATE_UNSPECIFIED @@ -3263,20 +3366,14 @@ components: creator: readOnly: true type: string - description: |- - The name of the creator. - Format: users/{user} + description: "The name of the creator.\r\n Format: users/{user}" createTime: type: string - description: |- - The creation timestamp. - If not set on creation, the server will set it to the current time. + description: "The creation timestamp.\r\n If not set on creation, the server will set it to the current time." format: date-time updateTime: type: string - description: |- - The last update timestamp. - If not set on creation, the server will set it to the current time. + description: "The last update timestamp.\r\n If not set on creation, the server will set it to the current time." format: date-time content: type: string @@ -3287,11 +3384,9 @@ components: - PRIVATE - PROTECTED - PUBLIC + - GROUP type: string - description: |- - The visibility of the memo. - One of PRIVATE (creator only), PROTECTED (signed-in users), or - PUBLIC (anyone). Defaults to PRIVATE on creation when unspecified. + description: The visibility of the memo. format: enum tags: readOnly: true @@ -3326,9 +3421,7 @@ components: parent: readOnly: true type: string - description: |- - Output only. The name of the parent memo. - Format: memos/{memo} + description: "Output only. The name of the parent memo.\r\n Format: memos/{memo}" snippet: readOnly: true type: string @@ -3337,6 +3430,9 @@ components: allOf: - $ref: '#/components/schemas/Location' description: Optional. The location of the memo. + group: + type: string + description: "Optional. The resource name of the group.\r\n Format: groups/{group}" MemoRelation: required: - memo @@ -3366,9 +3462,7 @@ components: properties: name: type: string - description: |- - The resource name of the memo. - Format: memos/{memo} + description: "The resource name of the memo.\r\n Format: memos/{memo}" snippet: readOnly: true type: string @@ -3379,9 +3473,7 @@ components: properties: name: type: string - description: |- - The resource name of the share. Format: memos/{memo}/shares/{share} - The {share} segment is the opaque token used in the share URL. + description: "The resource name of the share. Format: memos/{memo}/shares/{share}\r\n The {share} segment is the opaque token used in the share URL." createTime: readOnly: true type: string @@ -3389,9 +3481,7 @@ components: format: date-time expireTime: type: string - description: |- - Optional. When set, the share link stops working after this time. - If unset, the link never expires. + description: "Optional. When set, the share link stops working after this time.\r\n If unset, the link never expires." format: date-time description: MemoShare is an access grant that permits read-only access to a memo via an opaque bearer token. Memo_Property: @@ -3483,9 +3573,7 @@ components: properties: name: type: string - description: |- - The resource name of the personal access token. - Format: users/{user}/personalAccessTokens/{personal_access_token} + description: "The resource name of the personal access token.\r\n Format: users/{user}/personalAccessTokens/{personal_access_token}" description: type: string description: The description of the token. @@ -3503,9 +3591,7 @@ components: type: string description: Output only. The last used timestamp. format: date-time - description: |- - PersonalAccessToken represents a long-lived token for API/script access. - PATs are distinct from short-lived JWT access tokens used for session authentication. + description: "PersonalAccessToken represents a long-lived token for API/script access.\r\n PATs are distinct from short-lived JWT access tokens used for session authentication." Reaction: required: - contentId @@ -3515,21 +3601,14 @@ components: name: readOnly: true type: string - description: |- - The resource name of the reaction. - Format: memos/{memo}/reactions/{reaction} + description: "The resource name of the reaction.\r\n Format: memos/{memo}/reactions/{reaction}" creator: readOnly: true type: string - description: |- - The resource name of the creator. - Format: users/{user} + description: "The resource name of the creator.\r\n Format: users/{user}" contentId: type: string - description: |- - The resource name of the content. - For memo reactions, this should be the memo's resource name. - Format: memos/{memo} + description: "The resource name of the content.\r\n For memo reactions, this should be the memo's resource name.\r\n Format: memos/{memo}" reactionType: type: string description: "Required. The type of reaction (e.g., \"\U0001F44D\", \"❤️\", \"\U0001F604\")." @@ -3559,9 +3638,7 @@ components: properties: name: type: string - description: |- - Required. The resource name of the memo. - Format: memos/{memo} + description: "Required. The resource name of the memo.\r\n Format: memos/{memo}" attachments: type: array items: @@ -3575,9 +3652,7 @@ components: properties: name: type: string - description: |- - Required. The resource name of the memo. - Format: memos/{memo} + description: "Required. The resource name of the memo.\r\n Format: memos/{memo}" relations: type: array items: @@ -3590,17 +3665,13 @@ components: properties: name: type: string - description: |- - The resource name of the shortcut. - Format: users/{username}/shortcuts/{shortcut} + description: "The resource name of the shortcut.\r\n Format: users/{username}/shortcuts/{shortcut}" title: type: string description: The title of the shortcut. filter: type: string - description: |- - The CEL filter expression for the shortcut, using the same grammar as the - ListMemos `filter` argument. Reuse it by passing this value to ListMemos. + description: The filter expression for the shortcut. SignInRequest: type: object properties: @@ -3634,9 +3705,7 @@ components: properties: idpName: type: string - description: |- - The resource name of the SSO provider. - Format: identity-providers/{uid} + description: "The resource name of the SSO provider.\r\n Format: identity-providers/{uid}" code: type: string description: The authorization code from the SSO provider. @@ -3645,9 +3714,7 @@ components: description: The redirect URI used in the SSO flow. codeVerifier: type: string - description: |- - The PKCE code verifier for enhanced security (RFC 7636). - Optional - enables PKCE flow protection against authorization code interception. + description: "The PKCE code verifier for enhanced security (RFC 7636).\r\n Optional - enables PKCE flow protection against authorization code interception." description: Nested message for SSO authentication credentials. SignInResponse: type: object @@ -3658,14 +3725,10 @@ components: description: The authenticated user's information. accessToken: type: string - description: |- - The short-lived access token for API requests. - Store in memory only, not in localStorage. + description: "The short-lived access token for API requests.\r\n Store in memory only, not in localStorage." accessTokenExpiresAt: type: string - description: |- - When the access token expires. - Client should call RefreshToken before this time. + description: "When the access token expires.\r\n Client should call RefreshToken before this time." format: date-time Status: type: object @@ -3699,15 +3762,7 @@ components: type: string usePathStyle: type: boolean - insecureSkipTlsVerify: - type: boolean - description: |- - insecure_skip_tls_verify disables TLS certificate verification when connecting - to the S3 endpoint. Only enable this for trusted endpoints that use a self-signed - certificate; it removes protection against man-in-the-middle attacks. - description: |- - S3 configuration for cloud storage backend. - Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ + description: "S3 configuration for cloud storage backend.\r\n Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/" TestInstanceEmailSettingRequest: type: object properties: @@ -3759,9 +3814,7 @@ components: properties: name: type: string - description: |- - Required. The resource name of the memo. - Format: memos/{memo} + description: "Required. The resource name of the memo.\r\n Format: memos/{memo}" reaction: allOf: - $ref: '#/components/schemas/Reaction' @@ -3775,9 +3828,7 @@ components: properties: name: type: string - description: |- - The resource name of the user. - Format: users/{user} + description: "The resource name of the user.\r\n Format: users/{user}" role: enum: - ROLE_UNSPECIFIED @@ -3829,15 +3880,11 @@ components: name: readOnly: true type: string - description: |- - The resource name of the notification. - Format: users/{user}/notifications/{notification} + description: "The resource name of the notification.\r\n Format: users/{user}/notifications/{notification}" sender: readOnly: true type: string - description: |- - The sender of the notification. - Format: users/{user} + description: "The sender of the notification.\r\n Format: users/{user}" senderUser: readOnly: true allOf: @@ -3878,14 +3925,10 @@ components: properties: memo: type: string - description: |- - The memo name of comment. - Format: memos/{memo} + description: "The memo name of comment.\r\n Format: memos/{memo}" relatedMemo: type: string - description: |- - The name of related memo. - Format: memos/{memo} + description: "The name of related memo.\r\n Format: memos/{memo}" memoSnippet: type: string description: Preview text of the comment memo. @@ -3897,14 +3940,10 @@ components: properties: memo: type: string - description: |- - The memo that contains the mention. - Format: memos/{memo} + description: "The memo that contains the mention.\r\n Format: memos/{memo}" relatedMemo: type: string - description: |- - The related parent memo when the mention was created in a comment. - Format: memos/{memo} + description: "The related parent memo when the mention was created in a comment.\r\n Format: memos/{memo}" memoSnippet: type: string description: Preview text of the memo that contains the mention. @@ -3916,16 +3955,11 @@ components: properties: name: type: string - description: |- - The name of the user setting. - Format: users/{username}/settings/{setting}, {setting} is the key for the setting. - For example, "users/steven/settings/GENERAL" for general settings. + description: "The name of the user setting.\r\n Format: users/{username}/settings/{setting}, {setting} is the key for the setting.\r\n For example, \"users/steven/settings/GENERAL\" for general settings." generalSetting: $ref: '#/components/schemas/UserSetting_GeneralSetting' webhooksSetting: $ref: '#/components/schemas/UserSetting_WebhooksSetting' - tagsSetting: - $ref: '#/components/schemas/UserSetting_TagsSetting' description: User settings message UserSetting_GeneralSetting: type: object @@ -3938,35 +3972,8 @@ components: description: The default visibility of the memo. theme: type: string - description: |- - The preferred theme of the user. - This references a CSS file in the web/public/themes/ directory. - If not set, the default theme will be used. + description: "The preferred theme of the user.\r\n This references a CSS file in the web/public/themes/ directory.\r\n If not set, the default theme will be used." description: General user settings configuration. - UserSetting_TagMetadata: - type: object - properties: - backgroundColor: - allOf: - - $ref: '#/components/schemas/Color' - description: |- - Optional background color for the tag label. - When unset, the default tag color is used. - blurContent: - type: boolean - description: Whether memos with this tag should have their content blurred. - description: Tag metadata for user-specific display rules. - UserSetting_TagsSetting: - type: object - properties: - tags: - type: object - additionalProperties: - $ref: '#/components/schemas/UserSetting_TagMetadata' - description: |- - Map of tag name pattern to tag metadata. - Each key is treated as an anchored regular expression (^pattern$). - description: User-specific tag metadata. UserSetting_WebhooksSetting: type: object properties: @@ -3981,9 +3988,7 @@ components: properties: name: type: string - description: |- - The resource name of the user whose stats these are. - Format: users/{user} + description: "The resource name of the user whose stats these are.\r\n Format: users/{user}" memoTypeStats: allOf: - $ref: '#/components/schemas/UserStats_MemoTypeStats' @@ -4005,10 +4010,7 @@ components: items: type: string format: date-time - description: |- - The latest update timestamps of the user's memos (one per memo, - mirrors memo_created_timestamps). Used by the activity heatmap when - the client's view is set to update_time basis. + description: "The latest update timestamps of the user's memos (one per memo,\r\n mirrors memo_created_timestamps). Used by the activity heatmap when\r\n the client's view is set to update_time basis." pinnedMemos: type: array items: @@ -4040,9 +4042,7 @@ components: properties: name: type: string - description: |- - The name of the webhook. - Format: users/{user}/webhooks/{webhook} + description: "The name of the webhook.\r\n Format: users/{user}/webhooks/{webhook}" url: type: string description: The URL to send the webhook to. @@ -4059,21 +4059,12 @@ components: type: string description: The last update time of the webhook. format: date-time - signingSecret: - writeOnly: true - type: string - description: |- - Optional. Signing secret used to HMAC-SHA256 sign the webhook request body. - This field is input-only; it is never returned in responses. - signingSecretSet: - readOnly: true - type: boolean - description: Whether a signing secret is configured for this webhook. description: UserWebhook represents a webhook owned by a user. tags: - name: AIService - name: AttachmentService - name: AuthService + - name: GroupService - name: IdentityProviderService - name: InstanceService - name: MemoService diff --git a/proto/gen/store/instance_setting.pb.go b/proto/gen/store/instance_setting.pb.go index 263b289e072e8..fe3647e808400 100644 --- a/proto/gen/store/instance_setting.pb.go +++ b/proto/gen/store/instance_setting.pb.go @@ -1116,7 +1116,6 @@ type TranscriptionConfig struct { // - whisper-1 (legacy, lower cost) // - gpt-4o-transcribe, gpt-4o-mini-transcribe (higher quality) // - gpt-4o-transcribe-diarize (includes speaker labels) - // // GEMINI examples: // - gemini-2.5-flash (default, multimodal call) // - gemini-2.5-pro diff --git a/server/router/api/v1/connect_handler.go b/server/router/api/v1/connect_handler.go index 95d90d7bc154a..7f8d71dbb42d6 100644 --- a/server/router/api/v1/connect_handler.go +++ b/server/router/api/v1/connect_handler.go @@ -42,6 +42,7 @@ func (s *ConnectServiceHandler) RegisterConnectHandlers(mux *http.ServeMux, opts wrap(apiv1connect.NewAIServiceHandler(s, opts...)), wrap(apiv1connect.NewShortcutServiceHandler(s, opts...)), wrap(apiv1connect.NewIdentityProviderServiceHandler(s, opts...)), + wrap(apiv1connect.NewGroupServiceHandler(s, opts...)), } for _, h := range handlers { diff --git a/server/router/api/v1/connect_services.go b/server/router/api/v1/connect_services.go index 0c266fa9f9b4a..cae059016903f 100644 --- a/server/router/api/v1/connect_services.go +++ b/server/router/api/v1/connect_services.go @@ -606,3 +606,78 @@ func (s *ConnectServiceHandler) DeleteIdentityProvider(ctx context.Context, req } return connect.NewResponse(resp), nil } + +// GroupService + +func (s *ConnectServiceHandler) CreateGroup(ctx context.Context, req *connect.Request[v1pb.CreateGroupRequest]) (*connect.Response[v1pb.Group], error) { + resp, err := s.APIV1Service.CreateGroup(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + +func (s *ConnectServiceHandler) ListGroups(ctx context.Context, req *connect.Request[v1pb.ListGroupsRequest]) (*connect.Response[v1pb.ListGroupsResponse], error) { + resp, err := s.APIV1Service.ListGroups(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + +func (s *ConnectServiceHandler) GetGroup(ctx context.Context, req *connect.Request[v1pb.GetGroupRequest]) (*connect.Response[v1pb.Group], error) { + resp, err := s.APIV1Service.GetGroup(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + +func (s *ConnectServiceHandler) UpdateGroup(ctx context.Context, req *connect.Request[v1pb.UpdateGroupRequest]) (*connect.Response[v1pb.Group], error) { + resp, err := s.APIV1Service.UpdateGroup(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + +func (s *ConnectServiceHandler) DeleteGroup(ctx context.Context, req *connect.Request[v1pb.DeleteGroupRequest]) (*connect.Response[emptypb.Empty], error) { + resp, err := s.APIV1Service.DeleteGroup(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + +func (s *ConnectServiceHandler) AddGroupMember(ctx context.Context, req *connect.Request[v1pb.AddGroupMemberRequest]) (*connect.Response[v1pb.GroupMember], error) { + resp, err := s.APIV1Service.AddGroupMember(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + +func (s *ConnectServiceHandler) ListGroupMembers(ctx context.Context, req *connect.Request[v1pb.ListGroupMembersRequest]) (*connect.Response[v1pb.ListGroupMembersResponse], error) { + resp, err := s.APIV1Service.ListGroupMembers(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + +func (s *ConnectServiceHandler) UpdateGroupMember(ctx context.Context, req *connect.Request[v1pb.UpdateGroupMemberRequest]) (*connect.Response[v1pb.GroupMember], error) { + resp, err := s.APIV1Service.UpdateGroupMember(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + +func (s *ConnectServiceHandler) RemoveGroupMember(ctx context.Context, req *connect.Request[v1pb.RemoveGroupMemberRequest]) (*connect.Response[emptypb.Empty], error) { + resp, err := s.APIV1Service.RemoveGroupMember(ctx, req.Msg) + if err != nil { + return nil, convertGRPCError(err) + } + return connect.NewResponse(resp), nil +} + diff --git a/server/router/api/v1/group_service.go b/server/router/api/v1/group_service.go new file mode 100644 index 0000000000000..d9a081f3f97ab --- /dev/null +++ b/server/router/api/v1/group_service.go @@ -0,0 +1,215 @@ +package v1 + +import ( + "context" + "fmt" + "time" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) CreateGroup(ctx context.Context, request *v1pb.CreateGroupRequest) (*v1pb.Group, error) { + user, err := s.fetchCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + create := &store.Group{ + Name: request.Group.DisplayName, + Description: request.Group.Description, + CreatorID: user.ID, + Visibility: store.Public, // Default to public for now + } + group, err := s.Store.CreateGroup(ctx, create) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create group") + } + + return s.convertGroupFromStore(ctx, group) +} + +func (s *APIV1Service) ListGroups(ctx context.Context, request *v1pb.ListGroupsRequest) (*v1pb.ListGroupsResponse, error) { + groups, err := s.Store.ListGroups(ctx, &store.FindGroup{}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list groups") + } + + var pbGroups []*v1pb.Group + for _, g := range groups { + if pbGroup, err := s.convertGroupFromStore(ctx, g); err == nil { + pbGroups = append(pbGroups, pbGroup) + } + } + + return &v1pb.ListGroupsResponse{Groups: pbGroups}, nil +} + +func (s *APIV1Service) GetGroup(ctx context.Context, request *v1pb.GetGroupRequest) (*v1pb.Group, error) { + // Simplified get implementation + return nil, status.Errorf(codes.Unimplemented, "GetGroup is not implemented") +} + +func (s *APIV1Service) UpdateGroup(ctx context.Context, request *v1pb.UpdateGroupRequest) (*v1pb.Group, error) { + user, err := s.fetchCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + var groupID int32 + _, err = fmt.Sscanf(request.Group.Name, "groups/%d", &groupID) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid group name: %s", request.Group.Name) + } + + group, err := s.Store.GetGroup(ctx, &store.FindGroup{ID: &groupID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to find group") + } + if group == nil { + return nil, status.Errorf(codes.NotFound, "group not found") + } + + // Verify permission: only group owner/creator can update + members, err := s.Store.ListGroupMembers(ctx, &store.FindGroupMember{ + GroupID: &groupID, + UserID: &user.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list group members") + } + + + + isOwnerOrAdmin := false + if user.Role == store.RoleAdmin { + isOwnerOrAdmin = true + } + for _, member := range members { + if member.Role == "OWNER" || member.Role == "ADMIN" { + isOwnerOrAdmin = true + break + } + } + if group.CreatorID == user.ID { + isOwnerOrAdmin = true + } + + if !isOwnerOrAdmin { + return nil, status.Errorf(codes.PermissionDenied, "permission denied to update group") + } + + update := &store.UpdateGroup{ + ID: groupID, + } + + for _, path := range request.UpdateMask.Paths { + if path == "display_name" { + update.Name = &request.Group.DisplayName + } else if path == "description" { + update.Description = &request.Group.Description + } + } + + updatedGroup, err := s.Store.UpdateGroup(ctx, update) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update group") + } + + return s.convertGroupFromStore(ctx, updatedGroup) +} + +func (s *APIV1Service) DeleteGroup(ctx context.Context, request *v1pb.DeleteGroupRequest) (*emptypb.Empty, error) { + user, err := s.fetchCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + var groupID int32 + _, err = fmt.Sscanf(request.Name, "groups/%d", &groupID) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid group name: %s", request.Name) + } + + group, err := s.Store.GetGroup(ctx, &store.FindGroup{ID: &groupID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to find group") + } + if group == nil { + return nil, status.Errorf(codes.NotFound, "group not found") + } + + // Verify permission: only group owner/creator can delete + members, err := s.Store.ListGroupMembers(ctx, &store.FindGroupMember{ + GroupID: &groupID, + UserID: &user.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list group members") + } + + + + isOwnerOrAdmin := false + if user.Role == store.RoleAdmin { + isOwnerOrAdmin = true + } + for _, member := range members { + if member.Role == "OWNER" || member.Role == "ADMIN" { + isOwnerOrAdmin = true + break + } + } + if group.CreatorID == user.ID { + isOwnerOrAdmin = true + } + + if !isOwnerOrAdmin { + return nil, status.Errorf(codes.PermissionDenied, "permission denied to delete group") + } + + err = s.Store.DeleteGroup(ctx, groupID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete group") + } + + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) AddGroupMember(ctx context.Context, request *v1pb.AddGroupMemberRequest) (*v1pb.GroupMember, error) { + return nil, status.Errorf(codes.Unimplemented, "AddGroupMember is not implemented") +} + +func (s *APIV1Service) ListGroupMembers(ctx context.Context, request *v1pb.ListGroupMembersRequest) (*v1pb.ListGroupMembersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "ListGroupMembers is not implemented") +} + +func (s *APIV1Service) UpdateGroupMember(ctx context.Context, request *v1pb.UpdateGroupMemberRequest) (*v1pb.GroupMember, error) { + return nil, status.Errorf(codes.Unimplemented, "UpdateGroupMember is not implemented") +} + +func (s *APIV1Service) RemoveGroupMember(ctx context.Context, request *v1pb.RemoveGroupMemberRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "RemoveGroupMember is not implemented") +} + +func (s *APIV1Service) convertGroupFromStore(ctx context.Context, group *store.Group) (*v1pb.Group, error) { + return &v1pb.Group{ + Name: fmt.Sprintf("groups/%d", group.ID), + DisplayName: group.Name, + Description: group.Description, + CreateTime: timestamppb.New(time.Unix(group.CreatedTs, 0)), + }, nil +} diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index 21afdf3721613..85d6d6e31c393 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -66,6 +66,19 @@ func (s *APIV1Service) checkMemoReadAccess(ctx context.Context, memo *store.Memo if memo.Visibility == store.Private && memo.CreatorID != user.ID { return status.Errorf(codes.PermissionDenied, "permission denied") } + if memo.Visibility == store.GroupVisibility { + if memo.GroupID == nil { + return status.Errorf(codes.Internal, "group visibility requires a valid group_id") + } + isMember, err := s.Store.CheckUserInGroup(ctx, user.ID, *memo.GroupID) + if err != nil || !isMember { + // Fallback: check if the user is the group creator + group, err := s.Store.GetGroup(ctx, &store.FindGroup{ID: memo.GroupID}) + if err != nil || group == nil || group.CreatorID != user.ID { + return status.Errorf(codes.PermissionDenied, "permission denied: not a group member") + } + } + } } return nil } @@ -90,6 +103,26 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR Content: request.Memo.Content, Visibility: convertVisibilityToStore(request.Memo.Visibility), } + if request.Memo.Group != nil { + var groupID int32 + if _, err := fmt.Sscanf(*request.Memo.Group, "groups/%d", &groupID); err == nil { + create.GroupID = &groupID + } + } + + if create.Visibility == store.GroupVisibility || create.GroupID != nil { + if create.GroupID == nil { + return nil, status.Errorf(codes.InvalidArgument, "group visibility or selection requires a valid group_id") + } + isMember, err := s.Store.CheckUserInGroup(ctx, user.ID, *create.GroupID) + if err != nil || !isMember { + // Check if they are the group creator + group, err := s.Store.GetGroup(ctx, &store.FindGroup{ID: create.GroupID}) + if err != nil || group == nil || group.CreatorID != user.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied: not a group member") + } + } + } // Set custom timestamps if provided in the request. if request.Memo.CreateTime != nil && request.Memo.CreateTime.IsValid() { @@ -221,11 +254,34 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq if currentUser == nil { memoFind.VisibilityList = []store.Visibility{store.Public} } else { + // Fetch user's groups to build GROUP visibility filter + myGroupIDs := []int32{} + members, err := s.Store.ListGroupMembers(ctx, &store.FindGroupMember{UserID: ¤tUser.ID}) + if err == nil { + for _, member := range members { + myGroupIDs = append(myGroupIDs, member.GroupID) + } + } + + var groupFilter string + if len(myGroupIDs) > 0 { + var groupConds []string + for _, gid := range myGroupIDs { + groupConds = append(groupConds, fmt.Sprintf("group_id == %d", gid)) + } + groupFilter = fmt.Sprintf(`(visibility == "GROUP" && (%s))`, strings.Join(groupConds, " || ")) + } else { + groupFilter = `(visibility == "GROUP" && group_id == -1)` + } + if memoFind.CreatorID == nil { - filter := fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID) + filter := fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"] || %s`, currentUser.ID, groupFilter) memoFind.Filters = append(memoFind.Filters, filter) } else if *memoFind.CreatorID != currentUser.ID { - memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} + // If we are viewing another user's memos, we can only see their public, protected, or group memos where we are members. + filter := fmt.Sprintf(`creator_id == %d && (visibility in ["PUBLIC", "PROTECTED"] || %s)`, *memoFind.CreatorID, groupFilter) + memoFind.Filters = append(memoFind.Filters, filter) + memoFind.CreatorID = nil // Cleared because it is now enforced via the CEL filter! } } @@ -313,6 +369,8 @@ func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosReq return nil, status.Errorf(codes.Internal, "failed to list memo creators: %v", err) } for _, memo := range memos { + + memoName := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID) reactions := reactionMap[memoName] attachments := attachmentMap[memo.ID] @@ -510,6 +568,16 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR visibility = parentMemo.Visibility } update.Visibility = &visibility + } else if path == "group" { + if request.Memo.Group != nil { + var groupID int32 + if _, err := fmt.Sscanf(*request.Memo.Group, "groups/%d", &groupID); err == nil { + update.GroupID = &groupID + } + } else { + // If group is null, set to null in DB + update.ClearGroupID = true + } } else if path == "pinned" { update.Pinned = &request.Memo.Pinned } else if path == "state" { @@ -541,6 +609,53 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR } } + // Validate GroupVisibility and membership constraints after resolving all update paths. + finalVisibility := memo.Visibility + if update.Visibility != nil { + finalVisibility = *update.Visibility + } + + var finalGroupID *int32 + hasGroupUpdate := false + for _, path := range request.UpdateMask.Paths { + if path == "group" { + hasGroupUpdate = true + break + } + } + if hasGroupUpdate { + if update.ClearGroupID { + finalGroupID = nil + } else { + finalGroupID = update.GroupID + } + } else { + finalGroupID = memo.GroupID + } + + if finalVisibility == store.GroupVisibility { + if finalGroupID == nil { + return nil, status.Errorf(codes.InvalidArgument, "group visibility requires a valid group_id") + } + isMember, err := s.Store.CheckUserInGroup(ctx, user.ID, *finalGroupID) + if err != nil || !isMember { + // Check if they are the group creator + group, err := s.Store.GetGroup(ctx, &store.FindGroup{ID: finalGroupID}) + if err != nil || group == nil || group.CreatorID != user.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied: not a group member") + } + } + } else if hasGroupUpdate && finalGroupID != nil { + // Even if not GroupVisibility, check membership before associating with group + isMember, err := s.Store.CheckUserInGroup(ctx, user.ID, *finalGroupID) + if err != nil || !isMember { + group, err := s.Store.GetGroup(ctx, &store.FindGroup{ID: finalGroupID}) + if err != nil || group == nil || group.CreatorID != user.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied: not a group member") + } + } + } + if err = s.Store.UpdateMemo(ctx, update); err != nil { return nil, status.Errorf(codes.Internal, "failed to update memo") } diff --git a/server/router/api/v1/memo_service_converter.go b/server/router/api/v1/memo_service_converter.go index b949570e99993..51ff87faeb206 100644 --- a/server/router/api/v1/memo_service_converter.go +++ b/server/router/api/v1/memo_service_converter.go @@ -44,6 +44,10 @@ func (s *APIV1Service) convertMemoFromStoreWithCreators(ctx context.Context, mem Visibility: convertVisibilityFromStore(memo.Visibility), Pinned: memo.Pinned, } + if memo.GroupID != nil { + groupName := fmt.Sprintf("groups/%d", *memo.GroupID) + memoMessage.Group = &groupName + } if memo.Payload != nil { memoMessage.Tags = memo.Payload.Tags memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property) @@ -355,6 +359,8 @@ func convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility { return v1pb.Visibility_PROTECTED case store.Public: return v1pb.Visibility_PUBLIC + case store.GroupVisibility: + return v1pb.Visibility(4) default: return v1pb.Visibility_VISIBILITY_UNSPECIFIED } @@ -366,6 +372,8 @@ func convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility { return store.Protected case v1pb.Visibility_PUBLIC: return store.Public + case v1pb.Visibility(4): + return store.GroupVisibility default: return store.Private } diff --git a/server/router/api/v1/sse_handler.go b/server/router/api/v1/sse_handler.go index dedfbec6b3767..40473d0663532 100644 --- a/server/router/api/v1/sse_handler.go +++ b/server/router/api/v1/sse_handler.go @@ -25,13 +25,13 @@ type sseRouteRegistrar interface { func RegisterSSERoutes(router sseRouteRegistrar, hub *SSEHub, storeInstance *store.Store, secret string) { authenticator := auth.NewAuthenticator(storeInstance, secret) router.GET("/api/v1/sse", func(c *echo.Context) error { - return handleSSE(c, hub, authenticator) + return handleSSE(c, hub, authenticator, storeInstance) }) } // handleSSE handles the SSE connection for live memo refresh. // Authentication is done via Bearer token in the Authorization header. -func handleSSE(c *echo.Context, hub *SSEHub, authenticator *auth.Authenticator) error { +func handleSSE(c *echo.Context, hub *SSEHub, authenticator *auth.Authenticator, storeInstance *store.Store) error { // Authenticate the request. authHeader := c.Request().Header.Get("Authorization") result := authenticator.Authenticate(c.Request().Context(), authHeader) @@ -56,8 +56,17 @@ func handleSSE(c *echo.Context, hub *SSEHub, authenticator *auth.Authenticator) f.Flush() } + // Fetch user's groups to populate groupIDs + groupIDs := []int32{} + members, err := storeInstance.ListGroupMembers(c.Request().Context(), &store.FindGroupMember{UserID: &userID}) + if err == nil { + for _, member := range members { + groupIDs = append(groupIDs, member.GroupID) + } + } + // Subscribe to the hub. - client := hub.Subscribe(userID, role) + client := hub.Subscribe(userID, role, groupIDs) defer hub.Unsubscribe(client) // Create a ticker for heartbeat pings. diff --git a/server/router/api/v1/sse_hub.go b/server/router/api/v1/sse_hub.go index 88887c4a33706..ae3c7b71cf346 100644 --- a/server/router/api/v1/sse_hub.go +++ b/server/router/api/v1/sse_hub.go @@ -31,6 +31,7 @@ type SSEEvent struct { // Visibility and CreatorID are used only for server-side delivery filtering. Visibility store.Visibility `json:"-"` CreatorID int32 `json:"-"` + GroupID *int32 `json:"-"` } // JSON returns the JSON representation of the event. @@ -46,9 +47,10 @@ func (e *SSEEvent) JSON() []byte { // SSEClient represents a single SSE connection. type SSEClient struct { - events chan []byte - userID int32 - role store.Role + events chan []byte + userID int32 + role store.Role + groupIDs []int32 // Cached group IDs for filtering. } // SSEHub manages SSE client connections and broadcasts events. @@ -68,12 +70,13 @@ func NewSSEHub() *SSEHub { // Subscribe registers a new client and returns it. // The caller must call Unsubscribe when done. -func (h *SSEHub) Subscribe(userID int32, role store.Role) *SSEClient { +func (h *SSEHub) Subscribe(userID int32, role store.Role, groupIDs []int32) *SSEClient { c := &SSEClient{ // Buffer a few events so a slow client doesn't block broadcasting. - events: make(chan []byte, 32), - userID: userID, - role: role, + events: make(chan []byte, 32), + userID: userID, + role: role, + groupIDs: groupIDs, } h.mu.Lock() if h.closed { @@ -135,6 +138,18 @@ func (c *SSEClient) canReceive(event *SSEEvent) bool { switch event.Visibility { case store.Private: return c.userID == event.CreatorID || c.role == store.RoleAdmin + case store.GroupVisibility: + if c.role == store.RoleAdmin { + return true + } + if event.GroupID != nil { + for _, gid := range c.groupIDs { + if gid == *event.GroupID { + return true + } + } + } + return false case store.Public, store.Protected, "": return true default: diff --git a/server/router/api/v1/sse_hub_test.go b/server/router/api/v1/sse_hub_test.go index 2e2005ff19a38..3078001588c1a 100644 --- a/server/router/api/v1/sse_hub_test.go +++ b/server/router/api/v1/sse_hub_test.go @@ -35,7 +35,7 @@ func mustNotReceive(t *testing.T, ch <-chan []byte, within time.Duration) { func TestSSEHub_SubscribeUnsubscribe(t *testing.T) { hub := NewSSEHub() - client := hub.Subscribe(1, store.RoleUser) + client := hub.Subscribe(1, store.RoleUser, nil) require.NotNil(t, client) require.NotNil(t, client.events) @@ -49,8 +49,8 @@ func TestSSEHub_SubscribeUnsubscribe(t *testing.T) { func TestSSEHub_Close(t *testing.T) { hub := NewSSEHub() - c1 := hub.Subscribe(1, store.RoleUser) - c2 := hub.Subscribe(2, store.RoleAdmin) + c1 := hub.Subscribe(1, store.RoleUser, nil) + c2 := hub.Subscribe(2, store.RoleAdmin, nil) hub.Close() hub.Close() @@ -60,7 +60,7 @@ func TestSSEHub_Close(t *testing.T) { assert.False(t, ok, "channel should be closed after hub close") } - late := hub.Subscribe(3, store.RoleUser) + late := hub.Subscribe(3, store.RoleUser, nil) _, ok := <-late.events assert.False(t, ok, "late subscriber should be closed immediately") @@ -71,7 +71,7 @@ func TestSSEHub_Close(t *testing.T) { func TestSSEHub_Broadcast(t *testing.T) { hub := NewSSEHub() - client := hub.Subscribe(1, store.RoleUser) + client := hub.Subscribe(1, store.RoleUser, nil) defer hub.Unsubscribe(client) event := &SSEEvent{Type: SSEEventMemoCreated, Name: "memos/123"} @@ -88,9 +88,9 @@ func TestSSEHub_Broadcast(t *testing.T) { func TestSSEHub_BroadcastMultipleClients(t *testing.T) { hub := NewSSEHub() - c1 := hub.Subscribe(1, store.RoleUser) + c1 := hub.Subscribe(1, store.RoleUser, nil) defer hub.Unsubscribe(c1) - c2 := hub.Subscribe(2, store.RoleUser) + c2 := hub.Subscribe(2, store.RoleUser, nil) defer hub.Unsubscribe(c2) event := &SSEEvent{Type: SSEEventMemoDeleted, Name: "memos/456"} @@ -118,11 +118,11 @@ func TestSSEEvent_JSON(t *testing.T) { func TestSSEHub_PrivateEventsAreScoped(t *testing.T) { hub := NewSSEHub() - owner := hub.Subscribe(1, store.RoleUser) + owner := hub.Subscribe(1, store.RoleUser, nil) defer hub.Unsubscribe(owner) - other := hub.Subscribe(2, store.RoleUser) + other := hub.Subscribe(2, store.RoleUser, nil) defer hub.Unsubscribe(other) - admin := hub.Subscribe(3, store.RoleAdmin) + admin := hub.Subscribe(3, store.RoleAdmin, nil) defer hub.Unsubscribe(admin) hub.Broadcast(&SSEEvent{ @@ -153,7 +153,7 @@ func TestSSEHub_PrivateEventsAreScoped(t *testing.T) { func TestSSEClient_CanReceive_UnknownVisibility(t *testing.T) { hub := NewSSEHub() - client := hub.Subscribe(1, store.RoleUser) + client := hub.Subscribe(1, store.RoleUser, nil) defer hub.Unsubscribe(client) // An event with an unrecognised visibility value should be denied (safe default). @@ -169,7 +169,7 @@ func TestSSEClient_CanReceive_UnknownVisibility(t *testing.T) { func TestSSEHub_SlowClientEventsDropped(t *testing.T) { hub := NewSSEHub() // Subscribe but never read, so the channel fills up. - slow := hub.Subscribe(1, store.RoleUser) + slow := hub.Subscribe(1, store.RoleUser, nil) defer hub.Unsubscribe(slow) event := &SSEEvent{Type: SSEEventMemoCreated, Name: "memos/x"} diff --git a/server/router/api/v1/sse_service_test.go b/server/router/api/v1/sse_service_test.go index f7910bcbecc35..d0ea44be36801 100644 --- a/server/router/api/v1/sse_service_test.go +++ b/server/router/api/v1/sse_service_test.go @@ -93,7 +93,7 @@ func TestCreateMemoComment_NoDuplicateSSEBroadcast(t *testing.T) { // Subscribe after the parent memo is created so the memo.created event // for the parent does not pollute the assertion window. - client := svc.SSEHub.Subscribe(author.ID, store.RoleAdmin) + client := svc.SSEHub.Subscribe(author.ID, store.RoleAdmin, nil) defer svc.SSEHub.Unsubscribe(client) // Create a comment. Before the fix, this fired both memo.created (for the @@ -133,7 +133,7 @@ func TestCreateMemoWithAttachment_NoDuplicateUpdatedSSEBroadcast(t *testing.T) { }) require.NoError(t, err) - client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin) + client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin, nil) defer svc.SSEHub.Unsubscribe(client) memo, err := svc.CreateMemo(uctx, &v1pb.CreateMemoRequest{ @@ -172,7 +172,7 @@ func TestUpsertMemoReaction_SSEEvent(t *testing.T) { }) require.NoError(t, err) - client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin) + client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin, nil) defer svc.SSEHub.Unsubscribe(client) _, err = svc.UpsertMemoReaction(uctx, &v1pb.UpsertMemoReactionRequest{ @@ -215,7 +215,7 @@ func TestDeleteMemoReaction_SSEEvent(t *testing.T) { }) require.NoError(t, err) - client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin) + client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin, nil) defer svc.SSEHub.Unsubscribe(client) _, err = svc.DeleteMemoReaction(uctx, &v1pb.DeleteMemoReactionRequest{ @@ -255,7 +255,7 @@ func TestSetMemoAttachments_EmitsMemoUpdatedSSEEvent(t *testing.T) { }) require.NoError(t, err) - client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin) + client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin, nil) defer svc.SSEHub.Unsubscribe(client) _, err = svc.SetMemoAttachments(uctx, &v1pb.SetMemoAttachmentsRequest{ @@ -292,7 +292,7 @@ func TestSetMemoRelations_EmitsMemoUpdatedSSEEvent(t *testing.T) { }) require.NoError(t, err) - client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin) + client := svc.SSEHub.Subscribe(user.ID, store.RoleAdmin, nil) defer svc.SSEHub.Unsubscribe(client) _, err = svc.SetMemoRelations(uctx, &v1pb.SetMemoRelationsRequest{ diff --git a/server/router/api/v1/user_service_stats.go b/server/router/api/v1/user_service_stats.go index 3f6c99b9e2654..d79cc010d6914 100644 --- a/server/router/api/v1/user_service_stats.go +++ b/server/router/api/v1/user_service_stats.go @@ -3,6 +3,7 @@ package v1 import ( "context" "fmt" + "strings" "time" "google.golang.org/grpc/codes" @@ -80,17 +81,43 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, request *v1pb.ListA return &v1pb.ListAllUserStatsResponse{}, nil } memoFind.CreatorID = ¤tUser.ID - } else if currentUser == nil { - memoFind.VisibilityList = []store.Visibility{store.Public} } else { - if memoFind.CreatorID == nil { - filter := fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID) - memoFind.Filters = append(memoFind.Filters, filter) - } else if *memoFind.CreatorID != currentUser.ID { - memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} + if currentUser == nil { + memoFind.VisibilityList = []store.Visibility{store.Public} + } else { + // Fetch user's groups to build GROUP visibility filter + myGroupIDs := []int32{} + members, err := s.Store.ListGroupMembers(ctx, &store.FindGroupMember{UserID: ¤tUser.ID}) + if err == nil { + for _, member := range members { + myGroupIDs = append(myGroupIDs, member.GroupID) + } + } + + var groupFilter string + if len(myGroupIDs) > 0 { + var groupConds []string + for _, gid := range myGroupIDs { + groupConds = append(groupConds, fmt.Sprintf("group_id == %d", gid)) + } + groupFilter = fmt.Sprintf(`(visibility == "GROUP" && (%s))`, strings.Join(groupConds, " || ")) + } else { + groupFilter = `(visibility == "GROUP" && group_id == -1)` + } + + if memoFind.CreatorID == nil { + filter := fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"] || %s`, currentUser.ID, groupFilter) + memoFind.Filters = append(memoFind.Filters, filter) + } else if *memoFind.CreatorID != currentUser.ID { + filter := fmt.Sprintf(`creator_id == %d && (visibility in ["PUBLIC", "PROTECTED"] || %s)`, *memoFind.CreatorID, groupFilter) + memoFind.Filters = append(memoFind.Filters, filter) + memoFind.CreatorID = nil + } } } + + userMemoStatMap := make(map[int32]*v1pb.UserStats) pinnedMemoIDsByUserID := make(map[int32][]int32) limit := 1000 @@ -108,6 +135,8 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, request *v1pb.ListA } for _, memo := range memos { + + // Initialize user stats if not exists if _, exists := userMemoStatMap[memo.CreatorID]; !exists { userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{ @@ -216,10 +245,36 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt if currentUser == nil { memoFind.VisibilityList = []store.Visibility{store.Public} - } else if currentUser.ID != userID { - memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} + } else if currentUser.ID == userID { + // Creator can view all their memos + } else { + // Fetch user's groups to build GROUP visibility filter + myGroupIDs := []int32{} + members, err := s.Store.ListGroupMembers(ctx, &store.FindGroupMember{UserID: ¤tUser.ID}) + if err == nil { + for _, member := range members { + myGroupIDs = append(myGroupIDs, member.GroupID) + } + } + + var groupFilter string + if len(myGroupIDs) > 0 { + var groupConds []string + for _, gid := range myGroupIDs { + groupConds = append(groupConds, fmt.Sprintf("group_id == %d", gid)) + } + groupFilter = fmt.Sprintf(`(visibility == "GROUP" && (%s))`, strings.Join(groupConds, " || ")) + } else { + groupFilter = `(visibility == "GROUP" && group_id == -1)` + } + + filter := fmt.Sprintf(`creator_id == %d && (visibility in ["PUBLIC", "PROTECTED"] || %s)`, userID, groupFilter) + memoFind.Filters = append(memoFind.Filters, filter) + memoFind.CreatorID = nil } + + createdTimestamps := []*timestamppb.Timestamp{} updatedTimestamps := []*timestamppb.Timestamp{} tagCount := make(map[string]int32) @@ -244,9 +299,10 @@ func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserSt break } - totalMemoCount += int32(len(memos)) - for _, memo := range memos { + + + totalMemoCount++ createdTimestamps = append(createdTimestamps, timestamppb.New(time.Unix(memo.CreatedTs, 0))) updatedTimestamps = append(updatedTimestamps, timestamppb.New(time.Unix(memo.UpdatedTs, 0))) // Count different memo types based on content. diff --git a/server/router/api/v1/v1.go b/server/router/api/v1/v1.go index 378b41e8030f2..3da2440c1aae4 100644 --- a/server/router/api/v1/v1.go +++ b/server/router/api/v1/v1.go @@ -28,6 +28,7 @@ type APIV1Service struct { v1pb.UnimplementedAIServiceServer v1pb.UnimplementedShortcutServiceServer v1pb.UnimplementedIdentityProviderServiceServer + v1pb.UnimplementedGroupServiceServer Secret string Profile *profile.Profile @@ -120,6 +121,9 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech if err := v1pb.RegisterIdentityProviderServiceHandlerServer(ctx, gwMux, s); err != nil { return err } + if err := v1pb.RegisterGroupServiceHandlerServer(ctx, gwMux, s); err != nil { + return err + } gwGroup := echoServer.Group("") // Register SSE endpoint with same CORS as rest of /api/v1. RegisterSSERoutes(gwGroup, s.SSEHub, s.Store, s.Secret) diff --git a/server/router/frontend/dist/index.html b/server/router/frontend/dist/index.html index a612ed1f7de3a..0cffb5e789da0 100644 --- a/server/router/frontend/dist/index.html +++ b/server/router/frontend/dist/index.html @@ -1,11 +1,35 @@ - - - - - - Memos - - - No embeddable frontend found. - - + + + + + + + + + + + + + + + + + Memos + + + + + + + + + + + + + + +
+ + + diff --git a/store/db/mysql/group.go b/store/db/mysql/group.go new file mode 100644 index 0000000000000..2df62bc402d75 --- /dev/null +++ b/store/db/mysql/group.go @@ -0,0 +1,221 @@ +package mysql + +import ( + "context" + "strings" + + "github.com/pkg/errors" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateGroup(ctx context.Context, create *store.Group) (*store.Group, error) { + tx, err := d.db.BeginTx(ctx, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to begin transaction") + } + defer func() { + _ = tx.Rollback() + }() + + stmt := ` + INSERT INTO groups ( + name, + description, + creator_id, + visibility + ) + VALUES (?, ?, ?, ?) + ` + result, err := tx.ExecContext( + ctx, + stmt, + create.Name, + create.Description, + create.CreatorID, + create.Visibility, + ) + if err != nil { + return nil, errors.Wrap(err, "failed to insert group") + } + + rawID, err := result.LastInsertId() + if err != nil { + return nil, errors.Wrap(err, "failed to get last insert id") + } + create.ID = int32(rawID) + + memberStmt := ` + INSERT INTO group_members ( + group_id, + user_id, + role + ) + VALUES (?, ?, 'OWNER') + ` + if _, err := tx.ExecContext(ctx, memberStmt, create.ID, create.CreatorID); err != nil { + return nil, errors.Wrap(err, "failed to insert owner member") + } + + if err := tx.Commit(); err != nil { + return nil, errors.Wrap(err, "failed to commit transaction") + } + + group, err := d.GetGroup(ctx, &store.FindGroup{ID: &create.ID}) + if err != nil { + return nil, errors.Wrap(err, "failed to get group") + } + return group, nil +} + +func (d *DB) ListGroups(ctx context.Context, find *store.FindGroup) ([]*store.Group, error) { + where, args := []string{"1 = 1"}, []any{} + if find.ID != nil { + where, args = append(where, "id = ?"), append(args, *find.ID) + } + if find.Name != nil { + where, args = append(where, "name = ?"), append(args, *find.Name) + } + if find.CreatorID != nil { + where, args = append(where, "creator_id = ?"), append(args, *find.CreatorID) + } + + query := "SELECT id, name, description, creator_id, visibility, UNIX_TIMESTAMP(created_ts) FROM groups WHERE " + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to query groups") + } + defer rows.Close() + + var list []*store.Group + for rows.Next() { + var group store.Group + if err := rows.Scan( + &group.ID, + &group.Name, + &group.Description, + &group.CreatorID, + &group.Visibility, + &group.CreatedTs, + ); err != nil { + return nil, errors.Wrap(err, "failed to scan group") + } + list = append(list, &group) + } + + return list, nil +} + +func (d *DB) UpdateGroup(ctx context.Context, update *store.UpdateGroup) (*store.Group, error) { + set, args := []string{}, []any{} + if update.Name != nil { + set, args = append(set, "name = ?"), append(args, *update.Name) + } + if update.Description != nil { + set, args = append(set, "description = ?"), append(args, *update.Description) + } + if update.Visibility != nil { + set, args = append(set, "visibility = ?"), append(args, *update.Visibility) + } + + if len(set) == 0 { + return d.GetGroup(ctx, &store.FindGroup{ID: &update.ID}) + } + + args = append(args, update.ID) + query := "UPDATE groups SET " + strings.Join(set, ", ") + " WHERE id = ?" + if _, err := d.db.ExecContext(ctx, query, args...); err != nil { + return nil, errors.Wrap(err, "failed to execute update group query") + } + + return d.GetGroup(ctx, &store.FindGroup{ID: &update.ID}) +} + +func (d *DB) DeleteGroup(ctx context.Context, id int32) error { + if _, err := d.db.ExecContext(ctx, "UPDATE memo SET group_id = NULL WHERE group_id = ?", id); err != nil { + return errors.Wrap(err, "failed to unset group_id in memo") + } + if _, err := d.db.ExecContext(ctx, "DELETE FROM group_members WHERE group_id = ?", id); err != nil { + return errors.Wrap(err, "failed to delete group members") + } + if _, err := d.db.ExecContext(ctx, "DELETE FROM groups WHERE id = ?", id); err != nil { + return errors.Wrap(err, "failed to delete group") + } + return nil +} + +func (d *DB) GetGroup(ctx context.Context, find *store.FindGroup) (*store.Group, error) { + list, err := d.ListGroups(ctx, find) + if err != nil { + return nil, errors.Wrap(err, "failed to list groups in GetGroup") + } + if len(list) == 0 { + return nil, nil + } + return list[0], nil +} + +func (d *DB) CreateGroupMember(ctx context.Context, create *store.GroupMember) (*store.GroupMember, error) { + stmt := ` + INSERT INTO group_members ( + group_id, + user_id, + role + ) + VALUES (?, ?, ?) + ` + if _, err := d.db.ExecContext(ctx, stmt, create.GroupID, create.UserID, create.Role); err != nil { + return nil, errors.Wrap(err, "failed to insert group member") + } + return create, nil +} + +func (d *DB) ListGroupMembers(ctx context.Context, find *store.FindGroupMember) ([]*store.GroupMember, error) { + where, args := []string{"1 = 1"}, []any{} + if find.GroupID != nil { + where, args = append(where, "group_id = ?"), append(args, *find.GroupID) + } + if find.UserID != nil { + where, args = append(where, "user_id = ?"), append(args, *find.UserID) + } + + query := "SELECT group_id, user_id, role FROM group_members WHERE " + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to query group members") + } + defer rows.Close() + + var list []*store.GroupMember + for rows.Next() { + var member store.GroupMember + if err := rows.Scan( + &member.GroupID, + &member.UserID, + &member.Role, + ); err != nil { + return nil, errors.Wrap(err, "failed to scan group member") + } + list = append(list, &member) + } + + return list, nil +} + +func (d *DB) UpdateGroupMember(ctx context.Context, update *store.GroupMember) (*store.GroupMember, error) { + query := "UPDATE group_members SET role = ? WHERE group_id = ? AND user_id = ?" + if _, err := d.db.ExecContext(ctx, query, update.Role, update.GroupID, update.UserID); err != nil { + return nil, errors.Wrap(err, "failed to update group member") + } + return update, nil +} + +func (d *DB) DeleteGroupMember(ctx context.Context, delete *store.GroupMember) error { + if delete.GroupID == 0 || delete.UserID == 0 { + return errors.New("missing group_id or user_id") + } + query := "DELETE FROM group_members WHERE group_id = ? AND user_id = ?" + if _, err := d.db.ExecContext(ctx, query, delete.GroupID, delete.UserID); err != nil { + return errors.Wrap(err, "failed to delete group member") + } + return nil +} diff --git a/store/db/mysql/memo.go b/store/db/mysql/memo.go index 1f5f1b61cf157..d450a41d4a9ef 100644 --- a/store/db/mysql/memo.go +++ b/store/db/mysql/memo.go @@ -14,8 +14,8 @@ import ( ) func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) { - fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`payload`"} - placeholder := []string{"?", "?", "?", "?", "?"} + fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`payload`", "`group_id`"} + placeholder := []string{"?", "?", "?", "?", "?", "?"} payload := "{}" if create.Payload != nil { payloadBytes, err := protojson.Marshal(create.Payload) @@ -24,7 +24,7 @@ func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, e } payload = string(payloadBytes) } - args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload} + args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload, create.GroupID} // Add custom timestamps if provided if create.CreatedTs != 0 { @@ -109,6 +109,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.GroupID; v != nil { + where, args = append(where, "`memo`.`group_id` = ?"), append(args, *v) + } if find.ExcludeComments { having = append(having, "`parent_uid` IS NULL") } @@ -136,6 +139,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`.`group_id` AS `group_id`", "`memo`.`pinned` AS `pinned`", "`memo`.`payload` AS `payload`", "CASE WHEN `parent_memo`.`uid` IS NOT NULL THEN `parent_memo`.`uid` ELSE NULL END AS `parent_uid`", @@ -176,6 +180,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo &memo.UpdatedTs, &memo.RowStatus, &memo.Visibility, + &memo.GroupID, &memo.Pinned, &payloadBytes, &memo.ParentUID, @@ -234,6 +239,11 @@ 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 update.ClearGroupID { + set = append(set, "`group_id` = NULL") + } else if v := update.GroupID; v != nil { + set, args = append(set, "`group_id` = ?"), append(args, *v) + } if v := update.Pinned; v != nil { set, args = append(set, "`pinned` = ?"), append(args, *v) } diff --git a/store/db/postgres/group.go b/store/db/postgres/group.go new file mode 100644 index 0000000000000..2a1e0698672f3 --- /dev/null +++ b/store/db/postgres/group.go @@ -0,0 +1,190 @@ +package postgres + +import ( + "context" + "strings" + + "github.com/pkg/errors" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateGroup(ctx context.Context, create *store.Group) (*store.Group, error) { + tx, err := d.db.BeginTx(ctx, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to begin transaction") + } + defer func() { + _ = tx.Rollback() + }() + + fields := []string{"name", "description", "creator_id", "visibility"} + args := []any{create.Name, create.Description, create.CreatorID, create.Visibility} + stmt := "INSERT INTO groups (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts" + if err := tx.QueryRowContext(ctx, stmt, args...).Scan( + &create.ID, + &create.CreatedTs, + ); err != nil { + return nil, errors.Wrap(err, "failed to insert group") + } + + memberFields := []string{"group_id", "user_id", "role"} + memberArgs := []any{create.ID, create.CreatorID, "OWNER"} + memberStmt := "INSERT INTO group_members (" + strings.Join(memberFields, ", ") + ") VALUES (" + placeholders(len(memberArgs)) + ")" + if _, err := tx.ExecContext(ctx, memberStmt, memberArgs...); err != nil { + return nil, errors.Wrap(err, "failed to insert owner member") + } + + if err := tx.Commit(); err != nil { + return nil, errors.Wrap(err, "failed to commit transaction") + } + + return create, nil +} + +func (d *DB) ListGroups(ctx context.Context, find *store.FindGroup) ([]*store.Group, 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.Name != nil { + where, args = append(where, "name = "+placeholder(len(args)+1)), append(args, *find.Name) + } + if find.CreatorID != nil { + where, args = append(where, "creator_id = "+placeholder(len(args)+1)), append(args, *find.CreatorID) + } + + query := "SELECT id, name, description, creator_id, visibility, created_ts FROM groups WHERE " + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to query groups") + } + defer rows.Close() + + var list []*store.Group + for rows.Next() { + var group store.Group + if err := rows.Scan( + &group.ID, + &group.Name, + &group.Description, + &group.CreatorID, + &group.Visibility, + &group.CreatedTs, + ); err != nil { + return nil, errors.Wrap(err, "failed to scan group") + } + list = append(list, &group) + } + + return list, nil +} + +func (d *DB) UpdateGroup(ctx context.Context, update *store.UpdateGroup) (*store.Group, error) { + set, args := []string{}, []any{} + if update.Name != nil { + set, args = append(set, "name = "+placeholder(len(args)+1)), append(args, *update.Name) + } + if update.Description != nil { + set, args = append(set, "description = "+placeholder(len(args)+1)), append(args, *update.Description) + } + if update.Visibility != nil { + set, args = append(set, "visibility = "+placeholder(len(args)+1)), append(args, *update.Visibility) + } + + if len(set) == 0 { + return d.GetGroup(ctx, &store.FindGroup{ID: &update.ID}) + } + + stmt := "UPDATE groups SET " + strings.Join(set, ", ") + " WHERE id = " + placeholder(len(args)+1) + args = append(args, update.ID) + if _, err := d.db.ExecContext(ctx, stmt, args...); err != nil { + return nil, errors.Wrap(err, "failed to execute update group query") + } + + return d.GetGroup(ctx, &store.FindGroup{ID: &update.ID}) +} + +func (d *DB) DeleteGroup(ctx context.Context, id int32) error { + if _, err := d.db.ExecContext(ctx, "UPDATE memo SET group_id = NULL WHERE group_id = "+placeholder(1), id); err != nil { + return errors.Wrap(err, "failed to unset group_id in memo") + } + if _, err := d.db.ExecContext(ctx, "DELETE FROM group_members WHERE group_id = "+placeholder(1), id); err != nil { + return errors.Wrap(err, "failed to delete group members") + } + if _, err := d.db.ExecContext(ctx, "DELETE FROM groups WHERE id = "+placeholder(1), id); err != nil { + return errors.Wrap(err, "failed to delete group") + } + return nil +} + +func (d *DB) GetGroup(ctx context.Context, find *store.FindGroup) (*store.Group, error) { + list, err := d.ListGroups(ctx, find) + if err != nil { + return nil, errors.Wrap(err, "failed to list groups in GetGroup") + } + if len(list) == 0 { + return nil, nil + } + return list[0], nil +} + +func (d *DB) CreateGroupMember(ctx context.Context, create *store.GroupMember) (*store.GroupMember, error) { + fields := []string{"group_id", "user_id", "role"} + args := []any{create.GroupID, create.UserID, create.Role} + stmt := "INSERT INTO group_members (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ")" + if _, err := d.db.ExecContext(ctx, stmt, args...); err != nil { + return nil, errors.Wrap(err, "failed to insert group member") + } + return create, nil +} + +func (d *DB) ListGroupMembers(ctx context.Context, find *store.FindGroupMember) ([]*store.GroupMember, error) { + where, args := []string{"1 = 1"}, []any{} + if find.GroupID != nil { + where, args = append(where, "group_id = "+placeholder(len(args)+1)), append(args, *find.GroupID) + } + if find.UserID != nil { + where, args = append(where, "user_id = "+placeholder(len(args)+1)), append(args, *find.UserID) + } + + query := "SELECT group_id, user_id, role FROM group_members WHERE " + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to query group members") + } + defer rows.Close() + + var list []*store.GroupMember + for rows.Next() { + var member store.GroupMember + if err := rows.Scan( + &member.GroupID, + &member.UserID, + &member.Role, + ); err != nil { + return nil, errors.Wrap(err, "failed to scan group member") + } + list = append(list, &member) + } + + return list, nil +} + +func (d *DB) UpdateGroupMember(ctx context.Context, update *store.GroupMember) (*store.GroupMember, error) { + query := "UPDATE group_members SET role = " + placeholder(1) + " WHERE group_id = " + placeholder(2) + " AND user_id = " + placeholder(3) + if _, err := d.db.ExecContext(ctx, query, update.Role, update.GroupID, update.UserID); err != nil { + return nil, errors.Wrap(err, "failed to update group member") + } + return update, nil +} + +func (d *DB) DeleteGroupMember(ctx context.Context, delete *store.GroupMember) error { + if delete.GroupID == 0 || delete.UserID == 0 { + return errors.New("missing group_id or user_id") + } + query := "DELETE FROM group_members WHERE group_id = " + placeholder(1) + " AND user_id = " + placeholder(2) + if _, err := d.db.ExecContext(ctx, query, delete.GroupID, delete.UserID); err != nil { + return errors.Wrap(err, "failed to delete group member") + } + return nil +} diff --git a/store/db/postgres/memo.go b/store/db/postgres/memo.go index 973b637c51a6d..0ff5a2f3c7e2b 100644 --- a/store/db/postgres/memo.go +++ b/store/db/postgres/memo.go @@ -14,7 +14,7 @@ import ( ) func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) { - fields := []string{"uid", "creator_id", "content", "visibility", "payload"} + fields := []string{"uid", "creator_id", "content", "visibility", "payload", "group_id"} payload := "{}" if create.Payload != nil { payloadBytes, err := protojson.Marshal(create.Payload) @@ -23,7 +23,7 @@ func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, e } payload = string(payloadBytes) } - args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload} + args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload, create.GroupID} // Add custom timestamps if provided if create.CreatedTs != 0 { @@ -94,6 +94,9 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo } where = append(where, fmt.Sprintf("memo.visibility in (%s)", strings.Join(holders, ", "))) } + if v := find.GroupID; v != nil { + where, args = append(where, "memo.group_id = "+placeholder(len(args)+1)), append(args, *v) + } if find.ExcludeComments { where = append(where, "memo_relation.related_memo_id IS NULL") } @@ -121,6 +124,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo `memo.updated_ts AS updated_ts`, `memo.row_status AS row_status`, `memo.visibility AS visibility`, + `memo.group_id AS group_id`, `memo.pinned AS pinned`, `memo.payload AS payload`, `CASE WHEN parent_memo.uid IS NOT NULL THEN parent_memo.uid ELSE NULL END AS parent_uid`, @@ -161,6 +165,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo &memo.UpdatedTs, &memo.RowStatus, &memo.Visibility, + &memo.GroupID, &memo.Pinned, &payloadBytes, &memo.ParentUID, @@ -219,6 +224,11 @@ func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error { if v := update.Visibility; v != nil { set, args = append(set, "visibility = "+placeholder(len(args)+1)), append(args, *v) } + if update.ClearGroupID { + set = append(set, "group_id = NULL") + } else if v := update.GroupID; v != nil { + set, args = append(set, "group_id = "+placeholder(len(args)+1)), append(args, *v) + } if v := update.Pinned; v != nil { set, args = append(set, "pinned = "+placeholder(len(args)+1)), append(args, *v) } diff --git a/store/db/sqlite/group.go b/store/db/sqlite/group.go new file mode 100644 index 0000000000000..defe5d09730ab --- /dev/null +++ b/store/db/sqlite/group.go @@ -0,0 +1,215 @@ +package sqlite + +import ( + "context" + "strings" + + "github.com/pkg/errors" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateGroup(ctx context.Context, create *store.Group) (*store.Group, error) { + tx, err := d.db.BeginTx(ctx, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to begin transaction") + } + defer func() { + _ = tx.Rollback() + }() + + stmt := ` + INSERT INTO groups ( + name, + description, + creator_id, + visibility + ) + VALUES (?, ?, ?, ?) + RETURNING id, created_ts + ` + err = tx.QueryRowContext( + ctx, + stmt, + create.Name, + create.Description, + create.CreatorID, + create.Visibility, + ).Scan( + &create.ID, + &create.CreatedTs, + ) + if err != nil { + return nil, errors.Wrap(err, "failed to insert group") + } + + memberStmt := ` + INSERT INTO group_members ( + group_id, + user_id, + role + ) + VALUES (?, ?, 'OWNER') + ` + if _, err := tx.ExecContext(ctx, memberStmt, create.ID, create.CreatorID); err != nil { + return nil, errors.Wrap(err, "failed to insert owner member") + } + + if err := tx.Commit(); err != nil { + return nil, errors.Wrap(err, "failed to commit transaction") + } + + return create, nil +} + +func (d *DB) ListGroups(ctx context.Context, find *store.FindGroup) ([]*store.Group, error) { + where, args := []string{"1 = 1"}, []any{} + if find.ID != nil { + where, args = append(where, "id = ?"), append(args, *find.ID) + } + if find.Name != nil { + where, args = append(where, "name = ?"), append(args, *find.Name) + } + if find.CreatorID != nil { + where, args = append(where, "creator_id = ?"), append(args, *find.CreatorID) + } + + query := "SELECT id, name, description, creator_id, visibility, created_ts FROM groups WHERE " + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to query groups") + } + defer rows.Close() + + var list []*store.Group + for rows.Next() { + var group store.Group + if err := rows.Scan( + &group.ID, + &group.Name, + &group.Description, + &group.CreatorID, + &group.Visibility, + &group.CreatedTs, + ); err != nil { + return nil, errors.Wrap(err, "failed to scan group") + } + list = append(list, &group) + } + + return list, nil +} + +func (d *DB) UpdateGroup(ctx context.Context, update *store.UpdateGroup) (*store.Group, error) { + set, args := []string{}, []any{} + if update.Name != nil { + set, args = append(set, "name = ?"), append(args, *update.Name) + } + if update.Description != nil { + set, args = append(set, "description = ?"), append(args, *update.Description) + } + if update.Visibility != nil { + set, args = append(set, "visibility = ?"), append(args, *update.Visibility) + } + + if len(set) == 0 { + return d.GetGroup(ctx, &store.FindGroup{ID: &update.ID}) + } + + args = append(args, update.ID) + query := "UPDATE groups SET " + strings.Join(set, ", ") + " WHERE id = ?" + if _, err := d.db.ExecContext(ctx, query, args...); err != nil { + return nil, errors.Wrap(err, "failed to execute update group query") + } + + return d.GetGroup(ctx, &store.FindGroup{ID: &update.ID}) +} + +func (d *DB) DeleteGroup(ctx context.Context, id int32) error { + if _, err := d.db.ExecContext(ctx, "UPDATE memo SET group_id = NULL WHERE group_id = ?", id); err != nil { + return errors.Wrap(err, "failed to unset group_id in memo") + } + if _, err := d.db.ExecContext(ctx, "DELETE FROM group_members WHERE group_id = ?", id); err != nil { + return errors.Wrap(err, "failed to delete group members") + } + if _, err := d.db.ExecContext(ctx, "DELETE FROM groups WHERE id = ?", id); err != nil { + return errors.Wrap(err, "failed to delete group") + } + return nil +} + +func (d *DB) GetGroup(ctx context.Context, find *store.FindGroup) (*store.Group, error) { + list, err := d.ListGroups(ctx, find) + if err != nil { + return nil, errors.Wrap(err, "failed to list groups in GetGroup") + } + if len(list) == 0 { + return nil, nil + } + return list[0], nil +} + +func (d *DB) CreateGroupMember(ctx context.Context, create *store.GroupMember) (*store.GroupMember, error) { + stmt := ` + INSERT INTO group_members ( + group_id, + user_id, + role + ) + VALUES (?, ?, ?) + ` + if _, err := d.db.ExecContext(ctx, stmt, create.GroupID, create.UserID, create.Role); err != nil { + return nil, errors.Wrap(err, "failed to insert group member") + } + return create, nil +} + +func (d *DB) ListGroupMembers(ctx context.Context, find *store.FindGroupMember) ([]*store.GroupMember, error) { + where, args := []string{"1 = 1"}, []any{} + if find.GroupID != nil { + where, args = append(where, "group_id = ?"), append(args, *find.GroupID) + } + if find.UserID != nil { + where, args = append(where, "user_id = ?"), append(args, *find.UserID) + } + + query := "SELECT group_id, user_id, role FROM group_members WHERE " + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to query group members") + } + defer rows.Close() + + var list []*store.GroupMember + for rows.Next() { + var member store.GroupMember + if err := rows.Scan( + &member.GroupID, + &member.UserID, + &member.Role, + ); err != nil { + return nil, errors.Wrap(err, "failed to scan group member") + } + list = append(list, &member) + } + + return list, nil +} + +func (d *DB) UpdateGroupMember(ctx context.Context, update *store.GroupMember) (*store.GroupMember, error) { + query := "UPDATE group_members SET role = ? WHERE group_id = ? AND user_id = ?" + if _, err := d.db.ExecContext(ctx, query, update.Role, update.GroupID, update.UserID); err != nil { + return nil, errors.Wrap(err, "failed to update group member") + } + return update, nil +} + +func (d *DB) DeleteGroupMember(ctx context.Context, delete *store.GroupMember) error { + if delete.GroupID == 0 || delete.UserID == 0 { + return errors.New("missing group_id or user_id") + } + query := "DELETE FROM group_members WHERE group_id = ? AND user_id = ?" + if _, err := d.db.ExecContext(ctx, query, delete.GroupID, delete.UserID); err != nil { + return errors.Wrap(err, "failed to delete group member") + } + return nil +} diff --git a/store/db/sqlite/memo.go b/store/db/sqlite/memo.go index 1d1ac6861387a..5a48ce1ebeb4f 100644 --- a/store/db/sqlite/memo.go +++ b/store/db/sqlite/memo.go @@ -14,8 +14,8 @@ import ( ) func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) { - fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`payload`"} - placeholder := []string{"?", "?", "?", "?", "?"} + fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`payload`", "`group_id`"} + placeholder := []string{"?", "?", "?", "?", "?", "?"} payload := "{}" if create.Payload != nil { payloadBytes, err := protojson.Marshal(create.Payload) @@ -24,7 +24,7 @@ func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, e } payload = string(payloadBytes) } - args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload} + args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload, create.GroupID} // Add custom timestamps if provided if create.CreatedTs != 0 { @@ -101,6 +101,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.GroupID; v != nil { + where, args = append(where, "`memo`.`group_id` = ?"), append(args, *v) + } if find.ExcludeComments { where = append(where, "`parent_uid` IS NULL") } @@ -128,6 +131,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo "`memo`.`updated_ts` AS `updated_ts`", "`memo`.`row_status` AS `row_status`", "`memo`.`visibility` AS `visibility`", + "`memo`.`group_id` AS `group_id`", "`memo`.`pinned` AS `pinned`", "`memo`.`payload` AS `payload`", "CASE WHEN `parent_memo`.`uid` IS NOT NULL THEN `parent_memo`.`uid` ELSE NULL END AS `parent_uid`", @@ -167,6 +171,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo &memo.UpdatedTs, &memo.RowStatus, &memo.Visibility, + &memo.GroupID, &memo.Pinned, &payloadBytes, &memo.ParentUID, @@ -212,6 +217,11 @@ 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 update.ClearGroupID { + set = append(set, "`group_id` = NULL") + } else if v := update.GroupID; v != nil { + set, args = append(set, "`group_id` = ?"), append(args, *v) + } if v := update.Pinned; v != nil { set, args = append(set, "`pinned` = ?"), append(args, *v) } diff --git a/store/driver.go b/store/driver.go index 7cd6b699cb0f0..d587c4c40baee 100644 --- a/store/driver.go +++ b/store/driver.go @@ -81,4 +81,16 @@ type Driver interface { CreateUserIdentity(ctx context.Context, create *UserIdentity) (*UserIdentity, error) ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error) DeleteUserIdentities(ctx context.Context, delete *DeleteUserIdentity) error + + // Group model related methods. + CreateGroup(ctx context.Context, create *Group) (*Group, error) + ListGroups(ctx context.Context, find *FindGroup) ([]*Group, error) + UpdateGroup(ctx context.Context, update *UpdateGroup) (*Group, error) + DeleteGroup(ctx context.Context, id int32) error + + // GroupMember model related methods. + CreateGroupMember(ctx context.Context, create *GroupMember) (*GroupMember, error) + ListGroupMembers(ctx context.Context, find *FindGroupMember) ([]*GroupMember, error) + UpdateGroupMember(ctx context.Context, update *GroupMember) (*GroupMember, error) + DeleteGroupMember(ctx context.Context, delete *GroupMember) error } diff --git a/store/group.go b/store/group.go new file mode 100644 index 0000000000000..b283ec2ba0642 --- /dev/null +++ b/store/group.go @@ -0,0 +1,98 @@ +package store + +import ( + "context" +) + +// Group is the schema for a group. +type Group struct { + ID int32 + Name string + Description string + CreatorID int32 + Visibility Visibility + CreatedTs int64 +} + +// GroupMember is the schema for a group member. +type GroupMember struct { + GroupID int32 + UserID int32 + Role string // "MEMBER", "ADMIN", "OWNER" +} + +// FindGroup is the query filter for listing groups. +type FindGroup struct { + ID *int32 + Name *string + CreatorID *int32 +} + +// FindGroupMember is the query filter for listing group members. +type FindGroupMember struct { + GroupID *int32 + UserID *int32 +} + +// UpdateGroup is the schema for updating a group. +type UpdateGroup struct { + ID int32 + Name *string + Description *string + Visibility *Visibility +} + +func (s *Store) CreateGroup(ctx context.Context, create *Group) (*Group, error) { + return s.driver.CreateGroup(ctx, create) +} + +func (s *Store) ListGroups(ctx context.Context, find *FindGroup) ([]*Group, error) { + return s.driver.ListGroups(ctx, find) +} + +func (s *Store) GetGroup(ctx context.Context, find *FindGroup) (*Group, error) { + list, err := s.driver.ListGroups(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + return list[0], nil +} + +func (s *Store) UpdateGroup(ctx context.Context, update *UpdateGroup) (*Group, error) { + return s.driver.UpdateGroup(ctx, update) +} + +func (s *Store) DeleteGroup(ctx context.Context, id int32) error { + return s.driver.DeleteGroup(ctx, id) +} + +func (s *Store) CreateGroupMember(ctx context.Context, create *GroupMember) (*GroupMember, error) { + return s.driver.CreateGroupMember(ctx, create) +} + +func (s *Store) ListGroupMembers(ctx context.Context, find *FindGroupMember) ([]*GroupMember, error) { + return s.driver.ListGroupMembers(ctx, find) +} + +func (s *Store) UpdateGroupMember(ctx context.Context, update *GroupMember) (*GroupMember, error) { + return s.driver.UpdateGroupMember(ctx, update) +} + +func (s *Store) DeleteGroupMember(ctx context.Context, delete *GroupMember) error { + return s.driver.DeleteGroupMember(ctx, delete) +} + +// CheckUserInGroup returns true if the user is a member of the group. +func (s *Store) CheckUserInGroup(ctx context.Context, userID int32, groupID int32) (bool, error) { + members, err := s.driver.ListGroupMembers(ctx, &FindGroupMember{ + GroupID: &groupID, + UserID: &userID, + }) + if err != nil { + return false, err + } + return len(members) > 0, nil +} diff --git a/store/memo.go b/store/memo.go index ce6cde28d9e8f..1453902a6d3cf 100644 --- a/store/memo.go +++ b/store/memo.go @@ -19,6 +19,8 @@ const ( Protected Visibility = "PROTECTED" // Private is the PRIVATE visibility. Private Visibility = "PRIVATE" + // GroupVisibility is the GROUP visibility. + GroupVisibility Visibility = "GROUP" ) func (v Visibility) String() string { @@ -27,6 +29,8 @@ func (v Visibility) String() string { return "PUBLIC" case Protected: return "PROTECTED" + case GroupVisibility: + return "GROUP" default: return "PRIVATE" } @@ -47,6 +51,7 @@ type Memo struct { // Domain specific fields Content string Visibility Visibility + GroupID *int32 Pinned bool Payload *storepb.MemoPayload @@ -70,6 +75,7 @@ type FindMemo struct { ExcludeContent bool ExcludeComments bool Filters []string + GroupID *int32 // Pagination Limit *int @@ -91,15 +97,17 @@ type FindMemoPayload struct { } type UpdateMemo struct { - ID int32 - UID *string - CreatedTs *int64 - UpdatedTs *int64 - RowStatus *RowStatus - Content *string - Visibility *Visibility - Pinned *bool - Payload *storepb.MemoPayload + ID int32 + UID *string + CreatedTs *int64 + UpdatedTs *int64 + RowStatus *RowStatus + Content *string + Visibility *Visibility + GroupID *int32 + ClearGroupID bool + Pinned *bool + Payload *storepb.MemoPayload } type DeleteMemo struct { diff --git a/store/migration/mysql/0.29/00__group.sql b/store/migration/mysql/0.29/00__group.sql new file mode 100644 index 0000000000000..18c386f653890 --- /dev/null +++ b/store/migration/mysql/0.29/00__group.sql @@ -0,0 +1,24 @@ +-- Create groups table +CREATE TABLE `groups` ( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(256) NOT NULL, + description TEXT NOT NULL, + creator_id INT NOT NULL, + visibility VARCHAR(32) NOT NULL DEFAULT 'PRIVATE', + created_ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (creator_id) REFERENCES user(id) ON DELETE CASCADE +); + +-- Add group_id column to memo table +ALTER TABLE memo ADD COLUMN group_id INT DEFAULT NULL; +ALTER TABLE memo ADD CONSTRAINT fk_memo_group_id FOREIGN KEY (group_id) REFERENCES `groups`(id) ON DELETE SET NULL; + +-- Create group_members table +CREATE TABLE group_members ( + group_id INT NOT NULL, + user_id INT NOT NULL, + role VARCHAR(32) NOT NULL DEFAULT 'MEMBER', + PRIMARY KEY (group_id, user_id), + FOREIGN KEY (group_id) REFERENCES `groups`(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE +); diff --git a/store/migration/mysql/LATEST.sql b/store/migration/mysql/LATEST.sql index cb15be067e6a1..87ee7a0043bac 100644 --- a/store/migration/mysql/LATEST.sql +++ b/store/migration/mysql/LATEST.sql @@ -38,8 +38,11 @@ CREATE TABLE `memo` ( `row_status` VARCHAR(256) NOT NULL DEFAULT 'NORMAL', `content` TEXT NOT NULL, `visibility` VARCHAR(256) NOT NULL DEFAULT 'PRIVATE', + `group_id` INT DEFAULT NULL, `pinned` BOOLEAN NOT NULL DEFAULT FALSE, - `payload` JSON NOT NULL + `payload` JSON NOT NULL, + FOREIGN KEY (`group_id`) REFERENCES `groups`(`id`) ON DELETE SET NULL, + FOREIGN KEY (`creator_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ); -- memo_relation @@ -123,3 +126,24 @@ CREATE TABLE `user_identity` ( ); CREATE INDEX `idx_user_identity_user_id` ON `user_identity`(`user_id`); + +-- groups +CREATE TABLE `groups` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(256) NOT NULL, + `description` TEXT NOT NULL, + `creator_id` INT NOT NULL, + `visibility` VARCHAR(256) NOT NULL DEFAULT 'PRIVATE', + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`creator_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +); + +-- group_members +CREATE TABLE `group_members` ( + `group_id` INT NOT NULL, + `user_id` INT NOT NULL, + `role` VARCHAR(256) NOT NULL DEFAULT 'MEMBER', + PRIMARY KEY (`group_id`, `user_id`), + FOREIGN KEY (`group_id`) REFERENCES `groups`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +); diff --git a/store/migration/postgres/0.29/00__group.sql b/store/migration/postgres/0.29/00__group.sql new file mode 100644 index 0000000000000..f7fa9a9bbfa29 --- /dev/null +++ b/store/migration/postgres/0.29/00__group.sql @@ -0,0 +1,23 @@ +-- Create groups table +CREATE TABLE groups ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + creator_id INTEGER NOT NULL, + visibility TEXT NOT NULL DEFAULT 'PRIVATE', + created_ts BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())), + FOREIGN KEY (creator_id) REFERENCES "user"(id) ON DELETE CASCADE +); + +-- Add group_id column to memo table +ALTER TABLE memo ADD COLUMN group_id INTEGER DEFAULT NULL REFERENCES groups(id) ON DELETE SET NULL; + +-- Create group_members table +CREATE TABLE group_members ( + group_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + role TEXT NOT NULL DEFAULT 'MEMBER', + PRIMARY KEY (group_id, user_id), + FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE +); diff --git a/store/migration/postgres/LATEST.sql b/store/migration/postgres/LATEST.sql index 328c32bd5d14b..55850c5fa90b4 100644 --- a/store/migration/postgres/LATEST.sql +++ b/store/migration/postgres/LATEST.sql @@ -38,8 +38,11 @@ CREATE TABLE memo ( row_status TEXT NOT NULL DEFAULT 'NORMAL', content TEXT NOT NULL, visibility TEXT NOT NULL DEFAULT 'PRIVATE', + group_id INTEGER DEFAULT NULL, pinned BOOLEAN NOT NULL DEFAULT FALSE, - payload JSONB NOT NULL DEFAULT '{}' + payload JSONB NOT NULL DEFAULT '{}', + FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE SET NULL, + FOREIGN KEY (creator_id) REFERENCES "user"(id) ON DELETE CASCADE ); -- memo_relation @@ -123,3 +126,24 @@ CREATE TABLE user_identity ( ); CREATE INDEX idx_user_identity_user_id ON user_identity(user_id); + +-- groups +CREATE TABLE groups ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + creator_id INTEGER NOT NULL, + visibility TEXT NOT NULL DEFAULT 'PRIVATE', + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + FOREIGN KEY (creator_id) REFERENCES "user"(id) ON DELETE CASCADE +); + +-- group_members +CREATE TABLE group_members ( + group_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + role TEXT NOT NULL DEFAULT 'MEMBER', + PRIMARY KEY (group_id, user_id), + FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE +); diff --git a/store/migration/sqlite/0.29/00__group.sql b/store/migration/sqlite/0.29/00__group.sql new file mode 100644 index 0000000000000..13748cfbcc607 --- /dev/null +++ b/store/migration/sqlite/0.29/00__group.sql @@ -0,0 +1,23 @@ +-- Create groups table +CREATE TABLE groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + creator_id INTEGER NOT NULL, + visibility TEXT NOT NULL DEFAULT 'PRIVATE', + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + FOREIGN KEY (creator_id) REFERENCES user(id) ON DELETE CASCADE +); + +-- Add group_id column to memo table +ALTER TABLE memo ADD COLUMN group_id INTEGER DEFAULT NULL REFERENCES groups(id) ON DELETE SET NULL; + +-- Create group_members table +CREATE TABLE group_members ( + group_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + role TEXT NOT NULL DEFAULT 'MEMBER', + PRIMARY KEY (group_id, user_id), + FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE +); diff --git a/store/migration/sqlite/0.29/01__fix_memo_visibility.sql b/store/migration/sqlite/0.29/01__fix_memo_visibility.sql new file mode 100644 index 0000000000000..e3c11062a0630 --- /dev/null +++ b/store/migration/sqlite/0.29/01__fix_memo_visibility.sql @@ -0,0 +1,27 @@ +-- Fix memo visibility constraint to allow 'GROUP' +-- Note: Transaction is handled by Memos migrator + +-- Create a temporary table with the updated constraint +CREATE TABLE memo_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT NOT NULL UNIQUE, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + 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', 'GROUP')) DEFAULT 'PRIVATE', + group_id INTEGER DEFAULT NULL, + pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, + payload TEXT NOT NULL DEFAULT '{}' +); + +-- Copy data from the old table to the new one +INSERT INTO memo_new (id, uid, creator_id, created_ts, updated_ts, row_status, content, visibility, group_id, pinned, payload) +SELECT id, uid, creator_id, created_ts, updated_ts, row_status, content, visibility, group_id, pinned, payload FROM memo; + +-- Drop the old table +DROP TABLE memo; + +-- Rename the new table to the original name +ALTER TABLE memo_new RENAME TO memo; diff --git a/store/migration/sqlite/LATEST.sql b/store/migration/sqlite/LATEST.sql index f43fbdfc0aad8..79d369f0b61d3 100644 --- a/store/migration/sqlite/LATEST.sql +++ b/store/migration/sqlite/LATEST.sql @@ -38,9 +38,12 @@ 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', 'GROUP')) DEFAULT 'PRIVATE', + group_id INTEGER DEFAULT NULL, pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, - payload TEXT NOT NULL DEFAULT '{}' + payload TEXT NOT NULL DEFAULT '{}', + FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE SET NULL, + FOREIGN KEY (creator_id) REFERENCES user(id) ON DELETE CASCADE ); -- memo_relation @@ -124,3 +127,24 @@ CREATE TABLE user_identity ( ); CREATE INDEX idx_user_identity_user_id ON user_identity(user_id); + +-- groups +CREATE TABLE groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + creator_id INTEGER NOT NULL, + visibility TEXT NOT NULL DEFAULT 'PRIVATE', + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + FOREIGN KEY (creator_id) REFERENCES user(id) ON DELETE CASCADE +); + +-- group_members +CREATE TABLE group_members ( + group_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + role TEXT NOT NULL DEFAULT 'MEMBER', + PRIMARY KEY (group_id, user_id), + FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE +); diff --git a/store/test/migrator_test.go b/store/test/migrator_test.go index dcbe64eb03800..4a279ef60f92d 100644 --- a/store/test/migrator_test.go +++ b/store/test/migrator_test.go @@ -145,7 +145,17 @@ func TestMigrationCopiesInstanceTagsToUserSettings(t *testing.T) { UNIQUE(user_id, key) ); CREATE TABLE memo ( - id INTEGER PRIMARY KEY AUTOINCREMENT + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT NOT NULL UNIQUE, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT 0, + updated_ts BIGINT NOT NULL DEFAULT 0, + row_status TEXT NOT NULL DEFAULT 'NORMAL', + content TEXT NOT NULL DEFAULT '', + visibility TEXT NOT NULL DEFAULT 'PRIVATE', + group_id INTEGER DEFAULT NULL, + pinned INTEGER NOT NULL DEFAULT 0, + payload TEXT NOT NULL DEFAULT '{}' ); `) require.NoError(t, err) diff --git a/web/src/components/CreateGroupDialog.tsx b/web/src/components/CreateGroupDialog.tsx new file mode 100644 index 0000000000000..3523400657d67 --- /dev/null +++ b/web/src/components/CreateGroupDialog.tsx @@ -0,0 +1,128 @@ +import { create } from "@bufbuild/protobuf"; +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { useCreateGroup, useUpdateGroup } from "@/hooks/useGroupQueries"; +import useLoading from "@/hooks/useLoading"; +import { handleError } from "@/lib/error"; +import { Group, GroupSchema } from "@/types/proto/api/v1/group_service_pb"; +import { useTranslate } from "@/utils/i18n"; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + group?: Group; + onConfirm?: () => void; +} + +function CreateGroupDialog({ open, onOpenChange, group: initialGroup, onConfirm }: Props) { + const t = useTranslate(); + const createGroup = useCreateGroup(); + const updateGroup = useUpdateGroup(); + const requestState = useLoading(false); + + const [displayName, setDisplayName] = useState(""); + const [description, setDescription] = useState(""); + + const isCreating = !initialGroup; + + useEffect(() => { + if (initialGroup) { + setDisplayName(initialGroup.displayName); + setDescription(initialGroup.description); + } else { + setDisplayName(""); + setDescription(""); + } + }, [initialGroup, open]); + + const handleConfirm = async () => { + if (!displayName.trim()) { + toast.error(t("group.name-cannot-be-empty")); + return; + } + + try { + requestState.setLoading(); + if (isCreating) { + await createGroup.mutateAsync({ displayName, description }); + toast.success(t("group.create-successfully")); + } else { + const updateMask: string[] = []; + const groupToUpdate = create(GroupSchema, { + name: initialGroup.name, + displayName, + description, + }); + + if (displayName !== initialGroup.displayName) { + updateMask.push("display_name"); + } + if (description !== initialGroup.description) { + updateMask.push("description"); + } + + if (updateMask.length > 0) { + await updateGroup.mutateAsync({ + group: groupToUpdate, + updateMask, + }); + } + toast.success(t("group.update-successfully")); + } + requestState.setFinish(); + onConfirm?.(); + onOpenChange(false); + } catch (error: unknown) { + handleError(error, toast.error, { + context: isCreating ? t("group.create-group") : t("group.update-group"), + onError: () => requestState.setError(), + }); + } + }; + + return ( + + + + {`${isCreating ? t("common.create") : t("common.edit")} ${t("common.groups")}`} + +
+
+ + setDisplayName(e.target.value)} + /> +
+
+ +