feat: api - GET /api/v2/versions/{versionCode}/servers/{serverKey}/players - add more sort options (#7)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#7
This commit is contained in:
Dawid Wysokiński 2024-02-27 07:09:24 +00:00
parent 70deae8696
commit d12bcf5e4e
17 changed files with 971 additions and 312 deletions

View File

@ -180,6 +180,7 @@ paths:
- $ref: "#/components/parameters/CursorQueryParam" - $ref: "#/components/parameters/CursorQueryParam"
- $ref: "#/components/parameters/LimitQueryParam" - $ref: "#/components/parameters/LimitQueryParam"
- $ref: "#/components/parameters/PlayerDeletedQueryParam" - $ref: "#/components/parameters/PlayerDeletedQueryParam"
- $ref: "#/components/parameters/PlayerSortQueryParam"
responses: responses:
200: 200:
$ref: "#/components/responses/ListPlayersResponse" $ref: "#/components/responses/ListPlayersResponse"
@ -1100,6 +1101,26 @@ components:
schema: schema:
type: boolean type: boolean
required: false required: false
PlayerSortQueryParam:
name: sort
in: query
description: Order matters!
schema:
type: array
items:
type: string
enum:
- odScoreAtt:ASC
- odScoreAtt:DESC
- odScoreDef:ASC
- odScoreDef:DESC
- odScoreTotal:ASC
- odScoreTotal:DESC
- points:ASC
- points:DESC
- deletedAt:ASC
- deletedAt:DESC
maxItems: 2
VersionCodePathParam: VersionCodePathParam:
in: path in: path
name: versionCode name: versionCode

View File

@ -3,6 +3,5 @@ package adapter
import "errors" import "errors"
var ( var (
errUnsupportedSortValue = errors.New("unsupported sort value") errInvalidSortValue = errors.New("invalid sort value")
errSortNoUniqueField = errors.New("no unique field used for sorting")
) )

View File

@ -1,6 +1,11 @@
package adapter package adapter
import "github.com/uptrace/bun" import (
"errors"
"slices"
"github.com/uptrace/bun"
)
func appendODSetClauses(q *bun.InsertQuery) *bun.InsertQuery { func appendODSetClauses(q *bun.InsertQuery) *bun.InsertQuery {
return q.Set("rank_att = EXCLUDED.rank_att"). return q.Set("rank_att = EXCLUDED.rank_att").
@ -23,3 +28,90 @@ func separateListResultAndNext[T any](res []T, limit int) ([]T, T) {
return res, next return res, next
} }
type sortDirection uint8
var errInvalidSortDirection = errors.New("invalid sort direction")
const (
sortDirectionASC sortDirection = iota + 1
sortDirectionDESC
)
func (sd sortDirection) String() string {
switch sd {
case sortDirectionASC:
return "ASC"
case sortDirectionDESC:
return "DESC"
default:
return "unknown"
}
}
type cursorPaginationApplierDataElement struct {
unique bool
column string
direction sortDirection
value any
}
type cursorPaginationApplier struct {
data []cursorPaginationApplierDataElement
}
var errCursorNoUniqueFieldUsedForSorting = errors.New("cursor - no unique field used for sorting")
func (a cursorPaginationApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if len(a.data) == 0 {
return q
}
uniqueIndex := slices.IndexFunc(a.data, func(element cursorPaginationApplierDataElement) bool {
return element.unique
})
if uniqueIndex < 0 {
return q.Err(errCursorNoUniqueFieldUsedForSorting)
}
return q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
dataLen := len(a.data)
// based on https://github.com/prisma/prisma/issues/19159#issuecomment-1713389245
return q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
for i := 0; i < dataLen; i++ {
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
current := a.data[i]
for j := 0; j < i; j++ {
prev := a.data[j]
q = q.Where("? = ?", bun.Safe(prev.column), prev.value)
}
column := bun.Safe(current.column)
greaterSymbol := bun.Safe(">")
lessSymbol := bun.Safe("<")
if i == dataLen-1 {
greaterSymbol = ">="
lessSymbol = "<="
}
switch current.direction {
case sortDirectionASC:
q = q.Where("? ? ?", column, greaterSymbol, current.value)
case sortDirectionDESC:
q = q.Where("? ? ?", column, lessSymbol, current.value)
default:
return q.Err(errInvalidSortDirection)
}
return q
})
}
return q
})
})
}

View File

@ -92,7 +92,7 @@ func (a listEnnoblementsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuer
case domain.EnnoblementSortServerKeyDESC: case domain.EnnoblementSortServerKeyDESC:
q = q.Order("ennoblement.server_key DESC") q = q.Order("ennoblement.server_key DESC")
default: default:
return q.Err(errUnsupportedSortValue) return q.Err(errInvalidSortValue)
} }
} }

View File

@ -162,8 +162,28 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Order("player.server_key ASC") q = q.Order("player.server_key ASC")
case domain.PlayerSortServerKeyDESC: case domain.PlayerSortServerKeyDESC:
q = q.Order("player.server_key DESC") q = q.Order("player.server_key DESC")
case domain.PlayerSortODScoreAttASC:
q = q.Order("player.score_att ASC")
case domain.PlayerSortODScoreAttDESC:
q = q.Order("player.score_att DESC")
case domain.PlayerSortODScoreDefASC:
q = q.Order("player.score_def ASC")
case domain.PlayerSortODScoreDefDESC:
q = q.Order("player.score_def DESC")
case domain.PlayerSortODScoreTotalASC:
q = q.Order("player.score_total ASC")
case domain.PlayerSortODScoreTotalDESC:
q = q.Order("player.score_total DESC")
case domain.PlayerSortPointsASC:
q = q.Order("player.points ASC")
case domain.PlayerSortPointsDESC:
q = q.Order("player.points DESC")
case domain.PlayerSortDeletedAtASC:
q = q.OrderExpr("COALESCE(player.deleted_at, ?) ASC", time.Time{})
case domain.PlayerSortDeletedAtDESC:
q = q.OrderExpr("COALESCE(player.deleted_at, ?) DESC", time.Time{})
default: default:
return q.Err(errUnsupportedSortValue) return q.Err(errInvalidSortValue)
} }
} }
@ -172,85 +192,97 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
//nolint:gocyclo //nolint:gocyclo
func (a listPlayersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery { func (a listPlayersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery {
if a.params.Cursor().IsZero() { cursor := a.params.Cursor()
if cursor.IsZero() {
return q return q
} }
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { sort := a.params.Sort()
cursor := a.params.Cursor() cursorApplier := cursorPaginationApplier{
cursorID := cursor.ID() data: make([]cursorPaginationApplierDataElement, 0, len(sort)),
cursorServerKey := cursor.ServerKey() }
sort := a.params.Sort() for _, s := range sort {
sortLen := len(sort) var el cursorPaginationApplierDataElement
// based on https://github.com/prisma/prisma/issues/19159#issuecomment-1713389245 switch s {
case domain.PlayerSortIDASC:
switch { el.value = cursor.ID()
case sortLen == 1: el.unique = true
switch sort[0] { el.column = "player.id"
case domain.PlayerSortIDASC: el.direction = sortDirectionASC
q = q.Where("player.id >= ?", cursorID) case domain.PlayerSortIDDESC:
case domain.PlayerSortIDDESC: el.value = cursor.ID()
q = q.Where("player.id <= ?", cursorID) el.unique = true
case domain.PlayerSortServerKeyASC, el.column = "player.id"
domain.PlayerSortServerKeyDESC: el.direction = sortDirectionDESC
return q.Err(errSortNoUniqueField) case domain.PlayerSortServerKeyASC:
default: el.value = cursor.ServerKey()
return q.Err(errUnsupportedSortValue) el.unique = false
} el.column = "player.server_key"
case sortLen > 1: el.direction = sortDirectionASC
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { case domain.PlayerSortServerKeyDESC:
for i := 0; i < sortLen; i++ { el.value = cursor.ServerKey()
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { el.unique = false
current := sort[i] el.column = "player.server_key"
el.direction = sortDirectionDESC
for j := 0; j < i; j++ { case domain.PlayerSortODScoreAttASC:
s := sort[j] el.value = cursor.ODScoreAtt()
el.unique = false
switch s { el.column = "player.score_att"
case domain.PlayerSortIDASC, el.direction = sortDirectionASC
domain.PlayerSortIDDESC: case domain.PlayerSortODScoreAttDESC:
q = q.Where("player.id = ?", cursorID) el.value = cursor.ODScoreAtt()
case domain.PlayerSortServerKeyASC, el.unique = false
domain.PlayerSortServerKeyDESC: el.column = "player.score_att"
q = q.Where("player.server_key = ?", cursorServerKey) el.direction = sortDirectionDESC
default: case domain.PlayerSortODScoreDefASC:
return q.Err(errUnsupportedSortValue) el.value = cursor.ODScoreDef()
} el.unique = false
} el.column = "player.score_def"
el.direction = sortDirectionASC
greaterSymbol := bun.Safe(">") case domain.PlayerSortODScoreDefDESC:
lessSymbol := bun.Safe("<") el.value = cursor.ODScoreDef()
el.unique = false
if i == sortLen-1 { el.column = "player.score_def"
greaterSymbol = ">=" el.direction = sortDirectionDESC
lessSymbol = "<=" case domain.PlayerSortODScoreTotalASC:
} el.value = cursor.ODScoreTotal()
el.unique = false
switch current { el.column = "player.score_total"
case domain.PlayerSortIDASC: el.direction = sortDirectionASC
q = q.Where("player.id ? ?", greaterSymbol, cursorID) case domain.PlayerSortODScoreTotalDESC:
case domain.PlayerSortIDDESC: el.value = cursor.ODScoreTotal()
q = q.Where("player.id ? ?", lessSymbol, cursorID) el.unique = false
case domain.PlayerSortServerKeyASC: el.column = "player.score_total"
q = q.Where("player.server_key ? ?", greaterSymbol, cursorServerKey) el.direction = sortDirectionDESC
case domain.PlayerSortServerKeyDESC: case domain.PlayerSortPointsASC:
q = q.Where("player.server_key ? ?", lessSymbol, cursorServerKey) el.value = cursor.Points()
default: el.unique = false
return q.Err(errUnsupportedSortValue) el.column = "player.points"
} el.direction = sortDirectionASC
case domain.PlayerSortPointsDESC:
return q el.value = cursor.Points()
}) el.unique = false
} el.column = "player.points"
el.direction = sortDirectionDESC
return q case domain.PlayerSortDeletedAtASC:
}) el.value = cursor.DeletedAt()
el.unique = false
el.column = "COALESCE(player.deleted_at, '0001-01-01 00:00:00+00:00')"
el.direction = sortDirectionASC
case domain.PlayerSortDeletedAtDESC:
el.value = cursor.DeletedAt()
el.unique = false
el.column = "COALESCE(player.deleted_at, '0001-01-01 00:00:00+00:00')"
el.direction = sortDirectionDESC
default:
return q.Err(errInvalidSortValue)
} }
return q cursorApplier.data = append(cursorApplier.data, el)
}) }
return q return q.Apply(cursorApplier.apply)
} }

View File

@ -97,7 +97,7 @@ func (a listPlayerSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQ
case domain.PlayerSnapshotSortServerKeyDESC: case domain.PlayerSnapshotSortServerKeyDESC:
q = q.Order("ps.server_key DESC") q = q.Order("ps.server_key DESC")
default: default:
return q.Err(errUnsupportedSortValue) return q.Err(errInvalidSortValue)
} }
} }

View File

@ -228,92 +228,55 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
case domain.ServerSortOpenDESC: case domain.ServerSortOpenDESC:
q = q.Order("server.open DESC") q = q.Order("server.open DESC")
default: default:
return q.Err(errUnsupportedSortValue) return q.Err(errInvalidSortValue)
} }
} }
return q.Apply(a.applyCursor).Limit(a.params.Limit() + 1) return q.Apply(a.applyCursor).Limit(a.params.Limit() + 1)
} }
//nolint:gocyclo
func (a listServersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery { func (a listServersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery {
if a.params.Cursor().IsZero() { cursor := a.params.Cursor()
if cursor.IsZero() {
return q return q
} }
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { sort := a.params.Sort()
cursorKey := a.params.Cursor().Key() cursorApplier := cursorPaginationApplier{
cursorOpen := a.params.Cursor().Open() data: make([]cursorPaginationApplierDataElement, 0, len(sort)),
}
sort := a.params.Sort() for _, s := range sort {
sortLen := len(sort) var el cursorPaginationApplierDataElement
// based on https://github.com/prisma/prisma/issues/19159#issuecomment-1713389245 switch s {
case domain.ServerSortKeyASC:
switch { el.value = cursor.Key()
case sortLen == 1: el.unique = true
switch sort[0] { el.column = "server.key"
case domain.ServerSortKeyASC: el.direction = sortDirectionASC
q = q.Where("server.key >= ?", cursorKey) case domain.ServerSortKeyDESC:
case domain.ServerSortKeyDESC: el.value = cursor.Key()
q = q.Where("server.key <= ?", cursorKey) el.unique = true
case domain.ServerSortOpenASC, domain.ServerSortOpenDESC: el.column = "server.key"
return q.Err(errSortNoUniqueField) el.direction = sortDirectionDESC
default: case domain.ServerSortOpenASC:
return q.Err(errUnsupportedSortValue) el.value = cursor.Open()
} el.unique = false
case sortLen > 1: el.column = "server.open"
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { el.direction = sortDirectionASC
for i := 0; i < sortLen; i++ { case domain.ServerSortOpenDESC:
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { el.value = cursor.Open()
current := sort[i] el.unique = false
el.column = "server.open"
for j := 0; j < i; j++ { el.direction = sortDirectionDESC
s := sort[j] default:
return q.Err(errInvalidSortValue)
switch s {
case domain.ServerSortKeyASC,
domain.ServerSortKeyDESC:
q = q.Where("server.key = ?", cursorKey)
case domain.ServerSortOpenASC,
domain.ServerSortOpenDESC:
q = q.Where("server.open = ?", cursorOpen)
default:
return q.Err(errUnsupportedSortValue)
}
}
greaterSymbol := bun.Safe(">")
lessSymbol := bun.Safe("<")
if i == sortLen-1 {
greaterSymbol = ">="
lessSymbol = "<="
}
switch current {
case domain.ServerSortKeyASC:
q = q.Where("server.key ? ?", greaterSymbol, cursorKey)
case domain.ServerSortKeyDESC:
q = q.Where("server.key ? ?", lessSymbol, cursorKey)
case domain.ServerSortOpenASC:
q = q.Where("server.open ? ?", greaterSymbol, cursorOpen)
case domain.ServerSortOpenDESC:
q = q.Where("server.open ? ?", lessSymbol, cursorOpen)
default:
return q.Err(errUnsupportedSortValue)
}
return q
})
}
return q
})
} }
return q cursorApplier.data = append(cursorApplier.data, el)
}) }
return q return q.Apply(cursorApplier.apply)
} }

View File

@ -117,6 +117,10 @@ func (repo *TribeBunRepository) List(
) (domain.ListTribesResult, error) { ) (domain.ListTribesResult, error) {
var tribes bunmodel.Tribes var tribes bunmodel.Tribes
fmt.Println(repo.db.NewSelect().
Model(&tribes).
Apply(listTribesParamsApplier{params: params}.apply).String())
if err := repo.db.NewSelect(). if err := repo.db.NewSelect().
Model(&tribes). Model(&tribes).
Apply(listTribesParamsApplier{params: params}.apply). Apply(listTribesParamsApplier{params: params}.apply).
@ -212,7 +216,7 @@ func (a listTribesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
case domain.TribeSortDeletedAtDESC: case domain.TribeSortDeletedAtDESC:
q = q.OrderExpr("COALESCE(tribe.deleted_at, ?) DESC", time.Time{}) q = q.OrderExpr("COALESCE(tribe.deleted_at, ?) DESC", time.Time{})
default: default:
return q.Err(errUnsupportedSortValue) return q.Err(errInvalidSortValue)
} }
} }
@ -221,145 +225,107 @@ func (a listTribesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
//nolint:gocyclo //nolint:gocyclo
func (a listTribesParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery { func (a listTribesParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery {
if a.params.Cursor().IsZero() { cursor := a.params.Cursor()
if cursor.IsZero() {
return q return q
} }
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { sort := a.params.Sort()
cursor := a.params.Cursor() cursorApplier := cursorPaginationApplier{
cursorID := cursor.ID() data: make([]cursorPaginationApplierDataElement, 0, len(sort)),
cursorServerKey := cursor.ServerKey() }
cursorODScoreAtt := cursor.ODScoreAtt()
cursorODScoreDef := cursor.ODScoreDef()
cursorODScoreTotal := cursor.ODScoreTotal()
cursorPoints := cursor.Points()
cursorDominance := cursor.Dominance()
cursorDeletedAt := cursor.DeletedAt()
sort := a.params.Sort() for _, s := range sort {
sortLen := len(sort) var el cursorPaginationApplierDataElement
// based on https://github.com/prisma/prisma/issues/19159#issuecomment-1713389245 switch s {
case domain.TribeSortIDASC:
switch { el.value = cursor.ID()
case sortLen == 1: el.unique = true
switch sort[0] { el.column = "tribe.id"
case domain.TribeSortIDASC: el.direction = sortDirectionASC
q = q.Where("tribe.id >= ?", cursorID) case domain.TribeSortIDDESC:
case domain.TribeSortIDDESC: el.value = cursor.ID()
q = q.Where("tribe.id <= ?", cursorID) el.unique = true
case domain.TribeSortServerKeyASC, el.column = "tribe.id"
domain.TribeSortServerKeyDESC, el.direction = sortDirectionDESC
domain.TribeSortODScoreAttASC, case domain.TribeSortServerKeyASC:
domain.TribeSortODScoreAttDESC, el.value = cursor.ServerKey()
domain.TribeSortODScoreDefASC, el.unique = false
domain.TribeSortODScoreDefDESC, el.column = "tribe.server_key"
domain.TribeSortODScoreTotalASC, el.direction = sortDirectionASC
domain.TribeSortODScoreTotalDESC, case domain.TribeSortServerKeyDESC:
domain.TribeSortPointsASC, el.value = cursor.ServerKey()
domain.TribeSortPointsDESC, el.unique = false
domain.TribeSortDominanceASC, el.column = "tribe.server_key"
domain.TribeSortDominanceDESC, el.direction = sortDirectionDESC
domain.TribeSortDeletedAtASC, case domain.TribeSortODScoreAttASC:
domain.TribeSortDeletedAtDESC: el.value = cursor.ODScoreAtt()
return q.Err(errSortNoUniqueField) el.unique = false
default: el.column = "tribe.score_att"
return q.Err(errUnsupportedSortValue) el.direction = sortDirectionASC
} case domain.TribeSortODScoreAttDESC:
case sortLen > 1: el.value = cursor.ODScoreAtt()
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { el.unique = false
for i := 0; i < sortLen; i++ { el.column = "tribe.score_att"
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { el.direction = sortDirectionDESC
current := sort[i] case domain.TribeSortODScoreDefASC:
el.value = cursor.ODScoreDef()
for j := 0; j < i; j++ { el.unique = false
s := sort[j] el.column = "tribe.score_def"
el.direction = sortDirectionASC
switch s { case domain.TribeSortODScoreDefDESC:
case domain.TribeSortIDASC, el.value = cursor.ODScoreDef()
domain.TribeSortIDDESC: el.unique = false
q = q.Where("tribe.id = ?", cursorID) el.column = "tribe.score_def"
case domain.TribeSortServerKeyASC, el.direction = sortDirectionDESC
domain.TribeSortServerKeyDESC: case domain.TribeSortODScoreTotalASC:
q = q.Where("tribe.server_key = ?", cursorServerKey) el.value = cursor.ODScoreTotal()
case domain.TribeSortODScoreAttASC, el.unique = false
domain.TribeSortODScoreAttDESC: el.column = "tribe.score_total"
q = q.Where("tribe.score_att = ?", cursorODScoreAtt) el.direction = sortDirectionASC
case domain.TribeSortODScoreDefASC, case domain.TribeSortODScoreTotalDESC:
domain.TribeSortODScoreDefDESC: el.value = cursor.ODScoreTotal()
q = q.Where("tribe.score_def = ?", cursorODScoreDef) el.unique = false
case domain.TribeSortODScoreTotalASC, el.column = "tribe.score_total"
domain.TribeSortODScoreTotalDESC: el.direction = sortDirectionDESC
q = q.Where("tribe.score_total = ?", cursorODScoreTotal) case domain.TribeSortPointsASC:
case domain.TribeSortPointsASC, el.value = cursor.Points()
domain.TribeSortPointsDESC: el.unique = false
q = q.Where("tribe.points = ?", cursorPoints) el.column = "tribe.points"
case domain.TribeSortDominanceASC, el.direction = sortDirectionASC
domain.TribeSortDominanceDESC: case domain.TribeSortPointsDESC:
q = q.Where("tribe.dominance = ?", cursorDominance) el.value = cursor.Points()
case domain.TribeSortDeletedAtASC, el.unique = false
domain.TribeSortDeletedAtDESC: el.column = "tribe.points"
q = q.Where("COALESCE(tribe.deleted_at, ?) = ?", time.Time{}, cursorDeletedAt) el.direction = sortDirectionDESC
default: case domain.TribeSortDominanceASC:
return q.Err(errUnsupportedSortValue) el.value = cursor.Dominance()
} el.unique = false
} el.column = "tribe.dominance"
el.direction = sortDirectionASC
greaterSymbol := bun.Safe(">") case domain.TribeSortDominanceDESC:
lessSymbol := bun.Safe("<") el.value = cursor.Dominance()
el.unique = false
if i == sortLen-1 { el.column = "tribe.dominance"
greaterSymbol = ">=" el.direction = sortDirectionDESC
lessSymbol = "<=" case domain.TribeSortDeletedAtASC:
} el.value = cursor.DeletedAt()
el.unique = false
switch current { el.column = "COALESCE(tribe.deleted_at, '0001-01-01 00:00:00+00:00')"
case domain.TribeSortIDASC: el.direction = sortDirectionASC
q = q.Where("tribe.id ? ?", greaterSymbol, cursorID) case domain.TribeSortDeletedAtDESC:
case domain.TribeSortIDDESC: el.value = cursor.DeletedAt()
q = q.Where("tribe.id ? ?", lessSymbol, cursorID) el.unique = false
case domain.TribeSortServerKeyASC: el.column = "COALESCE(tribe.deleted_at, '0001-01-01 00:00:00+00:00')"
q = q.Where("tribe.server_key ? ?", greaterSymbol, cursorServerKey) el.direction = sortDirectionDESC
case domain.TribeSortServerKeyDESC: default:
q = q.Where("tribe.server_key ? ?", lessSymbol, cursorServerKey) return q.Err(errInvalidSortValue)
case domain.TribeSortODScoreAttASC:
q = q.Where("tribe.score_att ? ?", greaterSymbol, cursorODScoreAtt)
case domain.TribeSortODScoreAttDESC:
q = q.Where("tribe.score_att ? ?", lessSymbol, cursorODScoreAtt)
case domain.TribeSortODScoreDefASC:
q = q.Where("tribe.score_def ? ?", greaterSymbol, cursorODScoreDef)
case domain.TribeSortODScoreDefDESC:
q = q.Where("tribe.score_def ? ?", lessSymbol, cursorODScoreDef)
case domain.TribeSortODScoreTotalASC:
q = q.Where("tribe.score_total ? ?", greaterSymbol, cursorODScoreTotal)
case domain.TribeSortODScoreTotalDESC:
q = q.Where("tribe.score_total ? ?", lessSymbol, cursorODScoreTotal)
case domain.TribeSortPointsASC:
q = q.Where("tribe.points ? ?", greaterSymbol, cursorPoints)
case domain.TribeSortPointsDESC:
q = q.Where("tribe.points ? ?", lessSymbol, cursorPoints)
case domain.TribeSortDominanceASC:
q = q.Where("tribe.dominance ? ?", greaterSymbol, cursorDominance)
case domain.TribeSortDominanceDESC:
q = q.Where("tribe.dominance ? ?", lessSymbol, cursorDominance)
case domain.TribeSortDeletedAtASC:
q = q.Where("COALESCE(tribe.deleted_at, ?) ? ?", time.Time{}, greaterSymbol, cursorDeletedAt)
case domain.TribeSortDeletedAtDESC:
q = q.Where("COALESCE(tribe.deleted_at, ?) ? ?", time.Time{}, lessSymbol, cursorDeletedAt)
default:
return q.Err(errUnsupportedSortValue)
}
return q
})
}
return q
})
} }
return q cursorApplier.data = append(cursorApplier.data, el)
}) }
return q return q.Apply(cursorApplier.apply)
} }

View File

@ -90,7 +90,7 @@ func (a listTribeChangesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuer
case domain.TribeChangeSortServerKeyDESC: case domain.TribeChangeSortServerKeyDESC:
q = q.Order("tc.server_key DESC") q = q.Order("tc.server_key DESC")
default: default:
return q.Err(errUnsupportedSortValue) return q.Err(errInvalidSortValue)
} }
} }

View File

@ -96,7 +96,7 @@ func (a listTribeSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQu
case domain.TribeSnapshotSortServerKeyDESC: case domain.TribeSnapshotSortServerKeyDESC:
q = q.Order("ts.server_key DESC") q = q.Order("ts.server_key DESC")
default: default:
return q.Err(errUnsupportedSortValue) return q.Err(errInvalidSortValue)
} }
} }

View File

@ -50,26 +50,51 @@ func (a listVersionsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
} }
for _, s := range a.params.Sort() { for _, s := range a.params.Sort() {
cursorZero := a.params.Cursor().IsZero()
cursorCode := a.params.Cursor().Code()
switch s { switch s {
case domain.VersionSortCodeASC: case domain.VersionSortCodeASC:
if !cursorZero {
q = q.Where("version.code >= ?", cursorCode)
}
q = q.Order("version.code ASC") q = q.Order("version.code ASC")
case domain.VersionSortCodeDESC: case domain.VersionSortCodeDESC:
if !cursorZero {
q = q.Where("version.code <= ?", cursorCode)
}
q = q.Order("version.code DESC") q = q.Order("version.code DESC")
default: default:
return q.Err(errUnsupportedSortValue) return q.Err(errInvalidSortValue)
} }
} }
return q.Limit(a.params.Limit() + 1) return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
}
func (a listVersionsParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery {
cursor := a.params.Cursor()
if cursor.IsZero() {
return q
}
sort := a.params.Sort()
cursorApplier := cursorPaginationApplier{
data: make([]cursorPaginationApplierDataElement, 0, len(sort)),
}
for _, s := range sort {
var el cursorPaginationApplierDataElement
switch s {
case domain.VersionSortCodeASC:
el.value = cursor.Code()
el.unique = true
el.column = "version.code"
el.direction = sortDirectionASC
case domain.VersionSortCodeDESC:
el.value = cursor.Code()
el.unique = true
el.column = "version.code"
el.direction = sortDirectionDESC
default:
return q.Err(errInvalidSortValue)
}
cursorApplier.data = append(cursorApplier.data, el)
}
return q.Apply(cursorApplier.apply)
} }

View File

@ -135,7 +135,7 @@ func (a listVillagesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
case domain.VillageSortServerKeyDESC: case domain.VillageSortServerKeyDESC:
q = q.Order("village.server_key DESC") q = q.Order("village.server_key DESC")
default: default:
return q.Err(errUnsupportedSortValue) return q.Err(errInvalidSortValue)
} }
} }

View File

@ -159,6 +159,180 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, err) require.NoError(t, err)
}, },
}, },
{
name: "OK: sort=[scoreAtt DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortODScoreAttDESC,
domain.PlayerSortServerKeyASC,
domain.PlayerSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
cmp.Compare(a.OD().ScoreAtt(), b.OD().ScoreAtt())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[scoreDef DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortODScoreDefDESC,
domain.PlayerSortServerKeyASC,
domain.PlayerSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
cmp.Compare(a.OD().ScoreDef(), b.OD().ScoreDef())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[scoreTotal DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortODScoreTotalDESC,
domain.PlayerSortServerKeyASC,
domain.PlayerSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
cmp.Compare(a.OD().ScoreTotal(), b.OD().ScoreTotal())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[points DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortPointsDESC,
domain.PlayerSortServerKeyASC,
domain.PlayerSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
cmp.Compare(a.Points(), b.Points())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[deletedAt ASC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortDeletedAtASC,
domain.PlayerSortServerKeyASC,
domain.PlayerSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
a.DeletedAt().Compare(b.DeletedAt()),
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[deletedAt DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortDeletedAtDESC,
domain.PlayerSortServerKeyASC,
domain.PlayerSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
players := res.Players()
assert.NotEmpty(t, len(players))
assert.True(t, slices.IsSortedFunc(players, func(a, b domain.Player) int {
return cmp.Or(
a.DeletedAt().Compare(b.DeletedAt())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{ {
name: "OK: ids serverKeys", name: "OK: ids serverKeys",
params: func(t *testing.T) domain.ListPlayersParams { params: func(t *testing.T) domain.ListPlayersParams {

View File

@ -6,6 +6,7 @@ import (
"math" "math"
"net/url" "net/url"
"slices" "slices"
"strings"
"time" "time"
) )
@ -371,8 +372,48 @@ const (
PlayerSortIDDESC PlayerSortIDDESC
PlayerSortServerKeyASC PlayerSortServerKeyASC
PlayerSortServerKeyDESC PlayerSortServerKeyDESC
PlayerSortODScoreAttASC
PlayerSortODScoreAttDESC
PlayerSortODScoreDefASC
PlayerSortODScoreDefDESC
PlayerSortODScoreTotalASC
PlayerSortODScoreTotalDESC
PlayerSortPointsASC
PlayerSortPointsDESC
PlayerSortDeletedAtASC
PlayerSortDeletedAtDESC
) )
//nolint:gocyclo
func newPlayerSortFromString(s string) (PlayerSort, error) {
switch strings.ToLower(s) {
case "odscoreatt:asc":
return PlayerSortODScoreAttASC, nil
case "odscoreatt:desc":
return PlayerSortODScoreAttDESC, nil
case "odscoredef:asc":
return PlayerSortODScoreDefASC, nil
case "odscoredef:desc":
return PlayerSortODScoreDefDESC, nil
case "odscoretotal:asc":
return PlayerSortODScoreTotalASC, nil
case "odscoretotal:desc":
return PlayerSortODScoreTotalDESC, nil
case "points:asc":
return PlayerSortPointsASC, nil
case "points:desc":
return PlayerSortPointsDESC, nil
case "deletedat:asc":
return PlayerSortDeletedAtASC, nil
case "deletedat:desc":
return PlayerSortDeletedAtDESC, nil
default:
return 0, UnsupportedSortStringError{
Sort: s,
}
}
}
type PlayerCursor struct { type PlayerCursor struct {
id int id int
serverKey string serverKey string
@ -628,7 +669,7 @@ func (params *ListPlayersParams) Sort() []PlayerSort {
const ( const (
playerSortMinLength = 1 playerSortMinLength = 1
playerSortMaxLength = 2 playerSortMaxLength = 3
) )
func (params *ListPlayersParams) SetSort(sort []PlayerSort) error { func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
@ -645,6 +686,31 @@ func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
return nil return nil
} }
func (params *ListPlayersParams) PrependSortString(sort []string) error {
if err := validateSliceLen(sort, playerSortMinLength, max(playerSortMaxLength-len(params.sort), 0)); err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "sort",
Err: err,
}
}
for i := len(sort) - 1; i >= 0; i-- {
converted, err := newPlayerSortFromString(sort[i])
if err != nil {
return SliceElementValidationError{
Model: listPlayersParamsModelName,
Field: "sort",
Index: i,
Err: err,
}
}
params.sort = append([]PlayerSort{converted}, params.sort...)
}
return nil
}
func (params *ListPlayersParams) Cursor() PlayerCursor { func (params *ListPlayersParams) Cursor() PlayerCursor {
return params.cursor return params.cursor
} }

View File

@ -490,6 +490,7 @@ func TestListPlayersParams_SetSort(t *testing.T) {
name: "OK", name: "OK",
args: args{ args: args{
sort: []domain.PlayerSort{ sort: []domain.PlayerSort{
domain.PlayerSortPointsASC,
domain.PlayerSortIDASC, domain.PlayerSortIDASC,
domain.PlayerSortServerKeyASC, domain.PlayerSortServerKeyASC,
}, },
@ -505,18 +506,19 @@ func TestListPlayersParams_SetSort(t *testing.T) {
Field: "sort", Field: "sort",
Err: domain.LenOutOfRangeError{ Err: domain.LenOutOfRangeError{
Min: 1, Min: 1,
Max: 2, Max: 3,
Current: 0, Current: 0,
}, },
}, },
}, },
{ {
name: "ERR: len(sort) > 2", name: "ERR: len(sort) > 3",
args: args{ args: args{
sort: []domain.PlayerSort{ sort: []domain.PlayerSort{
domain.PlayerSortDeletedAtDESC,
domain.PlayerSortPointsASC,
domain.PlayerSortIDASC, domain.PlayerSortIDASC,
domain.PlayerSortServerKeyASC, domain.PlayerSortServerKeyASC,
domain.PlayerSortServerKeyDESC,
}, },
}, },
expectedErr: domain.ValidationError{ expectedErr: domain.ValidationError{
@ -524,8 +526,8 @@ func TestListPlayersParams_SetSort(t *testing.T) {
Field: "sort", Field: "sort",
Err: domain.LenOutOfRangeError{ Err: domain.LenOutOfRangeError{
Min: 1, Min: 1,
Max: 2, Max: 3,
Current: 3, Current: 4,
}, },
}, },
}, },
@ -546,6 +548,181 @@ func TestListPlayersParams_SetSort(t *testing.T) {
} }
} }
func TestListPlayersParams_PrependSortString(t *testing.T) {
t.Parallel()
defaultNewParams := func(t *testing.T) domain.ListPlayersParams {
t.Helper()
return domain.ListPlayersParams{}
}
type args struct {
sort []string
}
tests := []struct {
name string
newParams func(t *testing.T) domain.ListPlayersParams
args args
expectedSort []domain.PlayerSort
expectedErr error
}{
{
name: "OK: [odScoreAtt:ASC, odScoreDef:ASC, odScoreTotal:ASC]",
args: args{
sort: []string{
"odScoreAtt:ASC",
"odScoreDef:ASC",
"odScoreTotal:ASC",
},
},
expectedSort: []domain.PlayerSort{
domain.PlayerSortODScoreAttASC,
domain.PlayerSortODScoreDefASC,
domain.PlayerSortODScoreTotalASC,
},
},
{
name: "OK: [odScoreAtt:DESC, odScoreDef:DESC, odScoreTotal:DESC]",
args: args{
sort: []string{
"odScoreAtt:DESC",
"odScoreDef:DESC",
"odScoreTotal:DESC",
},
},
expectedSort: []domain.PlayerSort{
domain.PlayerSortODScoreAttDESC,
domain.PlayerSortODScoreDefDESC,
domain.PlayerSortODScoreTotalDESC,
},
},
{
name: "OK: [points:ASC, deletedAt:ASC]",
args: args{
sort: []string{
"points:ASC",
"deletedAt:ASC",
},
},
expectedSort: []domain.PlayerSort{
domain.PlayerSortPointsASC,
domain.PlayerSortDeletedAtASC,
},
},
{
name: "OK: [points:DESC, deletedAt:DESC]",
args: args{
sort: []string{
"points:DESC",
"deletedAt:DESC",
},
},
expectedSort: []domain.PlayerSort{
domain.PlayerSortPointsDESC,
domain.PlayerSortDeletedAtDESC,
},
},
{
name: "ERR: len(sort) < 1",
args: args{
sort: nil,
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 3,
Current: 0,
},
},
},
{
name: "ERR: len(sort) > 3",
args: args{
sort: []string{
"odScoreAtt:ASC",
"odScoreDef:ASC",
"odScoreTotal:ASC",
"points:ASC",
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 3,
Current: 4,
},
},
},
{
name: "ERR: custom params + len(sort) > 2",
newParams: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}))
return params
},
args: args{
sort: []string{
"odScoreAtt:ASC",
"odScoreDef:ASC",
"odScoreTotal:ASC",
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 2,
Current: 3,
},
},
},
{
name: "ERR: unsupported sort string",
newParams: defaultNewParams,
args: args{
sort: []string{
"odScoreAtt:ASC",
"odScoreDef:ASC",
"odScoreTotal:",
},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListPlayersParams",
Field: "sort",
Index: 2,
Err: domain.UnsupportedSortStringError{
Sort: "odScoreTotal:",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newParams := defaultNewParams
if tt.newParams != nil {
newParams = tt.newParams
}
params := newParams(t)
require.ErrorIs(t, params.PrependSortString(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedSort, params.Sort())
})
}
}
func TestListPlayersParams_SetEncodedCursor(t *testing.T) { func TestListPlayersParams_SetEncodedCursor(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -8,6 +8,7 @@ import (
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel" "gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
) )
//nolint:gocyclo
func (h *apiHTTPHandler) ListPlayers( func (h *apiHTTPHandler) ListPlayers(
w http.ResponseWriter, w http.ResponseWriter,
r *http.Request, r *http.Request,
@ -22,6 +23,13 @@ func (h *apiHTTPHandler) ListPlayers(
return return
} }
if params.Sort != nil {
if err := domainParams.PrependSortString(*params.Sort); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
}
if err := domainParams.SetServerKeys([]string{serverKey}); err != nil { if err := domainParams.SetServerKeys([]string{serverKey}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err) h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return return

View File

@ -9,6 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain" "gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest" "gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
@ -111,6 +112,70 @@ func TestListPlayers(t *testing.T) {
assert.Len(t, body.Data, limit) assert.Len(t, body.Data, limit)
}, },
}, },
{
name: "OK: sort=[deletedAt:DESC,points:ASC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "deletedAt:DESC")
q.Add("sort", "points:ASC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Player) int {
var aDeletedAt time.Time
if a.DeletedAt != nil {
aDeletedAt = *a.DeletedAt
}
var bDeletedAt time.Time
if b.DeletedAt != nil {
bDeletedAt = *b.DeletedAt
}
return cmp.Or(
aDeletedAt.Compare(bDeletedAt)*-1,
cmp.Compare(a.Points, b.Points),
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "OK: sort=[odScoreAtt:DESC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("sort", "odScoreAtt:DESC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Player) int {
return cmp.Or(
cmp.Compare(a.OpponentsDefeated.ScoreAtt, b.OpponentsDefeated.ScoreAtt)*-1,
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{ {
name: "OK: deleted=false", name: "OK: deleted=false",
reqModifier: func(t *testing.T, req *http.Request) { reqModifier: func(t *testing.T, req *http.Request) {
@ -127,8 +192,8 @@ func TestListPlayers(t *testing.T) {
// body // body
body := decodeJSON[apimodel.ListPlayersResponse](t, resp.Body) body := decodeJSON[apimodel.ListPlayersResponse](t, resp.Body)
assert.NotZero(t, body.Data) assert.NotZero(t, body.Data)
for _, tr := range body.Data { for _, p := range body.Data {
assert.Nil(t, tr.DeletedAt) assert.Nil(t, p.DeletedAt)
} }
}, },
}, },
@ -148,8 +213,8 @@ func TestListPlayers(t *testing.T) {
// body // body
body := decodeJSON[apimodel.ListPlayersResponse](t, resp.Body) body := decodeJSON[apimodel.ListPlayersResponse](t, resp.Body)
assert.NotZero(t, body.Data) assert.NotZero(t, body.Data)
for _, tr := range body.Data { for _, p := range body.Data {
assert.NotNil(t, tr.DeletedAt) assert.NotNil(t, p.DeletedAt)
} }
}, },
}, },
@ -354,6 +419,77 @@ func TestListPlayers(t *testing.T) {
}, body) }, body)
}, },
}, },
{
name: "ERR: len(sort) > 2",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "odScoreAtt:DESC")
q.Add("sort", "odScoreAtt:ASC")
q.Add("sort", "points:ASC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 2,
Current: len(req.URL.Query()["sort"]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "sort"},
},
},
}, body)
},
},
{
name: "ERR: invalid sort",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "odScoreAtt:ASC")
q.Add("sort", "test:DESC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.UnsupportedSortStringError{
Sort: req.URL.Query()["sort"][1],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"sort": domainErr.Sort,
},
Path: []string{"$query", "sort", "1"},
},
},
}, body)
},
},
{ {
name: "ERR: deleted is not a valid boolean", name: "ERR: deleted is not a valid boolean",
reqModifier: func(t *testing.T, req *http.Request) { reqModifier: func(t *testing.T, req *http.Request) {