mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
feat(wip): leaderboard pagination (resolve #417) [ci-skip]
This commit is contained in:
parent
8a21be4306
commit
41f6db8f34
@ -34,6 +34,11 @@ type KeyedInterval struct {
|
|||||||
Key *IntervalKey
|
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)
|
// 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
|
type CustomTime time.Time
|
||||||
|
|
||||||
@ -99,3 +104,17 @@ func (j CustomTime) T() time.Time {
|
|||||||
func (j CustomTime) Valid() bool {
|
func (j CustomTime) Valid() bool {
|
||||||
return j.T().Unix() >= 0
|
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
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@ type LeaderboardViewModel struct {
|
|||||||
TopKeys []string
|
TopKeys []string
|
||||||
UserLanguages map[string][]string
|
UserLanguages map[string][]string
|
||||||
ApiKey string
|
ApiKey string
|
||||||
|
PageParams *models.PageParams
|
||||||
Success string
|
Success string
|
||||||
Error string
|
Error string
|
||||||
}
|
}
|
||||||
|
@ -33,13 +33,17 @@ func (r *LeaderboardRepository) CountAllByUser(userId string) (int64, error) {
|
|||||||
return count, err
|
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 ?
|
// TODO: distinct by (user, key) to filter out potential duplicates ?
|
||||||
var items []*models.LeaderboardItem
|
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\"").
|
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
|
||||||
Where("\"interval\" in ?", *key)
|
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 {
|
if err := q.Find(&items).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -47,13 +51,17 @@ func (r *LeaderboardRepository) GetAllAggregatedByInterval(key *models.IntervalK
|
|||||||
return items, nil
|
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
|
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\"").
|
Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\"").
|
||||||
Where("user_id = ?", userId).
|
Where("user_id = ?", userId).
|
||||||
Where("\"interval\" in ?", *key)
|
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 {
|
if err := q.Find(&items).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -79,3 +87,13 @@ func (r *LeaderboardRepository) DeleteByUserAndInterval(userId string, key *mode
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -92,6 +92,6 @@ type ILeaderboardRepository interface {
|
|||||||
CountAllByUser(string) (int64, error)
|
CountAllByUser(string) (int64, error)
|
||||||
DeleteByUser(string) error
|
DeleteByUser(string) error
|
||||||
DeleteByUserAndInterval(string, *models.IntervalKey) error
|
DeleteByUserAndInterval(string, *models.IntervalKey) error
|
||||||
GetAllAggregatedByInterval(*models.IntervalKey, *uint8) ([]*models.LeaderboardItem, error)
|
GetAllAggregatedByInterval(*models.IntervalKey, *uint8, int, int) ([]*models.LeaderboardItem, error)
|
||||||
GetAggregatedByUserAndInterval(string, *models.IntervalKey, *uint8) ([]*models.LeaderboardItem, error)
|
GetAggregatedByUserAndInterval(string, *models.IntervalKey, *uint8, int, int) ([]*models.LeaderboardItem, error)
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/muety/wakapi/models/view"
|
"github.com/muety/wakapi/models/view"
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
|
"github.com/muety/wakapi/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -56,6 +57,7 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi
|
|||||||
user := middlewares.GetPrincipal(r)
|
user := middlewares.GetPrincipal(r)
|
||||||
byParam := strings.ToLower(r.URL.Query().Get("by"))
|
byParam := strings.ToLower(r.URL.Query().Get("by"))
|
||||||
keyParam := strings.ToLower(r.URL.Query().Get("key"))
|
keyParam := strings.ToLower(r.URL.Query().Get("key"))
|
||||||
|
pageParams := utils.ParsePageParams(r)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var leaderboard models.Leaderboard
|
var leaderboard models.Leaderboard
|
||||||
@ -63,14 +65,14 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi
|
|||||||
var topKeys []string
|
var topKeys []string
|
||||||
|
|
||||||
if byParam == "" {
|
if byParam == "" {
|
||||||
leaderboard, err = h.leaderboardService.GetByInterval(models.IntervalPast7Days, true)
|
leaderboard, err = h.leaderboardService.GetByInterval(models.IntervalPast7Days, pageParams, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||||
return &view.LeaderboardViewModel{Error: criticalError}
|
return &view.LeaderboardViewModel{Error: criticalError}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if by, ok := allowedAggregations[byParam]; ok {
|
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 {
|
if err != nil {
|
||||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||||
return &view.LeaderboardViewModel{Error: criticalError}
|
return &view.LeaderboardViewModel{Error: criticalError}
|
||||||
@ -110,6 +112,7 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi
|
|||||||
UserLanguages: userLanguages,
|
UserLanguages: userLanguages,
|
||||||
TopKeys: topKeys,
|
TopKeys: topKeys,
|
||||||
ApiKey: apiKey,
|
ApiKey: apiKey,
|
||||||
|
PageParams: pageParams,
|
||||||
Success: r.URL.Query().Get("success"),
|
Success: r.URL.Query().Get("success"),
|
||||||
Error: r.URL.Query().Get("error"),
|
Error: r.URL.Query().Get("error"),
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -125,18 +126,18 @@ func (srv *LeaderboardService) ExistsAnyByUser(userId string) (bool, error) {
|
|||||||
return count > 0, err
|
return count > 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *LeaderboardService) GetByInterval(interval *models.IntervalKey, resolveUsers bool) (models.Leaderboard, error) {
|
func (srv *LeaderboardService) GetByInterval(interval *models.IntervalKey, pageParams *models.PageParams, resolveUsers bool) (models.Leaderboard, error) {
|
||||||
return srv.GetAggregatedByInterval(interval, nil, resolveUsers)
|
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
|
// check cache
|
||||||
cacheKey := srv.getHash(interval, by)
|
cacheKey := srv.getHash(interval, by, pageParams)
|
||||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
||||||
return cacheResult.([]*models.LeaderboardItem), nil
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -208,10 +209,11 @@ func (srv *LeaderboardService) GenerateAggregatedByUser(user *models.User, inter
|
|||||||
return items, nil
|
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, "__")
|
k := strings.Join(*interval, "__")
|
||||||
if by != nil && !reflect.ValueOf(by).IsNil() {
|
if by != nil && !reflect.ValueOf(by).IsNil() {
|
||||||
k += "__" + models.GetEntityColumn(*by)
|
k += "__" + models.GetEntityColumn(*by)
|
||||||
}
|
}
|
||||||
|
k = "__" + strconv.Itoa(pageParams.Page) + "__" + strconv.Itoa(pageParams.PageSize)
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
@ -101,8 +101,8 @@ type ILeaderboardService interface {
|
|||||||
ScheduleDefault()
|
ScheduleDefault()
|
||||||
Run([]*models.User, *models.IntervalKey, []uint8) error
|
Run([]*models.User, *models.IntervalKey, []uint8) error
|
||||||
ExistsAnyByUser(string) (bool, error)
|
ExistsAnyByUser(string) (bool, error)
|
||||||
GetByInterval(*models.IntervalKey, bool) (models.Leaderboard, error)
|
GetByInterval(*models.IntervalKey, *models.PageParams, bool) (models.Leaderboard, error)
|
||||||
GetAggregatedByInterval(*models.IntervalKey, *uint8, bool) (models.Leaderboard, error)
|
GetAggregatedByInterval(*models.IntervalKey, *uint8, *models.PageParams, bool) (models.Leaderboard, error)
|
||||||
GenerateByUser(*models.User, *models.IntervalKey) (*models.LeaderboardItem, error)
|
GenerateByUser(*models.User, *models.IntervalKey) (*models.LeaderboardItem, error)
|
||||||
GenerateAggregatedByUser(*models.User, *models.IntervalKey, uint8) ([]*models.LeaderboardItem, error)
|
GenerateAggregatedByUser(*models.User, *models.IntervalKey, uint8) ([]*models.LeaderboardItem, error)
|
||||||
}
|
}
|
||||||
|
10
utils/db.go
10
utils/db.go
@ -39,3 +39,13 @@ func WhereNullable(query *gorm.DB, col string, val any) *gorm.DB {
|
|||||||
}
|
}
|
||||||
return query.Where(fmt.Sprintf("%s = ?", col), val)
|
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
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/muety/wakapi/config"
|
"github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -42,3 +43,16 @@ func IsNoCache(r *http.Request, cacheTtl time.Duration) bool {
|
|||||||
}
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user