Add user settings key/value DB table (#16834)

This commit is contained in:
techknowlogick 2021-11-22 04:47:23 -05:00 committed by GitHub
parent a159c3175f
commit 499b05da22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 198 additions and 2 deletions

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/klauspost/cpuid/v2" "github.com/klauspost/cpuid/v2"
) )

View File

@ -357,6 +357,8 @@ var migrations = []Migration{
NewMigration("Add table app_state", addTableAppState), NewMigration("Add table app_state", addTableAppState),
// v201 -> v202 // v201 -> v202
NewMigration("Drop table remote_version (if exists)", dropTableRemoteVersion), NewMigration("Drop table remote_version (if exists)", dropTableRemoteVersion),
// v202 -> v203
NewMigration("Create key/value table for user settings", createUserSettingsTable),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

25
models/migrations/v202.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"xorm.io/xorm"
)
func createUserSettingsTable(x *xorm.Engine) error {
type UserSetting struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"index unique(key_userid)"` // to load all of someone's settings
SettingKey string `xorm:"varchar(255) index unique(key_userid)"` // ensure key is always lowercase
SettingValue string `xorm:"text"`
}
if err := x.Sync2(new(UserSetting)); err != nil {
return fmt.Errorf("sync2: %v", err)
}
return nil
}

View File

@ -1192,6 +1192,7 @@ func DeleteUser(ctx context.Context, u *User) (err error) {
&TeamUser{UID: u.ID}, &TeamUser{UID: u.ID},
&Collaboration{UserID: u.ID}, &Collaboration{UserID: u.ID},
&Stopwatch{UserID: u.ID}, &Stopwatch{UserID: u.ID},
&user_model.Setting{UserID: u.ID},
); err != nil { ); err != nil {
return fmt.Errorf("deleteBeans: %v", err) return fmt.Errorf("deleteBeans: %v", err)
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 The Gitea Authors. All rights reserved. // Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

116
models/user/setting.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"xorm.io/builder"
)
// Setting is a key value store of user settings
type Setting struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"index unique(key_userid)"` // to load all of someone's settings
SettingKey string `xorm:"varchar(255) index unique(key_userid)"` // ensure key is always lowercase
SettingValue string `xorm:"text"`
}
// TableName sets the table name for the settings struct
func (s *Setting) TableName() string {
return "user_setting"
}
func init() {
db.RegisterModel(new(Setting))
}
// GetSettings returns specific settings from user
func GetSettings(uid int64, keys []string) (map[string]*Setting, error) {
settings := make([]*Setting, 0, len(keys))
if err := db.GetEngine(db.DefaultContext).
Where("user_id=?", uid).
And(builder.In("setting_key", keys)).
Find(&settings); err != nil {
return nil, err
}
settingsMap := make(map[string]*Setting)
for _, s := range settings {
settingsMap[s.SettingKey] = s
}
return settingsMap, nil
}
// GetUserAllSettings returns all settings from user
func GetUserAllSettings(uid int64) (map[string]*Setting, error) {
settings := make([]*Setting, 0, 5)
if err := db.GetEngine(db.DefaultContext).
Where("user_id=?", uid).
Find(&settings); err != nil {
return nil, err
}
settingsMap := make(map[string]*Setting)
for _, s := range settings {
settingsMap[s.SettingKey] = s
}
return settingsMap, nil
}
// DeleteSetting deletes a specific setting for a user
func DeleteSetting(setting *Setting) error {
_, err := db.GetEngine(db.DefaultContext).Delete(setting)
return err
}
// SetSetting updates a users' setting for a specific key
func SetSetting(setting *Setting) error {
if strings.ToLower(setting.SettingKey) != setting.SettingKey {
return fmt.Errorf("setting key should be lowercase")
}
return upsertSettingValue(setting.UserID, setting.SettingKey, setting.SettingValue)
}
func upsertSettingValue(userID int64, key string, value string) error {
return db.WithTx(func(ctx context.Context) error {
e := db.GetEngine(ctx)
// here we use a general method to do a safe upsert for different databases (and most transaction levels)
// 1. try to UPDATE the record and acquire the transaction write lock
// if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly
// if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist (b) value is not changed
// 2. do a SELECT to check if the row exists or not (we already have the transaction lock)
// 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe)
//
// to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1`
// to make sure the UPDATE always returns a non-zero value for existing (unchanged) records.
res, err := e.Exec("UPDATE user_setting SET setting_value=? WHERE setting_key=? AND user_id=?", value, key, userID)
if err != nil {
return err
}
rows, _ := res.RowsAffected()
if rows > 0 {
// the existing row is updated, so we can return
return nil
}
// in case the value isn't changed, update would return 0 rows changed, so we need this check
has, err := e.Exist(&Setting{UserID: userID, SettingKey: key})
if err != nil {
return err
}
if has {
return nil
}
// if no existing row, insert a new row
_, err = e.Insert(&Setting{UserID: userID, SettingKey: key, SettingValue: value})
return err
})
}

View File

@ -0,0 +1,51 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestSettings(t *testing.T) {
keyName := "test_user_setting"
assert.NoError(t, unittest.PrepareTestDatabase())
newSetting := &Setting{UserID: 99, SettingKey: keyName, SettingValue: "Gitea User Setting Test"}
// create setting
err := SetSetting(newSetting)
assert.NoError(t, err)
// test about saving unchanged values
err = SetSetting(newSetting)
assert.NoError(t, err)
// get specific setting
settings, err := GetSettings(99, []string{keyName})
assert.NoError(t, err)
assert.Len(t, settings, 1)
assert.EqualValues(t, newSetting.SettingValue, settings[keyName].SettingValue)
// updated setting
updatedSetting := &Setting{UserID: 99, SettingKey: keyName, SettingValue: "Updated"}
err = SetSetting(updatedSetting)
assert.NoError(t, err)
// get all settings
settings, err = GetUserAllSettings(99)
assert.NoError(t, err)
assert.Len(t, settings, 1)
assert.EqualValues(t, updatedSetting.SettingValue, settings[updatedSetting.SettingKey].SettingValue)
// delete setting
err = DeleteSetting(&Setting{UserID: 99, SettingKey: keyName})
assert.NoError(t, err)
settings, err = GetUserAllSettings(99)
assert.NoError(t, err)
assert.Len(t, settings, 0)
}