diff --git a/main.go b/main.go index b0048e1..52cf55e 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,7 @@ func setupRouter(env *Env, spotifyID string, spotifySecret string) *gin.Engine { // See: https://pkg.go.dev/github.com/gin-gonic/gin#Engine.SetTrustedProxies r.SetTrustedProxies(nil) r.Use(spotifyAuth(spotifyID, spotifySecret)) + r.Use(paginator()) r.GET("/alive", env.alive) r.GET("/artists/:artistID", env.getArtistByID) r.GET("/artists", env.getArtistByName) diff --git a/routes.go b/routes.go index 53072fb..58236b6 100644 --- a/routes.go +++ b/routes.go @@ -6,8 +6,53 @@ import ( "gorm.io/gorm/clause" "log/slog" "net/http" + "strconv" ) +// Pagination helper middleware +// This just checks and inserts some default pagination values +func paginator() gin.HandlerFunc { + return func(c *gin.Context) { + queryPage := c.DefaultQuery("page", "1") + queryPageSize := c.DefaultQuery("page_size", "10") + + // add some defaults here if user gives us empty values + if queryPage == ""{ + queryPage = "1" + } + if queryPageSize == "" { + queryPageSize = "10" + } + + page, err := strconv.Atoi(queryPage) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"Error": "Failed to convert page parameter to number"}) + c.Abort() + return + } + + pageSize, err := strconv.Atoi(queryPageSize) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"Error": "Failed to convert page_size parameter to number"}) + c.Abort() + return + } + + if page < 1 || pageSize < 1 { + page, pageSize = 1, 10 + } + + // Calculate the correct SQL offset for this page + offset := (page - 1) * pageSize + c.Set("page", page) + c.Set("page_size", pageSize) + c.Set("page_offset", offset) + + // Process request + c.Next() + } +} + func (env *Env) alive(c *gin.Context) { c.String(http.StatusOK, "yes!") } @@ -70,13 +115,15 @@ func (env *Env) getArtistByName(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"Error": "name parameter was not supplied"}) return } + pageSize := c.GetInt("page_size") + pageOffset := c.GetInt("page_offset") // Lookup this name in the DB and return any ArtistProfile objects // NOTE: This is case insensitive for ascii on sqlite // NOTE: However unicode is treated case sensitive due to very difficult conversions for some languages/characters // NOTE: In future better DBs with unicode support such as postgres should be used var artistProfiles []ArtistProfile - dbResult := env.db.Where("name LIKE ?", fmt.Sprintf("%%%s%%", artistName)).Preload("Genres").Find(&artistProfiles) + dbResult := env.db.Where("name LIKE ?", fmt.Sprintf("%%%s%%", artistName)).Preload("Genres").Offset(pageOffset).Limit(pageSize).Find(&artistProfiles) if dbResult.Error != nil { slog.Error("[GOMUSIC] Failed to query local database for artist name", "Name", artistName) c.JSON(http.StatusInternalServerError, gin.H{"Error": "Failed to lookup name"}) @@ -88,7 +135,10 @@ func (env *Env) getArtistByName(c *gin.Context) { func (env *Env) getGenres(c *gin.Context) { var genres []Genre - dbResult := env.db.Find(&genres) + pageSize := c.GetInt("page_size") + pageOffset := c.GetInt("page_offset") + + dbResult := env.db.Offset(pageOffset).Limit(pageSize).Find(&genres) if dbResult.Error != nil { slog.Error("[GOMUSIC] Failed to query local database for genre data") c.JSON(http.StatusInternalServerError, gin.H{"Error": "Failed to query database for genre data"}) diff --git a/routes_test.go b/routes_test.go index e584c81..58aa2e7 100644 --- a/routes_test.go +++ b/routes_test.go @@ -97,7 +97,7 @@ func TestGetArtistByName(t *testing.T) { assert.Fail(t, fmt.Sprintf("Could not validate and parse JSON response: %s", err.Error())) } - // only response should be kanye + // only response should be aesop assert.Equal(t, "Aesop Rock", resp[0].Name) assert.Equal(t, "2fSaE6BXtQy0x7R7v9IOmZ", resp[0].SpotifyID) }