refactor: village - cursor pagination (#16)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#16
This commit is contained in:
Dawid Wysokiński 2024-03-05 06:14:41 +00:00
parent e3bb2eb5c4
commit 847cf220da
16 changed files with 667 additions and 277 deletions

View File

@ -76,17 +76,25 @@ func (repo *VillageBunRepository) CreateOrUpdate(ctx context.Context, params ...
return nil
}
func (repo *VillageBunRepository) List(ctx context.Context, params domain.ListVillagesParams) (domain.Villages, error) {
func (repo *VillageBunRepository) List(
ctx context.Context,
params domain.ListVillagesParams,
) (domain.ListVillagesResult, error) {
var villages bunmodel.Villages
if err := repo.db.NewSelect().
Model(&villages).
Apply(listVillagesParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select villages from the db: %w", err)
return domain.ListVillagesResult{}, fmt.Errorf("couldn't select villages from the db: %w", err)
}
return villages.ToDomain()
converted, err := villages.ToDomain()
if err != nil {
return domain.ListVillagesResult{}, err
}
return domain.NewListVillagesResult(separateListResultAndNext(converted, params.Limit()))
}
func (repo *VillageBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
@ -116,10 +124,6 @@ func (a listVillagesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Where("village.id IN (?)", bun.In(ids))
}
if idGT := a.params.IDGT(); idGT.Valid {
q = q.Where("village.id > ?", idGT.V)
}
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("village.server_key IN (?)", bun.In(serverKeys))
}
@ -139,5 +143,49 @@ func (a listVillagesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
}
}
return q.Limit(a.params.Limit()).Offset(a.params.Offset())
return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
}
func (a listVillagesParamsApplier) 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.VillageSortIDASC:
el.value = cursor.ID()
el.unique = true
el.column = "village.id"
el.direction = sortDirectionASC
case domain.VillageSortIDDESC:
el.value = cursor.ID()
el.unique = true
el.column = "village.id"
el.direction = sortDirectionDESC
case domain.VillageSortServerKeyASC:
el.value = cursor.ServerKey()
el.column = "village.server_key"
el.direction = sortDirectionASC
case domain.VillageSortServerKeyDESC:
el.value = cursor.ServerKey()
el.column = "village.server_key"
el.direction = sortDirectionDESC
default:
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)
}
return q.Apply(cursorApplier.apply)
}

View File

@ -371,7 +371,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
res, err := repos.player.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Players()))
require.NotEmpty(t, res.Players())
randPlayer := res.Players()[0]
require.NoError(t, params.SetIDs([]int{randPlayer.ID()}))
@ -406,7 +406,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
res, err := repos.player.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Players()))
require.NotEmpty(t, res.Players())
randPlayer := res.Players()[0]
require.NoError(t, params.SetNames([]string{randPlayer.Name()}))
@ -544,7 +544,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
players := res.Players()
assert.NotEmpty(t, len(players))
for _, p := range res.Players() {
for _, p := range players {
assert.GreaterOrEqual(t, p.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, p.ServerKey()))
}
@ -590,7 +590,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
)
}))
assert.GreaterOrEqual(t, players[0].ID(), params.Cursor().ID())
for _, p := range res.Players() {
for _, p := range players {
assert.GreaterOrEqual(t, p.ServerKey(), params.Cursor().ServerKey())
}
},
@ -632,7 +632,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
) * -1
}))
assert.LessOrEqual(t, players[0].ID(), params.Cursor().ID())
for _, p := range res.Players() {
for _, p := range players {
assert.LessOrEqual(t, p.ServerKey(), params.Cursor().ServerKey())
}
},
@ -652,6 +652,8 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
assert.Len(t, res.Players(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {
t.Helper()

View File

@ -488,6 +488,7 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
assert.Len(t, res.Servers(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {

View File

@ -37,7 +37,7 @@ type playerRepository interface {
type villageRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error
List(ctx context.Context, params domain.ListVillagesParams) (domain.Villages, error)
List(ctx context.Context, params domain.ListVillagesParams) (domain.ListVillagesResult, error)
Delete(ctx context.Context, serverKey string, ids ...int) error
}

View File

@ -434,7 +434,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
res, err := repos.tribe.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Tribes()))
require.NotEmpty(t, res.Tribes())
randTribe := res.Tribes()[0]
require.NoError(t, params.SetIDs([]int{randTribe.ID()}))
@ -469,7 +469,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
res, err := repos.tribe.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Tribes()))
require.NotEmpty(t, res.Tribes())
randTribe := res.Tribes()[0]
require.NoError(t, params.SetTags([]string{randTribe.Tag()}))
@ -615,7 +615,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
)
}))
assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID())
for _, tr := range res.Tribes() {
for _, tr := range tribes {
assert.GreaterOrEqual(t, tr.ServerKey(), params.Cursor().ServerKey())
}
},
@ -657,7 +657,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
) * -1
}))
assert.LessOrEqual(t, tribes[0].ID(), params.Cursor().ID())
for _, tr := range res.Tribes() {
for _, tr := range tribes {
assert.LessOrEqual(t, tr.ServerKey(), params.Cursor().ServerKey())
}
},
@ -703,7 +703,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
}))
assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID())
assert.GreaterOrEqual(t, tribes[0].ServerKey(), params.Cursor().ServerKey())
for _, tr := range res.Tribes() {
for _, tr := range tribes {
assert.LessOrEqual(t, tr.Points(), params.Cursor().Points())
}
},
@ -749,7 +749,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
}))
assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID())
assert.GreaterOrEqual(t, tribes[0].ServerKey(), params.Cursor().ServerKey())
for _, tr := range res.Tribes() {
for _, tr := range tribes {
assert.GreaterOrEqual(t, tr.DeletedAt(), params.Cursor().DeletedAt())
}
},
@ -769,6 +769,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
assert.Len(t, res.Tribes(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {

View File

@ -175,6 +175,7 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) {
t.Helper()
assert.Len(t, res.Versions(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {

View File

@ -3,7 +3,6 @@ package adapter_test
import (
"cmp"
"context"
"fmt"
"math"
"slices"
"testing"
@ -38,8 +37,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
require.NoError(t, listParams.SetIDs(ids))
require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()}))
villages, err := repos.village.List(ctx, listParams)
res, err := repos.village.List(ctx, listParams)
require.NoError(t, err)
villages := res.Villages()
assert.Len(t, villages, len(params))
for i, p := range params {
idx := slices.IndexFunc(villages, func(village domain.Village) bool {
@ -97,17 +97,11 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
repos := newRepos(t)
villages, listVillagesErr := repos.village.List(ctx, domain.NewListVillagesParams())
require.NoError(t, listVillagesErr)
require.NotEmpty(t, villages)
randVillage := villages[0]
tests := []struct {
name string
params func(t *testing.T) domain.ListVillagesParams
assertVillages func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages)
assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListVillagesParams, total int)
name string
params func(t *testing.T) domain.ListVillagesParams
assertResult func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
@ -115,8 +109,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Helper()
return domain.NewListVillagesParams()
},
assertVillages: func(t *testing.T, _ domain.ListVillagesParams, villages domain.Villages) {
assertResult: func(t *testing.T, _ domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper()
villages := res.Villages()
assert.NotEmpty(t, len(villages))
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Or(
@ -124,15 +119,13 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.False(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListVillagesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[serverKey DESC, id DESC]",
@ -142,8 +135,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
require.NoError(t, params.SetSort([]domain.VillageSort{domain.VillageSortServerKeyDESC, domain.VillageSortIDDESC}))
return params
},
assertVillages: func(t *testing.T, _ domain.ListVillagesParams, villages domain.Villages) {
assertResult: func(t *testing.T, _ domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper()
villages := res.Villages()
assert.NotEmpty(t, len(villages))
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Or(
@ -156,26 +150,32 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListVillagesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: fmt.Sprintf("OK: ids=[%d] serverKeys=[%s]", randVillage.ID(), randVillage.ServerKey()),
name: "OK: ids serverKeys",
params: func(t *testing.T) domain.ListVillagesParams {
t.Helper()
params := domain.NewListVillagesParams()
res, err := repos.village.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, res.Villages())
randVillage := res.Villages()[0]
require.NoError(t, params.SetIDs([]int{randVillage.ID()}))
require.NoError(t, params.SetServerKeys([]string{randVillage.ServerKey()}))
return params
},
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) {
assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper()
ids := params.IDs()
serverKeys := params.ServerKeys()
villages := res.Villages()
assert.Len(t, villages, len(ids))
for _, v := range villages {
assert.True(t, slices.Contains(ids, v.ID()))
assert.True(t, slices.Contains(serverKeys, v.ServerKey()))
@ -185,58 +185,147 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListVillagesParams, total int) {
},
{
name: "OK: cursor serverKeys sort=[id ASC]",
params: func(t *testing.T) domain.ListVillagesParams {
t.Helper()
assert.NotEmpty(t, total)
params := domain.NewListVillagesParams()
res, err := repos.village.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Villages()), 2)
require.NoError(t, params.SetSort([]domain.VillageSort{domain.VillageSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.Villages()[1].ServerKey()}))
require.NoError(t, params.SetCursor(domaintest.NewVillageCursor(t, func(cfg *domaintest.VillageCursorConfig) {
cfg.ID = res.Villages()[1].ID()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper()
serverKeys := params.ServerKeys()
villages := res.Villages()
assert.NotEmpty(t, len(villages))
for _, v := range villages {
assert.GreaterOrEqual(t, v.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, v.ServerKey()))
}
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Compare(a.ID(), b.ID())
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: fmt.Sprintf("OK: idGT=%d", randVillage.ID()),
name: "OK: cursor sort=[serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListVillagesParams {
t.Helper()
params := domain.NewListVillagesParams()
require.NoError(t, params.SetIDGT(domain.NullInt{
V: randVillage.ID(),
Valid: true,
require.NoError(t, params.SetSort([]domain.VillageSort{
domain.VillageSortServerKeyASC,
domain.VillageSortIDASC,
}))
res, err := repos.village.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Villages()), 2)
require.NoError(t, params.SetCursor(domaintest.NewVillageCursor(t, func(cfg *domaintest.VillageCursorConfig) {
cfg.ID = res.Villages()[1].ID()
cfg.ServerKey = res.Villages()[1].ServerKey()
})))
return params
},
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) {
assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper()
assert.NotEmpty(t, villages)
villages := res.Villages()
assert.NotEmpty(t, len(villages))
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, villages[0].ID(), params.Cursor().ID())
for _, v := range villages {
assert.Greater(t, v.ID(), params.IDGT().V, v.ID())
assert.GreaterOrEqual(t, v.ServerKey(), params.Cursor().ServerKey())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListVillagesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: offset=1 limit=2",
name: "OK: cursor sort=[serverKey DESC, id DESC]",
params: func(t *testing.T) domain.ListVillagesParams {
t.Helper()
params := domain.NewListVillagesParams()
require.NoError(t, params.SetOffset(1))
require.NoError(t, params.SetLimit(2))
require.NoError(t, params.SetSort([]domain.VillageSort{
domain.VillageSortServerKeyDESC,
domain.VillageSortIDDESC,
}))
res, err := repos.village.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Villages()), 2)
require.NoError(t, params.SetCursor(domaintest.NewVillageCursor(t, func(cfg *domaintest.VillageCursorConfig) {
cfg.ID = res.Villages()[1].ID()
cfg.ServerKey = res.Villages()[1].ServerKey()
})))
return params
},
assertVillages: func(t *testing.T, params domain.ListVillagesParams, villages domain.Villages) {
assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper()
assert.Len(t, villages, params.Limit())
villages := res.Villages()
assert.NotEmpty(t, len(villages))
assert.True(t, slices.IsSortedFunc(villages, func(a, b domain.Village) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
assert.LessOrEqual(t, villages[0].ID(), params.Cursor().ID())
for _, v := range villages {
assert.LessOrEqual(t, v.ServerKey(), params.Cursor().ServerKey())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListVillagesParams, total int) {
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListVillagesParams {
t.Helper()
assert.NotEmpty(t, total)
params := domain.NewListVillagesParams()
require.NoError(t, params.SetLimit(2))
return params
},
assertResult: func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult) {
t.Helper()
assert.Len(t, res.Villages(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
}
@ -249,7 +338,7 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
res, err := repos.village.List(ctx, params)
tt.assertError(t, err)
tt.assertVillages(t, params, res)
tt.assertResult(t, params, res)
})
}
})
@ -276,8 +365,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
listVillagesParams := domain.NewListVillagesParams()
require.NoError(t, listVillagesParams.SetServerKeys(serverKeys))
villagesBeforeDelete, err := repos.village.List(ctx, listVillagesParams)
res, err := repos.village.List(ctx, listVillagesParams)
require.NoError(t, err)
villagesBeforeDelete := res.Villages()
var serverKey string
var ids []int
@ -296,8 +386,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
require.NoError(t, repos.village.Delete(ctx, serverKey, idsToDelete...))
villagesAfterDelete, err := repos.village.List(ctx, listVillagesParams)
res, err = repos.village.List(ctx, listVillagesParams)
require.NoError(t, err)
villagesAfterDelete := res.Villages()
assert.Len(t, villagesAfterDelete, len(villagesBeforeDelete)-len(idsToDelete))
for _, v := range villagesAfterDelete {
if v.ServerKey() == serverKey && slices.Contains(ids, v.ID()) {

View File

@ -9,7 +9,7 @@ import (
type VillageRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error
List(ctx context.Context, params domain.ListVillagesParams) (domain.Villages, error)
List(ctx context.Context, params domain.ListVillagesParams) (domain.ListVillagesResult, error)
Delete(ctx context.Context, serverKey string, ids ...int) error
}
@ -94,24 +94,28 @@ func (svc *VillageService) delete(ctx context.Context, serverKey string, village
var toDelete []int
for {
storedVillages, err := svc.repo.List(ctx, listParams)
res, err := svc.repo.List(ctx, listParams)
if err != nil {
return err
}
if len(storedVillages) == 0 {
toDelete = append(toDelete, res.Villages().Delete(serverKey, villages)...)
if res.Next().IsZero() {
break
}
toDelete = append(toDelete, storedVillages.Delete(serverKey, villages)...)
if err = listParams.SetIDGT(domain.NullInt{
V: storedVillages[len(storedVillages)-1].ID(),
Valid: true,
}); err != nil {
if err = listParams.SetCursor(res.Next()); err != nil {
return err
}
}
return svc.repo.Delete(ctx, serverKey, toDelete...)
}
func (svc *VillageService) List(
ctx context.Context,
params domain.ListVillagesParams,
) (domain.ListVillagesResult, error) {
return svc.repo.List(ctx, params)
}

View File

@ -8,6 +8,32 @@ import (
"github.com/stretchr/testify/require"
)
type VillageCursorConfig struct {
ID int
ServerKey string
}
func NewVillageCursor(tb TestingTB, opts ...func(cfg *VillageCursorConfig)) domain.VillageCursor {
tb.Helper()
cfg := &VillageCursorConfig{
ID: RandID(),
ServerKey: RandServerKey(),
}
for _, opt := range opts {
opt(cfg)
}
pc, err := domain.NewVillageCursor(
cfg.ID,
cfg.ServerKey,
)
require.NoError(tb, err)
return pc
}
type VillageConfig struct {
ID int
ServerKey string

View File

@ -412,6 +412,33 @@ const (
PlayerSortDeletedAtDESC
)
func newPlayerSortFromString(s string) (PlayerSort, error) {
allowed := []PlayerSort{
PlayerSortODScoreAttASC,
PlayerSortODScoreAttDESC,
PlayerSortODScoreDefASC,
PlayerSortODScoreDefDESC,
PlayerSortODScoreSupASC,
PlayerSortODScoreSupDESC,
PlayerSortODScoreTotalASC,
PlayerSortODScoreTotalDESC,
PlayerSortPointsASC,
PlayerSortPointsDESC,
PlayerSortDeletedAtASC,
PlayerSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
// IsInConflict returns true if two sorts can't be used together (e.g. PlayerSortIDASC and PlayerSortIDDESC).
func (s PlayerSort) IsInConflict(s2 PlayerSort) bool {
ss := []PlayerSort{s, s2}
@ -460,33 +487,6 @@ func (s PlayerSort) String() string {
}
}
func newPlayerSortFromString(s string) (PlayerSort, error) {
allowed := []PlayerSort{
PlayerSortODScoreAttASC,
PlayerSortODScoreAttDESC,
PlayerSortODScoreDefASC,
PlayerSortODScoreDefDESC,
PlayerSortODScoreSupASC,
PlayerSortODScoreSupDESC,
PlayerSortODScoreTotalASC,
PlayerSortODScoreTotalDESC,
PlayerSortPointsASC,
PlayerSortPointsDESC,
PlayerSortDeletedAtASC,
PlayerSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
type PlayerCursor struct {
id int
serverKey string

View File

@ -510,7 +510,7 @@ func TestNewPlayerCursor(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tc, err := domain.NewPlayerCursor(
pc, err := domain.NewPlayerCursor(
tt.args.id,
tt.args.serverKey,
tt.args.odScoreAtt,
@ -524,15 +524,15 @@ func TestNewPlayerCursor(t *testing.T) {
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, tc.ID())
assert.Equal(t, tt.args.serverKey, tc.ServerKey())
assert.Equal(t, tt.args.odScoreAtt, tc.ODScoreAtt())
assert.Equal(t, tt.args.odScoreDef, tc.ODScoreDef())
assert.Equal(t, tt.args.odScoreSup, tc.ODScoreSup())
assert.Equal(t, tt.args.odScoreTotal, tc.ODScoreTotal())
assert.Equal(t, tt.args.points, tc.Points())
assert.Equal(t, tt.args.deletedAt, tc.DeletedAt())
assert.NotEmpty(t, tc.Encode())
assert.Equal(t, tt.args.id, pc.ID())
assert.Equal(t, tt.args.serverKey, pc.ServerKey())
assert.Equal(t, tt.args.odScoreAtt, pc.ODScoreAtt())
assert.Equal(t, tt.args.odScoreDef, pc.ODScoreDef())
assert.Equal(t, tt.args.odScoreSup, pc.ODScoreSup())
assert.Equal(t, tt.args.odScoreTotal, pc.ODScoreTotal())
assert.Equal(t, tt.args.points, pc.Points())
assert.Equal(t, tt.args.deletedAt, pc.DeletedAt())
assert.NotEmpty(t, pc.Encode())
})
}
}

View File

@ -450,6 +450,33 @@ const (
TribeSortDeletedAtDESC
)
func newTribeSortFromString(s string) (TribeSort, error) {
allowed := []TribeSort{
TribeSortODScoreAttASC,
TribeSortODScoreAttDESC,
TribeSortODScoreDefASC,
TribeSortODScoreDefDESC,
TribeSortODScoreTotalASC,
TribeSortODScoreTotalDESC,
TribeSortPointsASC,
TribeSortPointsDESC,
TribeSortDominanceASC,
TribeSortDominanceDESC,
TribeSortDeletedAtASC,
TribeSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
// IsInConflict returns true if two sorts can't be used together (e.g. TribeSortIDASC and TribeSortIDDESC).
func (s TribeSort) IsInConflict(s2 TribeSort) bool {
ss := []TribeSort{s, s2}
@ -498,33 +525,6 @@ func (s TribeSort) String() string {
}
}
func newTribeSortFromString(s string) (TribeSort, error) {
allowed := []TribeSort{
TribeSortODScoreAttASC,
TribeSortODScoreAttDESC,
TribeSortODScoreDefASC,
TribeSortODScoreDefDESC,
TribeSortODScoreTotalASC,
TribeSortODScoreTotalDESC,
TribeSortPointsASC,
TribeSortPointsDESC,
TribeSortDominanceASC,
TribeSortDominanceDESC,
TribeSortDeletedAtASC,
TribeSortDeletedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
type TribeCursor struct {
id int
serverKey string

View File

@ -31,7 +31,7 @@ type keyValuePair struct {
value any
}
func (kvp keyValuePair) valueString() string {
func (kvp keyValuePair) valueToString() string {
switch v := kvp.value.(type) {
case string:
return v
@ -59,7 +59,7 @@ func encodeCursor(kvps []keyValuePair) string {
n := len(cursorSeparator) * (len(kvps) - 1)
for i, kvp := range kvps {
value := kvp.valueString()
value := kvp.valueToString()
n += len(kvp.key) + len(value) + len(cursorKeyValueSeparator)
values[i] = value
}

View File

@ -147,6 +147,10 @@ func (v Village) Base() BaseVillage {
}
}
func (v Village) IsZero() bool {
return v == Village{}
}
type Villages []Village
// Delete finds all villages with the given serverKey that are not in the given slice with active villages
@ -248,13 +252,93 @@ func (s VillageSort) String() string {
}
}
type VillageCursor struct {
id int
serverKey string
}
const villageCursorModelName = "VillageCursor"
func NewVillageCursor(id int, serverKey string) (VillageCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return VillageCursor{}, ValidationError{
Model: villageCursorModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return VillageCursor{}, ValidationError{
Model: villageCursorModelName,
Field: "serverKey",
Err: err,
}
}
return VillageCursor{
id: id,
serverKey: serverKey,
}, nil
}
//nolint:gocyclo
func decodeVillageCursor(encoded string) (VillageCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return VillageCursor{}, err
}
id, err := m.int("id")
if err != nil {
return VillageCursor{}, ErrInvalidCursor
}
serverKey, err := m.string("serverKey")
if err != nil {
return VillageCursor{}, ErrInvalidCursor
}
vc, err := NewVillageCursor(
id,
serverKey,
)
if err != nil {
return VillageCursor{}, ErrInvalidCursor
}
return vc, nil
}
func (vc VillageCursor) ID() int {
return vc.id
}
func (vc VillageCursor) ServerKey() string {
return vc.serverKey
}
func (vc VillageCursor) IsZero() bool {
return vc == VillageCursor{}
}
func (vc VillageCursor) Encode() string {
if vc.IsZero() {
return ""
}
return encodeCursor([]keyValuePair{
{"id", vc.id},
{"serverKey", vc.serverKey},
})
}
type ListVillagesParams struct {
ids []int
idGT NullInt
serverKeys []string
sort []VillageSort
cursor VillageCursor
limit int
offset int
}
const (
@ -293,26 +377,6 @@ func (params *ListVillagesParams) SetIDs(ids []int) error {
return nil
}
func (params *ListVillagesParams) IDGT() NullInt {
return params.idGT
}
func (params *ListVillagesParams) SetIDGT(idGT NullInt) error {
if idGT.Valid {
if err := validateIntInRange(idGT.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listVillagesParamsModelName,
Field: "idGT",
Err: err,
}
}
}
params.idGT = idGT
return nil
}
func (params *ListVillagesParams) ServerKeys() []string {
return params.serverKeys
}
@ -357,6 +421,30 @@ func (params *ListVillagesParams) SetSort(sort []VillageSort) error {
return nil
}
func (params *ListVillagesParams) Cursor() VillageCursor {
return params.cursor
}
func (params *ListVillagesParams) SetCursor(cursor VillageCursor) error {
params.cursor = cursor
return nil
}
func (params *ListVillagesParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeVillageCursor(encoded)
if err != nil {
return ValidationError{
Model: listVillagesParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListVillagesParams) Limit() int {
return params.limit
}
@ -375,20 +463,53 @@ func (params *ListVillagesParams) SetLimit(limit int) error {
return nil
}
func (params *ListVillagesParams) Offset() int {
return params.offset
type ListVillagesResult struct {
villages Villages
self VillageCursor
next VillageCursor
}
func (params *ListVillagesParams) SetOffset(offset int) error {
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listVillagesParamsModelName,
Field: "offset",
Err: err,
const listVillagesResultModelName = "ListVillagesResult"
func NewListVillagesResult(villages Villages, next Village) (ListVillagesResult, error) {
var err error
res := ListVillagesResult{
villages: villages,
}
if len(villages) > 0 {
res.self, err = NewVillageCursor(villages[0].ID(), villages[0].ServerKey())
if err != nil {
return ListVillagesResult{}, ValidationError{
Model: listVillagesResultModelName,
Field: "self",
Err: err,
}
}
}
params.offset = offset
if !next.IsZero() {
res.next, err = NewVillageCursor(next.ID(), next.ServerKey())
if err != nil {
return ListVillagesResult{}, ValidationError{
Model: listVillagesResultModelName,
Field: "next",
Err: err,
}
}
}
return nil
return res, nil
}
func (res ListVillagesResult) Villages() Villages {
return res.villages
}
func (res ListVillagesResult) Self() VillageCursor {
return res.self
}
func (res ListVillagesResult) Next() VillageCursor {
return res.next
}

View File

@ -8,6 +8,7 @@ import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -146,6 +147,82 @@ func TestVillageSort_IsInConflict(t *testing.T) {
}
}
func TestNewVillageCursor(t *testing.T) {
t.Parallel()
validVillageCursor := domaintest.NewVillageCursor(t)
type args struct {
id int
serverKey string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
id: validVillageCursor.ID(),
serverKey: validVillageCursor.ServerKey(),
},
expectedErr: nil,
},
{
name: "ERR: id < 1",
args: args{
id: 0,
serverKey: validVillageCursor.ServerKey(),
},
expectedErr: domain.ValidationError{
Model: "VillageCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
id: validVillageCursor.ID(),
serverKey: serverKeyTest.key,
},
expectedErr: domain.ValidationError{
Model: "VillageCursor",
Field: "serverKey",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
vc, err := domain.NewVillageCursor(
tt.args.id,
tt.args.serverKey,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, vc.ID())
assert.Equal(t, tt.args.serverKey, vc.ServerKey())
assert.NotEmpty(t, vc.Encode())
})
}
}
func TestListVillagesParams_SetIDs(t *testing.T) {
t.Parallel()
@ -206,61 +283,6 @@ func TestListVillagesParams_SetIDs(t *testing.T) {
}
}
func TestListVillagesParams_SetIDGT(t *testing.T) {
t.Parallel()
type args struct {
idGT domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
idGT: domain.NullInt{
V: domaintest.RandID(),
Valid: true,
},
},
},
{
name: "ERR: value < 0",
args: args{
idGT: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
Field: "idGT",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListVillagesParams()
require.ErrorIs(t, params.SetIDGT(tt.args.idGT), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.idGT, params.IDGT())
})
}
}
func TestListVillagesParams_SetServerKeys(t *testing.T) {
t.Parallel()
@ -403,6 +425,87 @@ func TestListVillagesParams_SetSort(t *testing.T) {
}
}
func TestListVillagesParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewVillageCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.VillageCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 0,
},
},
},
{
name: "ERR: len(cursor) > 1000",
args: args{
cursor: gofakeit.LetterN(1001),
},
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListVillagesParams()
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedCursor, params.Cursor())
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
})
}
}
func TestListVillagesParams_SetLimit(t *testing.T) {
t.Parallel()
@ -466,51 +569,46 @@ func TestListVillagesParams_SetLimit(t *testing.T) {
}
}
func TestListVillagesParams_SetOffset(t *testing.T) {
func TestNewListVillagesResult(t *testing.T) {
t.Parallel()
type args struct {
offset int
villages := domain.Villages{
domaintest.NewVillage(t),
domaintest.NewVillage(t),
domaintest.NewVillage(t),
}
next := domaintest.NewVillage(t)
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
offset: 100,
},
},
{
name: "ERR: offset < 0",
args: args{
offset: -1,
},
expectedErr: domain.ValidationError{
Model: "ListVillagesParams",
Field: "offset",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
t.Run("OK: with next", func(t *testing.T) {
t.Parallel()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
res, err := domain.NewListVillagesResult(villages, next)
require.NoError(t, err)
assert.Equal(t, villages, res.Villages())
assert.Equal(t, villages[0].ID(), res.Self().ID())
assert.Equal(t, villages[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, next.ID(), res.Next().ID())
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
})
params := domain.NewListVillagesParams()
t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.offset, params.Offset())
})
}
res, err := domain.NewListVillagesResult(villages, domain.Village{})
require.NoError(t, err)
assert.Equal(t, villages, res.Villages())
assert.Equal(t, villages[0].ID(), res.Self().ID())
assert.Equal(t, villages[0].ServerKey(), res.Self().ServerKey())
assert.True(t, res.Next().IsZero())
})
t.Run("OK: 0 villages", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListVillagesResult(nil, domain.Village{})
require.NoError(t, err)
assert.Zero(t, res.Villages())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}

View File

@ -421,19 +421,16 @@ func TestDataSync(t *testing.T) {
allVillages := make(domain.Villages, 0, len(expectedVillages))
for {
villages, err := villageRepo.List(ctx, listParams)
res, err := villageRepo.List(ctx, listParams)
require.NoError(collect, err)
if len(villages) == 0 {
allVillages = append(allVillages, res.Villages()...)
if res.Next().IsZero() {
break
}
allVillages = append(allVillages, villages...)
require.NoError(collect, listParams.SetIDGT(domain.NullInt{
V: villages[len(villages)-1].ID(),
Valid: true,
}))
require.NoError(collect, listParams.SetCursor(res.Next()))
}
if !assert.Len(collect, allVillages, len(expectedVillages)) {