feat(wip): leaderboard pagination (resolve #417) [ci-skip]

This commit is contained in:
Ferdinand Mütsch 2022-10-16 18:59:19 +02:00
parent 8a21be4306
commit 41f6db8f34
9 changed files with 85 additions and 18 deletions

View File

@ -34,6 +34,11 @@ type KeyedInterval struct {
Key *IntervalKey
}
type PageParams struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// CustomTime is a wrapper type around time.Time, mainly used for the purpose of transparently unmarshalling Python timestamps in the format <sec>.<nsec> (e.g. 1619335137.3324468)
type CustomTime time.Time
@ -99,3 +104,17 @@ func (j CustomTime) T() time.Time {
func (j CustomTime) Valid() bool {
return j.T().Unix() >= 0
}
func (p *PageParams) Limit() int {
if p.PageSize < 0 {
return 0
}
return p.PageSize
}
func (p *PageParams) Offset() int {
if p.PageSize <= 0 {
return 0
}
return (p.Page - 1) * p.PageSize
}

View File

@ -14,6 +14,7 @@ type LeaderboardViewModel struct {
TopKeys []string
UserLanguages map[string][]string
ApiKey string
PageParams *models.PageParams
Success string
Error string
}

View File

@ -33,13 +33,17 @@ func (r *LeaderboardRepository) CountAllByUser(userId string) (int64, error) {
return count, err
}
func (r *LeaderboardRepository) GetAllAggregatedByInterval(key *models.IntervalKey, by *uint8) ([]*models.LeaderboardItem, error) {
func (r *LeaderboardRepository) GetAllAggregatedByInterval(key *models.IntervalKey, by *uint8, limit, skip int) ([]*models.LeaderboardItem, error) {
// TODO: distinct by (user, key) to filter out potential duplicates ?
var items []*models.LeaderboardItem
q := r.db.
subq := r.db.
Table("leaderboard_items").
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
Where("\"interval\" in ?", *key)
q = utils.WhereNullable(q, "\"by\"", by)
subq = utils.WhereNullable(subq, "\"by\"", by)
q := r.db.Table("(?) as ranked", subq)
q = r.withPaging(q, limit, skip)
if err := q.Find(&items).Error; err != nil {
return nil, err
@ -47,13 +51,17 @@ func (r *LeaderboardRepository) GetAllAggregatedByInterval(key *models.IntervalK
return items, nil
}
func (r *LeaderboardRepository) GetAggregatedByUserAndInterval(userId string, key *models.IntervalKey, by *uint8) ([]*models.LeaderboardItem, error) {
func (r *LeaderboardRepository) GetAggregatedByUserAndInterval(userId string, key *models.IntervalKey, by *uint8, limit, skip int) ([]*models.LeaderboardItem, error) {
var items []*models.LeaderboardItem
q := r.db.
subq := r.db.
Table("leaderboard_items").
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
Where("user_id = ?", userId).
Where("\"interval\" in ?", *key)
q = utils.WhereNullable(q, "\"by\"", by)
subq = utils.WhereNullable(subq, "\"by\"", by)
q := r.db.Table("(?) as ranked", subq)
q = r.withPaging(q, limit, skip)
if err := q.Find(&items).Error; err != nil {
return nil, err
@ -79,3 +87,13 @@ func (r *LeaderboardRepository) DeleteByUserAndInterval(userId string, key *mode
}
return nil
}
func (r *LeaderboardRepository) withPaging(q *gorm.DB, limit, skip int) *gorm.DB {
if limit > 0 {
q = q.Where("\"rank\" <= ?", skip+limit)
}
if skip > 0 {
q = q.Where("\"rank\" > ?", skip)
}
return q
}

View File

@ -92,6 +92,6 @@ type ILeaderboardRepository interface {
CountAllByUser(string) (int64, error)
DeleteByUser(string) error
DeleteByUserAndInterval(string, *models.IntervalKey) error
GetAllAggregatedByInterval(*models.IntervalKey, *uint8) ([]*models.LeaderboardItem, error)
GetAggregatedByUserAndInterval(string, *models.IntervalKey, *uint8) ([]*models.LeaderboardItem, error)
GetAllAggregatedByInterval(*models.IntervalKey, *uint8, int, int) ([]*models.LeaderboardItem, error)
GetAggregatedByUserAndInterval(string, *models.IntervalKey, *uint8, int, int) ([]*models.LeaderboardItem, error)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/models/view"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"net/http"
"strings"
)
@ -56,6 +57,7 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi
user := middlewares.GetPrincipal(r)
byParam := strings.ToLower(r.URL.Query().Get("by"))
keyParam := strings.ToLower(r.URL.Query().Get("key"))
pageParams := utils.ParsePageParams(r)
var err error
var leaderboard models.Leaderboard
@ -63,14 +65,14 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi
var topKeys []string
if byParam == "" {
leaderboard, err = h.leaderboardService.GetByInterval(models.IntervalPast7Days, true)
leaderboard, err = h.leaderboardService.GetByInterval(models.IntervalPast7Days, pageParams, true)
if err != nil {
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
return &view.LeaderboardViewModel{Error: criticalError}
}
} else {
if by, ok := allowedAggregations[byParam]; ok {
leaderboard, err = h.leaderboardService.GetAggregatedByInterval(models.IntervalPast7Days, &by, true)
leaderboard, err = h.leaderboardService.GetAggregatedByInterval(models.IntervalPast7Days, &by, pageParams, true)
if err != nil {
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
return &view.LeaderboardViewModel{Error: criticalError}
@ -110,6 +112,7 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi
UserLanguages: userLanguages,
TopKeys: topKeys,
ApiKey: apiKey,
PageParams: pageParams,
Success: r.URL.Query().Get("success"),
Error: r.URL.Query().Get("error"),
}

View File

@ -10,6 +10,7 @@ import (
"github.com/muety/wakapi/utils"
"github.com/patrickmn/go-cache"
"reflect"
"strconv"
"strings"
"time"
)
@ -125,18 +126,18 @@ func (srv *LeaderboardService) ExistsAnyByUser(userId string) (bool, error) {
return count > 0, err
}
func (srv *LeaderboardService) GetByInterval(interval *models.IntervalKey, resolveUsers bool) (models.Leaderboard, error) {
return srv.GetAggregatedByInterval(interval, nil, resolveUsers)
func (srv *LeaderboardService) GetByInterval(interval *models.IntervalKey, pageParams *models.PageParams, resolveUsers bool) (models.Leaderboard, error) {
return srv.GetAggregatedByInterval(interval, nil, pageParams, resolveUsers)
}
func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.IntervalKey, by *uint8, resolveUsers bool) (models.Leaderboard, error) {
func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.IntervalKey, by *uint8, pageParams *models.PageParams, resolveUsers bool) (models.Leaderboard, error) {
// check cache
cacheKey := srv.getHash(interval, by)
cacheKey := srv.getHash(interval, by, pageParams)
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
return cacheResult.([]*models.LeaderboardItem), nil
}
items, err := srv.repository.GetAllAggregatedByInterval(interval, by)
items, err := srv.repository.GetAllAggregatedByInterval(interval, by, pageParams.Limit(), pageParams.Offset())
if err != nil {
return nil, err
}
@ -208,10 +209,11 @@ func (srv *LeaderboardService) GenerateAggregatedByUser(user *models.User, inter
return items, nil
}
func (srv *LeaderboardService) getHash(interval *models.IntervalKey, by *uint8) string {
func (srv *LeaderboardService) getHash(interval *models.IntervalKey, by *uint8, pageParams *models.PageParams) string {
k := strings.Join(*interval, "__")
if by != nil && !reflect.ValueOf(by).IsNil() {
k += "__" + models.GetEntityColumn(*by)
}
k = "__" + strconv.Itoa(pageParams.Page) + "__" + strconv.Itoa(pageParams.PageSize)
return k
}

View File

@ -101,8 +101,8 @@ type ILeaderboardService interface {
ScheduleDefault()
Run([]*models.User, *models.IntervalKey, []uint8) error
ExistsAnyByUser(string) (bool, error)
GetByInterval(*models.IntervalKey, bool) (models.Leaderboard, error)
GetAggregatedByInterval(*models.IntervalKey, *uint8, bool) (models.Leaderboard, error)
GetByInterval(*models.IntervalKey, *models.PageParams, bool) (models.Leaderboard, error)
GetAggregatedByInterval(*models.IntervalKey, *uint8, *models.PageParams, bool) (models.Leaderboard, error)
GenerateByUser(*models.User, *models.IntervalKey) (*models.LeaderboardItem, error)
GenerateAggregatedByUser(*models.User, *models.IntervalKey, uint8) ([]*models.LeaderboardItem, error)
}

View File

@ -39,3 +39,13 @@ func WhereNullable(query *gorm.DB, col string, val any) *gorm.DB {
}
return query.Where(fmt.Sprintf("%s = ?", col), val)
}
func WithPaging(query *gorm.DB, limit, skip int) *gorm.DB {
if limit >= 0 {
query = query.Limit(limit)
}
if skip >= 0 {
query = query.Offset(skip)
}
return query
}

View File

@ -3,6 +3,7 @@ package utils
import (
"encoding/json"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"net/http"
"regexp"
"strconv"
@ -42,3 +43,16 @@ func IsNoCache(r *http.Request, cacheTtl time.Duration) bool {
}
return false
}
func ParsePageParams(r *http.Request) *models.PageParams {
pageParams := &models.PageParams{}
page := r.URL.Query().Get("page")
pageSize := r.URL.Query().Get("page_size")
if p, err := strconv.Atoi(page); err == nil {
pageParams.Page = p
}
if p, err := strconv.Atoi(pageSize); err == nil && pageParams.Page > 0 {
pageParams.PageSize = p
}
return pageParams
}