diff --git a/api/graphql/models/actions/search_actions.go b/api/graphql/models/actions/search_actions.go new file mode 100644 index 00000000..d55b811b --- /dev/null +++ b/api/graphql/models/actions/search_actions.go @@ -0,0 +1,73 @@ +package actions + +import ( + "github.com/photoview/photoview/api/graphql/models" + "github.com/pkg/errors" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func Search(db *gorm.DB, query string, userID int, _limitMedia *int, _limitAlbums *int) (*models.SearchResult, error) { + limitMedia := 10 + limitAlbums := 10 + + if _limitMedia != nil { + limitMedia = *_limitMedia + } + + if _limitAlbums != nil { + limitAlbums = *_limitAlbums + } + + wildQuery := "%" + query + "%" + + var media []*models.Media + + userSubquery := db.Table("user_albums").Where("user_id = ?", userID) + if db.Dialector.Name() == "postgres" { + userSubquery = userSubquery.Where("album_id = \"Album\".id") + } else { + userSubquery = userSubquery.Where("album_id = Album.id") + } + + err := db.Joins("Album"). + Where("EXISTS (?)", userSubquery). + Where("media.title LIKE ? OR media.path LIKE ?", wildQuery, wildQuery). + Clauses(clause.OrderBy{ + Expression: clause.Expr{ + SQL: "(CASE WHEN media.title LIKE ? THEN 2 WHEN media.path LIKE ? THEN 1 END) DESC", + Vars: []interface{}{wildQuery, wildQuery}, + WithoutParentheses: true}, + }). + Limit(limitMedia).Find(&media).Error + + if err != nil { + return nil, errors.Wrapf(err, "searching media") + } + + var albums []*models.Album + + err = db. + Where("EXISTS (?)", db.Table("user_albums").Where("user_id = ?", userID).Where("album_id = albums.id")). + Where("albums.title LIKE ? OR albums.path LIKE ?", wildQuery, wildQuery). + Clauses(clause.OrderBy{ + Expression: clause.Expr{ + SQL: "(CASE WHEN albums.title LIKE ? THEN 2 WHEN albums.path LIKE ? THEN 1 END) DESC", + Vars: []interface{}{wildQuery, wildQuery}, + WithoutParentheses: true}, + }). + Limit(limitAlbums). + Find(&albums).Error + + if err != nil { + return nil, errors.Wrapf(err, "searching albums") + } + + result := models.SearchResult{ + Query: query, + Media: media, + Albums: albums, + } + + return &result, nil +} diff --git a/api/graphql/models/actions/search_actions_test.go b/api/graphql/models/actions/search_actions_test.go new file mode 100644 index 00000000..73aaf994 --- /dev/null +++ b/api/graphql/models/actions/search_actions_test.go @@ -0,0 +1,102 @@ +package actions_test + +import ( + "fmt" + "testing" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/graphql/models/actions" + "github.com/photoview/photoview/api/test_utils" + "github.com/stretchr/testify/assert" +) + +func TestSearch(t *testing.T) { + db := test_utils.DatabaseTest(t) + + user, err := models.RegisterUser(db, "user", nil, true) + assert.NoError(t, err) + + rootAlbum := models.Album{ + Title: "root_album", + Path: "/media/", + } + + assert.NoError(t, db.Create(&rootAlbum).Error) + assert.NoError(t, db.Model(&rootAlbum).Association("Owners").Append(user)) + + type Result struct { + ID int + UserID int + AlbumID int + } + + mediaTitles := []string{ + "SOME_IMAGE.jpg", + "imageA.jpg", + "imageB.jpg", + "imageC.jpg", + "movie.mp4", + "person.png", + "123.png", + "ABC.gif", + "dog.mov", + "cat.mov", + "IMG_3255.JPG", + "IMG_5532.JPG", + "IMG_5533.JPG", + "IMG_5534.JPG", + "IMG_5535.JPG", + "IMG_5536.JPG", + } + + for _, mediaTitle := range mediaTitles { + image := models.Media{ + Title: mediaTitle, + Path: fmt.Sprintf("/media/%s", mediaTitle), + AlbumID: rootAlbum.ID, + } + assert.NoError(t, db.Create(&image).Error) + } + + type SearchTest = struct { + query string + userID int + limitMedia *int + limitAlbum *int + + expectedMediaCount int + expectedAlbumCount int + } + + searchTests := []SearchTest{ + { + query: "image", + userID: user.ID, + expectedMediaCount: 4, + expectedAlbumCount: 0, + }, + { + query: "g", + userID: user.ID, + expectedMediaCount: 10, + expectedAlbumCount: 0, + }, + { + query: "media", + userID: user.ID, + expectedMediaCount: 10, + expectedAlbumCount: 1, + }, + } + + for _, test := range searchTests { + t.Run(fmt.Sprintf("Search query: '%s'", test.query), func(t *testing.T) { + result, err := actions.Search(db, test.query, test.userID, test.limitMedia, test.limitAlbum) + assert.NoError(t, err) + + assert.Equal(t, result.Query, test.query) + assert.Len(t, result.Albums, test.expectedAlbumCount) + assert.Len(t, result.Media, test.expectedMediaCount) + }) + } +} diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go index 259d6b31..0a1490d4 100644 --- a/api/graphql/models/media.go +++ b/api/graphql/models/media.go @@ -71,7 +71,7 @@ type MediaURL struct { Model MediaID int `gorm:"not null;index"` Media *Media `gorm:"constraint:OnDelete:CASCADE;"` - MediaName string `gorm:"not null"` + MediaName string `gorm:"not null;index"` Width int `gorm:"not null"` Height int `gorm:"not null"` Purpose MediaPurpose `gorm:"not null;index"` diff --git a/api/graphql/resolvers/search.go b/api/graphql/resolvers/search.go index 07829e70..96b811a7 100644 --- a/api/graphql/resolvers/search.go +++ b/api/graphql/resolvers/search.go @@ -4,78 +4,16 @@ import ( "context" "github.com/photoview/photoview/api/graphql/auth" - "github.com/pkg/errors" - "gorm.io/gorm/clause" + "github.com/photoview/photoview/api/graphql/models/actions" "github.com/photoview/photoview/api/graphql/models" ) -func (r *Resolver) Search(ctx context.Context, query string, _limitMedia *int, _limitAlbums *int) (*models.SearchResult, error) { +func (r *Resolver) Search(ctx context.Context, query string, limitMedia *int, limitAlbums *int) (*models.SearchResult, error) { user := auth.UserFromContext(ctx) if user == nil { return nil, auth.ErrUnauthorized } - limitMedia := 10 - limitAlbums := 10 - - if _limitMedia != nil { - limitMedia = *_limitMedia - } - - if _limitAlbums != nil { - limitAlbums = *_limitAlbums - } - - wildQuery := "%" + query + "%" - - var media []*models.Media - - userSubquery := r.Database.Table("user_albums").Where("user_id = ?", user.ID) - if r.Database.Dialector.Name() == "postgres" { - userSubquery = userSubquery.Where("album_id = \"Album\".id") - } else { - userSubquery = userSubquery.Where("album_id = Album.id") - } - - err := r.Database.Joins("Album"). - Where("EXISTS (?)", userSubquery). - Where("media.title LIKE ? OR media.path LIKE ?", wildQuery, wildQuery). - Clauses(clause.OrderBy{ - Expression: clause.Expr{ - SQL: "(CASE WHEN media.title LIKE ? THEN 2 WHEN media.path LIKE ? THEN 1 END) DESC", - Vars: []interface{}{wildQuery, wildQuery}, - WithoutParentheses: true}, - }). - Limit(limitMedia).Find(&media).Error - - if err != nil { - return nil, errors.Wrapf(err, "searching media") - } - - var albums []*models.Album - - err = r.Database. - Where("EXISTS (?)", r.Database.Table("user_albums").Where("user_id = ?", user.ID).Where("album_id = albums.id")). - Where("albums.title LIKE ? OR albums.path LIKE ?", wildQuery, wildQuery). - Clauses(clause.OrderBy{ - Expression: clause.Expr{ - SQL: "(CASE WHEN albums.title LIKE ? THEN 2 WHEN albums.path LIKE ? THEN 1 END) DESC", - Vars: []interface{}{wildQuery, wildQuery}, - WithoutParentheses: true}, - }). - Limit(limitAlbums). - Find(&albums).Error - - if err != nil { - return nil, errors.Wrapf(err, "searching albums") - } - - result := models.SearchResult{ - Query: query, - Media: media, - Albums: albums, - } - - return &result, nil + return actions.Search(r.Database, query, user.ID, limitMedia, limitAlbums) }