Commit 506b477d authored by johnnyjoy's avatar johnnyjoy

fix: get user by username

parent fa2fa8a5
...@@ -36,6 +36,11 @@ var MemoFilterCELAttributes = []cel.EnvOption{ ...@@ -36,6 +36,11 @@ var MemoFilterCELAttributes = []cel.EnvOption{
), ),
} }
// UserFilterCELAttributes are the CEL attributes for user.
var UserFilterCELAttributes = []cel.EnvOption{
cel.Variable("username", cel.StringType),
}
// Parse parses the filter string and returns the parsed expression. // Parse parses the filter string and returns the parsed expression.
// The filter string should be a CEL expression. // The filter string should be a CEL expression.
func Parse(filter string, opts ...cel.EnvOption) (expr *exprv1.ParsedExpr, err error) { func Parse(filter string, opts ...cel.EnvOption) (expr *exprv1.ParsedExpr, err error) {
......
...@@ -50,12 +50,6 @@ service UserService { ...@@ -50,12 +50,6 @@ service UserService {
option (google.api.method_signature) = "name"; option (google.api.method_signature) = "name";
} }
// SearchUsers searches for users based on query.
rpc SearchUsers(SearchUsersRequest) returns (SearchUsersResponse) {
option (google.api.http) = {get: "/api/v1/users:search"};
option (google.api.method_signature) = "query";
}
// GetUserAvatar gets the avatar of a user. // GetUserAvatar gets the avatar of a user.
rpc GetUserAvatar(GetUserAvatarRequest) returns (google.api.HttpBody) { rpc GetUserAvatar(GetUserAvatarRequest) returns (google.api.HttpBody) {
option (google.api.http) = {get: "/api/v1/{name=users/*}/avatar"}; option (google.api.http) = {get: "/api/v1/{name=users/*}/avatar"};
...@@ -231,12 +225,8 @@ message ListUsersRequest { ...@@ -231,12 +225,8 @@ message ListUsersRequest {
// Supported fields: username, email, role, state, create_time, update_time // Supported fields: username, email, role, state, create_time, update_time
string filter = 3 [(google.api.field_behavior) = OPTIONAL]; string filter = 3 [(google.api.field_behavior) = OPTIONAL];
// Optional. The order to sort results by.
// Example: "create_time desc" or "username asc"
string order_by = 4 [(google.api.field_behavior) = OPTIONAL];
// Optional. If true, show deleted users in the response. // Optional. If true, show deleted users in the response.
bool show_deleted = 5 [(google.api.field_behavior) = OPTIONAL]; bool show_deleted = 4 [(google.api.field_behavior) = OPTIONAL];
} }
message ListUsersResponse { message ListUsersResponse {
...@@ -307,28 +297,6 @@ message DeleteUserRequest { ...@@ -307,28 +297,6 @@ message DeleteUserRequest {
bool force = 2 [(google.api.field_behavior) = OPTIONAL]; bool force = 2 [(google.api.field_behavior) = OPTIONAL];
} }
message SearchUsersRequest {
// Required. The search query.
string query = 1 [(google.api.field_behavior) = REQUIRED];
// Optional. The maximum number of users to return.
int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];
// Optional. A page token for pagination.
string page_token = 3 [(google.api.field_behavior) = OPTIONAL];
}
message SearchUsersResponse {
// The list of users matching the search query.
repeated User users = 1;
// A token for the next page of results.
string next_page_token = 2;
// The total count of matching users.
int32 total_size = 3;
}
message GetUserAvatarRequest { message GetUserAvatarRequest {
// Required. The resource name of the user. // Required. The resource name of the user.
// Format: users/{user} // Format: users/{user}
......
This diff is collapsed.
...@@ -298,41 +298,6 @@ func local_request_UserService_DeleteUser_0(ctx context.Context, marshaler runti ...@@ -298,41 +298,6 @@ func local_request_UserService_DeleteUser_0(ctx context.Context, marshaler runti
return msg, metadata, err return msg, metadata, err
} }
var filter_UserService_SearchUsers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
func request_UserService_SearchUsers_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq SearchUsersRequest
metadata runtime.ServerMetadata
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_SearchUsers_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.SearchUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_SearchUsers_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq SearchUsersRequest
metadata runtime.ServerMetadata
)
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_SearchUsers_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.SearchUsers(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_GetUserAvatar_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { func request_UserService_GetUserAvatar_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var ( var (
protoReq GetUserAvatarRequest protoReq GetUserAvatarRequest
...@@ -1144,26 +1109,6 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ...@@ -1144,26 +1109,6 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
} }
forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle(http.MethodGet, pattern_UserService_SearchUsers_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.UserService/SearchUsers", runtime.WithHTTPPathPattern("/api/v1/users:search"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_SearchUsers_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_UserService_SearchUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetUserAvatar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle(http.MethodGet, pattern_UserService_GetUserAvatar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
...@@ -1589,23 +1534,6 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux ...@@ -1589,23 +1534,6 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
} }
forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle(http.MethodGet, pattern_UserService_SearchUsers_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.UserService/SearchUsers", runtime.WithHTTPPathPattern("/api/v1/users:search"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_SearchUsers_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_SearchUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetUserAvatar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle(http.MethodGet, pattern_UserService_GetUserAvatar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
...@@ -1870,7 +1798,6 @@ var ( ...@@ -1870,7 +1798,6 @@ var (
pattern_UserService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "")) pattern_UserService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
pattern_UserService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "user.name"}, "")) pattern_UserService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "user.name"}, ""))
pattern_UserService_DeleteUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "")) pattern_UserService_DeleteUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, ""))
pattern_UserService_SearchUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "search"))
pattern_UserService_GetUserAvatar_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "name", "avatar"}, "")) pattern_UserService_GetUserAvatar_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "name", "avatar"}, ""))
pattern_UserService_ListAllUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "stats")) pattern_UserService_ListAllUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "stats"))
pattern_UserService_GetUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "getStats")) pattern_UserService_GetUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "getStats"))
...@@ -1894,7 +1821,6 @@ var ( ...@@ -1894,7 +1821,6 @@ var (
forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteUser_0 = runtime.ForwardResponseMessage forward_UserService_DeleteUser_0 = runtime.ForwardResponseMessage
forward_UserService_SearchUsers_0 = runtime.ForwardResponseMessage
forward_UserService_GetUserAvatar_0 = runtime.ForwardResponseMessage forward_UserService_GetUserAvatar_0 = runtime.ForwardResponseMessage
forward_UserService_ListAllUserStats_0 = runtime.ForwardResponseMessage forward_UserService_ListAllUserStats_0 = runtime.ForwardResponseMessage
forward_UserService_GetUserStats_0 = runtime.ForwardResponseMessage forward_UserService_GetUserStats_0 = runtime.ForwardResponseMessage
......
...@@ -26,7 +26,6 @@ const ( ...@@ -26,7 +26,6 @@ const (
UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser" UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser"
UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser" UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser"
UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser" UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser"
UserService_SearchUsers_FullMethodName = "/memos.api.v1.UserService/SearchUsers"
UserService_GetUserAvatar_FullMethodName = "/memos.api.v1.UserService/GetUserAvatar" UserService_GetUserAvatar_FullMethodName = "/memos.api.v1.UserService/GetUserAvatar"
UserService_ListAllUserStats_FullMethodName = "/memos.api.v1.UserService/ListAllUserStats" UserService_ListAllUserStats_FullMethodName = "/memos.api.v1.UserService/ListAllUserStats"
UserService_GetUserStats_FullMethodName = "/memos.api.v1.UserService/GetUserStats" UserService_GetUserStats_FullMethodName = "/memos.api.v1.UserService/GetUserStats"
...@@ -58,8 +57,6 @@ type UserServiceClient interface { ...@@ -58,8 +57,6 @@ type UserServiceClient interface {
UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error) UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error)
// DeleteUser deletes a user. // DeleteUser deletes a user.
DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// SearchUsers searches for users based on query.
SearchUsers(ctx context.Context, in *SearchUsersRequest, opts ...grpc.CallOption) (*SearchUsersResponse, error)
// GetUserAvatar gets the avatar of a user. // GetUserAvatar gets the avatar of a user.
GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
// ListAllUserStats returns statistics for all users. // ListAllUserStats returns statistics for all users.
...@@ -150,16 +147,6 @@ func (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserReques ...@@ -150,16 +147,6 @@ func (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserReques
return out, nil return out, nil
} }
func (c *userServiceClient) SearchUsers(ctx context.Context, in *SearchUsersRequest, opts ...grpc.CallOption) (*SearchUsersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SearchUsersResponse)
err := c.cc.Invoke(ctx, UserService_SearchUsers_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) { func (c *userServiceClient) GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(httpbody.HttpBody) out := new(httpbody.HttpBody)
...@@ -324,8 +311,6 @@ type UserServiceServer interface { ...@@ -324,8 +311,6 @@ type UserServiceServer interface {
UpdateUser(context.Context, *UpdateUserRequest) (*User, error) UpdateUser(context.Context, *UpdateUserRequest) (*User, error)
// DeleteUser deletes a user. // DeleteUser deletes a user.
DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error)
// SearchUsers searches for users based on query.
SearchUsers(context.Context, *SearchUsersRequest) (*SearchUsersResponse, error)
// GetUserAvatar gets the avatar of a user. // GetUserAvatar gets the avatar of a user.
GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error) GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error)
// ListAllUserStats returns statistics for all users. // ListAllUserStats returns statistics for all users.
...@@ -381,9 +366,6 @@ func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserReq ...@@ -381,9 +366,6 @@ func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserReq
func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) { func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented") return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented")
} }
func (UnimplementedUserServiceServer) SearchUsers(context.Context, *SearchUsersRequest) (*SearchUsersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SearchUsers not implemented")
}
func (UnimplementedUserServiceServer) GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error) { func (UnimplementedUserServiceServer) GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserAvatar not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetUserAvatar not implemented")
} }
...@@ -540,24 +522,6 @@ func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec f ...@@ -540,24 +522,6 @@ func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _UserService_SearchUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SearchUsersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).SearchUsers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_SearchUsers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).SearchUsers(ctx, req.(*SearchUsersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_GetUserAvatar_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _UserService_GetUserAvatar_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserAvatarRequest) in := new(GetUserAvatarRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
...@@ -855,10 +819,6 @@ var UserService_ServiceDesc = grpc.ServiceDesc{ ...@@ -855,10 +819,6 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName: "DeleteUser", MethodName: "DeleteUser",
Handler: _UserService_DeleteUser_Handler, Handler: _UserService_DeleteUser_Handler,
}, },
{
MethodName: "SearchUsers",
Handler: _UserService_SearchUsers_Handler,
},
{ {
MethodName: "GetUserAvatar", MethodName: "GetUserAvatar",
Handler: _UserService_GetUserAvatar_Handler, Handler: _UserService_GetUserAvatar_Handler,
......
This diff is collapsed.
package v1
import (
"testing"
"github.com/usememos/memos/plugin/filter"
)
func TestUserFilterValidation(t *testing.T) {
testCases := []struct {
name string
filter string
expectErr bool
}{
{
name: "valid username filter with equals",
filter: `username == "testuser"`,
expectErr: false,
},
{
name: "valid username filter with contains",
filter: `username.contains("admin")`,
expectErr: false,
},
{
name: "invalid filter - unknown field",
filter: `invalid_field == "test"`,
expectErr: true,
},
{
name: "empty filter",
filter: "",
expectErr: true,
},
{
name: "invalid syntax",
filter: `username ==`,
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Test the filter parsing directly
_, err := filter.Parse(tc.filter, filter.UserFilterCELAttributes...)
if tc.expectErr && err == nil {
t.Errorf("Expected error for filter %q, but got none", tc.filter)
}
if !tc.expectErr && err != nil {
t.Errorf("Expected no error for filter %q, but got: %v", tc.filter, err)
}
})
}
}
func TestUserFilterCELAttributes(t *testing.T) {
// Test that our UserFilterCELAttributes contains the username variable
expectedAttributes := map[string]bool{
"username": true,
}
// This is a basic test to ensure the attributes are defined
// In a real test, you would create a CEL environment and verify the attributes
for attrName := range expectedAttributes {
t.Logf("Expected attribute %s should be available in UserFilterCELAttributes", attrName)
}
}
...@@ -25,12 +25,13 @@ import ( ...@@ -25,12 +25,13 @@ import (
"github.com/usememos/memos/internal/base" "github.com/usememos/memos/internal/base"
"github.com/usememos/memos/internal/util" "github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/filter"
v1pb "github.com/usememos/memos/proto/gen/api/v1" v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store" storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
func (s *APIV1Service) ListUsers(ctx context.Context, _ *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) { func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) {
currentUser, err := s.GetCurrentUser(ctx) currentUser, err := s.GetCurrentUser(ctx)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
...@@ -39,12 +40,21 @@ func (s *APIV1Service) ListUsers(ctx context.Context, _ *v1pb.ListUsersRequest) ...@@ -39,12 +40,21 @@ func (s *APIV1Service) ListUsers(ctx context.Context, _ *v1pb.ListUsersRequest)
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
users, err := s.Store.ListUsers(ctx, &store.FindUser{}) userFind := &store.FindUser{}
if request.Filter != "" {
if err := s.validateUserFilter(ctx, request.Filter); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
}
userFind.Filters = append(userFind.Filters, request.Filter)
}
users, err := s.Store.ListUsers(ctx, userFind)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list users: %v", err) return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
} }
// TODO: Implement proper filtering, ordering, and pagination // TODO: Implement proper ordering, and pagination
// For now, return all users with basic structure // For now, return all users with basic structure
response := &v1pb.ListUsersResponse{ response := &v1pb.ListUsersResponse{
Users: []*v1pb.User{}, Users: []*v1pb.User{},
...@@ -70,47 +80,7 @@ func (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest ...@@ -70,47 +80,7 @@ func (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest
if user == nil { if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found") return nil, status.Errorf(codes.NotFound, "user not found")
} }
userPb := convertUserFromStore(user) return convertUserFromStore(user), nil
// TODO: Implement read_mask field filtering
// For now, return all fields
return userPb, nil
}
func (s *APIV1Service) SearchUsers(ctx context.Context, request *v1pb.SearchUsersRequest) (*v1pb.SearchUsersResponse, error) {
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
// Search users by username, email, or display name
users, err := s.Store.ListUsers(ctx, &store.FindUser{})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
}
var filteredUsers []*store.User
query := strings.ToLower(request.Query)
for _, user := range users {
if strings.Contains(strings.ToLower(user.Username), query) ||
strings.Contains(strings.ToLower(user.Email), query) ||
strings.Contains(strings.ToLower(user.Nickname), query) {
filteredUsers = append(filteredUsers, user)
}
}
response := &v1pb.SearchUsersResponse{
Users: []*v1pb.User{},
TotalSize: int32(len(filteredUsers)),
}
for _, user := range filteredUsers {
response.Users = append(response.Users, convertUserFromStore(user))
}
return response, nil
} }
func (s *APIV1Service) GetUserAvatar(ctx context.Context, request *v1pb.GetUserAvatarRequest) (*httpbody.HttpBody, error) { func (s *APIV1Service) GetUserAvatar(ctx context.Context, request *v1pb.GetUserAvatarRequest) (*httpbody.HttpBody, error) {
...@@ -1316,3 +1286,37 @@ func extractWebhookIDFromName(name string) string { ...@@ -1316,3 +1286,37 @@ func extractWebhookIDFromName(name string) string {
} }
return "" return ""
} }
// validateUserFilter validates the user filter string.
func (s *APIV1Service) validateUserFilter(_ context.Context, filterStr string) error {
if filterStr == "" {
return errors.New("filter cannot be empty")
}
// Validate the filter.
parsedExpr, err := filter.Parse(filterStr, filter.UserFilterCELAttributes...)
if err != nil {
return errors.Wrap(err, "failed to parse filter")
}
convertCtx := filter.NewConvertContext()
// Determine the dialect based on the actual database driver
var dialect filter.SQLDialect
switch s.Profile.Driver {
case "sqlite":
dialect = &filter.SQLiteDialect{}
case "mysql":
dialect = &filter.MySQLDialect{}
case "postgres":
dialect = &filter.PostgreSQLDialect{}
default:
// Default to SQLite for unknown drivers
dialect = &filter.SQLiteDialect{}
}
converter := filter.NewCommonSQLConverter(dialect)
err = converter.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr())
if err != nil {
return errors.Wrap(err, "failed to convert filter to SQL")
}
return nil
}
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/usememos/memos/plugin/filter"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
...@@ -84,6 +85,26 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U ...@@ -84,6 +85,26 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) { func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {
where, args := []string{"1 = 1"}, []any{} where, args := []string{"1 = 1"}, []any{}
for _, filterStr := range find.Filters {
// Parse filter string and return the parsed expression.
// The filter string should be a CEL expression.
parsedExpr, err := filter.Parse(filterStr, filter.UserFilterCELAttributes...)
if err != nil {
return nil, err
}
convertCtx := filter.NewConvertContext()
// ConvertExprToSQL converts the parsed expression to a SQL condition string.
converter := filter.NewCommonSQLConverter(&filter.MySQLDialect{})
if err := converter.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil {
return nil, err
}
condition := convertCtx.Buffer.String()
if condition != "" {
where = append(where, fmt.Sprintf("(%s)", condition))
args = append(args, convertCtx.Args...)
}
}
if v := find.ID; v != nil { if v := find.ID; v != nil {
where, args = append(where, "`id` = ?"), append(args, *v) where, args = append(where, "`id` = ?"), append(args, *v)
} }
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/usememos/memos/plugin/filter"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
...@@ -85,6 +86,26 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U ...@@ -85,6 +86,26 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) { func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {
where, args := []string{"1 = 1"}, []any{} where, args := []string{"1 = 1"}, []any{}
for _, filterStr := range find.Filters {
// Parse filter string and return the parsed expression.
// The filter string should be a CEL expression.
parsedExpr, err := filter.Parse(filterStr, filter.UserFilterCELAttributes...)
if err != nil {
return nil, err
}
convertCtx := filter.NewConvertContext()
// ConvertExprToSQL converts the parsed expression to a SQL condition string.
converter := filter.NewCommonSQLConverter(&filter.PostgreSQLDialect{})
if err := converter.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil {
return nil, err
}
condition := convertCtx.Buffer.String()
if condition != "" {
where = append(where, fmt.Sprintf("(%s)", condition))
args = append(args, convertCtx.Args...)
}
}
if v := find.ID; v != nil { if v := find.ID; v != nil {
where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *v) where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *v)
} }
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/usememos/memos/plugin/filter"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
...@@ -86,6 +87,26 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U ...@@ -86,6 +87,26 @@ func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.U
func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) { func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {
where, args := []string{"1 = 1"}, []any{} where, args := []string{"1 = 1"}, []any{}
for _, filterStr := range find.Filters {
// Parse filter string and return the parsed expression.
// The filter string should be a CEL expression.
parsedExpr, err := filter.Parse(filterStr, filter.UserFilterCELAttributes...)
if err != nil {
return nil, err
}
convertCtx := filter.NewConvertContext()
// ConvertExprToSQL converts the parsed expression to a SQL condition string.
converter := filter.NewCommonSQLConverter(&filter.SQLiteDialect{})
if err := converter.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil {
return nil, err
}
condition := convertCtx.Buffer.String()
if condition != "" {
where = append(where, fmt.Sprintf("(%s)", condition))
args = append(args, convertCtx.Args...)
}
}
if v := find.ID; v != nil { if v := find.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v) where, args = append(where, "id = ?"), append(args, *v)
} }
......
...@@ -83,6 +83,9 @@ type FindUser struct { ...@@ -83,6 +83,9 @@ type FindUser struct {
Email *string Email *string
Nickname *string Nickname *string
// Domain specific fields
Filters []string
// The maximum number of users to return. // The maximum number of users to return.
Limit *int Limit *int
} }
......
...@@ -84,8 +84,8 @@ const userStore = (() => { ...@@ -84,8 +84,8 @@ const userStore = (() => {
} }
} }
// Use search instead of the deprecated getUserByUsername // Use search instead of the deprecated getUserByUsername
const { users } = await userServiceClient.searchUsers({ const { users } = await userServiceClient.listUsers({
query: username, filter: `username == "${username}"`,
pageSize: 10, pageSize: 10,
}); });
const user = users.find((u) => u.username === username); const user = users.find((u) => u.username === username);
......
...@@ -114,11 +114,6 @@ export interface ListUsersRequest { ...@@ -114,11 +114,6 @@ export interface ListUsersRequest {
* Supported fields: username, email, role, state, create_time, update_time * Supported fields: username, email, role, state, create_time, update_time
*/ */
filter: string; filter: string;
/**
* Optional. The order to sort results by.
* Example: "create_time desc" or "username asc"
*/
orderBy: string;
/** Optional. If true, show deleted users in the response. */ /** Optional. If true, show deleted users in the response. */
showDeleted: boolean; showDeleted: boolean;
} }
...@@ -191,24 +186,6 @@ export interface DeleteUserRequest { ...@@ -191,24 +186,6 @@ export interface DeleteUserRequest {
force: boolean; force: boolean;
} }
export interface SearchUsersRequest {
/** Required. The search query. */
query: string;
/** Optional. The maximum number of users to return. */
pageSize: number;
/** Optional. A page token for pagination. */
pageToken: string;
}
export interface SearchUsersResponse {
/** The list of users matching the search query. */
users: User[];
/** A token for the next page of results. */
nextPageToken: string;
/** The total count of matching users. */
totalSize: number;
}
export interface GetUserAvatarRequest { export interface GetUserAvatarRequest {
/** /**
* Required. The resource name of the user. * Required. The resource name of the user.
...@@ -780,7 +757,7 @@ export const User: MessageFns<User> = { ...@@ -780,7 +757,7 @@ export const User: MessageFns<User> = {
}; };
function createBaseListUsersRequest(): ListUsersRequest { function createBaseListUsersRequest(): ListUsersRequest {
return { pageSize: 0, pageToken: "", filter: "", orderBy: "", showDeleted: false }; return { pageSize: 0, pageToken: "", filter: "", showDeleted: false };
} }
export const ListUsersRequest: MessageFns<ListUsersRequest> = { export const ListUsersRequest: MessageFns<ListUsersRequest> = {
...@@ -794,11 +771,8 @@ export const ListUsersRequest: MessageFns<ListUsersRequest> = { ...@@ -794,11 +771,8 @@ export const ListUsersRequest: MessageFns<ListUsersRequest> = {
if (message.filter !== "") { if (message.filter !== "") {
writer.uint32(26).string(message.filter); writer.uint32(26).string(message.filter);
} }
if (message.orderBy !== "") {
writer.uint32(34).string(message.orderBy);
}
if (message.showDeleted !== false) { if (message.showDeleted !== false) {
writer.uint32(40).bool(message.showDeleted); writer.uint32(32).bool(message.showDeleted);
} }
return writer; return writer;
}, },
...@@ -835,15 +809,7 @@ export const ListUsersRequest: MessageFns<ListUsersRequest> = { ...@@ -835,15 +809,7 @@ export const ListUsersRequest: MessageFns<ListUsersRequest> = {
continue; continue;
} }
case 4: { case 4: {
if (tag !== 34) { if (tag !== 32) {
break;
}
message.orderBy = reader.string();
continue;
}
case 5: {
if (tag !== 40) {
break; break;
} }
...@@ -867,7 +833,6 @@ export const ListUsersRequest: MessageFns<ListUsersRequest> = { ...@@ -867,7 +833,6 @@ export const ListUsersRequest: MessageFns<ListUsersRequest> = {
message.pageSize = object.pageSize ?? 0; message.pageSize = object.pageSize ?? 0;
message.pageToken = object.pageToken ?? ""; message.pageToken = object.pageToken ?? "";
message.filter = object.filter ?? ""; message.filter = object.filter ?? "";
message.orderBy = object.orderBy ?? "";
message.showDeleted = object.showDeleted ?? false; message.showDeleted = object.showDeleted ?? false;
return message; return message;
}, },
...@@ -1211,146 +1176,6 @@ export const DeleteUserRequest: MessageFns<DeleteUserRequest> = { ...@@ -1211,146 +1176,6 @@ export const DeleteUserRequest: MessageFns<DeleteUserRequest> = {
}, },
}; };
function createBaseSearchUsersRequest(): SearchUsersRequest {
return { query: "", pageSize: 0, pageToken: "" };
}
export const SearchUsersRequest: MessageFns<SearchUsersRequest> = {
encode(message: SearchUsersRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.query !== "") {
writer.uint32(10).string(message.query);
}
if (message.pageSize !== 0) {
writer.uint32(16).int32(message.pageSize);
}
if (message.pageToken !== "") {
writer.uint32(26).string(message.pageToken);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): SearchUsersRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseSearchUsersRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.query = reader.string();
continue;
}
case 2: {
if (tag !== 16) {
break;
}
message.pageSize = reader.int32();
continue;
}
case 3: {
if (tag !== 26) {
break;
}
message.pageToken = reader.string();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
create(base?: DeepPartial<SearchUsersRequest>): SearchUsersRequest {
return SearchUsersRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<SearchUsersRequest>): SearchUsersRequest {
const message = createBaseSearchUsersRequest();
message.query = object.query ?? "";
message.pageSize = object.pageSize ?? 0;
message.pageToken = object.pageToken ?? "";
return message;
},
};
function createBaseSearchUsersResponse(): SearchUsersResponse {
return { users: [], nextPageToken: "", totalSize: 0 };
}
export const SearchUsersResponse: MessageFns<SearchUsersResponse> = {
encode(message: SearchUsersResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.users) {
User.encode(v!, writer.uint32(10).fork()).join();
}
if (message.nextPageToken !== "") {
writer.uint32(18).string(message.nextPageToken);
}
if (message.totalSize !== 0) {
writer.uint32(24).int32(message.totalSize);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): SearchUsersResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseSearchUsersResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.users.push(User.decode(reader, reader.uint32()));
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.nextPageToken = reader.string();
continue;
}
case 3: {
if (tag !== 24) {
break;
}
message.totalSize = reader.int32();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
create(base?: DeepPartial<SearchUsersResponse>): SearchUsersResponse {
return SearchUsersResponse.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<SearchUsersResponse>): SearchUsersResponse {
const message = createBaseSearchUsersResponse();
message.users = object.users?.map((e) => User.fromPartial(e)) || [];
message.nextPageToken = object.nextPageToken ?? "";
message.totalSize = object.totalSize ?? 0;
return message;
},
};
function createBaseGetUserAvatarRequest(): GetUserAvatarRequest { function createBaseGetUserAvatarRequest(): GetUserAvatarRequest {
return { name: "" }; return { name: "" };
} }
...@@ -3586,46 +3411,6 @@ export const UserServiceDefinition = { ...@@ -3586,46 +3411,6 @@ export const UserServiceDefinition = {
}, },
}, },
}, },
/** SearchUsers searches for users based on query. */
searchUsers: {
name: "SearchUsers",
requestType: SearchUsersRequest,
requestStream: false,
responseType: SearchUsersResponse,
responseStream: false,
options: {
_unknownFields: {
8410: [new Uint8Array([5, 113, 117, 101, 114, 121])],
578365826: [
new Uint8Array([
22,
18,
20,
47,
97,
112,
105,
47,
118,
49,
47,
117,
115,
101,
114,
115,
58,
115,
101,
97,
114,
99,
104,
]),
],
},
},
},
/** GetUserAvatar gets the avatar of a user. */ /** GetUserAvatar gets the avatar of a user. */
getUserAvatar: { getUserAvatar: {
name: "GetUserAvatar", name: "GetUserAvatar",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment