mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-27 21:57:17 +01:00
feat(database): enhance user retrieval with library associations
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
40bbae16c4
commit
a736d79985
2 changed files with 167 additions and 12 deletions
|
|
@ -3,6 +3,7 @@ package persistence
|
|||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
|
@ -17,6 +18,7 @@ import (
|
|||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
"github.com/navidrome/navidrome/utils"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
|
|
@ -24,6 +26,26 @@ type userRepository struct {
|
|||
sqlRepository
|
||||
}
|
||||
|
||||
type dbUser struct {
|
||||
*model.User `structs:",flatten"`
|
||||
LibrariesJSON string `structs:"-" json:"-"`
|
||||
}
|
||||
|
||||
func (u *dbUser) PostScan() error {
|
||||
if u.LibrariesJSON != "" {
|
||||
if err := json.Unmarshal([]byte(u.LibrariesJSON), &u.User.Libraries); err != nil {
|
||||
return fmt.Errorf("parsing user libraries from db: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbUsers []dbUser
|
||||
|
||||
func (us dbUsers) toModels() model.Users {
|
||||
return slice.Map(us, func(u dbUser) model.User { return *u.User })
|
||||
}
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
encKey []byte
|
||||
|
|
@ -33,6 +55,7 @@ func NewUserRepository(ctx context.Context, db dbx.Builder) model.UserRepository
|
|||
r := &userRepository{}
|
||||
r.ctx = ctx
|
||||
r.db = db
|
||||
r.tableName = "user"
|
||||
r.registerModel(&model.User{}, map[string]filterFunc{
|
||||
"password": invalidFilter(ctx),
|
||||
})
|
||||
|
|
@ -42,28 +65,48 @@ func NewUserRepository(ctx context.Context, db dbx.Builder) model.UserRepository
|
|||
return r
|
||||
}
|
||||
|
||||
// selectUserWithLibraries returns a SelectBuilder that includes library information
|
||||
func (r *userRepository) selectUserWithLibraries(options ...model.QueryOptions) SelectBuilder {
|
||||
return r.newSelect(options...).
|
||||
Columns(`user.*`,
|
||||
`COALESCE(json_group_array(json_object(
|
||||
'id', library.id,
|
||||
'name', library.name,
|
||||
'path', library.path,
|
||||
'remote_path', library.remote_path,
|
||||
'last_scan_at', library.last_scan_at,
|
||||
'last_scan_started_at', library.last_scan_started_at,
|
||||
'full_scan_in_progress', library.full_scan_in_progress,
|
||||
'updated_at', library.updated_at,
|
||||
'created_at', library.created_at
|
||||
)) FILTER (WHERE library.id IS NOT NULL), '[]') AS libraries_json`).
|
||||
LeftJoin("user_library ul ON user.id = ul.user_id").
|
||||
LeftJoin("library ON ul.library_id = library.id").
|
||||
GroupBy("user.id")
|
||||
}
|
||||
|
||||
func (r *userRepository) CountAll(qo ...model.QueryOptions) (int64, error) {
|
||||
return r.count(Select(), qo...)
|
||||
}
|
||||
|
||||
func (r *userRepository) Get(id string) (*model.User, error) {
|
||||
sel := r.newSelect().Columns("*").Where(Eq{"id": id})
|
||||
var res model.User
|
||||
sel := r.selectUserWithLibraries().Where(Eq{"user.id": id})
|
||||
var res dbUser
|
||||
err := r.queryOne(sel, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
return res.User, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) GetAll(options ...model.QueryOptions) (model.Users, error) {
|
||||
sel := r.newSelect(options...).Columns("*")
|
||||
res := model.Users{}
|
||||
sel := r.selectUserWithLibraries(options...)
|
||||
var res dbUsers
|
||||
err := r.queryAll(sel, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
return res.toModels(), nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Put(u *model.User) error {
|
||||
|
|
@ -110,23 +153,23 @@ func (r *userRepository) Put(u *model.User) error {
|
|||
}
|
||||
|
||||
func (r *userRepository) FindFirstAdmin() (*model.User, error) {
|
||||
sel := r.newSelect(model.QueryOptions{Sort: "updated_at", Max: 1}).Columns("*").Where(Eq{"is_admin": true})
|
||||
var usr model.User
|
||||
sel := r.selectUserWithLibraries(model.QueryOptions{Sort: "updated_at", Max: 1}).Where(Eq{"user.is_admin": true})
|
||||
var usr dbUser
|
||||
err := r.queryOne(sel, &usr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &usr, nil
|
||||
return usr.User, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByUsername(username string) (*model.User, error) {
|
||||
sel := r.newSelect().Columns("*").Where(Expr("user_name = ? COLLATE NOCASE", username))
|
||||
var usr model.User
|
||||
sel := r.selectUserWithLibraries().Where(Expr("user.user_name = ? COLLATE NOCASE", username))
|
||||
var usr dbUser
|
||||
err := r.queryOne(sel, &usr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &usr, nil
|
||||
return usr.User, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByUsernameWithPassword(username string) (*model.User, error) {
|
||||
|
|
|
|||
|
|
@ -492,4 +492,116 @@ var _ = Describe("UserRepository", func() {
|
|||
Expect(libraries).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Libraries Field Population", func() {
|
||||
var (
|
||||
libRepo model.LibraryRepository
|
||||
library1 model.Library
|
||||
library2 model.Library
|
||||
testUser model.User
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
libRepo = NewLibraryRepository(log.NewContext(context.TODO()), GetDBXBuilder())
|
||||
library1 = model.Library{ID: 301, Name: "Field Test Library 1", Path: "/field/test/path1"}
|
||||
library2 = model.Library{ID: 302, Name: "Field Test Library 2", Path: "/field/test/path2"}
|
||||
|
||||
// Create test libraries
|
||||
Expect(libRepo.Put(&library1)).To(BeNil())
|
||||
Expect(libRepo.Put(&library2)).To(BeNil())
|
||||
|
||||
// Create test user
|
||||
testUser = model.User{
|
||||
ID: "field-test-user",
|
||||
UserName: "fieldtestuser",
|
||||
Name: "Field Test User",
|
||||
Email: "fieldtest@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: false,
|
||||
}
|
||||
Expect(repo.Put(&testUser)).To(BeNil())
|
||||
|
||||
// Assign libraries to user
|
||||
Expect(repo.SetUserLibraries(testUser.ID, []int{library1.ID, library2.ID})).To(BeNil())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// Clean up test libraries and their associations
|
||||
_ = libRepo.(*libraryRepository).delete(squirrel.Eq{"id": []int{301, 302}})
|
||||
_ = repo.(*userRepository).delete(squirrel.Eq{"id": testUser.ID})
|
||||
|
||||
// Clean up user-library associations for these test libraries
|
||||
_, _ = repo.(*userRepository).executeSQL(squirrel.Delete("user_library").Where(squirrel.Eq{"library_id": []int{301, 302}}))
|
||||
})
|
||||
|
||||
It("populates Libraries field when getting a single user", func() {
|
||||
user, err := repo.Get(testUser.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(user.Libraries).To(HaveLen(2))
|
||||
|
||||
libIDs := []int{user.Libraries[0].ID, user.Libraries[1].ID}
|
||||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID))
|
||||
|
||||
// Check that library details are properly populated
|
||||
for _, lib := range user.Libraries {
|
||||
if lib.ID == library1.ID {
|
||||
Expect(lib.Name).To(Equal("Field Test Library 1"))
|
||||
Expect(lib.Path).To(Equal("/field/test/path1"))
|
||||
} else if lib.ID == library2.ID {
|
||||
Expect(lib.Name).To(Equal("Field Test Library 2"))
|
||||
Expect(lib.Path).To(Equal("/field/test/path2"))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
It("populates Libraries field when getting all users", func() {
|
||||
users, err := repo.(*userRepository).GetAll()
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Find our test user in the results
|
||||
var foundUser *model.User
|
||||
for i := range users {
|
||||
if users[i].ID == testUser.ID {
|
||||
foundUser = &users[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Expect(foundUser).ToNot(BeNil())
|
||||
Expect(foundUser.Libraries).To(HaveLen(2))
|
||||
|
||||
libIDs := []int{foundUser.Libraries[0].ID, foundUser.Libraries[1].ID}
|
||||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID))
|
||||
})
|
||||
|
||||
It("populates Libraries field when finding user by username", func() {
|
||||
user, err := repo.FindByUsername(testUser.UserName)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(user.Libraries).To(HaveLen(2))
|
||||
|
||||
libIDs := []int{user.Libraries[0].ID, user.Libraries[1].ID}
|
||||
Expect(libIDs).To(ContainElements(library1.ID, library2.ID))
|
||||
})
|
||||
|
||||
It("returns empty Libraries array when user has no library associations", func() {
|
||||
// Create a user with no library associations
|
||||
userWithoutLibs := model.User{
|
||||
ID: "no-libs-user",
|
||||
UserName: "nolibsuser",
|
||||
Name: "No Libs User",
|
||||
Email: "nolibs@example.com",
|
||||
NewPassword: "password",
|
||||
IsAdmin: false,
|
||||
}
|
||||
Expect(repo.Put(&userWithoutLibs)).To(BeNil())
|
||||
defer func() {
|
||||
_ = repo.(*userRepository).delete(squirrel.Eq{"id": userWithoutLibs.ID})
|
||||
}()
|
||||
|
||||
user, err := repo.Get(userWithoutLibs.ID)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(user.Libraries).ToNot(BeNil())
|
||||
Expect(user.Libraries).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue