-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add user settings key/value DB table (#16834)
- Loading branch information
1 parent
a159c31
commit 499b05d
Showing
8 changed files
with
198 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters