feat: add user-defined name to memo

This commit is contained in:
Steven 2024-01-20 23:48:35 +08:00
parent 264e6e6e9c
commit 8382354ef7
19 changed files with 929 additions and 524 deletions

View file

@ -21,10 +21,6 @@ import (
"github.com/usememos/memos/store"
)
var (
usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$")
)
type SignIn struct {
Username string `json:"username"`
Password string `json:"password"`
@ -293,7 +289,7 @@ func (s *APIV1Service) SignUp(c echo.Context) error {
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err)
}
if !usernameMatcher.MatchString(strings.ToLower(signup.Username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(signup.Username)) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", signup.Username)).SetInternal(err)
}

View file

@ -158,7 +158,7 @@ func (s *APIV1Service) CreateUser(c echo.Context) error {
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
}
if !usernameMatcher.MatchString(strings.ToLower(userCreate.Username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(userCreate.Username)) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", userCreate.Username)).SetInternal(err)
}
// Disallow host user to be created.
@ -379,7 +379,7 @@ func (s *APIV1Service) UpdateUser(c echo.Context) error {
}
}
if request.Username != nil {
if !usernameMatcher.MatchString(strings.ToLower(*request.Username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(*request.Username)) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid username %s", *request.Username)).SetInternal(err)
}
userUpdate.Username = request.Username

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/google/cel-go/cel"
"github.com/lithammer/shortuuid/v4"
"github.com/pkg/errors"
"go.uber.org/zap"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
@ -16,6 +17,7 @@ import (
apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/internal/log"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/gomark/ast"
"github.com/usememos/memos/plugin/gomark/parser"
"github.com/usememos/memos/plugin/gomark/parser/tokenizer"
@ -49,9 +51,10 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe
}
create := &store.Memo{
CreatorID: user.ID,
Content: request.Content,
Visibility: store.Visibility(request.Visibility.String()),
ResourceName: shortuuid.New(),
CreatorID: user.ID,
Content: request.Content,
Visibility: store.Visibility(request.Visibility.String()),
}
// Find disable public memos system setting.
disablePublicMemosSystem, err := s.getDisablePublicMemosSystemSettingValue(ctx)
@ -234,6 +237,27 @@ func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv2pb.GetMemoRequ
return response, nil
}
func (s *APIV2Service) GetMemoByName(ctx context.Context, request *apiv2pb.GetMemoByNameRequest) (*apiv2pb.GetMemoByNameResponse, error) {
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ResourceName: &request.Name,
})
if err != nil {
return nil, err
}
if memo == nil {
return nil, status.Errorf(codes.NotFound, "memo not found")
}
memoMessage, err := s.convertMemoFromStore(ctx, memo)
if err != nil {
return nil, errors.Wrap(err, "failed to convert memo")
}
response := &apiv2pb.GetMemoByNameResponse{
Memo: memoMessage,
}
return response, nil
}
func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMemoRequest) (*apiv2pb.UpdateMemoResponse, error) {
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
@ -282,6 +306,11 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
nodes := convertToASTNodes(request.Memo.Nodes)
content := restore.Restore(nodes)
update.Content = &content
} else if path == "resource_name" {
update.ResourceName = &request.Memo.Name
if !util.ResourceNameMatcher.MatchString(*update.ResourceName) {
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name")
}
} else if path == "visibility" {
visibility := convertVisibilityToStore(request.Memo.Visibility)
update.Visibility = &visibility
@ -574,6 +603,7 @@ func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
return &apiv2pb.Memo{
Id: int32(memo.ID),
Name: memo.ResourceName,
RowStatus: convertRowStatusFromStore(memo.RowStatus),
Creator: fmt.Sprintf("%s%s", UserNamePrefix, creator.Username),
CreatorId: int32(memo.CreatorID),

View file

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"time"
@ -18,15 +17,12 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/usememos/memos/api/auth"
"github.com/usememos/memos/internal/util"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store"
)
var (
usernameMatcher = regexp.MustCompile("^[a-z0-9]([a-z0-9-]{1,30}[a-z0-9])$")
)
func (s *APIV2Service) ListUsers(ctx context.Context, _ *apiv2pb.ListUsersRequest) (*apiv2pb.ListUsersResponse, error) {
currentUser, err := getCurrentUser(ctx, s.Store)
if err != nil {
@ -85,7 +81,7 @@ func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUs
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "name is required")
}
if !usernameMatcher.MatchString(strings.ToLower(username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(username)) {
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", username)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
@ -141,7 +137,7 @@ func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUs
}
for _, field := range request.UpdateMask.Paths {
if field == "username" {
if !usernameMatcher.MatchString(strings.ToLower(request.User.Username)) {
if !util.ResourceNameMatcher.MatchString(strings.ToLower(request.User.Username)) {
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username)
}
update.Username = &request.User.Username

1
go.mod
View file

@ -18,6 +18,7 @@ require (
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.11.4
github.com/lib/pq v1.10.9
github.com/lithammer/shortuuid/v4 v4.0.0
github.com/microcosm-cc/bluemonday v1.0.26
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.0

2
go.sum
View file

@ -286,6 +286,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=

View file

@ -0,0 +1,7 @@
package util
import "regexp"
var (
ResourceNameMatcher = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9-]{1,30}[a-zA-Z0-9])$")
)

View file

@ -31,6 +31,11 @@ service MemoService {
option (google.api.http) = {get: "/api/v2/memos/{id}"};
option (google.api.method_signature) = "id";
}
// GetMemoByName gets a memo by name.
rpc GetMemoByName(GetMemoByNameRequest) returns (GetMemoByNameResponse) {
option (google.api.http) = {get: "/api/v2/memos/{name}"};
option (google.api.method_signature) = "name";
}
// UpdateMemo updates a memo.
rpc UpdateMemo(UpdateMemoRequest) returns (UpdateMemoResponse) {
option (google.api.http) = {
@ -98,35 +103,39 @@ enum Visibility {
}
message Memo {
// id is the unique auto-incremented id.
int32 id = 1;
RowStatus row_status = 2;
// name is the user-defined name.
string name = 2;
RowStatus row_status = 3;
// The name of the creator.
// Format: users/{username}
string creator = 3;
string creator = 4;
int32 creator_id = 4;
int32 creator_id = 5;
google.protobuf.Timestamp create_time = 5;
google.protobuf.Timestamp create_time = 6;
google.protobuf.Timestamp update_time = 6;
google.protobuf.Timestamp update_time = 7;
google.protobuf.Timestamp display_time = 7;
google.protobuf.Timestamp display_time = 8;
string content = 8;
string content = 9;
repeated Node nodes = 9;
repeated Node nodes = 10;
Visibility visibility = 10;
Visibility visibility = 11;
bool pinned = 11;
bool pinned = 12;
optional int32 parent_id = 12;
optional int32 parent_id = 13 [(google.api.field_behavior) = OUTPUT_ONLY];
repeated Resource resources = 13 [(google.api.field_behavior) = OUTPUT_ONLY];
repeated Resource resources = 14 [(google.api.field_behavior) = OUTPUT_ONLY];
repeated MemoRelation relations = 14 [(google.api.field_behavior) = OUTPUT_ONLY];
repeated MemoRelation relations = 15 [(google.api.field_behavior) = OUTPUT_ONLY];
}
message CreateMemoRequest {
@ -163,6 +172,14 @@ message GetMemoResponse {
Memo memo = 1;
}
message GetMemoByNameRequest {
string name = 1;
}
message GetMemoByNameResponse {
Memo memo = 1;
}
message UpdateMemoRequest {
int32 id = 1;

View file

@ -129,6 +129,8 @@
- [CreateMemoResponse](#memos-api-v2-CreateMemoResponse)
- [DeleteMemoRequest](#memos-api-v2-DeleteMemoRequest)
- [DeleteMemoResponse](#memos-api-v2-DeleteMemoResponse)
- [GetMemoByNameRequest](#memos-api-v2-GetMemoByNameRequest)
- [GetMemoByNameResponse](#memos-api-v2-GetMemoByNameResponse)
- [GetMemoRequest](#memos-api-v2-GetMemoRequest)
- [GetMemoResponse](#memos-api-v2-GetMemoResponse)
- [GetUserMemosStatsRequest](#memos-api-v2-GetUserMemosStatsRequest)
@ -1864,6 +1866,36 @@
<a name="memos-api-v2-GetMemoByNameRequest"></a>
### GetMemoByNameRequest
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| name | [string](#string) | | |
<a name="memos-api-v2-GetMemoByNameResponse"></a>
### GetMemoByNameResponse
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| memo | [Memo](#memos-api-v2-Memo) | | |
<a name="memos-api-v2-GetMemoRequest"></a>
### GetMemoRequest
@ -2072,7 +2104,8 @@
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| id | [int32](#int32) | | |
| id | [int32](#int32) | | id is the unique auto-incremented id. |
| name | [string](#string) | | name is the user-defined name. |
| row_status | [RowStatus](#memos-api-v2-RowStatus) | | |
| creator | [string](#string) | | The name of the creator. Format: users/{username} |
| creator_id | [int32](#int32) | | |
@ -2206,6 +2239,7 @@
| CreateMemo | [CreateMemoRequest](#memos-api-v2-CreateMemoRequest) | [CreateMemoResponse](#memos-api-v2-CreateMemoResponse) | CreateMemo creates a memo. |
| ListMemos | [ListMemosRequest](#memos-api-v2-ListMemosRequest) | [ListMemosResponse](#memos-api-v2-ListMemosResponse) | ListMemos lists memos with pagination and filter. |
| GetMemo | [GetMemoRequest](#memos-api-v2-GetMemoRequest) | [GetMemoResponse](#memos-api-v2-GetMemoResponse) | GetMemo gets a memo by id. |
| GetMemoByName | [GetMemoByNameRequest](#memos-api-v2-GetMemoByNameRequest) | [GetMemoByNameResponse](#memos-api-v2-GetMemoByNameResponse) | GetMemoByName gets a memo by name. |
| UpdateMemo | [UpdateMemoRequest](#memos-api-v2-UpdateMemoRequest) | [UpdateMemoResponse](#memos-api-v2-UpdateMemoResponse) | UpdateMemo updates a memo. |
| DeleteMemo | [DeleteMemoRequest](#memos-api-v2-DeleteMemoRequest) | [DeleteMemoResponse](#memos-api-v2-DeleteMemoResponse) | DeleteMemo deletes a memo by id. |
| SetMemoResources | [SetMemoResourcesRequest](#memos-api-v2-SetMemoResourcesRequest) | [SetMemoResourcesResponse](#memos-api-v2-SetMemoResourcesResponse) | SetMemoResources sets resources for a memo. |

File diff suppressed because it is too large Load diff

View file

@ -153,6 +153,58 @@ func local_request_MemoService_GetMemo_0(ctx context.Context, marshaler runtime.
}
func request_MemoService_GetMemoByName_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetMemoByNameRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
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.GetMemoByName(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_MemoService_GetMemoByName_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetMemoByNameRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
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.GetMemoByName(ctx, &protoReq)
return msg, metadata, err
}
func request_MemoService_UpdateMemo_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq UpdateMemoRequest
var metadata runtime.ServerMetadata
@ -752,6 +804,31 @@ func RegisterMemoServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
})
mux.Handle("GET", pattern_MemoService_GetMemoByName_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v2.MemoService/GetMemoByName", runtime.WithHTTPPathPattern("/api/v2/memos/{name}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_MemoService_GetMemoByName_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_MemoService_GetMemoByName_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("PATCH", pattern_MemoService_UpdateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -1084,6 +1161,28 @@ func RegisterMemoServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
})
mux.Handle("GET", pattern_MemoService_GetMemoByName_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)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/memos.api.v2.MemoService/GetMemoByName", runtime.WithHTTPPathPattern("/api/v2/memos/{name}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_MemoService_GetMemoByName_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_MemoService_GetMemoByName_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("PATCH", pattern_MemoService_UpdateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -1292,6 +1391,8 @@ var (
pattern_MemoService_GetMemo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v2", "memos", "id"}, ""))
pattern_MemoService_GetMemoByName_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v2", "memos", "name"}, ""))
pattern_MemoService_UpdateMemo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v2", "memos", "id"}, ""))
pattern_MemoService_DeleteMemo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v2", "memos", "id"}, ""))
@ -1318,6 +1419,8 @@ var (
forward_MemoService_GetMemo_0 = runtime.ForwardResponseMessage
forward_MemoService_GetMemoByName_0 = runtime.ForwardResponseMessage
forward_MemoService_UpdateMemo_0 = runtime.ForwardResponseMessage
forward_MemoService_DeleteMemo_0 = runtime.ForwardResponseMessage

View file

@ -22,6 +22,7 @@ const (
MemoService_CreateMemo_FullMethodName = "/memos.api.v2.MemoService/CreateMemo"
MemoService_ListMemos_FullMethodName = "/memos.api.v2.MemoService/ListMemos"
MemoService_GetMemo_FullMethodName = "/memos.api.v2.MemoService/GetMemo"
MemoService_GetMemoByName_FullMethodName = "/memos.api.v2.MemoService/GetMemoByName"
MemoService_UpdateMemo_FullMethodName = "/memos.api.v2.MemoService/UpdateMemo"
MemoService_DeleteMemo_FullMethodName = "/memos.api.v2.MemoService/DeleteMemo"
MemoService_SetMemoResources_FullMethodName = "/memos.api.v2.MemoService/SetMemoResources"
@ -43,6 +44,8 @@ type MemoServiceClient interface {
ListMemos(ctx context.Context, in *ListMemosRequest, opts ...grpc.CallOption) (*ListMemosResponse, error)
// GetMemo gets a memo by id.
GetMemo(ctx context.Context, in *GetMemoRequest, opts ...grpc.CallOption) (*GetMemoResponse, error)
// GetMemoByName gets a memo by name.
GetMemoByName(ctx context.Context, in *GetMemoByNameRequest, opts ...grpc.CallOption) (*GetMemoByNameResponse, error)
// UpdateMemo updates a memo.
UpdateMemo(ctx context.Context, in *UpdateMemoRequest, opts ...grpc.CallOption) (*UpdateMemoResponse, error)
// DeleteMemo deletes a memo by id.
@ -98,6 +101,15 @@ func (c *memoServiceClient) GetMemo(ctx context.Context, in *GetMemoRequest, opt
return out, nil
}
func (c *memoServiceClient) GetMemoByName(ctx context.Context, in *GetMemoByNameRequest, opts ...grpc.CallOption) (*GetMemoByNameResponse, error) {
out := new(GetMemoByNameResponse)
err := c.cc.Invoke(ctx, MemoService_GetMemoByName_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *memoServiceClient) UpdateMemo(ctx context.Context, in *UpdateMemoRequest, opts ...grpc.CallOption) (*UpdateMemoResponse, error) {
out := new(UpdateMemoResponse)
err := c.cc.Invoke(ctx, MemoService_UpdateMemo_FullMethodName, in, out, opts...)
@ -189,6 +201,8 @@ type MemoServiceServer interface {
ListMemos(context.Context, *ListMemosRequest) (*ListMemosResponse, error)
// GetMemo gets a memo by id.
GetMemo(context.Context, *GetMemoRequest) (*GetMemoResponse, error)
// GetMemoByName gets a memo by name.
GetMemoByName(context.Context, *GetMemoByNameRequest) (*GetMemoByNameResponse, error)
// UpdateMemo updates a memo.
UpdateMemo(context.Context, *UpdateMemoRequest) (*UpdateMemoResponse, error)
// DeleteMemo deletes a memo by id.
@ -223,6 +237,9 @@ func (UnimplementedMemoServiceServer) ListMemos(context.Context, *ListMemosReque
func (UnimplementedMemoServiceServer) GetMemo(context.Context, *GetMemoRequest) (*GetMemoResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMemo not implemented")
}
func (UnimplementedMemoServiceServer) GetMemoByName(context.Context, *GetMemoByNameRequest) (*GetMemoByNameResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMemoByName not implemented")
}
func (UnimplementedMemoServiceServer) UpdateMemo(context.Context, *UpdateMemoRequest) (*UpdateMemoResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateMemo not implemented")
}
@ -317,6 +334,24 @@ func _MemoService_GetMemo_Handler(srv interface{}, ctx context.Context, dec func
return interceptor(ctx, in, info, handler)
}
func _MemoService_GetMemoByName_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMemoByNameRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MemoServiceServer).GetMemoByName(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MemoService_GetMemoByName_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MemoServiceServer).GetMemoByName(ctx, req.(*GetMemoByNameRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MemoService_UpdateMemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateMemoRequest)
if err := dec(in); err != nil {
@ -498,6 +533,10 @@ var MemoService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetMemo",
Handler: _MemoService_GetMemo_Handler,
},
{
MethodName: "GetMemoByName",
Handler: _MemoService_GetMemoByName_Handler,
},
{
MethodName: "UpdateMemo",
Handler: _MemoService_UpdateMemo_Handler,

View file

@ -10,11 +10,11 @@ import (
)
func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) {
fields := []string{"`creator_id`", "`content`", "`visibility`"}
placeholder := []string{"?", "?", "?"}
args := []any{create.CreatorID, create.Content, create.Visibility}
fields := []string{"`resource_name`", "`creator_id`", "`content`", "`visibility`"}
placeholder := []string{"?", "?", "?", "?"}
args := []any{create.ResourceName, create.CreatorID, create.Content, create.Visibility}
stmt := "INSERT INTO memo (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`, `row_status`"
stmt := "INSERT INTO `memo` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`, `row_status`"
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
&create.ID,
&create.CreatedTs,
@ -31,29 +31,32 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil {
where, args = append(where, "memo.id = ?"), append(args, *v)
where, args = append(where, "`memo`.`id` = ?"), append(args, *v)
}
if v := find.ResourceName; v != nil {
where, args = append(where, "`memo`.`resource_name` = ?"), append(args, *v)
}
if v := find.CreatorID; v != nil {
where, args = append(where, "memo.creator_id = ?"), append(args, *v)
where, args = append(where, "`memo`.`creator_id` = ?"), append(args, *v)
}
if v := find.RowStatus; v != nil {
where, args = append(where, "memo.row_status = ?"), append(args, *v)
where, args = append(where, "`memo`.`row_status` = ?"), append(args, *v)
}
if v := find.CreatedTsBefore; v != nil {
where, args = append(where, "memo.created_ts < ?"), append(args, *v)
where, args = append(where, "`memo`.`created_ts` < ?"), append(args, *v)
}
if v := find.CreatedTsAfter; v != nil {
where, args = append(where, "memo.created_ts > ?"), append(args, *v)
where, args = append(where, "`memo`.`created_ts` > ?"), append(args, *v)
}
if v := find.UpdatedTsBefore; v != nil {
where, args = append(where, "memo.updated_ts < ?"), append(args, *v)
where, args = append(where, "`memo`.`updated_ts` < ?"), append(args, *v)
}
if v := find.UpdatedTsAfter; v != nil {
where, args = append(where, "memo.updated_ts > ?"), append(args, *v)
where, args = append(where, "`memo`.`updated_ts` > ?"), append(args, *v)
}
if v := find.ContentSearch; len(v) != 0 {
for _, s := range v {
where, args = append(where, "memo.content LIKE ?"), append(args, fmt.Sprintf("%%%s%%", s))
where, args = append(where, "`memo`.`content` LIKE ?"), append(args, fmt.Sprintf("%%%s%%", s))
}
}
if v := find.VisibilityList; len(v) != 0 {
@ -62,43 +65,43 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
placeholder = append(placeholder, "?")
args = append(args, visibility.String())
}
where = append(where, fmt.Sprintf("memo.visibility in (%s)", strings.Join(placeholder, ",")))
where = append(where, fmt.Sprintf("`memo`.`visibility` IN (%s)", strings.Join(placeholder, ",")))
}
if find.ExcludeComments {
where = append(where, "parent_id IS NULL")
where = append(where, "`parent_id` IS NULL")
}
orders := []string{}
if find.OrderByPinned {
orders = append(orders, "pinned DESC")
orders = append(orders, "`pinned` DESC")
}
if find.OrderByUpdatedTs {
orders = append(orders, "updated_ts DESC")
orders = append(orders, "`updated_ts` DESC")
} else {
orders = append(orders, "created_ts DESC")
orders = append(orders, "`created_ts` DESC")
}
orders = append(orders, "id DESC")
orders = append(orders, "`id` DESC")
fields := []string{
`memo.id AS id`,
`memo.creator_id AS creator_id`,
`memo.created_ts AS created_ts`,
`memo.updated_ts AS updated_ts`,
`memo.row_status AS row_status`,
`memo.visibility AS visibility`,
`IFNULL(memo_organizer.pinned, 0) AS pinned`,
`memo_relation.related_memo_id AS parent_id`,
"`memo`.`id` AS `id`",
"`memo`.`resource_name` AS `resource_name`",
"`memo`.`creator_id` AS `creator_id`",
"`memo`.`created_ts` AS `created_ts`",
"`memo`.`updated_ts` AS `updated_ts`",
"`memo`.`row_status` AS `row_status`",
"`memo`.`visibility` AS `visibility`",
"IFNULL(`memo_organizer`.`pinned`, 0) AS `pinned`",
"`memo_relation`.`related_memo_id` AS `parent_id`",
}
if !find.ExcludeContent {
fields = append(fields, `memo.content AS content`)
fields = append(fields, "`memo`.`content` AS `content`")
}
query := `SELECT ` + strings.Join(fields, ", ") + `
FROM memo
LEFT JOIN memo_organizer ON memo.id = memo_organizer.memo_id AND memo.creator_id = memo_organizer.user_id
FULL JOIN memo_relation ON memo.id = memo_relation.memo_id AND memo_relation.type = "COMMENT"
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY ` + strings.Join(orders, ", ")
query := "SELECT " + strings.Join(fields, ", ") + "FROM `memo` " +
"LEFT JOIN `memo_organizer` ON `memo`.`id` = `memo_organizer`.`memo_id` AND `memo`.`creator_id` = `memo_organizer`.`user_id` " +
"FULL JOIN `memo_relation` ON `memo`.`id` = `memo_relation`.`memo_id` AND `memo_relation`.`type` = \"COMMENT\"" + " " +
"WHERE " + strings.Join(where, " AND ") + " " +
"ORDER BY " + strings.Join(orders, ", ")
if find.Limit != nil {
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
if find.Offset != nil {
@ -117,6 +120,7 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
var memo store.Memo
dests := []any{
&memo.ID,
&memo.ResourceName,
&memo.CreatorID,
&memo.CreatedTs,
&memo.UpdatedTs,
@ -143,24 +147,27 @@ func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo
func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error {
set, args := []string{}, []any{}
if v := update.ResourceName; v != nil {
set, args = append(set, "`resource_name` = ?"), append(args, *v)
}
if v := update.CreatedTs; v != nil {
set, args = append(set, "created_ts = ?"), append(args, *v)
set, args = append(set, "`created_ts` = ?"), append(args, *v)
}
if v := update.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
set, args = append(set, "`updated_ts` = ?"), append(args, *v)
}
if v := update.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, *v)
set, args = append(set, "`row_status` = ?"), append(args, *v)
}
if v := update.Content; v != nil {
set, args = append(set, "content = ?"), append(args, *v)
set, args = append(set, "`content` = ?"), append(args, *v)
}
if v := update.Visibility; v != nil {
set, args = append(set, "visibility = ?"), append(args, *v)
set, args = append(set, "`visibility` = ?"), append(args, *v)
}
args = append(args, update.ID)
stmt := `UPDATE memo SET ` + strings.Join(set, ", ") + ` WHERE id = ?`
stmt := "UPDATE `memo` SET " + strings.Join(set, ", ") + " WHERE `id` = ?"
if _, err := d.db.ExecContext(ctx, stmt, args...); err != nil {
return err
}
@ -168,8 +175,8 @@ func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error {
}
func (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error {
where, args := []string{"id = ?"}, []any{delete.ID}
stmt := `DELETE FROM memo WHERE ` + strings.Join(where, " AND ")
where, args := []string{"`id` = ?"}, []any{delete.ID}
stmt := "DELETE FROM `memo` WHERE " + strings.Join(where, " AND ")
result, err := d.db.ExecContext(ctx, stmt, args...)
if err != nil {
return err
@ -186,16 +193,7 @@ func (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error {
}
func vacuumMemo(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
stmt := "DELETE FROM `memo` WHERE `creator_id` NOT IN (SELECT `id` FROM `user`)"
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return err

View file

@ -55,6 +55,7 @@ CREATE TABLE user_setting (
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
resource_name 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')),

View file

@ -1,8 +1,14 @@
INSERT INTO
memo (`id`, `content`, `creator_id`)
memo (
`id`,
`resource_name`,
`content`,
`creator_id`
)
VALUES
(
1,
"hello",
"#Hello 👋 Welcome to memos.",
101
);
@ -10,6 +16,7 @@ VALUES
INSERT INTO
memo (
`id`,
`resource_name`,
`content`,
`creator_id`,
`visibility`
@ -17,6 +24,7 @@ INSERT INTO
VALUES
(
2,
"todo",
'#TODO
- [x] Take more photos about **🌄 sunset**;
- [x] Clean the room;
@ -28,6 +36,7 @@ VALUES
INSERT INTO
memo (
`id`,
`resource_name`,
`content`,
`creator_id`,
`visibility`
@ -35,6 +44,7 @@ INSERT INTO
VALUES
(
3,
"links",
'**[Memos](https://github.com/usememos/memos)**: A lightweight, self-hosted memo hub. Open Source and Free forever.
**[Slash](https://github.com/yourselfhosted/slash)**: An open source, self-hosted bookmarks and link sharing platform. Save and share your links very easily.',
101,
@ -44,6 +54,7 @@ VALUES
INSERT INTO
memo (
`id`,
`resource_name`,
`content`,
`creator_id`,
`visibility`
@ -51,6 +62,7 @@ INSERT INTO
VALUES
(
4,
"todo2",
'#TODO
- [x] Take more photos about **🌄 sunset**;
- [ ] Clean the classroom;
@ -62,6 +74,7 @@ VALUES
INSERT INTO
memo (
`id`,
`resource_name`,
`content`,
`creator_id`,
`visibility`
@ -69,6 +82,7 @@ INSERT INTO
VALUES
(
5,
"words",
'三人行,必有我师焉!👨‍🏫',
102,
'PUBLIC'

View file

@ -2,6 +2,9 @@ package store
import (
"context"
"errors"
"github.com/usememos/memos/internal/util"
)
// Visibility is the type of a visibility.
@ -29,7 +32,8 @@ func (v Visibility) String() string {
}
type Memo struct {
ID int32
ID int32
ResourceName string
// Standard fields
RowStatus RowStatus
@ -47,7 +51,8 @@ type Memo struct {
}
type FindMemo struct {
ID *int32
ID *int32
ResourceName *string
// Standard fields
RowStatus *RowStatus
@ -71,12 +76,13 @@ type FindMemo struct {
}
type UpdateMemo struct {
ID int32
CreatedTs *int64
UpdatedTs *int64
RowStatus *RowStatus
Content *string
Visibility *Visibility
ID int32
ResourceName *string
CreatedTs *int64
UpdatedTs *int64
RowStatus *RowStatus
Content *string
Visibility *Visibility
}
type DeleteMemo struct {
@ -84,6 +90,9 @@ type DeleteMemo struct {
}
func (s *Store) CreateMemo(ctx context.Context, create *Memo) (*Memo, error) {
if !util.ResourceNameMatcher.MatchString(create.ResourceName) {
return nil, errors.New("resource name is invalid")
}
return s.driver.CreateMemo(ctx, create)
}
@ -105,6 +114,9 @@ func (s *Store) GetMemo(ctx context.Context, find *FindMemo) (*Memo, error) {
}
func (s *Store) UpdateMemo(ctx context.Context, update *UpdateMemo) error {
if update.ResourceName != nil && !util.ResourceNameMatcher.MatchString(*update.ResourceName) {
return errors.New("resource name is invalid")
}
return s.driver.UpdateMemo(ctx, update)
}

View file

@ -15,9 +15,10 @@ func TestMemoOrganizerStore(t *testing.T) {
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
memoCreate := &store.Memo{
CreatorID: user.ID,
Content: "main memo content",
Visibility: store.Public,
ResourceName: "main-memo",
CreatorID: user.ID,
Content: "main memo content",
Visibility: store.Public,
}
memo, err := ts.CreateMemo(ctx, memoCreate)
require.NoError(t, err)

View file

@ -15,25 +15,28 @@ func TestMemoRelationStore(t *testing.T) {
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
memoCreate := &store.Memo{
CreatorID: user.ID,
Content: "main memo content",
Visibility: store.Public,
ResourceName: "main-memo",
CreatorID: user.ID,
Content: "main memo content",
Visibility: store.Public,
}
memo, err := ts.CreateMemo(ctx, memoCreate)
require.NoError(t, err)
require.Equal(t, memoCreate.Content, memo.Content)
relatedMemoCreate := &store.Memo{
CreatorID: user.ID,
Content: "related memo content",
Visibility: store.Public,
ResourceName: "related-memo",
CreatorID: user.ID,
Content: "related memo content",
Visibility: store.Public,
}
relatedMemo, err := ts.CreateMemo(ctx, relatedMemoCreate)
require.NoError(t, err)
require.Equal(t, relatedMemoCreate.Content, relatedMemo.Content)
commentMemoCreate := &store.Memo{
CreatorID: user.ID,
Content: "comment memo content",
Visibility: store.Public,
ResourceName: "comment-memo",
CreatorID: user.ID,
Content: "comment memo content",
Visibility: store.Public,
}
commentMemo, err := ts.CreateMemo(ctx, commentMemoCreate)
require.NoError(t, err)

View file

@ -15,9 +15,10 @@ func TestMemoStore(t *testing.T) {
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
memoCreate := &store.Memo{
CreatorID: user.ID,
Content: "test_content",
Visibility: store.Public,
ResourceName: "test-resource-name",
CreatorID: user.ID,
Content: "test_content",
Visibility: store.Public,
}
memo, err := ts.CreateMemo(ctx, memoCreate)
require.NoError(t, err)
@ -67,9 +68,10 @@ func TestDeleteMemoStore(t *testing.T) {
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
memoCreate := &store.Memo{
CreatorID: user.ID,
Content: "test_content",
Visibility: store.Public,
ResourceName: "test-resource-name",
CreatorID: user.ID,
Content: "test_content",
Visibility: store.Public,
}
memo, err := ts.CreateMemo(ctx, memoCreate)
require.NoError(t, err)