From b44f2b5ffb5a9f9a52a706811a4112213a581388 Mon Sep 17 00:00:00 2001 From: boojack Date: Mon, 26 Jun 2023 23:06:53 +0800 Subject: [PATCH] chore: migrate user setting to api v1 package (#1855) * chore: migrate to api v1 package * chore: update --- api/v1/activity.go | 137 +++++++++++++++++++++ api/v1/auth.go | 71 +++++------ api/v1/memo.go | 25 ++++ api/v1/system_setting.go | 194 ++++++++++++++++++++++++++++++ api/v1/user_setting.go | 134 +++++++++++++++++++++ server/auth/auth.go | 4 +- server/jwt.go | 4 +- server/memo.go | 5 +- server/telegram.go | 11 +- server/user.go | 41 ++++++- store/activity.go | 49 +++++++- store/cache.go | 10 +- store/idp.go | 2 +- store/store.go | 4 +- store/system_setting.go | 95 +++++++++++++++ store/user.go | 21 ---- store/user_setting.go | 110 ++++++----------- test/server/memo_relation_test.go | 7 +- test/server/memo_test.go | 9 +- test/store/memo_test.go | 4 +- 20 files changed, 764 insertions(+), 173 deletions(-) create mode 100644 api/v1/activity.go create mode 100644 api/v1/memo.go create mode 100644 api/v1/system_setting.go create mode 100644 api/v1/user_setting.go diff --git a/api/v1/activity.go b/api/v1/activity.go new file mode 100644 index 00000000..399dba0d --- /dev/null +++ b/api/v1/activity.go @@ -0,0 +1,137 @@ +package v1 + +import "github.com/usememos/memos/server/profile" + +// ActivityType is the type for an activity. +type ActivityType string + +const ( + // User related. + + // ActivityUserCreate is the type for creating users. + ActivityUserCreate ActivityType = "user.create" + // ActivityUserUpdate is the type for updating users. + ActivityUserUpdate ActivityType = "user.update" + // ActivityUserDelete is the type for deleting users. + ActivityUserDelete ActivityType = "user.delete" + // ActivityUserAuthSignIn is the type for user signin. + ActivityUserAuthSignIn ActivityType = "user.auth.signin" + // ActivityUserAuthSignUp is the type for user signup. + ActivityUserAuthSignUp ActivityType = "user.auth.signup" + // ActivityUserSettingUpdate is the type for updating user settings. + ActivityUserSettingUpdate ActivityType = "user.setting.update" + + // Memo related. + + // ActivityMemoCreate is the type for creating memos. + ActivityMemoCreate ActivityType = "memo.create" + // ActivityMemoUpdate is the type for updating memos. + ActivityMemoUpdate ActivityType = "memo.update" + // ActivityMemoDelete is the type for deleting memos. + ActivityMemoDelete ActivityType = "memo.delete" + + // Shortcut related. + + // ActivityShortcutCreate is the type for creating shortcuts. + ActivityShortcutCreate ActivityType = "shortcut.create" + // ActivityShortcutUpdate is the type for updating shortcuts. + ActivityShortcutUpdate ActivityType = "shortcut.update" + // ActivityShortcutDelete is the type for deleting shortcuts. + ActivityShortcutDelete ActivityType = "shortcut.delete" + + // Resource related. + + // ActivityResourceCreate is the type for creating resources. + ActivityResourceCreate ActivityType = "resource.create" + // ActivityResourceDelete is the type for deleting resources. + ActivityResourceDelete ActivityType = "resource.delete" + + // Tag related. + + // ActivityTagCreate is the type for creating tags. + ActivityTagCreate ActivityType = "tag.create" + // ActivityTagDelete is the type for deleting tags. + ActivityTagDelete ActivityType = "tag.delete" + + // Server related. + + // ActivityServerStart is the type for starting server. + ActivityServerStart ActivityType = "server.start" +) + +// ActivityLevel is the level of activities. +type ActivityLevel string + +const ( + // ActivityInfo is the INFO level of activities. + ActivityInfo ActivityLevel = "INFO" + // ActivityWarn is the WARN level of activities. + ActivityWarn ActivityLevel = "WARN" + // ActivityError is the ERROR level of activities. + ActivityError ActivityLevel = "ERROR" +) + +type ActivityUserCreatePayload struct { + UserID int `json:"userId"` + Username string `json:"username"` + Role Role `json:"role"` +} + +type ActivityUserAuthSignInPayload struct { + UserID int `json:"userId"` + IP string `json:"ip"` +} + +type ActivityUserAuthSignUpPayload struct { + Username string `json:"username"` + IP string `json:"ip"` +} + +type ActivityMemoCreatePayload struct { + Content string `json:"content"` + Visibility string `json:"visibility"` +} + +type ActivityShortcutCreatePayload struct { + Title string `json:"title"` + Payload string `json:"payload"` +} + +type ActivityResourceCreatePayload struct { + Filename string `json:"filename"` + Type string `json:"type"` + Size int64 `json:"size"` +} + +type ActivityTagCreatePayload struct { + TagName string `json:"tagName"` +} + +type ActivityServerStartPayload struct { + ServerID string `json:"serverId"` + Profile *profile.Profile `json:"profile"` +} + +type Activity struct { + ID int `json:"id"` + + // Standard fields + CreatorID int `json:"creatorId"` + CreatedTs int64 `json:"createdTs"` + + // Domain specific fields + Type ActivityType `json:"type"` + Level ActivityLevel `json:"level"` + Payload string `json:"payload"` +} + +// ActivityCreate is the API message for creating an activity. +type ActivityCreate struct { + // Standard fields + CreatorID int + + // Domain specific fields + Type ActivityType `json:"type"` + Level ActivityLevel + Payload string `json:"payload"` +} diff --git a/api/v1/auth.go b/api/v1/auth.go index dd70d82d..755588e9 100644 --- a/api/v1/auth.go +++ b/api/v1/auth.go @@ -8,7 +8,6 @@ import ( "github.com/labstack/echo/v4" "github.com/pkg/errors" - "github.com/usememos/memos/api" "github.com/usememos/memos/common" "github.com/usememos/memos/plugin/idp" "github.com/usememos/memos/plugin/idp/oauth2" @@ -41,16 +40,15 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err) } - userFind := &api.UserFind{ + user, err := s.Store.GetUser(ctx, &store.FindUserMessage{ Username: &signin.Username, - } - user, err := s.Store.FindUser(ctx, userFind) + }) if err != nil && common.ErrorCode(err) != common.NotFound { return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again") } if user == nil { return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect login credentials, please try again") - } else if user.RowStatus == api.Archived { + } else if user.RowStatus == store.Archived { return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username)) } @@ -110,20 +108,19 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) { } } - user, err := s.Store.FindUser(ctx, &api.UserFind{ + user, err := s.Store.GetUser(ctx, &store.FindUserMessage{ Username: &userInfo.Identifier, }) if err != nil && common.ErrorCode(err) != common.NotFound { return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again") } if user == nil { - userCreate := &api.UserCreate{ + userCreate := &store.UserMessage{ Username: userInfo.Identifier, // The new signup user should be normal user by default. - Role: api.NormalUser, + Role: store.NormalUser, Nickname: userInfo.DisplayName, Email: userInfo.Email, - Password: userInfo.Email, OpenID: common.GenUUID(), } password, err := common.RandomString(20) @@ -135,12 +132,12 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err) } userCreate.PasswordHash = string(passwordHash) - user, err = s.Store.CreateUser(ctx, userCreate) + user, err = s.Store.CreateUserV1(ctx, userCreate) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err) } } - if user.RowStatus == api.Archived { + if user.RowStatus == store.Archived { return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", userInfo.Identifier)) } @@ -160,27 +157,27 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err) } - userCreate := &api.UserCreate{ - Username: signup.Username, - // The new signup user should be normal user by default. - Role: api.NormalUser, - Nickname: signup.Username, - Password: signup.Password, - OpenID: common.GenUUID(), - } - hostUserType := api.Host - existedHostUsers, err := s.Store.FindUserList(ctx, &api.UserFind{ + hostUserType := store.Host + existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUserMessage{ Role: &hostUserType, }) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Failed to find users").SetInternal(err) } + + userCreate := &store.UserMessage{ + Username: signup.Username, + // The new signup user should be normal user by default. + Role: store.NormalUser, + Nickname: signup.Username, + OpenID: common.GenUUID(), + } if len(existedHostUsers) == 0 { // Change the default role to host if there is no host user. - userCreate.Role = api.Host + userCreate.Role = store.Host } else { - allowSignUpSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{ - Name: api.SystemSettingAllowSignUpName, + allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSettingMessage{ + Name: SystemSettingAllowSignUpName.String(), }) if err != nil && common.ErrorCode(err) != common.NotFound { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err) @@ -198,17 +195,13 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) { } } - if err := userCreate.Validate(); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err) - } - passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err) } userCreate.PasswordHash = string(passwordHash) - user, err := s.Store.CreateUser(ctx, userCreate) + user, err := s.Store.CreateUserV1(ctx, userCreate) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err) } @@ -228,9 +221,9 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) { }) } -func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *api.User) error { +func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *store.UserMessage) error { ctx := c.Request().Context() - payload := api.ActivityUserAuthSignInPayload{ + payload := ActivityUserAuthSignInPayload{ UserID: user.ID, IP: echo.ExtractIPFromRealIPHeader()(c.Request()), } @@ -238,10 +231,10 @@ func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *api.User) if err != nil { return errors.Wrap(err, "failed to marshal activity payload") } - activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{ + activity, err := s.Store.CreateActivityV1(ctx, &store.ActivityMessage{ CreatorID: user.ID, - Type: api.ActivityUserAuthSignIn, - Level: api.ActivityInfo, + Type: string(ActivityUserAuthSignIn), + Level: string(ActivityInfo), Payload: string(payloadBytes), }) if err != nil || activity == nil { @@ -250,9 +243,9 @@ func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *api.User) return err } -func (s *APIV1Service) createAuthSignUpActivity(c echo.Context, user *api.User) error { +func (s *APIV1Service) createAuthSignUpActivity(c echo.Context, user *store.UserMessage) error { ctx := c.Request().Context() - payload := api.ActivityUserAuthSignUpPayload{ + payload := ActivityUserAuthSignUpPayload{ Username: user.Username, IP: echo.ExtractIPFromRealIPHeader()(c.Request()), } @@ -260,10 +253,10 @@ func (s *APIV1Service) createAuthSignUpActivity(c echo.Context, user *api.User) if err != nil { return errors.Wrap(err, "failed to marshal activity payload") } - activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{ + activity, err := s.Store.CreateActivityV1(ctx, &store.ActivityMessage{ CreatorID: user.ID, - Type: api.ActivityUserAuthSignUp, - Level: api.ActivityInfo, + Type: string(ActivityUserAuthSignUp), + Level: string(ActivityInfo), Payload: string(payloadBytes), }) if err != nil || activity == nil { diff --git a/api/v1/memo.go b/api/v1/memo.go new file mode 100644 index 00000000..69631034 --- /dev/null +++ b/api/v1/memo.go @@ -0,0 +1,25 @@ +package v1 + +// Visibility is the type of a visibility. +type Visibility string + +const ( + // Public is the PUBLIC visibility. + Public Visibility = "PUBLIC" + // Protected is the PROTECTED visibility. + Protected Visibility = "PROTECTED" + // Private is the PRIVATE visibility. + Private Visibility = "PRIVATE" +) + +func (v Visibility) String() string { + switch v { + case Public: + return "PUBLIC" + case Protected: + return "PROTECTED" + case Private: + return "PRIVATE" + } + return "PRIVATE" +} diff --git a/api/v1/system_setting.go b/api/v1/system_setting.go new file mode 100644 index 00000000..27b4661e --- /dev/null +++ b/api/v1/system_setting.go @@ -0,0 +1,194 @@ +package v1 + +import ( + "encoding/json" + "fmt" + "strings" +) + +type SystemSettingName string + +const ( + // SystemSettingServerIDName is the name of server id. + SystemSettingServerIDName SystemSettingName = "server-id" + // SystemSettingSecretSessionName is the name of secret session. + SystemSettingSecretSessionName SystemSettingName = "secret-session" + // SystemSettingAllowSignUpName is the name of allow signup setting. + SystemSettingAllowSignUpName SystemSettingName = "allow-signup" + // SystemSettingDisablePublicMemosName is the name of disable public memos setting. + SystemSettingDisablePublicMemosName SystemSettingName = "disable-public-memos" + // SystemSettingMaxUploadSizeMiBName is the name of max upload size setting. + SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib" + // SystemSettingAdditionalStyleName is the name of additional style. + SystemSettingAdditionalStyleName SystemSettingName = "additional-style" + // SystemSettingAdditionalScriptName is the name of additional script. + SystemSettingAdditionalScriptName SystemSettingName = "additional-script" + // SystemSettingCustomizedProfileName is the name of customized server profile. + SystemSettingCustomizedProfileName SystemSettingName = "customized-profile" + // SystemSettingStorageServiceIDName is the name of storage service ID. + SystemSettingStorageServiceIDName SystemSettingName = "storage-service-id" + // SystemSettingLocalStoragePathName is the name of local storage path. + SystemSettingLocalStoragePathName SystemSettingName = "local-storage-path" + // SystemSettingOpenAIConfigName is the name of OpenAI config. + SystemSettingOpenAIConfigName SystemSettingName = "openai-config" + // SystemSettingTelegramBotToken is the name of Telegram Bot Token. + SystemSettingTelegramBotTokenName SystemSettingName = "telegram-bot-token" + SystemSettingMemoDisplayWithUpdatedTsName SystemSettingName = "memo-display-with-updated-ts" +) + +// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item. +type CustomizedProfile struct { + // Name is the server name, default is `memos` + Name string `json:"name"` + // LogoURL is the url of logo image. + LogoURL string `json:"logoUrl"` + // Description is the server description. + Description string `json:"description"` + // Locale is the server default locale. + Locale string `json:"locale"` + // Appearance is the server default appearance. + Appearance string `json:"appearance"` + // ExternalURL is the external url of server. e.g. https://usermemos.com + ExternalURL string `json:"externalUrl"` +} + +type OpenAIConfig struct { + Key string `json:"key"` + Host string `json:"host"` +} + +func (key SystemSettingName) String() string { + switch key { + case SystemSettingServerIDName: + return "server-id" + case SystemSettingSecretSessionName: + return "secret-session" + case SystemSettingAllowSignUpName: + return "allow-signup" + case SystemSettingDisablePublicMemosName: + return "disable-public-memos" + case SystemSettingMaxUploadSizeMiBName: + return "max-upload-size-mib" + case SystemSettingAdditionalStyleName: + return "additional-style" + case SystemSettingAdditionalScriptName: + return "additional-script" + case SystemSettingCustomizedProfileName: + return "customized-profile" + case SystemSettingStorageServiceIDName: + return "storage-service-id" + case SystemSettingLocalStoragePathName: + return "local-storage-path" + case SystemSettingOpenAIConfigName: + return "openai-config" + case SystemSettingTelegramBotTokenName: + return "telegram-bot-token" + case SystemSettingMemoDisplayWithUpdatedTsName: + return "memo-display-with-updated-ts" + } + return "" +} + +type SystemSetting struct { + Name SystemSettingName `json:"name"` + // Value is a JSON string with basic value. + Value string `json:"value"` + Description string `json:"description"` +} + +type SystemSettingUpsert struct { + Name SystemSettingName `json:"name"` + Value string `json:"value"` + Description string `json:"description"` +} + +const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"` + +func (upsert SystemSettingUpsert) Validate() error { + switch settingName := upsert.Name; settingName { + case SystemSettingServerIDName: + return fmt.Errorf("updating %v is not allowed", settingName) + case SystemSettingAllowSignUpName: + var value bool + if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + case SystemSettingDisablePublicMemosName: + var value bool + if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + case SystemSettingMaxUploadSizeMiBName: + var value int + if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + case SystemSettingAdditionalStyleName: + var value string + if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + case SystemSettingAdditionalScriptName: + var value string + if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + case SystemSettingCustomizedProfileName: + customizedProfile := CustomizedProfile{ + Name: "memos", + LogoURL: "", + Description: "", + Locale: "en", + Appearance: "system", + ExternalURL: "", + } + if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + case SystemSettingStorageServiceIDName: + // Note: 0 is the default value(database) for storage service ID. + value := 0 + if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + return nil + case SystemSettingLocalStoragePathName: + value := "" + if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + case SystemSettingOpenAIConfigName: + value := OpenAIConfig{} + if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + case SystemSettingTelegramBotTokenName: + if upsert.Value == "" { + return nil + } + // Bot Token with Reverse Proxy shoule like `http.../bot` + if strings.HasPrefix(upsert.Value, "http") { + slashIndex := strings.LastIndexAny(upsert.Value, "/") + if strings.HasPrefix(upsert.Value[slashIndex:], "/bot") { + return nil + } + return fmt.Errorf("token start with `http` must end with `/bot`") + } + fragments := strings.Split(upsert.Value, ":") + if len(fragments) != 2 { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + case SystemSettingMemoDisplayWithUpdatedTsName: + var value bool + if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { + return fmt.Errorf(systemSettingUnmarshalError, settingName) + } + default: + return fmt.Errorf("invalid system setting name") + } + return nil +} + +type SystemSettingFind struct { + Name SystemSettingName `json:"name"` +} diff --git a/api/v1/user_setting.go b/api/v1/user_setting.go new file mode 100644 index 00000000..cdfefecc --- /dev/null +++ b/api/v1/user_setting.go @@ -0,0 +1,134 @@ +package v1 + +import ( + "encoding/json" + "fmt" + "strconv" + + "golang.org/x/exp/slices" +) + +type UserSettingKey string + +const ( + // UserSettingLocaleKey is the key type for user locale. + UserSettingLocaleKey UserSettingKey = "locale" + // UserSettingAppearanceKey is the key type for user appearance. + UserSettingAppearanceKey UserSettingKey = "appearance" + // UserSettingMemoVisibilityKey is the key type for user preference memo default visibility. + UserSettingMemoVisibilityKey UserSettingKey = "memo-visibility" + // UserSettingTelegramUserID is the key type for telegram UserID of memos user. + UserSettingTelegramUserIDKey UserSettingKey = "telegram-user-id" +) + +// String returns the string format of UserSettingKey type. +func (key UserSettingKey) String() string { + switch key { + case UserSettingLocaleKey: + return "locale" + case UserSettingAppearanceKey: + return "appearance" + case UserSettingMemoVisibilityKey: + return "memo-visibility" + case UserSettingTelegramUserIDKey: + return "telegram-user-id" + } + return "" +} + +var ( + UserSettingLocaleValue = []string{ + "de", + "en", + "es", + "fr", + "hr", + "it", + "ja", + "ko", + "nl", + "pl", + "pt-BR", + "ru", + "sl", + "sv", + "tr", + "uk", + "vi", + "zh-Hans", + "zh-Hant", + } + UserSettingAppearanceValue = []string{"system", "light", "dark"} + UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public} +) + +type UserSetting struct { + UserID int + Key UserSettingKey `json:"key"` + // Value is a JSON string with basic value + Value string `json:"value"` +} + +type UserSettingUpsert struct { + UserID int `json:"-"` + Key UserSettingKey `json:"key"` + Value string `json:"value"` +} + +func (upsert UserSettingUpsert) Validate() error { + if upsert.Key == UserSettingLocaleKey { + localeValue := "en" + err := json.Unmarshal([]byte(upsert.Value), &localeValue) + if err != nil { + return fmt.Errorf("failed to unmarshal user setting locale value") + } + if !slices.Contains(UserSettingLocaleValue, localeValue) { + return fmt.Errorf("invalid user setting locale value") + } + } else if upsert.Key == UserSettingAppearanceKey { + appearanceValue := "system" + err := json.Unmarshal([]byte(upsert.Value), &appearanceValue) + if err != nil { + return fmt.Errorf("failed to unmarshal user setting appearance value") + } + if !slices.Contains(UserSettingAppearanceValue, appearanceValue) { + return fmt.Errorf("invalid user setting appearance value") + } + } else if upsert.Key == UserSettingMemoVisibilityKey { + memoVisibilityValue := Private + err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue) + if err != nil { + return fmt.Errorf("failed to unmarshal user setting memo visibility value") + } + if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) { + return fmt.Errorf("invalid user setting memo visibility value") + } + } else if upsert.Key == UserSettingTelegramUserIDKey { + var s string + err := json.Unmarshal([]byte(upsert.Value), &s) + if err != nil { + return fmt.Errorf("invalid user setting telegram user id value") + } + + if s == "" { + return nil + } + if _, err := strconv.Atoi(s); err != nil { + return fmt.Errorf("invalid user setting telegram user id value") + } + } else { + return fmt.Errorf("invalid user setting key") + } + + return nil +} + +type UserSettingFind struct { + UserID *int + + Key UserSettingKey `json:"key"` +} + +type UserSettingDelete struct { + UserID int +} diff --git a/server/auth/auth.go b/server/auth/auth.go index c926a2c6..7fe63781 100644 --- a/server/auth/auth.go +++ b/server/auth/auth.go @@ -8,7 +8,7 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/labstack/echo/v4" "github.com/pkg/errors" - "github.com/usememos/memos/api" + "github.com/usememos/memos/store" ) const ( @@ -64,7 +64,7 @@ func GenerateRefreshToken(userName string, userID int, secret string) (string, e } // GenerateTokensAndSetCookies generates jwt token and saves it to the http-only cookie. -func GenerateTokensAndSetCookies(c echo.Context, user *api.User, secret string) error { +func GenerateTokensAndSetCookies(c echo.Context, user *store.UserMessage, secret string) error { accessToken, err := GenerateAccessToken(user.Username, user.ID, secret) if err != nil { return errors.Wrap(err, "failed to generate access token") diff --git a/server/jwt.go b/server/jwt.go index 7bfd3c8b..4f6a31aa 100644 --- a/server/jwt.go +++ b/server/jwt.go @@ -10,9 +10,9 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/labstack/echo/v4" "github.com/pkg/errors" - "github.com/usememos/memos/api" "github.com/usememos/memos/common" "github.com/usememos/memos/server/auth" + "github.com/usememos/memos/store" ) const ( @@ -136,7 +136,7 @@ func JWTMiddleware(server *Server, next echo.HandlerFunc, secret string) echo.Ha } // Even if there is no error, we still need to make sure the user still exists. - user, err := server.Store.FindUser(ctx, &api.UserFind{ + user, err := server.Store.GetUser(ctx, &store.FindUserMessage{ ID: &userID, }) if err != nil { diff --git a/server/memo.go b/server/memo.go index 5ea84782..da6c04f7 100644 --- a/server/memo.go +++ b/server/memo.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/usememos/memos/api" + apiv1 "github.com/usememos/memos/api/v1" "github.com/usememos/memos/common" "github.com/usememos/memos/store" @@ -37,9 +38,9 @@ func (s *Server) registerMemoRoutes(g *echo.Group) { } if createMemoRequest.Visibility == "" { - userMemoVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{ + userMemoVisibilitySetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSettingMessage{ UserID: &userID, - Key: api.UserSettingMemoVisibilityKey, + Key: apiv1.UserSettingMemoVisibilityKey.String(), }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err) diff --git a/server/telegram.go b/server/telegram.go index 13268151..5fe95089 100644 --- a/server/telegram.go +++ b/server/telegram.go @@ -7,7 +7,9 @@ import ( "path" "strconv" + "github.com/pkg/errors" "github.com/usememos/memos/api" + apiv1 "github.com/usememos/memos/api/v1" "github.com/usememos/memos/common" "github.com/usememos/memos/plugin/telegram" "github.com/usememos/memos/store" @@ -37,14 +39,13 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot, } var creatorID int - userSettingList, err := t.store.FindUserSettingList(ctx, &api.UserSettingFind{ - Key: api.UserSettingTelegramUserIDKey, + userSettingMessageList, err := t.store.ListUserSettings(ctx, &store.FindUserSettingMessage{ + Key: apiv1.UserSettingTelegramUserIDKey.String(), }) if err != nil { - _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Fail to find memo user: %s", err), nil) - return err + return errors.Wrap(err, "Failed to find userSettingList") } - for _, userSetting := range userSettingList { + for _, userSetting := range userSettingMessageList { var value string if err := json.Unmarshal([]byte(userSetting.Value), &value); err != nil { continue diff --git a/server/user.go b/server/user.go index c1f23b5b..894c08cd 100644 --- a/server/user.go +++ b/server/user.go @@ -9,7 +9,9 @@ import ( "github.com/pkg/errors" "github.com/usememos/memos/api" + apiv1 "github.com/usememos/memos/api/v1" "github.com/usememos/memos/common" + "github.com/usememos/memos/store" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" @@ -83,7 +85,7 @@ func (s *Server) registerUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session") } - userSettingUpsert := &api.UserSettingUpsert{} + userSettingUpsert := &apiv1.UserSettingUpsert{} if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err) } @@ -92,10 +94,15 @@ func (s *Server) registerUserRoutes(g *echo.Group) { } userSettingUpsert.UserID = userID - userSetting, err := s.Store.UpsertUserSetting(ctx, userSettingUpsert) + userSettingMessage, err := s.Store.UpsertUserSettingV1(ctx, &store.UserSettingMessage{ + UserID: userID, + Key: userSettingUpsert.Key.String(), + Value: userSettingUpsert.Value, + }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err) } + userSetting := convertUserSettingFromStore(userSettingMessage) return c.JSON(http.StatusOK, composeResponse(userSetting)) }) @@ -115,12 +122,21 @@ func (s *Server) registerUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err) } - userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{ + userSettingMessageList, err := s.Store.ListUserSettings(ctx, &store.FindUserSettingMessage{ UserID: &userID, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err) } + userSettingList := []*api.UserSetting{} + for _, userSettingMessage := range userSettingMessageList { + userSettingV1 := convertUserSettingFromStore(userSettingMessage) + userSettingList = append(userSettingList, &api.UserSetting{ + UserID: userSettingV1.UserID, + Key: api.UserSettingKey(userSettingV1.Key), + Value: userSettingV1.Value, + }) + } user.UserSettingList = userSettingList return c.JSON(http.StatusOK, composeResponse(user)) }) @@ -202,12 +218,21 @@ func (s *Server) registerUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err) } - userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{ + userSettingMessageList, err := s.Store.ListUserSettings(ctx, &store.FindUserSettingMessage{ UserID: &userID, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err) } + userSettingList := []*api.UserSetting{} + for _, userSettingMessage := range userSettingMessageList { + userSettingV1 := convertUserSettingFromStore(userSettingMessage) + userSettingList = append(userSettingList, &api.UserSetting{ + UserID: userSettingV1.UserID, + Key: api.UserSettingKey(userSettingV1.Key), + Value: userSettingV1.Value, + }) + } user.UserSettingList = userSettingList return c.JSON(http.StatusOK, composeResponse(user)) }) @@ -271,3 +296,11 @@ func (s *Server) createUserCreateActivity(c echo.Context, user *api.User) error } return err } + +func convertUserSettingFromStore(userSetting *store.UserSettingMessage) *apiv1.UserSetting { + return &apiv1.UserSetting{ + UserID: userSetting.UserID, + Key: apiv1.UserSettingKey(userSetting.Key), + Value: userSetting.Value, + } +} diff --git a/store/activity.go b/store/activity.go index 3fe05596..8f9283db 100644 --- a/store/activity.go +++ b/store/activity.go @@ -7,6 +7,51 @@ import ( "github.com/usememos/memos/api" ) +type ActivityMessage struct { + ID int + + // Standard fields + CreatorID int + CreatedTs int64 + + // Domain specific fields + Type string + Level string + Payload string +} + +// CreateActivityV1 creates an instance of Activity. +func (s *Store) CreateActivityV1(ctx context.Context, create *ActivityMessage) (*ActivityMessage, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, FormatError(err) + } + defer tx.Rollback() + + query := ` + INSERT INTO activity ( + creator_id, + type, + level, + payload + ) + VALUES (?, ?, ?, ?) + RETURNING id, created_ts + ` + if err := tx.QueryRowContext(ctx, query, create.CreatorID, create.Type, create.Level, create.Payload).Scan( + &create.ID, + &create.CreatedTs, + ); err != nil { + return nil, FormatError(err) + } + + if err := tx.Commit(); err != nil { + return nil, FormatError(err) + } + activityMessage := create + return activityMessage, nil +} + // activityRaw is the store model for an Activity. // Fields have exactly the same meanings as Activity. type activityRaw struct { @@ -38,10 +83,6 @@ func (raw *activityRaw) toActivity() *api.Activity { // CreateActivity creates an instance of Activity. func (s *Store) CreateActivity(ctx context.Context, create *api.ActivityCreate) (*api.Activity, error) { - if s.Profile.Mode == "prod" { - return nil, nil - } - tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, FormatError(err) diff --git a/store/cache.go b/store/cache.go index dcb0958e..c85b9b93 100644 --- a/store/cache.go +++ b/store/cache.go @@ -2,14 +2,8 @@ package store import ( "fmt" - - "github.com/usememos/memos/api" ) -func getUserSettingCacheKey(userSetting userSettingRaw) string { - return fmt.Sprintf("%d-%s", userSetting.UserID, userSetting.Key.String()) -} - -func getUserSettingFindCacheKey(userSettingFind *api.UserSettingFind) string { - return fmt.Sprintf("%d-%s", userSettingFind.UserID, userSettingFind.Key.String()) +func getUserSettingCacheKeyV1(userID int, key string) string { + return fmt.Sprintf("%d-%s", userID, key) } diff --git a/store/idp.go b/store/idp.go index c444b8fb..9b51e14a 100644 --- a/store/idp.go +++ b/store/idp.go @@ -291,7 +291,7 @@ func listIdentityProviders(ctx context.Context, tx *sql.Tx, find *FindIdentityPr } if err := rows.Err(); err != nil { - return nil, FormatError(err) + return nil, err } return identityProviderMessages, nil diff --git a/store/store.go b/store/store.go index cd35b1ee..5c4f55f5 100644 --- a/store/store.go +++ b/store/store.go @@ -14,9 +14,9 @@ type Store struct { db *sql.DB systemSettingCache sync.Map // map[string]*systemSettingRaw userCache sync.Map // map[int]*userRaw - userSettingCache sync.Map // map[string]*userSettingRaw + userSettingCache sync.Map // map[string]*UserSettingMessage shortcutCache sync.Map // map[int]*shortcutRaw - idpCache sync.Map // map[int]*identityProviderMessage + idpCache sync.Map // map[int]*IdentityProviderMessage resourceCache sync.Map // map[int]*resourceRaw } diff --git a/store/system_setting.go b/store/system_setting.go index 0fe7ac71..080baf11 100644 --- a/store/system_setting.go +++ b/store/system_setting.go @@ -10,6 +10,101 @@ import ( "github.com/usememos/memos/common" ) +type SystemSettingMessage struct { + Name string + Value string + Description string +} + +type FindSystemSettingMessage struct { + Name string +} + +func (s *Store) ListSystemSettings(ctx context.Context, find *FindSystemSettingMessage) ([]*SystemSettingMessage, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, FormatError(err) + } + defer tx.Rollback() + + list, err := listSystemSettings(ctx, tx, find) + if err != nil { + return nil, err + } + + for _, systemSettingMessage := range list { + s.systemSettingCache.Store(systemSettingMessage.Name, systemSettingMessage) + } + return list, nil +} + +func (s *Store) GetSystemSetting(ctx context.Context, find *FindSystemSettingMessage) (*SystemSettingMessage, error) { + if find.Name != "" { + if cache, ok := s.systemSettingCache.Load(find.Name); ok { + return cache.(*SystemSettingMessage), nil + } + } + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, FormatError(err) + } + defer tx.Rollback() + + list, err := listSystemSettings(ctx, tx, find) + if err != nil { + return nil, err + } + + if len(list) == 0 { + return nil, nil + } + + systemSettingMessage := list[0] + s.systemSettingCache.Store(systemSettingMessage.Name, systemSettingMessage) + return systemSettingMessage, nil +} + +func listSystemSettings(ctx context.Context, tx *sql.Tx, find *FindSystemSettingMessage) ([]*SystemSettingMessage, error) { + where, args := []string{"1 = 1"}, []any{} + if find.Name != "" { + where, args = append(where, "name = ?"), append(args, find.Name) + } + + query := ` + SELECT + name, + value, + description + FROM system_setting + WHERE ` + strings.Join(where, " AND ") + + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + return nil, FormatError(err) + } + defer rows.Close() + + list := []*SystemSettingMessage{} + for rows.Next() { + systemSettingMessage := &SystemSettingMessage{} + if err := rows.Scan( + &systemSettingMessage.Name, + &systemSettingMessage.Value, + &systemSettingMessage.Description, + ); err != nil { + return nil, FormatError(err) + } + list = append(list, systemSettingMessage) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + type systemSettingRaw struct { Name api.SystemSettingName Value string diff --git a/store/user.go b/store/user.go index 00bd604e..5c38056b 100644 --- a/store/user.go +++ b/store/user.go @@ -10,23 +10,6 @@ import ( "github.com/usememos/memos/common" ) -func (s *Store) SeedDataForNewUser(ctx context.Context, user *api.User) error { - // Create a memo for the user. - _, err := s.CreateMemo(ctx, &MemoMessage{ - CreatorID: user.ID, - Content: "#inbox Welcome to Memos!", - Visibility: Private, - }) - if err != nil { - return err - } - _, err = s.UpsertTag(ctx, &api.TagUpsert{ - CreatorID: user.ID, - Name: "inbox", - }) - return err -} - // Role is the type of a role. type Role string @@ -289,10 +272,6 @@ func (s *Store) CreateUser(ctx context.Context, create *api.UserCreate) (*api.Us s.userCache.Store(userRaw.ID, userRaw) user := userRaw.toUser() - if err := s.SeedDataForNewUser(ctx, user); err != nil { - return nil, err - } - return user, nil } diff --git a/store/user_setting.go b/store/user_setting.go index 50a2eb9b..85cd4ecc 100644 --- a/store/user_setting.go +++ b/store/user_setting.go @@ -4,33 +4,35 @@ import ( "context" "database/sql" "strings" - - "github.com/usememos/memos/api" ) -type userSettingRaw struct { +type UserSettingMessage struct { UserID int - Key api.UserSettingKey + Key string Value string } -func (raw *userSettingRaw) toUserSetting() *api.UserSetting { - return &api.UserSetting{ - UserID: raw.UserID, - Key: raw.Key, - Value: raw.Value, - } +type FindUserSettingMessage struct { + UserID *int + Key string } -func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUpsert) (*api.UserSetting, error) { +func (s *Store) UpsertUserSettingV1(ctx context.Context, upsert *UserSettingMessage) (*UserSettingMessage, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, FormatError(err) } defer tx.Rollback() - userSettingRaw, err := upsertUserSetting(ctx, tx, upsert) - if err != nil { + query := ` + INSERT INTO user_setting ( + user_id, key, value + ) + VALUES (?, ?, ?) + ON CONFLICT(user_id, key) DO UPDATE + SET value = EXCLUDED.value + ` + if _, err := tx.ExecContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value); err != nil { return nil, err } @@ -38,39 +40,34 @@ func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUp return nil, err } - s.userSettingCache.Store(getUserSettingCacheKey(*userSettingRaw), userSettingRaw) - userSetting := userSettingRaw.toUserSetting() - - return userSetting, nil + userSettingMessage := upsert + s.userSettingCache.Store(getUserSettingCacheKeyV1(userSettingMessage.UserID, userSettingMessage.Key), userSettingMessage) + return userSettingMessage, nil } -func (s *Store) FindUserSettingList(ctx context.Context, find *api.UserSettingFind) ([]*api.UserSetting, error) { +func (s *Store) ListUserSettings(ctx context.Context, find *FindUserSettingMessage) ([]*UserSettingMessage, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, FormatError(err) } defer tx.Rollback() - userSettingRawList, err := findUserSettingList(ctx, tx, find) + userSettingList, err := listUserSettings(ctx, tx, find) if err != nil { return nil, err } - list := []*api.UserSetting{} - for _, raw := range userSettingRawList { - s.userSettingCache.Store(getUserSettingCacheKey(*raw), raw) - list = append(list, raw.toUserSetting()) + for _, userSetting := range userSettingList { + s.userSettingCache.Store(getUserSettingCacheKeyV1(userSetting.UserID, userSetting.Key), userSetting) } - - return list, nil + return userSettingList, nil } -func (s *Store) FindUserSetting(ctx context.Context, find *api.UserSettingFind) (*api.UserSetting, error) { - if userSetting, ok := s.userSettingCache.Load(getUserSettingFindCacheKey(find)); ok { - if userSetting == nil { - return nil, nil +func (s *Store) GetUserSetting(ctx context.Context, find *FindUserSettingMessage) (*UserSettingMessage, error) { + if find.UserID != nil { + if cache, ok := s.userSettingCache.Load(getUserSettingCacheKeyV1(*find.UserID, find.Key)); ok { + return cache.(*UserSettingMessage), nil } - return userSetting.(*userSettingRaw).toUserSetting(), nil } tx, err := s.db.BeginTx(ctx, nil) @@ -79,51 +76,25 @@ func (s *Store) FindUserSetting(ctx context.Context, find *api.UserSettingFind) } defer tx.Rollback() - list, err := findUserSettingList(ctx, tx, find) + list, err := listUserSettings(ctx, tx, find) if err != nil { return nil, err } if len(list) == 0 { - s.userSettingCache.Store(getUserSettingFindCacheKey(find), nil) return nil, nil } - - userSettingRaw := list[0] - s.userSettingCache.Store(getUserSettingCacheKey(*userSettingRaw), userSettingRaw) - return userSettingRaw.toUserSetting(), nil + userSettingMessage := list[0] + s.userSettingCache.Store(getUserSettingCacheKeyV1(userSettingMessage.UserID, userSettingMessage.Key), userSettingMessage) + return userSettingMessage, nil } -func upsertUserSetting(ctx context.Context, tx *sql.Tx, upsert *api.UserSettingUpsert) (*userSettingRaw, error) { - query := ` - INSERT INTO user_setting ( - user_id, key, value - ) - VALUES (?, ?, ?) - ON CONFLICT(user_id, key) DO UPDATE - SET - value = EXCLUDED.value - RETURNING user_id, key, value - ` - var userSettingRaw userSettingRaw - if err := tx.QueryRowContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value).Scan( - &userSettingRaw.UserID, - &userSettingRaw.Key, - &userSettingRaw.Value, - ); err != nil { - return nil, FormatError(err) - } - - return &userSettingRaw, nil -} - -func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingFind) ([]*userSettingRaw, error) { +func listUserSettings(ctx context.Context, tx *sql.Tx, find *FindUserSettingMessage) ([]*UserSettingMessage, error) { where, args := []string{"1 = 1"}, []any{} - if v := find.Key.String(); v != "" { + if v := find.Key; v != "" { where, args = append(where, "key = ?"), append(args, v) } - if v := find.UserID; v != nil { where, args = append(where, "user_id = ?"), append(args, *find.UserID) } @@ -141,25 +112,24 @@ func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingF } defer rows.Close() - userSettingRawList := make([]*userSettingRaw, 0) + userSettingMessageList := make([]*UserSettingMessage, 0) for rows.Next() { - var userSettingRaw userSettingRaw + var userSettingMessage UserSettingMessage if err := rows.Scan( - &userSettingRaw.UserID, - &userSettingRaw.Key, - &userSettingRaw.Value, + &userSettingMessage.UserID, + &userSettingMessage.Key, + &userSettingMessage.Value, ); err != nil { return nil, FormatError(err) } - - userSettingRawList = append(userSettingRawList, &userSettingRaw) + userSettingMessageList = append(userSettingMessageList, &userSettingMessage) } if err := rows.Err(); err != nil { return nil, FormatError(err) } - return userSettingRawList, nil + return userSettingMessageList, nil } func vacuumUserSetting(ctx context.Context, tx *sql.Tx) error { diff --git a/test/server/memo_relation_test.go b/test/server/memo_relation_test.go index a1cb840b..28e3a16a 100644 --- a/test/server/memo_relation_test.go +++ b/test/server/memo_relation_test.go @@ -26,9 +26,6 @@ func TestMemoRelationServer(t *testing.T) { user, err := s.postAuthSignup(signup) require.NoError(t, err) require.Equal(t, signup.Username, user.Username) - memoList, err := s.getMemoList() - require.NoError(t, err) - require.Len(t, memoList, 1) memo, err := s.postMemoCreate(&api.CreateMemoRequest{ Content: "test memo", }) @@ -45,9 +42,9 @@ func TestMemoRelationServer(t *testing.T) { }) require.NoError(t, err) require.Equal(t, "test memo2", memo2.Content) - memoList, err = s.getMemoList() + memoList, err := s.getMemoList() require.NoError(t, err) - require.Len(t, memoList, 3) + require.Len(t, memoList, 2) require.Len(t, memo2.RelationList, 1) err = s.deleteMemoRelation(memo2.ID, memo.ID, api.MemoRelationReference) require.NoError(t, err) diff --git a/test/server/memo_test.go b/test/server/memo_test.go index d1c61a31..30fa4734 100644 --- a/test/server/memo_test.go +++ b/test/server/memo_test.go @@ -26,17 +26,14 @@ func TestMemoServer(t *testing.T) { user, err := s.postAuthSignup(signup) require.NoError(t, err) require.Equal(t, signup.Username, user.Username) - memoList, err := s.getMemoList() - require.NoError(t, err) - require.Len(t, memoList, 1) memo, err := s.postMemoCreate(&api.CreateMemoRequest{ Content: "test memo", }) require.NoError(t, err) require.Equal(t, "test memo", memo.Content) - memoList, err = s.getMemoList() + memoList, err := s.getMemoList() require.NoError(t, err) - require.Len(t, memoList, 2) + require.Len(t, memoList, 1) updatedContent := "updated memo" memo, err = s.patchMemo(&api.PatchMemoRequest{ ID: memo.ID, @@ -62,7 +59,7 @@ func TestMemoServer(t *testing.T) { require.NoError(t, err) memoList, err = s.getMemoList() require.NoError(t, err) - require.Len(t, memoList, 1) + require.Len(t, memoList, 0) } func (s *TestingServer) getMemo(memoID int) (*api.MemoResponse, error) { diff --git a/test/store/memo_test.go b/test/store/memo_test.go index 2ab6e4a3..d4294585 100644 --- a/test/store/memo_test.go +++ b/test/store/memo_test.go @@ -36,8 +36,8 @@ func TestMemoStore(t *testing.T) { CreatorID: &user.ID, }) require.NoError(t, err) - require.Equal(t, 2, len(memoList)) - require.Equal(t, memo, memoList[1]) + require.Equal(t, 1, len(memoList)) + require.Equal(t, memo, memoList[0]) err = ts.DeleteMemo(ctx, &store.DeleteMemoMessage{ ID: memo.ID, })